Nuria has submitted this change and it was merged.
Change subject: Implement breakdowns for metrics with CSV data
......................................................................
Implement breakdowns for metrics with CSV data
Some notable steps away from Pau's original design:
* we are graphing the different breakdowns with patterns and labels
instead of colors
* we are not showing the breakdowns under each language in the project
selector
* we turned off animation by default because performance suffered
otherwise
The reason for these departures is that in practice the original design
was harder to implement and less performant.
Bug: T74739
Change-Id: I809d0b748d567e98174d1cb630b85546238f4d5f
---
M src/app/apis/config-api.js
M src/app/apis/legacy-pageview-api.js
M src/app/config.js
M src/app/data-converters/separated-values.js
M src/app/data-converters/wikimetrics-timeseries.js
M src/app/startup.js
A src/components/breakdown-toggle/breakdown-toggle.html
A src/components/breakdown-toggle/breakdown-toggle.js
M src/components/metric-selector/metric-selector.js
M src/components/visualizers/vega-timeseries/bindings.js
M src/components/visualizers/vega-timeseries/vega-timeseries.js
M src/components/wikimetrics-layout/wikimetrics-layout.html
M src/components/wikimetrics-layout/wikimetrics-layout.js
M src/components/wikimetrics-visualizer/wikimetrics-visualizer.js
M src/css/styles.css
M stubs/available-metrics-stub.json
M test/app/data-converters.js
A test/components/breakdown-toggle.js
18 files changed, 212 insertions(+), 62 deletions(-)
Approvals:
Nuria: Verified; Looks good to me, approved
diff --git a/src/app/apis/config-api.js b/src/app/apis/config-api.js
index 49da83a..633bd8d 100644
--- a/src/app/apis/config-api.js
+++ b/src/app/apis/config-api.js
@@ -60,4 +60,4 @@
};
return new ConfigApi(siteConfig);
-});
\ No newline at end of file
+});
diff --git a/src/app/apis/legacy-pageview-api.js
b/src/app/apis/legacy-pageview-api.js
index c8edacc..06ae3cc 100644
--- a/src/app/apis/legacy-pageview-api.js
+++ b/src/app/apis/legacy-pageview-api.js
@@ -28,15 +28,13 @@
};
/**
* Parameters
- * metric : an object representing metric
- * project : a Wiki project (English Wikipedia is 'enwiki', Commons is
'commonswiki', etc.)
- *
+ * metric : an object representing a metric
+ * project : a Wiki project database name (enwiki, commonswiki,
etc.) *
+ * showBreakdown : whether to materialize breakdowns
* Returns
* A promise with that wraps data for the metric/project transformed via
the converter
*/
- PageviewApi.prototype.getData = function (metric, project) {
-
- var metricName = metric.name;
+ PageviewApi.prototype.getData = function (metric, project, showBreakdown) {
//using christian's endpoint
// http://quelltextlich.at/wmf/projectcounts/daily/enwiki.csv
@@ -46,10 +44,13 @@
project: project
}).toString();
- var opt = {
- label: project
- };
+ var opt = {
+ label: project,
+ showBreakdown: showBreakdown,
+ breakdownColumns: metric.breakdown.columns
+
+ };
var converter = this.getDataConverter().bind(null, opt);
@@ -61,4 +62,4 @@
};
return new PageviewApi(siteConfig);
-});
\ No newline at end of file
+});
diff --git a/src/app/config.js b/src/app/config.js
index 3525f79..7b3e663 100644
--- a/src/app/config.js
+++ b/src/app/config.js
@@ -10,9 +10,8 @@
// but the confiApi will be switched to mediawiki in the future
configApi: {
endpoint: 'metrics-staging.wmflabs.org',
- urlCategorizedMetrics:
'https://metrics-staging.wmflabs.org/static/public/datafiles/available-metrics.json',
- //using stubs to iron any blockers with pageviews
- //urlCategorizedMetrics:
'https://metrics.wmflabs.org/static/public/datafiles/available-metrics.json',
+ //urlCategorizedMetrics:
'https://metrics-staging.wmflabs.org/static/public/datafiles/available-metrics.json',
+ urlCategorizedMetrics: '/stubs/available-metrics-stub.json',
urlDefaultDashboard:
'https://metrics.wmflabs.org/static/public/datafiles/defaultDashboard.json'
},
@@ -39,4 +38,4 @@
//urlCategorizedMetrics:
'/stubs/fake-wikimetrics/categorizedMetrics.json',
//urlDefaultDashboard: '/stubs/fake-wikimetrics/defaultDashboard.json',
};
-});
\ No newline at end of file
+});
diff --git a/src/app/data-converters/separated-values.js
b/src/app/data-converters/separated-values.js
index a039350..8015929 100644
--- a/src/app/data-converters/separated-values.js
+++ b/src/app/data-converters/separated-values.js
@@ -11,9 +11,6 @@
* * The default column to get values from is the second column
*/
return function (valueSeparator) {
- var self = this;
- // adding 1 level of indirection to deal with value separators
- var valueSeparator = valueSeparator;
return function (options, rawData) {
@@ -21,41 +18,52 @@
label: '(not named)',
lineSeparator: '\n',
valueSeparator: valueSeparator,
+ showBreakdown: false,
+
}, options);
-
-
var rows = rawData.split(opt.lineSeparator).map(function (row) {
return row.split(opt.valueSeparator);
});
var header = rows.splice(0, 1)[0],
- valueColumn = opt.columnToUse ?
header.indexOf(opt.columnToUse) : 1;
+ mainColumn = opt.columnToUse || header[1],
+ mainColumnIndex = header.indexOf(mainColumn),
+ allColumns = [mainColumn];
+
+ if (opt.showBreakdown) {
+ allColumns = allColumns.concat(opt.breakdownColumns);
+ }
var data = rows.map(function (row) {
- //some records are bad, filter them
+ // some records are bad, filter them
var date = moment(row[0]).toDate().getTime();
- var value = parseInt(row[valueColumn]);
+ var value = parseInt(row[mainColumnIndex], 10);
if (date && value && !isNaN(date) && !isNaN(value)) {
- return {
- date: moment(row[0]).toDate().getTime(),
- label: opt.label,
- value: value,
- };
+ // NOTE: for demos until we add a time-selector, uncomment
this line:
+ //if (date < 1413836239592) return [];
+ return allColumns.map(function (column) {
+ return {
+ date: date,
+ color: opt.label,
+ label: opt.showBreakdown ? opt.label + ': ' +
column : opt.label,
+ value: parseInt(row[header.indexOf(column)], 10),
+ type: column,
+ main: column === mainColumn,
+ };
+ });
}
+ });
+ data = [].concat.apply([], data);
- }).sort(function (a, b) {
+ return data.sort(function (a, b) {
return a.date - b.date;
}).filter(function (item) {
- //make sure not to return 'undefined' items
+ // make sure not to return 'undefined' items
return item;
});
-
- return data;
};
-
-
- }
-});
\ No newline at end of file
+ };
+});
diff --git a/src/app/data-converters/wikimetrics-timeseries.js
b/src/app/data-converters/wikimetrics-timeseries.js
index c091e11..0f15db6 100644
--- a/src/app/data-converters/wikimetrics-timeseries.js
+++ b/src/app/data-converters/wikimetrics-timeseries.js
@@ -49,8 +49,11 @@
for (i = 0; i < keys.length; i++) {
normalized.push({
date: moment(keys[i]).toDate().getTime(),
+ color: parameters.Cohort,
label: parameters.Cohort,
- value: rawData.result[aggregate][submetric][keys[i]]
+ value: rawData.result[aggregate][submetric][keys[i]],
+ type: 'Total',
+ main: true,
});
}
}
diff --git a/src/app/startup.js b/src/app/startup.js
index afa3880..096ce06 100644
--- a/src/app/startup.js
+++ b/src/app/startup.js
@@ -12,6 +12,7 @@
ko.components.register('project-selector', { require:
'components/project-selector/project-selector' });
ko.components.register('metric-selector', { require:
'components/metric-selector/metric-selector' });
ko.components.register('time-selector', { require:
'components/time-selector/time-selector' });
+ ko.components.register('breakdown-toggle', { require:
'components/breakdown-toggle/breakdown-toggle' });
ko.components.register('vega-timeseries', { require:
'components/visualizers/vega-timeseries/vega-timeseries' });
// Start the application
diff --git a/src/components/breakdown-toggle/breakdown-toggle.html
b/src/components/breakdown-toggle/breakdown-toggle.html
new file mode 100644
index 0000000..41b8d1b
--- /dev/null
+++ b/src/components/breakdown-toggle/breakdown-toggle.html
@@ -0,0 +1,25 @@
+<div class="ui secondary inverted vertical fluid menu">
+ <a class="header item" data-bind="css: {'off': !metric().showBreakdown()},
click: toggle">
+ <span class="default text">
+ <i class="block layout icon"></i>
+ Data Breakdowns
+ </span>
+ </a>
+ <!-- ko if: metric().showBreakdown -->
+ <div class="colored menu">
+ <!-- ko foreach: metric().breakdown.columns -->
+ <div class="item">
+ <span>
+ <i class="circle icon"></i>
+ <span data-bind="text: $data"></span>
+ <svg height="10" width="80">
+ <path stroke="white"
+ data-bind="attr: {'stroke-dasharray':
$parent.dashes($index)}"
+ d="M6 5 l215 0" />
+ </svg>
+ </span>
+ </div>
+ <!-- /ko -->
+ </div>
+ <!-- /ko -->
+</div>
diff --git a/src/components/breakdown-toggle/breakdown-toggle.js
b/src/components/breakdown-toggle/breakdown-toggle.js
new file mode 100644
index 0000000..9b1422a
--- /dev/null
+++ b/src/components/breakdown-toggle/breakdown-toggle.js
@@ -0,0 +1,32 @@
+define(function (require) {
+ 'use strict';
+
+ var templateMarkup = require('text!./breakdown-toggle.html');
+
+ function BreakdownToggle(params) {
+ var self = this;
+
+ self.metric = params.metric;
+
+ self.toggle = function () {
+ self.metric().showBreakdown(!self.metric().showBreakdown());
+ };
+
+ self.dashes = function (index) {
+ // NOTE: this is the same list used in the vega visualization
+ // it would be possible to relate them directly but we should
+ // think carefully about the interface to that component.
+ return [
+ '',
+ '2, 5',
+ '15, 15',
+ '30, 5',
+ ][index() + 1];
+ };
+ }
+
+ return {
+ viewModel: BreakdownToggle,
+ template: templateMarkup
+ };
+});
diff --git a/src/components/metric-selector/metric-selector.js
b/src/components/metric-selector/metric-selector.js
index adf3551..6453fd7 100644
--- a/src/components/metric-selector/metric-selector.js
+++ b/src/components/metric-selector/metric-selector.js
@@ -18,12 +18,12 @@
defaultSelection : (optional) array of pre-added metric names
"/>
*/
-define(function(require) {
+define(function (require) {
'use strict';
- var ko = require('knockout'),
- templateMarkup = require('text!./metric-selector.html'),
- utils = require('utils');
+ var ko = require('knockout'),
+ templateMarkup = require('text!./metric-selector.html'),
+ utils = require('utils');
require('./bindings');
@@ -54,14 +54,14 @@
return defaultMetrics;
}, this);
- this.categories = ko.computed(function(){
+ this.categories = ko.computed(function () {
var unwrap = ko.unwrap(this.metricsByCategory) || [],
copy = unwrap.slice(),
categories = copy.sort(utils.sortByNameIgnoreCase);
categories.splice(0, 0, {
name: 'All metrics',
- metrics: [].concat.apply([], categories.map(function(c) {
+ metrics: [].concat.apply([], categories.map(function (c) {
return c.metrics;
})).sort(utils.sortByNameIgnoreCase)
});
diff --git a/src/components/visualizers/vega-timeseries/bindings.js
b/src/components/visualizers/vega-timeseries/bindings.js
index fbc5983..435ecd5 100644
--- a/src/components/visualizers/vega-timeseries/bindings.js
+++ b/src/components/visualizers/vega-timeseries/bindings.js
@@ -37,9 +37,7 @@
width: 'auto',
height: 'auto',
parentSelector: '.parent-of-resizable',
- updateOptions: {
- duration: 300
- },
+ animate: false,
padding: {
top: 30,
right: 108,
@@ -50,9 +48,9 @@
colorScale: undefined
});
- // don't animate if there's a ton of data
- if (withDefaults.data.length >= 1000) {
- withDefaults.updateOptions = null;
+ // don't animate unless requested, animations cause performance
problems
+ if (withDefaults.data.length < 1000 && withDefaults.animate) {
+ withDefaults.updateOptions = { duration: 300 };
}
return withDefaults;
}
@@ -81,6 +79,14 @@
return [{
name: 'timeseries',
values: dataInCanonicalForm
+ },{
+ name: 'dashes',
+ values: [
+ {spacing: []},
+ {spacing: [2, 5]},
+ {spacing: [15, 15]},
+ {spacing: [30, 5]},
+ ]
}];
}
@@ -111,6 +117,17 @@
name: 'color',
type: 'ordinal',
range: 'category10'
+ }, {
+ name: 'dash',
+ type: 'ordinal',
+ domain: {
+ data: 'sub-metrics',
+ field: 'data.type'
+ },
+ range: {
+ data: 'dashes',
+ field: 'data.spacing'
+ }
}],
axes: [{
type: 'x',
@@ -166,7 +183,7 @@
data: 'timeseries',
transform: [{
type: 'facet',
- keys: ['data.label']
+ keys: ['data.label', 'data.type']
}]
},
marks: [{
@@ -188,7 +205,11 @@
},
stroke: {
scale: 'color',
- field: 'data.label'
+ field: 'data.color'
+ },
+ strokeDash: {
+ scale: 'dash',
+ field: 'data.type'
}
}
}
@@ -210,7 +231,7 @@
x: {
scale: 'x',
field: 'data.date',
- offset: 2
+ offset: 3
},
y: {
scale: 'y',
@@ -218,7 +239,7 @@
},
fill: {
scale: 'color',
- field: 'data.label'
+ field: 'data.color'
},
text: {
field: 'data.label'
diff --git a/src/components/visualizers/vega-timeseries/vega-timeseries.js
b/src/components/visualizers/vega-timeseries/vega-timeseries.js
index d99ea8e..944d1e6 100644
--- a/src/components/visualizers/vega-timeseries/vega-timeseries.js
+++ b/src/components/visualizers/vega-timeseries/vega-timeseries.js
@@ -14,7 +14,7 @@
width : 'auto',
height : 'auto',
parentSelector : '.parent-of-resizable',
- updateOptions : {duration: 300},
+ animate : false,
padding : {top: 30, right: 40, bottom: 30, left: 35},
strokeWidth : 2
colorScale : undefined (pass a ko.observable to monitor color)
diff --git a/src/components/wikimetrics-layout/wikimetrics-layout.html
b/src/components/wikimetrics-layout/wikimetrics-layout.html
index 136183d..3fa7da0 100644
--- a/src/components/wikimetrics-layout/wikimetrics-layout.html
+++ b/src/components/wikimetrics-layout/wikimetrics-layout.html
@@ -8,6 +8,9 @@
selectedProjects: selectedProjects">
</project-selector>
+ <!-- ko if: selectedMetric() && selectedMetric().breakdown -->
+ <breakdown-toggle params="metric:
selectedMetric"></breakdown-toggle>
+ <!-- /ko -->
</section>
<section class="page column main">
<div class="ui segment metric">
diff --git a/src/components/wikimetrics-layout/wikimetrics-layout.js
b/src/components/wikimetrics-layout/wikimetrics-layout.js
index 528a655..e0d4321 100644
--- a/src/components/wikimetrics-layout/wikimetrics-layout.js
+++ b/src/components/wikimetrics-layout/wikimetrics-layout.js
@@ -29,8 +29,20 @@
stateManagerFactory.getManager(this.selectedProjects,
this.selectedMetric,
this.defaultProjects, this.defaultMetrics);
-
configApi.getCategorizedMetrics(function (config) {
+ if (config.categorizedMetrics) {
+ config.categorizedMetrics.forEach(function (category) {
+ if (!category.metrics) {
+ return;
+ }
+ category.metrics.forEach(function (metric) {
+ // whether the metric was configured to be broken down
+ metric.breakdown = metric.breakdown || false;
+ // whether to graph the available breakdowns
+ metric.showBreakdown = ko.observable(false);
+ });
+ });
+ }
self.metrics(config.categorizedMetrics);
});
}
@@ -39,4 +51,4 @@
viewModel: WikimetricsLayout,
template: templateMarkup
};
-});
\ No newline at end of file
+});
diff --git a/src/components/wikimetrics-visualizer/wikimetrics-visualizer.js
b/src/components/wikimetrics-visualizer/wikimetrics-visualizer.js
index e09786d..6403290 100644
--- a/src/components/wikimetrics-visualizer/wikimetrics-visualizer.js
+++ b/src/components/wikimetrics-visualizer/wikimetrics-visualizer.js
@@ -42,7 +42,8 @@
metric = ko.unwrap(this.metric);
if (metric && projects && projects.length) {
- var promises;
+ var promises,
+ showBreakdown = ko.unwrap(metric.showBreakdown);
var api = getAPIFromMetric(metric);
@@ -50,7 +51,7 @@
// For a more optimal, but perhaps prematurely optimized,
version see:
//
https://gerrit.wikimedia.org/r/#/c/158244/8/src/components/wikimetrics-visualizer/wikimetrics-visualizer.js
promises = projects.map(function (project) {
- return api.getData(metric, project.database);
+ return api.getData(metric, project.database,
showBreakdown);
});
$.when.apply(this, promises).then(function () {
visualizer.mergedData([].concat.apply([], arguments));
@@ -98,4 +99,4 @@
viewModel: WikimetricsVisualizer,
template: templateMarkup
};
-});
\ No newline at end of file
+});
diff --git a/src/css/styles.css b/src/css/styles.css
index 1f377ca..14e3f69 100644
--- a/src/css/styles.css
+++ b/src/css/styles.css
@@ -141,6 +141,27 @@
width: 94%!important;
}
+breakdown-toggle {
+ display: block;
+ margin-top: 60px;
+}
+breakdown-toggle .ui.vertical.menu.fluid {
+ width: 94%!important;
+}
+breakdown-toggle .ui.menu a.header.item {
+ opacity: 1!important;
+}
+breakdown-toggle .ui.menu a.header.item,
+breakdown-toggle .ui.menu a.header.item:hover {
+ background-color: #4D4D4D;
+ color: #BDBDBD;
+}
+breakdown-toggle .ui.menu a.header.item.off,
+breakdown-toggle .ui.menu a.header.item.off:hover {
+ background-color: #808080;
+ color: #454545;
+}
+
/**
* Twitter typeahead stuff
*/
diff --git a/stubs/available-metrics-stub.json
b/stubs/available-metrics-stub.json
index 4740dff..d6a5cca 100644
--- a/stubs/available-metrics-stub.json
+++ b/stubs/available-metrics-stub.json
@@ -30,8 +30,11 @@
{
"metrics": [{
"definition":
"https://meta.wikimedia.org/wiki/Research:Pageviews",
- "name": "DailyPageviews",
- "api": "pageviewApi"
+ "name": "DailyPageviews",
+ "api": "pageviewApi",
+ "breakdown": {
+ "columns": ["Desktop site", "Mobile site"]
+ }
}],
"name": "Readers"
},
diff --git a/test/app/data-converters.js b/test/app/data-converters.js
index 0abc484..b21e4af 100644
--- a/test/app/data-converters.js
+++ b/test/app/data-converters.js
@@ -124,4 +124,4 @@
expect(converted[1].value).toEqual(1120.0);
});
});
-});
\ No newline at end of file
+});
diff --git a/test/components/breakdown-toggle.js
b/test/components/breakdown-toggle.js
new file mode 100644
index 0000000..f03176d
--- /dev/null
+++ b/test/components/breakdown-toggle.js
@@ -0,0 +1,20 @@
+define(['components/breakdown-toggle/breakdown-toggle', 'knockout'],
function(component, ko) {
+ var BreakdownToggle = component.viewModel;
+
+ describe('BreakdownToggle view model', function() {
+
+ it('should have a toggle function', function() {
+ var params = {
+ metric: ko.observable({
+ showBreakdown: ko.observable(false)
+ })
+ };
+ var instance = new BreakdownToggle(params);
+
+ expect(typeof(instance.toggle)).toBe('function');
+
+ instance.toggle();
+ expect(instance.metric().showBreakdown()).toBe(true);
+ });
+ });
+});
--
To view, visit https://gerrit.wikimedia.org/r/175458
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings
Gerrit-MessageType: merged
Gerrit-Change-Id: I809d0b748d567e98174d1cb630b85546238f4d5f
Gerrit-PatchSet: 6
Gerrit-Project: analytics/dashiki
Gerrit-Branch: master
Gerrit-Owner: Milimetric <[email protected]>
Gerrit-Reviewer: Nuria <[email protected]>
_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits