Skip to content

189 ordinal scale domain item ordering #1

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

Open
wants to merge 42 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 34 commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
87ab745
Reifying current default ordering logic with a passing test case and …
monfera Mar 25, 2016
a547482
Ensure that null / undefined removal is in place, even in the presenc…
monfera Mar 25, 2016
69bf832
Ensure that no errors arise from the possibility of broader order spe…
monfera Mar 25, 2016
3fd3cc0
Ensure that it adheres to the categories array even if it doesn't com…
monfera Mar 25, 2016
e2977c5
Adding attribute definitions: categorymode and categories; simplifyin…
monfera Mar 26, 2016
62c4ac5
Renaming of 'categories' to 'categorylist' (it was explicitly deleted…
monfera Mar 27, 2016
85d60d6
Minimal commit to demonstrate the working of categorymode = 'array'
monfera Mar 29, 2016
f6c40b6
Minimal change for implementing 'category ascending' and 'category de…
monfera Mar 31, 2016
e4f2167
factored out orderedCategories into a separate function
monfera Mar 31, 2016
03643fe
factored out orderedCategories into a separate function
monfera Mar 31, 2016
9c366ce
lint orderedCategories
monfera Mar 31, 2016
f626b08
#189 role: info
monfera Apr 7, 2016
2f939af
#189 updating preexisting test cases in line with PR feedback (if the…
monfera Apr 7, 2016
ce35e8b
#189 updating preexisting test cases so that order itself is checked;…
monfera Apr 7, 2016
262c747
#189 updating preexisting test cases: further updates to proper axis …
monfera Apr 7, 2016
5f33a7b
#189 updating preexisting test cases: further updates to proper axis …
monfera Apr 7, 2016
f4214cb
#189 adding a path for when categorymode is not in ['array', 'categor…
monfera Apr 8, 2016
cbc98fd
#189 updating test case with non-utilized categorylist elements at be…
monfera Apr 8, 2016
a806ca2
#189 test case with null value for a category we just want for an axi…
monfera Apr 8, 2016
5ad6b5c
#189 baseline test case based on codepen pointed to by @etpinard
monfera Apr 8, 2016
82076f2
#189 commented out categorymode value ascending/descending as it's no…
monfera Apr 9, 2016
6fb2871
#189 adding test cases for trace lines with mutually exclusive catego…
monfera Apr 9, 2016
616121b
#189 adding test cases for trace lines with partially overlapping, fu…
monfera Apr 9, 2016
89794c6
#189 adding test cases for combinations of category ordering and stac…
monfera Apr 9, 2016
849978e
#189 test case linting
monfera Apr 9, 2016
d6fad10
#189 adding missing docs and reducing assumptions
monfera Apr 9, 2016
7c355b8
#189 adding actual DOM axis tick order tests for a couple of less tri…
monfera Apr 9, 2016
8a2292c
#189 rewriting cateogory sorter to O(1) + one sort call
monfera Apr 9, 2016
9b01bcc
#189 rewriting cateogory sorter: extract out logic
monfera Apr 9, 2016
010451b
#189 rewriting cateogory sorter: switching to switch
monfera Apr 9, 2016
6c9d0b5
#189 rewriting cateogory sorter: misc. improvements
monfera Apr 9, 2016
166aeb4
#189 renaming for uniformity (predominantly createGraphDiv() seems to…
monfera Apr 12, 2016
e472e14
#189 initial round of coercions with test cases
monfera Apr 12, 2016
d16fe6b
#189 additional tests for categorymode coercions
monfera Apr 12, 2016
0314f37
#189 comment fix
monfera Apr 12, 2016
043ac1b
#189 PR feedback and linting
monfera Apr 12, 2016
b98bd65
#189 image based regression test JSONs
monfera Apr 12, 2016
e007f73
#189 reworking the order code because it converted numbers to strings…
monfera Apr 12, 2016
34a5054
#189 adding image based tests
monfera Apr 12, 2016
c6d44d9
#189 comment update
monfera Apr 12, 2016
44a0c3d
#189 switching from O(n) to O(log(N)) complexity unique insertion sort
monfera Apr 12, 2016
613fda3
#189 adding axis attributes to 3D plots
monfera Apr 12, 2016
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/plot_api/plot_api.js
Original file line number Diff line number Diff line change
Expand Up @@ -851,10 +851,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();
Copy link

Choose a reason for hiding this comment

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

nice find.

}

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 @@ -17,7 +17,9 @@ var Plots = require('../plots');
var layoutAttributes = require('./layout_attributes');
var handleTickValueDefaults = require('./tick_value_defaults');
var handleTickDefaults = require('./tick_defaults');
var handleCategoryModeDefaults = require('./category_mode_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 @@ -64,6 +66,10 @@ module.exports = function handleAxisDefaults(containerIn, containerOut, coerce,
}
}

containerOut._initialCategories = axType === 'category' ?
orderedCategories(letter, containerIn.categorymode, containerIn.categorylist, options.data) :
Copy link

Choose a reason for hiding this comment

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

We'll need to coerce categorymode and cateogrylist before sending them to orderedCategories.

Coercion is done in this scope by the shortcut coerce function defined in plots/cartesian/layout_defaults.js. The shortcut coerce function is wrapper around Lib.coerce which applies coercion rules based on the type of the user input. For example, an enumerated attribute can only be set to the possible values listed in the attributes definitions, if not, it gets coerced to the default value.

Moreover, we'll need to add logic around categorymode and categorylist. For instance, at the moment, categorylist only has an effect if categorymode is set to array. Meaning that if a user specifies categorylist without categorymode, the defaults categories will be shown even though it is clear that the user wanted custom categories. So, we can be smarter by overriding the catogorymode default if categoylist is an array, similar to what's being done currently for tickmode and tickvals in plots/cartesian/tick_value_defaults.js (see tests here for more info).

We may want to look up lib/nested_properly.js while familiarizing yourself with Lib.coerce (see its test cases here).

Don't hesitate to ask me any questions about our coercion framework. Surprising, it may the trickiest part to understand about plotly.js.

Copy link
Owner Author

Choose a reason for hiding this comment

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

@etpinard Looking into this now as it appears to be the last piece in this CR. My test cases will need an update once coercion is in place, to switch to the more data-driven default or override behavior.

Copy link
Owner Author

Choose a reason for hiding this comment

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

@etpinard tl;dr blocking question for you at the end.

As an aid to thinking about goals, here's a summary of what coercions I'm planning:

  1. If categorymode is not one of those in the attributes definition, it'll be coerced to the default trace value.
  2. No matter what categorymode is specified or coerced in item#1, if a categorylist is provided and it is an array, categorymode will be coerced into array.

... well it looks like the two things you listed :-) My original take was that categorymode, as a switch, drives the show in an unambiguous way, and the user can flip-flop among modes without worrying about removing and readding (or commenting/uncommenting) the categorylist property. Even now I'm vague about the case where categorymode is category ascending and categorylist is also given. Moreover, it renders the categorymode === 'array' redundant (documentational at best) since the presence of categorylist takes precedence. Maybe we should even do away with categorymode === 'array', making categorymode and categorylist mutually exclusive?

Taking it one step further, shouldn't we just have one property, e.g. called categoryorder that can be category ascending, category descending or an array?

I'm now plunging into the basic coercion so we don't have an unexpected enumerative value but your input to the above question would be useful. It's not a big deal to unify the two properties and at least technically, it would be less ambiguous.

Choose a reason for hiding this comment

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

My original take was that categorymode, as a switch, drives the show in an unambiguous way, and the user can flip-flop among modes without worrying about removing and readding (or commenting/uncommenting) the categorylist property.

... and you would be correct.

What smart coercion is trying to remove redundancy. For example,

var layout: {
  xaxis: {
    categorylist: ['apples', 'bananas', 'clementines']
  }
};

should be considered the same as:

var layout: {
  xaxis: {
    categorymode: 'array',  // redundant !!!
    categorylist: ['apples', 'bananas', 'clementines']
  }
};

But,

var layout: {
  xaxis: {
    categorymode: 'category descending',  // still used as a switch 
    categorylist: ['apples', 'bananas', 'clementines']  // not used in graph
  }
};

Copy link
Owner Author

Choose a reason for hiding this comment

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

@etpinard Thanks!

Choose a reason for hiding this comment

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

and @monfera I should add, patterns like ⏫ are common in plotly.js.

Don't try to reinvent the wheel. As mentioned before, this block should serve as a good guide for how categorymode and categorylist should interact.

[];

setConvert(containerOut);

coerce('title', defaultTitle);
Expand Down Expand Up @@ -91,6 +97,7 @@ module.exports = function handleAxisDefaults(containerIn, containerOut, coerce,

handleTickValueDefaults(containerIn, containerOut, coerce, axType);
handleTickDefaults(containerIn, containerOut, coerce, axType, options);
handleCategoryModeDefaults(containerIn, containerOut, coerce);

var lineColor = Lib.coerce2(containerIn, containerOut, layoutAttributes, 'linecolor'),
lineWidth = Lib.coerce2(containerIn, containerOut, layoutAttributes, 'linewidth'),
Expand Down
39 changes: 39 additions & 0 deletions src/plots/cartesian/category_mode_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';

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

if(containerIn.type === 'category') {

Choose a reason for hiding this comment

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

we like early returns in case like these.

So,

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

and flatten ⏬


var validCategories = ['trace', 'category ascending', 'category descending', 'array'];

Choose a reason for hiding this comment

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

we should require('./layout_attrbitues') into this file and have:

var validCategories = layoutAttributes.categorymode.values;

to keep things DRY 🌴


var properCategoryList = Array.isArray(containerIn.categorylist) && containerIn.categorylist.length > 0;

if(validCategories.indexOf(containerIn.categorymode) === -1 && properCategoryList) {

Choose a reason for hiding this comment

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

👍

Coercing while keeping track of whether the user input in containerIn is valid was the idea behind Lib.coerce2.

Lib.coerce2 is not very well implemented at the moment, and I want to change its API to make more convenient.

So, no action needed here. This logic works great and is well tested.


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

} else if(containerIn.categorymode === 'array' && !properCategoryList) {

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

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

} else {

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

}
}
};
Copy link
Owner Author

Choose a reason for hiding this comment

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

@etpinard Is this a reasonable cut at the coercion?

Choose a reason for hiding this comment

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

nice job.

30 changes: 30 additions & 0 deletions src/plots/cartesian/layout_attributes.js
Original file line number Diff line number Diff line change
Expand Up @@ -446,6 +446,36 @@ module.exports = {
'Only has an effect if `anchor` is set to *free*.'
].join(' ')
},
categorymode: {
valType: 'enumerated',
values: [
'trace', 'category ascending', 'category descending', 'array'
/*, '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 `categorymode` to *category ascending* or *category descending* if order should be determined by',
'the alphanumerical order of the category names.',
/*'Set `categorymode` 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 `categorymode` to *array* to derive the ordering from the attribute `categorylist`. If a category',
'is not found in the `categorylist` array, the sorting behavior for that attribute will be identical to',
'the *trace* mode. The unspecified categories will follow the categories in `categorylist`.'
].join(' ')
},
categorylist: {
valType: 'data_array',
role: 'info',
description: [
'Sets the order in which categories on this axis appear.',
'Only has an effect if `categorymode` is set to *array*.',
'Used with `categorymode`.'
].join(' ')
},


_deprecated: {
autotick: {
Expand Down
59 changes: 59 additions & 0 deletions src/plots/cartesian/ordered_categories.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/**
* 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');

// flattenUnique :: String -> [[String]] -> Object
function flattenUnique(axisLetter, data) {
var traceLines = data.map(function(d) {return d[axisLetter];});
var categoryMap = {}; // hashmap is O(1);
var i, j, tracePoints, category;
for(i = 0; i < traceLines.length; i++) {
tracePoints = traceLines[i];
for(j = 0; j < tracePoints.length; j++) {
category = tracePoints[j];
if(!categoryMap[category]) {
categoryMap[category] = true;
}
}
}
return categoryMap;
}

// flattenUniqueSort :: String -> Function -> [[String]] -> [String]
function flattenUniqueSort(axisLetter, sortFunction, data) {
return Object.keys(flattenUnique(axisLetter, data)).sort(sortFunction);
}

Copy link
Owner Author

Choose a reason for hiding this comment

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

@etpinard I quite rewrote the ordering logic this evening on initial suspicion that image based cases may fail in presence of a large number of points, because of possible slowness due to the O(N) complexity. I switched to bisection. While it didn't make a difference, better scalability is good anyway. As it changed a good bit, it's useful if you know about it.

Choose a reason for hiding this comment

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

looks good 👍


/**
* This pure function returns the ordered categories for specified axisLetter, categorymode, categorylist and data.
*
* If categorymode is 'array', the result is a fresh copy of categorylist, or if unspecified, an empty array.
*
* If categorymode 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 categorymode and categorylist
*
*/

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

switch(categorymode) {
case 'array': return Array.isArray(categorylist) ? categorylist : [];
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

Choose a reason for hiding this comment

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

@monfera can you elaborate on this?

What else have you tried?

Copy link
Owner Author

Choose a reason for hiding this comment

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

@etpinard I tried things like this:

  1. I've first tried the change inside the suggested place (https://github.com/plotly/plotly.js/blob/bbb8205903cdd34ae019d5cd6a2c197e9a2550e7/src/plots/cartesian/set_convert.js#L179-L194): instead of pushing into the array, I inserted it to ensure orderedness (insertion sort). More accurately, for simplicity, I just sorted upon each insertion to try before doing too much work (paid attention to the fact that [].sort mutates the array).
  2. Then I also quickly tried doing the sort only after the loop that invokes ax.d2c but the issue is, by that point we already have the indices, and a sort renders them obsolete.

Then I spent a bit of time better understanding concepts, the data flow and the various ways it can be invoked (e.g. plotting multiple lines; overplotting onto an already existing plot etc). Learnt that currently, the categorical X axis ordering is totally driven by the order in which the point tuples arrive and made sample plots. Which was the point at which I gave up on this point for modification and went for the lower hanging fruit of array. The other options will, I think, be more intrusive to the data flow.

Copy link
Owner Author

Choose a reason for hiding this comment

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

@etpinard maybe an example is helpful: The first vector has X cat. values ['a', 'c', 'd']. This is already sorted. Then a subsequent series is plotted in the same chart, with values for ['a', 'b', 'c', 'd']. By this point, the function has returned indices such as 0, 1, 2 for ['a', 'c', 'd'], respectively. Then in the next series comes a new point 'b' correctly inserted at index 1 but the previously returned indices will become invalid and the plot will come out nonsensical (more precisely, having checked the plot, the way it came out was consistent with this mechanics).

Copy link
Owner Author

Choose a reason for hiding this comment

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

@etpinard I also considered doing a sorting a lot earlier (more upstream), but it would have issues: 1) the code should possibly reorder only after it's established that it's a categorical axis (it can be user specified or determined heuristically); 2) by that point there are lots of places where _fullData etc. are persisted on objects in the trace order; 3) I believe it would be wrong to change the trace order just for this CR because of the snail trail example and the general possibility that the user does not want to have plotly change the user input order (which is currently implicitly taken as the trace order).

So my current thought is that we need to do the axis tick sorting downstream, nearer the point which is responsible for the d3 data binding order for the axis ticks, and we have to accommodate for the possibility that new lines introduce new points that have to be inserted in lines that have already been added previously.

Choose a reason for hiding this comment

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

@monfera Thanks for this very detailed overview.

You've got me convinced, ax._categories can't be sorted from inside the d2c routine.

We need to fill in ax._categories earlier. I think within makeCalcdata makes the most sense at the moment.

Choose a reason for hiding this comment

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

Filling in ax._categories within the default step would definitely work (albeit with a slight performance hit).

We already need to loop all traces to check for box plot irregularities here, so maybe you could fill in ax._categories within the same loop.

Copy link
Owner Author

Choose a reason for hiding this comment

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

@etpinard I'll look into it, thanks! Unrelated: I learnt that date axes take ISO strings "2016-03-29" as values. When I expressly indicate axis type = 'category' it behaves as categories and the axis ticks get rendered as plaintext "2016-03-29" rather than nicely formatted date. So I'm not yet grounded in the approach for coercion which was step #2 in your original comment.

Copy link
Owner Author

Choose a reason for hiding this comment

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

@etpinard I'll look into it more closely today, but on an initial look, the suggested place isn't as trivial (to me) and I'd put it slightly after this point, because

  • setAutoType bails if ax.type!=='-' and at the only place it's called from, there is an identical (redundant) check
  • the loop in question depends on isBoxWithoutPositionCoords returning true
  • the inside of the loop (now) is solely dedicated to building up the boxPositions
  • if axis type 'category' isn't explicitly specified, it won't be known until the subsequent call to autoType() at the end

Therefore I'm planning to put the logic after having returned from setAutoType; the earliest point seems to be just before the call to setConvert.

This way, we wouldn't reuse the suggested loop, but the loop over the traces probably doesn't have a measurable performance impact anyway (I'm assuming there usually aren't thousands or 10ks of trace lines).

I'll do some more tests to ensure I'm not overlooking something; just wanted to share current thinking.

Choose a reason for hiding this comment

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

Anywhere, in plots/cartesian/axis_defaults.js is fine at this stage. We'll fine tune the location for performance once the functionality is in a working state.

Copy link
Owner Author

Choose a reason for hiding this comment

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

@etpinard Thanks for the quick note, I'll go ahead.


if(v !== null && v !== undefined && ax._categories.indexOf(v) === -1) {
ax._categories.push(v);
Expand Down
130 changes: 126 additions & 4 deletions test/jasmine/tests/axes_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ var Color = require('@src/components/color');
var handleTickValueDefaults = require('@src/plots/cartesian/tick_value_defaults');
var Axes = PlotlyInternal.Axes;

var createGraph = require('../assets/create_graph_div');
var destroyGraph = require('../assets/destroy_graph_div');
var createGraphDiv = require('../assets/create_graph_div');
var destroyGraphDiv = require('../assets/destroy_graph_div');

Copy link
Owner Author

Choose a reason for hiding this comment

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

@etpinard I appended 'Div' to these, to be in sync with how it's usually called.

Choose a reason for hiding this comment

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

nicely done.


describe('Test axes', function() {
Expand Down Expand Up @@ -321,15 +321,137 @@ describe('Test axes', function() {
});
});

describe('categorymode', function() {

var gd;

beforeEach(function() {
gd = createGraphDiv();
});

afterEach(destroyGraphDiv);

describe('setting, or not setting categorymode if it is not explicitly declared', function() {

it('should set categorymode to default if categorymode and categorylist are not supplied', function() {
PlotlyInternal.plot(gd, [{x: ['c','a','e','b','d'], y: [15,11,12,13,14]}], {xaxis: {type: 'category'}});
expect(gd._fullLayout.xaxis.categorymode).toBe('trace');
});

it('should set categorymode to default even if type is not set to category explicitly', function() {
PlotlyInternal.plot(gd, [{x: ['c','a','e','b','d'], y: [15,11,12,13,14]}]);
expect(gd._fullLayout.xaxis.categorymode).toBe('trace');
});

it('should NOT set categorymode to default if type is not category', function() {
PlotlyInternal.plot(gd, [{x: ['c','a','e','b','d'], y: [15,11,12,13,14]}]);
expect(gd._fullLayout.yaxis.categorymode).toBe(undefined);
});

it('should set categorymode to default if type is overridden to be category', function() {
PlotlyInternal.plot(gd, [{x: [1,2,3,4,5], y: [15,11,12,13,14]}], {yaxis: {type: 'category'}});
expect(gd._fullLayout.xaxis.categorymode).toBe(undefined);
expect(gd._fullLayout.yaxis.categorymode).toBe('trace');
});

});

describe('setting categorymode to "array"', function() {

it('should leave categorymode on "array" if it is supplied', function() {
PlotlyInternal.plot(gd, [{x: ['c','a','e','b','d'], y: [15,11,12,13,14]}], {
xaxis: {type: 'category', categorymode: 'array', categorylist: ['b','a','d','e','c']}
});
expect(gd._fullLayout.xaxis.categorymode).toBe('array');
});

it('should switch categorymode on "array" if it is not supplied but categorylist is supplied', function() {
PlotlyInternal.plot(gd, [{x: ['c','a','e','b','d'], y: [15,11,12,13,14]}], {
xaxis: {type: 'category', categorylist: ['b','a','d','e','c']}
});
expect(gd._fullLayout.xaxis.categorymode).toBe('array');
});

it('should revert categorymode to "trace" if "array" is supplied but there is no list', function() {
PlotlyInternal.plot(gd, [{x: ['c','a','e','b','d'], y: [15,11,12,13,14]}], {
xaxis: {type: 'category', categorymode: 'array'}
});
expect(gd._fullLayout.xaxis.categorymode).toBe('trace');
});

});

describe('do not set categorymode to "array" if list exists but empty', function() {

it('should switch categorymode to default if list is not supplied', function() {
PlotlyInternal.plot(gd, [{x: ['c','a','e','b','d'], y: [15,11,12,13,14]}], {
xaxis: {type: 'category', categorymode: 'array', categorylist: []}
});
expect(gd._fullLayout.xaxis.categorymode).toBe('trace');
});

it('should not switch categorymode on "array" if categorylist is supplied but empty', function() {
PlotlyInternal.plot(gd, [{x: ['c','a','e','b','d'], y: [15,11,12,13,14]}], {
xaxis: {type: 'category', categorylist: []}
});
expect(gd._fullLayout.xaxis.categorymode).toBe('trace');
});
});

describe('do NOT set categorymode to "array" if it has some other proper value', function() {

it('should use specified categorymode if it is supplied even if categorylist exists', function() {
PlotlyInternal.plot(gd, [{x: ['c','a','e','b','d'], y: [15,11,12,13,14]}], {
xaxis: {type: 'category', categorymode: 'trace', categorylist: ['b','a','d','e','c']}
});
expect(gd._fullLayout.xaxis.categorymode).toBe('trace');
});

it('should use specified categorymode if it is supplied even if categorylist exists', function() {
PlotlyInternal.plot(gd, [{x: ['c','a','e','b','d'], y: [15,11,12,13,14]}], {
xaxis: {type: 'category', categorymode: 'category ascending', categorylist: ['b','a','d','e','c']}
});
expect(gd._fullLayout.xaxis.categorymode).toBe('category ascending');
});

it('should use specified categorymode if it is supplied even if categorylist exists', function() {
PlotlyInternal.plot(gd, [{x: ['c','a','e','b','d'], y: [15,11,12,13,14]}], {
xaxis: {type: 'category', categorymode: 'category descending', categorylist: ['b','a','d','e','c']}
});
expect(gd._fullLayout.xaxis.categorymode).toBe('category descending');
});

});

describe('setting categorymode to the default if the value is unexpected', function() {

it('should switch categorymode to "trace" if mode is supplied but invalid', function() {
PlotlyInternal.plot(gd, [{x: ['c','a','e','b','d'], y: [15,11,12,13,14]}], {
xaxis: {type: 'category', categorymode: 'invalid value'}
});
expect(gd._fullLayout.xaxis.categorymode).toBe('trace');
});

it('should switch categorymode to "array" if mode is supplied but invalid and list is supplied', function() {
PlotlyInternal.plot(gd, [{x: ['c','a','e','b','d'], y: [15,11,12,13,14]}], {
xaxis: {type: 'category', categorymode: 'invalid value', categorylist: ['b','a','d','e','c']}
});
expect(gd._fullLayout.xaxis.categorymode).toBe('array');
});

});

});

Copy link
Owner Author

Choose a reason for hiding this comment

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

@etpinard May I ask you to take a look at these test cases if they seem to be in line with what you wrote about categorymode coercion earlier.

Choose a reason for hiding this comment

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

Great. All the test cases you added (along with d16fe6b) look great!

describe('handleTickDefaults', function() {
var data = [{ x: [1,2,3], y: [3,4,5] }],
gd;

beforeEach(function() {
gd = createGraph();
gd = createGraphDiv();
});

afterEach(destroyGraph);
afterEach(destroyGraphDiv);

it('should set defaults on bad inputs', function() {
var layout = {
Expand Down
Loading