diff --git a/src/plot_api/plot_api.js b/src/plot_api/plot_api.js index f94b03c959a..983d6e85bfb 100644 --- a/src/plot_api/plot_api.js +++ b/src/plot_api/plot_api.js @@ -854,10 +854,10 @@ function doCalcdata(gd) { fullLayout._piecolormap = {}; fullLayout._piedefaultcolorcount = 0; - // delete category list, if there is one, so we start over + // initialize the category list, if there is one, so we start over // to be filled in later by ax.d2c for(i = 0; i < axList.length; i++) { - axList[i]._categories = []; + axList[i]._categories = axList[i]._initialCategories.slice(); } for(i = 0; i < fullData.length; i++) { diff --git a/src/plots/cartesian/axis_defaults.js b/src/plots/cartesian/axis_defaults.js index f7aa309e20b..45f34895978 100644 --- a/src/plots/cartesian/axis_defaults.js +++ b/src/plots/cartesian/axis_defaults.js @@ -20,7 +20,9 @@ var layoutAttributes = require('./layout_attributes'); var handleTickValueDefaults = require('./tick_value_defaults'); var handleTickMarkDefaults = require('./tick_mark_defaults'); var handleTickLabelDefaults = require('./tick_label_defaults'); +var handleCategoryOrderDefaults = require('./category_order_defaults'); var setConvert = require('./set_convert'); +var orderedCategories = require('./ordered_categories'); var cleanDatum = require('./clean_datum'); var axisIds = require('./axis_ids'); @@ -72,6 +74,10 @@ module.exports = function handleAxisDefaults(containerIn, containerOut, coerce, } } + containerOut._initialCategories = axType === 'category' ? + orderedCategories(letter, containerIn.categoryorder, containerIn.categoryarray, options.data) : + []; + setConvert(containerOut); var dfltColor = coerce('color'); @@ -105,6 +111,7 @@ module.exports = function handleAxisDefaults(containerIn, containerOut, coerce, handleTickValueDefaults(containerIn, containerOut, coerce, axType); handleTickLabelDefaults(containerIn, containerOut, coerce, axType, options); handleTickMarkDefaults(containerIn, containerOut, coerce, options); + handleCategoryOrderDefaults(containerIn, containerOut, coerce); var lineColor = coerce2('linecolor', dfltColor), lineWidth = coerce2('linewidth'), diff --git a/src/plots/cartesian/category_order_defaults.js b/src/plots/cartesian/category_order_defaults.js new file mode 100644 index 00000000000..39da91ba992 --- /dev/null +++ b/src/plots/cartesian/category_order_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'; + +var layoutAttributes = require('./layout_attributes'); + +module.exports = function handleCategoryOrderDefaults(containerIn, containerOut, coerce) { + + if(containerIn.type !== 'category') return; + + var validCategories = layoutAttributes.categoryorder.values; + + var propercategoryarray = Array.isArray(containerIn.categoryarray) && containerIn.categoryarray.length > 0; + + if(validCategories.indexOf(containerIn.categoryorder) === -1 && propercategoryarray) { + + // when unspecified or invalid, use the default, unless categoryarray implies 'array' + coerce('categoryorder', 'array'); // promote to 'array' + + } else if(containerIn.categoryorder === 'array' && !propercategoryarray) { + + // when mode is 'array' but no list is given, revert to default + + containerIn.categoryorder = 'trace'; // revert to default + coerce('categoryorder'); + + } else { + + // otherwise use the supplied mode, or the default one if unsupplied or invalid + coerce('categoryorder'); + + } +}; diff --git a/src/plots/cartesian/layout_attributes.js b/src/plots/cartesian/layout_attributes.js index 731d9b9d81d..43de76d0ce1 100644 --- a/src/plots/cartesian/layout_attributes.js +++ b/src/plots/cartesian/layout_attributes.js @@ -465,6 +465,36 @@ module.exports = { 'Only has an effect if `anchor` is set to *free*.' ].join(' ') }, + categoryorder: { + valType: 'enumerated', + values: [ + 'trace', 'category ascending', 'category descending', 'array' + /*, 'value ascending', 'value descending'*/ // value ascending / descending to be implemented later + ], + dflt: 'trace', + role: 'info', + description: [ + 'Specifies the ordering logic for the case of categorical variables.', + 'By default, plotly uses *trace*, which specifies the order that is present in the data supplied.', + 'Set `categoryorder` to *category ascending* or *category descending* if order should be determined by', + 'the alphanumerical order of the category names.', + /*'Set `categoryorder` to *value ascending* or *value descending* if order should be determined by the', + 'numerical order of the values.',*/ // // value ascending / descending to be implemented later + 'Set `categoryorder` to *array* to derive the ordering from the attribute `categoryarray`. If a category', + 'is not found in the `categoryarray` array, the sorting behavior for that attribute will be identical to', + 'the *trace* mode. The unspecified categories will follow the categories in `categoryarray`.' + ].join(' ') + }, + categoryarray: { + valType: 'data_array', + role: 'info', + description: [ + 'Sets the order in which categories on this axis appear.', + 'Only has an effect if `categoryorder` is set to *array*.', + 'Used with `categoryorder`.' + ].join(' ') + }, + _deprecated: { autotick: { diff --git a/src/plots/cartesian/ordered_categories.js b/src/plots/cartesian/ordered_categories.js new file mode 100644 index 00000000000..546578ecb15 --- /dev/null +++ b/src/plots/cartesian/ordered_categories.js @@ -0,0 +1,77 @@ +/** +* Copyright 2012-2016, Plotly, Inc. +* All rights reserved. +* +* This source code is licensed under the MIT license found in the +* LICENSE file in the root directory of this source tree. +*/ + + +'use strict'; + +var d3 = require('d3'); + +// flattenUniqueSort :: String -> Function -> [[String]] -> [String] +function flattenUniqueSort(axisLetter, sortFunction, data) { + + // Bisection based insertion sort of distinct values for logarithmic time complexity. + // Can't use a hashmap, which is O(1), because ES5 maps coerce keys to strings. If it ever becomes a bottleneck, + // code can be separated: a hashmap (JS object) based version if all values encountered are strings; and + // downgrading to this O(log(n)) array on the first encounter of a non-string value. + + var categoryArray = []; + + var traceLines = data.map(function(d) {return d[axisLetter];}); + + var i, j, tracePoints, category, insertionIndex; + + var bisector = d3.bisector(sortFunction).left; + + for(i = 0; i < traceLines.length; i++) { + + tracePoints = traceLines[i]; + + for(j = 0; j < tracePoints.length; j++) { + + category = tracePoints[j]; + + // skip loop: ignore null and undefined categories + if(category === null || category === undefined) continue; + + insertionIndex = bisector(categoryArray, category); + + // skip loop on already encountered values + if(insertionIndex < categoryArray.length - 1 && categoryArray[insertionIndex] === category) continue; + + // insert value + categoryArray.splice(insertionIndex, 0, category); + } + } + + return categoryArray; +} + + +/** + * This pure function returns the ordered categories for specified axisLetter, categoryorder, categoryarray and data. + * + * If categoryorder is 'array', the result is a fresh copy of categoryarray, or if unspecified, an empty array. + * + * If categoryorder is 'category ascending' or 'category descending', the result is an array of ascending or descending + * order of the unique categories encountered in the data for specified axisLetter. + * + * See cartesian/layout_attributes.js for the definition of categoryorder and categoryarray + * + */ + +// orderedCategories :: String -> String -> [String] -> [[String]] -> [String] +module.exports = function orderedCategories(axisLetter, categoryorder, categoryarray, data) { + + switch(categoryorder) { + case 'array': return Array.isArray(categoryarray) ? categoryarray.slice() : []; + case 'category ascending': return flattenUniqueSort(axisLetter, d3.ascending, data); + case 'category descending': return flattenUniqueSort(axisLetter, d3.descending, data); + case 'trace': return []; + default: return []; + } +}; diff --git a/src/plots/cartesian/set_convert.js b/src/plots/cartesian/set_convert.js index e35c039429e..e601ee80062 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); diff --git a/src/plots/gl3d/layout/axis_attributes.js b/src/plots/gl3d/layout/axis_attributes.js index 15bfb6b483a..f311e63d4c4 100644 --- a/src/plots/gl3d/layout/axis_attributes.js +++ b/src/plots/gl3d/layout/axis_attributes.js @@ -68,6 +68,8 @@ module.exports = { description: 'Sets whether or not this axis is labeled' }, color: axesAttrs.color, + categoryorder: axesAttrs.categoryorder, + categoryarray: axesAttrs.categoryarray, title: axesAttrs.title, titlefont: axesAttrs.titlefont, type: axesAttrs.type, diff --git a/test/image/baselines/axes_category_ascending.png b/test/image/baselines/axes_category_ascending.png new file mode 100644 index 00000000000..f6252c4594b Binary files /dev/null and b/test/image/baselines/axes_category_ascending.png differ diff --git a/test/image/baselines/axes_category_categoryarray.png b/test/image/baselines/axes_category_categoryarray.png new file mode 100644 index 00000000000..288c3446f8c Binary files /dev/null and b/test/image/baselines/axes_category_categoryarray.png differ diff --git a/test/image/baselines/axes_category_categoryarray_truncated_tails.png b/test/image/baselines/axes_category_categoryarray_truncated_tails.png new file mode 100644 index 00000000000..9ffed02481c Binary files /dev/null and b/test/image/baselines/axes_category_categoryarray_truncated_tails.png differ diff --git a/test/image/baselines/axes_category_descending.png b/test/image/baselines/axes_category_descending.png new file mode 100644 index 00000000000..6eb5d95693b Binary files /dev/null and b/test/image/baselines/axes_category_descending.png differ 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 00000000000..6981f01ca0b Binary files /dev/null and b/test/image/baselines/axes_category_descending_with_gaps.png differ diff --git a/test/image/mocks/axes_category_ascending.json b/test/image/mocks/axes_category_ascending.json new file mode 100644 index 00000000000..7d20ecef13a --- /dev/null +++ b/test/image/mocks/axes_category_ascending.json @@ -0,0 +1,13 @@ +{ + "data": [{ + "x": ["c","a","e","b","d"], + "y": [15,11,12,13,14]} + ], + "layout": { + "xaxis": { + "title": "category ascending", + "type": "category", + "categoryorder": "category ascending", + "categoryarray": ["y","b","x","a","d","z","e","c", "q", "k"] + }} +} diff --git a/test/image/mocks/axes_category_categoryarray.json b/test/image/mocks/axes_category_categoryarray.json new file mode 100644 index 00000000000..3d96a0d53f5 --- /dev/null +++ b/test/image/mocks/axes_category_categoryarray.json @@ -0,0 +1,46 @@ +{ + "data": [ + { + "x": [ + 1, + 2, + null, + 4, + 5 + ], + "y": [ + 1, + 2, + 3, + 4, + 5 + ], + "connectgaps": false, + "uid": "8ac13a" + } + ], + "layout": { + "title": "categoryarray", + "xaxis": { + "type": "category", + "range": [ + -0.18336673346693386, + 3.1833667334669338 + ], + "autorange": true, + "categoryorder": "array", + "categoryarray": [2,4,5,1] + }, + "yaxis": { + "type": "linear", + "range": [ + 0.7070063694267517, + 5.292993630573249 + ], + "autorange": true + }, + "height": 450, + "width": 1000, + "autosize": true + } +} diff --git a/test/image/mocks/axes_category_categoryarray_truncated_tails.json b/test/image/mocks/axes_category_categoryarray_truncated_tails.json new file mode 100644 index 00000000000..70f0ed7bfd4 --- /dev/null +++ b/test/image/mocks/axes_category_categoryarray_truncated_tails.json @@ -0,0 +1,13 @@ +{ + "data": [{ + "x": ["c","a","e","b","d"], + "y": [15,11,12,13,14]} + ], + "layout": { + "title": "categoryarray with truncated tails (y, q, k not plotted)", + "xaxis": { + "type": "category", + "categoryorder": "array", + "categoryarray": ["y","b","x","a","d","z","e","c", "q", "k"] + }} +} diff --git a/test/image/mocks/axes_category_descending.json b/test/image/mocks/axes_category_descending.json new file mode 100644 index 00000000000..1c44a4ff1be --- /dev/null +++ b/test/image/mocks/axes_category_descending.json @@ -0,0 +1,41 @@ +{ + "data": [ + { + "x": [ + 5, + 1, + 3, + 2, + 4 + ], + "y": [ + 1, + 2, + 3, + 4, + 5 + ], + "connectgaps": false, + "uid": "8ac13a" + } + ], + "layout": { + "title": "category descending", + "xaxis": { + "type": "category", + "categoryorder": "category descending", + "categoryarray": [2,4,5,1] + }, + "yaxis": { + "type": "linear", + "range": [ + 0.7070063694267517, + 5.292993630573249 + ], + "autorange": true + }, + "height": 450, + "width": 1000, + "autosize": true + } +} 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..11afaf2d72e --- /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", + "categoryorder": "category descending", + "categoryarray": [2,4,5,1] + }, + "yaxis": { + "type": "linear", + "range": [ + 0.7070063694267517, + 5.292993630573249 + ], + "autorange": true + }, + "height": 450, + "width": 1000, + "autosize": true + } +} diff --git a/test/jasmine/tests/axes_test.js b/test/jasmine/tests/axes_test.js index f820e830263..bc61b5603ed 100644 --- a/test/jasmine/tests/axes_test.js +++ b/test/jasmine/tests/axes_test.js @@ -8,8 +8,8 @@ var tinycolor = require('tinycolor2'); 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() { @@ -384,15 +384,137 @@ describe('Test axes', function() { }); }); + describe('categoryorder', function() { + + var gd; + + beforeEach(function() { + gd = createGraphDiv(); + }); + + afterEach(destroyGraphDiv); + + describe('setting, or not setting categoryorder if it is not explicitly declared', function() { + + it('should set categoryorder to default if categoryorder and categoryarray 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.categoryorder).toBe('trace'); + }); + + it('should set categoryorder 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.categoryorder).toBe('trace'); + }); + + it('should NOT set categoryorder 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.categoryorder).toBe(undefined); + }); + + it('should set categoryorder 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.categoryorder).toBe(undefined); + expect(gd._fullLayout.yaxis.categoryorder).toBe('trace'); + }); + + }); + + describe('setting categoryorder to "array"', function() { + + it('should leave categoryorder 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', categoryorder: 'array', categoryarray: ['b','a','d','e','c']} + }); + expect(gd._fullLayout.xaxis.categoryorder).toBe('array'); + }); + + it('should switch categoryorder on "array" if it is not supplied but categoryarray is supplied', function() { + PlotlyInternal.plot(gd, [{x: ['c','a','e','b','d'], y: [15,11,12,13,14]}], { + xaxis: {type: 'category', categoryarray: ['b','a','d','e','c']} + }); + expect(gd._fullLayout.xaxis.categoryorder).toBe('array'); + }); + + it('should revert categoryorder 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', categoryorder: 'array'} + }); + expect(gd._fullLayout.xaxis.categoryorder).toBe('trace'); + }); + + }); + + describe('do not set categoryorder to "array" if list exists but empty', function() { + + it('should switch categoryorder 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', categoryorder: 'array', categoryarray: []} + }); + expect(gd._fullLayout.xaxis.categoryorder).toBe('trace'); + }); + + it('should not switch categoryorder on "array" if categoryarray is supplied but empty', function() { + PlotlyInternal.plot(gd, [{x: ['c','a','e','b','d'], y: [15,11,12,13,14]}], { + xaxis: {type: 'category', categoryarray: []} + }); + expect(gd._fullLayout.xaxis.categoryorder).toBe('trace'); + }); + }); + + describe('do NOT set categoryorder to "array" if it has some other proper value', function() { + + it('should use specified categoryorder if it is supplied even if categoryarray exists', function() { + PlotlyInternal.plot(gd, [{x: ['c','a','e','b','d'], y: [15,11,12,13,14]}], { + xaxis: {type: 'category', categoryorder: 'trace', categoryarray: ['b','a','d','e','c']} + }); + expect(gd._fullLayout.xaxis.categoryorder).toBe('trace'); + }); + + it('should use specified categoryorder if it is supplied even if categoryarray exists', function() { + PlotlyInternal.plot(gd, [{x: ['c','a','e','b','d'], y: [15,11,12,13,14]}], { + xaxis: {type: 'category', categoryorder: 'category ascending', categoryarray: ['b','a','d','e','c']} + }); + expect(gd._fullLayout.xaxis.categoryorder).toBe('category ascending'); + }); + + it('should use specified categoryorder if it is supplied even if categoryarray exists', function() { + PlotlyInternal.plot(gd, [{x: ['c','a','e','b','d'], y: [15,11,12,13,14]}], { + xaxis: {type: 'category', categoryorder: 'category descending', categoryarray: ['b','a','d','e','c']} + }); + expect(gd._fullLayout.xaxis.categoryorder).toBe('category descending'); + }); + + }); + + describe('setting categoryorder to the default if the value is unexpected', function() { + + it('should switch categoryorder 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', categoryorder: 'invalid value'} + }); + expect(gd._fullLayout.xaxis.categoryorder).toBe('trace'); + }); + + it('should switch categoryorder 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', categoryorder: 'invalid value', categoryarray: ['b','a','d','e','c']} + }); + expect(gd._fullLayout.xaxis.categoryorder).toBe('array'); + }); + + }); + + }); + describe('handleTickDefaults', function() { var data = [{ x: [1,2,3], y: [3,4,5] }], gd; beforeEach(function() { - gd = createGraph(); + gd = createGraphDiv(); }); - afterEach(destroyGraph); + afterEach(destroyGraphDiv); it('should set defaults on bad inputs', function() { var layout = { diff --git a/test/jasmine/tests/calcdata_test.js b/test/jasmine/tests/calcdata_test.js index df134e774cc..d727f51c904 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,825 @@ describe('calculated data and points', function() { expect(gd.calcdata[0][3]).toEqual({ x: false, y: false}); }); }); + + describe('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', + categoryorder: 'trace' + // 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. Currently, 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', + categoryorder: '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 descending domain alphanumerical order', function() { + + Plotly.plot(gd, [{x: ['c','a','e','b','d'], y: [15,11,12,13,14]}], { xaxis: { + type: 'category', + categoryorder: 'category descending' + }}); + + 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 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', + categoryorder: '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 categoryorder 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', + categoryorder: 'category ascending', + categoryarray: ['b','a','d','e','c'] // These must be ignored. Alternative: error? + }}); + + 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() { + + Plotly.plot(gd, [{x: ['c',undefined,'e','b','d'], y: [15,11,12,13,14]}], { xaxis: { + type: 'category', + categoryorder: 'category ascending' + }}); + + 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('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', + categoryorder: 'array', + categoryarray: ['b','a','d','e','c'] + }}); + + 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 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', + categoryorder: 'array', + categoryarray: [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: { + type: 'category', + categoryorder: 'array', + categoryarray: ['b','a','d','e','c'] + }}); + + 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() { + + Plotly.plot(gd, [{x: ['c','a','e','b','d'], y: [15,11,12,13,14]}], { xaxis: { + type: 'category', + categoryorder: 'array', + categoryarray: ['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(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 even if some missing categories were at the beginning or end of categoryarray', function() { + + Plotly.plot(gd, [{x: ['c','a','e','b','d'], y: [15,11,12,13,14]}], { xaxis: { + type: 'category', + categoryorder: 'array', + categoryarray: ['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})); + + // 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 categoryarray', 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', + categoryorder: 'array', + categoryarray: ['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})); + + 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() { + + Plotly.plot(gd, [{x: ['c','a','e','b','d'], y: [15,null,12,13,14]}], { xaxis: { + type: 'category', + categoryorder: 'array', + categoryarray: ['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})); + }); + + 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', + categoryorder: 'array', + categoryarray: ['b','a','x','c'] + }}); + + 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_ categoryorder and + // categories effective; categories would take precedence and the remaining items would be sorted + // based on the categoryorder. This of course means that the mere presence of categories triggers this + // behavior, rather than an explicit 'explicit' categoryorder. + }); + }); + + describe('ordering tests in the presence of multiple traces - mutually exclusive', 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})); + }); + + it('category order follows the trace order (even if categoryarray 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 + categoryorder: 'trace', + categoryarray: ['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 categoryarray 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 + categoryorder: 'category ascending', + categoryarray: ['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 categoryarray 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 + categoryorder: 'category descending', + categoryarray: ['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 categoryarray', 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 + categoryorder: 'array', + categoryarray: ['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})); + }); + }); + + 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 categoryarray 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 + categoryorder: 'trace', + categoryarray: ['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 categoryarray 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 + categoryorder: 'category ascending', + categoryarray: ['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 categoryarray 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 + categoryorder: 'category descending', + categoryarray: ['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 categoryarray', 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 + categoryorder: 'array', + categoryarray: ['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 categoryarray 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 + categoryorder: 'trace', + categoryarray: ['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 categoryarray 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 + categoryorder: 'category ascending', + categoryarray: ['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 categoryarray 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 + categoryorder: 'category descending', + categoryarray: ['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 categoryarray', 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 + categoryorder: 'array', + categoryarray: ['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 categoryarray 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 + categoryorder: 'array', + categoryarray: ['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})); + }); + }); + + describe('ordering and stacking combined', function() { + + it('partially overlapping category order follows categoryarray 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 + categoryorder: 'array', + categoryarray: ['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 categoryarray 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 + categoryorder: 'array', + categoryarray: ['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})); + }); + }); + }); });