Mforns has submitted this change and it was merged.

Change subject: Use Dygraphs in Vital Signs
......................................................................


Use Dygraphs in Vital Signs

Bug: T96339
Change-Id: If20a496a7dbee13b73a013cd36213f4e55cded0c
---
M .jshintrc
M src/app/apis/annotations-api.js
A src/app/data-converters/annotations-data.js
M src/app/require.config.js
M src/components/a-b-compare/compare-sunburst.html
M src/components/breakdown-toggle/breakdown-toggle.js
M src/components/compare-layout/compare-layout.js
M src/components/visualizers/dygraphs-timeseries/bindings.js
M src/components/visualizers/dygraphs-timeseries/dygraphs-timeseries.html
M src/components/wikimetrics-layout/wikimetrics-layout.html
M src/components/wikimetrics-visualizer/wikimetrics-visualizer.html
M src/components/wikimetrics-visualizer/wikimetrics-visualizer.js
M src/layouts/metrics-by-project/01_styles.css
M test/components/wikimetrics-visualizer.js
14 files changed, 242 insertions(+), 63 deletions(-)

Approvals:
  Mforns: Verified; Looks good to me, approved



diff --git a/.jshintrc b/.jshintrc
index adb4994..8c65e7a 100644
--- a/.jshintrc
+++ b/.jshintrc
@@ -9,7 +9,6 @@
     "undef": true,
     "unused": true,
     "esnext": false,
-    "moz": true,
     "boss": true,
     "node": true,
     "validthis": true,
diff --git a/src/app/apis/annotations-api.js b/src/app/apis/annotations-api.js
index d853d1e..481869b 100644
--- a/src/app/apis/annotations-api.js
+++ b/src/app/apis/annotations-api.js
@@ -2,13 +2,65 @@
  * This module gets metric annotations that reside in Mediawiki.
  * To get them, it uses mediawiki-storage library.
  */
-define(['mediawiki-storage', 'moment', 'logger'], function (mediawikiStorage, 
moment) {
+define(function (require) {
     'use strict';
+
+    var mediawikiStorage = require('mediawiki-storage'),
+        moment = require('moment'),
+        logger = require('logger'),
+        converter = require('converters.annotations'),
+        TimeseriesData = require('converters.timeseries');
 
     function AnnotationsApi () {}
 
     /**
-     * Retrieves the annotations for the given metric.
+     * Gets the annotations in a TimeseriesData format.  Important notes:
+     *   1. range annotations (from date A to date B) come in as separate 
rows, prefixed
+     *      with Start: [the note] and End: [the note]
+     *   2. if duplicate dates are present, duplicateDates is set to true on 
the returned
+     *      object.  Callers should keep this in mind since it disallows merges
+     *
+     * Parameters
+     *   metric  : Metric object that has an annotations object:
+     *             {
+     *                 ...
+     *                 annotations: {
+     *                     host: 'mediawiki.host',
+     *                     pageName: 'PageName'
+     *                 },
+     *                 ...
+     *             }
+     *
+     * Returns
+     *   A promise to a TimeseriesData instance with the annotations
+     */
+    AnnotationsApi.prototype.getTimeseriesData = function (metric) {
+
+        var params = metric.annotations,
+            deferred = new $.Deferred();
+
+        if (!this.checkParams(params)) {
+            deferred.resolve(new TimeseriesData());
+
+        } else {
+            mediawikiStorage.get({
+                    host: params.host,
+                    pageName: params.pageName
+            }).done(function (data) {
+                deferred.resolve(converter()({}, data));
+
+            }).fail(function (error) {
+                // resolve as done with empty results and log the error
+                deferred.resolve(new TimeseriesData());
+                logger.error(error);
+            });
+        }
+        return deferred;
+    };
+
+    /**
+     * Retrieves the annotations for the given metric, as written on the wiki
+     *   but verified to be in the correct format
      *
      * Parameters
      *
@@ -51,10 +103,10 @@
         }
 
         var params = metric.annotations,
-            deferred = $.Deferred(),
-            that = this;
+            that = this,
+            deferred = new $.Deferred();
 
-        if (!this._checkParams(params)) {
+        if (!this.checkParams(params)) {
             // accept metrics without annotation params
             // and just return an empty array
             deferred.resolve([]);
@@ -66,7 +118,7 @@
             })
             .fail(deferred.reject)
             .done(function (data) {
-                that._checkAnnotations(data, deferred, params);
+                that.checkAnnotations(data, deferred, params);
             });
         }
 
@@ -75,7 +127,7 @@
         return deferred.promise();
     };
 
-    AnnotationsApi.prototype._checkParams = function (params) {
+    AnnotationsApi.prototype.checkParams = function (params) {
         return (
             typeof params === 'object' &&
             typeof params.host === 'string' &&
@@ -83,7 +135,7 @@
         );
     };
 
-    AnnotationsApi.prototype._checkDate = function (date) {
+    AnnotationsApi.prototype.checkDate = function (date) {
         return (
             date === void 0 ||
             typeof date === 'string' &&
@@ -91,8 +143,8 @@
         );
     };
 
-    AnnotationsApi.prototype._checkInterval = function (start, end) {
-        // assumes both dates have passed _checkDate
+    AnnotationsApi.prototype.checkInterval = function (start, end) {
+        // assumes both dates have passed checkDate
         return (
             start === void 0 ||
             end === void 0 ||
@@ -100,7 +152,7 @@
         );
     };
 
-    AnnotationsApi.prototype._checkAnnotations = function (data, deferred, 
params) {
+    AnnotationsApi.prototype.checkAnnotations = function (data, deferred, 
params) {
         if (!(data instanceof Array)) {
             var message = (
                 'Mediawiki page with host "' + params.host +
@@ -117,9 +169,9 @@
             var annotations = data.filter(function (annotation) {
                 return (
                     typeof annotation === 'object' &&
-                    that._checkDate(annotation.start) &&
-                    that._checkDate(annotation.end) &&
-                    that._checkInterval(annotation.start, annotation.end) &&
+                    that.checkDate(annotation.start) &&
+                    that.checkDate(annotation.end) &&
+                    that.checkInterval(annotation.start, annotation.end) &&
                     typeof annotation.note === 'string'
                 );
             });
diff --git a/src/app/data-converters/annotations-data.js 
b/src/app/data-converters/annotations-data.js
new file mode 100644
index 0000000..09a089f
--- /dev/null
+++ b/src/app/data-converters/annotations-data.js
@@ -0,0 +1,72 @@
+/**
+ * This module returns a function that parses a Wiki article which
+ *   holds annotations in a pre-defined format:
+ *
+ * [
+ *   {
+ *     "start": "2014-12-25", "end": "2015-01-01",
+ *     "note": "information about this range of dates"
+ *   }
+ * ]
+ *
+ * Parameters for the generated function:
+ *  label               : Used for header, colors, etc. default '(annotations)'
+ *  startRangePrefix    : Prefixed to notes that have a range. default 'Start: 
'
+ *  endRangePrefix      : Prefixed to notes that have a range. default 'End: '
+ */
+define(function (require) {
+    'use strict';
+
+    var _ = require('lodash'),
+        TimeseriesData = require('converters.timeseries');
+
+
+    return function () {
+
+        return function (options, rawData) {
+
+            var opt = $.extend({
+                label: '(annotations)',
+                startRangePrefix: 'Begin: ',
+                endRangePrefix: 'End: ',
+            }, options);
+
+            var ret = _.attempt(function () {
+                var duplicateDates = false;
+
+                var notesByDate = _(rawData).transform(function (result, 
annotation) {
+                    if (!annotation || !annotation.start) {
+                        return true;
+                    }
+
+                    if (annotation.end && (annotation.start !== 
annotation.end)) {
+                        result.push([annotation.start, opt.startRangePrefix + 
annotation.note]);
+                        result.push([annotation.end, opt.endRangePrefix + 
annotation.note]);
+                    } else {
+                        result.push([annotation.start, annotation.note]);
+                    }
+
+                }).transform(function (result, annotation) {
+
+                    if (_.has(result, annotation[0])) {
+                        duplicateDates = true;
+                    } else {
+                        result[annotation[0]] = [];
+                    }
+                    result[annotation[0]].push(annotation.splice(1));
+
+                }, {}).value();
+
+                return new TimeseriesData(
+                    [opt.label],
+                    notesByDate,
+                    [opt.label],
+                    [opt.label],
+                    duplicateDates
+                );
+            });
+
+            return _.isError(ret) ? new TimeseriesData() : ret;
+        };
+    };
+});
diff --git a/src/app/require.config.js b/src/app/require.config.js
index 36d6a0e..bf76e7b 100644
--- a/src/app/require.config.js
+++ b/src/app/require.config.js
@@ -58,6 +58,7 @@
         'converters.wikimetrics-timeseries' : 
'app/data-converters/wikimetrics-timeseries',
         'converters.funnel-data'            : 
'app/data-converters/funnel-data',
         'converters.timeseries'             : 
'app/data-converters/timeseries-data',
+        'converters.annotations'            : 
'app/data-converters/annotations-data',
 
         // *** lib
         'lib.polyfills'             : 'lib/polyfills',
diff --git a/src/components/a-b-compare/compare-sunburst.html 
b/src/components/a-b-compare/compare-sunburst.html
index 9d88cb4..80191a1 100644
--- a/src/components/a-b-compare/compare-sunburst.html
+++ b/src/components/a-b-compare/compare-sunburst.html
@@ -1,4 +1,4 @@
-<div class="ui grid height540">
+<div class="ui grid height540" data-bind="if: data">
     <div data-bind="if: data().showAB.a" class="eight wide full height column">
         <sunburst params="height: 450, data: data().a.data, colors: colors"/>
     </div>
diff --git a/src/components/breakdown-toggle/breakdown-toggle.js 
b/src/components/breakdown-toggle/breakdown-toggle.js
index 9b1422a..933336f 100644
--- a/src/components/breakdown-toggle/breakdown-toggle.js
+++ b/src/components/breakdown-toggle/breakdown-toggle.js
@@ -18,7 +18,7 @@
             // think carefully about the interface to that component.
             return [
                 '',
-                '2, 5',
+                '5, 5',
                 '15, 15',
                 '30, 5',
             ][index() + 1];
diff --git a/src/components/compare-layout/compare-layout.js 
b/src/components/compare-layout/compare-layout.js
index e400e3c..e925c4e 100644
--- a/src/components/compare-layout/compare-layout.js
+++ b/src/components/compare-layout/compare-layout.js
@@ -116,7 +116,7 @@
                 this.comparisons(config.comparisons.map(function (c) {
 
                     // a container for the async output of the following logic
-                    c.data = ko.observable({showAB: {}});
+                    c.data = ko.observable();
 
                     // promise what is being selected from the api
                     var apiPromises = ko.computed(function () {
@@ -125,8 +125,8 @@
 
                         return ['a', 'b'].map(function (side) {
                             return {
-                                promise: showAB[side]
-                                    ? api.getData(c.metric, config[side], wiki)
+                                promise: showAB[side] ?
+                                      api.getData(c.metric, config[side], wiki)
                                     : emptyPromise,
                                 label: config[side]
                             };
diff --git a/src/components/visualizers/dygraphs-timeseries/bindings.js 
b/src/components/visualizers/dygraphs-timeseries/bindings.js
index 510b743..a2d1602 100644
--- a/src/components/visualizers/dygraphs-timeseries/bindings.js
+++ b/src/components/visualizers/dygraphs-timeseries/bindings.js
@@ -8,15 +8,21 @@
     require('dygraphs');
 
     ko.bindingHandlers.dygraphs = {
+        init: function (element, valueAccessor) {
+            var val = ko.unwrap(valueAccessor());
+
+            if (val.height) {
+                $(element).height(val.height);
+            }
+
+            $(element).append('<div class="resizable container graph"></div>');
+        },
         update: function (element, valueAccessor) {
             var val = ko.unwrap(valueAccessor()),
                 data = ko.unwrap(val.data),
                 annotations = ko.unwrap(val.annotations),
-                colors = val.colors;
-
-            var graph = $(element).append(
-                $('<div/>').addClass('resizable').addClass('container')
-            ).find('div.resizable')[0];
+                colors = val.colors,
+                graph = $(element).find('div.graph')[0];
 
             if (data) {
                 var rows = data.rowData({convertToDate: true});
@@ -25,7 +31,7 @@
                             if (type === 'Date') {
                                 return moment(d).format('YYYY-MM-DD');
                             }
-                            return d.toFixed(3);
+                            return d.toFixed ? d.toFixed(3) : d;
                         },
                         labels: ['Date'],
                         labelsDivWidth: 350,
@@ -45,15 +51,20 @@
                         gridLinePattern: [10, 5],
                         series: {},
                         showRoller: true,
+                        animatedZooms: true,
                     };
 
                 var patterns = _(data.patternLabels)
                                     .uniq()
-                                    .zipObject([[], [5, 5], [15, 15], [30, 5]])
+                                    .zipObject([null, [5, 5], [15, 15], [30, 
5]])
                                     .value();
 
                 data.header.forEach(function (column, i) {
-                    var label = data.patternLabels[i] + ': ' + column;
+                    var label = data.patternLabels[i] === column
+                        ? data.colorLabels[i] + ': ' + column
+                        : isNaN(data.patternLabels[i])
+                            ? data.patternLabels[i] + ': ' + column
+                            : column;
                     options.labels.push(label);
                     options.series[label] = {
                         color: colors(data.colorLabels[i]),
@@ -69,14 +80,31 @@
 
                 if (annotations) {
                     dygraphChart.ready(function () {
-                        
dygraphChart.setAnnotations(annotations.rowData().map(function (a) {
+                        var i = 0;
+                        
dygraphChart.setAnnotations(annotations.rowData().map(function (a){
+                            // find the closest date in the data that fits
+                            var closestDate = null;
+                            var lastDistance = Math.pow(2, 53) - 1;
+                            for (; i < rows.length; i++) {
+                                var date = rows[i][0];
+                                var thisDistance = Math.min(lastDistance, 
Math.abs(date.getTime() - a[0]));
+                                // both arrays are sorted so the distance will 
only get further now
+                                if (thisDistance === lastDistance) {
+                                    i--;
+                                    break;
+                                }
+                                lastDistance = thisDistance;
+                                closestDate = rows[i][0];
+                            }
                             return {
                                 // just attach to the first series and show on 
X axis
                                 series: options.labels[1],
                                 attachAtBottom: true,
-                                shortText: 'D',
-                                // strip time information from the annotation 
so it matches the data
-                                x: new Date(a[0].getFullYear(), 
a[0].getMonth(), a[0].getDate()).getTime(),
+                                shortText: 'A',
+                                // annoying thing to learn through 
experimentation:
+                                //   Dygraphs requires Date instances for the 
data, but
+                                //   Dygraphs requires milliseconds since 
epoch for the annotations
+                                x: closestDate.getTime(),
                                 text: a[1],
                             };
                         }));
diff --git 
a/src/components/visualizers/dygraphs-timeseries/dygraphs-timeseries.html 
b/src/components/visualizers/dygraphs-timeseries/dygraphs-timeseries.html
index 9ed26af..34819c4 100644
--- a/src/components/visualizers/dygraphs-timeseries/dygraphs-timeseries.html
+++ b/src/components/visualizers/dygraphs-timeseries/dygraphs-timeseries.html
@@ -1,7 +1,7 @@
-<div data-bind="
+<div class="parent-of-resizable" data-bind="
     if: data && data.header,
-    style: {height: height + 'px'},
     dygraphs: {
+        height: $data.height,
         data: data,
         annotations: $data.annotations,
         colors: colors,
diff --git a/src/components/wikimetrics-layout/wikimetrics-layout.html 
b/src/components/wikimetrics-layout/wikimetrics-layout.html
index 3fa7da0..b06184b 100644
--- a/src/components/wikimetrics-layout/wikimetrics-layout.html
+++ b/src/components/wikimetrics-layout/wikimetrics-layout.html
@@ -20,7 +20,7 @@
                 </metric-selector>
                 <time-selector></time-selector>
             </div>
-            <div class="ui segment graph">
+            <div class="ui segment">
                 <wikimetrics-visualizer params="projects: selectedProjects,
                                                 metric: selectedMetric">
                 </wikimetrics-visualizer>
diff --git a/src/components/wikimetrics-visualizer/wikimetrics-visualizer.html 
b/src/components/wikimetrics-visualizer/wikimetrics-visualizer.html
index bf3e26a..7f62d92 100644
--- a/src/components/wikimetrics-visualizer/wikimetrics-visualizer.html
+++ b/src/components/wikimetrics-visualizer/wikimetrics-visualizer.html
@@ -1,15 +1,24 @@
 <!-- ko if: mergedData && mergedData() && mergedData().header.length -->
+
 <a class="header"
    data-bind="attr: {href: metric().definition}"
    title="view metric definition" target="_blank">
     <span data-bind="metricName: metric"></span>
     <i class="external url icon"></i>
 </a>
-<vega-timeseries params="data: mergedData, colorScale: 
colorScale"></vega-timeseries>
+
+<dygraphs-timeseries params="
+    data: mergedData,
+    colors: colorScale,
+    annotations: annotations,
+"></dygraphs-timeseries>
+
 <!-- /ko -->
+
+
 <!-- ko ifnot: mergedData && mergedData() && mergedData().header.length -->
+
 <p>Please select a metric above <i class="up icon"/><p>
 <p><i class="left icon"/> And projects from the left</p>
+
 <!-- /ko -->
-<annotation-list params="metric: metric">
-</annotation-list>
diff --git a/src/components/wikimetrics-visualizer/wikimetrics-visualizer.js 
b/src/components/wikimetrics-visualizer/wikimetrics-visualizer.js
index bc09170..a633cbe 100644
--- a/src/components/wikimetrics-visualizer/wikimetrics-visualizer.js
+++ b/src/components/wikimetrics-visualizer/wikimetrics-visualizer.js
@@ -17,6 +17,7 @@
         _ = require('lodash'),
         TimeseriesData = require('converters.timeseries'),
         templateMarkup = require('text!./wikimetrics-visualizer.html'),
+        annotationsApi = require('apis.annotations'),
         apiFinder = require('app/apis/api-finder');
 
     function WikimetricsVisualizer(params) {
@@ -25,16 +26,6 @@
         this.metric = params.metric;
         this.projects = params.projects;
         this.mergedData = ko.observable();
-        this.colorScale = ko.observable();
-
-        this.applyColors = function (projects, color) {
-            var scale = color || this.colorScale();
-            if (scale) {
-                projects.forEach(function (project) {
-                    project.color(scale(project.database));
-                });
-            }
-        };
 
         this.datasets = ko.computed(function () {
             var projects = ko.unwrap(this.projects),
@@ -53,19 +44,44 @@
                     return api.getData(metric, project.database, 
showBreakdown);
                 });
                 $.when.apply(this, promises).then(function () {
-                    
visualizer.mergedData(TimeseriesData.mergeAll(_.toArray(arguments)));
-                    visualizer.applyColors(projects);
-                });
+                    
this.mergedData(TimeseriesData.mergeAll(_.toArray(arguments)));
+                    this.applyColors(projects);
+                }.bind(this));
             } else {
-                visualizer.mergedData(new TimeseriesData([]));
+                this.mergedData(new TimeseriesData([]));
             }
 
         }, this);
 
-        this.colorScale.subscribe(function (color) {
-            var projects = ko.unwrap(this.projects);
-            this.applyColors(projects, color);
+        // get annotations as a TimeseriesData observable
+        this.annotations = ko.observable();
+        ko.computed(function () {
+            var metric = ko.unwrap(this.metric);
+            if (metric) {
+                
annotationsApi.getTimeseriesData(metric).done(this.annotations);
+            }
         }, this);
+
+        // build our own simple color scale to match the d3.scale.category10
+        // because otherwise we'd have to import all of d3
+        this.coloredProjects = [];
+        this.colors = [
+            '#1f77b4', '#ff7f0e', '#2ca02c', '#d62728', '#9467bd',
+            '#8c564b', '#e377c2', '#7f7f7f', '#bcbd22', '#17becf',
+        ];
+        this.colorScale = function (project) {
+            var i = _.indexOf(visualizer.coloredProjects, project);
+            if (i === -1) {
+                i = visualizer.coloredProjects.push(project) - 1;
+            }
+            return visualizer.colors[i % visualizer.colors.length];
+        };
+
+        this.applyColors = function (projects) {
+            projects.forEach(function (project) {
+                project.color(visualizer.colorScale(project.database));
+            });
+        };
     }
 
     return {
diff --git a/src/layouts/metrics-by-project/01_styles.css 
b/src/layouts/metrics-by-project/01_styles.css
index 8f9bf25..fa1c56f 100644
--- a/src/layouts/metrics-by-project/01_styles.css
+++ b/src/layouts/metrics-by-project/01_styles.css
@@ -111,18 +111,16 @@
     left: 300px;
 }
 .graph {
-    height: 87vh;
+    height: 80vh;
 }
-.graph .parent-of-resizable {
-    height: 100%;
-    min-height: 300px;
-    min-width: 800px;
-    max-height: 76vh;
+dygraphs-timeseries {
+    display: block;
+    margin-top: 20px;
 }
 .parent-of-resizable {
-    width: 100%;
-    height: 100%;
     padding: 0;
+    min-height: 300px;
+    min-width: 800px;
 }
 .colored i.swatch {
     vertical-align: bottom;
diff --git a/test/components/wikimetrics-visualizer.js 
b/test/components/wikimetrics-visualizer.js
index d78f042..46b94ac 100644
--- a/test/components/wikimetrics-visualizer.js
+++ b/test/components/wikimetrics-visualizer.js
@@ -51,10 +51,14 @@
                 projects: selectedProjects,
             });
             deferred.resolveWith(this, [transformedResponse]);
+
+            // just stub the apply colors function to get it out of the way
+            sinon.stub(visualizer, 'applyColors');
         });
 
         afterEach(function () {
             api.getData.restore();
+            visualizer.applyColors.restore();
         });
 
         it('should not do anything without a selected metric', function () {

-- 
To view, visit https://gerrit.wikimedia.org/r/214270
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings

Gerrit-MessageType: merged
Gerrit-Change-Id: If20a496a7dbee13b73a013cd36213f4e55cded0c
Gerrit-PatchSet: 6
Gerrit-Project: analytics/dashiki
Gerrit-Branch: master
Gerrit-Owner: Milimetric <dandree...@wikimedia.org>
Gerrit-Reviewer: Mforns <mfo...@wikimedia.org>
Gerrit-Reviewer: Milimetric <dandree...@wikimedia.org>

_______________________________________________
MediaWiki-commits mailing list
MediaWiki-commits@lists.wikimedia.org
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits

Reply via email to