Skip to content

Commit 9c09d8c

Browse files
committed
sort categorical Cartesian axes by value
1 parent d617a1b commit 9c09d8c

14 files changed

+537
-6
lines changed

Diff for: src/plot_api/plot_schema.js

+2
Original file line numberDiff line numberDiff line change
@@ -503,6 +503,8 @@ function getTraceAttributes(type) {
503503

504504
var out = {
505505
meta: _module.meta || {},
506+
categories: _module.categories || {},
507+
type: type,
506508
attributes: formatAttributes(attributes),
507509
};
508510

Diff for: src/plots/cartesian/layout_attributes.js

+9-5
Original file line numberDiff line numberDiff line change
@@ -817,8 +817,11 @@ module.exports = {
817817
categoryorder: {
818818
valType: 'enumerated',
819819
values: [
820-
'trace', 'category ascending', 'category descending', 'array'
821-
/* , 'value ascending', 'value descending'*/ // value ascending / descending to be implemented later
820+
'trace', 'category ascending', 'category descending', 'array',
821+
'value ascending', 'value descending',
822+
'min ascending', 'min descending',
823+
'max ascending', 'max descending',
824+
'sum ascending', 'sum descending'
822825
],
823826
dflt: 'trace',
824827
role: 'info',
@@ -828,11 +831,12 @@ module.exports = {
828831
'By default, plotly uses *trace*, which specifies the order that is present in the data supplied.',
829832
'Set `categoryorder` to *category ascending* or *category descending* if order should be determined by',
830833
'the alphanumerical order of the category names.',
831-
/* 'Set `categoryorder` to *value ascending* or *value descending* if order should be determined by the',
832-
'numerical order of the values.',*/ // // value ascending / descending to be implemented later
833834
'Set `categoryorder` to *array* to derive the ordering from the attribute `categoryarray`. If a category',
834835
'is not found in the `categoryarray` array, the sorting behavior for that attribute will be identical to',
835-
'the *trace* mode. The unspecified categories will follow the categories in `categoryarray`.'
836+
'the *trace* mode. The unspecified categories will follow the categories in `categoryarray`.',
837+
'Set `categoryorder` to *value ascending* or *value descending* if order should be determined by the',
838+
'numerical order of the values.',
839+
'Similarly, the order can be determined by the min, max or the sums of the values.'
836840
].join(' ')
837841
},
838842
categoryarray: {

Diff for: src/plots/cartesian/set_convert.js

+31
Original file line numberDiff line numberDiff line change
@@ -612,6 +612,37 @@ module.exports = function setConvert(ax, fullLayout) {
612612
}
613613
};
614614

615+
// sort the axis (and all the matching ones) by _initialCategories
616+
// returns the indices of the traces affected by the reordering
617+
ax.sortByInitialCategories = function() {
618+
var affectedTraces = [];
619+
var emptyCategories = function() {
620+
ax._categories = [];
621+
ax._categoriesMap = {};
622+
};
623+
624+
emptyCategories();
625+
626+
if(ax._initialCategories) {
627+
for(var j = 0; j < ax._initialCategories.length; j++) {
628+
setCategoryIndex(ax._initialCategories[j]);
629+
}
630+
}
631+
632+
affectedTraces = affectedTraces.concat(ax._traceIndices);
633+
634+
// Propagate to matching axes
635+
var group = ax._matchGroup;
636+
for(var axId2 in group) {
637+
if(axId === axId2) continue;
638+
var ax2 = fullLayout[axisIds.id2name(axId2)];
639+
ax2._categories = ax._categories;
640+
ax2._categoriesMap = ax._categoriesMap;
641+
affectedTraces = affectedTraces.concat(ax2._traceIndices);
642+
}
643+
return affectedTraces;
644+
};
645+
615646
// Propagate localization into the axis so that
616647
// methods in Axes can use it w/o having to pass fullLayout
617648
// Default (non-d3) number formatting uses separators directly

Diff for: src/plots/plots.js

+166
Original file line numberDiff line numberDiff line change
@@ -2843,10 +2843,176 @@ plots.doCalcdata = function(gd, traces) {
28432843

28442844
doCrossTraceCalc(gd);
28452845

2846+
// Sort axis categories per value if specified
2847+
var sorted = sortAxisCategoriesByValue(axList, gd);
2848+
if(sorted.length) {
2849+
// If a sort operation was performed, run calc() again
2850+
for(i = 0; i < sorted.length; i++) calci(sorted[i], true);
2851+
for(i = 0; i < sorted.length; i++) calci(sorted[i], false);
2852+
doCrossTraceCalc(gd);
2853+
}
2854+
28462855
Registry.getComponentMethod('fx', 'calc')(gd);
28472856
Registry.getComponentMethod('errorbars', 'calc')(gd);
28482857
};
28492858

2859+
var sortAxisCategoriesByValueRegex = /(value|sum|min|max) (ascending|descending)/;
2860+
2861+
function sortAxisCategoriesByValue(axList, gd) {
2862+
var affectedTraces = [];
2863+
var i, j, k, l, o;
2864+
for(i = 0; i < axList.length; i++) {
2865+
var ax = axList[i];
2866+
if(ax.type !== 'category') continue;
2867+
2868+
// Order by value
2869+
var match = ax.categoryorder.match(sortAxisCategoriesByValueRegex);
2870+
if(match) {
2871+
// Store values associated with each category
2872+
var categoriesValue = [];
2873+
for(j = 0; j < ax._categories.length; j++) {
2874+
categoriesValue.push([ax._categories[j], []]);
2875+
}
2876+
2877+
// Collect values across traces
2878+
for(j = 0; j < ax._traceIndices.length; j++) {
2879+
var traceIndex = ax._traceIndices[j];
2880+
var fullData = gd._fullData[traceIndex];
2881+
2882+
// Skip over invisible traces
2883+
if(fullData.visible !== true) continue;
2884+
2885+
var type = fullData.type;
2886+
if(type === 'histogram') delete fullData._autoBinFinished;
2887+
2888+
var cd = gd.calcdata[traceIndex];
2889+
for(k = 0; k < cd.length; k++) {
2890+
var cdi = cd[k];
2891+
var cat, catIndex, value;
2892+
2893+
// If `splom`, collect values across dimensions
2894+
if(type === 'splom') {
2895+
// Find which dimension the current axis is representing
2896+
var currentDimensionIndex = cdi.trace[ax._id.charAt(0) + 'axes'].indexOf(ax._id);
2897+
2898+
// Apply logic to associated x axis
2899+
if(ax._id.charAt(0) === 'y') {
2900+
var associatedXAxis = ax._id.split('');
2901+
associatedXAxis[0] = 'x';
2902+
associatedXAxis = associatedXAxis.join('');
2903+
ax = gd._fullLayout[axisIDs.id2name(associatedXAxis)];
2904+
}
2905+
2906+
var categories = cdi.trace.dimensions[currentDimensionIndex].values;
2907+
for(l = 0; l < categories.length; l++) {
2908+
cat = categories[l];
2909+
catIndex = ax._categoriesMap[cat];
2910+
2911+
// Collect values over all other dimensions
2912+
for(o = 0; o < cdi.trace.dimensions.length; o++) {
2913+
if(o === currentDimensionIndex) continue;
2914+
var dimension = cdi.trace.dimensions[o];
2915+
categoriesValue[catIndex][1].push(dimension.values[l]);
2916+
}
2917+
}
2918+
// If `scattergl`, collect all values stashed under cdi.t
2919+
} else if(type === 'scattergl') {
2920+
for(l = 0; l < cdi.t.x.length; l++) {
2921+
if(ax._id.charAt(0) === 'x') {
2922+
cat = cdi.t.x[l];
2923+
catIndex = cat;
2924+
value = cdi.t.y[l];
2925+
}
2926+
2927+
if(ax._id.charAt(0) === 'y') {
2928+
cat = cdi.t.y[l];
2929+
catIndex = cat;
2930+
value = cdi.t.x[l];
2931+
}
2932+
categoriesValue[catIndex][1].push(value);
2933+
}
2934+
// must clear scene 'batches', so that 2nd
2935+
// _module.calc call starts from scratch
2936+
if(cdi.t && cdi.t._scene) {
2937+
delete cdi.t._scene.dirty;
2938+
}
2939+
// For all other 2d cartesian traces
2940+
} else {
2941+
if(ax._id.charAt(0) === 'x') {
2942+
cat = cdi.p + 1 ? cdi.p : cdi.x;
2943+
value = cdi.s || cdi.v || cdi.y;
2944+
} else if(ax._id.charAt(0) === 'y') {
2945+
cat = cdi.p + 1 ? cdi.p : cdi.y;
2946+
value = cdi.s || cdi.v || cdi.x;
2947+
}
2948+
2949+
// If 2dMap, collect values in `z`
2950+
if(cdi.hasOwnProperty('z')) {
2951+
value = cdi.z;
2952+
2953+
for(l = 0; l < value.length; l++) {
2954+
for(o = 0; o < value[l].length; o++) {
2955+
catIndex = ax._id.charAt(0) === 'y' ? l : o;
2956+
categoriesValue[catIndex][1].push(value[l][o]);
2957+
}
2958+
}
2959+
} else {
2960+
if(!Array.isArray(value)) value = [value];
2961+
for(l = 0; l < value.length; l++) {
2962+
categoriesValue[cat][1].push(value[l]);
2963+
}
2964+
}
2965+
}
2966+
}
2967+
}
2968+
2969+
// Aggregate values
2970+
var aggFn;
2971+
switch(match[1]) {
2972+
case 'min':
2973+
aggFn = Math.min;
2974+
break;
2975+
case 'max':
2976+
aggFn = Math.max;
2977+
break;
2978+
default:
2979+
aggFn = function(a, b) { return a + b;};
2980+
}
2981+
2982+
ax._categoriesValue = categoriesValue;
2983+
2984+
var categoriesAggregatedValue = [];
2985+
for(j = 0; j < categoriesValue.length; j++) {
2986+
categoriesAggregatedValue.push([
2987+
categoriesValue[j][0],
2988+
Lib.aggNums(aggFn, null, categoriesValue[j][1])
2989+
]);
2990+
}
2991+
2992+
// Sort by aggregated value
2993+
categoriesAggregatedValue.sort(function(a, b) {
2994+
return a[1] - b[1];
2995+
});
2996+
2997+
ax._categoriesAggregatedValue = categoriesAggregatedValue;
2998+
2999+
// Set new category order
3000+
ax._initialCategories = categoriesAggregatedValue.map(function(c) {
3001+
return c[0];
3002+
});
3003+
3004+
// Reverse if descending
3005+
if(match[2] === 'descending') {
3006+
ax._initialCategories.reverse();
3007+
}
3008+
3009+
// Sort all matching axes
3010+
affectedTraces = affectedTraces.concat(ax.sortByInitialCategories());
3011+
}
3012+
}
3013+
return affectedTraces;
3014+
}
3015+
28503016
function setupAxisCategories(axList, fullData) {
28513017
for(var i = 0; i < axList.length; i++) {
28523018
var ax = axList[i];

Diff for: src/traces/bar/calc.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ module.exports = function calc(gd, trace) {
3333

3434
// set position and size
3535
for(var i = 0; i < serieslen; i++) {
36-
cd[i] = { p: pos[i], s: size[i] };
36+
cd[i] = { p: pos[i], s: size[i], v: size[i] };
3737

3838
if(trace.ids) {
3939
cd[i].id = String(trace.ids[i]);

Diff for: src/traces/box/calc.js

+4
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,10 @@ module.exports = function calc(gd, trace) {
8080
cdi.pos = posDistinct[i];
8181
cdi.pts = pts;
8282

83+
// Sort categories by values
84+
cdi[posLetter] = cdi.pos;
85+
cdi[valLetter] = cdi.pts.map(function(pt) { return pt.v; });
86+
8387
cdi.min = boxVals[0];
8488
cdi.max = boxVals[bvLen - 1];
8589
cdi.mean = Lib.mean(boxVals, bvLen);

Diff for: src/traces/ohlc/calc.js

+4
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,10 @@ function calcCommon(gd, trace, x, ya, ptFunc) {
8686
pt.i = i;
8787
pt.dir = increasing ? 'increasing' : 'decreasing';
8888

89+
// For categoryorder, store low and high
90+
pt.x = pt.pos;
91+
pt.y = [li, hi];
92+
8993
if(hasTextArray) pt.tx = trace.text[i];
9094
if(hasHovertextArray) pt.htx = trace.hovertext[i];
9195

19.2 KB
Loading
13.1 KB
Loading

Diff for: test/image/baselines/sort_by_value_matching_axes.png

18.5 KB
Loading

Diff for: test/image/mocks/hist_category_value_ascending.json

+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
{
2+
"data": [{
3+
"x": ["a", "b", "c", "a", "b", "d", "b", "c", "b", "b"],
4+
"type": "histogram"
5+
},
6+
{
7+
"x": ["d", "c", "a", "e", "a"],
8+
"type": "histogram"
9+
},
10+
{
11+
"y": ["a", "b", "c", "a", "b", "d", "b", "c"],
12+
"type": "histogram",
13+
"xaxis": "x2",
14+
"yaxis": "y2"
15+
},
16+
{
17+
"y": ["d", "c", "b", "a", "e", "a", "b"],
18+
"type": "histogram",
19+
"xaxis": "x2",
20+
"yaxis": "y2"
21+
}],
22+
"layout": {
23+
"title": "categoryorder: \"value ascending\"",
24+
"height": 400,
25+
"width": 600,
26+
"barmode": "stack",
27+
"xaxis": {
28+
"domain": [0, 0.45],
29+
"categoryorder": "value ascending"
30+
},
31+
"xaxis2": {
32+
"domain": [0.55, 1]
33+
},
34+
"yaxis2": {
35+
"anchor": "x2",
36+
"categoryorder": "value ascending"
37+
}
38+
}
39+
}
+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
{
2+
"data": [
3+
{
4+
"x": ["a", "b", "c", "d"],
5+
"y": [4, 2, 3, 1],
6+
"type": "scatter",
7+
"mode": "markers"
8+
},
9+
{
10+
"x": ["a", "b", "c", "d", "c", "c"],
11+
"type": "histogram"
12+
}
13+
],
14+
"layout": {
15+
"title": "xaxis.categoryorder: \"value descending\"",
16+
"width": 400,
17+
"height": 400,
18+
"xaxis": {
19+
"domain": [
20+
0,
21+
1
22+
],
23+
"categoryorder": "value descending"
24+
}
25+
}
26+
}

Diff for: test/image/mocks/sort_by_value_matching_axes.json

+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
{
2+
"data": [{
3+
"type": "scatter",
4+
"orientation": "v",
5+
"x": ["a", "b", "c"],
6+
"y": [7, 2, 3],
7+
8+
"z": [
9+
[7, 2, 3],
10+
[0, 0, 0],
11+
[0, 0, 0]
12+
],
13+
"dimensions": [{
14+
"label": "DimensionA",
15+
"values": ["a", "b", "c"]
16+
}, {
17+
"label": "DimensionB",
18+
"values": [7, 2, 3]
19+
}]
20+
}, {
21+
"type": "bar",
22+
"x": ["a", "b", "c"],
23+
"y": [10, 20, 30],
24+
"yaxis": "y2",
25+
"xaxis": "x2"
26+
27+
}],
28+
"layout": {
29+
"xaxis": {
30+
"type": "category",
31+
"categoryorder": "value ascending"
32+
},
33+
"xaxis2": {
34+
"matches": "x"
35+
},
36+
"yaxis": {
37+
"domain": [0, 0.45]
38+
},
39+
"yaxis2": {
40+
"domain": [0.55, 1]
41+
}
42+
}
43+
}

0 commit comments

Comments
 (0)