-
Notifications
You must be signed in to change notification settings - Fork 0
189 ordinal scale domain item ordering #1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from 11 commits
87ab745
a547482
69bf832
3fd3cc0
e2977c5
62c4ac5
85d60d6
f6c40b6
e4f2167
03643fe
9c366ce
f626b08
2f939af
ce35e8b
262c747
5f33a7b
f4214cb
cbc98fd
a806ca2
5ad6b5c
82076f2
6fb2871
616121b
89794c6
849978e
d6fad10
7c355b8
8a2292c
9b01bcc
010451b
6c9d0b5
166aeb4
e472e14
d16fe6b
0314f37
043ac1b
b98bd65
e007f73
34a5054
c6d44d9
44a0c3d
613fda3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -18,6 +18,7 @@ var layoutAttributes = require('./layout_attributes'); | |
var handleTickValueDefaults = require('./tick_value_defaults'); | ||
var handleTickDefaults = require('./tick_defaults'); | ||
var setConvert = require('./set_convert'); | ||
var orderedCategories = require('./ordered_categories'); | ||
var cleanDatum = require('./clean_datum'); | ||
var axisIds = require('./axis_ids'); | ||
|
||
|
@@ -64,6 +65,10 @@ module.exports = function handleAxisDefaults(containerIn, containerOut, coerce, | |
} | ||
} | ||
|
||
containerOut._initialCategories = axType === 'category' ? | ||
orderedCategories(letter, containerIn.categorymode, containerIn.categorylist, options.data) : | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We'll need to coerce Coercion is done in this scope by the shortcut coerce function defined in Moreover, we'll need to add logic around We may want to look up Don't hesitate to ask me any questions about our coercion framework. Surprising, it may the trickiest part to understand about plotly.js. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @etpinard Looking into this now as it appears to be the last piece in this CR. My test cases will need an update once coercion is in place, to switch to the more data-driven default or override behavior. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @etpinard tl;dr blocking question for you at the end. As an aid to thinking about goals, here's a summary of what coercions I'm planning:
... well it looks like the two things you listed :-) My original take was that Taking it one step further, shouldn't we just have one property, e.g. called I'm now plunging into the basic coercion so we don't have an unexpected enumerative value but your input to the above question would be useful. It's not a big deal to unify the two properties and at least technically, it would be less ambiguous. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
... and you would be correct. What smart coercion is trying to remove redundancy. For example, var layout: {
xaxis: {
categorylist: ['apples', 'bananas', 'clementines']
}
}; should be considered the same as: var layout: {
xaxis: {
categorymode: 'array', // redundant !!!
categorylist: ['apples', 'bananas', 'clementines']
}
}; But, var layout: {
xaxis: {
categorymode: 'category descending', // still used as a switch
categorylist: ['apples', 'bananas', 'clementines'] // not used in graph
}
}; There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @etpinard Thanks! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
[]; | ||
|
||
setConvert(containerOut); | ||
|
||
coerce('title', defaultTitle); | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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', | ||
'value ascending', 'value descending','array' | ||
], | ||
dflt: 'trace', | ||
role: 'style', | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
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.', | ||
'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: 'style', | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. same here |
||
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: { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
/** | ||
* 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'); | ||
|
||
|
||
/** | ||
* TODO add documentation | ||
*/ | ||
module.exports = function orderedCategories(axisLetter, categorymode, categorylist, data) { | ||
|
||
return categorymode === 'array' ? | ||
|
||
// just return a copy of the specified array ... | ||
categorylist.slice() : | ||
|
||
// ... or take the union of all encountered tick keys and sort them as specified | ||
// (could be simplified with lodash-fp or ramda) | ||
[].concat.apply([], data.map(function(d) {return d[axisLetter];})) | ||
.filter(function(element, index, array) {return index === array.indexOf(element);}) | ||
.sort(({ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We like using for loops in these situations. Although they may be not as nice to look at than these functional patterns, they are much faster. 🐎 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @etpinard Thanks, it'll be a quick update from me. |
||
'category ascending': d3.ascending, | ||
'category descending': d3.descending | ||
})[categorymode]); | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @monfera can you elaborate on this? What else have you tried? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @etpinard I tried things like this:
Then I spent a bit of time better understanding concepts, the data flow and the various ways it can be invoked (e.g. plotting multiple lines; overplotting onto an already existing plot etc). Learnt that currently, the categorical X axis ordering is totally driven by the order in which the point tuples arrive and made sample plots. Which was the point at which I gave up on this point for modification and went for the lower hanging fruit of There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @etpinard maybe an example is helpful: The first vector has X cat. values There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @etpinard I also considered doing a sorting a lot earlier (more upstream), but it would have issues: 1) the code should possibly reorder only after it's established that it's a categorical axis (it can be user specified or determined heuristically); 2) by that point there are lots of places where So my current thought is that we need to do the axis tick sorting downstream, nearer the point which is responsible for the d3 data binding order for the axis ticks, and we have to accommodate for the possibility that new lines introduce new points that have to be inserted in lines that have already been added previously. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @monfera Thanks for this very detailed overview. You've got me convinced, We need to fill in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Filling in We already need to loop all traces to check for box plot irregularities here, so maybe you could fill in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @etpinard I'll look into it, thanks! Unrelated: I learnt that date axes take ISO strings There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @etpinard I'll look into it more closely today, but on an initial look, the suggested place isn't as trivial (to me) and I'd put it slightly after this point, because
Therefore I'm planning to put the logic after having returned from setAutoType; the earliest point seems to be just before the call to This way, we wouldn't reuse the suggested loop, but the loop over the traces probably doesn't have a measurable performance impact anyway (I'm assuming there usually aren't thousands or 10ks of trace lines). I'll do some more tests to ensure I'm not overlooking something; just wanted to share current thinking. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Anywhere, in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @etpinard Thanks for the quick note, I'll go ahead. |
||
|
||
if(v !== null && v !== undefined && ax._categories.indexOf(v) === -1) { | ||
ax._categories.push(v); | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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,215 @@ describe('calculated data and points', function() { | |
expect(gd.calcdata[0][3]).toEqual({ x: false, y: false}); | ||
}); | ||
}); | ||
|
||
xdescribe('category ordering', function() { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @monfera these tests are great. The test 🐅 thanks you! Could you add a couple more cases where multiple traces with shared and/or mutually exclusive categories? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @etpinard Thanks :-) Yes, I guess we'll have at least double of the number of test cases (corner cases etc.). Just didn't want to race ahead of your initial feedback and instead, I looked into the unrelated bug you sent my way. Thanks for these specific suggestions. |
||
|
||
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' | ||
// Wouldn't it be preferred to supply a function and plotly would have several functions like this? | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
// E.g. it's easier for symbol completion (whereas there's no symbol completion on string config) | ||
// See arguments from Mike Bostock, highlighted in medium green here: | ||
// https://medium.com/@mbostock/what-makes-software-good-943557f8a488#eef9 | ||
// Plus if it's a function, then users can roll their own. | ||
// | ||
// 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. In this round, I'm assuming that the trace order is implied | ||
// by the order the {x,y} arrays are specified. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
... and you would be correct. |
||
}}); | ||
|
||
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].y).toEqual(11); | ||
expect(gd.calcdata[0][1].y).toEqual(13); | ||
expect(gd.calcdata[0][2].y).toEqual(15); | ||
expect(gd.calcdata[0][3].y).toEqual(14); | ||
expect(gd.calcdata[0][4].y).toEqual(12); | ||
}); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @etpinard Just an FYI, since I started this CR with the test cases, my conception on where order would be present became outdated. As now I understand that gd.calcdata will keep the trace order, I'm updating test cases such that the original trace order is expected, and explicit checks on the x/y tuples are in place. It looks good to me but please tell me if you have an alternative suggestion. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nicely done. |
||
|
||
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].y).toEqual(12); | ||
expect(gd.calcdata[0][1].y).toEqual(14); | ||
expect(gd.calcdata[0][2].y).toEqual(15); | ||
expect(gd.calcdata[0][3].y).toEqual(13); | ||
expect(gd.calcdata[0][4].y).toEqual(11); | ||
}); | ||
|
||
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].y).toEqual(11); | ||
expect(gd.calcdata[0][1].y).toEqual(13); | ||
expect(gd.calcdata[0][2].y).toEqual(15); | ||
expect(gd.calcdata[0][3].y).toEqual(14); | ||
expect(gd.calcdata[0][4].y).toEqual(12); | ||
}); | ||
|
||
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].y).toEqual(11); | ||
expect(gd.calcdata[0][1].y).toEqual(15); | ||
expect(gd.calcdata[0][2].y).toEqual(14); | ||
expect(gd.calcdata[0][3].y).toEqual(12); | ||
}); | ||
}); | ||
|
||
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); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this is correct, the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @etpinard Clarifications:
As I'm looking at the generated plots as well, I found something unexpected: when the first N or last N categories in I believe it's not the desired behavior for categorical axes. If it's indeed a range focus thing, then it's a preexisting condition that arose now because in the past, the axis ticks were driven by the trace points, so trivially, there used to be at least one point for each category. Should I work on solving it, or should we optimize for as quick merging of the baseline functionality as possible? Here's the data:
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @monfera thanks for this writeup. You bring up some interesting points. plotly.js has always been very data-focused. For example, data linked to In the case where Therefore, I'd vote for the status quo, where orphan There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @etpinard Thanks for the answer, it means we don't need to worry about chopping off unused categories on the axis tails for now. I'm just adding a test case that renders the tick for a |
||
|
||
}); | ||
}); | ||
|
||
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].y).toEqual(13); | ||
expect(gd.calcdata[0][1].y).toEqual(11); | ||
expect(gd.calcdata[0][2].y).toEqual(14); | ||
expect(gd.calcdata[0][3].y).toEqual(12); | ||
expect(gd.calcdata[0][4].y).toEqual(15); | ||
}); | ||
|
||
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].y).toEqual(13); | ||
expect(gd.calcdata[0][1].y).toEqual(14); | ||
expect(gd.calcdata[0][2].y).toEqual(15); | ||
}); | ||
|
||
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: ['y','b','x','a','d','z','e','c'] | ||
}}); | ||
|
||
expect(gd.calcdata[0][0].y).toEqual(13); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same here. Specified categories in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @etpinard Yes, totally agree with both these comments. The test cases came before the implementation, i.e. before some of my learning and haven't been updated. |
||
expect(gd.calcdata[0][1].y).toEqual(11); | ||
expect(gd.calcdata[0][2].y).toEqual(14); | ||
expect(gd.calcdata[0][3].y).toEqual(12); | ||
expect(gd.calcdata[0][4].y).toEqual(15); | ||
}); | ||
|
||
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].y).toEqual(13); | ||
expect(gd.calcdata[0][1].y).toEqual(11); | ||
expect(gd.calcdata[0][2].y).toEqual(15); | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmm. I think |
||
// 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. | ||
}); | ||
}); | ||
}); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nice find.