diff --git a/src/plots/cartesian/axis_defaults.js b/src/plots/cartesian/axis_defaults.js index 41d88f83ab4..cf34c9d4b21 100644 --- a/src/plots/cartesian/axis_defaults.js +++ b/src/plots/cartesian/axis_defaults.js @@ -18,7 +18,6 @@ var handleTickLabelDefaults = require('./tick_label_defaults'); var handleCategoryOrderDefaults = require('./category_order_defaults'); var handleLineGridDefaults = require('./line_grid_defaults'); var setConvert = require('./set_convert'); -var orderedCategories = require('./ordered_categories'); /** * options: object containing: @@ -60,11 +59,7 @@ module.exports = function handleAxisDefaults(containerIn, containerOut, coerce, coerce('range'); containerOut.cleanRange(); - handleCategoryOrderDefaults(containerIn, containerOut, coerce); - containerOut._initialCategories = axType === 'category' ? - orderedCategories(letter, containerOut.categoryorder, containerOut.categoryarray, options.data) : - []; - + handleCategoryOrderDefaults(containerIn, containerOut, coerce, options); if(axType !== 'category' && !options.noHover) coerce('hoverformat'); diff --git a/src/plots/cartesian/category_order_defaults.js b/src/plots/cartesian/category_order_defaults.js index bf98c57901e..6165c9ea8ca 100644 --- a/src/plots/cartesian/category_order_defaults.js +++ b/src/plots/cartesian/category_order_defaults.js @@ -8,25 +8,85 @@ 'use strict'; +function findCategories(ax, opts) { + var dataAttr = opts.dataAttr || ax._id.charAt(0); + var lookup = {}; + var axData; + var i, j; -module.exports = function handleCategoryOrderDefaults(containerIn, containerOut, coerce) { - if(containerOut.type !== 'category') return; + if(opts.axData) { + // non-x/y case + axData = opts.axData; + } else { + // x/y case + axData = []; + for(i = 0; i < opts.data.length; i++) { + var trace = opts.data[i]; + if(trace[dataAttr + 'axis'] === ax._id) { + axData.push(trace); + } + } + } + + for(i = 0; i < axData.length; i++) { + var vals = axData[i][dataAttr]; + for(j = 0; j < vals.length; j++) { + var v = vals[j]; + if(v !== null && v !== undefined) { + lookup[v] = 1; + } + } + } + + return Object.keys(lookup); +} - var arrayIn = containerIn.categoryarray, - orderDefault; +/** + * Fills in category* default and initial categories. + * + * @param {object} containerIn : input axis object + * @param {object} containerOut : full axis object + * @param {function} coerce : Lib.coerce fn wrapper + * @param {object} opts : + * - data {array} : (full) data trace + * OR + * - axData {array} : (full) data associated with axis being coerced here + * - dataAttr {string} : attribute name corresponding to coordinate array + */ +module.exports = function handleCategoryOrderDefaults(containerIn, containerOut, coerce, opts) { + if(containerOut.type !== 'category') return; + var arrayIn = containerIn.categoryarray; var isValidArray = (Array.isArray(arrayIn) && arrayIn.length > 0); // override default 'categoryorder' value when non-empty array is supplied + var orderDefault; if(isValidArray) orderDefault = 'array'; var order = coerce('categoryorder', orderDefault); + var array; // coerce 'categoryarray' only in array order case - if(order === 'array') coerce('categoryarray'); + if(order === 'array') { + array = coerce('categoryarray'); + } // cannot set 'categoryorder' to 'array' with an invalid 'categoryarray' if(!isValidArray && order === 'array') { - containerOut.categoryorder = 'trace'; + order = containerOut.categoryorder = 'trace'; + } + + // set up things for makeCalcdata + if(order === 'trace') { + containerOut._initialCategories = []; + } else if(order === 'array') { + containerOut._initialCategories = array.slice(); + } else { + array = findCategories(containerOut, opts).sort(); + if(order === 'category ascending') { + containerOut._initialCategories = array; + } else if(order === 'category descending') { + containerOut._initialCategories = array.reverse(); + } } }; diff --git a/src/plots/cartesian/ordered_categories.js b/src/plots/cartesian/ordered_categories.js deleted file mode 100644 index d50c6b195ce..00000000000 --- a/src/plots/cartesian/ordered_categories.js +++ /dev/null @@ -1,70 +0,0 @@ -/** -* Copyright 2012-2018, 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 bisector = d3.bisector(sortFunction).left; - - for(var i = 0; i < traceLines.length; i++) { - var tracePoints = traceLines[i] || []; - - for(var j = 0; j < tracePoints.length; j++) { - var category = tracePoints[j]; - - // skip loop: ignore null and undefined categories - if(category === null || category === undefined) continue; - - var insertionIndex = bisector(categoryArray, category); - - // skip loop on already encountered values - if(insertionIndex < categoryArray.length && 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/polar/layout_defaults.js b/src/plots/polar/layout_defaults.js index 9e4d49ad15e..ec383eafe69 100644 --- a/src/plots/polar/layout_defaults.js +++ b/src/plots/polar/layout_defaults.js @@ -19,7 +19,6 @@ var handleTickLabelDefaults = require('../cartesian/tick_label_defaults'); var handleCategoryOrderDefaults = require('../cartesian/category_order_defaults'); var handleLineGridDefaults = require('../cartesian/line_grid_defaults'); var autoType = require('../cartesian/axis_autotype'); -var orderedCategories = require('../cartesian/ordered_categories'); var setConvert = require('../cartesian/set_convert'); var setConvertAngular = require('./helpers').setConvertAngular; @@ -56,10 +55,10 @@ function handleDefaults(contIn, contOut, coerce, opts) { var dataAttr = constants.axisName2dataArray[axName]; var axType = handleAxisTypeDefaults(axIn, axOut, coerceAxis, subplotData, dataAttr); - handleCategoryOrderDefaults(axIn, axOut, coerceAxis); - axOut._initialCategories = axType === 'category' ? - orderedCategories(dataAttr, axOut.categoryorder, axOut.categoryarray, subplotData) : - []; + handleCategoryOrderDefaults(axIn, axOut, coerceAxis, { + axData: subplotData, + dataAttr: dataAttr + }); var visible = coerceAxis('visible'); setConvert(axOut, layoutOut); diff --git a/src/traces/carpet/axis_defaults.js b/src/traces/carpet/axis_defaults.js index cc4bf6e6c6c..1846a032cad 100644 --- a/src/traces/carpet/axis_defaults.js +++ b/src/traces/carpet/axis_defaults.js @@ -17,13 +17,12 @@ var handleTickValueDefaults = require('../../plots/cartesian/tick_value_defaults var handleTickLabelDefaults = require('../../plots/cartesian/tick_label_defaults'); var handleCategoryOrderDefaults = require('../../plots/cartesian/category_order_defaults'); var setConvert = require('../../plots/cartesian/set_convert'); -var orderedCategories = require('../../plots/cartesian/ordered_categories'); var autoType = require('../../plots/cartesian/axis_autotype'); /** * options: object containing: * - * letter: 'x' or 'y' + * letter: 'a' or 'b' * title: name of the axis (ie 'Colorbar') to go in default title * name: axis object name (ie 'xaxis') if one should be stored * font: the default font to inherit @@ -133,7 +132,10 @@ module.exports = function handleAxisDefaults(containerIn, containerOut, options) handleTickValueDefaults(containerIn, containerOut, coerce, axType); handleTickLabelDefaults(containerIn, containerOut, coerce, axType, options); - handleCategoryOrderDefaults(containerIn, containerOut, coerce); + handleCategoryOrderDefaults(containerIn, containerOut, coerce, { + data: options.data, + dataAttr: letter + }); var gridColor = coerce2('gridcolor', addOpacity(dfltColor, 0.3)); var gridWidth = coerce2('gridwidth'); @@ -176,11 +178,6 @@ module.exports = function handleAxisDefaults(containerIn, containerOut, options) } } - // fill in categories - containerOut._initialCategories = axType === 'category' ? - orderedCategories(letter, containerOut.categoryorder, containerOut.categoryarray, options.data) : - []; - if(containerOut.showticklabels === 'none') { delete containerOut.tickfont; delete containerOut.tickangle; diff --git a/test/jasmine/tests/calcdata_test.js b/test/jasmine/tests/calcdata_test.js index a7c7b103b46..c9dbfd3180b 100644 --- a/test/jasmine/tests/calcdata_test.js +++ b/test/jasmine/tests/calcdata_test.js @@ -883,6 +883,35 @@ describe('calculated data and points', function() { expect(gd.calcdata[2][2]).toEqual(jasmine.objectContaining({x: 0, y: 11 + 20 + 32})); }); }); + + it('should order categories per axis', function() { + Plotly.plot(gd, [ + {x: ['a', 'c', 'g', 'e']}, + {x: ['b', 'h', 'f', 'd'], xaxis: 'x2'} + ], { + xaxis: {categoryorder: 'category ascending', domain: [0, 0.4]}, + xaxis2: {categoryorder: 'category descending', domain: [0.6, 1]} + }); + + expect(gd._fullLayout.xaxis._categories).toEqual(['a', 'c', 'e', 'g']); + expect(gd._fullLayout.xaxis2._categories).toEqual(['h', 'f', 'd', 'b']); + }); + + it('should consider number categories and their string representation to be the same', function() { + Plotly.plot(gd, [{ + x: ['a', 'b', 1, '1'], + y: [1, 2, 3, 4] + }], { + xaxis: {type: 'category'} + }); + + expect(gd._fullLayout.xaxis._categories).toEqual(['a', 'b', 1]); + expect(gd._fullLayout.xaxis._categoriesMap).toEqual({ + '1': 2, + 'a': 0, + 'b': 1 + }); + }); }); describe('customdata', function() {