Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Axis category ordering - adds feature #189 #419

Merged
merged 9 commits into from
Apr 18, 2016
4 changes: 2 additions & 2 deletions src/plot_api/plot_api.js
Original file line number Diff line number Diff line change
Expand Up @@ -854,10 +854,10 @@ function doCalcdata(gd) {
fullLayout._piecolormap = {};
fullLayout._piedefaultcolorcount = 0;

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

for(i = 0; i < fullData.length; i++) {
Expand Down
7 changes: 7 additions & 0 deletions src/plots/cartesian/axis_defaults.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@ var layoutAttributes = require('./layout_attributes');
var handleTickValueDefaults = require('./tick_value_defaults');
var handleTickMarkDefaults = require('./tick_mark_defaults');
var handleTickLabelDefaults = require('./tick_label_defaults');
var handleCategoryOrderDefaults = require('./category_order_defaults');
var setConvert = require('./set_convert');
var orderedCategories = require('./ordered_categories');
var cleanDatum = require('./clean_datum');
var axisIds = require('./axis_ids');

Expand Down Expand Up @@ -72,6 +74,10 @@ module.exports = function handleAxisDefaults(containerIn, containerOut, coerce,
}
}

containerOut._initialCategories = axType === 'category' ?
orderedCategories(letter, containerIn.categoryorder, containerIn.categoryarray, options.data) :
[];

setConvert(containerOut);

var dfltColor = coerce('color');
Expand Down Expand Up @@ -105,6 +111,7 @@ module.exports = function handleAxisDefaults(containerIn, containerOut, coerce,
handleTickValueDefaults(containerIn, containerOut, coerce, axType);
handleTickLabelDefaults(containerIn, containerOut, coerce, axType, options);
handleTickMarkDefaults(containerIn, containerOut, coerce, options);
handleCategoryOrderDefaults(containerIn, containerOut, coerce);

var lineColor = coerce2('linecolor', dfltColor),
lineWidth = coerce2('linewidth'),
Expand Down
39 changes: 39 additions & 0 deletions src/plots/cartesian/category_order_defaults.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/**
* Copyright 2012-2016, Plotly, Inc.
* All rights reserved.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

'use strict';

var layoutAttributes = require('./layout_attributes');

module.exports = function handleCategoryOrderDefaults(containerIn, containerOut, coerce) {

if(containerIn.type !== 'category') return;

var validCategories = layoutAttributes.categoryorder.values;

var propercategoryarray = Array.isArray(containerIn.categoryarray) && containerIn.categoryarray.length > 0;

if(validCategories.indexOf(containerIn.categoryorder) === -1 && propercategoryarray) {

// when unspecified or invalid, use the default, unless categoryarray implies 'array'
coerce('categoryorder', 'array'); // promote to 'array'

} else if(containerIn.categoryorder === 'array' && !propercategoryarray) {

// when mode is 'array' but no list is given, revert to default

containerIn.categoryorder = 'trace'; // revert to default
coerce('categoryorder');

} else {

// otherwise use the supplied mode, or the default one if unsupplied or invalid
coerce('categoryorder');

}
};
30 changes: 30 additions & 0 deletions src/plots/cartesian/layout_attributes.js
Original file line number Diff line number Diff line change
Expand Up @@ -465,6 +465,36 @@ module.exports = {
'Only has an effect if `anchor` is set to *free*.'
].join(' ')
},
categoryorder: {
valType: 'enumerated',
values: [
'trace', 'category ascending', 'category descending', 'array'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@cldougl @chriddyp does ⏫ look ok to you?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 for me

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we should include truncated versions of those values as well e.g. 'cat asc', 'cat des' ?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd suggest avoiding muti-word strings. Maybe just ascending and descending?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for me just ascending`descendingdoesn't have a clear relation to _alpha_numeric ordering. I definitely understand the aversion to multi-word strings, but I would vote for more description thanascending\descending`

plus if 'value ascending' \ 'value descending' are going to be implemented eventually

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

but I gotta say, catergoryorder doesn't sound bad at all.

Copy link
Member

@chriddyp chriddyp Apr 14, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can add them as labels to the axes (currently described as ticktext) or use them as data (currently described as x). Given that the axis.type is category, one might think that categories is how you would add data to that axis.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and maybe categoryarray instead of categorylist

@chriddyp we're already using the value 'array' for tickmode.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can add them as labels to the axes

More like: categories are added automatically. Categories have no effect on ticktext.

Copy link
Contributor

@etpinard etpinard Apr 14, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given that the axis.type is category, one might think that categories is how you would add data to that axis.

Great point 👍

That's why I'm thinking that categoryorder might be the winner here.

/*, 'value ascending', 'value descending'*/ // value ascending / descending to be implemented later
],
dflt: 'trace',
role: 'info',
description: [
'Specifies the ordering logic for the case of categorical variables.',
'By default, plotly uses *trace*, which specifies the order that is present in the data supplied.',
'Set `categoryorder` to *category ascending* or *category descending* if order should be determined by',
'the alphanumerical order of the category names.',
/*'Set `categoryorder` to *value ascending* or *value descending* if order should be determined by the',
'numerical order of the values.',*/ // // value ascending / descending to be implemented later
'Set `categoryorder` to *array* to derive the ordering from the attribute `categoryarray`. If a category',
'is not found in the `categoryarray` array, the sorting behavior for that attribute will be identical to',
'the *trace* mode. The unspecified categories will follow the categories in `categoryarray`.'
].join(' ')
},
categoryarray: {
valType: 'data_array',
role: 'info',
description: [
'Sets the order in which categories on this axis appear.',
'Only has an effect if `categoryorder` is set to *array*.',
'Used with `categoryorder`.'
].join(' ')
},


_deprecated: {
autotick: {
Expand Down
77 changes: 77 additions & 0 deletions src/plots/cartesian/ordered_categories.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/**
* Copyright 2012-2016, Plotly, Inc.
* All rights reserved.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/


'use strict';

var d3 = require('d3');

// flattenUniqueSort :: String -> Function -> [[String]] -> [String]
function flattenUniqueSort(axisLetter, sortFunction, data) {

// Bisection based insertion sort of distinct values for logarithmic time complexity.
// Can't use a hashmap, which is O(1), because ES5 maps coerce keys to strings. If it ever becomes a bottleneck,
// code can be separated: a hashmap (JS object) based version if all values encountered are strings; and
// downgrading to this O(log(n)) array on the first encounter of a non-string value.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great comment. Thanks!


var categoryArray = [];

var traceLines = data.map(function(d) {return d[axisLetter];});

var i, j, tracePoints, category, insertionIndex;

var bisector = d3.bisector(sortFunction).left;

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

tracePoints = traceLines[i];

for(j = 0; j < tracePoints.length; j++) {

category = tracePoints[j];

// skip loop: ignore null and undefined categories
if(category === null || category === undefined) continue;

insertionIndex = bisector(categoryArray, category);

// skip loop on already encountered values
if(insertionIndex < categoryArray.length - 1 && categoryArray[insertionIndex] === category) continue;

// insert value
categoryArray.splice(insertionIndex, 0, category);
}
}

return categoryArray;
}


/**
* This pure function returns the ordered categories for specified axisLetter, categoryorder, categoryarray and data.
*
* If categoryorder is 'array', the result is a fresh copy of categoryarray, or if unspecified, an empty array.
*
* If categoryorder is 'category ascending' or 'category descending', the result is an array of ascending or descending
* order of the unique categories encountered in the data for specified axisLetter.
*
* See cartesian/layout_attributes.js for the definition of categoryorder and categoryarray
*
*/

// orderedCategories :: String -> String -> [String] -> [[String]] -> [String]
module.exports = function orderedCategories(axisLetter, categoryorder, categoryarray, data) {

switch(categoryorder) {
case 'array': return Array.isArray(categoryarray) ? categoryarray.slice() : [];
case 'category ascending': return flattenUniqueSort(axisLetter, d3.ascending, data);
case 'category descending': return flattenUniqueSort(axisLetter, d3.descending, data);
case 'trace': return [];
default: return [];
}
};
5 changes: 3 additions & 2 deletions src/plots/cartesian/set_convert.js
Original file line number Diff line number Diff line change
Expand Up @@ -182,8 +182,9 @@ module.exports = function setConvert(ax) {
// encounters them, ie all the categories from the
// first data set, then all the ones from the second
// that aren't in the first etc.
// TODO: sorting options - do the sorting
// progressively here as we insert?
// it is assumed that this function is being invoked in the
// already sorted category order; otherwise there would be
// a disconnect between the array and the index returned

if(v !== null && v !== undefined && ax._categories.indexOf(v) === -1) {
ax._categories.push(v);
Expand Down
2 changes: 2 additions & 0 deletions src/plots/gl3d/layout/axis_attributes.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ module.exports = {
description: 'Sets whether or not this axis is labeled'
},
color: axesAttrs.color,
categoryorder: axesAttrs.categoryorder,
categoryarray: axesAttrs.categoryarray,
title: axesAttrs.title,
titlefont: axesAttrs.titlefont,
type: axesAttrs.type,
Expand Down
Binary file added test/image/baselines/axes_category_ascending.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added test/image/baselines/axes_category_descending.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
13 changes: 13 additions & 0 deletions test/image/mocks/axes_category_ascending.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"data": [{
"x": ["c","a","e","b","d"],
"y": [15,11,12,13,14]}
],
"layout": {
"xaxis": {
"title": "category ascending",
"type": "category",
"categoryorder": "category ascending",
"categoryarray": ["y","b","x","a","d","z","e","c", "q", "k"]
}}
}
46 changes: 46 additions & 0 deletions test/image/mocks/axes_category_categoryarray.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
{
"data": [
{
"x": [
1,
2,
null,
4,
5
],
"y": [
1,
2,
3,
4,
5
],
"connectgaps": false,
"uid": "8ac13a"
}
],
"layout": {
"title": "categoryarray",
"xaxis": {
"type": "category",
"range": [
-0.18336673346693386,
3.1833667334669338
],
"autorange": true,
"categoryorder": "array",
"categoryarray": [2,4,5,1]
},
"yaxis": {
"type": "linear",
"range": [
0.7070063694267517,
5.292993630573249
],
"autorange": true
},
"height": 450,
"width": 1000,
"autosize": true
}
}
13 changes: 13 additions & 0 deletions test/image/mocks/axes_category_categoryarray_truncated_tails.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"data": [{
"x": ["c","a","e","b","d"],
"y": [15,11,12,13,14]}
],
"layout": {
"title": "categoryarray with truncated tails (y, q, k not plotted)",
"xaxis": {
"type": "category",
"categoryorder": "array",
"categoryarray": ["y","b","x","a","d","z","e","c", "q", "k"]
}}
}
41 changes: 41 additions & 0 deletions test/image/mocks/axes_category_descending.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
{
"data": [
{
"x": [
5,
1,
3,
2,
4
],
"y": [
1,
2,
3,
4,
5
],
"connectgaps": false,
"uid": "8ac13a"
}
],
"layout": {
"title": "category descending",
"xaxis": {
"type": "category",
"categoryorder": "category descending",
"categoryarray": [2,4,5,1]
},
"yaxis": {
"type": "linear",
"range": [
0.7070063694267517,
5.292993630573249
],
"autorange": true
},
"height": 450,
"width": 1000,
"autosize": true
}
}
41 changes: 41 additions & 0 deletions test/image/mocks/axes_category_descending_with_gaps.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
{
"data": [
{
"x": [
5,
null,
3,
2,
4
],
"y": [
1,
2,
3,
null,
5
],
"connectgaps": false,
"uid": "8ac13a"
}
],
"layout": {
"title": "category descending",
"xaxis": {
"type": "category",
"categoryorder": "category descending",
"categoryarray": [2,4,5,1]
},
"yaxis": {
"type": "linear",
"range": [
0.7070063694267517,
5.292993630573249
],
"autorange": true
},
"height": 450,
"width": 1000,
"autosize": true
}
}
Loading