diff --git a/src/plot_api/plot_api.js b/src/plot_api/plot_api.js index 026356a1f04..2fa73dbd81b 100644 --- a/src/plot_api/plot_api.js +++ b/src/plot_api/plot_api.js @@ -851,10 +851,10 @@ function doCalcdata(gd) { fullLayout._piecolormap = {}; fullLayout._piedefaultcolorcount = 0; - // delete category list, if there is one, so we start over + // initialize the category list, if there is one, so we start over // to be filled in later by ax.d2c for(i = 0; i < axList.length; i++) { - axList[i]._categories = []; + axList[i]._categories = axList[i]._initialCategories.slice(); } 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..24833945f95 100644 --- a/src/plots/cartesian/axis_defaults.js +++ b/src/plots/cartesian/axis_defaults.js @@ -17,7 +17,9 @@ var Plots = require('../plots'); var layoutAttributes = require('./layout_attributes'); var handleTickValueDefaults = require('./tick_value_defaults'); var handleTickDefaults = require('./tick_defaults'); +var handleCategoryModeDefaults = require('./category_mode_defaults'); var setConvert = require('./set_convert'); +var orderedCategories = require('./ordered_categories'); var cleanDatum = require('./clean_datum'); var axisIds = require('./axis_ids'); @@ -64,6 +66,10 @@ module.exports = function handleAxisDefaults(containerIn, containerOut, coerce, } } + containerOut._initialCategories = axType === 'category' ? + orderedCategories(letter, containerIn.categorymode, containerIn.categorylist, options.data) : + []; + setConvert(containerOut); coerce('title', defaultTitle); @@ -91,6 +97,7 @@ module.exports = function handleAxisDefaults(containerIn, containerOut, coerce, handleTickValueDefaults(containerIn, containerOut, coerce, axType); handleTickDefaults(containerIn, containerOut, coerce, axType, options); + handleCategoryModeDefaults(containerIn, containerOut, coerce); var lineColor = Lib.coerce2(containerIn, containerOut, layoutAttributes, 'linecolor'), lineWidth = Lib.coerce2(containerIn, containerOut, layoutAttributes, 'linewidth'), diff --git a/src/plots/cartesian/category_mode_defaults.js b/src/plots/cartesian/category_mode_defaults.js new file mode 100644 index 00000000000..d671941d5ee --- /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'; + +var layoutAttributes = require('./layout_attributes'); + +module.exports = function handleCategoryModeDefaults(containerIn, containerOut, coerce) { + + if(containerIn.type !== 'category') return; + + var validCategories = layoutAttributes.categorymode.values; + + 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/src/plots/cartesian/layout_attributes.js b/src/plots/cartesian/layout_attributes.js index 8ec877fc6a2..328aae57fac 100644 --- a/src/plots/cartesian/layout_attributes.js +++ b/src/plots/cartesian/layout_attributes.js @@ -446,6 +446,36 @@ module.exports = { 'Only has an effect if `anchor` is set to *free*.' ].join(' ') }, + categorymode: { + valType: 'enumerated', + values: [ + 'trace', 'category ascending', 'category descending', 'array' + /*, 'value ascending', 'value descending'*/ // value ascending / descending to be implemented later + ], + dflt: 'trace', + role: 'info', + description: [ + 'Specifies the ordering logic for the case of categorical variables.', + 'By default, plotly uses *trace*, which specifies the order that is present in the data supplied.', + 'Set `categorymode` to *category ascending* or *category descending* if order should be determined by', + 'the alphanumerical order of the category names.', + /*'Set `categorymode` to *value ascending* or *value descending* if order should be determined by the', + 'numerical order of the values.',*/ // // value ascending / descending to be implemented later + 'Set `categorymode` to *array* to derive the ordering from the attribute `categorylist`. If a category', + 'is not found in the `categorylist` array, the sorting behavior for that attribute will be identical to', + 'the *trace* mode. The unspecified categories will follow the categories in `categorylist`.' + ].join(' ') + }, + categorylist: { + valType: 'data_array', + role: 'info', + description: [ + 'Sets the order in which categories on this axis appear.', + 'Only has an effect if `categorymode` is set to *array*.', + 'Used with `categorymode`.' + ].join(' ') + }, + _deprecated: { autotick: { diff --git a/src/plots/cartesian/ordered_categories.js b/src/plots/cartesian/ordered_categories.js new file mode 100644 index 00000000000..5f9f25a485c --- /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, categorymode, categorylist and data. + * + * If categorymode is 'array', the result is a fresh copy of categorylist, or if unspecified, an empty array. + * + * If categorymode is 'category ascending' or 'category descending', the result is an array of ascending or descending + * order of the unique categories encountered in the data for specified axisLetter. + * + * See cartesian/layout_attributes.js for the definition of categorymode and categorylist + * + */ + +// orderedCategories :: String -> String -> [String] -> [[String]] -> [String] +module.exports = function orderedCategories(axisLetter, categorymode, categorylist, data) { + + switch(categorymode) { + case 'array': return Array.isArray(categorylist) ? categorylist.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 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); 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, 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_categorylist.png b/test/image/baselines/axes_category_categorylist.png new file mode 100644 index 00000000000..b29a3262d6d Binary files /dev/null and b/test/image/baselines/axes_category_categorylist.png differ 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 00000000000..f7c0bbc461b Binary files /dev/null and b/test/image/baselines/axes_category_categorylist_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..29f76d87465 --- /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", + "categorymode": "category ascending", + "categorylist": ["y","b","x","a","d","z","e","c", "q", "k"] + }} +} 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_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 new file mode 100644 index 00000000000..0df8fc761ca --- /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", + "categorymode": "category descending", + "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_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 + } +} diff --git a/test/jasmine/tests/axes_test.js b/test/jasmine/tests/axes_test.js index abb8a3c9953..717cdc6148d 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() { @@ -321,15 +321,137 @@ describe('Test axes', function() { }); }); + describe('categorymode', function() { + + var gd; + + beforeEach(function() { + gd = createGraphDiv(); + }); + + afterEach(destroyGraphDiv); + + describe('setting, or not setting categorymode if it is not explicitly declared', function() { + + it('should set categorymode to default if categorymode and categorylist are not supplied', function() { + PlotlyInternal.plot(gd, [{x: ['c','a','e','b','d'], y: [15,11,12,13,14]}], {xaxis: {type: 'category'}}); + expect(gd._fullLayout.xaxis.categorymode).toBe('trace'); + }); + + it('should set categorymode to default even if type is not set to category explicitly', function() { + PlotlyInternal.plot(gd, [{x: ['c','a','e','b','d'], y: [15,11,12,13,14]}]); + expect(gd._fullLayout.xaxis.categorymode).toBe('trace'); + }); + + it('should NOT set categorymode to default if type is not category', function() { + PlotlyInternal.plot(gd, [{x: ['c','a','e','b','d'], y: [15,11,12,13,14]}]); + expect(gd._fullLayout.yaxis.categorymode).toBe(undefined); + }); + + it('should set categorymode to default if type is overridden to be category', function() { + PlotlyInternal.plot(gd, [{x: [1,2,3,4,5], y: [15,11,12,13,14]}], {yaxis: {type: 'category'}}); + expect(gd._fullLayout.xaxis.categorymode).toBe(undefined); + expect(gd._fullLayout.yaxis.categorymode).toBe('trace'); + }); + + }); + + describe('setting categorymode to "array"', function() { + + it('should leave categorymode on "array" if it is supplied', function() { + PlotlyInternal.plot(gd, [{x: ['c','a','e','b','d'], y: [15,11,12,13,14]}], { + xaxis: {type: 'category', categorymode: 'array', categorylist: ['b','a','d','e','c']} + }); + expect(gd._fullLayout.xaxis.categorymode).toBe('array'); + }); + + it('should switch categorymode on "array" if it is not supplied but categorylist is supplied', function() { + PlotlyInternal.plot(gd, [{x: ['c','a','e','b','d'], y: [15,11,12,13,14]}], { + xaxis: {type: 'category', categorylist: ['b','a','d','e','c']} + }); + expect(gd._fullLayout.xaxis.categorymode).toBe('array'); + }); + + it('should revert categorymode to "trace" if "array" is supplied but there is no list', function() { + PlotlyInternal.plot(gd, [{x: ['c','a','e','b','d'], y: [15,11,12,13,14]}], { + xaxis: {type: 'category', categorymode: 'array'} + }); + expect(gd._fullLayout.xaxis.categorymode).toBe('trace'); + }); + + }); + + describe('do not set categorymode to "array" if list exists but empty', function() { + + it('should switch categorymode to default if list is not supplied', function() { + PlotlyInternal.plot(gd, [{x: ['c','a','e','b','d'], y: [15,11,12,13,14]}], { + xaxis: {type: 'category', categorymode: 'array', categorylist: []} + }); + expect(gd._fullLayout.xaxis.categorymode).toBe('trace'); + }); + + it('should not switch categorymode on "array" if categorylist is supplied but empty', function() { + PlotlyInternal.plot(gd, [{x: ['c','a','e','b','d'], y: [15,11,12,13,14]}], { + xaxis: {type: 'category', categorylist: []} + }); + expect(gd._fullLayout.xaxis.categorymode).toBe('trace'); + }); + }); + + describe('do NOT set categorymode to "array" if it has some other proper value', function() { + + it('should use specified categorymode if it is supplied even if categorylist exists', function() { + PlotlyInternal.plot(gd, [{x: ['c','a','e','b','d'], y: [15,11,12,13,14]}], { + xaxis: {type: 'category', categorymode: 'trace', categorylist: ['b','a','d','e','c']} + }); + expect(gd._fullLayout.xaxis.categorymode).toBe('trace'); + }); + + it('should use specified categorymode if it is supplied even if categorylist exists', function() { + PlotlyInternal.plot(gd, [{x: ['c','a','e','b','d'], y: [15,11,12,13,14]}], { + xaxis: {type: 'category', categorymode: 'category ascending', categorylist: ['b','a','d','e','c']} + }); + expect(gd._fullLayout.xaxis.categorymode).toBe('category ascending'); + }); + + it('should use specified categorymode if it is supplied even if categorylist exists', function() { + PlotlyInternal.plot(gd, [{x: ['c','a','e','b','d'], y: [15,11,12,13,14]}], { + xaxis: {type: 'category', categorymode: 'category descending', categorylist: ['b','a','d','e','c']} + }); + expect(gd._fullLayout.xaxis.categorymode).toBe('category descending'); + }); + + }); + + describe('setting categorymode to the default if the value is unexpected', function() { + + it('should switch categorymode to "trace" if mode is supplied but invalid', function() { + PlotlyInternal.plot(gd, [{x: ['c','a','e','b','d'], y: [15,11,12,13,14]}], { + xaxis: {type: 'category', categorymode: 'invalid value'} + }); + expect(gd._fullLayout.xaxis.categorymode).toBe('trace'); + }); + + it('should switch categorymode to "array" if mode is supplied but invalid and list is supplied', function() { + PlotlyInternal.plot(gd, [{x: ['c','a','e','b','d'], y: [15,11,12,13,14]}], { + xaxis: {type: 'category', categorymode: 'invalid value', categorylist: ['b','a','d','e','c']} + }); + expect(gd._fullLayout.xaxis.categorymode).toBe('array'); + }); + + }); + + }); + 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..8c20fca44d6 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,872 @@ 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', + categorymode: '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', + 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 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]).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', + 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: { + type: 'category', + categorymode: 'category ascending', + categorylist: ['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', + categorymode: '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('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: 'value 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: 'value 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); + }); + + 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: 'value 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() { + + 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: 'array', + categorylist: ['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', + 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: { + type: 'category', + categorymode: 'array', + categorylist: ['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', + 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(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 categorylist', 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', '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 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})); + + 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', + 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})); + }); + + 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: 'array', + categorylist: ['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_ 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. + }); + }); + + 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 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})); + }); + }); + + 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})); + }); + }); + + 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})); + }); + }); + }); });