Fix multicategory axis ordering and support categoryorder / categoryarray - #7929
Draft
chriddyp wants to merge 2 commits into
Draft
Fix multicategory axis ordering and support categoryorder / categoryarray#7929chriddyp wants to merge 2 commits into
chriddyp wants to merge 2 commits into
Conversation
Second-level categories on a multicategory axis shared one global
ordering keyed on where each label first appeared anywhere in the data,
so every first-level category rendered the same child sequence
regardless of its own data order. With x = [['2023','2024'], ...] and
2023 contributing Jul-Dec first, 2024 rendered Jul-Dec then Jan-Jun even
though it was supplied Jan-Dec.
`setupMultiCategory` now tracks the child first-appearance index per
parent, so each group keeps the order found in its own data. The lookup
objects are prototype-less, so a category named 'toString' no longer
resolves through Object.prototype.
`categoryorder` and `categoryarray` were also never coerced on
multicategory axes - `handleCategoryOrderDefaults` returned early for any
non-category type - so setting them was a silent no-op. They are now
honoured:
- 'trace' (default) keeps the per-parent data order
- 'array' takes `categoryarray` as [first-level, second-level] pairs;
malformed entries are dropped, and an array holding no valid pair
falls back to 'trace'
- 'category ascending'/'descending' sort the pairs by label
- ordering by aggregated value ('total ascending', ...) is not
implemented for these axes and falls back to 'trace' rather than being
accepted and silently ignored
Three existing baselines encode the old order and need regenerating:
multicategory-sorting, multicategory-y and multicategory2. In
multicategory2 the data supplies 2018 q1, q2, q3 and the current
baseline shows q1, q3, q2.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Written with Claude Code
Draft — the three baselines noted below still need regenerating, see Baselines.
The problem
On a
multicategoryaxis the second-level categories share one global ordering, keyed on where each label first appears anywhere in the data. Every first-level category therefore renders the same child sequence, regardless of its own data order.Minimal case — data order is
P1/b, P1/a, P2/a, P2/b:P1/b P1/a P2/a P2/bP1/b P1/a P2/b P2/aP2is flipped:bwas seen first underP1, sobprecedesaunder every parent.Real-world shape — months under years, rows supplied in strict chronological order (2023 Jul–Dec, 2024 Jan–Dec, 2025 Jan–Jun). 2023 contributes Jul–Dec first, which pins
Jul…Decahead ofJan…Junfor every year:2023 and 2025 looked correct before only because each happens to be a contiguous slice of that one global ordering.
Separately,
categoryorderandcategoryarraywere never coerced on multicategory axes —handleCategoryOrderDefaultsreturned early for any non-categorytype — so setting them was a silent no-op with no way to work around the ordering above.Reported downstream at plotly/dash-ai-analyst#171.
The fix
set_convert.js—setupMultiCategory. Track the child first-appearance index per parent instead of globally, and sort by(parent rank, child rank within that parent). The lookup objects are now prototype-less, so a category namedtoStringno longer resolves throughObject.prototype.category_order_defaults.js. Let multicategory axes through, and handle the pair shape:trace(default) — per-parent data order, as abovearray—categoryarrayentries are[first-level, second-level]pairs. Malformed entries are dropped; an array holding no valid pair falls back totrace. Categories absent fromcategoryarrayfollow in trace order, matching howcategoryaxes already behavecategory ascending/category descending— sort the pairs by labeltotal ascending, …) is not implemented for these axes —sortAxisCategoriesByValueskips non-categoryaxes, and interleaving children across parents would break the grouping brackets anyway. Rather than accept it and silently do nothing, it now falls back totraceNo new attributes;
categoryarrayis alreadydata_array. Descriptions updated for both, withtest/plot-schema.jsonregenerated.Tests
Visual — new mock
test/image/mocks/multicategory-categoryorder.json, four panels over identical data so each ordering is distinguishable. Data is supplied as 2023 → Q4, Q3 and 2024 → Q2, Q1, Q4, Q3, so trace order is deliberately not alphabetical and all four panels differ:I verified this renders correctly in Chromium against a bundle built from this branch — year brackets intact, each panel distinct — but I can't attach screenshots through the API, so the baseline PNG will be the first rendering committed here.
Unit — 10 new specs in
test/jasmine/tests/axes_test.js: per-parent ordering, prototype-name safety, and eachcategoryordermode including both fallbacks.npm run test-jasmine -- axesgoes from 398 to 408 passing. The 2insiderangefailures in my sandbox are font-metric tolerances that fail identically on unmodifiedmaster.Regression sweep
Rendered all 1067 non-gl3d/map/geo mocks under
masterand this branch and diffed each multicategory axis's resolved_categories. 1064 identical, 3 changed — all three corrections:multicategory22018/q1 2018/q3 2018/q22018/q1 2018/q2 2018/q3multicategory-y2018/q1 2018/q3 2018/q22018/q1 2018/q2 2018/q3multicategory-sorting4/1 4/2 … 6/1 6/24/2 4/1 … 6/2 6/1multicategory2is the clearest: the mock supplies2018 q1, q2, q3and the committed baseline showsq1, q3, q2— the existing baseline encodes the bug.multicategory-sortingsubplot 2 draws4/2from the first trace before4/1from the second, so per-parent order is4/2, 4/1.Baselines — needs a maintainer
Four baselines need generating: the three above, plus the new mock. I did not commit them. My sandbox's kaleido rendering does not match CI's — regenerating the untouched
multicategorybaseline as a control produced 10910 differing pixels (max channel delta 205), i.e. font rendering differs, so any baseline I generated would be wrong in a way unrelated to this change.Happy to push them if a maintainer would rather paste the generated PNGs, or to split the three baseline updates into their own commit.
Notes for review
findCategoryPairsduplicates a little ofsetupMultiCategory's traversal. It runs at defaults time, before calc, wheretrace._lengthisn't available yet — henceMath.minon the two row lengths rather thanLib.minRowLength. Happy to factor it out if you'd prefer.VALUE_ORDER_REmirrorssortAxisCategoriesByValueRegexinplots.js. I kept them as separate literals to avoid a require cycle and left a comment on each; exporting one from a shared module would also work._initialCategoriesseeding inclearCalcalready handled array-valued categories —setCategoryIndexstringifies pairs to"parent,child"for_categoriesMapand pushes the array onto_categories. That mechanism needed no change; multicategory simply never reached it.🤖 Generated with Claude Code
https://claude.ai/code/session_01XAGQnaXsViqVPvak39Y4qo