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