Mforns has uploaded a new change for review.

  https://gerrit.wikimedia.org/r/214036

Change subject: Add stacked bars component to compare layout
......................................................................

Add stacked bars component to compare layout

* Add visualizer that draws a stacked bar chart
* Add component that compares two stacked bar charts
* Add data converter that takes data for the bar chart

Bug: T91123
Change-Id: Iba4cc5c4145e1dfa1d7d5d47c09bc4247c7a383b
---
A src/app/data-converters/matrix-data.js
M src/app/data-converters/timeseries-data.js
M src/app/require.config.js
M src/app/startup.js
A src/components/a-b-compare/compare-stacked-bars.html
A src/components/a-b-compare/compare-stacked-bars.js
M src/components/compare-layout/compare-layout.js
A src/components/visualizers/stacked-bars/bindings.js
A src/components/visualizers/stacked-bars/stacked-bars.html
A src/components/visualizers/stacked-bars/stacked-bars.js
A src/css/070_stacked_bars.css
M src/layouts/compare/index.js
M src/lib/utils.js
13 files changed, 498 insertions(+), 5 deletions(-)


  git pull ssh://gerrit.wikimedia.org:29418/analytics/dashiki 
refs/changes/36/214036/1

diff --git a/src/app/data-converters/matrix-data.js 
b/src/app/data-converters/matrix-data.js
new file mode 100644
index 0000000..6999823
--- /dev/null
+++ b/src/app/data-converters/matrix-data.js
@@ -0,0 +1,64 @@
+/**
+ * This module returns a method that knows how to parse data of the form:
+ *   date        type   col1 col2 col3
+ *   2015-01-01  row1     1    2    3
+ *   2015-01-01  row2     4    5    6
+ *   2015-01-02  row1     7    8    9
+ * And aggregate it by date, returning a TimeseriesData object
+ * with the following data format:
+ *   date        row1@col1 row1@col2 row1@col3 row2@col1 row2@col2 row2@col3
+ *   2015-01-01      1         2         3         4         5         6
+ *   2015-01-02      7         8         9         0         0         0
+ */
+define(function (require) {
+
+    var TimeseriesData = require('converters.timeseries-data'),
+        utils = require('utils'),
+        _ = require('lodash');
+
+    return function (data) {
+        var colNames = data.header.slice(2, data.header.length),
+            headerMap = {},
+            rowsByDate = {};
+
+        // First pass: aggregate by date and get all crossNames
+        data.rows.forEach(function (row) {
+            var dateStr = utils.formatDate(row[0]),
+                rowName = row[1],
+                values = row.slice(2, row.length).map(function (value) {
+                    return +value || 0;
+                });
+
+            if (!_.has(rowsByDate, dateStr)) {
+                rowsByDate[dateStr] = {};
+            }
+            var rowByDate = rowsByDate[dateStr];
+
+            for (var i = 0; i < values.length; i++) {
+                var colName = colNames[i],
+                    value = values[i],
+                    crossName = rowName + '@' + colName;
+
+                headerMap[crossName] = true;
+                rowByDate[crossName] = value;
+            }
+        });
+
+        // Get header sorted alphabetically
+        var header = _.chain(headerMap).keys().sort().value();
+
+        // Second pass: sort values and fill undefineds
+        _.keys(rowsByDate).forEach(function (dateStr) {
+            var row = [];
+
+            header.forEach(function (crossName) {
+                var value = _.get(rowsByDate[dateStr], crossName, 0);
+                row.push(value);
+            });
+
+            rowsByDate[dateStr] = row;
+        });
+
+        return new TimeseriesData(header, rowsByDate);
+    };
+});
diff --git a/src/app/data-converters/timeseries-data.js 
b/src/app/data-converters/timeseries-data.js
index 65f6aa8..d6039d4 100644
--- a/src/app/data-converters/timeseries-data.js
+++ b/src/app/data-converters/timeseries-data.js
@@ -76,13 +76,11 @@
         };
 
         _.reduce(from, function (dest, src) {
-            console.log('processing: ', src);
             _.map(src.rowsByDate, function (value, key) {
                 if (!_.has(dest.rowsByDate, key)) {
                     // fill arrays that are nonexistent up to this point
                     dest.rowsByDate[key] = _.fill(Array(dest.header.length), 
null);
                 }
-                console.log(dest.rowsByDate);
                 dest.rowsByDate[key] = dest.rowsByDate[key].concat(value);
             });
 
diff --git a/src/app/require.config.js b/src/app/require.config.js
index 36d6a0e..53359c8 100644
--- a/src/app/require.config.js
+++ b/src/app/require.config.js
@@ -58,6 +58,8 @@
         'converters.wikimetrics-timeseries' : 
'app/data-converters/wikimetrics-timeseries',
         'converters.funnel-data'            : 
'app/data-converters/funnel-data',
         'converters.timeseries'             : 
'app/data-converters/timeseries-data',
+        'converters.matrix-data'            : 
'app/data-converters/matrix-data',
+        'converters.timeseries-data'        : 
'app/data-converters/timeseries-data',
 
         // *** lib
         'lib.polyfills'             : 'lib/polyfills',
diff --git a/src/app/startup.js b/src/app/startup.js
index 73e274f..99dd92d 100644
--- a/src/app/startup.js
+++ b/src/app/startup.js
@@ -32,12 +32,13 @@
     ko.components.register('rickshaw-timeseries', { require: 
'components/visualizers/rickshaw-timeseries/rickshaw-timeseries' });
     ko.components.register('nvd3-timeseries', { require: 
'components/visualizers/nvd3-timeseries/nvd3-timeseries' });
     ko.components.register('dygraphs-timeseries', { require: 
'components/visualizers/dygraphs-timeseries/dygraphs-timeseries' });
-    //ko.components.register('stacked-bar', { require: 
'components/visualizers/stacked-bar' });
+    ko.components.register('stacked-bars', { require: 
'components/visualizers/stacked-bars/stacked-bars' });
 
     // comparison components
     ko.components.register('a-b-compare', { require: 
'components/a-b-compare/a-b-compare' });
     ko.components.register('compare-sunburst', { require: 
'components/a-b-compare/compare-sunburst' });
     ko.components.register('compare-timeseries', { require: 
'components/a-b-compare/compare-timeseries' });
+    ko.components.register('compare-stacked-bars', { require: 
'components/a-b-compare/compare-stacked-bars' });
 
     // *********** END Funnel Layout Components ************ //
 
diff --git a/src/components/a-b-compare/compare-stacked-bars.html 
b/src/components/a-b-compare/compare-stacked-bars.html
new file mode 100644
index 0000000..6af4735
--- /dev/null
+++ b/src/components/a-b-compare/compare-stacked-bars.html
@@ -0,0 +1,9 @@
+<div class="ui grid height540">
+    <div data-bind="if: data.showAB().a" class="eight wide full height column">
+        <stacked-bars params="data: data.a.data(), colors: $data.colors, side: 
'left'"/>
+    </div>
+    <div data-bind="if: data.showAB().b" class="eight wide full height column">
+        <stacked-bars params="data: data.b.data(), colors: $data.colors, side: 
'right'"/>
+    </div>
+    <div class="stacked-bars-legend"></div>
+</div>
diff --git a/src/components/a-b-compare/compare-stacked-bars.js 
b/src/components/a-b-compare/compare-stacked-bars.js
new file mode 100644
index 0000000..34f0f88
--- /dev/null
+++ b/src/components/a-b-compare/compare-stacked-bars.js
@@ -0,0 +1,11 @@
+define(function (require) {
+    'use strict';
+
+    var templateMarkup = require('text!./compare-stacked-bars.html'),
+        CopyParams = require('viewmodels.copy-params');
+
+    return {
+        viewModel: CopyParams,
+        template: templateMarkup
+    };
+});
diff --git a/src/components/compare-layout/compare-layout.js 
b/src/components/compare-layout/compare-layout.js
index 3958c88..34e4d47 100644
--- a/src/components/compare-layout/compare-layout.js
+++ b/src/components/compare-layout/compare-layout.js
@@ -88,6 +88,16 @@
         wikiPromise.done(function (){
             configApi.getDefaultDashboard(function (config) {
 
+                ///////////////////////////////////////
+                // ADDED THIS JUST FOR TESTING
+                config.comparisons.push({
+                   "title": "Failure Types by User Type",
+                   "type": "stacked-bars",
+                   "metric": "failure_types_by_user_type",
+                   "desc": "***"
+                });
+                ///////////////////////////////////////
+
                 this.config = config;
 
                 // dates
@@ -115,7 +125,7 @@
                     if (c.type === 'timeseries') {
                         c.data = asyncMergedDataForAB(api, c.metric, config.a, 
config.b);
                     }
-                    else if (c.type === 'sunburst') {
+                    else if (c.type === 'sunburst' || c.type === 
'stacked-bars') {
                         c.data.a = {
                             label: config.a,
                             data: asyncData(api, c.metric, config.a)
diff --git a/src/components/visualizers/stacked-bars/bindings.js 
b/src/components/visualizers/stacked-bars/bindings.js
new file mode 100644
index 0000000..58b2f63
--- /dev/null
+++ b/src/components/visualizers/stacked-bars/bindings.js
@@ -0,0 +1,347 @@
+define(function(require) {
+
+    var ko = require('knockout'),
+        d3 = require('d3')
+        _ = require('lodash');
+
+    function unfoldData (timeseriesData) {
+        // Transforms the standard timeseriesData format
+        // into the format needed for the stacked bars.
+        var dataMap = {};
+
+        for (colIndex = 0; colIndex < timeseriesData.header.length; 
colIndex++) {
+            var header = timeseriesData.header[colIndex],
+                names = header.split('@'),
+                rowName = names[0],
+                colName = names[1];
+
+            if (!_.has(dataMap, rowName)) {
+                dataMap[rowName] = {};
+                dataMap[rowName]['type'] = rowName;
+            }
+
+            var totalValue = 0;
+            _.values(timeseriesData.rowsByDate).forEach(function (rowByDate) {
+                totalValue += rowByDate[colIndex];
+            });
+
+            dataMap[rowName][colName] = totalValue;
+        };
+
+        return _.values(dataMap);
+    }
+
+    ko.bindingHandlers.stackedBars = {
+        update: function (element, valueAccessor) {
+            var timeseriesData = ko.unwrap(valueAccessor().timeseriesData),
+                side = ko.unwrap(valueAccessor().side),
+                data = unfoldData(timeseriesData);
+
+            // This code is taken from:
+            // 
http://stackoverflow.com/questions/5560248/programmatically-lighten-or-darken-a-hex-color-or-rgb-and-blend-colors
+            // Thanks Pimp Trizkit
+            function highlightColor(color, percent){
+                var num = parseInt(color.slice(1), 16),
+                    amt = Math.round(2.55 * percent),
+                    R = (num >> 16) + amt,
+                    G = (num >> 8 & 0x00FF) + amt,
+                    B = (num & 0x0000FF) + amt;
+
+                return ("#" + (0x1000000 + (R < 255 ? R < 1 ? 0 : R : 255) * 
0x10000 +
+                    (G < 255 ? G < 1 ? 0 : G : 255) * 0x100 + ( B < 255 ? B < 
1 ? 0 : B : 255)
+                    ).toString(16).slice(1));
+            }
+
+            if (!_.isEmpty(data)) {
+
+                ///////////////////////////////////////////////////
+                // This code is based on this example:
+                // http://bl.ocks.org/yuuniverse4444/8325617
+                // Thanks yuuniverse4444
+
+                var margin = {top: 20, right: 20, bottom: 30, left: 60},
+                    width = 500 - margin.left - margin.right,
+                    height = 400 - margin.top - margin.bottom;
+
+                var x = d3.scale.ordinal().rangeRoundBands([0, width], .1),
+                    yAbsolute = d3.scale.linear().rangeRound([height, 0]),
+                    yRelative = d3.scale.linear().rangeRound([height, 0]);
+
+
+                var xAxis = d3.svg.axis().scale(x).orient("bottom");
+
+                var yAxisRelative = d3.svg.axis()
+                    .scale(yRelative)
+                    .orient("left")
+                    .tickFormat(d3.format(".0%"));
+
+                var yAxisAbsolute = d3.svg.axis()
+                    .scale(yAbsolute)
+                    .orient("left")
+                    .tickFormat(d3.format("2s"));
+
+                var svg = d3.select(element).append("svg")
+                    .attr("width", width + margin.left + margin.right)
+                    .attr("height", height + margin.top + margin.bottom)
+                    .append("g")
+                    .attr("transform", "translate(" + margin.left + "," + 
margin.top + ")");
+
+                var color = d3.scale.ordinal()
+                    .range(["#a5c5d5", "#88a1b3", "#967784", "#b06474",
+                        "#b27476", "#b98574", "#cd8c65", "#cdad55", 
"#b4ac54"]);
+
+                color.domain(d3.keys(data[0]).filter(function(key) { return 
key !== "type"; }));
+
+                data.forEach(function(d) {
+                    var mystate = d.type;
+                    var y0 = 0;
+                    d.columns = color.domain().map(function(name) {
+                        return {mystate:mystate, name: name, y0: y0, y1: y0 += 
+d[name]};
+                    });
+
+                    d.total = d.columns[d.columns.length - 1].y1; // the last 
row
+                    d.pct = [];
+
+                    for (var i=0;i <d.columns.length;i ++ ){
+
+                        var y_coordinate = +d.columns[i].y1/d.total;
+                        var y_height1 = (d.columns[i].y1)/d.total;
+                        var y_height0 = (d.columns[i].y0)/d.total;
+                        var y_pct = y_height1 - y_height0;
+                        d.pct.push({
+                            y_coordinate: y_coordinate,
+                            y_height1: y_height1,
+                            y_height0: y_height0,
+                            name: d.columns[i].name,
+                            mystate: d.type,
+                            y_pct: y_pct
+                        });
+                    }
+                });
+
+                x.domain(data.map(function(d) { return d.type; }));
+                yAbsolute.domain([0, d3.max(data, function(d) { return 
d.total; })]); // Absolute View scale
+                yRelative.domain([0,1]); // Relative View domain
+
+                var absoluteView = false; // define a boolean variable, true 
is absolute view,
+                                          // false is relative view. Initial 
view is absolute
+
+                svg.append("g")
+                    .attr("class", "x axis")
+                    .attr("transform", "translate(0," + height + ")")
+                    .style("font-size", "12px")
+                    .call(xAxis);
+
+                //Define the rect of Relative
+
+                var stateRelative = svg.selectAll(".relative")
+                    .data(data)
+                    .enter().append("g")
+                    .attr("class", "relative")
+                    .attr("transform", function(d) {
+                        return "translate(" + "0 "+ ",0)";
+                });
+
+                stateRelative.selectAll("rect")
+                    .data(function(d) {
+                        return d.pct;
+                    })
+                    .enter().append("rect")
+                    .attr("width", x.rangeBand())
+                    .attr("y", function(d) {
+                        return yRelative(d.y_coordinate);
+                    })
+                    .attr("x",function(d) {return x(d.mystate)})
+                    .attr("height", function(d) {
+                        return yRelative(d.y_height0) - 
yRelative(d.y_height1); //distance
+                    })
+                    .attr("fill", function(d){return color(d.name)})
+                    .attr("stroke","pink")
+                    .attr("stroke-width",0.2)
+                    .attr("id",function(d) {return d.mystate})
+                    .attr("class","relative")
+                    .attr("id",function(d) {return d.mystate})
+                    .attr("name", function(d) { return d.name; })
+                    .style("pointer-events","visible");
+
+                stateRelative.selectAll("rect")
+                    .on("mouseover", function(d){
+                        var xPos = parseFloat(d3.select(this).attr("x"));
+                        var yPos = parseFloat(d3.select(this).attr("y"));
+                        var height = parseFloat(d3.select(this).attr("height"))
+                        var width = parseFloat(d3.select(this).attr("width"))
+
+                        var name = d3.select(this).attr("name");
+
+                        d.previousFill = d3.select(this).attr("fill");
+                        var newFill = highlightColor(d.previousFill, 10);
+                        d3.select(this).attr("fill", newFill);
+
+                        var tooltip = svg.append("text")
+                            .attr("x",xPos + width/2)
+                            .attr("y",yPos + height/2)
+                            .attr("id","tooltip")
+                            .style("pointer-events","none")
+                            .style("font-size", "12px")
+                            .style("text-anchor","middle")
+                            .style("alignment-baseline","middle")
+                            .text(name + ": " + 
Math.floor(d.y_pct.toFixed(2)*100) + "%");
+
+                        var tooltipBox = tooltip[0][0].getBBox(),
+                            tooltipWidth = tooltipBox.width + 16,
+                            tooltipHeight = tooltipBox.height + 16;
+
+                        var rect = 
document.createElementNS("http://www.w3.org/2000/svg";, "rect");
+                            rect.setAttribute("x", xPos + width/2 - 
tooltipWidth/2);
+                            rect.setAttribute("y", yPos + height/2 - 
tooltipHeight/2);
+                            rect.setAttribute("rx", 4);
+                            rect.setAttribute("ry", 4);
+                            rect.setAttribute("id", "tooltipBox");
+                            rect.setAttribute("width", tooltipWidth);
+                            rect.setAttribute("height", tooltipHeight);
+                            rect.setAttribute("fill", "white");
+                            rect.setAttribute("fill-opacity", 0.7);
+                            rect.setAttribute("stroke", "black");
+                            rect.setAttribute("stroke-width", 0.2);
+                            rect.setAttribute("style", 
"pointer-events:none;text-anchor:middle;alignment-baseline:midele");
+                            svg[0][0].insertBefore(rect, tooltip[0][0]);
+                    })
+                    .on("mouseout",function(d){
+                        svg.select("#tooltip").remove();
+                        svg.select("#tooltipBox").remove();
+                        d3.select(this).attr("fill",d.previousFill);
+                    });
+
+                // End of define rect of relative
+
+                // define rect for absolute
+
+                var stateAbsolute= svg.selectAll(".absolute")
+                    .data(data)
+                    .enter().append("g")
+                    .attr("class", "absolute")
+                    .attr("transform", function(d) { return "translate(" + "0" 
+ ",0)"; });
+
+                stateAbsolute.selectAll("rect")
+                    .data(function(d) { return d.columns})
+                    .enter().append("rect")
+                    .attr("width", x.rangeBand())
+                    .attr("y", function(d) {
+
+                          return yAbsolute(d.y1);
+                    })
+                    .attr("x",function(d) {
+                          return x(d.mystate)
+                    })
+                    .attr("height", function(d) {
+                          return yAbsolute(d.y0) - yAbsolute(d.y1);
+                          })
+                    .attr("fill", function(d){
+                          return color(d.name)
+                          })
+                    .attr("id",function(d) {
+                          return d.mystate
+                    })
+                    .attr("class","absolute")
+                    .style("pointer-events","visible")
+                    .attr("opacity",0) // initially it is invisible, i.e. 
start with Absolute View
+                    .on("mouseover", function(d){
+                        var xPos = parseFloat(d3.select(this).attr("x"));
+                        var yPos = parseFloat(d3.select(this).attr("y"));
+                        var height = parseFloat(d3.select(this).attr("height"))
+                        var width = parseFloat(d3.select(this).attr("width"))
+
+                        d.previousFill = d3.select(this).attr("fill");
+                        var newFill = highlightColor(d.previousFill, 10);
+                        d3.select(this).attr("fill", newFill);
+
+                        svg.append("text")
+                            .attr("x",xPos + width/2)
+                            .attr("y",yPos + height/2)
+                            .attr("class","tooltip")
+                            .style("pointer-events","none")
+                            .text(Math.floor((d.y1-d.y0).toFixed(2)));
+                    })
+                    .on("mouseout",function(d){
+                        svg.select(".tooltip").remove();
+                        d3.select(this).attr("fill",d.previousFill);
+                    });
+
+                //define two different scales, but one of them will always be 
hidden.
+                svg.append("g")
+                    .attr("class", "y axis absolute")
+                    .style("font-size", "12px")
+                    .call(yAxisAbsolute)
+                    .append("text")
+                    .attr("transform", "rotate(-90)")
+                    .attr("y", 6)
+                    .attr("dy", ".71em")
+                    .style("text-anchor", "end");
+
+                svg.append("g")
+                    .attr("class", "y axis relative")
+                    .style("font-size", "12px")
+                    .call(yAxisRelative)
+                    .append("text")
+                    .attr("transform", "rotate(-90)")
+                    .attr("y", 6)
+                    .attr("dy", ".71em")
+                    .style("text-anchor", "end");
+
+                svg.select(".y.axis.absolute").style("opacity",0);
+
+                // end of define absolute
+
+                var clickButton = svg.selectAll(".clickButton")
+                    .data([30,30])
+                    .enter().append("g")
+                    .attr("class","clickButton")
+                    .style("cursor", "pointer");
+
+                clickButton.append("text")
+                    .attr("x", width - 8)
+                    .attr("y", -10)
+                    .attr("dy", ".35em")
+                    .style("text-anchor", "end")
+                    .text("Switch View")
+                    .style("font-size", "12px")
+                    .attr("fill","blue")
+                    .attr("id","clickChangeView" + side);
+
+                // start with relative view
+                Transition2Relative();
+
+                // Switch view on click the clickButton
+                d3.selectAll("#"+ "clickChangeView" + side)
+                    .on("click",function(){
+                        if(absoluteView){ // absolute, otherwise relative
+                            Transition2Relative();
+                        } else {
+                            Transition2Absolute();
+                        }
+                        absoluteView = !absoluteView // change the current 
view status
+                    });
+
+                function Transition2Absolute(){
+                    // Currently it is Relative
+                    
stateRelative.selectAll("rect").transition().style("opacity",0).style("visibility","hidden");
+                    
stateAbsolute.selectAll("rect").transition().style("opacity",1).style("visibility","visible");
+                    
svg.select(".y.axis.relative").transition().style("opacity",0);
+                    
svg.select(".y.axis.absolute").transition().style("opacity",1);
+                }
+
+                function Transition2Relative(){
+                    // Currently it is absolute
+                    
stateAbsolute.selectAll("rect").transition().style("opacity",0).style("visibility","hidden");
+                    
stateRelative.selectAll("rect").transition().style("opacity",1).style("visibility","visible");
+                    
svg.select(".y.axis.absolute").transition().style("opacity",0);
+                    
svg.select(".y.axis.relative").transition().style("opacity",1);
+                }
+
+            }
+            
+            ///////////////////////////////////////////////////
+        }
+    };
+
+});
diff --git a/src/components/visualizers/stacked-bars/stacked-bars.html 
b/src/components/visualizers/stacked-bars/stacked-bars.html
new file mode 100644
index 0000000..9ee7ee7
--- /dev/null
+++ b/src/components/visualizers/stacked-bars/stacked-bars.html
@@ -0,0 +1 @@
+<div data-bind="stackedBars: {timeseriesData: timeseriesData, side: 
side}"></div>
diff --git a/src/components/visualizers/stacked-bars/stacked-bars.js 
b/src/components/visualizers/stacked-bars/stacked-bars.js
new file mode 100644
index 0000000..3478467
--- /dev/null
+++ b/src/components/visualizers/stacked-bars/stacked-bars.js
@@ -0,0 +1,19 @@
+define(function(require) {
+
+    var ko = require('knockout'),
+        templateMarkup = require('text!./stacked-bars.html'),
+        buildMatrix = require('converters.matrix-data');
+
+    require('./bindings');
+
+    function StackedBars(params) {
+        this.rawData = params.data;
+        this.timeseriesData = ko.computed(function() {
+            var rd = ko.unwrap(this.rawData);
+            return buildMatrix(rd);
+        }, this);
+        this.side = ko.unwrap(params.side);
+    }
+
+    return { viewModel: StackedBars, template: templateMarkup };
+});
diff --git a/src/css/070_stacked_bars.css b/src/css/070_stacked_bars.css
new file mode 100644
index 0000000..e2c4a8d
--- /dev/null
+++ b/src/css/070_stacked_bars.css
@@ -0,0 +1,29 @@
+/*.axis {
+  font: 13px sans-serif;
+  font-weight: bold;
+  fill: #444;
+}
+
+.axis path {
+  fill: none;
+  stroke: #444;
+  shape-rendering: crispEdges;
+}
+
+.clickButton {
+    cursor: pointer;
+    text-decoration: normal;
+    font-weight: normal;
+}
+.clickButton text:hover {
+    text-decoration: underline;
+}
+
+.x.axis path {
+  display: none;
+}
+
+.tooltip{
+    text-anchor: middle;
+}
+*/
\ No newline at end of file
diff --git a/src/layouts/compare/index.js b/src/layouts/compare/index.js
index 694fbd5..b78c1fc 100644
--- a/src/layouts/compare/index.js
+++ b/src/layouts/compare/index.js
@@ -12,11 +12,13 @@
                 'components/a-b-compare/a-b-compare',
                 'components/a-b-compare/compare-sunburst',
                 'components/a-b-compare/compare-timeseries',
+                'components/a-b-compare/compare-stacked-bars',
             ],
             bundles: {
                 // If you want parts of the site to load on demand, remove 
them from the 'include' list
                 // above, and group them into bundles here.
                 'sunburst': ['components/visualizers/sunburst/sunburst'],
+                'stacked-bars': 
['components/visualizers/stacked-bars/stacked-bars'],
                 'dygraphs-timeseries': 
['components/visualizers/dygraphs-timeseries/dygraphs-timeseries'],
             }
         }
diff --git a/src/lib/utils.js b/src/lib/utils.js
index f657055..9be63e3 100644
--- a/src/lib/utils.js
+++ b/src/lib/utils.js
@@ -48,7 +48,7 @@
         },
 
         formatDate: function (d) {
-            return d ? d.format('YYYY-MM-DD') : '(invalid)';
+            return d ? moment(d).format('YYYY-MM-DD') : '(invalid)';
         },
 
         /**

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

Gerrit-MessageType: newchange
Gerrit-Change-Id: Iba4cc5c4145e1dfa1d7d5d47c09bc4247c7a383b
Gerrit-PatchSet: 1
Gerrit-Project: analytics/dashiki
Gerrit-Branch: master
Gerrit-Owner: Mforns <mfo...@wikimedia.org>

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

Reply via email to