Skip to content

Commit f1b5101

Browse files
committed
Fix multicategory axes ordering second-level categories globally
On a multicategory axis, second-level categories were keyed on where each label first appears anywhere in the data, so every first-level category rendered the same child sequence regardless of its own data order. Track children per parent instead: collect parents in first-appearance order and each parent's children in the order its own data supplies them, then emit the pairs directly - no flat row list and no sort needed, as setCategoryIndex already dedups. The lookups are now prototype-less objects, so a category named e.g. 'toString' no longer resolves through Object.prototype and ends up without an index. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VuN1reWCtHE6WFsNZjdGCD
1 parent 21da158 commit f1b5101

2 files changed

Lines changed: 43 additions & 21 deletions

File tree

src/plots/cartesian/set_convert.js

Lines changed: 22 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -371,10 +371,15 @@ module.exports = function setConvert(ax, fullLayout) {
371371
}
372372
}
373373

374-
// [ [cnt, {$cat: index}], for 1,2 ]
375-
var seen = [[0, {}], [0, {}]];
376-
// [ [arrayIn[0][i], arrayIn[1][i]], for i .. N ]
377-
var list = [];
374+
// first-level categories in first-appearance order
375+
var parents = [];
376+
// {$parentCat: {seen: {$childCat: 1}, children: [$childCat, ..]}}
377+
// second-level categories are tracked *per parent*, so that each
378+
// parent keeps the child order found in its own data rather than
379+
// sharing one global order across all parents.
380+
// prototype-less objects so that e.g. a category named 'toString'
381+
// does not resolve through Object.prototype
382+
var childrenOf = Object.create(null);
378383

379384
for(i = 0; i < traceIndices.length; i++) {
380385
var trace = fullData[traceIndices[i]];
@@ -389,31 +394,27 @@ module.exports = function setConvert(ax, fullLayout) {
389394
var v1 = arrayIn[1][j];
390395

391396
if(isValidCategory(v0) && isValidCategory(v1)) {
392-
list.push([v0, v1]);
393-
394-
if(!(v0 in seen[0][1])) {
395-
seen[0][1][v0] = seen[0][0]++;
397+
if(!(v0 in childrenOf)) {
398+
childrenOf[v0] = {seen: Object.create(null), children: []};
399+
parents.push(v0);
396400
}
397-
if(!(v1 in seen[1][1])) {
398-
seen[1][1][v1] = seen[1][0]++;
401+
402+
var kids = childrenOf[v0];
403+
if(!(v1 in kids.seen)) {
404+
kids.seen[v1] = 1;
405+
kids.children.push(v1);
399406
}
400407
}
401408
}
402409
}
403410
}
404411
}
405412

406-
list.sort(function(a, b) {
407-
var ind0 = seen[0][1];
408-
var d = ind0[a[0]] - ind0[b[0]];
409-
if(d) return d;
410-
411-
var ind1 = seen[1][1];
412-
return ind1[a[1]] - ind1[b[1]];
413-
});
414-
415-
for(i = 0; i < list.length; i++) {
416-
setCategoryIndex(list[i]);
413+
for(i = 0; i < parents.length; i++) {
414+
var children = childrenOf[parents[i]].children;
415+
for(j = 0; j < children.length; j++) {
416+
setCategoryIndex([parents[i], children[j]]);
417+
}
417418
}
418419
};
419420
}

test/jasmine/tests/axes_test.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4354,6 +4354,27 @@ describe('Test axes', function() {
43544354
expect(ax._categoriesMap).toEqual({'1,a': 0, '1,b': 1, '2,a': 2, '2,b': 3});
43554355
});
43564356

4357+
it('should order second-level categories per parent, not globally', function() {
4358+
var out = _makeCalcdata({
4359+
x: [['1', '1', '2', '2'], ['b', 'a', 'a', 'b']]
4360+
}, 'x', 'multicategory');
4361+
4362+
// '2' keeps its own 'a' then 'b' order, even though 'b' is
4363+
// the first second-level category seen overall (under '1')
4364+
expect(out).toEqual([0, 1, 2, 3]);
4365+
expect(ax._categories).toEqual([['1', 'b'], ['1', 'a'], ['2', 'a'], ['2', 'b']]);
4366+
expect(ax._categoriesMap).toEqual({'1,b': 0, '1,a': 1, '2,a': 2, '2,b': 3});
4367+
});
4368+
4369+
it('should not let second-level categories inherit the prototype chain', function() {
4370+
var out = _makeCalcdata({
4371+
x: [['1', '1'], ['toString', 'a']]
4372+
}, 'x', 'multicategory');
4373+
4374+
expect(out).toEqual([0, 1]);
4375+
expect(ax._categories).toEqual([['1', 'toString'], ['1', 'a']]);
4376+
});
4377+
43574378
it('case invalid in x[0]', function() {
43584379
var out = _makeCalcdata({
43594380
x: [['1', '2', null, '2'], ['a', 'a', 'b', 'b']]

0 commit comments

Comments
 (0)