- supports grouping by day, week, and month
- new event-driven UI easier to maintain
- uses impala styles for extra pretty!

TODO
- rehabilitate csv table
- polish style for side notes
- bring back field selection menu
- bring back overview page
This commit is contained in:
Matt Claypotch 2011-10-10 15:22:41 -07:00
Родитель 6f06067d21
Коммит f717424d79
15 изменённых файлов: 821 добавлений и 571 удалений

Просмотреть файл

@ -1,6 +1,5 @@
{% set show_contributions = false %}
<div class="secondary-item-list report-menu">
<h3>{{ _('Add-on Statistics') }}</h3>
<nav id='side-nav'>
<ul>
{% for item in report_tree %}
{% if item.name != 'contributions' or show_contributions %}
@ -10,21 +9,21 @@
<li>
{% endif %}
<a href="{{ base_url + item.url }}">{{ item.title }}</a>
{% if item.children %}
<ul>
{% for child in item.children %}
{% if child.name == report %}
<li class="selected">
{% else %}
<li>
{% endif %}
<a href="{{ base_url + child.url }}">{{ child.title }}</a>
</li>
{% endfor %}
</ul>
{% endif %}
</li>
{% endif %}
</li>
{% endif %}
{% if item.children %}
<ul>
{% for child in item.children %}
{% if child.name == report %}
<li class="selected">
{% else %}
<li>
{% endif %}
<a href="{{ base_url + child.url }}">{{ child.title }}</a>
</li>
{% endfor %}
</ul>
{% endif %}
{% endfor %}
</ul>
</div>
</nav>

Просмотреть файл

@ -37,7 +37,7 @@
{% block stats_note %}
<aside class="highlight">
{% trans slug=addon.slug, id=addon.id %}
<h3>Tracking external sources</h3>
<h2>Tracking external sources</h2>
<p>
If you link to your add-on's details page or directly to its file from an
external site, such as your blog or website, you can append a parameter to be

Просмотреть файл

@ -1,9 +1,11 @@
{% extends "base.html" %}
{% extends "impala/base.html" %}
{% set range = view.range %}
{% block bodyclass %}statistics{% endblock %}
{% block extrahead %}
<link rel="stylesheet" href="{{ media('css/legacy/stats.css') }}"/>
{{ css('zamboni/stats') }}
<link rel="stylesheet"
href="{{ media('css/zamboni/jquery-ui/custom-1.7.2.css') }}">
{% endblock %}
@ -14,9 +16,9 @@
{% endblock %}
{% block navbar %}
{{ breadcrumbs([(addon.type_url(), amo.ADDON_TYPES[addon.type]),
(addon.get_url_path(), addon.name),
(link, _('Statistics'))]) }}
{{ impala_breadcrumbs([(addon.type_url(), amo.ADDON_TYPES[addon.type]),
(addon.get_url_path(), addon.name),
(link, _('Statistics'))]) }}
{# TODO: Replace above line with this --> once we serve the extension home. { breadcrumbs([(addon.type.get_url_path(), amo.ADDON_TYPES[addon.type_id])]) } #}
@ -41,18 +43,14 @@
<a href="#">{{ _('Custom') }}</a></li>
</ul>
</div>
<hgroup>
<h2 class="addon"{{ addon.name|locale_html }}>
<img src="{{ addon.icon_url }}" class="icon"/>
<span>
{# L10n: {0} is an add-on name #}
{{ _('Statistics for {0}')|f(addon.name) }}
</span>
</h2>
{# L10n: {0} is an add-on author #}
<h4 class="author">{{ _('by {0}')|f(users_list(addon.listed_authors))|xssafe }}</h4>
</hgroup>
<header>
<hgroup>
<h1 class="addon"{{ addon.name|locale_html }}>
{# L10n: {0} is an add-on name #}
{{ _('Statistics for {0}')|f(addon.name) }}
</h1>
</hgroup>
</header>
{% endblock %}
{% block content %}
@ -92,19 +90,8 @@
</form>
</div>
{% endif %}
<div class="featured">
<div class="featured-inner chart">
<div class="listing-header">
{% block chart_menu %}
<ul><li class="selected">&nbsp;</li></ul>
{% endblock %}
</div>
<div class="featured-body" id="head-chart" style="background:#fff;height:256px"
{% block chart_config %}
data-series="{{ series_fields }}"
{% endblock %}
>
</div>
<div class="island chart c">
<div id="head-chart" style="background:#fff;height:384px">
</div>
</div>
{% block stats %}

Просмотреть файл

@ -3,6 +3,7 @@
.amo-header {
font-family: @head-sans;
margin-bottom: 35px;
position: relative;
}
#masthead {

Просмотреть файл

@ -143,6 +143,15 @@ header + .island {
line-height: 16px;
margin-bottom: 28px;
}
ul ul {
margin-bottom: 0;
li {
border-top: 0;
a {
padding-left: 1em;
}
}
}
li {
border: 1px solid @border-black;
border-width: 0 0 1px 0;
@ -240,7 +249,8 @@ header + .island {
.s-users #side-nav .s-users a,
.s-downloads #side-nav .s-downloads a,
.s-rating #side-nav .s-rating a,
.s-created #side-nav .s-created a {
.s-created #side-nav .s-created a,
#side-nav .selected {
background: #ecf5fe;
color: #333;
font-weight: bold;
@ -249,7 +259,8 @@ header + .island {
.s-users #side-nav .s-users a:after,
.s-downloads #side-nav .s-downloads a:after,
.s-rating #side-nav .s-rating a:after,
.s-created #side-nav .s-created a:after {
.s-created #side-nav .s-created a:after,
#side-nav .selected a:after {
color: inherit;
}

Просмотреть файл

@ -126,6 +126,15 @@ div.statbox {
}
}
.statistics #page {
max-width: 1280px;
width: auto;
min-width: 960px;
padding-bottom: 500px;
padding-left: 20px;
padding-right: 20px;
}
.criteria li.divider:before {
font-weight: normal;
font-size: 160%;
@ -469,6 +478,7 @@ table.stats-aggregate tbody span.change.minus {
}
#head-chart {
border-radius: 1em;
overflow: hidden;
}
.loadmessage {

Просмотреть файл

@ -1,515 +0,0 @@
/* Clearfix! */
.statbox:after {
content: ".";
display: block;
clear: both;
height: 0;
visibility: hidden;
}
.statbox .pagination {
display: block;
width: 98%;
margin: 0;
padding: .5em 1%;
border-top: 1px solid #A5BFCE;
-moz-border-radius: 0 0 3px 3px;
background: -moz-linear-gradient(#DAF0F6, #FDFEFE) repeat scroll 0 0 transparent
}
/**
* Undo default table style
**/
table, tbody, thead, th, tr, td,
thead tr th, tbody tr td,
thead th, tbody tr {
border: 0;
}
thead tr th, tbody tr td {
border-top: 0;
}
table {
margin-bottom: 0;
}
.tabular {
margin: 1em;
overflow-x: auto;
}
.csv-table td {
min-width: 100px;
white-space:nowrap;
}
/**
* common styles
**/
.statbox .listing-header {
-moz-border-radius:0 0 4px 4px;
background-color:#F0F8FC;
background-image: -moz-linear-gradient(#daf0f6,#fdfefe);
border:0;
border-top:1px solid #A5BFCE;
line-height:2.5em;
overflow:hidden;
padding:0.1em 0 0.1em 0.25em;
}
table tbody tr {
border-top: 1px dotted #B5D9E5;
}
.featured-inner {
background: -moz-linear-gradient(#ffffff, #eff8fb);
}
div.statbox {
border: 1px solid #C9E8F3;
-moz-border-radius: 5px;
-webkit-border-radius: 5px;
border-radius: 5px;
background: #fff;
}
.secondary > aside,
.secondary > div {
margin-bottom: 2em;
}
/**
* Rules for date criteria selection
**/
.criteria {
background: -moz-linear-gradient(#fdfefe,#daf0f6);
border: .1em solid #abc4d2;
-moz-border-radius: 4px;
}
.criteria ul {
line-height: 2.5em;
}
.criteria.custom form {
line-height: 2.5em;
margin: .3em 1em;
}
.criteria.custom form input {
z-index: 10;
position: relative;
}
.criteria.custom {
position: relative;
margin-bottom: 1em;
display: none;
}
.criteria.range {
float: right;
position: relative;
top: -2px;
z-index: 1000;
}
.criteria li {
float: left;
font-weight: bold;
}
.criteria li:first-child {
padding-left: 1.4em;
}
.criteria li.divider:before {
font-weight: normal;
font-size: 160%;
color: #b8cdd9;
content: "|";
}
.criteria li a {
-moz-border-radius:3px 3px 3px 3px;
border:1px solid transparent;
color:#003595;
font-weight:bold;
padding:0.3em 0.8em;
text-decoration:none;
white-space:nowrap;
}
.criteria li a:active,
.criteria li a:active,
.criteria li.selected a {
-moz-border-radius:3px 3px 3px 3px;
color:#fff;
background:#003595;
}
/* creates the inner triangle */
.criteria.custom:before {
font-weight: normal;
content: "\00a0";
display: block; /* reduce the damage in FF3.0 */
position: absolute;
width: 0;
height: 0;
top: -38px; /* value = - border-top-width - border-bottom-width */
right: 25px; /* controls horizontal position */
border: 20px solid transparent;
border-bottom-color: #abc4d2;
border-width: 19px 14px; /* vary these values to change the angle of the vertex */
border-style: solid;
}
/* creates the outer triangle */
.criteria.custom:after {
content: "\00a0";
display: block; /* reduce the damage in FF3.0 */
position: absolute;
width: 0;
height: 0;
top: -37px; /* value = - border-top-width - border-bottom-width */
right: 24px; /* value = (:before right) + (:before border-right) - (:after border-right) */
border: 13px solid transparent;
border-bottom-color: #fdfdfe;
border-width: 20px 15px; /* vary these values to change the angle of the vertex */
border-style: solid;
}
/* @end */
/**
* Three-up stats
**/
.two-up > div {
float: left;
width: 50%;
margin: 10px 0;
text-align:center;
}
.two-up > div > div {
border-left: 1px dotted #aaa;
}
.two-up div a {
font-weight: bold;
font-size: 140%;
}
.two-up div small {
font-size: 110%;
}
.listing-header ul.chart_legend {
float: right;
}
.listing-header ul.chart_legend li {
margin: 0 .5em;
line-height: 2.5em;
display: block;
float: right;
}
/**
* Toplists
**/
.seriesdot {
display: block;
float: left;
position: relative;
top: .7em;
margin-right: .5em;
-moz-border-radius: 4px;
height: 8px;
width: 8px;
}
.listing-header .seriesdot {
border: 2px solid #fff;
-moz-border-radius: 6px;
margin: 0 .2em 0 .5em;
top: .8em;
}
.toplists {
margin-bottom: 2em;
overflow: hidden;
}
.toplist {
float: left;
width: 31%;
padding: 0;
margin-left: 3.5%;
}
.toplist:first-child {
margin-left: 0;
}
.toplist .statbox {
padding: 1em;
margin: 0;
}
.toplist h3 {
margin: 0 0 .2em 0;
}
#toplist1 {
width: 210px;
margin: .5em auto 0 auto;
}
.toplist tr:first-child {
border-top: 0;
}
.toplist table {
width: 100%;
height: 160px;
margin: .7em 0;
}
.toplist td {
text-align: right;
padding: 0;
line-height: 2em;
}
.toplist td:last-child {
text-align: left;
font-size: 90%;
width:40px;
padding-left: .3em;
}
.toplist td:first-child {
text-align: left;
}
/**
* Rules for Report Menu
**/
.report-menu nav {
display: block;
}
.report-menu h3 {
margin-top: 0;
}
.report-menu ul li {
font-size: 110%;
}
.report-menu ul li ul li {
font-size: 90%;
}
.report-menu ul li ul li a {
margin: .4em 0;
}
.report-menu ul li a {
margin: .4em 0;
}
.report-menu ul ul {
margin-left: 25px;
}
.report-menu ul li.selected > a {
position: relative;
color: #333;
}
.report-menu ul li.selected > a:hover {
text-decoration: none;
}
/* Makes the triangles for selected reports */
.report-menu ul li.selected > a:before {
content: "\00a0";
display: block; /* reduce the damage in FF3.0 */
position: absolute;
width: 0;
height: 0;
top: 20%; /* value = - border-top-width - border-bottom-width */
left: 10px; /* value = (:before right) + (:before border-right) - (:after border-right) */
border: 5px solid transparent;
border-left-color: #333;
border-style: solid;
}
.report-menu ul li ul li.selected > a:before {
border-width: 4px;
}
/* @end */
/**
* bar-chart tables
**/
.csv-table tbody {
display: none;
}
.csv-table tbody.selected {
display: table-row-group;
}
table.csv-table {
width: 100%;
}
table.csv-table td,
table.csv-table th {
text-align: right;
}
table.csv-table td.bar div {
text-align: right;
margin: 8px 0;
line-height: 100%;
height: 100%;
}
table.csv-table td.bar,
table.csv-table th.bar {
border-left: 1px solid #8db9c7;
padding: 0;
}
table.csv-table td:first-child,
table.csv-table th:first-child {
text-align: left;
}
div.piechart {
height: 200px;
}
div.piechart .highcharts-container {
margin: 8px auto 0 auto;
}
/**
* sidestats
**/
aside.highlight {
display: block;
}
.highlight dd {
font-size: .8em;
}
#export_data {
font: 13px "helvetica neue",arial,helvetica,sans-serif;
margin-left:1em;
}
/**
* Big table
**/
table.stats-aggregate {
width: 100%;
margin-top: .5em;
}
table.stats-aggregate thead th {
text-align: right;
padding-right: 72px;
line-height: 90%;
}
table.stats-aggregate thead th:first-child {
text-align: left;
padding-right: inherit;
}
table.stats-aggregate tbody td {
line-height: 120%;
font-size: 140%;
}
table.stats-aggregate tbody td.value {
padding: 5px 0 3 0px;
text-align: right;
}
table.stats-aggregate tbody td.label {
padding-left: 2px;
}
table.stats-aggregate tbody span.change {
display: block;
float: right;
width: 54px;
padding-left: .5em;
text-align: left;
font-size: 80%;
padding-top: 2px;
}
table.stats-aggregate tbody span.change.plus {
color: #00774d;
}
table.stats-aggregate tbody span.change.minus {
color: #850000;
}
.loading, .loaded {
position:relative;
}
.loading:after {
-moz-transition: opacity .5s;
content: "\00a0";
display: block;
position: absolute;
background: #040204 url("../../img/zamboni/loading.gif") no-repeat center center;;
opacity: .4;
width: 100%;
height: 100%;
z-index: 1000;
top: 0;
}
.loaded:after {
opacity: 0;
pointer-events: none;
}
.loadmessage {
position: fixed;
left: 0;
right: 0;
top: -2.5em;
height: 2em;
margin: 0 auto;
text-align: center;
pointer-events: none;
-moz-transition: top .5s;
-webkit-transition: top .5s;
transition: top .5s;
z-index:9000;
}
.loadmessage span {
background: url("../../img/zamboni/loading-small.gif") no-repeat 1em center;
background-color: #000;
padding: .5em 1em;
padding-left: 36px;
line-height: 2em;
color: #fff;
opacity: .75;
-moz-border-radius: 0 0 .75em .75em;
-webkit-border-radius: 0 0 .75em .75em;
border-radius: 0 0 .75em .75em;
}
.loadmessage.on {
top: 0;
}
.loadmessage.off {
top: -2.5em;
}
/* Field Menu */
#fieldMenu, #fieldList {
margin-bottom: 0;
}
#fieldList label {
display: block;
white-space: nowrap;
padding-right: 2em;
}
#fieldList label:hover {
background: #ccf;
}
#fieldMenu button {
width: 100%;
}
#fieldList {
max-height: 300px;
min-width: 160px;
overflow-y: auto;
}

Просмотреть файл

@ -0,0 +1,176 @@
(function () {
var $win = $(window),
baseConfig = {
chart: {
renderTo: 'head-chart',
zoomType: 'x',
},
credits: { enabled: false },
title: {
text: null
},
xAxis: {
type: 'datetime',
maxZoom: 7 * 24 * 3600000, // seven days
title: {
text: null
}
},
yAxis: {
title: {
text: null
},
labels: {
formatter: function() {
return Highcharts.numberFormat(this.value, 0);
}
},
min: 0,
startOnTick: false,
showFirstLabel: false
},
legend: {
enabled: true
},
tooltip: { },
plotOptions: {
line: {
lineWidth: 1,
animation: false,
shadow: false,
marker: {
enabled: false,
states: {
hover: {
enabled: true,
radius: 5
}
}
},
states: {
hover: {
lineWidth: 2
}
}
}
}
};
var chart;
// which unit do we use for a given metric?
var metricTypes = {
"usage" : "users",
"apps" : "users",
"locales" : "users",
"os" : "users",
"versions" : "users",
"statuses" : "users",
"downloads" : "downloads",
"sources" : "downloads"
};
$win.bind("dataready", function(e, obj) {
var view = obj.view,
metric = view.metric,
group = view.group,
range = z.date.normalizeRange(view.range),
start = range.start,
end = range.end,
fields = obj.fields ? obj.fields.slice(0,5) : ['count'],
data = obj.data,
series = {},
t, row, i, field, val;
// Initialize the empty series object.
_.each(fields, function(f) { series[f] = []; });
// Transmute the data into something Highcharts understands.
if (group == 'month') {
_.each(data, function(row, t) {
for (i = 0; i < fields.length; i++) {
field = fields[i];
val = parseFloat(z.StatsManager.getField(row, field));
if (val != val) val = null;
series[field].push({
'x' : parseInt(t, 10),
'y' : val
});
}
});
} else {
var step = z.date.millis('1 day');
if (group == 'week') {
step = z.date.millis('7 days');
while((new Date(start)).getDay() > 0) {
start += z.date.millis('1 day');
}
}
for (t = start; t < end; t += step) {
row = data[t];
for (i = 0; i < fields.length; i++) {
field = fields[i];
val = parseFloat(z.StatsManager.getField(row, field));
if (val != val) val = null;
series[field].push({
'x' : t,
'y' : val
});
}
}
}
// Populate the chart config object.
var chartData = [], id
for (i = 0; i < fields.length; i++) {
field = fields[i];
id = field.split("|").slice(-1)[0];
chartData.push({
'type' : 'line',
'name' : z.StatsManager.getPrettyName(view.metric, id),
'id' : id,
'data' : series[field]
});
}
// Generate the tooltip function for this chart.
// both x and y axis can be displayed differently.
var tooltipFormatter = (function(){
var xFormatter,
yFormatter;
function dayFormatter(d) { return Highcharts.dateFormat('%a, %b %e, %Y', new Date(d)); }
function weekFormatter(d) { return "Week of " + Highcharts.dateFormat('%b %e, %Y', new Date(d)); }
function monthFormatter(d) { return Highcharts.dateFormat('%B %Y', new Date(d)); }
function downloadFormatter(n) { return Highcharts.numberFormat(n, 0) + ' downloads'; }
function userFormatter(n) { return Highcharts.numberFormat(n, 0) + ' users'; }
if (group == "week") {
xFormatter = weekFormatter;
} else if (group == "month") {
xFormatter = monthFormatter;
} else {
xFormatter = dayFormatter;
}
if (metricTypes[metric] == "users") {
yFormatter = userFormatter;
} else {
yFormatter = downloadFormatter;
}
return function() {
return "<b>" + this.series.name + "</b><br>" +
xFormatter(this.x) + "<br>" +
yFormatter(this.y);
}
})();
// Set up the new chart's configuration.
var newConfig = $.extend(baseConfig, { series: chartData });
if (fields.length == 1) {
newConfig.legend.enabled = false;
newConfig.chart.margin = [50, 50, 50, 80]
}
newConfig.tooltip.formatter = tooltipFormatter;
if (chart) chart.destroy();
chart = new Highcharts.Chart(newConfig);
});
})();

75
media/js/impala/stats/controls.js поставляемый Normal file
Просмотреть файл

@ -0,0 +1,75 @@
(function (){
"use strict";
var $rangeSelector = $(".criteria.range ul"),
$customRangeForm = $("div.custom.criteria");
$.datepicker.setDefaults({showAnim: ''});
$("#date-range-start").datepicker();
$("#date-range-end").datepicker();
$rangeSelector.click(function(e) {
var $target = $(e.target).parent();
var newRange = $target.attr("data-range");
if (newRange) {
$rangeSelector.children("li.selected").removeClass("selected");
$target.addClass("selected");
if (newRange == "custom") {
$customRangeForm.removeClass("hidden").slideDown('fast');
} else {
$target.trigger('changeview', {range: newRange});
$customRangeForm.slideUp('fast');
}
}
e.preventDefault();
});
$(window).bind('changeview', function(e, newState) {
function populateCustomRange() {
var nRange = z.date.normalizeRange(newState.range);
$("#date-range-start").val(
z.date.datepicker_format(
new Date(nRange.start)
)
);
$("#date-range-end").val(
z.date.datepicker_format(
new Date(nRange.end)
)
);
$rangeSelector.children("li.selected").removeClass("selected");
$('[data-range="custom"]').addClass("selected");
$customRangeForm.removeClass("hidden").slideDown('fast');
}
if (newState && newState.range) {
if (!newState.range.custom) {
var newRange = newState.range,
$rangeEl = $('[data-range="' + newRange + '"]');
if ($rangeEl.length) {
$rangeSelector.children("li.selected").removeClass("selected");
$rangeEl.addClass("selected");
return;
} else {
populateCustomRange();
}
} else {
populateCustomRange();
}
}
});
$("#date-range-form").submit(function(e) {
e.preventDefault();
var start = new Date($("#date-range-start").val()),
end = new Date($("#date-range-end").val()),
newRange = {
custom: true,
start: z.date.date(start),
end: z.date.date(end)
};
$rangeSelector.trigger('changeview', {range: newRange});
return false;
});
})();

Просмотреть файл

Просмотреть файл

@ -0,0 +1,78 @@
// date management helpers
z.date = (function() {
var _millis = {
"day" : 1000 * 60 * 60 * 24,
"week" : 1000 * 60 * 60 * 24 * 7
};
// Returns the number of milliseconds for a given duration.
// millis("1 day")
// > 86400000
// millis("2 days")
// > 172800000
function millis(str) {
var tokens = str.split(/\s+/);
n = parseInt(tokens[0]);
if (!tokens[1]) throw "Invalid duration string";
unit = tokens[1].replace(/s$/,'').toLowerCase();
if (!_millis[ unit ]) throw "Invalid time unit";
return n * _millis[ unit ];
};
// pads a number with a preceding zero.
// pad2(2)
// > "02"
// pad2(20)
// > "20"
function pad2(n) {
var str = n.toString();
return ('0' + str).substr(-2);
};
// Takes a date object and converts it to a time-less
// representation of today's date.
function date(d) {
return Date.parse(date_string(d, '-'));
};
function date_string(d, del) {
del = del || '-';
return [d.getFullYear(), pad2(d.getMonth()+1), pad2(d.getDate())].join(del);
};
function datepicker_format(d) {
return [pad2(d.getMonth()+1), pad2(d.getDate()), d.getFullYear()].join('/');
};
// Truncates the current time off today's date.
function today() {
var d = new Date();
return date(d);
};
// returns a millisecond timestamp for a specified duration in the past.
function ago(str, times) {
times = (times !== undefined) ? times : 1;
return today() - millis(str) * times;
};
// takes a range object and normalizes it to have a `start` and `end` property.
function normalizeRange(range) {
var ret = {};
if (typeof range == "string") {
ret.start = ago(range);
ret.end = today();
} else if (typeof range == "object") {
ret.start = range.start;
ret.end = range.end;
} else {
throw "Invalid range values found."
}
return ret;
}
return {
'ago': ago, 'date': date, 'date_string': date_string,
'datepicker_format': datepicker_format, 'millis': millis, 'pad2': pad2,
'today': today, 'normalizeRange': normalizeRange
};
})();

Просмотреть файл

@ -0,0 +1,399 @@
function dbg() {
if(window.console && (typeof window.console.log == 'function')) {
window.console.log(Array.prototype.slice.apply(arguments));
}
}
z.hasPushState = (typeof history.replaceState === "function");
z.StatsManager = (function() {
"use strict";
// The version of the stats localStorage we are using.
// If you increment this number, you cache-bust everyone!
var STATS_VERSION = 21;
var storage = z.Storage("stats"),
storageCache = z.Storage("statscache"),
dataStore = {},
currentView = {},
addonId = parseInt($(".primary").attr("data-addon_id"), 10),
baseURL = $(".primary").attr("data-base_url"),
pendingFetches = 0,
writeInterval = false;
// It's a bummer, but we need to know which metrics have breakdown fields.
// check by saying `if (metric in breakdownMetrics)`
var breakdownMetrics = {
"apps": true,
"locales": true,
"os": true,
"sources": true,
"versions": true,
"statuses": true
};
// is a metric an average or a sum?
var metricTypes = {
"usage" : "mean",
"apps" : "mean",
"locales" : "mean",
"os" : "mean",
"versions" : "mean",
"statuses" : "mean",
"downloads" : "sum",
"sources" : "sum"
};
// Initialize from localStorage when dom is ready.
$(function() {
dbg("looking for local data");
if (verifyLocalStorage()) {
var cacheObject = storageCache.get(addonId);
if (cacheObject) {
dbg("found local data, loading...");
cacheObject = JSON.parse(cacheObject);
if (cacheObject) {
dataStore = cacheObject;
}
}
}
});
// These functions deal with our localStorage cache.
function writeLocalStorage() {
dbg("saving local data");
storageCache.set(addonId, JSON.stringify(dataStore));
storage.set("version", STATS_VERSION);
dbg("saved local data");
}
function clearLocalStorage() {
storageCache.remove(addonId);
dbg("cleared local data");
}
function verifyLocalStorage() {
if (storage.get("version") == STATS_VERSION) {
return true;
} else {
dbg("wrong offline data verion");
return false;
}
}
document.onbeforeunload = writeLocalStorage;
// Runs when 'changeview' event is detected.
function processView(e, newView) {
// Update our internal view state.
currentView = $.extend(currentView, newView);
// Fetch the data from the server or storage, and notify other components.
getDataRange(currentView, function(data) {
$(window).trigger("dataready", {
'view': currentView,
'fields': getAvailableFields(currentView),
'data': data
});
});
}
$(window).bind('changeview', processView);
// Returns a list of field names for a given data set.
function getAvailableFields(view) {
var metric = view.metric,
range = z.date.normalizeRange(view.range),
start = range.start,
end = range.end,
ds,
row,
numRows = 0,
step = z.date.millis("1 day"),
fields = {};
// Non-breakdwon metrics only have one field.
if (!(metric in breakdownMetrics)) return false;
ds = dataStore[metric];
if (!ds) throw "Expected metric with valid data!";
// Locate all unique fields.
for (var i=start; i<end; i+= step) {
if (ds[i]) {
row = (metric == 'apps') ? collapseVersions(ds[i], 1) : ds[i];
_.each(row.data, function(v, k) {
fields[k] = fields[k] ? fields[k] + v : v;
});
_.extend(fields, row.data);
}
}
// sort the fields, make them proper field identifiers, and return.
return _.map(
_.sortBy(
_.keys(fields),
function (f) {
return -fields[f];
}
),
function(f) {
return "data|" + f;
}
);
}
// getDataRange: ensures we have all the data from the server we need,
// and queues up requests to the server if the requested data is outside
// the range currently stored locally. Once all server requests return,
// we move on.
function getDataRange(view, callback) {
dbg("enter getDataRange");
var range = z.date.normalizeRange(view.range),
metric = view.metric,
ds,
needed = 0;
function finished() {
needed--;
dbg(pendingFetches, " fetches pending");
if (needed < 1) {
var ret = {}, i, row,
step = z.date.millis("1 day");
ds = dataStore[metric];
for (var i=range.start; i<range.end; i+= step) {
if (ds[i]) {
ret[i] = (metric == 'apps') ? collapseVersions(ds[i], 1) : ds[i];
}
}
ret = groupData(ret, view);
callback.call(this, ret);
}
}
if (dataStore[metric]) {
ds = dataStore[metric];
dbg("range", range.start, range.end)
if (ds.maxdate < range.end) {
needed++;
fetchData(metric, ds.maxdate, range.end, finished);
}
if (ds.mindate > range.start) {
needed++;
fetchData(metric, range.start, ds.mindate, finished);
}
if (ds.mindate <= range.start && ds.maxdate >= range.end) {
dbg("all data found locally");
finished();
}
} else {
dbg("metric not found");
needed++;
fetchData(metric, range.start, range.end, finished);
}
}
// Aggregate data based on our view's `group` setting.
function groupData(data, view) {
var metric = view.metric,
range = z.date.normalizeRange(view.range),
group = view.group || 'day',
groupedData = {};
// if grouping is by day, do nothing.
if (group == 'day') return data;
var groupKey = false,
groupVal = false,
groupCount = 0,
d, row;
// big loop!
for (var i=range.start; i<range.end; i+= z.date.millis('1 day')) {
d = new Date(i);
row = data[i];
// Here's where grouping points are caluculated.
if ((group == 'week' && d.getDay() == 0) || (group == 'month' && d.getDate() == 1)) {
// we drop the some days of data from the result set
// if they are not a complete grouping.
if (groupKey && groupVal) {
// average `count` for mean metrics
if (metricTypes[metric] == 'mean') {
groupVal['count'] /= groupCount;
}
if (metric in breakdownMetrics) {
_.each(groupVal.data, function(val, field) {
// average for mean metrics.
if (metricTypes[metric] == 'mean') {
groupVal.data[field] /= groupCount;
}
});
}
groupedData[groupKey] = groupVal;
}
// set the new group date to the current iteration.
groupKey = i;
// reset our aggregates.
groupCount = 0;
groupVal = {
date: z.date.date_string(new Date(groupKey), '-'),
count: 0,
data: {}
};
}
// add the current row to our aggregates.
if (row && groupVal) {
groupVal.count += row.count;
if (metric in breakdownMetrics) {
_.each(row.data, function(val, field) {
if (!groupVal.data[field]) {
groupVal.data[field] = 0;
}
groupVal.data[field] += val;
});
}
}
groupCount++;
}
return groupedData;
}
// The beef. Negotiates with the server for data.
function fetchData(metric, start, end, callback) {
var seriesStart = start;
var seriesEnd = end;
pendingFetches++;
var seriesURLStart = Highcharts.dateFormat('%Y%m%d', seriesStart),
seriesURLEnd = Highcharts.dateFormat('%Y%m%d', seriesEnd),
seriesURL = baseURL + ([metric,'day',seriesURLStart,seriesURLEnd]).join('-') + '.json';
dbg("GET", seriesURLStart, seriesURLEnd);
$.ajax({ url: seriesURL,
dataType: 'text',
success: fetchHandler});
function fetchHandler(raw_data, status, xhr) {
var maxdate = 0,
mindate = z.date.today();
if (xhr.status == 200) {
if (!dataStore[metric]) {
dataStore[metric] = {};
dataStore[metric].mindate = z.date.today();
dataStore[metric].maxdate = 0;
}
var ds = dataStore[metric], data;
data = JSON.parse(raw_data);
var i, datekey;
for (i=0; i<data.length; i++) {
datekey = parseInt(Date.parse(data[i].date), 10);
maxdate = Math.max(datekey, maxdate);
mindate = Math.min(datekey, mindate);
ds[datekey] = data[i];
}
ds.maxdate = Math.max(parseInt(maxdate, 10), parseInt(ds.maxdate, 10));
ds.mindate = Math.min(parseInt(mindate, 10), parseInt(ds.mindate, 10));
pendingFetches--;
callback.call(this, true);
clearTimeout(writeInterval);
writeInterval = setTimeout(writeLocalStorage, 1000);
} else if (xhr.status == 202) { //Handle a successful fetch but with no reponse
var retry_delay = 30000;
if (xhr.getResponseHeader("Retry-After")) {
retry_delay = parseInt(xhr.getResponseHeader("Retry-After"), 10) * 1000;
}
setTimeout(function () {
AMO.fetchData(metric, start, end, callback);
}, retry_delay);
}
}
}
// Rounds application version strings to a given precision.
// Passing `0` will truncate versions entirely.
function collapseVersions(row, precision) {
var out = {
count : row.count,
date : row.date,
end : row.end
},
set,
ver,
key,
apps = row.data,
ret = {};
for (var i in apps) {
if (apps.hasOwnProperty(i)) {
set = apps[i];
for (ver in set) {
key = i + '_' + ver.split('.').slice(0,precision).join('.');
if (!(key in ret)) {
ret[key] = 0;
}
var v = parseFloat(set[ver]);
ret[key] += v;
}
}
}
out.data = ret;
return out;
}
// Takes a data row and a field identifier and returns the value.
function getField(row, field) {
var parts = field.split('|'),
val = row;
if (!val) return null;
for (var i = 0; i < parts.length; i++) {
val = val[parts[i]];
if (!val) {
return null;
}
}
return val;
}
function getPrettyName(metric, field) {
var parts = field.split('_');
var key = parts[0];
parts = parts.slice(1);
if (metric in csv_keys) {
if (key in csv_keys[metric]) {
return csv_keys[metric][key] + ' ' + parts.join(' ');
}
}
return field;
}
// Expose some functionality to the z.StatsManager api.
return {
'fetchData' : fetchData,
'dataStore' : dataStore,
'getPrettyName' : getPrettyName,
'getField' : getField
};
})();

Просмотреть файл

@ -0,0 +1,21 @@
$(function() {
var initView = {
metric: $('.primary').attr('data-report'),
range: '365 days', //$('.primary').attr('data-range'),
group: 'month'
};
$(window).trigger('changeview', initView);
});
$(window).bind("changeview", function(e, view) {
var queryParams;
if (view.range) {
if (typeof view == "string") {
queryparams = "last=" + view.split(/\s+/)[0];
history.replaceState(view, document.title, '?' + queryparams);
} else if (typeof view == "object") {
}
}
})

Просмотреть файл

Просмотреть файл

@ -476,6 +476,9 @@ MINIFY_BUNDLES = {
'css/impala/apps.less',
'css/impala/formset.less',
),
'zamboni/stats': (
'css/impala/stats.less',
),
'zamboni/discovery-pane': (
'css/zamboni/discovery-pane.css',
'css/impala/promos.less',
@ -722,11 +725,16 @@ MINIFY_BUNDLES = {
'zamboni/stats': (
'js/lib/jquery-datepicker.js',
'js/lib/highcharts.src.js',
'js/zamboni/stats/csv_keys.js',
'js/zamboni/stats/helpers.js',
'js/zamboni/stats/stats_manager.js',
'js/zamboni/stats/stats_tables.js',
'js/zamboni/stats/stats.js',
'js/impala/stats/csv_keys.js',
# 'js/zamboni/stats/helpers.js',
# 'js/zamboni/stats/stats_manager.js',
# 'js/zamboni/stats/stats_tables.js',
# 'js/zamboni/stats/stats.js',
'js/impala/stats/dateutils.js',
'js/impala/stats/manager.js',
'js/impala/stats/controls.js',
'js/impala/stats/chart.js',
'js/impala/stats/stats.js',
),
'zamboni/admin': (
'js/zamboni/admin.js',