Skip to content

Commit 4065112

Browse files
committed
Merge pull request #419 from monfera/189-axis-category-ordering-squashed
Axis category ordering - adds feature #189
2 parents b9a1705 + b21ebb2 commit 4065112

19 files changed

+1268
-14
lines changed

src/plot_api/plot_api.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -854,10 +854,10 @@ function doCalcdata(gd) {
854854
fullLayout._piecolormap = {};
855855
fullLayout._piedefaultcolorcount = 0;
856856

857-
// delete category list, if there is one, so we start over
857+
// initialize the category list, if there is one, so we start over
858858
// to be filled in later by ax.d2c
859859
for(i = 0; i < axList.length; i++) {
860-
axList[i]._categories = [];
860+
axList[i]._categories = axList[i]._initialCategories.slice();
861861
}
862862

863863
for(i = 0; i < fullData.length; i++) {

src/plots/cartesian/axis_defaults.js

+7
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,9 @@ var layoutAttributes = require('./layout_attributes');
2020
var handleTickValueDefaults = require('./tick_value_defaults');
2121
var handleTickMarkDefaults = require('./tick_mark_defaults');
2222
var handleTickLabelDefaults = require('./tick_label_defaults');
23+
var handleCategoryOrderDefaults = require('./category_order_defaults');
2324
var setConvert = require('./set_convert');
25+
var orderedCategories = require('./ordered_categories');
2426
var cleanDatum = require('./clean_datum');
2527
var axisIds = require('./axis_ids');
2628

@@ -72,6 +74,10 @@ module.exports = function handleAxisDefaults(containerIn, containerOut, coerce,
7274
}
7375
}
7476

77+
containerOut._initialCategories = axType === 'category' ?
78+
orderedCategories(letter, containerIn.categoryorder, containerIn.categoryarray, options.data) :
79+
[];
80+
7581
setConvert(containerOut);
7682

7783
var dfltColor = coerce('color');
@@ -105,6 +111,7 @@ module.exports = function handleAxisDefaults(containerIn, containerOut, coerce,
105111
handleTickValueDefaults(containerIn, containerOut, coerce, axType);
106112
handleTickLabelDefaults(containerIn, containerOut, coerce, axType, options);
107113
handleTickMarkDefaults(containerIn, containerOut, coerce, options);
114+
handleCategoryOrderDefaults(containerIn, containerOut, coerce);
108115

109116
var lineColor = coerce2('linecolor', dfltColor),
110117
lineWidth = coerce2('linewidth'),
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/**
2+
* Copyright 2012-2016, Plotly, Inc.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the MIT license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
9+
'use strict';
10+
11+
var layoutAttributes = require('./layout_attributes');
12+
13+
module.exports = function handleCategoryOrderDefaults(containerIn, containerOut, coerce) {
14+
15+
if(containerIn.type !== 'category') return;
16+
17+
var validCategories = layoutAttributes.categoryorder.values;
18+
19+
var propercategoryarray = Array.isArray(containerIn.categoryarray) && containerIn.categoryarray.length > 0;
20+
21+
if(validCategories.indexOf(containerIn.categoryorder) === -1 && propercategoryarray) {
22+
23+
// when unspecified or invalid, use the default, unless categoryarray implies 'array'
24+
coerce('categoryorder', 'array'); // promote to 'array'
25+
26+
} else if(containerIn.categoryorder === 'array' && !propercategoryarray) {
27+
28+
// when mode is 'array' but no list is given, revert to default
29+
30+
containerIn.categoryorder = 'trace'; // revert to default
31+
coerce('categoryorder');
32+
33+
} else {
34+
35+
// otherwise use the supplied mode, or the default one if unsupplied or invalid
36+
coerce('categoryorder');
37+
38+
}
39+
};

src/plots/cartesian/layout_attributes.js

+30
Original file line numberDiff line numberDiff line change
@@ -466,6 +466,36 @@ module.exports = {
466466
'Only has an effect if `anchor` is set to *free*.'
467467
].join(' ')
468468
},
469+
categoryorder: {
470+
valType: 'enumerated',
471+
values: [
472+
'trace', 'category ascending', 'category descending', 'array'
473+
/*, 'value ascending', 'value descending'*/ // value ascending / descending to be implemented later
474+
],
475+
dflt: 'trace',
476+
role: 'info',
477+
description: [
478+
'Specifies the ordering logic for the case of categorical variables.',
479+
'By default, plotly uses *trace*, which specifies the order that is present in the data supplied.',
480+
'Set `categoryorder` to *category ascending* or *category descending* if order should be determined by',
481+
'the alphanumerical order of the category names.',
482+
/*'Set `categoryorder` to *value ascending* or *value descending* if order should be determined by the',
483+
'numerical order of the values.',*/ // // value ascending / descending to be implemented later
484+
'Set `categoryorder` to *array* to derive the ordering from the attribute `categoryarray`. If a category',
485+
'is not found in the `categoryarray` array, the sorting behavior for that attribute will be identical to',
486+
'the *trace* mode. The unspecified categories will follow the categories in `categoryarray`.'
487+
].join(' ')
488+
},
489+
categoryarray: {
490+
valType: 'data_array',
491+
role: 'info',
492+
description: [
493+
'Sets the order in which categories on this axis appear.',
494+
'Only has an effect if `categoryorder` is set to *array*.',
495+
'Used with `categoryorder`.'
496+
].join(' ')
497+
},
498+
469499

470500
_deprecated: {
471501
autotick: {
+77
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
/**
2+
* Copyright 2012-2016, Plotly, Inc.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the MIT license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
9+
10+
'use strict';
11+
12+
var d3 = require('d3');
13+
14+
// flattenUniqueSort :: String -> Function -> [[String]] -> [String]
15+
function flattenUniqueSort(axisLetter, sortFunction, data) {
16+
17+
// Bisection based insertion sort of distinct values for logarithmic time complexity.
18+
// Can't use a hashmap, which is O(1), because ES5 maps coerce keys to strings. If it ever becomes a bottleneck,
19+
// code can be separated: a hashmap (JS object) based version if all values encountered are strings; and
20+
// downgrading to this O(log(n)) array on the first encounter of a non-string value.
21+
22+
var categoryArray = [];
23+
24+
var traceLines = data.map(function(d) {return d[axisLetter];});
25+
26+
var i, j, tracePoints, category, insertionIndex;
27+
28+
var bisector = d3.bisector(sortFunction).left;
29+
30+
for(i = 0; i < traceLines.length; i++) {
31+
32+
tracePoints = traceLines[i];
33+
34+
for(j = 0; j < tracePoints.length; j++) {
35+
36+
category = tracePoints[j];
37+
38+
// skip loop: ignore null and undefined categories
39+
if(category === null || category === undefined) continue;
40+
41+
insertionIndex = bisector(categoryArray, category);
42+
43+
// skip loop on already encountered values
44+
if(insertionIndex < categoryArray.length - 1 && categoryArray[insertionIndex] === category) continue;
45+
46+
// insert value
47+
categoryArray.splice(insertionIndex, 0, category);
48+
}
49+
}
50+
51+
return categoryArray;
52+
}
53+
54+
55+
/**
56+
* This pure function returns the ordered categories for specified axisLetter, categoryorder, categoryarray and data.
57+
*
58+
* If categoryorder is 'array', the result is a fresh copy of categoryarray, or if unspecified, an empty array.
59+
*
60+
* If categoryorder is 'category ascending' or 'category descending', the result is an array of ascending or descending
61+
* order of the unique categories encountered in the data for specified axisLetter.
62+
*
63+
* See cartesian/layout_attributes.js for the definition of categoryorder and categoryarray
64+
*
65+
*/
66+
67+
// orderedCategories :: String -> String -> [String] -> [[String]] -> [String]
68+
module.exports = function orderedCategories(axisLetter, categoryorder, categoryarray, data) {
69+
70+
switch(categoryorder) {
71+
case 'array': return Array.isArray(categoryarray) ? categoryarray.slice() : [];
72+
case 'category ascending': return flattenUniqueSort(axisLetter, d3.ascending, data);
73+
case 'category descending': return flattenUniqueSort(axisLetter, d3.descending, data);
74+
case 'trace': return [];
75+
default: return [];
76+
}
77+
};

src/plots/cartesian/set_convert.js

+3-2
Original file line numberDiff line numberDiff line change
@@ -182,8 +182,9 @@ module.exports = function setConvert(ax) {
182182
// encounters them, ie all the categories from the
183183
// first data set, then all the ones from the second
184184
// that aren't in the first etc.
185-
// TODO: sorting options - do the sorting
186-
// progressively here as we insert?
185+
// it is assumed that this function is being invoked in the
186+
// already sorted category order; otherwise there would be
187+
// a disconnect between the array and the index returned
187188

188189
if(v !== null && v !== undefined && ax._categories.indexOf(v) === -1) {
189190
ax._categories.push(v);

src/plots/gl3d/layout/axis_attributes.js

+2
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,8 @@ module.exports = {
6868
description: 'Sets whether or not this axis is labeled'
6969
},
7070
color: axesAttrs.color,
71+
categoryorder: axesAttrs.categoryorder,
72+
categoryarray: axesAttrs.categoryarray,
7173
title: axesAttrs.title,
7274
titlefont: axesAttrs.titlefont,
7375
type: axesAttrs.type,
26.3 KB
Loading
Loading
Loading
Loading
Loading
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"data": [{
3+
"x": ["c","a","e","b","d"],
4+
"y": [15,11,12,13,14]}
5+
],
6+
"layout": {
7+
"xaxis": {
8+
"title": "category ascending",
9+
"type": "category",
10+
"categoryorder": "category ascending",
11+
"categoryarray": ["y","b","x","a","d","z","e","c", "q", "k"]
12+
}}
13+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
{
2+
"data": [
3+
{
4+
"x": [
5+
1,
6+
2,
7+
null,
8+
4,
9+
5
10+
],
11+
"y": [
12+
1,
13+
2,
14+
3,
15+
4,
16+
5
17+
],
18+
"connectgaps": false,
19+
"uid": "8ac13a"
20+
}
21+
],
22+
"layout": {
23+
"title": "categoryarray",
24+
"xaxis": {
25+
"type": "category",
26+
"range": [
27+
-0.18336673346693386,
28+
3.1833667334669338
29+
],
30+
"autorange": true,
31+
"categoryorder": "array",
32+
"categoryarray": [2,4,5,1]
33+
},
34+
"yaxis": {
35+
"type": "linear",
36+
"range": [
37+
0.7070063694267517,
38+
5.292993630573249
39+
],
40+
"autorange": true
41+
},
42+
"height": 450,
43+
"width": 1000,
44+
"autosize": true
45+
}
46+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"data": [{
3+
"x": ["c","a","e","b","d"],
4+
"y": [15,11,12,13,14]}
5+
],
6+
"layout": {
7+
"title": "categoryarray with truncated tails (y, q, k not plotted)",
8+
"xaxis": {
9+
"type": "category",
10+
"categoryorder": "array",
11+
"categoryarray": ["y","b","x","a","d","z","e","c", "q", "k"]
12+
}}
13+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
{
2+
"data": [
3+
{
4+
"x": [
5+
5,
6+
1,
7+
3,
8+
2,
9+
4
10+
],
11+
"y": [
12+
1,
13+
2,
14+
3,
15+
4,
16+
5
17+
],
18+
"connectgaps": false,
19+
"uid": "8ac13a"
20+
}
21+
],
22+
"layout": {
23+
"title": "category descending",
24+
"xaxis": {
25+
"type": "category",
26+
"categoryorder": "category descending",
27+
"categoryarray": [2,4,5,1]
28+
},
29+
"yaxis": {
30+
"type": "linear",
31+
"range": [
32+
0.7070063694267517,
33+
5.292993630573249
34+
],
35+
"autorange": true
36+
},
37+
"height": 450,
38+
"width": 1000,
39+
"autosize": true
40+
}
41+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
{
2+
"data": [
3+
{
4+
"x": [
5+
5,
6+
null,
7+
3,
8+
2,
9+
4
10+
],
11+
"y": [
12+
1,
13+
2,
14+
3,
15+
null,
16+
5
17+
],
18+
"connectgaps": false,
19+
"uid": "8ac13a"
20+
}
21+
],
22+
"layout": {
23+
"title": "category descending",
24+
"xaxis": {
25+
"type": "category",
26+
"categoryorder": "category descending",
27+
"categoryarray": [2,4,5,1]
28+
},
29+
"yaxis": {
30+
"type": "linear",
31+
"range": [
32+
0.7070063694267517,
33+
5.292993630573249
34+
],
35+
"autorange": true
36+
},
37+
"height": 450,
38+
"width": 1000,
39+
"autosize": true
40+
}
41+
}

0 commit comments

Comments
 (0)