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

Reply via email to