Cdentinger has submitted this change and it was merged.

Change subject: Groupings and stacked bar charts for X-by-Y
......................................................................


Groupings and stacked bar charts for X-by-Y

...finally have the 'by-Y' part of it for more than just time.
Purty colors, with clear associations between count and total for each
group (HSL FTW!)

TODO: don't need second pass, can use multiple 'x' arrays
TODO: show labels instead of values for things like contribution status

Bug: T86094
Change-Id: Ie55423132ad6ac74ff06c2afcc2aa419a768ffac
---
M src/app/widgetBase.js
M src/components/widgets/x-by-y/x-by-y.html
M src/components/widgets/x-by-y/x-by-y.js
3 files changed, 155 insertions(+), 62 deletions(-)

Approvals:
  Cdentinger: Looks good to me, approved
  jenkins-bot: Verified



diff --git a/src/app/widgetBase.js b/src/app/widgetBase.js
index 77369a4..9c432b3 100644
--- a/src/app/widgetBase.js
+++ b/src/app/widgetBase.js
@@ -108,16 +108,44 @@
 
                };
 
-               self.processData = function( rawdata, timescale, timestamp ){
+               self.processData = function( rawdata, timescale, grouping, 
timestamp ){
 
                        var timeWord = ( timescale === 'Day' ? 'Dai' : 
timescale ) + 'ly',
-                               totals = [ timeWord + ' Total'],
-                               counts = [ timeWord + ' Count'],
+                               totals,
+                               counts,
+                               isGrouped = ( grouping && grouping !== '' ),
+                               groupValue,
+                               groupValues,
+                               groupedTotals,
+                               groupedCounts,
+                               totalGroupNames,
+                               countGroupNames,
+                               totalName,
+                               countName,
+                               usedDates = [],
                                xs = [ 'x' ],
                                defaultYear = new Date().getFullYear(),
                                defaultMonth = new Date().getMonth() + 1,
-                               tempDate, timeFormat, now = new Date( timestamp 
);
-               
+                               tempDate, timeFormat, now = new Date( timestamp 
),
+                               sortFunction;
+
+                       if ( isGrouped ) {
+                               // distinct values of the group column
+                               groupValues = [];
+                               // for c3 to stack totals with totals and 
counts with counts
+                               totalGroupNames = [];
+                               countGroupNames = [];
+                               // these two are populated in the first pass 
with e.g.
+                               // groupedTotals['US']['2015-12-02 15'] = 123.45
+                               groupedTotals = [];
+                               groupedCounts = [];
+                               // these will be populated in a second pass
+                               totals = [];
+                               counts = [];
+                       } else {
+                               totals = [timeWord + ' Total'];
+                               counts = [timeWord + ' Count'];
+                       }
                        // coerce UTC into the default timezone.  Comparing 
offset values
                        // so we only have to adjust 'now', not each data point
                        now.setHours( now.getHours() + now.getTimezoneOffset() 
/ 60 );
@@ -131,17 +159,66 @@
                                if ( year < 2004 || new Date( year, month - 1, 
day, hour ) > now ) {
                                        return;
                                }
-                               totals.push( dataPoint.usd_total );
-                               counts.push( dataPoint.donations );
 
                                tempDate = year + '-';
                                tempDate += zeroPad( month ) + '-';
                                tempDate += zeroPad( day );
                                tempDate += ' ' + zeroPad( hour );
 
-                               xs.push( tempDate );
+                               if ( !usedDates[tempDate] ){
+                                       xs.push( tempDate );
+                                       usedDates[tempDate] = true;
+                               }
+                               if ( isGrouped ) {
+                                       groupValue = dataPoint[grouping];
+                                       if ( !totals[groupValue] ) {
+                                               groupValues.push( groupValue );
+                                               totalName = groupValue + ' 
total';
+                                               totals[groupValue] = 
[totalName];
+                                               groupedTotals[groupValue] = [];
+                                               totalGroupNames.push( totalName 
);
+                                               countName = groupValue + ' 
count';
+                                               counts[groupValue] = 
[countName];
+                                               groupedCounts[groupValue] = [];
+                                               countGroupNames.push( countName 
);
+                                       }
+                                       groupedTotals[groupValue][tempDate] = 
dataPoint.usd_total;
+                                       groupedCounts[groupValue][tempDate] = 
dataPoint.donations;
+                               } else {
+                                       // not grouped
+                                       totals.push( dataPoint.usd_total );
+                                       counts.push( dataPoint.donations );
+                               }
                        } );
 
+                       if ( isGrouped ) {
+                               // second pass to create data arrays with an 
entry for each x val
+                               // FIXME: remove this and use the xs property
+                               // http://c3js.org/reference.html#data-xs
+                               $.each( xs, function( index, xVal ) {
+                                       if ( xVal === 'x' ) {
+                                               return;
+                                       }
+                                       //clobber index because who cares
+                                       $.each( groupValues, function( index, 
groupVal ) {
+                                               if ( 
groupedTotals[groupVal][xVal] !== undefined ) {
+                                                       totals[groupVal].push( 
groupedTotals[groupVal][xVal] );
+                                                       counts[groupVal].push( 
groupedCounts[groupVal][xVal] );
+                                               } else {
+                                                       totals[groupVal].push( 
0 );
+                                                       counts[groupVal].push( 
0 );
+                                               }
+                                       } );
+                               } );
+                               groupValues.sort();
+                               totalGroupNames.sort();
+                               countGroupNames.sort();
+                               sortFunction = function( seriesA, seriesB ) {
+                                       return seriesA[0] < seriesB[0] ? -1 : 1;
+                               };
+                               totals.sort( sortFunction );
+                               counts.sort( sortFunction );
+                       }
                        switch(timescale){
                                case 'Year':
                                        timeFormat = '%Y';
@@ -161,7 +238,10 @@
                                totals: totals,
                                counts: counts,
                                xs: xs,
-                               timeFormat: timeFormat
+                               timeFormat: timeFormat,
+                               totalGroups: totalGroupNames,
+                               countGroups: countGroupNames,
+                               groupValues: groupValues
                        };
                };
 
@@ -179,6 +259,9 @@
                        for ( levelDiff = 1; index - levelDiff >= 0; 
levelDiff++ ) {
                                query = query + '&group=' + timeArray[index - 
levelDiff];
                        }
+                       if ( userChoices.xSlice ) {
+                               query = query + '&group=' + userChoices.xSlice;
+                       }
                        if ( index > 0 ) {
                                extraFilter = timeArray[index - 1] + 'sAgo lt 
\'1\'';
                                if ( filterQueryString === '' ) {
diff --git a/src/components/widgets/x-by-y/x-by-y.html 
b/src/components/widgets/x-by-y/x-by-y.html
index 8bbb285..8487deb 100644
--- a/src/components/widgets/x-by-y/x-by-y.html
+++ b/src/components/widgets/x-by-y/x-by-y.html
@@ -31,11 +31,11 @@
                                                                        <select 
data-bind="options: ySlices, value: showSlice"></select>
                                                                        <hr>
                                                                </div>
-                                                               <!-- <div 
class="row-fluid">
-                                                                       
<h4>By:</h4>
-                                                                       <select 
data-bind="options: xSlices, value: bySlice"></select>
+                                                               <div 
class="row-fluid">
+                                                                       
<h4>Group By:</h4>
+                                                                       <select 
data-bind="options: xSlices, optionsCaption: 'No group', value: bySlice, 
optionsText: 'text', optionsValue: 'value'"></select>
                                                                        <hr>
-                                                               </div> -->
+                                                               </div>
 
                                                                <div 
class="row-fluid">
                                                                        
<h4>Starting time range:</h4><br>
@@ -46,23 +46,6 @@
                                                                <div 
class="row-fluid">
                                                                        <label 
for="selectXYFilters">Additional filters:</label><br>
                                                                        
<filters params="change: logStateChange, userChoices: userChoices, 
metadataRequest: metadataRequest, queryString: filterQueryString" />
-                                                                       
<!--span data-bind="foreach: groupChoices">
-                                                                               
<div class="panel panel-default xyGroupOption">
-                                                                               
        <div class="panel-heading">
-                                                                               
                <span data-bind="text: $data.name" class="pull-left"></span>
-                                                                               
                <span class="pull-right" data-bind="if: $data.choices"><i 
class="fa fa-caret-down" data-bind="click: 
$parent.showPanelBody($data.name)"></i></span>
-                                                                               
        </div>
-                                                                               
        <div class="panel-body hide" data-bind="visible: $data.choices, attr: { 
id: $data.name + 'body' }">
-                                                                               
                <ul data-bind="foreach: $data.choices" class="filterPickerList 
list-inline">
-                                                                               
                        <li class="subFilterPickerList">
-                                                                               
                                <span class="label label-info">
-                                                                               
                                        <input type="checkbox" 
data-bind="value: $parent.name + ' eq \'' + $data + '\'', checked: 
$parents[1].chosenFilters"><span data-bind="text: $data"></span>
-                                                                               
                                </span>
-                                                                               
                        </li>
-                                                                               
                </ul>
-                                                                               
        </div>
-                                                                               
</div>
-                                                                       
</span-->
                                                                </div>
                                                        </form>
                                                </div>
diff --git a/src/components/widgets/x-by-y/x-by-y.js 
b/src/components/widgets/x-by-y/x-by-y.js
index d7a2c35..2d076bd 100644
--- a/src/components/widgets/x-by-y/x-by-y.js
+++ b/src/components/widgets/x-by-y/x-by-y.js
@@ -20,8 +20,7 @@
                self.displayedTimeChoice      = ko.observable('');
                self.queryRequest             = {};
                self.queryString              = '';
-               self.chosenFilters            = ko.observableArray();
-               self.subChoices               = ko.observableArray();
+               self.chosenFilters            = ko.observableArray(); // FIXME: 
remove, maybe adapt display to use filterText
                self.xByYChart                            = ko.observable( 
false );
                self.chartWidth(950);
 
@@ -50,27 +49,19 @@
                });
 
                self.makeChart = function(data){
-                       var colors = {}, axes = {};
-                       colors[data.totals[0]] = 'rgb(92,184,92)';
-                       colors[data.counts[0]] = '#f0ad4e';
-                       axes[data.totals[0]] = 'y';
-                       axes[data.counts[0]] = 'y2';
+                       var colors = {},
+                               axes = {},
+                               settings,
+                               columns = [],
+                               isGrouped = !!data.groupValues,
+                               numValues;
 
-                       self.xByYChart( false );
-                       self.xByYChart( {
+                       settings = {
                                size: {
                                        height: 450,
                                        width: window.width
                                },
                                zoom: { enabled: true },
-                               data: {
-                                       x: 'x',
-                                       columns: [ data.xs, data.totals, 
data.counts ],
-                                       type: 'bar',
-                                       colors: colors,
-                                       axes: axes,
-                                       xFormat: '%Y-%m-%d %H'
-                               },
                                grid: {
                                        x: {
                                                show: true
@@ -103,7 +94,49 @@
                                                ratio: 0.4
                                        }
                                }
-                       } );
+                       };
+
+                       if ( isGrouped ) {
+                               columns = [data.xs];
+                               numValues = data.groupValues.length;
+                               $.each( data.groupValues, function( index, 
groupVal ) {
+                                       var hue = index * 360 / numValues,
+                                               totalColumnName = 
data.totals[groupVal][0],
+                                               countColumnName = 
data.counts[groupVal][0];
+                                       columns[index + 1] = 
data.totals[groupVal];
+                                       columns[index + 1 + numValues] = 
data.counts[groupVal];
+                                       axes[totalColumnName] = 'y';
+                                       axes[countColumnName] = 'y2';
+                                       colors[totalColumnName] = 'hsl(' + hue 
+ ',100%,50%)';
+                                       colors[countColumnName] = 'hsl(' + hue 
+ ',100%,65%)';
+                               } );
+                               settings.data = {
+                                       columns: columns,
+                                       groups: [
+                                               data.totalGroups,
+                                               data.countGroups
+                                       ],
+                                       colors: colors
+                               };
+                       } else {
+                               colors[data.totals[0]] = 'rgb(92,184,92)';
+                               colors[data.counts[0]] = '#f0ad4e';
+                               axes[data.totals[0]] = 'y';
+                               axes[data.counts[0]] = 'y2';
+
+                               settings.data = {
+                                       columns: [ data.xs, data.totals, 
data.counts ],
+                                       colors: colors
+                               };
+                       }
+                       settings.data.x = 'x';
+                       settings.data.type = 'bar';
+                       settings.data.xFormat = '%Y-%m-%d %H';
+                       settings.data.axes = axes;
+
+                       self.xByYChart( false );
+
+                       self.xByYChart( settings );
                        self.chartLoaded(true);
                };
 
@@ -135,23 +168,15 @@
                        return self.metadataRequest.then( function( reqData ) {
                                self.metadata = reqData;
 
-                               var xArray = [], timeArray = ['Year', 'Month', 
'Day', 'Hour'], groupArray = [];
+                               var xArray = [],
+                                       timeArray = ['Year', 'Month', 'Day', 
'Hour'],
+                                       groupArray = [];
 
                                $.each(self.metadata.filters, function(prop, 
obj){
-
                                        if(obj.type !== 'number' || prop === 
'Amount'){
-
                                                if(obj.canGroup){
-                                                       if(obj.values){
-                                                               
groupArray.push({ 'name': prop, 'choices': obj.values });
-                                                       }
-
-                                                       $('select 
#'+prop).select2();
-
-                                                       //TODO: later this will 
do something different/more specific.
-                                                       xArray.push(prop);
+                                                       xArray.push( { text: 
obj.display, value: prop } );
                                                }
-
                                        }
                                });
                                self.xSlices(xArray);
@@ -164,12 +189,13 @@
                self.submitXY = function(){
 
                        self.queryRequest.ySlice = self.showSlice();
-                       //self.queryRequest.xSlice = self.bySlice();
+                       self.queryRequest.xSlice = self.bySlice();
                        //self.queryRequest.additionalFilters = 
self.chosenFilters();
                        self.queryRequest.timeBreakout = self.timeChoice();
 
                        self.queryString                 = 
self.convertToQuery(self.queryRequest);
                        self.config.showSlice    = self.showSlice();
+                       self.config.bySlice              = self.bySlice();
                        self.config.queryString  = self.queryString;
                        self.config.timeBreakout = 
self.queryRequest.timeBreakout;
                        self.config.chartData    = self.chartData;
@@ -180,7 +206,7 @@
                                self.displayedTimeChoice(self.timeChoice());
                                self.retrievedResults(dataArray.results);
 
-                               self.chartData = self.processData( 
self.retrievedResults(), self.timeChoice(), dataArray.timestamp );
+                               self.chartData = self.processData( 
self.retrievedResults(), self.timeChoice(), self.bySlice(), dataArray.timestamp 
);
 
                                self.makeChart(self.chartData);
 
@@ -193,6 +219,7 @@
                        self.preDataLoading(false);
                        if ( wasSaved ) {
                                // restore choices and show the chart
+                               self.bySlice( self.config.bySlice );
                                self.showSlice( self.config.showSlice );
                                self.timeChoice( self.config.timeBreakout );
                                self.chartSaved( true );

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

Gerrit-MessageType: merged
Gerrit-Change-Id: Ie55423132ad6ac74ff06c2afcc2aa419a768ffac
Gerrit-PatchSet: 5
Gerrit-Project: wikimedia/fundraising/dash
Gerrit-Branch: master
Gerrit-Owner: Ejegg <[email protected]>
Gerrit-Reviewer: Awight <[email protected]>
Gerrit-Reviewer: Cdentinger <[email protected]>
Gerrit-Reviewer: Ejegg <[email protected]>
Gerrit-Reviewer: jenkins-bot <>

_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits

Reply via email to