stats overview page
This commit is contained in:
Родитель
9d4fe9a21d
Коммит
d9b3f8d7a8
|
@ -18,7 +18,7 @@
|
|||
<div class="piechart"></div>
|
||||
<table data-metric="apps">
|
||||
</table>
|
||||
<a href="applications/">{{ _('See more applications…') }}</a>
|
||||
<a href="usage/applications/">{{ _('See more applications…') }}</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="toplist">
|
||||
|
@ -27,7 +27,7 @@
|
|||
<div class="piechart"></div>
|
||||
<table data-metric="locales">
|
||||
</table>
|
||||
<a href="languages/">{{ _('See more languages…') }}</a>
|
||||
<a href="usage/languages/">{{ _('See more languages…') }}</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="toplist">
|
||||
|
@ -36,7 +36,7 @@
|
|||
<div class="piechart"></div>
|
||||
<table data-metric="os">
|
||||
</table>
|
||||
<a href="os/">{{ _('See more operating systems…') }}</a>
|
||||
<a href="usage/os/">{{ _('See more operating systems…') }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -6,7 +6,6 @@
|
|||
max-width: 1280px;
|
||||
width: auto;
|
||||
min-width: 1024px;
|
||||
padding-bottom: 500px;
|
||||
padding-left: 20px;
|
||||
padding-right: 20px;
|
||||
.header-search {
|
||||
|
@ -172,99 +171,45 @@ table tbody tr {
|
|||
overflow: hidden;
|
||||
}
|
||||
.toplist {
|
||||
overflow: hidden;
|
||||
float: left;
|
||||
width: 31%;
|
||||
padding: 0;
|
||||
margin-left: 3.5%;
|
||||
&:first-child {
|
||||
margin-left: 0;
|
||||
}
|
||||
h3 {
|
||||
margin: 0 0 .2em 0;
|
||||
}
|
||||
.highcharts-tooltip {
|
||||
position: absolute;
|
||||
top: 0 !important;
|
||||
left: 0 !important;
|
||||
}
|
||||
table {
|
||||
width: 100%;
|
||||
height: 160px;
|
||||
margin: .7em 0;
|
||||
}
|
||||
tr:first-child {
|
||||
border-top: 0;
|
||||
}
|
||||
td {
|
||||
text-align: right;
|
||||
padding: 0;
|
||||
line-height: 2em;
|
||||
&:first-child {
|
||||
text-align: left;
|
||||
}
|
||||
&:last-child {
|
||||
text-align: left;
|
||||
font-size: 90%;
|
||||
width:40px;
|
||||
padding-left: .3em;
|
||||
}
|
||||
}
|
||||
}
|
||||
.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
|
||||
|
|
|
@ -57,7 +57,7 @@
|
|||
}
|
||||
}
|
||||
};
|
||||
|
||||
Highcharts.setOptions({ lang: { resetZoom: '' } });
|
||||
var chart;
|
||||
// which unit do we use for a given metric?
|
||||
var metricTypes = {
|
||||
|
@ -85,6 +85,7 @@
|
|||
fields = obj.fields ? obj.fields.slice(0,5) : ['count'],
|
||||
data = obj.data,
|
||||
series = {},
|
||||
chartRange = {},
|
||||
t, row, i, field, val;
|
||||
|
||||
// Initialize the empty series object.
|
||||
|
@ -179,33 +180,38 @@
|
|||
var newConfig = $.extend(baseConfig, { series: chartData });
|
||||
// set up dual-axes for the overview chart.
|
||||
if (metric == "overview" && newConfig.series.length) {
|
||||
newConfig.yAxis = [
|
||||
{ // Downloads
|
||||
title: {
|
||||
text: gettext('Downloads')
|
||||
},
|
||||
// min: 0,
|
||||
labels: {
|
||||
formatter: function() {
|
||||
return Highcharts.numberFormat(this.value, 0);
|
||||
_.extend(newConfig, {
|
||||
yAxis : [
|
||||
{ // Downloads
|
||||
title: {
|
||||
text: gettext('Downloads')
|
||||
},
|
||||
// min: 0,
|
||||
labels: {
|
||||
formatter: function() {
|
||||
return Highcharts.numberFormat(this.value, 0);
|
||||
}
|
||||
}
|
||||
}, { // Daily Users
|
||||
title: {
|
||||
text: gettext('Daily Users')
|
||||
},
|
||||
labels: {
|
||||
formatter: function() {
|
||||
return Highcharts.numberFormat(this.value, 0);
|
||||
}
|
||||
},
|
||||
// min: 0,
|
||||
opposite: true
|
||||
}
|
||||
}, { // Daily Users
|
||||
title: {
|
||||
text: gettext('Daily Users')
|
||||
},
|
||||
labels: {
|
||||
formatter: function() {
|
||||
return Highcharts.numberFormat(this.value, 0);
|
||||
}
|
||||
},
|
||||
// min: 0,
|
||||
opposite: true
|
||||
],
|
||||
tooltip: {
|
||||
shared : true,
|
||||
crosshairs : true
|
||||
}
|
||||
];
|
||||
});
|
||||
// set Daily Users series to use the right yAxis.
|
||||
newConfig.series[1].yAxis = 1;
|
||||
newConfig.tooltip.shared = true;
|
||||
}
|
||||
newConfig.tooltip.formatter = tooltipFormatter;
|
||||
|
||||
|
@ -228,6 +234,10 @@
|
|||
|
||||
if (chart) chart.destroy();
|
||||
chart = new Highcharts.Chart(newConfig);
|
||||
chartRange = chart.xAxis[0].getExtremes();
|
||||
$("h1").click(function() {
|
||||
chart.xAxis[0].setExtremes(chartRange.min, chartRange.max);
|
||||
})
|
||||
$chart.removeClass('loading');
|
||||
});
|
||||
})();
|
|
@ -11,7 +11,7 @@ z.StatsManager = (function() {
|
|||
|
||||
// The version of the stats localStorage we are using.
|
||||
// If you increment this number, you cache-bust everyone!
|
||||
var STATS_VERSION = 22;
|
||||
var STATS_VERSION = 1;
|
||||
|
||||
var storage = z.Storage("stats"),
|
||||
storageCache = z.Storage("statscache"),
|
||||
|
@ -155,21 +155,18 @@ z.StatsManager = (function() {
|
|||
// 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) {
|
||||
function getDataRange(view) {
|
||||
dbg("enter getDataRange", view.metric);
|
||||
var range = z.date.normalizeRange(view.range),
|
||||
metric = view.metric,
|
||||
ds,
|
||||
needed = 0,
|
||||
ds = dataStore[metric],
|
||||
reqs = [],
|
||||
$def = $.Deferred();
|
||||
|
||||
function finished() {
|
||||
needed--;
|
||||
dbg(pendingFetches, " fetches pending");
|
||||
if (needed < 1) {
|
||||
var ret = {}, row,
|
||||
step = z.date.millis("1 day");
|
||||
ds = dataStore[metric];
|
||||
var ret = {}, row,
|
||||
step = z.date.millis("1 day");
|
||||
if (ds) {
|
||||
for (var i=range.start; i<range.end; i+= step) {
|
||||
if (ds[i]) {
|
||||
ret[i] = (metric == 'apps') ? collapseVersions(ds[i], 1) : ds[i];
|
||||
|
@ -177,30 +174,26 @@ z.StatsManager = (function() {
|
|||
}
|
||||
ret = groupData(ret, view);
|
||||
ret.metric = metric;
|
||||
$def.resolve(ret);
|
||||
}
|
||||
if (_.isEmpty(ret)) {
|
||||
ret.empty = true;
|
||||
}
|
||||
$def.resolve(ret);
|
||||
}
|
||||
|
||||
if (dataStore[metric]) {
|
||||
ds = dataStore[metric];
|
||||
if (ds) {
|
||||
dbg("range", range.start, range.end);
|
||||
if (ds.maxdate < range.end) {
|
||||
needed++;
|
||||
fetchData(metric, ds.maxdate, range.end, finished);
|
||||
reqs.push(fetchData(metric, ds.maxdate, range.end));
|
||||
}
|
||||
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();
|
||||
reqs.push(fetchData(metric, range.start, ds.mindate));
|
||||
}
|
||||
} else {
|
||||
dbg("metric not found");
|
||||
needed++;
|
||||
fetchData(metric, range.start, range.end, finished);
|
||||
reqs.push(fetchData(metric, range.start, range.end));
|
||||
}
|
||||
|
||||
$.when.apply(null, reqs).then(finished);
|
||||
return $def;
|
||||
}
|
||||
|
||||
|
@ -217,32 +210,49 @@ z.StatsManager = (function() {
|
|||
groupVal = false,
|
||||
groupCount = 0,
|
||||
d, row;
|
||||
|
||||
if (group == 'all') {
|
||||
groupKey = range.start;
|
||||
groupCount = 0;
|
||||
groupVal = {
|
||||
date: z.date.date_string(new Date(groupKey), '-'),
|
||||
count: 0,
|
||||
data: {}
|
||||
};
|
||||
}
|
||||
|
||||
function performAggregation() {
|
||||
// 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;
|
||||
}
|
||||
// overview gets special treatment. Only average ADUs.
|
||||
if (metric == "overview") {
|
||||
groupVal.data.updates /= groupCount;
|
||||
} else if (metric in breakdownMetrics) {
|
||||
// average for mean metrics.
|
||||
_.each(groupVal.data, function(val, field) {
|
||||
if (metricTypes[metric] == 'mean') {
|
||||
groupVal.data[field] /= groupCount;
|
||||
}
|
||||
});
|
||||
}
|
||||
groupedData[groupKey] = groupVal;
|
||||
}
|
||||
}
|
||||
|
||||
// big loop!
|
||||
for (var i=range.start; i<range.end; i+= z.date.millis('1 day')) {
|
||||
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;
|
||||
}
|
||||
// overview gets special treatment. Only average ADUs.
|
||||
if (metric == "overview") {
|
||||
groupVal.data.updates /= groupCount;
|
||||
} else if (metric in breakdownMetrics) {
|
||||
// average for mean metrics.
|
||||
_.each(groupVal.data, function(val, field) {
|
||||
if (metricTypes[metric] == 'mean') {
|
||||
groupVal.data[field] /= groupCount;
|
||||
}
|
||||
});
|
||||
}
|
||||
groupedData[groupKey] = groupVal;
|
||||
}
|
||||
if ((group == 'week' && d.getDay() === 0) ||
|
||||
(group == 'month' && d.getDate() == 1)) {
|
||||
|
||||
performAggregation();
|
||||
// set the new group date to the current iteration.
|
||||
groupKey = i;
|
||||
// reset our aggregates.
|
||||
|
@ -267,16 +277,16 @@ z.StatsManager = (function() {
|
|||
}
|
||||
groupCount++;
|
||||
}
|
||||
if (group == 'all') performAggregation();
|
||||
return groupedData;
|
||||
}
|
||||
|
||||
|
||||
// The beef. Negotiates with the server for data.
|
||||
function fetchData(metric, start, end, callback) {
|
||||
function fetchData(metric, start, end) {
|
||||
var seriesStart = start,
|
||||
seriesEnd = end;
|
||||
|
||||
pendingFetches++;
|
||||
seriesEnd = end,
|
||||
$def = $.Deferred();
|
||||
|
||||
var seriesURLStart = Highcharts.dateFormat('%Y%m%d', seriesStart),
|
||||
seriesURLEnd = Highcharts.dateFormat('%Y%m%d', seriesEnd),
|
||||
|
@ -286,7 +296,12 @@ z.StatsManager = (function() {
|
|||
|
||||
$.ajax({ url: seriesURL,
|
||||
dataType: 'text',
|
||||
success: fetchHandler});
|
||||
success: fetchHandler,
|
||||
error: errorHandler });
|
||||
|
||||
function errorHandler() {
|
||||
$def.fail();
|
||||
}
|
||||
|
||||
function fetchHandler(raw_data, status, xhr) {
|
||||
var maxdate = 0,
|
||||
|
@ -312,10 +327,9 @@ z.StatsManager = (function() {
|
|||
}
|
||||
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);
|
||||
$def.resolve();
|
||||
|
||||
} else if (xhr.status == 202) { //Handle a successful fetch but with no reponse
|
||||
|
||||
|
@ -331,6 +345,7 @@ z.StatsManager = (function() {
|
|||
|
||||
}
|
||||
}
|
||||
return $def;
|
||||
}
|
||||
|
||||
|
||||
|
@ -402,9 +417,11 @@ z.StatsManager = (function() {
|
|||
|
||||
// Expose some functionality to the z.StatsManager api.
|
||||
return {
|
||||
'fetchData' : fetchData,
|
||||
'dataStore' : dataStore,
|
||||
'getPrettyName' : getPrettyName,
|
||||
'getField' : getField
|
||||
'getDataRange' : getDataRange,
|
||||
'fetchData' : fetchData,
|
||||
'dataStore' : dataStore,
|
||||
'getPrettyName' : getPrettyName,
|
||||
'getField' : getField,
|
||||
'clearLocalStorage' : clearLocalStorage
|
||||
};
|
||||
})();
|
|
@ -0,0 +1,4 @@
|
|||
$(function() {
|
||||
if ($('.primary').attr('data-report') != 'overview') return;
|
||||
$('.toplist').topChart();
|
||||
});
|
|
@ -0,0 +1,115 @@
|
|||
(function($) {
|
||||
// "use strict";
|
||||
var baseConfig = {
|
||||
chart: {
|
||||
backgroundColor: null
|
||||
},
|
||||
title: {
|
||||
text: null
|
||||
},
|
||||
plotArea: {
|
||||
shadow: null,
|
||||
borderWidth: null,
|
||||
},
|
||||
tooltip: {
|
||||
enabled: false
|
||||
},
|
||||
plotOptions: {
|
||||
pie: {
|
||||
allowPointSelect: true,
|
||||
dataLabels: {
|
||||
enabled: false,
|
||||
color: '#333'
|
||||
},
|
||||
animation: false,
|
||||
size:190
|
||||
}
|
||||
},
|
||||
credits: {enabled:false},
|
||||
legend: {
|
||||
enabled:false
|
||||
},
|
||||
series: [{
|
||||
type: 'pie'
|
||||
}]
|
||||
};
|
||||
|
||||
$.fn.topChart = function(cfg) {
|
||||
$(this).each(function() {
|
||||
var $self = $(this),
|
||||
$win = $(window),
|
||||
$chart = $self.find('.piechart'),
|
||||
hChart,
|
||||
$table = $self.find('table'),
|
||||
metric = $table.attr('data-metric'),
|
||||
view = {
|
||||
'metric': metric,
|
||||
'group' : 'all'
|
||||
};
|
||||
|
||||
$win.bind('changeview', function(e, newView) {
|
||||
// we only want to respond to changes in range.
|
||||
if (!newView.range) return;
|
||||
$self.addClass('loading');
|
||||
_.extend(view, {'range' : z.date.normalizeRange(newView.range)});
|
||||
$.when(z.StatsManager.getDataRange(view))
|
||||
.then(function(data) {
|
||||
generateRankedList(data, render);
|
||||
});
|
||||
});
|
||||
|
||||
// We take the data (aggregated to one row)
|
||||
function generateRankedList(data, done) {
|
||||
var totalValue = data[view.range.start].count,
|
||||
otherValue = totalValue;
|
||||
data = data[view.range.start].data;
|
||||
if (_.isEmpty(data)) return;
|
||||
// Convert all fields to percentages and prettify names.
|
||||
var rankedList = _.map(data, function(val, key) {
|
||||
var field = key.split("|").slice(-1)[0];
|
||||
return [z.StatsManager.getPrettyName(metric, field),
|
||||
val, val/totalValue*100];
|
||||
});
|
||||
// Sort by value.
|
||||
rankedList = _.sortBy(rankedList, function(a) {
|
||||
return -a[1];
|
||||
});
|
||||
// Calculate the 'Other' percentage
|
||||
for (var i=0; i<5; i++) {
|
||||
otherValue -= rankedList[i][1];
|
||||
}
|
||||
// Take the top 5 values and append an 'Other' row.
|
||||
rankedList = rankedList.slice(0,5);
|
||||
rankedList.push([gettext('Other'), otherValue, otherValue/totalValue*100]);
|
||||
// Move on with our lives.
|
||||
done(rankedList);
|
||||
}
|
||||
|
||||
var tableRow = template("<tr><td>{0}</td><td>{1}</td><td>({2}%)</td></tr>");
|
||||
|
||||
function render(data) {
|
||||
var newBody = "<tbody>";
|
||||
_.each(data, function(row) {
|
||||
var pct = Math.round(row[2]);
|
||||
num = Highcharts.numberFormat(row[1], 0);
|
||||
if (pct < 1) pct = "<1";
|
||||
newBody += tableRow([row[0], num, pct]);
|
||||
});
|
||||
newBody += "</tbody>";
|
||||
$table.html(newBody);
|
||||
|
||||
// set up chart.
|
||||
var newConfig = _.clone(baseConfig),
|
||||
row;
|
||||
newConfig.chart.renderTo = $chart[0];
|
||||
newConfig.series[0].data = _.map(data, function(r) { return r.slice(0,2); });
|
||||
hChart = new Highcharts.Chart(newConfig);
|
||||
for (i = 0; i < data.length; i++) {
|
||||
row = $table.find('tr').eq(i);
|
||||
row.children().eq(0).append($("<b class='seriesdot' style='background:" + hChart.series[0].data[i].color + "'> </b>"));
|
||||
}
|
||||
$self.removeClass('loading');
|
||||
}
|
||||
});
|
||||
};
|
||||
})(jQuery);
|
|
@ -746,6 +746,8 @@ MINIFY_BUNDLES = {
|
|||
'js/impala/stats/dateutils.js',
|
||||
'js/impala/stats/manager.js',
|
||||
'js/impala/stats/controls.js',
|
||||
'js/impala/stats/overview.js',
|
||||
'js/impala/stats/topchart.js',
|
||||
'js/impala/stats/chart.js',
|
||||
'js/impala/stats/stats.js',
|
||||
),
|
||||
|
|
Загрузка…
Ссылка в новой задаче