Skip to content

Commit 67cc14d

Browse files
committed
sort categories by values: implement mean and median
1 parent fac239b commit 67cc14d

File tree

3 files changed

+61
-27
lines changed

3 files changed

+61
-27
lines changed

src/plots/cartesian/layout_attributes.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -821,7 +821,9 @@ module.exports = {
821821
'value ascending', 'value descending',
822822
'min ascending', 'min descending',
823823
'max ascending', 'max descending',
824-
'sum ascending', 'sum descending'
824+
'sum ascending', 'sum descending',
825+
'mean ascending', 'mean descending',
826+
'median ascending', 'median descending'
825827
],
826828
dflt: 'trace',
827829
role: 'info',
@@ -836,7 +838,7 @@ module.exports = {
836838
'the *trace* mode. The unspecified categories will follow the categories in `categoryarray`.',
837839
'Set `categoryorder` to *value ascending* or *value descending* if order should be determined by the',
838840
'numerical order of the values.',
839-
'Similarly, the order can be determined by the min, max or the sums of the values.'
841+
'Similarly, the order can be determined by the min, max, sum, mean or media of all the values.'
840842
].join(' ')
841843
},
842844
categoryarray: {

src/plots/plots.js

Lines changed: 16 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2856,7 +2856,7 @@ plots.doCalcdata = function(gd, traces) {
28562856
Registry.getComponentMethod('errorbars', 'calc')(gd);
28572857
};
28582858

2859-
var sortAxisCategoriesByValueRegex = /(value|sum|min|max) (ascending|descending)/;
2859+
var sortAxisCategoriesByValueRegex = /(value|sum|min|max|mean|median) (ascending|descending)/;
28602860

28612861
function sortAxisCategoriesByValue(axList, gd) {
28622862
var affectedTraces = [];
@@ -2876,8 +2876,7 @@ function sortAxisCategoriesByValue(axList, gd) {
28762876
if(xCategorical && o === value[l].length - 1) return -1;
28772877
if(yCategorical && l === value.length - 1) return -1;
28782878

2879-
var catIndex = axLetter === 'y' ? l : o;
2880-
return catIndex - 1;
2879+
return (axLetter === 'y' ? l : o) - 1;
28812880
};
28822881
} else {
28832882
return function(o, l) {
@@ -2886,13 +2885,25 @@ function sortAxisCategoriesByValue(axList, gd) {
28862885
}
28872886
}
28882887

2888+
var aggFn = {
2889+
'min': function(values) {return Lib.aggNums(Math.min, null, values);},
2890+
'max': function(values) {return Lib.aggNums(Math.max, null, values);},
2891+
'sum': function(values) {return Lib.aggNums(function(a, b) { return a + b;}, null, values);},
2892+
'value': function(values) {return Lib.aggNums(function(a, b) { return a + b;}, null, values);},
2893+
'mean': function(values) {return Lib.mean(values);},
2894+
'median': function(values) {values.sort(); var mid = Math.round((values.length - 1) / 2); return values[mid];}
2895+
};
2896+
28892897
for(i = 0; i < axList.length; i++) {
28902898
var ax = axList[i];
28912899
if(ax.type !== 'category') continue;
28922900

28932901
// Order by value
28942902
var match = ax.categoryorder.match(sortAxisCategoriesByValueRegex);
28952903
if(match) {
2904+
var aggregator = match[1];
2905+
var order = match[2];
2906+
28962907
// Store values associated with each category
28972908
var categoriesValue = [];
28982909
for(j = 0; j < ax._categories.length; j++) {
@@ -2991,26 +3002,13 @@ function sortAxisCategoriesByValue(axList, gd) {
29913002
}
29923003
}
29933004

2994-
// Aggregate values
2995-
var aggFn;
2996-
switch(match[1]) {
2997-
case 'min':
2998-
aggFn = Math.min;
2999-
break;
3000-
case 'max':
3001-
aggFn = Math.max;
3002-
break;
3003-
default:
3004-
aggFn = function(a, b) { return a + b;};
3005-
}
3006-
30073005
ax._categoriesValue = categoriesValue;
30083006

30093007
var categoriesAggregatedValue = [];
30103008
for(j = 0; j < categoriesValue.length; j++) {
30113009
categoriesAggregatedValue.push([
30123010
categoriesValue[j][0],
3013-
Lib.aggNums(aggFn, null, categoriesValue[j][1])
3011+
aggFn[aggregator](categoriesValue[j][1])
30143012
]);
30153013
}
30163014

@@ -3027,7 +3025,7 @@ function sortAxisCategoriesByValue(axList, gd) {
30273025
});
30283026

30293027
// Reverse if descending
3030-
if(match[2] === 'descending') {
3028+
if(order === 'descending') {
30313029
ax._initialCategories.reverse();
30323030
}
30333031

test/jasmine/tests/calcdata_test.js

Lines changed: 41 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -896,7 +896,7 @@ describe('calculated data and points', function() {
896896
// oneOrientationTraces are traces for which swapping x/y is not supported
897897
var oneOrientationTraces = ['ohlc', 'candlestick'];
898898

899-
function makeData(type, a, b, axName) {
899+
function makeData(type, axName, a, b) {
900900
var input = [a, b];
901901
var cat = input[axName === 'yaxis' ? 1 : 0];
902902
var data = input[axName === 'yaxis' ? 0 : 1];
@@ -939,6 +939,10 @@ describe('calculated data and points', function() {
939939
high: data,
940940
low: data,
941941

942+
// For histogram
943+
nbinsx: cat.length,
944+
nbinsy: data.length,
945+
942946
// For waterfall
943947
measure: measure,
944948

@@ -988,7 +992,7 @@ describe('calculated data and points', function() {
988992
['value ascending', 'value descending'].forEach(function(categoryorder) {
989993
it('sorts ' + axName + ' by ' + categoryorder + ' for trace type ' + trace.type, function(done) {
990994
var data = [7, 2, 3];
991-
var baseMock = {data: [makeData(trace.type, cat, data, axName)], layout: {}};
995+
var baseMock = {data: [makeData(trace.type, axName, cat, data)], layout: {}};
992996
baseMock.layout[axName] = { type: 'category', categoryorder: categoryorder};
993997

994998
// Set expectations
@@ -1007,7 +1011,7 @@ describe('calculated data and points', function() {
10071011
var type = trace.type;
10081012
var data = [7, 2, 3];
10091013
var data2 = [5, 4, 2];
1010-
var baseMock = { data: [makeData(type, cat, data, axName), makeData(type, cat, data2, axName)], layout: {}};
1014+
var baseMock = { data: [makeData(type, axName, cat, data), makeData(type, axName, cat, data2)], layout: {}};
10111015
baseMock.layout[axName] = { type: 'category', categoryorder: 'value ascending'};
10121016

10131017
var expectedAgg = [['a', data[0] + data2[0]], ['b', data[1] + data2[1]], ['c', data[2] + data2[2]]];
@@ -1021,7 +1025,7 @@ describe('calculated data and points', function() {
10211025
var type = trace.type;
10221026
var data = [7, 2, 3];
10231027
var data2 = [5, 4, 2];
1024-
var baseMock = { data: [makeData(type, cat, data, axName), makeData(type, cat, data2, axName)], layout: {}};
1028+
var baseMock = { data: [makeData(type, axName, cat, data), makeData(type, axName, cat, data2)], layout: {}};
10251029
baseMock.layout[axName] = { type: 'category', categoryorder: 'value ascending'};
10261030

10271031
// Hide second trace
@@ -1037,7 +1041,7 @@ describe('calculated data and points', function() {
10371041
var type = trace.type;
10381042
var data = [7, 2, 3];
10391043
var data2 = [5, 4, 2];
1040-
var baseMock = { data: [makeData(type, cat, data, axName), makeData(type, cat, data2, axName)], layout: {}};
1044+
var baseMock = { data: [makeData(type, axName, cat, data), makeData(type, axName, cat, data2)], layout: {}};
10411045
baseMock.layout[axName] = { type: 'category', categoryorder: 'min ascending'};
10421046

10431047
var expectedAgg = [['a', Math.min(data[0], data2[0])], ['b', Math.min(data[1], data2[1])], ['c', Math.min(data[2], data2[2])]];
@@ -1051,15 +1055,45 @@ describe('calculated data and points', function() {
10511055
var type = trace.type;
10521056
var data = [7, 2, 3];
10531057
var data2 = [5, 4, 2];
1054-
var baseMock = { data: [makeData(type, cat, data, axName), makeData(type, cat, data2, axName)], layout: {}};
1058+
var baseMock = { data: [makeData(type, axName, cat, data), makeData(type, axName, cat, data2)], layout: {}};
10551059
baseMock.layout[axName] = { type: 'category', categoryorder: 'max ascending'};
10561060

10571061
var expectedAgg = [['a', Math.max(data[0], data2[0])], ['b', Math.max(data[1], data2[1])], ['c', Math.max(data[2], data2[2])]];
1058-
if(type === 'ohlc' || type === 'candlestick') expectedAgg = [['a', expectedAgg[0][1]], ['b', expectedAgg[1][1]], ['c', expectedAgg[2][1]]];
10591062
if(type.match(/histogram/)) expectedAgg = [['a', 2], ['b', 1], ['c', 1]];
10601063

10611064
checkAggregatedValue(baseMock, expectedAgg, false, done);
10621065
});
1066+
1067+
it('take the mean of all values per category across traces of type ' + trace.type, function(done) {
1068+
var type = trace.type;
1069+
var data = [7, 2, 3];
1070+
var data2 = [5, 4, 2];
1071+
var baseMock = { data: [makeData(type, axName, cat, data), makeData(type, axName, cat, data2)], layout: {}};
1072+
baseMock.layout[axName] = { type: 'category', categoryorder: 'mean ascending'};
1073+
1074+
var expectedAgg = [['a', (data[0] + data2[0]) / 2 ], ['b', (data[1] + data2[1]) / 2], ['c', (data[2] + data2[2]) / 2]];
1075+
if(type === 'histogram') expectedAgg = [['a', 2], ['b', 1], ['c', 1]];
1076+
if(type === 'histogram2d') expectedAgg = [['a', 2 / 3], ['b', 1 / 3], ['c', 1 / 3]];
1077+
if(type === 'contour' || type === 'heatmap') expectedAgg = [['a', expectedAgg[0][1] / 3], ['b', expectedAgg[1][1] / 3], ['c', expectedAgg[2][1] / 3]];
1078+
if(type === 'histogram2dcontour') expectedAgg = [['a', 2 / 4], ['b', 1 / 4], ['c', 1 / 4]]; // TODO: this result is inintuitive
1079+
1080+
checkAggregatedValue(baseMock, expectedAgg, false, done);
1081+
});
1082+
1083+
it('take the median of all values per category across traces of type ' + trace.type, function(done) {
1084+
var type = trace.type;
1085+
var data = [7, 2, 3];
1086+
var data2 = [5, 4, 2];
1087+
var data3 = [6, 5, 7];
1088+
var baseMock = { data: [makeData(type, axName, cat, data), makeData(type, axName, cat, data2), makeData(type, axName, cat, data3)], layout: {}};
1089+
baseMock.layout[axName] = { type: 'category', categoryorder: 'median ascending'};
1090+
1091+
var expectedAgg = [['a', 6], ['b', 4], ['c', 3]];
1092+
if(type === 'histogram') expectedAgg = [['a', 2], ['b', 1], ['c', 1]];
1093+
if(type === 'histogram2d') expectedAgg = [['a', 1], ['b', 0], ['c', 0]];
1094+
if(type === 'histogram2dcontour' || type === 'contour' || type === 'heatmap') expectedAgg = [['a', 0], ['b', 0], ['c', 0]];
1095+
checkAggregatedValue(baseMock, expectedAgg, false, done);
1096+
});
10631097
});
10641098
});
10651099
});

0 commit comments

Comments
 (0)