From 1c1ff21c4c5a03cb1a7565750f35cdee7f1a3693 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 5 Aug 2026 05:41:24 +0000 Subject: [PATCH 1/2] Support categoryorder and categoryarray on multicategory axes handleCategoryOrderDefaults returned early for any axis type other than 'category', so these attributes were a silent no-op on multicategory axes. Let those axes through and handle the [parent, child] pair shape: - 'trace' (default): per-parent data order - 'array': categoryarray entries are [first-level, second-level] pairs; malformed entries are dropped, and an array holding no valid pair falls back on 'trace'. Categories absent from categoryarray follow in trace order, matching 'category' axes - 'category ascending'/'category descending': sort the pairs by label - ordering by aggregated value ('total ascending', ...) is not implemented for these axes (sortAxisCategoriesByValue only handles 'category' axes, and interleaving children across parents would break the parent grouping), so categoryorder is coerced against the supported subset via an inline attribute override - the same pattern axis_defaults.js uses for ticklabelposition - and unsupported values fall back on the default The _initialCategories seeding in clearCalc already handles array-valued categories, so no calc-time changes are needed on top of the per-parent ordering fix this builds on. New mock multicategory-categoryorder.json renders the same data under four orderings; 9 new specs in axes_test.js cover each mode and fallback. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01VuN1reWCtHE6WFsNZjdGCD --- .../cartesian/category_order_defaults.js | 129 +++++++++++++++--- src/plots/cartesian/layout_attributes.js | 9 +- .../mocks/multicategory-categoryorder.json | 84 ++++++++++++ test/jasmine/tests/axes_test.js | 127 +++++++++++++++++ test/plot-schema.json | 28 ++-- 5 files changed, 343 insertions(+), 34 deletions(-) create mode 100644 test/image/mocks/multicategory-categoryorder.json diff --git a/src/plots/cartesian/category_order_defaults.js b/src/plots/cartesian/category_order_defaults.js index 76c1e5c261a..5f3c7840c24 100644 --- a/src/plots/cartesian/category_order_defaults.js +++ b/src/plots/cartesian/category_order_defaults.js @@ -1,32 +1,59 @@ 'use strict'; +var Lib = require('../../lib'); +var isArrayOrTypedArray = require('../../lib/array').isArrayOrTypedArray; var isTypedArraySpec = require('../../lib/array').isTypedArraySpec; -function findCategories(ax, opts) { +function isValidCategory(v) { + return v !== null && v !== undefined; +} + +// a multicategory entry is a [parent, child] pair +function isValidPair(v) { + return Array.isArray(v) && v.length === 2 && + isValidCategory(v[0]) && isValidCategory(v[1]); +} + +function compareAsString(a, b) { + a = String(a); + b = String(b); + return a < b ? -1 : (a > b ? 1 : 0); +} + +function comparePairs(a, b) { + return compareAsString(a[0], b[0]) || compareAsString(a[1], b[1]); +} + +function getAxData(ax, opts) { var dataAttr = opts.dataAttr || ax._id.charAt(0); - var lookup = {}; - var axData; - var i, j; 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); - } + return opts.axData; + } + + // x/y case + var axData = []; + for(var i = 0; i < opts.data.length; i++) { + var trace = opts.data[i]; + if(trace[dataAttr + 'axis'] === ax._id) { + axData.push(trace); } } + return axData; +} + +function findCategories(ax, opts) { + var dataAttr = opts.dataAttr || ax._id.charAt(0); + var axData = getAxData(ax, opts); + var lookup = {}; + var i, j; 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) { + if(isValidCategory(v)) { lookup[v] = 1; } } @@ -35,6 +62,45 @@ function findCategories(ax, opts) { return Object.keys(lookup); } +// multicategory variant: returns the unique [parent, child] pairs found in the +// data, which is what `_categories` holds for these axes +function findCategoryPairs(ax, opts) { + var dataAttr = opts.dataAttr || ax._id.charAt(0); + var axData = getAxData(ax, opts); + // prototype-less, so that e.g. a category named 'toString' does not + // resolve through Object.prototype + var lookup = Object.create(null); + var list = []; + var i, j; + + for(i = 0; i < axData.length; i++) { + var arrayIn = axData[i][dataAttr]; + if(!isArrayOrTypedArray(arrayIn) || + !isArrayOrTypedArray(arrayIn[0]) || + !isArrayOrTypedArray(arrayIn[1]) + ) continue; + + var len = Math.min(arrayIn[0].length, arrayIn[1].length); + + for(j = 0; j < len; j++) { + var v0 = arrayIn[0][j]; + var v1 = arrayIn[1][j]; + + if(isValidCategory(v0) && isValidCategory(v1)) { + // comma-joined keys match how `_categoriesMap` stringifies + // [parent, child] pairs in set_convert.js + var key = v0 + ',' + v1; + if(!(key in lookup)) { + lookup[key] = 1; + list.push([v0, v1]); + } + } + } + } + + return list; +} + /** * Fills in category* default and initial categories. * @@ -48,17 +114,38 @@ function findCategories(ax, opts) { * - dataAttr {string} : attribute name corresponding to coordinate array */ module.exports = function handleCategoryOrderDefaults(containerIn, containerOut, coerce, opts) { - if(containerOut.type !== 'category') return; + var isMultiCategory = containerOut.type === 'multicategory'; + if(containerOut.type !== 'category' && !isMultiCategory) return; var arrayIn = containerIn.categoryarray; var isValidArray = (Array.isArray(arrayIn) && arrayIn.length > 0) || isTypedArraySpec(arrayIn); + // on multicategory axes every entry must be a [parent, child] pair + if(isMultiCategory && isValidArray) { + isValidArray = Array.isArray(arrayIn) && arrayIn.some(isValidPair); + } + // override default 'categoryorder' value when non-empty array is supplied var orderDefault; if(isValidArray) orderDefault = 'array'; - var order = coerce('categoryorder', orderDefault); + var order; + if(isMultiCategory) { + // ordering by aggregated value ('total ascending', 'median descending', ...) + // is not implemented for multicategory axes - it would interleave + // children across different parents, breaking the parent grouping - + // so coerce against the modes that are + order = Lib.coerce(containerIn, containerOut, { + categoryorder: { + valType: 'enumerated', + values: ['trace', 'category ascending', 'category descending', 'array'], + dflt: 'trace' + } + }, 'categoryorder', orderDefault); + } else { + order = coerce('categoryorder', orderDefault); + } var array; // coerce 'categoryarray' only in array order case @@ -75,9 +162,15 @@ module.exports = function handleCategoryOrderDefaults(containerIn, containerOut, if(order === 'trace') { containerOut._initialCategories = []; } else if(order === 'array') { - containerOut._initialCategories = array.slice(); + array = array.slice(); + // drop malformed entries so they can't land in `_categories` + if(isMultiCategory) array = array.filter(isValidPair); + containerOut._initialCategories = array; } else { - array = findCategories(containerOut, opts).sort(); + array = isMultiCategory ? + findCategoryPairs(containerOut, opts).sort(comparePairs) : + findCategories(containerOut, opts).sort(); + if(order === 'category ascending') { containerOut._initialCategories = array; } else if(order === 'category descending') { diff --git a/src/plots/cartesian/layout_attributes.js b/src/plots/cartesian/layout_attributes.js index 25b60cfa28e..89cc5005268 100644 --- a/src/plots/cartesian/layout_attributes.js +++ b/src/plots/cartesian/layout_attributes.js @@ -1265,7 +1265,10 @@ module.exports = { 'the *trace* mode. The unspecified categories will follow the categories in `categoryarray`.', 'Set `categoryorder` to *total ascending* or *total descending* if order should be determined by the', 'numerical order of the values.', - 'Similarly, the order can be determined by the min, max, sum, mean, geometric mean or median of all the values.' + 'Similarly, the order can be determined by the min, max, sum, mean, geometric mean or median of all the values.', + 'On *multicategory* axes, *trace* orders the second-level categories by the order they appear in the data', + 'within each first-level category; only *trace*, *category ascending*, *category descending* and *array*', + 'are supported on these axes - other values fall back on the default.' ].join(' ') }, categoryarray: { @@ -1274,7 +1277,9 @@ module.exports = { description: [ 'Sets the order in which categories on this axis appear.', 'Only has an effect if `categoryorder` is set to *array*.', - 'Used with `categoryorder`.' + 'Used with `categoryorder`.', + 'On *multicategory* axes each entry is a [first-level, second-level] pair,', + 'e.g. `[[*2023*, *Q4*], [*2024*, *Q1*]]`; entries that are not such a pair are ignored.' ].join(' ') }, uirevision: { diff --git a/test/image/mocks/multicategory-categoryorder.json b/test/image/mocks/multicategory-categoryorder.json new file mode 100644 index 00000000000..46c6adbe09f --- /dev/null +++ b/test/image/mocks/multicategory-categoryorder.json @@ -0,0 +1,84 @@ +{ + "data": [ + { + "type": "bar", + "x": [ + ["2023", "2023", "2024", "2024", "2024", "2024"], + ["Q4", "Q3", "Q2", "Q1", "Q4", "Q3"] + ], + "y": [4, 3, 2, 1, 4, 3], + "marker": { "color": "#636efa" } + }, + + { + "type": "bar", + "x": [ + ["2023", "2023", "2024", "2024", "2024", "2024"], + ["Q4", "Q3", "Q2", "Q1", "Q4", "Q3"] + ], + "y": [4, 3, 2, 1, 4, 3], + "marker": { "color": "#ef553b" }, + "xaxis": "x2", + "yaxis": "y2" + }, + + { + "type": "bar", + "x": [ + ["2023", "2023", "2024", "2024", "2024", "2024"], + ["Q4", "Q3", "Q2", "Q1", "Q4", "Q3"] + ], + "y": [4, 3, 2, 1, 4, 3], + "marker": { "color": "#00cc96" }, + "xaxis": "x3", + "yaxis": "y3" + }, + + { + "type": "bar", + "x": [ + ["2023", "2023", "2024", "2024", "2024", "2024"], + ["Q4", "Q3", "Q2", "Q1", "Q4", "Q3"] + ], + "y": [4, 3, 2, 1, 4, 3], + "marker": { "color": "#ab63fa" }, + "xaxis": "x4", + "yaxis": "y4" + } + ], + "layout": { + "title": { "text": "multicategory categoryorder" }, + "grid": { + "rows": 2, + "columns": 2, + "pattern": "independent", + "xgap": 0.15, + "ygap": 0.4 + }, + "xaxis": { "title": { "text": "trace (default)" } }, + "xaxis2": { + "title": { "text": "array" }, + "categoryorder": "array", + "categoryarray": [ + ["2024", "Q1"], + ["2024", "Q2"], + ["2024", "Q3"], + ["2024", "Q4"], + ["2023", "Q3"], + ["2023", "Q4"] + ] + }, + "xaxis3": { + "title": { "text": "category ascending" }, + "categoryorder": "category ascending" + }, + "xaxis4": { + "title": { "text": "category descending" }, + "categoryorder": "category descending" + }, + "width": 700, + "height": 500, + "margin": { "l": 40, "b": 60, "t": 40, "r": 20 }, + "showlegend": false + } +} diff --git a/test/jasmine/tests/axes_test.js b/test/jasmine/tests/axes_test.js index c5fa0b3fec1..36547e794be 100644 --- a/test/jasmine/tests/axes_test.js +++ b/test/jasmine/tests/axes_test.js @@ -2386,6 +2386,133 @@ describe('Test axes', function() { .then(done, done.fail); }); }); + + describe('on multicategory axes', function() { + // '2023' contributes 'b' first, so under a single global + // second-level order '2024' would render as b, a + var trace = { + type: 'bar', + x: [ + ['2023', '2023', '2024', '2024'], + ['b', 'c', 'a', 'b'] + ], + y: [1, 2, 3, 4] + }; + + function _plot(xaxis) { + return Plotly.newPlot(gd, [Lib.extendDeep({}, trace)], xaxis ? {xaxis: xaxis} : {}); + } + + function _categories() { + return gd._fullLayout.xaxis._categories; + } + + it('should follow the data order within each first-level category', function(done) { + _plot() + .then(function() { + expect(gd._fullLayout.xaxis.categoryorder).toBe('trace'); + expect(_categories()).toEqual([ + ['2023', 'b'], ['2023', 'c'], ['2024', 'a'], ['2024', 'b'] + ]); + }) + .then(done, done.fail); + }); + + it('should honour categoryorder "array" with [parent, child] pairs', function(done) { + _plot({ + categoryorder: 'array', + categoryarray: [['2024', 'b'], ['2024', 'a'], ['2023', 'c'], ['2023', 'b']] + }) + .then(function() { + expect(gd._fullLayout.xaxis.categoryorder).toBe('array'); + expect(_categories()).toEqual([ + ['2024', 'b'], ['2024', 'a'], ['2023', 'c'], ['2023', 'b'] + ]); + }) + .then(done, done.fail); + }); + + it('should switch categoryorder to "array" when only categoryarray is supplied', function(done) { + _plot({categoryarray: [['2024', 'b'], ['2024', 'a']]}) + .then(function() { + expect(gd._fullLayout.xaxis.categoryorder).toBe('array'); + // categories missing from categoryarray follow in trace order + expect(_categories()).toEqual([ + ['2024', 'b'], ['2024', 'a'], ['2023', 'b'], ['2023', 'c'] + ]); + }) + .then(done, done.fail); + }); + + it('should revert categoryorder to "trace" when categoryarray holds no valid pair', function(done) { + _plot({categoryorder: 'array', categoryarray: ['a', 'b']}) + .then(function() { + expect(gd._fullLayout.xaxis.categoryorder).toBe('trace'); + expect(_categories()).toEqual([ + ['2023', 'b'], ['2023', 'c'], ['2024', 'a'], ['2024', 'b'] + ]); + }) + .then(done, done.fail); + }); + + it('should drop malformed categoryarray entries', function(done) { + _plot({ + categoryorder: 'array', + categoryarray: ['2024', ['2024', 'b'], null, ['2023', 'c', 'extra']] + }) + .then(function() { + expect(_categories()).toEqual([ + ['2024', 'b'], ['2023', 'b'], ['2023', 'c'], ['2024', 'a'] + ]); + }) + .then(done, done.fail); + }); + + it('should honour categoryorder "category ascending"', function(done) { + _plot({categoryorder: 'category ascending'}) + .then(function() { + expect(_categories()).toEqual([ + ['2023', 'b'], ['2023', 'c'], ['2024', 'a'], ['2024', 'b'] + ]); + }) + .then(done, done.fail); + }); + + it('should honour categoryorder "category descending"', function(done) { + _plot({categoryorder: 'category descending'}) + .then(function() { + expect(_categories()).toEqual([ + ['2024', 'b'], ['2024', 'a'], ['2023', 'c'], ['2023', 'b'] + ]); + }) + .then(done, done.fail); + }); + + it('should fall back to "trace" for value-based categoryorder', function(done) { + _plot({categoryorder: 'total descending'}) + .then(function() { + expect(gd._fullLayout.xaxis.categoryorder).toBe('trace'); + expect(_categories()).toEqual([ + ['2023', 'b'], ['2023', 'c'], ['2024', 'a'], ['2024', 'b'] + ]); + }) + .then(done, done.fail); + }); + + it('should fall back to "array" for value-based categoryorder with a valid categoryarray', function(done) { + _plot({ + categoryorder: 'total descending', + categoryarray: [['2024', 'b'], ['2024', 'a'], ['2023', 'c'], ['2023', 'b']] + }) + .then(function() { + expect(gd._fullLayout.xaxis.categoryorder).toBe('array'); + expect(_categories()).toEqual([ + ['2024', 'b'], ['2024', 'a'], ['2023', 'c'], ['2023', 'b'] + ]); + }) + .then(done, done.fail); + }); + }); }); describe('bar category autorange', function() { diff --git a/test/plot-schema.json b/test/plot-schema.json index 78614b86693..73ca028c0f1 100644 --- a/test/plot-schema.json +++ b/test/plot-schema.json @@ -5117,7 +5117,7 @@ ] }, "categoryarray": { - "description": "Sets the order in which categories on this axis appear. Only has an effect if `categoryorder` is set to *array*. Used with `categoryorder`.", + "description": "Sets the order in which categories on this axis appear. Only has an effect if `categoryorder` is set to *array*. Used with `categoryorder`. On *multicategory* axes each entry is a [first-level, second-level] pair, e.g. `[[*2023*, *Q4*], [*2024*, *Q1*]]`; entries that are not such a pair are ignored.", "editType": "calc", "valType": "data_array" }, @@ -5127,7 +5127,7 @@ "valType": "string" }, "categoryorder": { - "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 *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`. Set `categoryorder` to *total ascending* or *total descending* if order should be determined by the numerical order of the values. Similarly, the order can be determined by the min, max, sum, mean, geometric mean or median of all the values.", + "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 *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`. Set `categoryorder` to *total ascending* or *total descending* if order should be determined by the numerical order of the values. Similarly, the order can be determined by the min, max, sum, mean, geometric mean or median of all the values. On *multicategory* axes, *trace* orders the second-level categories by the order they appear in the data within each first-level category; only *trace*, *category ascending*, *category descending* and *array* are supported on these axes - other values fall back on the default.", "dflt": "trace", "editType": "calc", "valType": "enumerated", @@ -5820,7 +5820,7 @@ ] }, "categoryarray": { - "description": "Sets the order in which categories on this axis appear. Only has an effect if `categoryorder` is set to *array*. Used with `categoryorder`.", + "description": "Sets the order in which categories on this axis appear. Only has an effect if `categoryorder` is set to *array*. Used with `categoryorder`. On *multicategory* axes each entry is a [first-level, second-level] pair, e.g. `[[*2023*, *Q4*], [*2024*, *Q1*]]`; entries that are not such a pair are ignored.", "editType": "calc", "valType": "data_array" }, @@ -5830,7 +5830,7 @@ "valType": "string" }, "categoryorder": { - "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 *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`. Set `categoryorder` to *total ascending* or *total descending* if order should be determined by the numerical order of the values. Similarly, the order can be determined by the min, max, sum, mean, geometric mean or median of all the values.", + "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 *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`. Set `categoryorder` to *total ascending* or *total descending* if order should be determined by the numerical order of the values. Similarly, the order can be determined by the min, max, sum, mean, geometric mean or median of all the values. On *multicategory* axes, *trace* orders the second-level categories by the order they appear in the data within each first-level category; only *trace*, *category ascending*, *category descending* and *array* are supported on these axes - other values fall back on the default.", "dflt": "trace", "editType": "calc", "valType": "enumerated", @@ -7249,7 +7249,7 @@ ] }, "categoryarray": { - "description": "Sets the order in which categories on this axis appear. Only has an effect if `categoryorder` is set to *array*. Used with `categoryorder`.", + "description": "Sets the order in which categories on this axis appear. Only has an effect if `categoryorder` is set to *array*. Used with `categoryorder`. On *multicategory* axes each entry is a [first-level, second-level] pair, e.g. `[[*2023*, *Q4*], [*2024*, *Q1*]]`; entries that are not such a pair are ignored.", "editType": "plot", "valType": "data_array" }, @@ -7259,7 +7259,7 @@ "valType": "string" }, "categoryorder": { - "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 *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`. Set `categoryorder` to *total ascending* or *total descending* if order should be determined by the numerical order of the values. Similarly, the order can be determined by the min, max, sum, mean, geometric mean or median of all the values.", + "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 *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`. Set `categoryorder` to *total ascending* or *total descending* if order should be determined by the numerical order of the values. Similarly, the order can be determined by the min, max, sum, mean, geometric mean or median of all the values. On *multicategory* axes, *trace* orders the second-level categories by the order they appear in the data within each first-level category; only *trace*, *category ascending*, *category descending* and *array* are supported on these axes - other values fall back on the default.", "dflt": "trace", "editType": "plot", "valType": "enumerated", @@ -7989,7 +7989,7 @@ ] }, "categoryarray": { - "description": "Sets the order in which categories on this axis appear. Only has an effect if `categoryorder` is set to *array*. Used with `categoryorder`.", + "description": "Sets the order in which categories on this axis appear. Only has an effect if `categoryorder` is set to *array*. Used with `categoryorder`. On *multicategory* axes each entry is a [first-level, second-level] pair, e.g. `[[*2023*, *Q4*], [*2024*, *Q1*]]`; entries that are not such a pair are ignored.", "editType": "plot", "valType": "data_array" }, @@ -7999,7 +7999,7 @@ "valType": "string" }, "categoryorder": { - "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 *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`. Set `categoryorder` to *total ascending* or *total descending* if order should be determined by the numerical order of the values. Similarly, the order can be determined by the min, max, sum, mean, geometric mean or median of all the values.", + "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 *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`. Set `categoryorder` to *total ascending* or *total descending* if order should be determined by the numerical order of the values. Similarly, the order can be determined by the min, max, sum, mean, geometric mean or median of all the values. On *multicategory* axes, *trace* orders the second-level categories by the order they appear in the data within each first-level category; only *trace*, *category ascending*, *category descending* and *array* are supported on these axes - other values fall back on the default.", "dflt": "trace", "editType": "plot", "valType": "enumerated", @@ -8729,7 +8729,7 @@ ] }, "categoryarray": { - "description": "Sets the order in which categories on this axis appear. Only has an effect if `categoryorder` is set to *array*. Used with `categoryorder`.", + "description": "Sets the order in which categories on this axis appear. Only has an effect if `categoryorder` is set to *array*. Used with `categoryorder`. On *multicategory* axes each entry is a [first-level, second-level] pair, e.g. `[[*2023*, *Q4*], [*2024*, *Q1*]]`; entries that are not such a pair are ignored.", "editType": "plot", "valType": "data_array" }, @@ -8739,7 +8739,7 @@ "valType": "string" }, "categoryorder": { - "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 *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`. Set `categoryorder` to *total ascending* or *total descending* if order should be determined by the numerical order of the values. Similarly, the order can be determined by the min, max, sum, mean, geometric mean or median of all the values.", + "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 *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`. Set `categoryorder` to *total ascending* or *total descending* if order should be determined by the numerical order of the values. Similarly, the order can be determined by the min, max, sum, mean, geometric mean or median of all the values. On *multicategory* axes, *trace* orders the second-level categories by the order they appear in the data within each first-level category; only *trace*, *category ascending*, *category descending* and *array* are supported on these axes - other values fall back on the default.", "dflt": "trace", "editType": "plot", "valType": "enumerated", @@ -13675,7 +13675,7 @@ ] }, "categoryarray": { - "description": "Sets the order in which categories on this axis appear. Only has an effect if `categoryorder` is set to *array*. Used with `categoryorder`.", + "description": "Sets the order in which categories on this axis appear. Only has an effect if `categoryorder` is set to *array*. Used with `categoryorder`. On *multicategory* axes each entry is a [first-level, second-level] pair, e.g. `[[*2023*, *Q4*], [*2024*, *Q1*]]`; entries that are not such a pair are ignored.", "editType": "calc", "valType": "data_array" }, @@ -13685,7 +13685,7 @@ "valType": "string" }, "categoryorder": { - "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 *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`. Set `categoryorder` to *total ascending* or *total descending* if order should be determined by the numerical order of the values. Similarly, the order can be determined by the min, max, sum, mean, geometric mean or median of all the values.", + "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 *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`. Set `categoryorder` to *total ascending* or *total descending* if order should be determined by the numerical order of the values. Similarly, the order can be determined by the min, max, sum, mean, geometric mean or median of all the values. On *multicategory* axes, *trace* orders the second-level categories by the order they appear in the data within each first-level category; only *trace*, *category ascending*, *category descending* and *array* are supported on these axes - other values fall back on the default.", "dflt": "trace", "editType": "calc", "valType": "enumerated", @@ -15271,7 +15271,7 @@ ] }, "categoryarray": { - "description": "Sets the order in which categories on this axis appear. Only has an effect if `categoryorder` is set to *array*. Used with `categoryorder`.", + "description": "Sets the order in which categories on this axis appear. Only has an effect if `categoryorder` is set to *array*. Used with `categoryorder`. On *multicategory* axes each entry is a [first-level, second-level] pair, e.g. `[[*2023*, *Q4*], [*2024*, *Q1*]]`; entries that are not such a pair are ignored.", "editType": "calc", "valType": "data_array" }, @@ -15281,7 +15281,7 @@ "valType": "string" }, "categoryorder": { - "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 *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`. Set `categoryorder` to *total ascending* or *total descending* if order should be determined by the numerical order of the values. Similarly, the order can be determined by the min, max, sum, mean, geometric mean or median of all the values.", + "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 *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`. Set `categoryorder` to *total ascending* or *total descending* if order should be determined by the numerical order of the values. Similarly, the order can be determined by the min, max, sum, mean, geometric mean or median of all the values. On *multicategory* axes, *trace* orders the second-level categories by the order they appear in the data within each first-level category; only *trace*, *category ascending*, *category descending* and *array* are supported on these axes - other values fall back on the default.", "dflt": "trace", "editType": "calc", "valType": "enumerated", From 6ab6a77cbdfb6758842f7ffbe7d3fb4149e8004f Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 5 Aug 2026 05:43:57 +0000 Subject: [PATCH 2/2] Add draftlog for #7932 Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01VuN1reWCtHE6WFsNZjdGCD --- draftlogs/7932_add.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 draftlogs/7932_add.md diff --git a/draftlogs/7932_add.md b/draftlogs/7932_add.md new file mode 100644 index 00000000000..7dc005c59c2 --- /dev/null +++ b/draftlogs/7932_add.md @@ -0,0 +1 @@ + - Add support for `categoryorder` and `categoryarray` on `multicategory` axes [[#7932](https://github.com/plotly/plotly.js/pull/7932)]