From 87ab7452d47a8c2aa911a169af25b79469df0618 Mon Sep 17 00:00:00 2001 From: Robert Monfera Date: Fri, 25 Mar 2016 13:02:30 +0100 Subject: [PATCH 01/42] Reifying current default ordering logic with a passing test case and adding new usages with (currently) failing test cases --- test/jasmine/tests/calcdata_test.js | 151 ++++++++++++++++++++++++++-- 1 file changed, 145 insertions(+), 6 deletions(-) diff --git a/test/jasmine/tests/calcdata_test.js b/test/jasmine/tests/calcdata_test.js index df134e774cc..642b1656954 100644 --- a/test/jasmine/tests/calcdata_test.js +++ b/test/jasmine/tests/calcdata_test.js @@ -4,15 +4,16 @@ var createGraphDiv = require('../assets/create_graph_div'); var destroyGraphDiv = require('../assets/destroy_graph_div'); describe('calculated data and points', function() { - describe('connectGaps', function() { - var gd; + var gd; - beforeEach(function() { - gd = createGraphDiv(); - }); + beforeEach(function() { + gd = createGraphDiv(); + }); - afterEach(destroyGraphDiv); + afterEach(destroyGraphDiv); + + describe('connectGaps', function() { it('should exclude null and undefined points when false', function() { Plotly.plot(gd, [{ x: [1,2,3,undefined,5], y: [1,null,3,4,5]}], {}); @@ -28,4 +29,142 @@ describe('calculated data and points', function() { expect(gd.calcdata[0][3]).toEqual({ x: false, y: false}); }); }); + + xdescribe('category ordering', function() { + + describe('default category ordering reified', function() { + + it('should output categories in the given order by default', function() { + + Plotly.plot(gd, [{x: ['c','a','e','b','d'], y: [15,11,12,13,14]}], { xaxis: { + type: 'category' + }}); + + expect(gd.calcdata[0][0].y).toEqual(15); + expect(gd.calcdata[0][1].y).toEqual(11); + expect(gd.calcdata[0][2].y).toEqual(12); + expect(gd.calcdata[0][3].y).toEqual(13); + expect(gd.calcdata[0][4].y).toEqual(14); + }); + + it('should output categories in the given order if trace-order is explicitly specified', function() { + + Plotly.plot(gd, [{x: ['c','a','e','b','d'], y: [15,11,12,13,14]}], { xaxis: { + type: 'category', + categorymode: 'trace-order' + // Wouldn't it be preferred to supply a function and plotly would have several functions like this? + // E.g. it's easier for symbol completion (whereas there's no symbol completion on string config) + // See arguments from Mike Bostock, highlighted in medium green here: + // https://medium.com/@mbostock/what-makes-software-good-943557f8a488#eef9 + // Plus if it's a function, then users can roll their own. + // + // Also, if axis tick order is made configurable, shouldn't we make trace order configurable? + // Trace order as in, if a line or curve is drawn through points, what's the trace sequence. + // These are two orthogonal concepts. In this round, I'm assuming that the trace order is implied + // by the order the {x,y} arrays are specified. + }}); + + expect(gd.calcdata[0][0].y).toEqual(15); + expect(gd.calcdata[0][1].y).toEqual(11); + expect(gd.calcdata[0][2].y).toEqual(12); + expect(gd.calcdata[0][3].y).toEqual(13); + expect(gd.calcdata[0][4].y).toEqual(14); + }); + }); + + describe('domain alphanumerical category ordering', function() { + + it('should output categories in ascending domain alphanumerical order', function() { + + Plotly.plot(gd, [{x: ['c','a','e','b','d'], y: [15,11,12,13,14]}], { xaxis: { + type: 'category', + categorymode: 'domain-alphanumerical-ascending' + }}); + + expect(gd.calcdata[0][0].y).toEqual(11); + expect(gd.calcdata[0][1].y).toEqual(13); + expect(gd.calcdata[0][2].y).toEqual(15); + expect(gd.calcdata[0][3].y).toEqual(14); + expect(gd.calcdata[0][4].y).toEqual(12); + }); + + it('should output categories in descending domain alphanumerical order', function() { + + Plotly.plot(gd, [{x: ['c','a','e','b','d'], y: [15,11,12,13,14]}], { xaxis: { + type: 'category', + categorymode: 'domain-alphanumerical-descending' + }}); + + expect(gd.calcdata[0][0].y).toEqual(12); + expect(gd.calcdata[0][1].y).toEqual(14); + expect(gd.calcdata[0][2].y).toEqual(15); + expect(gd.calcdata[0][3].y).toEqual(13); + expect(gd.calcdata[0][4].y).toEqual(11); + }); + + it('should output categories in categorymode order even if category array is defined', function() { + + Plotly.plot(gd, [{x: ['c','a','e','b','d'], y: [15,11,12,13,14]}], { xaxis: { + type: 'category', + categorymode: 'domain-alphanumerical-ascending', + categories: ['b','a','d','e','c'] // These must be ignored. Alternative: error? + }}); + + expect(gd.calcdata[0][0].y).toEqual(11); + expect(gd.calcdata[0][1].y).toEqual(13); + expect(gd.calcdata[0][2].y).toEqual(15); + expect(gd.calcdata[0][3].y).toEqual(14); + expect(gd.calcdata[0][4].y).toEqual(12); + }); + }); + + describe('codomain numerical category ordering', function() { + + it('should output categories in ascending codomain numerical order', function() { + + Plotly.plot(gd, [{x: ['c','a','e','b','d'], y: [15,11,12,13,14]}], { xaxis: { + type: 'category', + categorymode: 'codomain-numerical-ascending' + }}); + + expect(gd.calcdata[0][0].y).toEqual(11); + expect(gd.calcdata[0][1].y).toEqual(12); + expect(gd.calcdata[0][2].y).toEqual(13); + expect(gd.calcdata[0][3].y).toEqual(14); + expect(gd.calcdata[0][4].y).toEqual(15); + }); + + it('should output categories in descending codomain numerical order', function() { + + Plotly.plot(gd, [{x: ['c','a','e','b','d'], y: [15,11,12,13,14]}], { xaxis: { + type: 'category', + categorymode: 'codomain-numerical-descending' + }}); + + expect(gd.calcdata[0][0].y).toEqual(15); + expect(gd.calcdata[0][1].y).toEqual(14); + expect(gd.calcdata[0][2].y).toEqual(13); + expect(gd.calcdata[0][3].y).toEqual(12); + expect(gd.calcdata[0][4].y).toEqual(11); + }); + }); + + describe('explicit category ordering', function() { + + it('should output categories in explicitly supplied order, independent of trace order', function() { + + Plotly.plot(gd, [{x: ['c','a','e','b','d'], y: [15,11,12,13,14]}], { xaxis: { + type: 'category', + categorymode: 'explicit', + categories: ['b','a','d','e','c'] + }}); + + expect(gd.calcdata[0][0].y).toEqual(13); + expect(gd.calcdata[0][1].y).toEqual(11); + expect(gd.calcdata[0][2].y).toEqual(14); + expect(gd.calcdata[0][3].y).toEqual(12); + expect(gd.calcdata[0][4].y).toEqual(15); + }); + }); + }); }); From a547482cc109f1411dc97594f18a15cc4e55286e Mon Sep 17 00:00:00 2001 From: Robert Monfera Date: Fri, 25 Mar 2016 13:20:25 +0100 Subject: [PATCH 02/42] Ensure that null / undefined removal is in place, even in the presence of order specification --- test/jasmine/tests/calcdata_test.js | 40 +++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/test/jasmine/tests/calcdata_test.js b/test/jasmine/tests/calcdata_test.js index 642b1656954..2ccd02a8ea3 100644 --- a/test/jasmine/tests/calcdata_test.js +++ b/test/jasmine/tests/calcdata_test.js @@ -116,6 +116,19 @@ describe('calculated data and points', function() { expect(gd.calcdata[0][3].y).toEqual(14); expect(gd.calcdata[0][4].y).toEqual(12); }); + + it('should output categories in ascending domain alphanumerical order, excluding undefined', function() { + + Plotly.plot(gd, [{x: ['c',undefined,'e','b','d'], y: [15,11,12,13,14]}], { xaxis: { + type: 'category', + categorymode: 'domain-alphanumerical-ascending' + }}); + + expect(gd.calcdata[0][0].y).toEqual(11); + expect(gd.calcdata[0][1].y).toEqual(15); + expect(gd.calcdata[0][2].y).toEqual(14); + expect(gd.calcdata[0][3].y).toEqual(12); + }); }); describe('codomain numerical category ordering', function() { @@ -147,6 +160,20 @@ describe('calculated data and points', function() { expect(gd.calcdata[0][3].y).toEqual(12); expect(gd.calcdata[0][4].y).toEqual(11); }); + + it('should output categories in descending codomain numerical order, excluding nulls', function() { + + Plotly.plot(gd, [{x: ['c','a','e','b','d'], y: [15,11,null,13,14]}], { xaxis: { + type: 'category', + categorymode: 'codomain-numerical-descending' + }}); + + expect(gd.calcdata[0][0].y).toEqual(15); + expect(gd.calcdata[0][1].y).toEqual(14); + expect(gd.calcdata[0][2].y).toEqual(12); + expect(gd.calcdata[0][3].y).toEqual(11); + + }); }); describe('explicit category ordering', function() { @@ -165,6 +192,19 @@ describe('calculated data and points', function() { expect(gd.calcdata[0][3].y).toEqual(12); expect(gd.calcdata[0][4].y).toEqual(15); }); + + it('should output categories in explicitly supplied order, independent of trace order, pruned', function() { + + Plotly.plot(gd, [{x: ['c',undefined,'e','b','d'], y: [15,11,12,null,14]}], { xaxis: { + type: 'category', + categorymode: 'explicit', + categories: ['b','a','d','e','c'] + }}); + + expect(gd.calcdata[0][0].y).toEqual(13); + expect(gd.calcdata[0][1].y).toEqual(14); + expect(gd.calcdata[0][2].y).toEqual(15); + }); }); }); }); From 69bf832619b6dea9baa8e5cdeabad35cdf46df04 Mon Sep 17 00:00:00 2001 From: Robert Monfera Date: Fri, 25 Mar 2016 13:24:35 +0100 Subject: [PATCH 03/42] Ensure that no errors arise from the possibility of broader order spec than data coverage --- test/jasmine/tests/calcdata_test.js | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/test/jasmine/tests/calcdata_test.js b/test/jasmine/tests/calcdata_test.js index 2ccd02a8ea3..4c07ca222ea 100644 --- a/test/jasmine/tests/calcdata_test.js +++ b/test/jasmine/tests/calcdata_test.js @@ -205,6 +205,21 @@ describe('calculated data and points', function() { expect(gd.calcdata[0][1].y).toEqual(14); expect(gd.calcdata[0][2].y).toEqual(15); }); + + it('should output categories in explicitly supplied order even if not all categories are present', function() { + + Plotly.plot(gd, [{x: ['c','a','e','b','d'], y: [15,11,12,13,14]}], { xaxis: { + type: 'category', + categorymode: 'explicit', + categories: ['y','b','x','a','d','z','e','c'] + }}); + + expect(gd.calcdata[0][0].y).toEqual(13); + expect(gd.calcdata[0][1].y).toEqual(11); + expect(gd.calcdata[0][2].y).toEqual(14); + expect(gd.calcdata[0][3].y).toEqual(12); + expect(gd.calcdata[0][4].y).toEqual(15); + }); }); }); }); From 3fd3cc0c898b49ed83a5c18a303c13ea31d8ca41 Mon Sep 17 00:00:00 2001 From: Robert Monfera Date: Fri, 25 Mar 2016 13:56:51 +0100 Subject: [PATCH 04/42] Ensure that it adheres to the categories array even if it doesn't completely cover the supplied input data --- test/jasmine/tests/calcdata_test.js | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/test/jasmine/tests/calcdata_test.js b/test/jasmine/tests/calcdata_test.js index 4c07ca222ea..1ba4f14d7d0 100644 --- a/test/jasmine/tests/calcdata_test.js +++ b/test/jasmine/tests/calcdata_test.js @@ -220,6 +220,24 @@ describe('calculated data and points', function() { expect(gd.calcdata[0][3].y).toEqual(12); expect(gd.calcdata[0][4].y).toEqual(15); }); + + it('should output categories in explicitly supplied order first, if not all categories are covered', function() { + + Plotly.plot(gd, [{x: ['c','a','e','b','d'], y: [15,11,12,13,14]}], { xaxis: { + type: 'category', + categorymode: 'explicit', + categories: ['b','a','x','c'] + }}); + + expect(gd.calcdata[0][0].y).toEqual(13); + expect(gd.calcdata[0][1].y).toEqual(11); + expect(gd.calcdata[0][2].y).toEqual(15); + + // The order of the rest is unspecified, no need to check. Alternative: make _both_ categorymode and + // categories effective; categories would take precedence and the remaining items would be sorted + // based on the categorymode. This of course means that the mere presence of categories triggers this + // behavior, rather than an explicit 'explicit' categorymode. + }); }); }); }); From e2977c5c482738be43dd3c82bab2f32ee199078c Mon Sep 17 00:00:00 2001 From: Robert Monfera Date: Sat, 26 Mar 2016 12:08:24 +0100 Subject: [PATCH 05/42] Adding attribute definitions: categorymode and categories; simplifying categorymode values --- src/plots/cartesian/layout_attributes.js | 27 ++++++++++++++++++++++++ test/jasmine/tests/calcdata_test.js | 26 +++++++++++------------ 2 files changed, 40 insertions(+), 13 deletions(-) diff --git a/src/plots/cartesian/layout_attributes.js b/src/plots/cartesian/layout_attributes.js index 8ec877fc6a2..78a0e3feb9e 100644 --- a/src/plots/cartesian/layout_attributes.js +++ b/src/plots/cartesian/layout_attributes.js @@ -446,6 +446,33 @@ module.exports = { 'Only has an effect if `anchor` is set to *free*.' ].join(' ') }, + categorymode: { + valType: 'enumerated', + values: [ + 'trace', 'category ascending', 'category descending', + 'value ascending', 'value descending','array' + ], + dflt: 'trace', + role: 'style', + 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.' + ].join(' ') + }, + categories: { + valType: 'data_array', + role: 'style', + 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: { diff --git a/test/jasmine/tests/calcdata_test.js b/test/jasmine/tests/calcdata_test.js index 1ba4f14d7d0..fce5af7c56a 100644 --- a/test/jasmine/tests/calcdata_test.js +++ b/test/jasmine/tests/calcdata_test.js @@ -47,11 +47,11 @@ describe('calculated data and points', function() { expect(gd.calcdata[0][4].y).toEqual(14); }); - it('should output categories in the given order if trace-order is explicitly specified', function() { + it('should output categories in the given order if `trace` order is explicitly specified', function() { Plotly.plot(gd, [{x: ['c','a','e','b','d'], y: [15,11,12,13,14]}], { xaxis: { type: 'category', - categorymode: 'trace-order' + categorymode: 'trace' // Wouldn't it be preferred to supply a function and plotly would have several functions like this? // E.g. it's easier for symbol completion (whereas there's no symbol completion on string config) // See arguments from Mike Bostock, highlighted in medium green here: @@ -78,7 +78,7 @@ describe('calculated data and points', function() { Plotly.plot(gd, [{x: ['c','a','e','b','d'], y: [15,11,12,13,14]}], { xaxis: { type: 'category', - categorymode: 'domain-alphanumerical-ascending' + categorymode: 'category ascending' }}); expect(gd.calcdata[0][0].y).toEqual(11); @@ -92,7 +92,7 @@ describe('calculated data and points', function() { Plotly.plot(gd, [{x: ['c','a','e','b','d'], y: [15,11,12,13,14]}], { xaxis: { type: 'category', - categorymode: 'domain-alphanumerical-descending' + categorymode: 'category descending' }}); expect(gd.calcdata[0][0].y).toEqual(12); @@ -106,7 +106,7 @@ describe('calculated data and points', function() { Plotly.plot(gd, [{x: ['c','a','e','b','d'], y: [15,11,12,13,14]}], { xaxis: { type: 'category', - categorymode: 'domain-alphanumerical-ascending', + categorymode: 'category ascending', categories: ['b','a','d','e','c'] // These must be ignored. Alternative: error? }}); @@ -121,7 +121,7 @@ describe('calculated data and points', function() { Plotly.plot(gd, [{x: ['c',undefined,'e','b','d'], y: [15,11,12,13,14]}], { xaxis: { type: 'category', - categorymode: 'domain-alphanumerical-ascending' + categorymode: 'category ascending' }}); expect(gd.calcdata[0][0].y).toEqual(11); @@ -137,7 +137,7 @@ describe('calculated data and points', function() { Plotly.plot(gd, [{x: ['c','a','e','b','d'], y: [15,11,12,13,14]}], { xaxis: { type: 'category', - categorymode: 'codomain-numerical-ascending' + categorymode: 'value ascending' }}); expect(gd.calcdata[0][0].y).toEqual(11); @@ -151,7 +151,7 @@ describe('calculated data and points', function() { Plotly.plot(gd, [{x: ['c','a','e','b','d'], y: [15,11,12,13,14]}], { xaxis: { type: 'category', - categorymode: 'codomain-numerical-descending' + categorymode: 'value descending' }}); expect(gd.calcdata[0][0].y).toEqual(15); @@ -165,7 +165,7 @@ describe('calculated data and points', function() { Plotly.plot(gd, [{x: ['c','a','e','b','d'], y: [15,11,null,13,14]}], { xaxis: { type: 'category', - categorymode: 'codomain-numerical-descending' + categorymode: 'value descending' }}); expect(gd.calcdata[0][0].y).toEqual(15); @@ -182,7 +182,7 @@ describe('calculated data and points', function() { Plotly.plot(gd, [{x: ['c','a','e','b','d'], y: [15,11,12,13,14]}], { xaxis: { type: 'category', - categorymode: 'explicit', + categorymode: 'array', categories: ['b','a','d','e','c'] }}); @@ -197,7 +197,7 @@ describe('calculated data and points', function() { Plotly.plot(gd, [{x: ['c',undefined,'e','b','d'], y: [15,11,12,null,14]}], { xaxis: { type: 'category', - categorymode: 'explicit', + categorymode: 'array', categories: ['b','a','d','e','c'] }}); @@ -210,7 +210,7 @@ describe('calculated data and points', function() { Plotly.plot(gd, [{x: ['c','a','e','b','d'], y: [15,11,12,13,14]}], { xaxis: { type: 'category', - categorymode: 'explicit', + categorymode: 'array', categories: ['y','b','x','a','d','z','e','c'] }}); @@ -225,7 +225,7 @@ describe('calculated data and points', function() { Plotly.plot(gd, [{x: ['c','a','e','b','d'], y: [15,11,12,13,14]}], { xaxis: { type: 'category', - categorymode: 'explicit', + categorymode: 'array', categories: ['b','a','x','c'] }}); From 62c4ac55d4ca74d0bce53c4562d192a04984f409 Mon Sep 17 00:00:00 2001 From: Robert Monfera Date: Sun, 27 Mar 2016 11:30:36 +0200 Subject: [PATCH 06/42] Renaming of 'categories' to 'categorylist' (it was explicitly deleted presumably as part of providing compatibility with an older API version) --- src/plots/cartesian/layout_attributes.js | 7 +++++-- test/jasmine/tests/calcdata_test.js | 10 +++++----- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/src/plots/cartesian/layout_attributes.js b/src/plots/cartesian/layout_attributes.js index 78a0e3feb9e..4a8026279d7 100644 --- a/src/plots/cartesian/layout_attributes.js +++ b/src/plots/cartesian/layout_attributes.js @@ -460,10 +460,13 @@ module.exports = { '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.' + 'numerical order of the values.', + '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(' ') }, - categories: { + categorylist: { valType: 'data_array', role: 'style', description: [ diff --git a/test/jasmine/tests/calcdata_test.js b/test/jasmine/tests/calcdata_test.js index fce5af7c56a..ffe4031968d 100644 --- a/test/jasmine/tests/calcdata_test.js +++ b/test/jasmine/tests/calcdata_test.js @@ -107,7 +107,7 @@ describe('calculated data and points', function() { Plotly.plot(gd, [{x: ['c','a','e','b','d'], y: [15,11,12,13,14]}], { xaxis: { type: 'category', categorymode: 'category ascending', - categories: ['b','a','d','e','c'] // These must be ignored. Alternative: error? + categorylist: ['b','a','d','e','c'] // These must be ignored. Alternative: error? }}); expect(gd.calcdata[0][0].y).toEqual(11); @@ -183,7 +183,7 @@ describe('calculated data and points', function() { Plotly.plot(gd, [{x: ['c','a','e','b','d'], y: [15,11,12,13,14]}], { xaxis: { type: 'category', categorymode: 'array', - categories: ['b','a','d','e','c'] + categorylist: ['b','a','d','e','c'] }}); expect(gd.calcdata[0][0].y).toEqual(13); @@ -198,7 +198,7 @@ describe('calculated data and points', function() { Plotly.plot(gd, [{x: ['c',undefined,'e','b','d'], y: [15,11,12,null,14]}], { xaxis: { type: 'category', categorymode: 'array', - categories: ['b','a','d','e','c'] + categorylist: ['b','a','d','e','c'] }}); expect(gd.calcdata[0][0].y).toEqual(13); @@ -211,7 +211,7 @@ describe('calculated data and points', function() { Plotly.plot(gd, [{x: ['c','a','e','b','d'], y: [15,11,12,13,14]}], { xaxis: { type: 'category', categorymode: 'array', - categories: ['y','b','x','a','d','z','e','c'] + categorylist: ['y','b','x','a','d','z','e','c'] }}); expect(gd.calcdata[0][0].y).toEqual(13); @@ -226,7 +226,7 @@ describe('calculated data and points', function() { Plotly.plot(gd, [{x: ['c','a','e','b','d'], y: [15,11,12,13,14]}], { xaxis: { type: 'category', categorymode: 'array', - categories: ['b','a','x','c'] + categorylist: ['b','a','x','c'] }}); expect(gd.calcdata[0][0].y).toEqual(13); From 85d60d6326067db153ca78f48a0e44626d7ff0e9 Mon Sep 17 00:00:00 2001 From: Robert Monfera Date: Tue, 29 Mar 2016 17:32:28 +0200 Subject: [PATCH 07/42] Minimal commit to demonstrate the working of categorymode = 'array' --- src/plot_api/plot_api.js | 10 +++++++--- src/plots/cartesian/axis_defaults.js | 3 +++ src/plots/cartesian/set_convert.js | 5 +++-- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/src/plot_api/plot_api.js b/src/plot_api/plot_api.js index 026356a1f04..a08e7d49cdf 100644 --- a/src/plot_api/plot_api.js +++ b/src/plot_api/plot_api.js @@ -832,7 +832,7 @@ function doCalcdata(gd) { fullData = gd._fullData, fullLayout = gd._fullLayout; - var i, trace, module, cd; + var i, trace, module, cd, ax; var calcdata = gd.calcdata = new Array(fullData.length); @@ -851,10 +851,14 @@ function doCalcdata(gd) { fullLayout._piecolormap = {}; fullLayout._piedefaultcolorcount = 0; - // delete category list, if there is one, so we start over + // initialize 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 = []; + + ax = axList[i]; + ax._categories = ax.categorymode === 'array' + ? ax.categorylist || [] + : []; } for(i = 0; i < fullData.length; i++) { diff --git a/src/plots/cartesian/axis_defaults.js b/src/plots/cartesian/axis_defaults.js index 2e2e5e19366..54b6a657337 100644 --- a/src/plots/cartesian/axis_defaults.js +++ b/src/plots/cartesian/axis_defaults.js @@ -121,6 +121,9 @@ module.exports = function handleAxisDefaults(containerIn, containerOut, coerce, delete containerOut.zerolinewidth; } + containerOut.categorymode = containerIn.categorymode; + containerOut.categorylist = containerIn.categorylist; + return containerOut; }; diff --git a/src/plots/cartesian/set_convert.js b/src/plots/cartesian/set_convert.js index e2848a77388..0902e70df7a 100644 --- a/src/plots/cartesian/set_convert.js +++ b/src/plots/cartesian/set_convert.js @@ -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); From f6c40b6fc5027eb70ade227b495ed897f84ef60f Mon Sep 17 00:00:00 2001 From: Robert Monfera Date: Thu, 31 Mar 2016 13:26:23 +0200 Subject: [PATCH 08/42] Minimal change for implementing 'category ascending' and 'category descending' at the discussed point --- src/plot_api/plot_api.js | 10 +++------- src/plots/cartesian/axis_defaults.js | 17 ++++++++++++++--- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/src/plot_api/plot_api.js b/src/plot_api/plot_api.js index a08e7d49cdf..2fa73dbd81b 100644 --- a/src/plot_api/plot_api.js +++ b/src/plot_api/plot_api.js @@ -832,7 +832,7 @@ function doCalcdata(gd) { fullData = gd._fullData, fullLayout = gd._fullLayout; - var i, trace, module, cd, ax; + var i, trace, module, cd; var calcdata = gd.calcdata = new Array(fullData.length); @@ -851,14 +851,10 @@ function doCalcdata(gd) { fullLayout._piecolormap = {}; fullLayout._piedefaultcolorcount = 0; - // initialize 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++) { - - ax = axList[i]; - ax._categories = ax.categorymode === 'array' - ? ax.categorylist || [] - : []; + axList[i]._categories = axList[i]._initialCategories.slice(); } for(i = 0; i < fullData.length; i++) { diff --git a/src/plots/cartesian/axis_defaults.js b/src/plots/cartesian/axis_defaults.js index 54b6a657337..067dc36b21d 100644 --- a/src/plots/cartesian/axis_defaults.js +++ b/src/plots/cartesian/axis_defaults.js @@ -64,6 +64,20 @@ module.exports = function handleAxisDefaults(containerIn, containerOut, coerce, } } + containerOut._initialCategories = axType === 'category' ? + + containerIn.categorymode === 'array' ? + + containerOut.categorylist.slice() : + + [].concat.apply([], options.data.map(function(d) {return d[letter]})) + .filter(function(element, index, array) {return index === array.indexOf(element);}) + .sort(({ + 'category ascending': d3.ascending, + 'category descending': d3.descending + })[containerIn.categorymode]) : + []; + setConvert(containerOut); coerce('title', defaultTitle); @@ -121,9 +135,6 @@ module.exports = function handleAxisDefaults(containerIn, containerOut, coerce, delete containerOut.zerolinewidth; } - containerOut.categorymode = containerIn.categorymode; - containerOut.categorylist = containerIn.categorylist; - return containerOut; }; From e4f216729e991d68956382462d7549dde5ab53a8 Mon Sep 17 00:00:00 2001 From: Robert Monfera Date: Thu, 31 Mar 2016 14:23:37 +0200 Subject: [PATCH 09/42] factored out orderedCategories into a separate function --- src/plots/cartesian/axis_defaults.js | 29 +++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/src/plots/cartesian/axis_defaults.js b/src/plots/cartesian/axis_defaults.js index 067dc36b21d..141346c18a9 100644 --- a/src/plots/cartesian/axis_defaults.js +++ b/src/plots/cartesian/axis_defaults.js @@ -21,6 +21,23 @@ var setConvert = require('./set_convert'); var cleanDatum = require('./clean_datum'); var axisIds = require('./axis_ids'); +function orderedCategories(axisLetter, categorymode, categorylist, data) { + + return categorymode === 'array' ? + + // just return a copy of the specified array ... + categorylist.slice() : + + // ... or take the union of all encountered tick keys and sort them as specified + // (could be simplified with lodash-fp or ramda) + [].concat.apply([], data.map(function(d) {return d[axisLetter]})) + .filter(function(element, index, array) {return index === array.indexOf(element);}) + .sort(({ + 'category ascending': d3.ascending, + 'category descending': d3.descending + })[categorymode]); +} + /** * options: object containing: @@ -65,17 +82,7 @@ module.exports = function handleAxisDefaults(containerIn, containerOut, coerce, } containerOut._initialCategories = axType === 'category' ? - - containerIn.categorymode === 'array' ? - - containerOut.categorylist.slice() : - - [].concat.apply([], options.data.map(function(d) {return d[letter]})) - .filter(function(element, index, array) {return index === array.indexOf(element);}) - .sort(({ - 'category ascending': d3.ascending, - 'category descending': d3.descending - })[containerIn.categorymode]) : + orderedCategories(letter, containerIn.categorymode, containerIn.categorylist, options.data) : []; setConvert(containerOut); From 03643fe50d00dee82fc855415ff1c20011bff8f6 Mon Sep 17 00:00:00 2001 From: Robert Monfera Date: Thu, 31 Mar 2016 14:27:12 +0200 Subject: [PATCH 10/42] factored out orderedCategories into a separate function --- src/plots/cartesian/axis_defaults.js | 18 +------------ src/plots/cartesian/ordered_categories.js | 33 +++++++++++++++++++++++ 2 files changed, 34 insertions(+), 17 deletions(-) create mode 100644 src/plots/cartesian/ordered_categories.js diff --git a/src/plots/cartesian/axis_defaults.js b/src/plots/cartesian/axis_defaults.js index 141346c18a9..793b0a51033 100644 --- a/src/plots/cartesian/axis_defaults.js +++ b/src/plots/cartesian/axis_defaults.js @@ -18,26 +18,10 @@ var layoutAttributes = require('./layout_attributes'); var handleTickValueDefaults = require('./tick_value_defaults'); var handleTickDefaults = require('./tick_defaults'); var setConvert = require('./set_convert'); +var orderedCategories = require('./ordered_categories'); var cleanDatum = require('./clean_datum'); var axisIds = require('./axis_ids'); -function orderedCategories(axisLetter, categorymode, categorylist, data) { - - return categorymode === 'array' ? - - // just return a copy of the specified array ... - categorylist.slice() : - - // ... or take the union of all encountered tick keys and sort them as specified - // (could be simplified with lodash-fp or ramda) - [].concat.apply([], data.map(function(d) {return d[axisLetter]})) - .filter(function(element, index, array) {return index === array.indexOf(element);}) - .sort(({ - 'category ascending': d3.ascending, - 'category descending': d3.descending - })[categorymode]); -} - /** * options: object containing: diff --git a/src/plots/cartesian/ordered_categories.js b/src/plots/cartesian/ordered_categories.js new file mode 100644 index 00000000000..8a22530ff87 --- /dev/null +++ b/src/plots/cartesian/ordered_categories.js @@ -0,0 +1,33 @@ +/** +* 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'); + + +/** + * TODO add documentation + */ +module.exports = function orderedCategories(axisLetter, categorymode, categorylist, data) { + + return categorymode === 'array' ? + + // just return a copy of the specified array ... + categorylist.slice() : + + // ... or take the union of all encountered tick keys and sort them as specified + // (could be simplified with lodash-fp or ramda) + [].concat.apply([], data.map(function(d) {return d[axisLetter]})) + .filter(function(element, index, array) {return index === array.indexOf(element);}) + .sort(({ + 'category ascending': d3.ascending, + 'category descending': d3.descending + })[categorymode]); +}; From 9c366ce959b604ca55c09ddf49b357e712fcf0eb Mon Sep 17 00:00:00 2001 From: Robert Monfera Date: Thu, 31 Mar 2016 14:43:28 +0200 Subject: [PATCH 11/42] lint orderedCategories --- src/plots/cartesian/ordered_categories.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/plots/cartesian/ordered_categories.js b/src/plots/cartesian/ordered_categories.js index 8a22530ff87..7da4b56f65f 100644 --- a/src/plots/cartesian/ordered_categories.js +++ b/src/plots/cartesian/ordered_categories.js @@ -16,18 +16,18 @@ var d3 = require('d3'); * TODO add documentation */ module.exports = function orderedCategories(axisLetter, categorymode, categorylist, data) { - + return categorymode === 'array' ? - + // just return a copy of the specified array ... categorylist.slice() : - + // ... or take the union of all encountered tick keys and sort them as specified // (could be simplified with lodash-fp or ramda) - [].concat.apply([], data.map(function(d) {return d[axisLetter]})) + [].concat.apply([], data.map(function(d) {return d[axisLetter];})) .filter(function(element, index, array) {return index === array.indexOf(element);}) .sort(({ - 'category ascending': d3.ascending, + 'category ascending': d3.ascending, 'category descending': d3.descending })[categorymode]); }; From f626b08c940f85e495af2f22dcfe3bf52a1dc9ef Mon Sep 17 00:00:00 2001 From: Robert Monfera Date: Thu, 7 Apr 2016 13:29:07 +0200 Subject: [PATCH 12/42] #189 role: info --- src/plots/cartesian/layout_attributes.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/plots/cartesian/layout_attributes.js b/src/plots/cartesian/layout_attributes.js index 4a8026279d7..422755e3934 100644 --- a/src/plots/cartesian/layout_attributes.js +++ b/src/plots/cartesian/layout_attributes.js @@ -453,7 +453,7 @@ module.exports = { 'value ascending', 'value descending','array' ], dflt: 'trace', - role: 'style', + 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.', @@ -468,7 +468,7 @@ module.exports = { }, categorylist: { valType: 'data_array', - role: 'style', + role: 'info', description: [ 'Sets the order in which categories on this axis appear.', 'Only has an effect if `categorymode` is set to *array*.', From 2f939afb167761715a1f9b44deee95b984b611f6 Mon Sep 17 00:00:00 2001 From: Robert Monfera Date: Thu, 7 Apr 2016 13:41:14 +0200 Subject: [PATCH 13/42] #189 updating preexisting test cases in line with PR feedback (if there's no category encountered for a specific categorylist, it should yield null rather than being skipped over) --- test/jasmine/tests/calcdata_test.js | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/test/jasmine/tests/calcdata_test.js b/test/jasmine/tests/calcdata_test.js index ffe4031968d..94e52786caf 100644 --- a/test/jasmine/tests/calcdata_test.js +++ b/test/jasmine/tests/calcdata_test.js @@ -52,15 +52,9 @@ describe('calculated data and points', function() { Plotly.plot(gd, [{x: ['c','a','e','b','d'], y: [15,11,12,13,14]}], { xaxis: { type: 'category', categorymode: 'trace' - // Wouldn't it be preferred to supply a function and plotly would have several functions like this? - // E.g. it's easier for symbol completion (whereas there's no symbol completion on string config) - // See arguments from Mike Bostock, highlighted in medium green here: - // https://medium.com/@mbostock/what-makes-software-good-943557f8a488#eef9 - // Plus if it's a function, then users can roll their own. - // // Also, if axis tick order is made configurable, shouldn't we make trace order configurable? // Trace order as in, if a line or curve is drawn through points, what's the trace sequence. - // These are two orthogonal concepts. In this round, I'm assuming that the trace order is implied + // These are two orthogonal concepts. Currently, the trace order is implied // by the order the {x,y} arrays are specified. }}); @@ -214,11 +208,14 @@ describe('calculated data and points', function() { categorylist: ['y','b','x','a','d','z','e','c'] }}); - expect(gd.calcdata[0][0].y).toEqual(13); - expect(gd.calcdata[0][1].y).toEqual(11); - expect(gd.calcdata[0][2].y).toEqual(14); - expect(gd.calcdata[0][3].y).toEqual(12); - expect(gd.calcdata[0][4].y).toEqual(15); + expect(gd.calcdata[0][0].y).toEqual(null); + expect(gd.calcdata[0][1].y).toEqual(13); + expect(gd.calcdata[0][2].y).toEqual(null); + expect(gd.calcdata[0][3].y).toEqual(11); + expect(gd.calcdata[0][4].y).toEqual(14); + expect(gd.calcdata[0][5].y).toEqual(null); + expect(gd.calcdata[0][6].y).toEqual(12); + expect(gd.calcdata[0][7].y).toEqual(15); }); it('should output categories in explicitly supplied order first, if not all categories are covered', function() { @@ -231,7 +228,8 @@ describe('calculated data and points', function() { expect(gd.calcdata[0][0].y).toEqual(13); expect(gd.calcdata[0][1].y).toEqual(11); - expect(gd.calcdata[0][2].y).toEqual(15); + expect(gd.calcdata[0][2].y).toEqual(null); + expect(gd.calcdata[0][3].y).toEqual(15); // The order of the rest is unspecified, no need to check. Alternative: make _both_ categorymode and // categories effective; categories would take precedence and the remaining items would be sorted From ce35e8b8c46104bed894635ddac134ad7fd84d3e Mon Sep 17 00:00:00 2001 From: Robert Monfera Date: Thu, 7 Apr 2016 14:29:03 +0200 Subject: [PATCH 14/42] #189 updating preexisting test cases so that order itself is checked; no assumption about trace order (unlike my first cut of the test cases) --- test/jasmine/tests/calcdata_test.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/test/jasmine/tests/calcdata_test.js b/test/jasmine/tests/calcdata_test.js index 94e52786caf..1aafa7acf63 100644 --- a/test/jasmine/tests/calcdata_test.js +++ b/test/jasmine/tests/calcdata_test.js @@ -75,11 +75,11 @@ describe('calculated data and points', function() { categorymode: 'category ascending' }}); - expect(gd.calcdata[0][0].y).toEqual(11); - expect(gd.calcdata[0][1].y).toEqual(13); - expect(gd.calcdata[0][2].y).toEqual(15); - expect(gd.calcdata[0][3].y).toEqual(14); - expect(gd.calcdata[0][4].y).toEqual(12); + expect(gd.calcdata[0][0]).toEqual(jasmine.objectContaining({x: 2, y: 15})); + expect(gd.calcdata[0][1]).toEqual(jasmine.objectContaining({x: 0, y: 11})); + expect(gd.calcdata[0][2]).toEqual(jasmine.objectContaining({x: 4, y: 12})); + expect(gd.calcdata[0][3]).toEqual(jasmine.objectContaining({x: 1, y: 13})); + expect(gd.calcdata[0][4]).toEqual(jasmine.objectContaining({x: 3, y: 14})); }); it('should output categories in descending domain alphanumerical order', function() { From 262c7470ddbd2d24396d1b4ed5b54f8b351f44d8 Mon Sep 17 00:00:00 2001 From: Robert Monfera Date: Thu, 7 Apr 2016 15:54:53 +0200 Subject: [PATCH 15/42] #189 updating preexisting test cases: further updates to proper axis order checking --- test/jasmine/tests/calcdata_test.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/test/jasmine/tests/calcdata_test.js b/test/jasmine/tests/calcdata_test.js index 1aafa7acf63..5316aeeed0d 100644 --- a/test/jasmine/tests/calcdata_test.js +++ b/test/jasmine/tests/calcdata_test.js @@ -82,18 +82,18 @@ describe('calculated data and points', function() { expect(gd.calcdata[0][4]).toEqual(jasmine.objectContaining({x: 3, y: 14})); }); - it('should output categories in descending domain alphanumerical order', function() { + fit('should output categories in descending domain alphanumerical order', function() { Plotly.plot(gd, [{x: ['c','a','e','b','d'], y: [15,11,12,13,14]}], { xaxis: { type: 'category', categorymode: 'category descending' }}); - expect(gd.calcdata[0][0].y).toEqual(12); - expect(gd.calcdata[0][1].y).toEqual(14); - expect(gd.calcdata[0][2].y).toEqual(15); - expect(gd.calcdata[0][3].y).toEqual(13); - expect(gd.calcdata[0][4].y).toEqual(11); + expect(gd.calcdata[0][0]).toEqual(jasmine.objectContaining({x: 2, y: 15})); + expect(gd.calcdata[0][1]).toEqual(jasmine.objectContaining({x: 4, y: 11})); + expect(gd.calcdata[0][2]).toEqual(jasmine.objectContaining({x: 0, y: 12})); + expect(gd.calcdata[0][3]).toEqual(jasmine.objectContaining({x: 3, y: 13})); + expect(gd.calcdata[0][4]).toEqual(jasmine.objectContaining({x: 1, y: 14})); }); it('should output categories in categorymode order even if category array is defined', function() { From 5f33a7b70500190effda6146f59832371b95371a Mon Sep 17 00:00:00 2001 From: Robert Monfera Date: Thu, 7 Apr 2016 17:16:06 +0200 Subject: [PATCH 16/42] #189 updating preexisting test cases: further updates to proper axis order checking --- test/jasmine/tests/calcdata_test.js | 105 ++++++++++++++++++---------- 1 file changed, 70 insertions(+), 35 deletions(-) diff --git a/test/jasmine/tests/calcdata_test.js b/test/jasmine/tests/calcdata_test.js index 5316aeeed0d..a2f87a5b883 100644 --- a/test/jasmine/tests/calcdata_test.js +++ b/test/jasmine/tests/calcdata_test.js @@ -30,7 +30,7 @@ describe('calculated data and points', function() { }); }); - xdescribe('category ordering', function() { + describe('category ordering', function() { describe('default category ordering reified', function() { @@ -68,6 +68,8 @@ describe('calculated data and points', function() { describe('domain alphanumerical category ordering', function() { + // TODO augment test cases with selection on the DOM to ensure that ticks are there in proper order + it('should output categories in ascending domain alphanumerical order', function() { Plotly.plot(gd, [{x: ['c','a','e','b','d'], y: [15,11,12,13,14]}], { xaxis: { @@ -82,7 +84,7 @@ describe('calculated data and points', function() { expect(gd.calcdata[0][4]).toEqual(jasmine.objectContaining({x: 3, y: 14})); }); - fit('should output categories in descending domain alphanumerical order', function() { + it('should output categories in descending domain alphanumerical order', function() { Plotly.plot(gd, [{x: ['c','a','e','b','d'], y: [15,11,12,13,14]}], { xaxis: { type: 'category', @@ -104,11 +106,11 @@ describe('calculated data and points', function() { categorylist: ['b','a','d','e','c'] // These must be ignored. Alternative: error? }}); - expect(gd.calcdata[0][0].y).toEqual(11); - expect(gd.calcdata[0][1].y).toEqual(13); - expect(gd.calcdata[0][2].y).toEqual(15); - expect(gd.calcdata[0][3].y).toEqual(14); - expect(gd.calcdata[0][4].y).toEqual(12); + expect(gd.calcdata[0][0]).toEqual(jasmine.objectContaining({x: 2, y: 15})); + expect(gd.calcdata[0][1]).toEqual(jasmine.objectContaining({x: 0, y: 11})); + expect(gd.calcdata[0][2]).toEqual(jasmine.objectContaining({x: 4, y: 12})); + expect(gd.calcdata[0][3]).toEqual(jasmine.objectContaining({x: 1, y: 13})); + expect(gd.calcdata[0][4]).toEqual(jasmine.objectContaining({x: 3, y: 14})); }); it('should output categories in ascending domain alphanumerical order, excluding undefined', function() { @@ -118,14 +120,14 @@ describe('calculated data and points', function() { categorymode: 'category ascending' }}); - expect(gd.calcdata[0][0].y).toEqual(11); - expect(gd.calcdata[0][1].y).toEqual(15); - expect(gd.calcdata[0][2].y).toEqual(14); - expect(gd.calcdata[0][3].y).toEqual(12); + expect(gd.calcdata[0][0]).toEqual(jasmine.objectContaining({x: 1, y: 15})); + expect(gd.calcdata[0][2]).toEqual(jasmine.objectContaining({x: 3, y: 12})); + expect(gd.calcdata[0][3]).toEqual(jasmine.objectContaining({x: 0, y: 13})); + expect(gd.calcdata[0][4]).toEqual(jasmine.objectContaining({x: 2, y: 14})); }); }); - describe('codomain numerical category ordering', function() { + xdescribe('codomain numerical category ordering', function() { it('should output categories in ascending codomain numerical order', function() { @@ -170,7 +172,7 @@ describe('calculated data and points', function() { }); }); - describe('explicit category ordering', function() { + fdescribe('explicit category ordering', function() { it('should output categories in explicitly supplied order, independent of trace order', function() { @@ -180,11 +182,11 @@ describe('calculated data and points', function() { categorylist: ['b','a','d','e','c'] }}); - expect(gd.calcdata[0][0].y).toEqual(13); - expect(gd.calcdata[0][1].y).toEqual(11); - expect(gd.calcdata[0][2].y).toEqual(14); - expect(gd.calcdata[0][3].y).toEqual(12); - expect(gd.calcdata[0][4].y).toEqual(15); + expect(gd.calcdata[0][0]).toEqual(jasmine.objectContaining({x: 4, y: 15})); + expect(gd.calcdata[0][1]).toEqual(jasmine.objectContaining({x: 1, y: 11})); + expect(gd.calcdata[0][2]).toEqual(jasmine.objectContaining({x: 3, y: 12})); + expect(gd.calcdata[0][3]).toEqual(jasmine.objectContaining({x: 0, y: 13})); + expect(gd.calcdata[0][4]).toEqual(jasmine.objectContaining({x: 2, y: 14})); }); it('should output categories in explicitly supplied order, independent of trace order, pruned', function() { @@ -195,9 +197,11 @@ describe('calculated data and points', function() { categorylist: ['b','a','d','e','c'] }}); - expect(gd.calcdata[0][0].y).toEqual(13); - expect(gd.calcdata[0][1].y).toEqual(14); - expect(gd.calcdata[0][2].y).toEqual(15); + expect(gd.calcdata[0][0]).toEqual(jasmine.objectContaining({x: 4, y: 15})); + expect(gd.calcdata[0][1]).toEqual({ x: false, y: false}); + expect(gd.calcdata[0][2]).toEqual(jasmine.objectContaining({x: 3, y: 12})); + expect(gd.calcdata[0][3]).toEqual({ x: false, y: false}); + expect(gd.calcdata[0][4]).toEqual(jasmine.objectContaining({x: 2, y: 14})); }); it('should output categories in explicitly supplied order even if not all categories are present', function() { @@ -205,20 +209,50 @@ describe('calculated data and points', function() { Plotly.plot(gd, [{x: ['c','a','e','b','d'], y: [15,11,12,13,14]}], { xaxis: { type: 'category', categorymode: 'array', - categorylist: ['y','b','x','a','d','z','e','c'] + categorylist: ['b','x','a','d','z','e','c'] }}); - expect(gd.calcdata[0][0].y).toEqual(null); - expect(gd.calcdata[0][1].y).toEqual(13); - expect(gd.calcdata[0][2].y).toEqual(null); - expect(gd.calcdata[0][3].y).toEqual(11); - expect(gd.calcdata[0][4].y).toEqual(14); - expect(gd.calcdata[0][5].y).toEqual(null); - expect(gd.calcdata[0][6].y).toEqual(12); - expect(gd.calcdata[0][7].y).toEqual(15); + expect(gd.calcdata[0][0]).toEqual(jasmine.objectContaining({x: 6, y: 15})); + expect(gd.calcdata[0][1]).toEqual(jasmine.objectContaining({x: 2, y: 11})); + expect(gd.calcdata[0][2]).toEqual(jasmine.objectContaining({x: 5, y: 12})); + expect(gd.calcdata[0][3]).toEqual(jasmine.objectContaining({x: 0, y: 13})); + expect(gd.calcdata[0][4]).toEqual(jasmine.objectContaining({x: 3, y: 14})); }); - it('should output categories in explicitly supplied order first, if not all categories are covered', function() { + it('should output categories in explicitly supplied order even if some missing categories were at the beginning or end of categorylist', function() { + + // TODO WARNING: THIS CASE *PASSES* BUT THE UNPOPULATED CATEGORIES AT THE EDGES AREN'T RENDERED IN THE DOM + // TODO enhance test cases with selection on the DOM to ensure that all unpopulated ticks are present + + Plotly.plot(gd, [{x: ['c','a','e','b','d'], y: [15,11,12,13,14]}], { xaxis: { + type: 'category', + categorymode: 'array', + categorylist: ['s','y','b','x','a','d','z','e','c', 'q', 'k'] + }}); + + expect(gd.calcdata[0][0]).toEqual(jasmine.objectContaining({x: 7, y: 15})); + expect(gd.calcdata[0][1]).toEqual(jasmine.objectContaining({x: 3, y: 11})); + expect(gd.calcdata[0][2]).toEqual(jasmine.objectContaining({x: 6, y: 12})); + expect(gd.calcdata[0][3]).toEqual(jasmine.objectContaining({x: 1, y: 13})); + expect(gd.calcdata[0][4]).toEqual(jasmine.objectContaining({x: 4, y: 14})); + }); + + it('should output categories in explicitly supplied order even if not all categories are present, and should interact with a null value orthogonally', function() { + + Plotly.plot(gd, [{x: ['c','a','e','b','d'], y: [15,null,12,13,14]}], { xaxis: { + type: 'category', + categorymode: 'array', + categorylist: ['b','x','a','d','z','e','c'] + }}); + + expect(gd.calcdata[0][0]).toEqual(jasmine.objectContaining({x: 6, y: 15})); + expect(gd.calcdata[0][1]).toEqual({x: false, y: false}); + expect(gd.calcdata[0][2]).toEqual(jasmine.objectContaining({x: 5, y: 12})); + expect(gd.calcdata[0][3]).toEqual(jasmine.objectContaining({x: 0, y: 13})); + expect(gd.calcdata[0][4]).toEqual(jasmine.objectContaining({x: 3, y: 14})); + }); + + fit('should output categories in explicitly supplied order first, if not all categories are covered', function() { Plotly.plot(gd, [{x: ['c','a','e','b','d'], y: [15,11,12,13,14]}], { xaxis: { type: 'category', @@ -226,10 +260,11 @@ describe('calculated data and points', function() { categorylist: ['b','a','x','c'] }}); - expect(gd.calcdata[0][0].y).toEqual(13); - expect(gd.calcdata[0][1].y).toEqual(11); - expect(gd.calcdata[0][2].y).toEqual(null); - expect(gd.calcdata[0][3].y).toEqual(15); + expect(gd.calcdata[0][0]).toEqual(jasmine.objectContaining({x: 3, y: 15})); + expect(gd.calcdata[0][1]).toEqual(jasmine.objectContaining({x: 1, y: 11})); + expect(gd.calcdata[0][2]).toEqual(jasmine.objectContaining({x: 4, y: 12})); + expect(gd.calcdata[0][3]).toEqual(jasmine.objectContaining({x: 0, y: 13})); + expect(gd.calcdata[0][4]).toEqual(jasmine.objectContaining({x: 5, y: 14})); // The order of the rest is unspecified, no need to check. Alternative: make _both_ categorymode and // categories effective; categories would take precedence and the remaining items would be sorted From f4214cbe4a7f583a2661875e71e8133fa30e8916 Mon Sep 17 00:00:00 2001 From: Robert Monfera Date: Fri, 8 Apr 2016 10:11:01 +0200 Subject: [PATCH 17/42] #189 adding a path for when categorymode is not in ['array', 'category ascending', 'category descending'] --- src/plots/cartesian/ordered_categories.js | 17 +++++++++++------ test/jasmine/tests/calcdata_test.js | 4 ++-- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/src/plots/cartesian/ordered_categories.js b/src/plots/cartesian/ordered_categories.js index 7da4b56f65f..5543d8b8424 100644 --- a/src/plots/cartesian/ordered_categories.js +++ b/src/plots/cartesian/ordered_categories.js @@ -24,10 +24,15 @@ module.exports = function orderedCategories(axisLetter, categorymode, categoryli // ... or take the union of all encountered tick keys and sort them as specified // (could be simplified with lodash-fp or ramda) - [].concat.apply([], data.map(function(d) {return d[axisLetter];})) - .filter(function(element, index, array) {return index === array.indexOf(element);}) - .sort(({ - 'category ascending': d3.ascending, - 'category descending': d3.descending - })[categorymode]); + + ['category ascending', 'category descending'].indexOf(categorymode) > -1 ? + + [].concat.apply([], data.map(function(d) {return d[axisLetter];})) + .filter(function(element, index, array) {return index === array.indexOf(element);}) + .sort(({ + 'category ascending': d3.ascending, + 'category descending': d3.descending + })[categorymode]) : + + [].slice(); }; diff --git a/test/jasmine/tests/calcdata_test.js b/test/jasmine/tests/calcdata_test.js index a2f87a5b883..ca72b0405f2 100644 --- a/test/jasmine/tests/calcdata_test.js +++ b/test/jasmine/tests/calcdata_test.js @@ -172,7 +172,7 @@ describe('calculated data and points', function() { }); }); - fdescribe('explicit category ordering', function() { + describe('explicit category ordering', function() { it('should output categories in explicitly supplied order, independent of trace order', function() { @@ -252,7 +252,7 @@ describe('calculated data and points', function() { expect(gd.calcdata[0][4]).toEqual(jasmine.objectContaining({x: 3, y: 14})); }); - fit('should output categories in explicitly supplied order first, if not all categories are covered', function() { + it('should output categories in explicitly supplied order first, if not all categories are covered', function() { Plotly.plot(gd, [{x: ['c','a','e','b','d'], y: [15,11,12,13,14]}], { xaxis: { type: 'category', From cbc98fd9c50c4c8e1a5202c193eaed70a4d413c6 Mon Sep 17 00:00:00 2001 From: Robert Monfera Date: Fri, 8 Apr 2016 10:14:52 +0200 Subject: [PATCH 18/42] #189 updating test case with non-utilized categorylist elements at beginning / end --- test/jasmine/tests/calcdata_test.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/test/jasmine/tests/calcdata_test.js b/test/jasmine/tests/calcdata_test.js index ca72b0405f2..4dd950280dc 100644 --- a/test/jasmine/tests/calcdata_test.js +++ b/test/jasmine/tests/calcdata_test.js @@ -221,13 +221,12 @@ describe('calculated data and points', function() { it('should output categories in explicitly supplied order even if some missing categories were at the beginning or end of categorylist', function() { - // TODO WARNING: THIS CASE *PASSES* BUT THE UNPOPULATED CATEGORIES AT THE EDGES AREN'T RENDERED IN THE DOM - // TODO enhance test cases with selection on the DOM to ensure that all unpopulated ticks are present + // The auto-range feature currently eliminates unutilized category ticks on the left/right edge Plotly.plot(gd, [{x: ['c','a','e','b','d'], y: [15,11,12,13,14]}], { xaxis: { type: 'category', categorymode: 'array', - categorylist: ['s','y','b','x','a','d','z','e','c', 'q', 'k'] + categorylist: ['y','b','x','a','d','z','e','c', 'q', 'k'] }}); expect(gd.calcdata[0][0]).toEqual(jasmine.objectContaining({x: 7, y: 15})); From a806ca2ef281847652c64807d3beef5d17c54e9d Mon Sep 17 00:00:00 2001 From: Robert Monfera Date: Fri, 8 Apr 2016 12:44:57 +0200 Subject: [PATCH 19/42] #189 test case with null value for a category we just want for an axis tail tick --- test/jasmine/tests/calcdata_test.js | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/test/jasmine/tests/calcdata_test.js b/test/jasmine/tests/calcdata_test.js index 4dd950280dc..9d2b53d1255 100644 --- a/test/jasmine/tests/calcdata_test.js +++ b/test/jasmine/tests/calcdata_test.js @@ -236,6 +236,25 @@ describe('calculated data and points', function() { expect(gd.calcdata[0][4]).toEqual(jasmine.objectContaining({x: 4, y: 14})); }); + it('should output categories in explicitly supplied order even if some missing categories were at the beginning or end of categorylist', function() { + + // The auto-range feature currently eliminates unutilized category ticks on the left/right edge + // BUT keeps it if a data point with null is added; test is almost identical to the one above + // except that it explicitly adds an axis tick for y + + Plotly.plot(gd, [{x: ['c','a','e','b','d', 'y'], y: [15,11,12,13,14, null]}], { xaxis: { + type: 'category', + categorymode: 'array', + categorylist: ['y','b','x','a','d','z','e','c', 'q', 'k'] + }}); + + expect(gd.calcdata[0][0]).toEqual(jasmine.objectContaining({x: 7, y: 15})); + expect(gd.calcdata[0][1]).toEqual(jasmine.objectContaining({x: 3, y: 11})); + expect(gd.calcdata[0][2]).toEqual(jasmine.objectContaining({x: 6, y: 12})); + expect(gd.calcdata[0][3]).toEqual(jasmine.objectContaining({x: 1, y: 13})); + expect(gd.calcdata[0][4]).toEqual(jasmine.objectContaining({x: 4, y: 14})); + }); + it('should output categories in explicitly supplied order even if not all categories are present, and should interact with a null value orthogonally', function() { Plotly.plot(gd, [{x: ['c','a','e','b','d'], y: [15,null,12,13,14]}], { xaxis: { From 5ad6b5c4623b9d9420d942f0dd7d8ee418dce0f7 Mon Sep 17 00:00:00 2001 From: Robert Monfera Date: Fri, 8 Apr 2016 15:53:47 +0200 Subject: [PATCH 20/42] #189 baseline test case based on codepen pointed to by @etpinard --- test/jasmine/tests/calcdata_test.js | 36 +++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/test/jasmine/tests/calcdata_test.js b/test/jasmine/tests/calcdata_test.js index 9d2b53d1255..802910514d9 100644 --- a/test/jasmine/tests/calcdata_test.js +++ b/test/jasmine/tests/calcdata_test.js @@ -290,5 +290,41 @@ describe('calculated data and points', function() { // behavior, rather than an explicit 'explicit' categorymode. }); }); + + describe('ordering tests in the presence of multiple traces', function() { + + it('baseline testing for the unordered, disjunct case', function() { + + var x1 = ['Gear', 'Bearing', 'Motor']; + var x2 = ['Switch', 'Plug', 'Cord', 'Fuse', 'Bulb']; + var x3 = ['Pump', 'Leak', 'Seals']; + + Plotly.plot(gd, [{ + x: x1, + y: x1.map(function(d, i) {return i + 10;}) + }, { + x: x2, + y: x2.map(function(d, i) {return i + 20;}) + }, { + x: x3, + y: x3.map(function(d, i) {return i + 30;}) + }]); + + expect(gd.calcdata[0][0]).toEqual(jasmine.objectContaining({x: 0, y: 10})); + expect(gd.calcdata[0][1]).toEqual(jasmine.objectContaining({x: 1, y: 11})); + expect(gd.calcdata[0][2]).toEqual(jasmine.objectContaining({x: 2, y: 12})); + + expect(gd.calcdata[1][0]).toEqual(jasmine.objectContaining({x: 3, y: 20})); + expect(gd.calcdata[1][1]).toEqual(jasmine.objectContaining({x: 4, y: 21})); + expect(gd.calcdata[1][2]).toEqual(jasmine.objectContaining({x: 5, y: 22})); + expect(gd.calcdata[1][3]).toEqual(jasmine.objectContaining({x: 6, y: 23})); + expect(gd.calcdata[1][4]).toEqual(jasmine.objectContaining({x: 7, y: 24})); + + expect(gd.calcdata[2][0]).toEqual(jasmine.objectContaining({x: 8, y: 30})); + expect(gd.calcdata[2][1]).toEqual(jasmine.objectContaining({x: 9, y: 31})); + expect(gd.calcdata[2][2]).toEqual(jasmine.objectContaining({x: 10, y: 32})); + }); + + }); }); }); From 82076f2bca7ad7a1315fa79b6593fdc522afff9c Mon Sep 17 00:00:00 2001 From: Robert Monfera Date: Sat, 9 Apr 2016 08:40:24 +0200 Subject: [PATCH 21/42] #189 commented out categorymode value ascending/descending as it's not part of this CR --- src/plots/cartesian/layout_attributes.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/plots/cartesian/layout_attributes.js b/src/plots/cartesian/layout_attributes.js index 422755e3934..fc6160889a5 100644 --- a/src/plots/cartesian/layout_attributes.js +++ b/src/plots/cartesian/layout_attributes.js @@ -450,7 +450,7 @@ module.exports = { valType: 'enumerated', values: [ 'trace', 'category ascending', 'category descending', - 'value ascending', 'value descending','array' + /*'value ascending', 'value descending',*/ 'array' // value ascending / descending to be implemented later ], dflt: 'trace', role: 'info', @@ -459,8 +459,8 @@ module.exports = { '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.', + /*'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`.' From 6fb2871c642743e81c7c76ef21ffb8f3e5260f00 Mon Sep 17 00:00:00 2001 From: Robert Monfera Date: Sat, 9 Apr 2016 09:25:49 +0200 Subject: [PATCH 22/42] #189 adding test cases for trace lines with mutually exclusive category sets --- test/jasmine/tests/calcdata_test.js | 143 +++++++++++++++++++++++++--- 1 file changed, 132 insertions(+), 11 deletions(-) diff --git a/test/jasmine/tests/calcdata_test.js b/test/jasmine/tests/calcdata_test.js index 802910514d9..b49b851960f 100644 --- a/test/jasmine/tests/calcdata_test.js +++ b/test/jasmine/tests/calcdata_test.js @@ -291,7 +291,7 @@ describe('calculated data and points', function() { }); }); - describe('ordering tests in the presence of multiple traces', function() { + describe('ordering tests in the presence of multiple traces - mutually exclusive', function() { it('baseline testing for the unordered, disjunct case', function() { @@ -299,16 +299,11 @@ describe('calculated data and points', function() { var x2 = ['Switch', 'Plug', 'Cord', 'Fuse', 'Bulb']; var x3 = ['Pump', 'Leak', 'Seals']; - Plotly.plot(gd, [{ - x: x1, - y: x1.map(function(d, i) {return i + 10;}) - }, { - x: x2, - y: x2.map(function(d, i) {return i + 20;}) - }, { - x: x3, - y: x3.map(function(d, i) {return i + 30;}) - }]); + Plotly.plot(gd, [ + {x: x1, y: x1.map(function(d, i) {return i + 10;})}, + {x: x2, y: x2.map(function(d, i) {return i + 20;})}, + {x: x3, y: x3.map(function(d, i) {return i + 30;})} + ]); expect(gd.calcdata[0][0]).toEqual(jasmine.objectContaining({x: 0, y: 10})); expect(gd.calcdata[0][1]).toEqual(jasmine.objectContaining({x: 1, y: 11})); @@ -325,6 +320,132 @@ describe('calculated data and points', function() { expect(gd.calcdata[2][2]).toEqual(jasmine.objectContaining({x: 10, y: 32})); }); + it('category order follows the trace order (even if categorylist is specified)', function() { + + var x1 = ['Gear', 'Bearing', 'Motor']; + var x2 = ['Switch', 'Plug', 'Cord', 'Fuse', 'Bulb']; + var x3 = ['Pump', 'Leak', 'Seals']; + + Plotly.plot(gd, [ + {x: x1, y: x1.map(function(d, i) {return i + 10;})}, + {x: x2, y: x2.map(function(d, i) {return i + 20;})}, + {x: x3, y: x3.map(function(d, i) {return i + 30;})} + ], { xaxis: { + // type: 'category', // commented out to rely on autotyping for added realism + categorymode: 'trace', + categorylist: ['Switch','Bearing','Motor','Seals','Pump','Cord','Plug','Bulb','Fuse','Gear','Leak'] + }}); + + expect(gd.calcdata[0][0]).toEqual(jasmine.objectContaining({x: 0, y: 10})); + expect(gd.calcdata[0][1]).toEqual(jasmine.objectContaining({x: 1, y: 11})); + expect(gd.calcdata[0][2]).toEqual(jasmine.objectContaining({x: 2, y: 12})); + + expect(gd.calcdata[1][0]).toEqual(jasmine.objectContaining({x: 3, y: 20})); + expect(gd.calcdata[1][1]).toEqual(jasmine.objectContaining({x: 4, y: 21})); + expect(gd.calcdata[1][2]).toEqual(jasmine.objectContaining({x: 5, y: 22})); + expect(gd.calcdata[1][3]).toEqual(jasmine.objectContaining({x: 6, y: 23})); + expect(gd.calcdata[1][4]).toEqual(jasmine.objectContaining({x: 7, y: 24})); + + expect(gd.calcdata[2][0]).toEqual(jasmine.objectContaining({x: 8, y: 30})); + expect(gd.calcdata[2][1]).toEqual(jasmine.objectContaining({x: 9, y: 31})); + expect(gd.calcdata[2][2]).toEqual(jasmine.objectContaining({x: 10, y: 32})); + }); + + it('category order is category ascending (even if categorylist is specified)', function() { + + var x1 = ['Gear', 'Bearing', 'Motor']; + var x2 = ['Switch', 'Plug', 'Cord', 'Fuse', 'Bulb']; + var x3 = ['Pump', 'Leak', 'Seals']; + + Plotly.plot(gd, [ + {x: x1, y: x1.map(function(d, i) {return i + 10;})}, + {x: x2, y: x2.map(function(d, i) {return i + 20;})}, + {x: x3, y: x3.map(function(d, i) {return i + 30;})} + ], { xaxis: { + // type: 'category', // commented out to rely on autotyping for added realism + categorymode: 'category ascending', + categorylist: ['Switch','Bearing','Motor','Seals','Pump','Cord','Plug','Bulb','Fuse','Gear','Leak'] + // this is the expected sorted outcome: ['Bearing','Bulb','Cord','Fuse','Gear','Leak','Motor','Plug','Pump','Seals','Switch'] + }}); + + expect(gd.calcdata[0][0]).toEqual(jasmine.objectContaining({x: 4, y: 10})); + expect(gd.calcdata[0][1]).toEqual(jasmine.objectContaining({x: 0, y: 11})); + expect(gd.calcdata[0][2]).toEqual(jasmine.objectContaining({x: 6, y: 12})); + + expect(gd.calcdata[1][0]).toEqual(jasmine.objectContaining({x: 10, y: 20})); + expect(gd.calcdata[1][1]).toEqual(jasmine.objectContaining({x: 7, y: 21})); + expect(gd.calcdata[1][2]).toEqual(jasmine.objectContaining({x: 2, y: 22})); + expect(gd.calcdata[1][3]).toEqual(jasmine.objectContaining({x: 3, y: 23})); + expect(gd.calcdata[1][4]).toEqual(jasmine.objectContaining({x: 1, y: 24})); + + expect(gd.calcdata[2][0]).toEqual(jasmine.objectContaining({x: 8, y: 30})); + expect(gd.calcdata[2][1]).toEqual(jasmine.objectContaining({x: 5, y: 31})); + expect(gd.calcdata[2][2]).toEqual(jasmine.objectContaining({x: 9, y: 32})); + }); + + it('category order is category descending (even if categorylist is specified)', function() { + + var x1 = ['Gear', 'Bearing', 'Motor']; + var x2 = ['Switch', 'Plug', 'Cord', 'Fuse', 'Bulb']; + var x3 = ['Pump', 'Leak', 'Seals']; + + Plotly.plot(gd, [ + {x: x1, y: x1.map(function(d, i) {return i + 10;})}, + {x: x2, y: x2.map(function(d, i) {return i + 20;})}, + {x: x3, y: x3.map(function(d, i) {return i + 30;})} + ], { xaxis: { + // type: 'category', // commented out to rely on autotyping for added realism + categorymode: 'category descending', + categorylist: ['Switch','Bearing','Motor','Seals','Pump','Cord','Plug','Bulb','Fuse','Gear','Leak'] + // this is the expected sorted outcome: ["Switch", "Seals", "Pump", "Plug", "Motor", "Leak", "Gear", "Fuse", "Cord", "Bulb", "Bearing"] + }}); + + expect(gd.calcdata[0][0]).toEqual(jasmine.objectContaining({x: 6, y: 10})); + expect(gd.calcdata[0][1]).toEqual(jasmine.objectContaining({x: 10, y: 11})); + expect(gd.calcdata[0][2]).toEqual(jasmine.objectContaining({x: 4, y: 12})); + + expect(gd.calcdata[1][0]).toEqual(jasmine.objectContaining({x: 0, y: 20})); + expect(gd.calcdata[1][1]).toEqual(jasmine.objectContaining({x: 3, y: 21})); + expect(gd.calcdata[1][2]).toEqual(jasmine.objectContaining({x: 8, y: 22})); + expect(gd.calcdata[1][3]).toEqual(jasmine.objectContaining({x: 7, y: 23})); + expect(gd.calcdata[1][4]).toEqual(jasmine.objectContaining({x: 9, y: 24})); + + expect(gd.calcdata[2][0]).toEqual(jasmine.objectContaining({x: 2, y: 30})); + expect(gd.calcdata[2][1]).toEqual(jasmine.objectContaining({x: 5, y: 31})); + expect(gd.calcdata[2][2]).toEqual(jasmine.objectContaining({x: 1, y: 32})); + }); + + it('category order follows categorylist', function() { + + var x1 = ['Gear', 'Bearing', 'Motor']; + var x2 = ['Switch', 'Plug', 'Cord', 'Fuse', 'Bulb']; + var x3 = ['Pump', 'Leak', 'Seals']; + + Plotly.plot(gd, [ + {x: x1, y: x1.map(function(d, i) {return i + 10;})}, + {x: x2, y: x2.map(function(d, i) {return i + 20;})}, + {x: x3, y: x3.map(function(d, i) {return i + 30;})} + ], { xaxis: { + // type: 'category', // commented out to rely on autotyping for added realism + categorymode: 'array', + categorylist: ['Switch','Bearing','Motor','Seals','Pump','Cord','Plug','Bulb','Fuse','Gear','Leak'] + }}); + + expect(gd.calcdata[0][0]).toEqual(jasmine.objectContaining({x: 9, y: 10})); + expect(gd.calcdata[0][1]).toEqual(jasmine.objectContaining({x: 1, y: 11})); + expect(gd.calcdata[0][2]).toEqual(jasmine.objectContaining({x: 2, y: 12})); + + expect(gd.calcdata[1][0]).toEqual(jasmine.objectContaining({x: 0, y: 20})); + expect(gd.calcdata[1][1]).toEqual(jasmine.objectContaining({x: 6, y: 21})); + expect(gd.calcdata[1][2]).toEqual(jasmine.objectContaining({x: 5, y: 22})); + expect(gd.calcdata[1][3]).toEqual(jasmine.objectContaining({x: 8, y: 23})); + expect(gd.calcdata[1][4]).toEqual(jasmine.objectContaining({x: 7, y: 24})); + + expect(gd.calcdata[2][0]).toEqual(jasmine.objectContaining({x: 4, y: 30})); + expect(gd.calcdata[2][1]).toEqual(jasmine.objectContaining({x: 10, y: 31})); + expect(gd.calcdata[2][2]).toEqual(jasmine.objectContaining({x: 3, y: 32})); + }); }); + }); }); From 616121bc165e8c2f62a9966bd56c01056dba19d6 Mon Sep 17 00:00:00 2001 From: Robert Monfera Date: Sat, 9 Apr 2016 10:16:34 +0200 Subject: [PATCH 23/42] #189 adding test cases for trace lines with partially overlapping, fully overlapping category sets --- test/jasmine/tests/calcdata_test.js | 343 +++++++++++++++++++++++++++- 1 file changed, 342 insertions(+), 1 deletion(-) diff --git a/test/jasmine/tests/calcdata_test.js b/test/jasmine/tests/calcdata_test.js index b49b851960f..4f94d231f39 100644 --- a/test/jasmine/tests/calcdata_test.js +++ b/test/jasmine/tests/calcdata_test.js @@ -127,7 +127,8 @@ describe('calculated data and points', function() { }); }); - xdescribe('codomain numerical category ordering', function() { +/* + describe('codomain numerical category ordering', function() { it('should output categories in ascending codomain numerical order', function() { @@ -171,6 +172,7 @@ describe('calculated data and points', function() { }); }); +*/ describe('explicit category ordering', function() { @@ -447,5 +449,344 @@ describe('calculated data and points', function() { }); }); + describe('ordering tests in the presence of multiple traces - partially overlapping', function() { + + it('baseline testing for the unordered, partially overlapping case', function() { + + var x1 = ['Gear', 'Bearing', 'Motor']; + var x2 = ['Switch', 'Plug', 'Cord', 'Fuse', 'Bulb']; + var x3 = ['Pump', 'Leak', 'Bearing', 'Seals']; + + Plotly.plot(gd, [ + {x: x1, y: x1.map(function(d, i) {return i + 10;})}, + {x: x2, y: x2.map(function(d, i) {return i + 20;})}, + {x: x3, y: x3.map(function(d, i) {return i + 30;})} + ]); + + expect(gd.calcdata[0][0]).toEqual(jasmine.objectContaining({x: 0, y: 10})); + expect(gd.calcdata[0][1]).toEqual(jasmine.objectContaining({x: 1, y: 11})); + expect(gd.calcdata[0][2]).toEqual(jasmine.objectContaining({x: 2, y: 12})); + + expect(gd.calcdata[1][0]).toEqual(jasmine.objectContaining({x: 3, y: 20})); + expect(gd.calcdata[1][1]).toEqual(jasmine.objectContaining({x: 4, y: 21})); + expect(gd.calcdata[1][2]).toEqual(jasmine.objectContaining({x: 5, y: 22})); + expect(gd.calcdata[1][3]).toEqual(jasmine.objectContaining({x: 6, y: 23})); + expect(gd.calcdata[1][4]).toEqual(jasmine.objectContaining({x: 7, y: 24})); + + expect(gd.calcdata[2][0]).toEqual(jasmine.objectContaining({x: 8, y: 30})); + expect(gd.calcdata[2][1]).toEqual(jasmine.objectContaining({x: 9, y: 31})); + expect(gd.calcdata[2][2]).toEqual(jasmine.objectContaining({x: 1, y: 32})); + expect(gd.calcdata[2][3]).toEqual(jasmine.objectContaining({x: 10, y: 33})); + }); + + it('category order follows the trace order (even if categorylist is specified)', function() { + + var x1 = ['Gear', 'Bearing', 'Motor']; + var x2 = ['Switch', 'Plug', 'Cord', 'Fuse', 'Bulb']; + var x3 = ['Pump', 'Leak', 'Bearing', 'Seals']; + + Plotly.plot(gd, [ + {x: x1, y: x1.map(function(d, i) {return i + 10;})}, + {x: x2, y: x2.map(function(d, i) {return i + 20;})}, + {x: x3, y: x3.map(function(d, i) {return i + 30;})} + ], { xaxis: { + // type: 'category', // commented out to rely on autotyping for added realism + categorymode: 'trace', + categorylist: ['Switch','Bearing','Motor','Seals','Pump','Cord','Plug','Bulb','Fuse','Gear','Leak'] + }}); + + expect(gd.calcdata[0][0]).toEqual(jasmine.objectContaining({x: 0, y: 10})); + expect(gd.calcdata[0][1]).toEqual(jasmine.objectContaining({x: 1, y: 11})); + expect(gd.calcdata[0][2]).toEqual(jasmine.objectContaining({x: 2, y: 12})); + + expect(gd.calcdata[1][0]).toEqual(jasmine.objectContaining({x: 3, y: 20})); + expect(gd.calcdata[1][1]).toEqual(jasmine.objectContaining({x: 4, y: 21})); + expect(gd.calcdata[1][2]).toEqual(jasmine.objectContaining({x: 5, y: 22})); + expect(gd.calcdata[1][3]).toEqual(jasmine.objectContaining({x: 6, y: 23})); + expect(gd.calcdata[1][4]).toEqual(jasmine.objectContaining({x: 7, y: 24})); + + expect(gd.calcdata[2][0]).toEqual(jasmine.objectContaining({x: 8, y: 30})); + expect(gd.calcdata[2][1]).toEqual(jasmine.objectContaining({x: 9, y: 31})); + expect(gd.calcdata[2][2]).toEqual(jasmine.objectContaining({x: 1, y: 32})); + expect(gd.calcdata[2][3]).toEqual(jasmine.objectContaining({x: 10, y: 33})); + }); + + it('category order is category ascending (even if categorylist is specified)', function() { + + var x1 = ['Gear', 'Bearing', 'Motor']; + var x2 = ['Switch', 'Plug', 'Cord', 'Fuse', 'Bulb']; + var x3 = ['Pump', 'Leak', 'Bearing', 'Seals']; + + Plotly.plot(gd, [ + {x: x1, y: x1.map(function(d, i) {return i + 10;})}, + {x: x2, y: x2.map(function(d, i) {return i + 20;})}, + {x: x3, y: x3.map(function(d, i) {return i + 30;})} + ], { xaxis: { + // type: 'category', // commented out to rely on autotyping for added realism + categorymode: 'category ascending', + categorylist: ['Switch','Bearing','Motor','Seals','Pump','Cord','Plug','Bulb','Fuse','Gear','Leak'] + // this is the expected sorted outcome: ['Bearing','Bulb','Cord','Fuse','Gear','Leak','Motor','Plug','Pump','Seals','Switch'] + }}); + + expect(gd.calcdata[0][0]).toEqual(jasmine.objectContaining({x: 4, y: 10})); + expect(gd.calcdata[0][1]).toEqual(jasmine.objectContaining({x: 0, y: 11})); + expect(gd.calcdata[0][2]).toEqual(jasmine.objectContaining({x: 6, y: 12})); + + expect(gd.calcdata[1][0]).toEqual(jasmine.objectContaining({x: 10, y: 20})); + expect(gd.calcdata[1][1]).toEqual(jasmine.objectContaining({x: 7, y: 21})); + expect(gd.calcdata[1][2]).toEqual(jasmine.objectContaining({x: 2, y: 22})); + expect(gd.calcdata[1][3]).toEqual(jasmine.objectContaining({x: 3, y: 23})); + expect(gd.calcdata[1][4]).toEqual(jasmine.objectContaining({x: 1, y: 24})); + + expect(gd.calcdata[2][0]).toEqual(jasmine.objectContaining({x: 8, y: 30})); + expect(gd.calcdata[2][1]).toEqual(jasmine.objectContaining({x: 5, y: 31})); + expect(gd.calcdata[2][2]).toEqual(jasmine.objectContaining({x: 0, y: 32})); + expect(gd.calcdata[2][3]).toEqual(jasmine.objectContaining({x: 9, y: 33})); + }); + + it('category order is category descending (even if categorylist is specified)', function() { + + var x1 = ['Gear', 'Bearing', 'Motor']; + var x2 = ['Switch', 'Plug', 'Cord', 'Fuse', 'Bulb']; + var x3 = ['Pump', 'Leak', 'Bearing', 'Seals']; + + Plotly.plot(gd, [ + {x: x1, y: x1.map(function(d, i) {return i + 10;})}, + {x: x2, y: x2.map(function(d, i) {return i + 20;})}, + {x: x3, y: x3.map(function(d, i) {return i + 30;})} + ], { xaxis: { + // type: 'category', // commented out to rely on autotyping for added realism + categorymode: 'category descending', + categorylist: ['Switch','Bearing','Motor','Seals','Pump','Cord','Plug','Bulb','Fuse','Gear','Leak'] + // this is the expected sorted outcome: ["Switch", "Seals", "Pump", "Plug", "Motor", "Leak", "Gear", "Fuse", "Cord", "Bulb", "Bearing"] + }}); + + expect(gd.calcdata[0][0]).toEqual(jasmine.objectContaining({x: 6, y: 10})); + expect(gd.calcdata[0][1]).toEqual(jasmine.objectContaining({x: 10, y: 11})); + expect(gd.calcdata[0][2]).toEqual(jasmine.objectContaining({x: 4, y: 12})); + + expect(gd.calcdata[1][0]).toEqual(jasmine.objectContaining({x: 0, y: 20})); + expect(gd.calcdata[1][1]).toEqual(jasmine.objectContaining({x: 3, y: 21})); + expect(gd.calcdata[1][2]).toEqual(jasmine.objectContaining({x: 8, y: 22})); + expect(gd.calcdata[1][3]).toEqual(jasmine.objectContaining({x: 7, y: 23})); + expect(gd.calcdata[1][4]).toEqual(jasmine.objectContaining({x: 9, y: 24})); + + expect(gd.calcdata[2][0]).toEqual(jasmine.objectContaining({x: 2, y: 30})); + expect(gd.calcdata[2][1]).toEqual(jasmine.objectContaining({x: 5, y: 31})); + expect(gd.calcdata[2][2]).toEqual(jasmine.objectContaining({x: 10, y: 32})); + expect(gd.calcdata[2][3]).toEqual(jasmine.objectContaining({x: 1, y: 33})); + }); + + it('category order follows categorylist', function() { + + var x1 = ['Gear', 'Bearing', 'Motor']; + var x2 = ['Switch', 'Plug', 'Cord', 'Fuse', 'Bulb']; + var x3 = ['Pump', 'Leak', 'Bearing', 'Seals']; + + Plotly.plot(gd, [ + {x: x1, y: x1.map(function(d, i) {return i + 10;})}, + {x: x2, y: x2.map(function(d, i) {return i + 20;})}, + {x: x3, y: x3.map(function(d, i) {return i + 30;})} + ], { xaxis: { + // type: 'category', // commented out to rely on autotyping for added realism + categorymode: 'array', + categorylist: ['Switch','Bearing','Motor','Seals','Pump','Cord','Plug','Bulb','Fuse','Gear','Leak'] + }}); + + expect(gd.calcdata[0][0]).toEqual(jasmine.objectContaining({x: 9, y: 10})); + expect(gd.calcdata[0][1]).toEqual(jasmine.objectContaining({x: 1, y: 11})); + expect(gd.calcdata[0][2]).toEqual(jasmine.objectContaining({x: 2, y: 12})); + + expect(gd.calcdata[1][0]).toEqual(jasmine.objectContaining({x: 0, y: 20})); + expect(gd.calcdata[1][1]).toEqual(jasmine.objectContaining({x: 6, y: 21})); + expect(gd.calcdata[1][2]).toEqual(jasmine.objectContaining({x: 5, y: 22})); + expect(gd.calcdata[1][3]).toEqual(jasmine.objectContaining({x: 8, y: 23})); + expect(gd.calcdata[1][4]).toEqual(jasmine.objectContaining({x: 7, y: 24})); + + expect(gd.calcdata[2][0]).toEqual(jasmine.objectContaining({x: 4, y: 30})); + expect(gd.calcdata[2][1]).toEqual(jasmine.objectContaining({x: 10, y: 31})); + expect(gd.calcdata[2][2]).toEqual(jasmine.objectContaining({x: 1, y: 32})); + expect(gd.calcdata[2][3]).toEqual(jasmine.objectContaining({x: 3, y: 33})); + }); + }); + + describe('ordering tests in the presence of multiple traces - fully overlapping', function() { + + it('baseline testing for the unordered, fully overlapping case', function() { + + var x1 = ['Gear', 'Bearing', 'Motor']; + var x2 = ['Bearing', 'Gear', 'Motor']; + var x3 = ['Motor', 'Gear', 'Bearing']; + + Plotly.plot(gd, [ + {x: x1, y: x1.map(function(d, i) {return i + 10;})}, + {x: x2, y: x2.map(function(d, i) {return i + 20;})}, + {x: x3, y: x3.map(function(d, i) {return i + 30;})} + ]); + + expect(gd.calcdata[0][0]).toEqual(jasmine.objectContaining({x: 0, y: 10})); + expect(gd.calcdata[0][1]).toEqual(jasmine.objectContaining({x: 1, y: 11})); + expect(gd.calcdata[0][2]).toEqual(jasmine.objectContaining({x: 2, y: 12})); + + expect(gd.calcdata[1][0]).toEqual(jasmine.objectContaining({x: 1, y: 20})); + expect(gd.calcdata[1][1]).toEqual(jasmine.objectContaining({x: 0, y: 21})); + expect(gd.calcdata[1][2]).toEqual(jasmine.objectContaining({x: 2, y: 22})); + + expect(gd.calcdata[2][0]).toEqual(jasmine.objectContaining({x: 2, y: 30})); + expect(gd.calcdata[2][1]).toEqual(jasmine.objectContaining({x: 0, y: 31})); + expect(gd.calcdata[2][2]).toEqual(jasmine.objectContaining({x: 1, y: 32})); + }); + + it('category order follows the trace order (even if categorylist is specified)', function() { + + var x1 = ['Gear', 'Bearing', 'Motor']; + var x2 = ['Bearing', 'Gear', 'Motor']; + var x3 = ['Motor', 'Gear', 'Bearing']; + + Plotly.plot(gd, [ + {x: x1, y: x1.map(function(d, i) {return i + 10;})}, + {x: x2, y: x2.map(function(d, i) {return i + 20;})}, + {x: x3, y: x3.map(function(d, i) {return i + 30;})} + ], { xaxis: { + // type: 'category', // commented out to rely on autotyping for added realism + categorymode: 'trace', + categorylist: ['Switch','Bearing','Motor','Seals','Pump','Cord','Plug','Bulb','Fuse','Gear','Leak'] + }}); + + expect(gd.calcdata[0][0]).toEqual(jasmine.objectContaining({x: 0, y: 10})); + expect(gd.calcdata[0][1]).toEqual(jasmine.objectContaining({x: 1, y: 11})); + expect(gd.calcdata[0][2]).toEqual(jasmine.objectContaining({x: 2, y: 12})); + + expect(gd.calcdata[1][0]).toEqual(jasmine.objectContaining({x: 1, y: 20})); + expect(gd.calcdata[1][1]).toEqual(jasmine.objectContaining({x: 0, y: 21})); + expect(gd.calcdata[1][2]).toEqual(jasmine.objectContaining({x: 2, y: 22})); + + expect(gd.calcdata[2][0]).toEqual(jasmine.objectContaining({x: 2, y: 30})); + expect(gd.calcdata[2][1]).toEqual(jasmine.objectContaining({x: 0, y: 31})); + expect(gd.calcdata[2][2]).toEqual(jasmine.objectContaining({x: 1, y: 32})); + }); + + it('category order is category ascending (even if categorylist is specified)', function() { + + var x1 = ['Gear', 'Bearing', 'Motor']; + var x2 = ['Bearing', 'Gear', 'Motor']; + var x3 = ['Motor', 'Gear', 'Bearing']; + + Plotly.plot(gd, [ + {x: x1, y: x1.map(function(d, i) {return i + 10;})}, + {x: x2, y: x2.map(function(d, i) {return i + 20;})}, + {x: x3, y: x3.map(function(d, i) {return i + 30;})} + ], { xaxis: { + // type: 'category', // commented out to rely on autotyping for added realism + categorymode: 'category ascending', + categorylist: ['Switch','Bearing','Motor','Seals','Pump','Cord','Plug','Bulb','Fuse','Gear','Leak'] + // this is the expected sorted outcome: ['Bearing','Bulb','Cord','Fuse','Gear','Leak','Motor','Plug','Pump','Seals','Switch'] + }}); + + expect(gd.calcdata[0][0]).toEqual(jasmine.objectContaining({x: 1, y: 10})); + expect(gd.calcdata[0][1]).toEqual(jasmine.objectContaining({x: 0, y: 11})); + expect(gd.calcdata[0][2]).toEqual(jasmine.objectContaining({x: 2, y: 12})); + + expect(gd.calcdata[1][0]).toEqual(jasmine.objectContaining({x: 0, y: 20})); + expect(gd.calcdata[1][1]).toEqual(jasmine.objectContaining({x: 1, y: 21})); + expect(gd.calcdata[1][2]).toEqual(jasmine.objectContaining({x: 2, y: 22})); + + expect(gd.calcdata[2][0]).toEqual(jasmine.objectContaining({x: 2, y: 30})); + expect(gd.calcdata[2][1]).toEqual(jasmine.objectContaining({x: 1, y: 31})); + expect(gd.calcdata[2][2]).toEqual(jasmine.objectContaining({x: 0, y: 32})); + }); + + it('category order is category descending (even if categorylist is specified)', function() { + + var x1 = ['Gear', 'Bearing', 'Motor']; + var x2 = ['Bearing', 'Gear', 'Motor']; + var x3 = ['Motor', 'Gear', 'Bearing']; + + Plotly.plot(gd, [ + {x: x1, y: x1.map(function(d, i) {return i + 10;})}, + {x: x2, y: x2.map(function(d, i) {return i + 20;})}, + {x: x3, y: x3.map(function(d, i) {return i + 30;})} + ], { xaxis: { + // type: 'category', // commented out to rely on autotyping for added realism + categorymode: 'category descending', + categorylist: ['Switch','Bearing','Motor','Seals','Pump','Cord','Plug','Bulb','Fuse','Gear','Leak'] + // this is the expected sorted outcome: ["Switch", "Seals", "Pump", "Plug", "Motor", "Leak", "Gear", "Fuse", "Cord", "Bulb", "Bearing"] + }}); + + expect(gd.calcdata[0][0]).toEqual(jasmine.objectContaining({x: 1, y: 10})); + expect(gd.calcdata[0][1]).toEqual(jasmine.objectContaining({x: 2, y: 11})); + expect(gd.calcdata[0][2]).toEqual(jasmine.objectContaining({x: 0, y: 12})); + + expect(gd.calcdata[1][0]).toEqual(jasmine.objectContaining({x: 2, y: 20})); + expect(gd.calcdata[1][1]).toEqual(jasmine.objectContaining({x: 1, y: 21})); + expect(gd.calcdata[1][2]).toEqual(jasmine.objectContaining({x: 0, y: 22})); + + expect(gd.calcdata[2][0]).toEqual(jasmine.objectContaining({x: 0, y: 30})); + expect(gd.calcdata[2][1]).toEqual(jasmine.objectContaining({x: 1, y: 31})); + expect(gd.calcdata[2][2]).toEqual(jasmine.objectContaining({x: 2, y: 32})); + }); + + it('category order follows categorylist', function() { + + var x1 = ['Gear', 'Bearing', 'Motor']; + var x2 = ['Bearing', 'Gear', 'Motor']; + var x3 = ['Motor', 'Gear', 'Bearing']; + + Plotly.plot(gd, [ + {x: x1, y: x1.map(function(d, i) {return i + 10;})}, + {x: x2, y: x2.map(function(d, i) {return i + 20;})}, + {x: x3, y: x3.map(function(d, i) {return i + 30;})} + ], { + xaxis: { + // type: 'category', // commented out to rely on autotyping for added realism + categorymode: 'array', + categorylist: ['Bearing','Motor','Gear'] + } + }); + + expect(gd.calcdata[0][0]).toEqual(jasmine.objectContaining({x: 2, y: 10})); + expect(gd.calcdata[0][1]).toEqual(jasmine.objectContaining({x: 0, y: 11})); + expect(gd.calcdata[0][2]).toEqual(jasmine.objectContaining({x: 1, y: 12})); + + expect(gd.calcdata[1][0]).toEqual(jasmine.objectContaining({x: 0, y: 20})); + expect(gd.calcdata[1][1]).toEqual(jasmine.objectContaining({x: 2, y: 21})); + expect(gd.calcdata[1][2]).toEqual(jasmine.objectContaining({x: 1, y: 22})); + + expect(gd.calcdata[2][0]).toEqual(jasmine.objectContaining({x: 1, y: 30})); + expect(gd.calcdata[2][1]).toEqual(jasmine.objectContaining({x: 2, y: 31})); + expect(gd.calcdata[2][2]).toEqual(jasmine.objectContaining({x: 0, y: 32})); + }); + + it('category order follows categorylist even if data is sparse', function() { + + var x1 = ['Gear', 'Bearing', 'Motor']; + var x2 = ['Bearing', 'Gear', 'Motor']; + var x3 = ['Motor', 'Gear', 'Bearing']; + + Plotly.plot(gd, [ + {x: x1, y: x1.map(function(d, i) {return i + 10;})}, + {x: x2, y: x2.map(function(d, i) {return i + 20;})}, + {x: x3, y: x3.map(function(d, i) {return i + 30;})} + ], { + xaxis: { + // type: 'category', // commented out to rely on autotyping for added realism + categorymode: 'array', + categorylist: ['Switch','Bearing','Motor','Seals','Pump','Cord','Plug','Bulb','Fuse','Gear','Leak'] + } + }); + + expect(gd.calcdata[0][0]).toEqual(jasmine.objectContaining({x: 9, y: 10})); + expect(gd.calcdata[0][1]).toEqual(jasmine.objectContaining({x: 1, y: 11})); + expect(gd.calcdata[0][2]).toEqual(jasmine.objectContaining({x: 2, y: 12})); + + expect(gd.calcdata[1][0]).toEqual(jasmine.objectContaining({x: 1, y: 20})); + expect(gd.calcdata[1][1]).toEqual(jasmine.objectContaining({x: 9, y: 21})); + expect(gd.calcdata[1][2]).toEqual(jasmine.objectContaining({x: 2, y: 22})); + + expect(gd.calcdata[2][0]).toEqual(jasmine.objectContaining({x: 2, y: 30})); + expect(gd.calcdata[2][1]).toEqual(jasmine.objectContaining({x: 9, y: 31})); + expect(gd.calcdata[2][2]).toEqual(jasmine.objectContaining({x: 1, y: 32})); + }); + }); }); }); From 89794c66f4f318a52984d469cf5f03db0086d39a Mon Sep 17 00:00:00 2001 From: Robert Monfera Date: Sat, 9 Apr 2016 10:39:57 +0200 Subject: [PATCH 24/42] #189 adding test cases for combinations of category ordering and stacking (aggregated y value correspondence check) --- test/jasmine/tests/calcdata_test.js | 70 +++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/test/jasmine/tests/calcdata_test.js b/test/jasmine/tests/calcdata_test.js index 4f94d231f39..3453b61ad7a 100644 --- a/test/jasmine/tests/calcdata_test.js +++ b/test/jasmine/tests/calcdata_test.js @@ -788,5 +788,75 @@ describe('calculated data and points', function() { expect(gd.calcdata[2][2]).toEqual(jasmine.objectContaining({x: 1, y: 32})); }); }); + + describe('ordering and stacking combined', function() { + + it('partially overlapping category order follows categorylist and stacking produces expected results', function() { + + var x1 = ['Gear', 'Bearing', 'Motor']; + var x2 = ['Switch', 'Plug', 'Cord', 'Fuse', 'Bulb']; + var x3 = ['Pump', 'Leak', 'Bearing', 'Seals']; + + Plotly.plot(gd, [ + {x: x1, y: x1.map(function(d, i) {return i + 10;}), type: 'bar'}, + {x: x2, y: x2.map(function(d, i) {return i + 20;}), type: 'bar'}, + {x: x3, y: x3.map(function(d, i) {return i + 30;}), type: 'bar'} + ], { + barmode: 'stack', + xaxis: { + // type: 'category', // commented out to rely on autotyping for added realism + categorymode: 'array', + categorylist: ['Switch','Bearing','Motor','Seals','Pump','Cord','Plug','Bulb','Fuse','Gear','Leak'] + } + }); + + expect(gd.calcdata[0][0]).toEqual(jasmine.objectContaining({x: 9, y: 10})); + expect(gd.calcdata[0][1]).toEqual(jasmine.objectContaining({x: 1, y: 11})); + expect(gd.calcdata[0][2]).toEqual(jasmine.objectContaining({x: 2, y: 12})); + + expect(gd.calcdata[1][0]).toEqual(jasmine.objectContaining({x: 0, y: 20})); + expect(gd.calcdata[1][1]).toEqual(jasmine.objectContaining({x: 6, y: 21})); + expect(gd.calcdata[1][2]).toEqual(jasmine.objectContaining({x: 5, y: 22})); + expect(gd.calcdata[1][3]).toEqual(jasmine.objectContaining({x: 8, y: 23})); + expect(gd.calcdata[1][4]).toEqual(jasmine.objectContaining({x: 7, y: 24})); + + expect(gd.calcdata[2][0]).toEqual(jasmine.objectContaining({x: 4, y: 30})); + expect(gd.calcdata[2][1]).toEqual(jasmine.objectContaining({x: 10, y: 31})); + expect(gd.calcdata[2][2]).toEqual(jasmine.objectContaining({x: 1, y: 11 + 32})); + expect(gd.calcdata[2][3]).toEqual(jasmine.objectContaining({x: 3, y: 33})); + }) + + it('fully overlapping - category order follows categorylist and stacking produces expected results', function() { + + var x1 = ['Gear', 'Bearing', 'Motor']; + var x2 = ['Bearing', 'Gear', 'Motor']; + var x3 = ['Motor', 'Gear', 'Bearing']; + + Plotly.plot(gd, [ + {x: x1, y: x1.map(function(d, i) {return i + 10;}), type: 'bar'}, + {x: x2, y: x2.map(function(d, i) {return i + 20;}), type: 'bar'}, + {x: x3, y: x3.map(function(d, i) {return i + 30;}), type: 'bar'} + ], { + barmode: 'stack', + xaxis: { + // type: 'category', // commented out to rely on autotyping for added realism + categorymode: 'array', + categorylist: ['Bearing','Motor','Gear'] + } + }); + + expect(gd.calcdata[0][0]).toEqual(jasmine.objectContaining({x: 2, y: 10})); + expect(gd.calcdata[0][1]).toEqual(jasmine.objectContaining({x: 0, y: 11})); + expect(gd.calcdata[0][2]).toEqual(jasmine.objectContaining({x: 1, y: 12})); + + expect(gd.calcdata[1][0]).toEqual(jasmine.objectContaining({x: 0, y: 11 + 20})); + expect(gd.calcdata[1][1]).toEqual(jasmine.objectContaining({x: 2, y: 10 + 21})); + expect(gd.calcdata[1][2]).toEqual(jasmine.objectContaining({x: 1, y: 12 + 22})); + + expect(gd.calcdata[2][0]).toEqual(jasmine.objectContaining({x: 1, y: 12 + 22 + 30})); + expect(gd.calcdata[2][1]).toEqual(jasmine.objectContaining({x: 2, y: 10 + 21 + 31})); + expect(gd.calcdata[2][2]).toEqual(jasmine.objectContaining({x: 0, y: 11 + 20 + 32})); + }); + }); }); }); From 849978e356fea2febd06f4b352f5afc420801ce1 Mon Sep 17 00:00:00 2001 From: Robert Monfera Date: Sat, 9 Apr 2016 10:44:02 +0200 Subject: [PATCH 25/42] #189 test case linting --- test/jasmine/tests/calcdata_test.js | 362 ++++++++++++++-------------- 1 file changed, 181 insertions(+), 181 deletions(-) diff --git a/test/jasmine/tests/calcdata_test.js b/test/jasmine/tests/calcdata_test.js index 3453b61ad7a..da55cbc40f6 100644 --- a/test/jasmine/tests/calcdata_test.js +++ b/test/jasmine/tests/calcdata_test.js @@ -307,18 +307,18 @@ describe('calculated data and points', function() { {x: x3, y: x3.map(function(d, i) {return i + 30;})} ]); - expect(gd.calcdata[0][0]).toEqual(jasmine.objectContaining({x: 0, y: 10})); - expect(gd.calcdata[0][1]).toEqual(jasmine.objectContaining({x: 1, y: 11})); - expect(gd.calcdata[0][2]).toEqual(jasmine.objectContaining({x: 2, y: 12})); - - expect(gd.calcdata[1][0]).toEqual(jasmine.objectContaining({x: 3, y: 20})); - expect(gd.calcdata[1][1]).toEqual(jasmine.objectContaining({x: 4, y: 21})); - expect(gd.calcdata[1][2]).toEqual(jasmine.objectContaining({x: 5, y: 22})); - expect(gd.calcdata[1][3]).toEqual(jasmine.objectContaining({x: 6, y: 23})); - expect(gd.calcdata[1][4]).toEqual(jasmine.objectContaining({x: 7, y: 24})); - - expect(gd.calcdata[2][0]).toEqual(jasmine.objectContaining({x: 8, y: 30})); - expect(gd.calcdata[2][1]).toEqual(jasmine.objectContaining({x: 9, y: 31})); + expect(gd.calcdata[0][0]).toEqual(jasmine.objectContaining({x: 0, y: 10})); + expect(gd.calcdata[0][1]).toEqual(jasmine.objectContaining({x: 1, y: 11})); + expect(gd.calcdata[0][2]).toEqual(jasmine.objectContaining({x: 2, y: 12})); + + expect(gd.calcdata[1][0]).toEqual(jasmine.objectContaining({x: 3, y: 20})); + expect(gd.calcdata[1][1]).toEqual(jasmine.objectContaining({x: 4, y: 21})); + expect(gd.calcdata[1][2]).toEqual(jasmine.objectContaining({x: 5, y: 22})); + expect(gd.calcdata[1][3]).toEqual(jasmine.objectContaining({x: 6, y: 23})); + expect(gd.calcdata[1][4]).toEqual(jasmine.objectContaining({x: 7, y: 24})); + + expect(gd.calcdata[2][0]).toEqual(jasmine.objectContaining({x: 8, y: 30})); + expect(gd.calcdata[2][1]).toEqual(jasmine.objectContaining({x: 9, y: 31})); expect(gd.calcdata[2][2]).toEqual(jasmine.objectContaining({x: 10, y: 32})); }); @@ -338,18 +338,18 @@ describe('calculated data and points', function() { categorylist: ['Switch','Bearing','Motor','Seals','Pump','Cord','Plug','Bulb','Fuse','Gear','Leak'] }}); - expect(gd.calcdata[0][0]).toEqual(jasmine.objectContaining({x: 0, y: 10})); - expect(gd.calcdata[0][1]).toEqual(jasmine.objectContaining({x: 1, y: 11})); - expect(gd.calcdata[0][2]).toEqual(jasmine.objectContaining({x: 2, y: 12})); + expect(gd.calcdata[0][0]).toEqual(jasmine.objectContaining({x: 0, y: 10})); + expect(gd.calcdata[0][1]).toEqual(jasmine.objectContaining({x: 1, y: 11})); + expect(gd.calcdata[0][2]).toEqual(jasmine.objectContaining({x: 2, y: 12})); - expect(gd.calcdata[1][0]).toEqual(jasmine.objectContaining({x: 3, y: 20})); - expect(gd.calcdata[1][1]).toEqual(jasmine.objectContaining({x: 4, y: 21})); - expect(gd.calcdata[1][2]).toEqual(jasmine.objectContaining({x: 5, y: 22})); - expect(gd.calcdata[1][3]).toEqual(jasmine.objectContaining({x: 6, y: 23})); - expect(gd.calcdata[1][4]).toEqual(jasmine.objectContaining({x: 7, y: 24})); + expect(gd.calcdata[1][0]).toEqual(jasmine.objectContaining({x: 3, y: 20})); + expect(gd.calcdata[1][1]).toEqual(jasmine.objectContaining({x: 4, y: 21})); + expect(gd.calcdata[1][2]).toEqual(jasmine.objectContaining({x: 5, y: 22})); + expect(gd.calcdata[1][3]).toEqual(jasmine.objectContaining({x: 6, y: 23})); + expect(gd.calcdata[1][4]).toEqual(jasmine.objectContaining({x: 7, y: 24})); - expect(gd.calcdata[2][0]).toEqual(jasmine.objectContaining({x: 8, y: 30})); - expect(gd.calcdata[2][1]).toEqual(jasmine.objectContaining({x: 9, y: 31})); + expect(gd.calcdata[2][0]).toEqual(jasmine.objectContaining({x: 8, y: 30})); + expect(gd.calcdata[2][1]).toEqual(jasmine.objectContaining({x: 9, y: 31})); expect(gd.calcdata[2][2]).toEqual(jasmine.objectContaining({x: 10, y: 32})); }); @@ -370,19 +370,19 @@ describe('calculated data and points', function() { // this is the expected sorted outcome: ['Bearing','Bulb','Cord','Fuse','Gear','Leak','Motor','Plug','Pump','Seals','Switch'] }}); - expect(gd.calcdata[0][0]).toEqual(jasmine.objectContaining({x: 4, y: 10})); - expect(gd.calcdata[0][1]).toEqual(jasmine.objectContaining({x: 0, y: 11})); - expect(gd.calcdata[0][2]).toEqual(jasmine.objectContaining({x: 6, y: 12})); + expect(gd.calcdata[0][0]).toEqual(jasmine.objectContaining({x: 4, y: 10})); + expect(gd.calcdata[0][1]).toEqual(jasmine.objectContaining({x: 0, y: 11})); + expect(gd.calcdata[0][2]).toEqual(jasmine.objectContaining({x: 6, y: 12})); expect(gd.calcdata[1][0]).toEqual(jasmine.objectContaining({x: 10, y: 20})); - expect(gd.calcdata[1][1]).toEqual(jasmine.objectContaining({x: 7, y: 21})); - expect(gd.calcdata[1][2]).toEqual(jasmine.objectContaining({x: 2, y: 22})); - expect(gd.calcdata[1][3]).toEqual(jasmine.objectContaining({x: 3, y: 23})); - expect(gd.calcdata[1][4]).toEqual(jasmine.objectContaining({x: 1, y: 24})); + expect(gd.calcdata[1][1]).toEqual(jasmine.objectContaining({x: 7, y: 21})); + expect(gd.calcdata[1][2]).toEqual(jasmine.objectContaining({x: 2, y: 22})); + expect(gd.calcdata[1][3]).toEqual(jasmine.objectContaining({x: 3, y: 23})); + expect(gd.calcdata[1][4]).toEqual(jasmine.objectContaining({x: 1, y: 24})); - expect(gd.calcdata[2][0]).toEqual(jasmine.objectContaining({x: 8, y: 30})); - expect(gd.calcdata[2][1]).toEqual(jasmine.objectContaining({x: 5, y: 31})); - expect(gd.calcdata[2][2]).toEqual(jasmine.objectContaining({x: 9, y: 32})); + expect(gd.calcdata[2][0]).toEqual(jasmine.objectContaining({x: 8, y: 30})); + expect(gd.calcdata[2][1]).toEqual(jasmine.objectContaining({x: 5, y: 31})); + expect(gd.calcdata[2][2]).toEqual(jasmine.objectContaining({x: 9, y: 32})); }); it('category order is category descending (even if categorylist is specified)', function() { @@ -402,19 +402,19 @@ describe('calculated data and points', function() { // this is the expected sorted outcome: ["Switch", "Seals", "Pump", "Plug", "Motor", "Leak", "Gear", "Fuse", "Cord", "Bulb", "Bearing"] }}); - expect(gd.calcdata[0][0]).toEqual(jasmine.objectContaining({x: 6, y: 10})); + expect(gd.calcdata[0][0]).toEqual(jasmine.objectContaining({x: 6, y: 10})); expect(gd.calcdata[0][1]).toEqual(jasmine.objectContaining({x: 10, y: 11})); - expect(gd.calcdata[0][2]).toEqual(jasmine.objectContaining({x: 4, y: 12})); + expect(gd.calcdata[0][2]).toEqual(jasmine.objectContaining({x: 4, y: 12})); - expect(gd.calcdata[1][0]).toEqual(jasmine.objectContaining({x: 0, y: 20})); - expect(gd.calcdata[1][1]).toEqual(jasmine.objectContaining({x: 3, y: 21})); - expect(gd.calcdata[1][2]).toEqual(jasmine.objectContaining({x: 8, y: 22})); - expect(gd.calcdata[1][3]).toEqual(jasmine.objectContaining({x: 7, y: 23})); - expect(gd.calcdata[1][4]).toEqual(jasmine.objectContaining({x: 9, y: 24})); + expect(gd.calcdata[1][0]).toEqual(jasmine.objectContaining({x: 0, y: 20})); + expect(gd.calcdata[1][1]).toEqual(jasmine.objectContaining({x: 3, y: 21})); + expect(gd.calcdata[1][2]).toEqual(jasmine.objectContaining({x: 8, y: 22})); + expect(gd.calcdata[1][3]).toEqual(jasmine.objectContaining({x: 7, y: 23})); + expect(gd.calcdata[1][4]).toEqual(jasmine.objectContaining({x: 9, y: 24})); - expect(gd.calcdata[2][0]).toEqual(jasmine.objectContaining({x: 2, y: 30})); - expect(gd.calcdata[2][1]).toEqual(jasmine.objectContaining({x: 5, y: 31})); - expect(gd.calcdata[2][2]).toEqual(jasmine.objectContaining({x: 1, y: 32})); + expect(gd.calcdata[2][0]).toEqual(jasmine.objectContaining({x: 2, y: 30})); + expect(gd.calcdata[2][1]).toEqual(jasmine.objectContaining({x: 5, y: 31})); + expect(gd.calcdata[2][2]).toEqual(jasmine.objectContaining({x: 1, y: 32})); }); it('category order follows categorylist', function() { @@ -433,19 +433,19 @@ describe('calculated data and points', function() { categorylist: ['Switch','Bearing','Motor','Seals','Pump','Cord','Plug','Bulb','Fuse','Gear','Leak'] }}); - expect(gd.calcdata[0][0]).toEqual(jasmine.objectContaining({x: 9, y: 10})); - expect(gd.calcdata[0][1]).toEqual(jasmine.objectContaining({x: 1, y: 11})); - expect(gd.calcdata[0][2]).toEqual(jasmine.objectContaining({x: 2, y: 12})); + expect(gd.calcdata[0][0]).toEqual(jasmine.objectContaining({x: 9, y: 10})); + expect(gd.calcdata[0][1]).toEqual(jasmine.objectContaining({x: 1, y: 11})); + expect(gd.calcdata[0][2]).toEqual(jasmine.objectContaining({x: 2, y: 12})); - expect(gd.calcdata[1][0]).toEqual(jasmine.objectContaining({x: 0, y: 20})); - expect(gd.calcdata[1][1]).toEqual(jasmine.objectContaining({x: 6, y: 21})); - expect(gd.calcdata[1][2]).toEqual(jasmine.objectContaining({x: 5, y: 22})); - expect(gd.calcdata[1][3]).toEqual(jasmine.objectContaining({x: 8, y: 23})); - expect(gd.calcdata[1][4]).toEqual(jasmine.objectContaining({x: 7, y: 24})); + expect(gd.calcdata[1][0]).toEqual(jasmine.objectContaining({x: 0, y: 20})); + expect(gd.calcdata[1][1]).toEqual(jasmine.objectContaining({x: 6, y: 21})); + expect(gd.calcdata[1][2]).toEqual(jasmine.objectContaining({x: 5, y: 22})); + expect(gd.calcdata[1][3]).toEqual(jasmine.objectContaining({x: 8, y: 23})); + expect(gd.calcdata[1][4]).toEqual(jasmine.objectContaining({x: 7, y: 24})); - expect(gd.calcdata[2][0]).toEqual(jasmine.objectContaining({x: 4, y: 30})); + expect(gd.calcdata[2][0]).toEqual(jasmine.objectContaining({x: 4, y: 30})); expect(gd.calcdata[2][1]).toEqual(jasmine.objectContaining({x: 10, y: 31})); - expect(gd.calcdata[2][2]).toEqual(jasmine.objectContaining({x: 3, y: 32})); + expect(gd.calcdata[2][2]).toEqual(jasmine.objectContaining({x: 3, y: 32})); }); }); @@ -463,19 +463,19 @@ describe('calculated data and points', function() { {x: x3, y: x3.map(function(d, i) {return i + 30;})} ]); - expect(gd.calcdata[0][0]).toEqual(jasmine.objectContaining({x: 0, y: 10})); - expect(gd.calcdata[0][1]).toEqual(jasmine.objectContaining({x: 1, y: 11})); - expect(gd.calcdata[0][2]).toEqual(jasmine.objectContaining({x: 2, y: 12})); + expect(gd.calcdata[0][0]).toEqual(jasmine.objectContaining({x: 0, y: 10})); + expect(gd.calcdata[0][1]).toEqual(jasmine.objectContaining({x: 1, y: 11})); + expect(gd.calcdata[0][2]).toEqual(jasmine.objectContaining({x: 2, y: 12})); - expect(gd.calcdata[1][0]).toEqual(jasmine.objectContaining({x: 3, y: 20})); - expect(gd.calcdata[1][1]).toEqual(jasmine.objectContaining({x: 4, y: 21})); - expect(gd.calcdata[1][2]).toEqual(jasmine.objectContaining({x: 5, y: 22})); - expect(gd.calcdata[1][3]).toEqual(jasmine.objectContaining({x: 6, y: 23})); - expect(gd.calcdata[1][4]).toEqual(jasmine.objectContaining({x: 7, y: 24})); + expect(gd.calcdata[1][0]).toEqual(jasmine.objectContaining({x: 3, y: 20})); + expect(gd.calcdata[1][1]).toEqual(jasmine.objectContaining({x: 4, y: 21})); + expect(gd.calcdata[1][2]).toEqual(jasmine.objectContaining({x: 5, y: 22})); + expect(gd.calcdata[1][3]).toEqual(jasmine.objectContaining({x: 6, y: 23})); + expect(gd.calcdata[1][4]).toEqual(jasmine.objectContaining({x: 7, y: 24})); - expect(gd.calcdata[2][0]).toEqual(jasmine.objectContaining({x: 8, y: 30})); - expect(gd.calcdata[2][1]).toEqual(jasmine.objectContaining({x: 9, y: 31})); - expect(gd.calcdata[2][2]).toEqual(jasmine.objectContaining({x: 1, y: 32})); + expect(gd.calcdata[2][0]).toEqual(jasmine.objectContaining({x: 8, y: 30})); + expect(gd.calcdata[2][1]).toEqual(jasmine.objectContaining({x: 9, y: 31})); + expect(gd.calcdata[2][2]).toEqual(jasmine.objectContaining({x: 1, y: 32})); expect(gd.calcdata[2][3]).toEqual(jasmine.objectContaining({x: 10, y: 33})); }); @@ -495,19 +495,19 @@ describe('calculated data and points', function() { categorylist: ['Switch','Bearing','Motor','Seals','Pump','Cord','Plug','Bulb','Fuse','Gear','Leak'] }}); - expect(gd.calcdata[0][0]).toEqual(jasmine.objectContaining({x: 0, y: 10})); - expect(gd.calcdata[0][1]).toEqual(jasmine.objectContaining({x: 1, y: 11})); - expect(gd.calcdata[0][2]).toEqual(jasmine.objectContaining({x: 2, y: 12})); + expect(gd.calcdata[0][0]).toEqual(jasmine.objectContaining({x: 0, y: 10})); + expect(gd.calcdata[0][1]).toEqual(jasmine.objectContaining({x: 1, y: 11})); + expect(gd.calcdata[0][2]).toEqual(jasmine.objectContaining({x: 2, y: 12})); - expect(gd.calcdata[1][0]).toEqual(jasmine.objectContaining({x: 3, y: 20})); - expect(gd.calcdata[1][1]).toEqual(jasmine.objectContaining({x: 4, y: 21})); - expect(gd.calcdata[1][2]).toEqual(jasmine.objectContaining({x: 5, y: 22})); - expect(gd.calcdata[1][3]).toEqual(jasmine.objectContaining({x: 6, y: 23})); - expect(gd.calcdata[1][4]).toEqual(jasmine.objectContaining({x: 7, y: 24})); + expect(gd.calcdata[1][0]).toEqual(jasmine.objectContaining({x: 3, y: 20})); + expect(gd.calcdata[1][1]).toEqual(jasmine.objectContaining({x: 4, y: 21})); + expect(gd.calcdata[1][2]).toEqual(jasmine.objectContaining({x: 5, y: 22})); + expect(gd.calcdata[1][3]).toEqual(jasmine.objectContaining({x: 6, y: 23})); + expect(gd.calcdata[1][4]).toEqual(jasmine.objectContaining({x: 7, y: 24})); - expect(gd.calcdata[2][0]).toEqual(jasmine.objectContaining({x: 8, y: 30})); - expect(gd.calcdata[2][1]).toEqual(jasmine.objectContaining({x: 9, y: 31})); - expect(gd.calcdata[2][2]).toEqual(jasmine.objectContaining({x: 1, y: 32})); + expect(gd.calcdata[2][0]).toEqual(jasmine.objectContaining({x: 8, y: 30})); + expect(gd.calcdata[2][1]).toEqual(jasmine.objectContaining({x: 9, y: 31})); + expect(gd.calcdata[2][2]).toEqual(jasmine.objectContaining({x: 1, y: 32})); expect(gd.calcdata[2][3]).toEqual(jasmine.objectContaining({x: 10, y: 33})); }); @@ -528,20 +528,20 @@ describe('calculated data and points', function() { // this is the expected sorted outcome: ['Bearing','Bulb','Cord','Fuse','Gear','Leak','Motor','Plug','Pump','Seals','Switch'] }}); - expect(gd.calcdata[0][0]).toEqual(jasmine.objectContaining({x: 4, y: 10})); - expect(gd.calcdata[0][1]).toEqual(jasmine.objectContaining({x: 0, y: 11})); - expect(gd.calcdata[0][2]).toEqual(jasmine.objectContaining({x: 6, y: 12})); + expect(gd.calcdata[0][0]).toEqual(jasmine.objectContaining({x: 4, y: 10})); + expect(gd.calcdata[0][1]).toEqual(jasmine.objectContaining({x: 0, y: 11})); + expect(gd.calcdata[0][2]).toEqual(jasmine.objectContaining({x: 6, y: 12})); expect(gd.calcdata[1][0]).toEqual(jasmine.objectContaining({x: 10, y: 20})); - expect(gd.calcdata[1][1]).toEqual(jasmine.objectContaining({x: 7, y: 21})); - expect(gd.calcdata[1][2]).toEqual(jasmine.objectContaining({x: 2, y: 22})); - expect(gd.calcdata[1][3]).toEqual(jasmine.objectContaining({x: 3, y: 23})); - expect(gd.calcdata[1][4]).toEqual(jasmine.objectContaining({x: 1, y: 24})); + expect(gd.calcdata[1][1]).toEqual(jasmine.objectContaining({x: 7, y: 21})); + expect(gd.calcdata[1][2]).toEqual(jasmine.objectContaining({x: 2, y: 22})); + expect(gd.calcdata[1][3]).toEqual(jasmine.objectContaining({x: 3, y: 23})); + expect(gd.calcdata[1][4]).toEqual(jasmine.objectContaining({x: 1, y: 24})); - expect(gd.calcdata[2][0]).toEqual(jasmine.objectContaining({x: 8, y: 30})); - expect(gd.calcdata[2][1]).toEqual(jasmine.objectContaining({x: 5, y: 31})); - expect(gd.calcdata[2][2]).toEqual(jasmine.objectContaining({x: 0, y: 32})); - expect(gd.calcdata[2][3]).toEqual(jasmine.objectContaining({x: 9, y: 33})); + expect(gd.calcdata[2][0]).toEqual(jasmine.objectContaining({x: 8, y: 30})); + expect(gd.calcdata[2][1]).toEqual(jasmine.objectContaining({x: 5, y: 31})); + expect(gd.calcdata[2][2]).toEqual(jasmine.objectContaining({x: 0, y: 32})); + expect(gd.calcdata[2][3]).toEqual(jasmine.objectContaining({x: 9, y: 33})); }); it('category order is category descending (even if categorylist is specified)', function() { @@ -561,20 +561,20 @@ describe('calculated data and points', function() { // this is the expected sorted outcome: ["Switch", "Seals", "Pump", "Plug", "Motor", "Leak", "Gear", "Fuse", "Cord", "Bulb", "Bearing"] }}); - expect(gd.calcdata[0][0]).toEqual(jasmine.objectContaining({x: 6, y: 10})); + expect(gd.calcdata[0][0]).toEqual(jasmine.objectContaining({x: 6, y: 10})); expect(gd.calcdata[0][1]).toEqual(jasmine.objectContaining({x: 10, y: 11})); - expect(gd.calcdata[0][2]).toEqual(jasmine.objectContaining({x: 4, y: 12})); + expect(gd.calcdata[0][2]).toEqual(jasmine.objectContaining({x: 4, y: 12})); - expect(gd.calcdata[1][0]).toEqual(jasmine.objectContaining({x: 0, y: 20})); - expect(gd.calcdata[1][1]).toEqual(jasmine.objectContaining({x: 3, y: 21})); - expect(gd.calcdata[1][2]).toEqual(jasmine.objectContaining({x: 8, y: 22})); - expect(gd.calcdata[1][3]).toEqual(jasmine.objectContaining({x: 7, y: 23})); - expect(gd.calcdata[1][4]).toEqual(jasmine.objectContaining({x: 9, y: 24})); + expect(gd.calcdata[1][0]).toEqual(jasmine.objectContaining({x: 0, y: 20})); + expect(gd.calcdata[1][1]).toEqual(jasmine.objectContaining({x: 3, y: 21})); + expect(gd.calcdata[1][2]).toEqual(jasmine.objectContaining({x: 8, y: 22})); + expect(gd.calcdata[1][3]).toEqual(jasmine.objectContaining({x: 7, y: 23})); + expect(gd.calcdata[1][4]).toEqual(jasmine.objectContaining({x: 9, y: 24})); - expect(gd.calcdata[2][0]).toEqual(jasmine.objectContaining({x: 2, y: 30})); - expect(gd.calcdata[2][1]).toEqual(jasmine.objectContaining({x: 5, y: 31})); + expect(gd.calcdata[2][0]).toEqual(jasmine.objectContaining({x: 2, y: 30})); + expect(gd.calcdata[2][1]).toEqual(jasmine.objectContaining({x: 5, y: 31})); expect(gd.calcdata[2][2]).toEqual(jasmine.objectContaining({x: 10, y: 32})); - expect(gd.calcdata[2][3]).toEqual(jasmine.objectContaining({x: 1, y: 33})); + expect(gd.calcdata[2][3]).toEqual(jasmine.objectContaining({x: 1, y: 33})); }); it('category order follows categorylist', function() { @@ -593,20 +593,20 @@ describe('calculated data and points', function() { categorylist: ['Switch','Bearing','Motor','Seals','Pump','Cord','Plug','Bulb','Fuse','Gear','Leak'] }}); - expect(gd.calcdata[0][0]).toEqual(jasmine.objectContaining({x: 9, y: 10})); - expect(gd.calcdata[0][1]).toEqual(jasmine.objectContaining({x: 1, y: 11})); - expect(gd.calcdata[0][2]).toEqual(jasmine.objectContaining({x: 2, y: 12})); + expect(gd.calcdata[0][0]).toEqual(jasmine.objectContaining({x: 9, y: 10})); + expect(gd.calcdata[0][1]).toEqual(jasmine.objectContaining({x: 1, y: 11})); + expect(gd.calcdata[0][2]).toEqual(jasmine.objectContaining({x: 2, y: 12})); - expect(gd.calcdata[1][0]).toEqual(jasmine.objectContaining({x: 0, y: 20})); - expect(gd.calcdata[1][1]).toEqual(jasmine.objectContaining({x: 6, y: 21})); - expect(gd.calcdata[1][2]).toEqual(jasmine.objectContaining({x: 5, y: 22})); - expect(gd.calcdata[1][3]).toEqual(jasmine.objectContaining({x: 8, y: 23})); - expect(gd.calcdata[1][4]).toEqual(jasmine.objectContaining({x: 7, y: 24})); + expect(gd.calcdata[1][0]).toEqual(jasmine.objectContaining({x: 0, y: 20})); + expect(gd.calcdata[1][1]).toEqual(jasmine.objectContaining({x: 6, y: 21})); + expect(gd.calcdata[1][2]).toEqual(jasmine.objectContaining({x: 5, y: 22})); + expect(gd.calcdata[1][3]).toEqual(jasmine.objectContaining({x: 8, y: 23})); + expect(gd.calcdata[1][4]).toEqual(jasmine.objectContaining({x: 7, y: 24})); - expect(gd.calcdata[2][0]).toEqual(jasmine.objectContaining({x: 4, y: 30})); + expect(gd.calcdata[2][0]).toEqual(jasmine.objectContaining({x: 4, y: 30})); expect(gd.calcdata[2][1]).toEqual(jasmine.objectContaining({x: 10, y: 31})); - expect(gd.calcdata[2][2]).toEqual(jasmine.objectContaining({x: 1, y: 32})); - expect(gd.calcdata[2][3]).toEqual(jasmine.objectContaining({x: 3, y: 33})); + expect(gd.calcdata[2][2]).toEqual(jasmine.objectContaining({x: 1, y: 32})); + expect(gd.calcdata[2][3]).toEqual(jasmine.objectContaining({x: 3, y: 33})); }); }); @@ -624,17 +624,17 @@ describe('calculated data and points', function() { {x: x3, y: x3.map(function(d, i) {return i + 30;})} ]); - expect(gd.calcdata[0][0]).toEqual(jasmine.objectContaining({x: 0, y: 10})); - expect(gd.calcdata[0][1]).toEqual(jasmine.objectContaining({x: 1, y: 11})); - expect(gd.calcdata[0][2]).toEqual(jasmine.objectContaining({x: 2, y: 12})); + expect(gd.calcdata[0][0]).toEqual(jasmine.objectContaining({x: 0, y: 10})); + expect(gd.calcdata[0][1]).toEqual(jasmine.objectContaining({x: 1, y: 11})); + expect(gd.calcdata[0][2]).toEqual(jasmine.objectContaining({x: 2, y: 12})); - expect(gd.calcdata[1][0]).toEqual(jasmine.objectContaining({x: 1, y: 20})); - expect(gd.calcdata[1][1]).toEqual(jasmine.objectContaining({x: 0, y: 21})); - expect(gd.calcdata[1][2]).toEqual(jasmine.objectContaining({x: 2, y: 22})); + expect(gd.calcdata[1][0]).toEqual(jasmine.objectContaining({x: 1, y: 20})); + expect(gd.calcdata[1][1]).toEqual(jasmine.objectContaining({x: 0, y: 21})); + expect(gd.calcdata[1][2]).toEqual(jasmine.objectContaining({x: 2, y: 22})); - expect(gd.calcdata[2][0]).toEqual(jasmine.objectContaining({x: 2, y: 30})); - expect(gd.calcdata[2][1]).toEqual(jasmine.objectContaining({x: 0, y: 31})); - expect(gd.calcdata[2][2]).toEqual(jasmine.objectContaining({x: 1, y: 32})); + expect(gd.calcdata[2][0]).toEqual(jasmine.objectContaining({x: 2, y: 30})); + expect(gd.calcdata[2][1]).toEqual(jasmine.objectContaining({x: 0, y: 31})); + expect(gd.calcdata[2][2]).toEqual(jasmine.objectContaining({x: 1, y: 32})); }); it('category order follows the trace order (even if categorylist is specified)', function() { @@ -653,17 +653,17 @@ describe('calculated data and points', function() { categorylist: ['Switch','Bearing','Motor','Seals','Pump','Cord','Plug','Bulb','Fuse','Gear','Leak'] }}); - expect(gd.calcdata[0][0]).toEqual(jasmine.objectContaining({x: 0, y: 10})); - expect(gd.calcdata[0][1]).toEqual(jasmine.objectContaining({x: 1, y: 11})); - expect(gd.calcdata[0][2]).toEqual(jasmine.objectContaining({x: 2, y: 12})); + expect(gd.calcdata[0][0]).toEqual(jasmine.objectContaining({x: 0, y: 10})); + expect(gd.calcdata[0][1]).toEqual(jasmine.objectContaining({x: 1, y: 11})); + expect(gd.calcdata[0][2]).toEqual(jasmine.objectContaining({x: 2, y: 12})); - expect(gd.calcdata[1][0]).toEqual(jasmine.objectContaining({x: 1, y: 20})); - expect(gd.calcdata[1][1]).toEqual(jasmine.objectContaining({x: 0, y: 21})); - expect(gd.calcdata[1][2]).toEqual(jasmine.objectContaining({x: 2, y: 22})); + expect(gd.calcdata[1][0]).toEqual(jasmine.objectContaining({x: 1, y: 20})); + expect(gd.calcdata[1][1]).toEqual(jasmine.objectContaining({x: 0, y: 21})); + expect(gd.calcdata[1][2]).toEqual(jasmine.objectContaining({x: 2, y: 22})); - expect(gd.calcdata[2][0]).toEqual(jasmine.objectContaining({x: 2, y: 30})); - expect(gd.calcdata[2][1]).toEqual(jasmine.objectContaining({x: 0, y: 31})); - expect(gd.calcdata[2][2]).toEqual(jasmine.objectContaining({x: 1, y: 32})); + expect(gd.calcdata[2][0]).toEqual(jasmine.objectContaining({x: 2, y: 30})); + expect(gd.calcdata[2][1]).toEqual(jasmine.objectContaining({x: 0, y: 31})); + expect(gd.calcdata[2][2]).toEqual(jasmine.objectContaining({x: 1, y: 32})); }); it('category order is category ascending (even if categorylist is specified)', function() { @@ -683,17 +683,17 @@ describe('calculated data and points', function() { // this is the expected sorted outcome: ['Bearing','Bulb','Cord','Fuse','Gear','Leak','Motor','Plug','Pump','Seals','Switch'] }}); - expect(gd.calcdata[0][0]).toEqual(jasmine.objectContaining({x: 1, y: 10})); - expect(gd.calcdata[0][1]).toEqual(jasmine.objectContaining({x: 0, y: 11})); - expect(gd.calcdata[0][2]).toEqual(jasmine.objectContaining({x: 2, y: 12})); + expect(gd.calcdata[0][0]).toEqual(jasmine.objectContaining({x: 1, y: 10})); + expect(gd.calcdata[0][1]).toEqual(jasmine.objectContaining({x: 0, y: 11})); + expect(gd.calcdata[0][2]).toEqual(jasmine.objectContaining({x: 2, y: 12})); - expect(gd.calcdata[1][0]).toEqual(jasmine.objectContaining({x: 0, y: 20})); - expect(gd.calcdata[1][1]).toEqual(jasmine.objectContaining({x: 1, y: 21})); - expect(gd.calcdata[1][2]).toEqual(jasmine.objectContaining({x: 2, y: 22})); + expect(gd.calcdata[1][0]).toEqual(jasmine.objectContaining({x: 0, y: 20})); + expect(gd.calcdata[1][1]).toEqual(jasmine.objectContaining({x: 1, y: 21})); + expect(gd.calcdata[1][2]).toEqual(jasmine.objectContaining({x: 2, y: 22})); - expect(gd.calcdata[2][0]).toEqual(jasmine.objectContaining({x: 2, y: 30})); - expect(gd.calcdata[2][1]).toEqual(jasmine.objectContaining({x: 1, y: 31})); - expect(gd.calcdata[2][2]).toEqual(jasmine.objectContaining({x: 0, y: 32})); + expect(gd.calcdata[2][0]).toEqual(jasmine.objectContaining({x: 2, y: 30})); + expect(gd.calcdata[2][1]).toEqual(jasmine.objectContaining({x: 1, y: 31})); + expect(gd.calcdata[2][2]).toEqual(jasmine.objectContaining({x: 0, y: 32})); }); it('category order is category descending (even if categorylist is specified)', function() { @@ -713,17 +713,17 @@ describe('calculated data and points', function() { // this is the expected sorted outcome: ["Switch", "Seals", "Pump", "Plug", "Motor", "Leak", "Gear", "Fuse", "Cord", "Bulb", "Bearing"] }}); - expect(gd.calcdata[0][0]).toEqual(jasmine.objectContaining({x: 1, y: 10})); - expect(gd.calcdata[0][1]).toEqual(jasmine.objectContaining({x: 2, y: 11})); - expect(gd.calcdata[0][2]).toEqual(jasmine.objectContaining({x: 0, y: 12})); + expect(gd.calcdata[0][0]).toEqual(jasmine.objectContaining({x: 1, y: 10})); + expect(gd.calcdata[0][1]).toEqual(jasmine.objectContaining({x: 2, y: 11})); + expect(gd.calcdata[0][2]).toEqual(jasmine.objectContaining({x: 0, y: 12})); - expect(gd.calcdata[1][0]).toEqual(jasmine.objectContaining({x: 2, y: 20})); - expect(gd.calcdata[1][1]).toEqual(jasmine.objectContaining({x: 1, y: 21})); - expect(gd.calcdata[1][2]).toEqual(jasmine.objectContaining({x: 0, y: 22})); + expect(gd.calcdata[1][0]).toEqual(jasmine.objectContaining({x: 2, y: 20})); + expect(gd.calcdata[1][1]).toEqual(jasmine.objectContaining({x: 1, y: 21})); + expect(gd.calcdata[1][2]).toEqual(jasmine.objectContaining({x: 0, y: 22})); - expect(gd.calcdata[2][0]).toEqual(jasmine.objectContaining({x: 0, y: 30})); - expect(gd.calcdata[2][1]).toEqual(jasmine.objectContaining({x: 1, y: 31})); - expect(gd.calcdata[2][2]).toEqual(jasmine.objectContaining({x: 2, y: 32})); + expect(gd.calcdata[2][0]).toEqual(jasmine.objectContaining({x: 0, y: 30})); + expect(gd.calcdata[2][1]).toEqual(jasmine.objectContaining({x: 1, y: 31})); + expect(gd.calcdata[2][2]).toEqual(jasmine.objectContaining({x: 2, y: 32})); }); it('category order follows categorylist', function() { @@ -744,17 +744,17 @@ describe('calculated data and points', function() { } }); - expect(gd.calcdata[0][0]).toEqual(jasmine.objectContaining({x: 2, y: 10})); - expect(gd.calcdata[0][1]).toEqual(jasmine.objectContaining({x: 0, y: 11})); - expect(gd.calcdata[0][2]).toEqual(jasmine.objectContaining({x: 1, y: 12})); + expect(gd.calcdata[0][0]).toEqual(jasmine.objectContaining({x: 2, y: 10})); + expect(gd.calcdata[0][1]).toEqual(jasmine.objectContaining({x: 0, y: 11})); + expect(gd.calcdata[0][2]).toEqual(jasmine.objectContaining({x: 1, y: 12})); - expect(gd.calcdata[1][0]).toEqual(jasmine.objectContaining({x: 0, y: 20})); - expect(gd.calcdata[1][1]).toEqual(jasmine.objectContaining({x: 2, y: 21})); - expect(gd.calcdata[1][2]).toEqual(jasmine.objectContaining({x: 1, y: 22})); + expect(gd.calcdata[1][0]).toEqual(jasmine.objectContaining({x: 0, y: 20})); + expect(gd.calcdata[1][1]).toEqual(jasmine.objectContaining({x: 2, y: 21})); + expect(gd.calcdata[1][2]).toEqual(jasmine.objectContaining({x: 1, y: 22})); - expect(gd.calcdata[2][0]).toEqual(jasmine.objectContaining({x: 1, y: 30})); - expect(gd.calcdata[2][1]).toEqual(jasmine.objectContaining({x: 2, y: 31})); - expect(gd.calcdata[2][2]).toEqual(jasmine.objectContaining({x: 0, y: 32})); + expect(gd.calcdata[2][0]).toEqual(jasmine.objectContaining({x: 1, y: 30})); + expect(gd.calcdata[2][1]).toEqual(jasmine.objectContaining({x: 2, y: 31})); + expect(gd.calcdata[2][2]).toEqual(jasmine.objectContaining({x: 0, y: 32})); }); it('category order follows categorylist even if data is sparse', function() { @@ -775,17 +775,17 @@ describe('calculated data and points', function() { } }); - expect(gd.calcdata[0][0]).toEqual(jasmine.objectContaining({x: 9, y: 10})); - expect(gd.calcdata[0][1]).toEqual(jasmine.objectContaining({x: 1, y: 11})); - expect(gd.calcdata[0][2]).toEqual(jasmine.objectContaining({x: 2, y: 12})); + expect(gd.calcdata[0][0]).toEqual(jasmine.objectContaining({x: 9, y: 10})); + expect(gd.calcdata[0][1]).toEqual(jasmine.objectContaining({x: 1, y: 11})); + expect(gd.calcdata[0][2]).toEqual(jasmine.objectContaining({x: 2, y: 12})); - expect(gd.calcdata[1][0]).toEqual(jasmine.objectContaining({x: 1, y: 20})); - expect(gd.calcdata[1][1]).toEqual(jasmine.objectContaining({x: 9, y: 21})); - expect(gd.calcdata[1][2]).toEqual(jasmine.objectContaining({x: 2, y: 22})); + expect(gd.calcdata[1][0]).toEqual(jasmine.objectContaining({x: 1, y: 20})); + expect(gd.calcdata[1][1]).toEqual(jasmine.objectContaining({x: 9, y: 21})); + expect(gd.calcdata[1][2]).toEqual(jasmine.objectContaining({x: 2, y: 22})); - expect(gd.calcdata[2][0]).toEqual(jasmine.objectContaining({x: 2, y: 30})); - expect(gd.calcdata[2][1]).toEqual(jasmine.objectContaining({x: 9, y: 31})); - expect(gd.calcdata[2][2]).toEqual(jasmine.objectContaining({x: 1, y: 32})); + expect(gd.calcdata[2][0]).toEqual(jasmine.objectContaining({x: 2, y: 30})); + expect(gd.calcdata[2][1]).toEqual(jasmine.objectContaining({x: 9, y: 31})); + expect(gd.calcdata[2][2]).toEqual(jasmine.objectContaining({x: 1, y: 32})); }); }); @@ -810,21 +810,21 @@ describe('calculated data and points', function() { } }); - expect(gd.calcdata[0][0]).toEqual(jasmine.objectContaining({x: 9, y: 10})); - expect(gd.calcdata[0][1]).toEqual(jasmine.objectContaining({x: 1, y: 11})); - expect(gd.calcdata[0][2]).toEqual(jasmine.objectContaining({x: 2, y: 12})); + expect(gd.calcdata[0][0]).toEqual(jasmine.objectContaining({x: 9, y: 10})); + expect(gd.calcdata[0][1]).toEqual(jasmine.objectContaining({x: 1, y: 11})); + expect(gd.calcdata[0][2]).toEqual(jasmine.objectContaining({x: 2, y: 12})); - expect(gd.calcdata[1][0]).toEqual(jasmine.objectContaining({x: 0, y: 20})); - expect(gd.calcdata[1][1]).toEqual(jasmine.objectContaining({x: 6, y: 21})); - expect(gd.calcdata[1][2]).toEqual(jasmine.objectContaining({x: 5, y: 22})); - expect(gd.calcdata[1][3]).toEqual(jasmine.objectContaining({x: 8, y: 23})); - expect(gd.calcdata[1][4]).toEqual(jasmine.objectContaining({x: 7, y: 24})); + expect(gd.calcdata[1][0]).toEqual(jasmine.objectContaining({x: 0, y: 20})); + expect(gd.calcdata[1][1]).toEqual(jasmine.objectContaining({x: 6, y: 21})); + expect(gd.calcdata[1][2]).toEqual(jasmine.objectContaining({x: 5, y: 22})); + expect(gd.calcdata[1][3]).toEqual(jasmine.objectContaining({x: 8, y: 23})); + expect(gd.calcdata[1][4]).toEqual(jasmine.objectContaining({x: 7, y: 24})); - expect(gd.calcdata[2][0]).toEqual(jasmine.objectContaining({x: 4, y: 30})); + expect(gd.calcdata[2][0]).toEqual(jasmine.objectContaining({x: 4, y: 30})); expect(gd.calcdata[2][1]).toEqual(jasmine.objectContaining({x: 10, y: 31})); - expect(gd.calcdata[2][2]).toEqual(jasmine.objectContaining({x: 1, y: 11 + 32})); - expect(gd.calcdata[2][3]).toEqual(jasmine.objectContaining({x: 3, y: 33})); - }) + expect(gd.calcdata[2][2]).toEqual(jasmine.objectContaining({x: 1, y: 11 + 32})); + expect(gd.calcdata[2][3]).toEqual(jasmine.objectContaining({x: 3, y: 33})); + }); it('fully overlapping - category order follows categorylist and stacking produces expected results', function() { @@ -845,17 +845,17 @@ describe('calculated data and points', function() { } }); - expect(gd.calcdata[0][0]).toEqual(jasmine.objectContaining({x: 2, y: 10})); - expect(gd.calcdata[0][1]).toEqual(jasmine.objectContaining({x: 0, y: 11})); - expect(gd.calcdata[0][2]).toEqual(jasmine.objectContaining({x: 1, y: 12})); + expect(gd.calcdata[0][0]).toEqual(jasmine.objectContaining({x: 2, y: 10})); + expect(gd.calcdata[0][1]).toEqual(jasmine.objectContaining({x: 0, y: 11})); + expect(gd.calcdata[0][2]).toEqual(jasmine.objectContaining({x: 1, y: 12})); - expect(gd.calcdata[1][0]).toEqual(jasmine.objectContaining({x: 0, y: 11 + 20})); - expect(gd.calcdata[1][1]).toEqual(jasmine.objectContaining({x: 2, y: 10 + 21})); - expect(gd.calcdata[1][2]).toEqual(jasmine.objectContaining({x: 1, y: 12 + 22})); + expect(gd.calcdata[1][0]).toEqual(jasmine.objectContaining({x: 0, y: 11 + 20})); + expect(gd.calcdata[1][1]).toEqual(jasmine.objectContaining({x: 2, y: 10 + 21})); + expect(gd.calcdata[1][2]).toEqual(jasmine.objectContaining({x: 1, y: 12 + 22})); - expect(gd.calcdata[2][0]).toEqual(jasmine.objectContaining({x: 1, y: 12 + 22 + 30})); - expect(gd.calcdata[2][1]).toEqual(jasmine.objectContaining({x: 2, y: 10 + 21 + 31})); - expect(gd.calcdata[2][2]).toEqual(jasmine.objectContaining({x: 0, y: 11 + 20 + 32})); + expect(gd.calcdata[2][0]).toEqual(jasmine.objectContaining({x: 1, y: 12 + 22 + 30})); + expect(gd.calcdata[2][1]).toEqual(jasmine.objectContaining({x: 2, y: 10 + 21 + 31})); + expect(gd.calcdata[2][2]).toEqual(jasmine.objectContaining({x: 0, y: 11 + 20 + 32})); }); }); }); From d6fad10e69ba8da7749f06841193b0868f0c7120 Mon Sep 17 00:00:00 2001 From: Robert Monfera Date: Sat, 9 Apr 2016 11:14:07 +0200 Subject: [PATCH 26/42] #189 adding missing docs and reducing assumptions --- src/plots/cartesian/ordered_categories.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/plots/cartesian/ordered_categories.js b/src/plots/cartesian/ordered_categories.js index 5543d8b8424..8d3ba78cb33 100644 --- a/src/plots/cartesian/ordered_categories.js +++ b/src/plots/cartesian/ordered_categories.js @@ -13,14 +13,20 @@ var d3 = require('d3'); /** - * TODO add documentation + * 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. + * */ module.exports = function orderedCategories(axisLetter, categorymode, categorylist, data) { return categorymode === 'array' ? // just return a copy of the specified array ... - categorylist.slice() : + (Array.isArray(categorylist) ? categorylist : []).slice() : // ... or take the union of all encountered tick keys and sort them as specified // (could be simplified with lodash-fp or ramda) From 7c355b89f99ac81899cc4d76fa8aaa71a688a2b5 Mon Sep 17 00:00:00 2001 From: Robert Monfera Date: Sat, 9 Apr 2016 11:38:10 +0200 Subject: [PATCH 27/42] #189 adding actual DOM axis tick order tests for a couple of less trivial places --- test/jasmine/tests/calcdata_test.js | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/test/jasmine/tests/calcdata_test.js b/test/jasmine/tests/calcdata_test.js index da55cbc40f6..5b799903a3d 100644 --- a/test/jasmine/tests/calcdata_test.js +++ b/test/jasmine/tests/calcdata_test.js @@ -68,8 +68,6 @@ describe('calculated data and points', function() { describe('domain alphanumerical category ordering', function() { - // TODO augment test cases with selection on the DOM to ensure that ticks are there in proper order - it('should output categories in ascending domain alphanumerical order', function() { Plotly.plot(gd, [{x: ['c','a','e','b','d'], y: [15,11,12,13,14]}], { xaxis: { @@ -223,8 +221,6 @@ describe('calculated data and points', function() { it('should output categories in explicitly supplied order even if some missing categories were at the beginning or end of categorylist', function() { - // The auto-range feature currently eliminates unutilized category ticks on the left/right edge - Plotly.plot(gd, [{x: ['c','a','e','b','d'], y: [15,11,12,13,14]}], { xaxis: { type: 'category', categorymode: 'array', @@ -236,6 +232,14 @@ describe('calculated data and points', function() { expect(gd.calcdata[0][2]).toEqual(jasmine.objectContaining({x: 6, y: 12})); expect(gd.calcdata[0][3]).toEqual(jasmine.objectContaining({x: 1, y: 13})); expect(gd.calcdata[0][4]).toEqual(jasmine.objectContaining({x: 4, y: 14})); + + // The auto-range feature currently eliminates unused category ticks on the left/right axis tails. + // The below test case reifies this current behavior, and checks proper order of categories kept. + + var domTickTexts = Array.prototype.slice.call(document.querySelectorAll('g.xtick')) + .map(function(e) {return e.__data__.text;}); + + expect(domTickTexts).toEqual(['b', 'x', 'a', 'd', 'z', 'e', 'c']); // y, q and k has no data points }); it('should output categories in explicitly supplied order even if some missing categories were at the beginning or end of categorylist', function() { @@ -255,6 +259,11 @@ describe('calculated data and points', function() { expect(gd.calcdata[0][2]).toEqual(jasmine.objectContaining({x: 6, y: 12})); expect(gd.calcdata[0][3]).toEqual(jasmine.objectContaining({x: 1, y: 13})); expect(gd.calcdata[0][4]).toEqual(jasmine.objectContaining({x: 4, y: 14})); + + var domTickTexts = Array.prototype.slice.call(document.querySelectorAll('g.xtick')) + .map(function(e) {return e.__data__.text;}); + + expect(domTickTexts).toEqual(['y', 'b', 'x', 'a', 'd', 'z', 'e', 'c']); // q, k has no data; y is null }); it('should output categories in explicitly supplied order even if not all categories are present, and should interact with a null value orthogonally', function() { From 8a2292cd7ddd01c45fd7c689dcf0617127304486 Mon Sep 17 00:00:00 2001 From: Robert Monfera Date: Sat, 9 Apr 2016 15:46:18 +0200 Subject: [PATCH 28/42] #189 rewriting cateogory sorter to O(1) + one sort call --- src/plots/cartesian/ordered_categories.js | 42 +++++++++++++---------- 1 file changed, 24 insertions(+), 18 deletions(-) diff --git a/src/plots/cartesian/ordered_categories.js b/src/plots/cartesian/ordered_categories.js index 8d3ba78cb33..44d79079251 100644 --- a/src/plots/cartesian/ordered_categories.js +++ b/src/plots/cartesian/ordered_categories.js @@ -23,22 +23,28 @@ var d3 = require('d3'); */ module.exports = function orderedCategories(axisLetter, categorymode, categorylist, data) { - return categorymode === 'array' ? - - // just return a copy of the specified array ... - (Array.isArray(categorylist) ? categorylist : []).slice() : - - // ... or take the union of all encountered tick keys and sort them as specified - // (could be simplified with lodash-fp or ramda) - - ['category ascending', 'category descending'].indexOf(categorymode) > -1 ? - - [].concat.apply([], data.map(function(d) {return d[axisLetter];})) - .filter(function(element, index, array) {return index === array.indexOf(element);}) - .sort(({ - 'category ascending': d3.ascending, - 'category descending': d3.descending - })[categorymode]) : - - [].slice(); + if(categorymode === 'array') { + // just return a copy of the specified array, if any + return (Array.isArray(categorylist) ? categorylist : []).slice(); + } else if(['category ascending', 'category descending'].indexOf(categorymode) === -1) { + return [].slice(); + } else { + 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 Object.keys(categoryMap) + .sort(({ + 'category ascending': d3.ascending, + 'category descending': d3.descending + })[categorymode]); + } }; From 9b01bccb263ac9c99e8eb772a0a2797fe973e0a5 Mon Sep 17 00:00:00 2001 From: Robert Monfera Date: Sat, 9 Apr 2016 15:49:57 +0200 Subject: [PATCH 29/42] #189 rewriting cateogory sorter: extract out logic --- src/plots/cartesian/ordered_categories.js | 40 +++++++++++++---------- 1 file changed, 23 insertions(+), 17 deletions(-) diff --git a/src/plots/cartesian/ordered_categories.js b/src/plots/cartesian/ordered_categories.js index 44d79079251..b67b4d3278a 100644 --- a/src/plots/cartesian/ordered_categories.js +++ b/src/plots/cartesian/ordered_categories.js @@ -11,6 +11,27 @@ var d3 = require('d3'); +function flattenUniqueSort(axisLetter, categorymode, 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 Object.keys(categoryMap) + .sort(({ + 'category ascending': d3.ascending, + 'category descending': d3.descending + })[categorymode]); + +} + /** * This pure function returns the ordered categories for specified axisLetter, categorymode, categorylist and data. @@ -21,6 +42,7 @@ var d3 = require('d3'); * order of the unique categories encountered in the data for specified axisLetter. * */ + module.exports = function orderedCategories(axisLetter, categorymode, categorylist, data) { if(categorymode === 'array') { @@ -29,22 +51,6 @@ module.exports = function orderedCategories(axisLetter, categorymode, categoryli } else if(['category ascending', 'category descending'].indexOf(categorymode) === -1) { return [].slice(); } else { - 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 Object.keys(categoryMap) - .sort(({ - 'category ascending': d3.ascending, - 'category descending': d3.descending - })[categorymode]); + return flattenUniqueSort(axisLetter, categorymode, data); } }; From 010451b439f0e410a3cd186cd059ce9d415b2021 Mon Sep 17 00:00:00 2001 From: Robert Monfera Date: Sat, 9 Apr 2016 15:55:40 +0200 Subject: [PATCH 30/42] #189 rewriting cateogory sorter: switching to switch --- src/plots/cartesian/ordered_categories.js | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/src/plots/cartesian/ordered_categories.js b/src/plots/cartesian/ordered_categories.js index b67b4d3278a..c1824b79605 100644 --- a/src/plots/cartesian/ordered_categories.js +++ b/src/plots/cartesian/ordered_categories.js @@ -11,7 +11,7 @@ var d3 = require('d3'); -function flattenUniqueSort(axisLetter, categorymode, data) { +function flattenUniqueSort(axisLetter, sortFunction, data) { var traceLines = data.map(function(d) {return d[axisLetter];}); var categoryMap = {}; // hashmap is O(1); var i, j, tracePoints, category; @@ -25,11 +25,7 @@ function flattenUniqueSort(axisLetter, categorymode, data) { } } return Object.keys(categoryMap) - .sort(({ - 'category ascending': d3.ascending, - 'category descending': d3.descending - })[categorymode]); - + .sort(sortFunction); } @@ -45,12 +41,10 @@ function flattenUniqueSort(axisLetter, categorymode, data) { module.exports = function orderedCategories(axisLetter, categorymode, categorylist, data) { - if(categorymode === 'array') { - // just return a copy of the specified array, if any - return (Array.isArray(categorylist) ? categorylist : []).slice(); - } else if(['category ascending', 'category descending'].indexOf(categorymode) === -1) { - return [].slice(); - } else { - return flattenUniqueSort(axisLetter, categorymode, data); + switch(categorymode) { + case 'array': return (Array.isArray(categorylist) ? categorylist : []).slice(); + case 'category ascending': return flattenUniqueSort(axisLetter, d3.ascending, data); + case 'category descending': return flattenUniqueSort(axisLetter, d3.descending, data); + default: return [].slice(); } }; From 6c9d0b5076a98af652115179f99c959637484d53 Mon Sep 17 00:00:00 2001 From: Robert Monfera Date: Sat, 9 Apr 2016 16:19:48 +0200 Subject: [PATCH 31/42] #189 rewriting cateogory sorter: misc. improvements --- src/plots/cartesian/layout_attributes.js | 4 ++-- src/plots/cartesian/ordered_categories.js | 19 ++++++++++++++----- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/src/plots/cartesian/layout_attributes.js b/src/plots/cartesian/layout_attributes.js index fc6160889a5..328aae57fac 100644 --- a/src/plots/cartesian/layout_attributes.js +++ b/src/plots/cartesian/layout_attributes.js @@ -449,8 +449,8 @@ module.exports = { categorymode: { valType: 'enumerated', values: [ - 'trace', 'category ascending', 'category descending', - /*'value ascending', 'value descending',*/ 'array' // value ascending / descending to be implemented later + 'trace', 'category ascending', 'category descending', 'array' + /*, 'value ascending', 'value descending'*/ // value ascending / descending to be implemented later ], dflt: 'trace', role: 'info', diff --git a/src/plots/cartesian/ordered_categories.js b/src/plots/cartesian/ordered_categories.js index c1824b79605..63ea716c8ec 100644 --- a/src/plots/cartesian/ordered_categories.js +++ b/src/plots/cartesian/ordered_categories.js @@ -11,7 +11,8 @@ var d3 = require('d3'); -function flattenUniqueSort(axisLetter, sortFunction, data) { +// 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; @@ -24,8 +25,12 @@ function flattenUniqueSort(axisLetter, sortFunction, data) { } } } - return Object.keys(categoryMap) - .sort(sortFunction); + return categoryMap; +} + +// flattenUniqueSort :: String -> Function -> [[String]] -> [String] +function flattenUniqueSort(axisLetter, sortFunction, data) { + return Object.keys(flattenUnique(axisLetter, data)).sort(sortFunction); } @@ -37,14 +42,18 @@ function flattenUniqueSort(axisLetter, sortFunction, data) { * 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 : []).slice(); + 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); - default: return [].slice(); + case 'trace': return []; + default: return []; } }; From 166aeb47c357a178d359ca6a27f68e83713af0f8 Mon Sep 17 00:00:00 2001 From: Robert Monfera Date: Tue, 12 Apr 2016 09:46:00 +0200 Subject: [PATCH 32/42] #189 renaming for uniformity (predominantly createGraphDiv() seems to be used across the entire suite) --- test/jasmine/tests/axes_test.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/jasmine/tests/axes_test.js b/test/jasmine/tests/axes_test.js index abb8a3c9953..11008d3cdd5 100644 --- a/test/jasmine/tests/axes_test.js +++ b/test/jasmine/tests/axes_test.js @@ -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'); describe('Test axes', function() { @@ -326,10 +326,10 @@ describe('Test axes', function() { gd; beforeEach(function() { - gd = createGraph(); + gd = createGraphDiv(); }); - afterEach(destroyGraph); + afterEach(destroyGraphDiv); it('should set defaults on bad inputs', function() { var layout = { From e472e145e088a41e6d75d60db3047a5b44459b4a Mon Sep 17 00:00:00 2001 From: Robert Monfera Date: Tue, 12 Apr 2016 14:26:26 +0200 Subject: [PATCH 33/42] #189 initial round of coercions with test cases --- src/plots/cartesian/axis_defaults.js | 2 + src/plots/cartesian/category_mode_defaults.js | 39 ++++++++++++ test/jasmine/tests/axes_test.js | 62 +++++++++++++++++++ 3 files changed, 103 insertions(+) create mode 100644 src/plots/cartesian/category_mode_defaults.js diff --git a/src/plots/cartesian/axis_defaults.js b/src/plots/cartesian/axis_defaults.js index 793b0a51033..24833945f95 100644 --- a/src/plots/cartesian/axis_defaults.js +++ b/src/plots/cartesian/axis_defaults.js @@ -17,6 +17,7 @@ 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'); @@ -96,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'), diff --git a/src/plots/cartesian/category_mode_defaults.js b/src/plots/cartesian/category_mode_defaults.js new file mode 100644 index 00000000000..e0a9de27bcd --- /dev/null +++ b/src/plots/cartesian/category_mode_defaults.js @@ -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') { + + var validCategories = ['trace', 'category ascending', 'category descending', 'array']; + + var properCategoryList = Array.isArray(containerIn.categorylist) && containerIn.categorylist.length > 0; + + if(validCategories.indexOf(containerIn.categorymode) === -1 && properCategoryList) { + + // 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'); + + } + } +}; diff --git a/test/jasmine/tests/axes_test.js b/test/jasmine/tests/axes_test.js index 11008d3cdd5..5f1172306a5 100644 --- a/test/jasmine/tests/axes_test.js +++ b/test/jasmine/tests/axes_test.js @@ -321,6 +321,68 @@ 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, or not 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('handleTickDefaults', function() { var data = [{ x: [1,2,3], y: [3,4,5] }], gd; From d16fe6b13a357b2e603caf6d7e376e5d1dea28b0 Mon Sep 17 00:00:00 2001 From: Robert Monfera Date: Tue, 12 Apr 2016 17:41:59 +0200 Subject: [PATCH 34/42] #189 additional tests for categorymode coercions --- test/jasmine/tests/axes_test.js | 68 +++++++++++++++++++++++++++++++-- 1 file changed, 64 insertions(+), 4 deletions(-) diff --git a/test/jasmine/tests/axes_test.js b/test/jasmine/tests/axes_test.js index 5f1172306a5..b183acfc87b 100644 --- a/test/jasmine/tests/axes_test.js +++ b/test/jasmine/tests/axes_test.js @@ -356,15 +356,15 @@ describe('Test axes', function() { }); - describe('setting, or not setting categorymode to "array"', function() { + 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']} + 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']} @@ -374,11 +374,71 @@ describe('Test axes', function() { 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"} + 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'); + }); + }); }); From 0314f37dae99a29957359f79569e5c081b18e5f9 Mon Sep 17 00:00:00 2001 From: Robert Monfera Date: Tue, 12 Apr 2016 17:55:28 +0200 Subject: [PATCH 35/42] #189 comment fix --- src/plots/cartesian/category_mode_defaults.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plots/cartesian/category_mode_defaults.js b/src/plots/cartesian/category_mode_defaults.js index e0a9de27bcd..ff870ae34d1 100644 --- a/src/plots/cartesian/category_mode_defaults.js +++ b/src/plots/cartesian/category_mode_defaults.js @@ -20,7 +20,7 @@ module.exports = function handleCategoryModeDefaults(containerIn, containerOut, if(validCategories.indexOf(containerIn.categorymode) === -1 && properCategoryList) { // when unspecified or invalid, use the default, unless categorylist implies 'array' - coerce('categorymode', 'array'); // promote to 'array + coerce('categorymode', 'array'); // promote to 'array' } else if(containerIn.categorymode === 'array' && !properCategoryList) { From 043ac1b9d125b35386dc732517620ac8f311952b Mon Sep 17 00:00:00 2001 From: Robert Monfera Date: Tue, 12 Apr 2016 18:27:14 +0200 Subject: [PATCH 36/42] #189 PR feedback and linting --- src/plots/cartesian/category_mode_defaults.js | 30 +++++++++---------- test/jasmine/tests/axes_test.js | 2 +- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/plots/cartesian/category_mode_defaults.js b/src/plots/cartesian/category_mode_defaults.js index ff870ae34d1..d671941d5ee 100644 --- a/src/plots/cartesian/category_mode_defaults.js +++ b/src/plots/cartesian/category_mode_defaults.js @@ -6,34 +6,34 @@ * LICENSE file in the root directory of this source tree. */ - 'use strict'; +var layoutAttributes = require('./layout_attributes'); + module.exports = function handleCategoryModeDefaults(containerIn, containerOut, coerce) { - if(containerIn.type === 'category') { + if(containerIn.type !== 'category') return; - var validCategories = ['trace', 'category ascending', 'category descending', 'array']; + var validCategories = layoutAttributes.categorymode.values; - var properCategoryList = Array.isArray(containerIn.categorylist) && containerIn.categorylist.length > 0; + var properCategoryList = Array.isArray(containerIn.categorylist) && containerIn.categorylist.length > 0; - if(validCategories.indexOf(containerIn.categorymode) === -1 && properCategoryList) { + if(validCategories.indexOf(containerIn.categorymode) === -1 && properCategoryList) { - // when unspecified or invalid, use the default, unless categorylist implies 'array' - coerce('categorymode', 'array'); // promote to 'array' + // when unspecified or invalid, use the default, unless categorylist implies 'array' + coerce('categorymode', 'array'); // promote to 'array' - } else if(containerIn.categorymode === 'array' && !properCategoryList) { + } else if(containerIn.categorymode === 'array' && !properCategoryList) { - // when mode is 'array' but no list is given, revert to default + // when mode is 'array' but no list is given, revert to default - containerIn.categorymode = 'trace'; // revert to default - coerce('categorymode'); + containerIn.categorymode = 'trace'; // revert to default + coerce('categorymode'); - } else { + } else { - // otherwise use the supplied mode, or the default one if unsupplied or invalid - coerce('categorymode'); + // otherwise use the supplied mode, or the default one if unsupplied or invalid + coerce('categorymode'); - } } }; diff --git a/test/jasmine/tests/axes_test.js b/test/jasmine/tests/axes_test.js index b183acfc87b..717cdc6148d 100644 --- a/test/jasmine/tests/axes_test.js +++ b/test/jasmine/tests/axes_test.js @@ -364,7 +364,7 @@ describe('Test axes', function() { }); 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']} From b98bd6515a7c84495bd6ceb61984f161835c82a1 Mon Sep 17 00:00:00 2001 From: Robert Monfera Date: Tue, 12 Apr 2016 19:28:32 +0200 Subject: [PATCH 37/42] #189 image based regression test JSONs --- test/image/mocks/axes_category_ascending.json | 46 +++++++++++++++++++ .../mocks/axes_category_categorylist.json | 46 +++++++++++++++++++ .../image/mocks/axes_category_descending.json | 46 +++++++++++++++++++ 3 files changed, 138 insertions(+) create mode 100644 test/image/mocks/axes_category_ascending.json create mode 100644 test/image/mocks/axes_category_categorylist.json create mode 100644 test/image/mocks/axes_category_descending.json diff --git a/test/image/mocks/axes_category_ascending.json b/test/image/mocks/axes_category_ascending.json new file mode 100644 index 00000000000..3cdbb657dc3 --- /dev/null +++ b/test/image/mocks/axes_category_ascending.json @@ -0,0 +1,46 @@ +{ + "data": [ + { + "x": [ + 5, + 1, + null, + 2, + 4 + ], + "y": [ + 1, + 2, + 3, + 4, + 5 + ], + "connectgaps": false, + "uid": "8ac13a" + } + ], + "layout": { + "title": "category ascending", + "xaxis": { + "type": "category", + "range": [ + -0.18336673346693386, + 3.1833667334669338 + ], + "autorange": true, + "categorymode": "category ascending", + "categorylist": [2,4,5,1] + }, + "yaxis": { + "type": "linear", + "range": [ + 0.7070063694267517, + 5.292993630573249 + ], + "autorange": true + }, + "height": 450, + "width": 1000, + "autosize": true + } +} diff --git a/test/image/mocks/axes_category_categorylist.json b/test/image/mocks/axes_category_categorylist.json new file mode 100644 index 00000000000..9e13216b552 --- /dev/null +++ b/test/image/mocks/axes_category_categorylist.json @@ -0,0 +1,46 @@ +{ + "data": [ + { + "x": [ + 1, + 2, + null, + 4, + 5 + ], + "y": [ + 1, + 2, + 3, + 4, + 5 + ], + "connectgaps": false, + "uid": "8ac13a" + } + ], + "layout": { + "title": "categorylist", + "xaxis": { + "type": "category", + "range": [ + -0.18336673346693386, + 3.1833667334669338 + ], + "autorange": true, + "categorymode": "array", + "categorylist": [2,4,5,1] + }, + "yaxis": { + "type": "linear", + "range": [ + 0.7070063694267517, + 5.292993630573249 + ], + "autorange": true + }, + "height": 450, + "width": 1000, + "autosize": true + } +} diff --git a/test/image/mocks/axes_category_descending.json b/test/image/mocks/axes_category_descending.json new file mode 100644 index 00000000000..f7298ef2abf --- /dev/null +++ b/test/image/mocks/axes_category_descending.json @@ -0,0 +1,46 @@ +{ + "data": [ + { + "x": [ + 5, + 1, + null, + 2, + 4 + ], + "y": [ + 1, + 2, + 3, + 4, + 5 + ], + "connectgaps": false, + "uid": "8ac13a" + } + ], + "layout": { + "title": "category descending", + "xaxis": { + "type": "category", + "range": [ + -0.18336673346693386, + 3.1833667334669338 + ], + "autorange": true, + "categorymode": "category descending", + "categorylist": [2,4,5,1] + }, + "yaxis": { + "type": "linear", + "range": [ + 0.7070063694267517, + 5.292993630573249 + ], + "autorange": true + }, + "height": 450, + "width": 1000, + "autosize": true + } +} From e007f730faf7c9620366b71b754497c1190f4f32 Mon Sep 17 00:00:00 2001 From: Robert Monfera Date: Tue, 12 Apr 2016 21:59:03 +0200 Subject: [PATCH 38/42] #189 reworking the order code because it converted numbers to strings (with test cases) --- src/plots/cartesian/ordered_categories.js | 16 ++++++++----- test/jasmine/tests/calcdata_test.js | 29 +++++++++++++++++++++++ 2 files changed, 39 insertions(+), 6 deletions(-) diff --git a/src/plots/cartesian/ordered_categories.js b/src/plots/cartesian/ordered_categories.js index 63ea716c8ec..8527345f63f 100644 --- a/src/plots/cartesian/ordered_categories.js +++ b/src/plots/cartesian/ordered_categories.js @@ -14,23 +14,27 @@ 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); + // 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. + var categoryArray = []; 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; + if(category === null || category === undefined) continue; + if(categoryArray.indexOf(category) === -1) { + categoryArray.push(category); } } } - return categoryMap; + return categoryArray; } // flattenUniqueSort :: String -> Function -> [[String]] -> [String] function flattenUniqueSort(axisLetter, sortFunction, data) { - return Object.keys(flattenUnique(axisLetter, data)).sort(sortFunction); + return flattenUnique(axisLetter, data).sort(sortFunction); } @@ -50,7 +54,7 @@ function flattenUniqueSort(axisLetter, sortFunction, data) { module.exports = function orderedCategories(axisLetter, categorymode, categorylist, data) { switch(categorymode) { - case 'array': return Array.isArray(categorylist) ? categorylist : []; + case 'array': return Array.isArray(categorylist) ? categorylist.slice() : []; case 'category ascending': return flattenUniqueSort(axisLetter, d3.ascending, data); case 'category descending': return flattenUniqueSort(axisLetter, d3.descending, data); case 'trace': return []; diff --git a/test/jasmine/tests/calcdata_test.js b/test/jasmine/tests/calcdata_test.js index 5b799903a3d..8c20fca44d6 100644 --- a/test/jasmine/tests/calcdata_test.js +++ b/test/jasmine/tests/calcdata_test.js @@ -96,6 +96,20 @@ describe('calculated data and points', function() { expect(gd.calcdata[0][4]).toEqual(jasmine.objectContaining({x: 1, y: 14})); }); + it('should output categories in ascending domain alphanumerical order even if categories are all numbers', function() { + + Plotly.plot(gd, [{x: [3,1,5,2,4], y: [15,11,12,13,14]}], { xaxis: { + type: 'category', + categorymode: 'category ascending' + }}); + + expect(gd.calcdata[0][0]).toEqual(jasmine.objectContaining({x: 2, y: 15})); + expect(gd.calcdata[0][1]).toEqual(jasmine.objectContaining({x: 0, y: 11})); + expect(gd.calcdata[0][2]).toEqual(jasmine.objectContaining({x: 4, y: 12})); + expect(gd.calcdata[0][3]).toEqual(jasmine.objectContaining({x: 1, y: 13})); + expect(gd.calcdata[0][4]).toEqual(jasmine.objectContaining({x: 3, y: 14})); + }); + it('should output categories in categorymode order even if category array is defined', function() { Plotly.plot(gd, [{x: ['c','a','e','b','d'], y: [15,11,12,13,14]}], { xaxis: { @@ -189,6 +203,21 @@ describe('calculated data and points', function() { expect(gd.calcdata[0][4]).toEqual(jasmine.objectContaining({x: 2, y: 14})); }); + it('should output categories in explicitly supplied order even if category values are all numbers', function() { + + Plotly.plot(gd, [{x: [3,1,5,2,4], y: [15,11,12,13,14]}], { xaxis: { + type: 'category', + categorymode: 'array', + categorylist: [2,1,4,5,3] + }}); + + expect(gd.calcdata[0][0]).toEqual(jasmine.objectContaining({x: 4, y: 15})); + expect(gd.calcdata[0][1]).toEqual(jasmine.objectContaining({x: 1, y: 11})); + expect(gd.calcdata[0][2]).toEqual(jasmine.objectContaining({x: 3, y: 12})); + expect(gd.calcdata[0][3]).toEqual(jasmine.objectContaining({x: 0, y: 13})); + expect(gd.calcdata[0][4]).toEqual(jasmine.objectContaining({x: 2, y: 14})); + }); + it('should output categories in explicitly supplied order, independent of trace order, pruned', function() { Plotly.plot(gd, [{x: ['c',undefined,'e','b','d'], y: [15,11,12,null,14]}], { xaxis: { From 34a5054e8bff58e2082a28d081744ee8c9c87fb1 Mon Sep 17 00:00:00 2001 From: Robert Monfera Date: Tue, 12 Apr 2016 22:00:19 +0200 Subject: [PATCH 39/42] #189 adding image based tests --- .../baselines/axes_category_ascending.png | Bin 0 -> 26925 bytes .../baselines/axes_category_categorylist.png | Bin 0 -> 21133 bytes ..._category_categorylist_truncated_tails.png | Bin 0 -> 31892 bytes .../baselines/axes_category_descending.png | Bin 0 -> 25538 bytes .../axes_category_descending_with_gaps.png | Bin 0 -> 17460 bytes test/image/mocks/axes_category_ascending.json | 45 +++--------------- ...category_categorylist_truncated_tails.json | 13 +++++ .../image/mocks/axes_category_descending.json | 7 +-- .../axes_category_descending_with_gaps.json | 41 ++++++++++++++++ 9 files changed, 61 insertions(+), 45 deletions(-) create mode 100644 test/image/baselines/axes_category_ascending.png create mode 100644 test/image/baselines/axes_category_categorylist.png create mode 100644 test/image/baselines/axes_category_categorylist_truncated_tails.png create mode 100644 test/image/baselines/axes_category_descending.png create mode 100644 test/image/baselines/axes_category_descending_with_gaps.png create mode 100644 test/image/mocks/axes_category_categorylist_truncated_tails.json create mode 100644 test/image/mocks/axes_category_descending_with_gaps.json diff --git a/test/image/baselines/axes_category_ascending.png b/test/image/baselines/axes_category_ascending.png new file mode 100644 index 0000000000000000000000000000000000000000..f6252c4594b4e17e6a0343a35ed341a19e3f5093 GIT binary patch literal 26925 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 He&xRazhdSc literal 0 HcmV?d00001 diff --git a/test/image/baselines/axes_category_categorylist.png b/test/image/baselines/axes_category_categorylist.png new file mode 100644 index 0000000000000000000000000000000000000000..b29a3262d6dc69ac4c01c9b4d70a4ea87c43c4ee GIT binary patch literal 21133 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 E0ErLsYXATM literal 0 HcmV?d00001 diff --git a/test/image/baselines/axes_category_categorylist_truncated_tails.png b/test/image/baselines/axes_category_categorylist_truncated_tails.png new file mode 100644 index 0000000000000000000000000000000000000000..f7c0bbc461bf3cc002ae556fa16a578319e58d2e GIT binary patch literal 31892 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

%&dt5 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 NC)IRR-yOT?{U4RIED!(y literal 0 HcmV?d00001 diff --git a/test/image/baselines/axes_category_descending.png b/test/image/baselines/axes_category_descending.png new file mode 100644 index 0000000000000000000000000000000000000000..6eb5d95693bddb2c1e25a6995ef88cb36ca94cdc GIT binary patch literal 25538 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 Xg^o(^i-o&6R$1&jU{bK>M8v-Ukmmk7 literal 0 HcmV?d00001 diff --git a/test/image/baselines/axes_category_descending_with_gaps.png b/test/image/baselines/axes_category_descending_with_gaps.png new file mode 100644 index 0000000000000000000000000000000000000000..6981f01ca0b4641f6e5fe0dd3cf0993d58aaa9e3 GIT binary patch literal 17460 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 literal 0 HcmV?d00001 diff --git a/test/image/mocks/axes_category_ascending.json b/test/image/mocks/axes_category_ascending.json index 3cdbb657dc3..29f76d87465 100644 --- a/test/image/mocks/axes_category_ascending.json +++ b/test/image/mocks/axes_category_ascending.json @@ -1,46 +1,13 @@ { - "data": [ - { - "x": [ - 5, - 1, - null, - 2, - 4 - ], - "y": [ - 1, - 2, - 3, - 4, - 5 - ], - "connectgaps": false, - "uid": "8ac13a" - } + "data": [{ + "x": ["c","a","e","b","d"], + "y": [15,11,12,13,14]} ], "layout": { - "title": "category ascending", "xaxis": { + "title": "category ascending", "type": "category", - "range": [ - -0.18336673346693386, - 3.1833667334669338 - ], - "autorange": true, "categorymode": "category ascending", - "categorylist": [2,4,5,1] - }, - "yaxis": { - "type": "linear", - "range": [ - 0.7070063694267517, - 5.292993630573249 - ], - "autorange": true - }, - "height": 450, - "width": 1000, - "autosize": true - } + "categorylist": ["y","b","x","a","d","z","e","c", "q", "k"] + }} } diff --git a/test/image/mocks/axes_category_categorylist_truncated_tails.json b/test/image/mocks/axes_category_categorylist_truncated_tails.json new file mode 100644 index 00000000000..c2c9793bc9f --- /dev/null +++ b/test/image/mocks/axes_category_categorylist_truncated_tails.json @@ -0,0 +1,13 @@ +{ + "data": [{ + "x": ["c","a","e","b","d"], + "y": [15,11,12,13,14]} + ], + "layout": { + "title": "categorylist with truncated tails (y, q, k not plotted)", + "xaxis": { + "type": "category", + "categorymode": "array", + "categorylist": ["y","b","x","a","d","z","e","c", "q", "k"] + }} +} diff --git a/test/image/mocks/axes_category_descending.json b/test/image/mocks/axes_category_descending.json index f7298ef2abf..0df8fc761ca 100644 --- a/test/image/mocks/axes_category_descending.json +++ b/test/image/mocks/axes_category_descending.json @@ -4,7 +4,7 @@ "x": [ 5, 1, - null, + 3, 2, 4 ], @@ -23,11 +23,6 @@ "title": "category descending", "xaxis": { "type": "category", - "range": [ - -0.18336673346693386, - 3.1833667334669338 - ], - "autorange": true, "categorymode": "category descending", "categorylist": [2,4,5,1] }, diff --git a/test/image/mocks/axes_category_descending_with_gaps.json b/test/image/mocks/axes_category_descending_with_gaps.json new file mode 100644 index 00000000000..cea8a7b1a41 --- /dev/null +++ b/test/image/mocks/axes_category_descending_with_gaps.json @@ -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", + "categorymode": "category descending", + "categorylist": [2,4,5,1] + }, + "yaxis": { + "type": "linear", + "range": [ + 0.7070063694267517, + 5.292993630573249 + ], + "autorange": true + }, + "height": 450, + "width": 1000, + "autosize": true + } +} From c6d44d9e9788c747d7c094455089fe632dca7ae4 Mon Sep 17 00:00:00 2001 From: Robert Monfera Date: Tue, 12 Apr 2016 22:06:55 +0200 Subject: [PATCH 40/42] #189 comment update --- src/plots/cartesian/ordered_categories.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/plots/cartesian/ordered_categories.js b/src/plots/cartesian/ordered_categories.js index 8527345f63f..e7cb9714a74 100644 --- a/src/plots/cartesian/ordered_categories.js +++ b/src/plots/cartesian/ordered_categories.js @@ -16,7 +16,9 @@ function flattenUnique(axisLetter, data) { var traceLines = data.map(function(d) {return d[axisLetter];}); // 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. + // downgrading to this O(n) array on the first encounter of a non-string value. + // Another possible speedup is bisection, but it's probably slower on the small array + // sizes typical of categorical axis values. var categoryArray = []; var i, j, tracePoints, category; for(i = 0; i < traceLines.length; i++) { From 44a0c3d0f3e98f5058948a6fcf3af3be58e3a875 Mon Sep 17 00:00:00 2001 From: Robert Monfera Date: Tue, 12 Apr 2016 23:04:50 +0200 Subject: [PATCH 41/42] #189 switching from O(n) to O(log(N)) complexity unique insertion sort --- src/plots/cartesian/ordered_categories.js | 42 +++++++++++++++-------- 1 file changed, 27 insertions(+), 15 deletions(-) diff --git a/src/plots/cartesian/ordered_categories.js b/src/plots/cartesian/ordered_categories.js index e7cb9714a74..5f9f25a485c 100644 --- a/src/plots/cartesian/ordered_categories.js +++ b/src/plots/cartesian/ordered_categories.js @@ -11,32 +11,44 @@ var d3 = require('d3'); -// flattenUnique :: String -> [[String]] -> Object -function flattenUnique(axisLetter, data) { - var traceLines = data.map(function(d) {return d[axisLetter];}); +// 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(n) array on the first encounter of a non-string value. - // Another possible speedup is bisection, but it's probably slower on the small array - // sizes typical of categorical axis values. + // downgrading to this O(log(n)) array on the first encounter of a non-string value. + var categoryArray = []; - var i, j, tracePoints, category; + + 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; - if(categoryArray.indexOf(category) === -1) { - categoryArray.push(category); - } + + 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; -} -// flattenUniqueSort :: String -> Function -> [[String]] -> [String] -function flattenUniqueSort(axisLetter, sortFunction, data) { - return flattenUnique(axisLetter, data).sort(sortFunction); + return categoryArray; } From 613fda3e3b0f189753bb9a8d30c9e9d90a77b5ad Mon Sep 17 00:00:00 2001 From: Robert Monfera Date: Wed, 13 Apr 2016 00:19:57 +0200 Subject: [PATCH 42/42] #189 adding axis attributes to 3D plots --- src/plots/gl3d/layout/axis_attributes.js | 29 ++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/src/plots/gl3d/layout/axis_attributes.js b/src/plots/gl3d/layout/axis_attributes.js index dbc8811bf18..9055144f57c 100644 --- a/src/plots/gl3d/layout/axis_attributes.js +++ b/src/plots/gl3d/layout/axis_attributes.js @@ -67,6 +67,35 @@ module.exports = { dflt: true, description: 'Sets whether or not this axis is labeled' }, + 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(' ') + }, title: axesAttrs.title, titlefont: axesAttrs.titlefont, type: axesAttrs.type,