Skip to content

Fix relayout axis category #510

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

Merged
merged 6 commits into from
May 5, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/plot_api/plot_api.js
Original file line number Diff line number Diff line change
Expand Up @@ -2211,6 +2211,12 @@ Plotly.relayout = function relayout(gd, astr, val) {
else if(/[xy]axis[0-9]*?$/.test(pleaf) && !Object.keys(vi || {}).length) {
docalc = true;
}
else if(/[xy]axis[0-9]*\.categoryorder$/.test(pleafPlus)) {
docalc = true;
}
else if(/[xy]axis[0-9]*\.categoryarray/.test(pleafPlus)) {
docalc = true;
}

if(pleafPlus.indexOf('rangeslider') !== -1) {
docalc = true;
Expand Down
13 changes: 7 additions & 6 deletions src/plots/cartesian/axis_defaults.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,10 +74,6 @@ module.exports = function handleAxisDefaults(containerIn, containerOut, coerce,
}
}

containerOut._initialCategories = axType === 'category' ?
orderedCategories(letter, containerIn.categoryorder, containerIn.categoryarray, options.data) :
[];

setConvert(containerOut);

var dfltColor = coerce('color');
Expand Down Expand Up @@ -142,13 +138,18 @@ module.exports = function handleAxisDefaults(containerIn, containerOut, coerce,
delete containerOut.zerolinewidth;
}

// fill in categories
containerOut._initialCategories = axType === 'category' ?
orderedCategories(letter, containerOut.categoryorder, containerOut.categoryarray, options.data) :
[];

return containerOut;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@etpinard a quick chat or pointer about containerIn vs Out would be interesting as I kept using the former.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As a rule, user data (i.e. gd.data / gd.layout or their nested containerIn objects) should not be used anywhere after the supply-defautls step. They are a few exceptions to that rule (namely scene.camera), but we should try to keep the number of exceptions as small as possible.

Previously, orderedCategories was using containerIn.categoryorder. In the case where categoryorder was coerced to 'array' via catgoryarray here, orderedCategories was not given the correct categoryorder value.

In order words, user data/layout is (exactly) what the user inputs, whereas full data/layout is what plotly.js uses to compute and draw things.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @etpinard! It's so useful it would deserve to stick around as a block comment in the code. Sometimes the thoughts and concepts behind coding choices are easily unravelled, but I definitely missed some.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would deserve more than a block comment in my opinion.

We should try tackling #66 soon.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@monfera I'm refactoring our top-level supply-defaults functions in #491 , I'll add the info you're requesting to the Plots.supplyDefaults jsDoc in PR #491 .

};

function setAutoType(ax, data) {
// new logic: let people specify any type they want,
// only autotype if type is '-'
if(ax.type!=='-') return;
if(ax.type !== '-') return;

var id = ax._id,
axLetter = id.charAt(0);
Expand All @@ -163,7 +164,7 @@ function setAutoType(ax, data) {
// should always default to a linear axis
if(d0.type==='histogram' &&
axLetter === {v: 'y', h: 'x'}[d0.orientation || 'v']) {
ax.type='linear';
ax.type = 'linear';
return;
}

Expand Down
31 changes: 12 additions & 19 deletions src/plots/cartesian/category_order_defaults.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,32 +8,25 @@

'use strict';

var layoutAttributes = require('./layout_attributes');

module.exports = function handleCategoryOrderDefaults(containerIn, containerOut, coerce) {
if(containerOut.type !== 'category') return;

if(containerIn.type !== 'category') return;
var arrayIn = containerIn.categoryarray,
orderDefault;

var validCategories = layoutAttributes.categoryorder.values;
var isValidArray = (Array.isArray(arrayIn) && arrayIn.length > 0);

var propercategoryarray = Array.isArray(containerIn.categoryarray) && containerIn.categoryarray.length > 0;
// override default 'categoryorder' value when non-empty array is supplied
if(isValidArray) orderDefault = 'array';

if(validCategories.indexOf(containerIn.categoryorder) === -1 && propercategoryarray) {
var order = coerce('categoryorder', orderDefault);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, this made me learn a bit about the safety/necessity of doing the coercion unconditionally, off the bat.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

doing the coercion unconditionally, off the bat.

Not sure if it's better, but coercing off the bat is definitely the most common approach in our code base.


// when unspecified or invalid, use the default, unless categoryarray implies 'array'
coerce('categoryorder', 'array'); // promote to 'array'

} else if(containerIn.categoryorder === 'array' && !propercategoryarray) {

// when mode is 'array' but no list is given, revert to default

containerIn.categoryorder = 'trace'; // revert to default
coerce('categoryorder');

} else {

// otherwise use the supplied mode, or the default one if unsupplied or invalid
coerce('categoryorder');
// coerce 'categoryarray' only in array order case
if(order === 'array') coerce('categoryarray');

// cannot set 'categoryorder' to 'array' with an invalid 'categoryarray'
if(!isValidArray && order === 'array') {
containerOut.categoryorder = 'trace';
}
};
15 changes: 15 additions & 0 deletions test/jasmine/tests/axes_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -399,22 +399,27 @@ describe('Test axes', function() {
it('should set categoryorder to default if categoryorder and categoryarray are not supplied', function() {
PlotlyInternal.plot(gd, [{x: ['c','a','e','b','d'], y: [15,11,12,13,14]}], {xaxis: {type: 'category'}});
expect(gd._fullLayout.xaxis.categoryorder).toBe('trace');
expect(gd._fullLayout.xaxis.categorarray).toBe(undefined);
});

it('should set categoryorder to default even if type is not set to category explicitly', function() {
PlotlyInternal.plot(gd, [{x: ['c','a','e','b','d'], y: [15,11,12,13,14]}]);
expect(gd._fullLayout.xaxis.categoryorder).toBe('trace');
expect(gd._fullLayout.xaxis.categorarray).toBe(undefined);
});

it('should NOT set categoryorder to default if type is not category', function() {
PlotlyInternal.plot(gd, [{x: ['c','a','e','b','d'], y: [15,11,12,13,14]}]);
expect(gd._fullLayout.yaxis.categoryorder).toBe(undefined);
expect(gd._fullLayout.xaxis.categorarray).toBe(undefined);
});

it('should set categoryorder to default if type is overridden to be category', function() {
PlotlyInternal.plot(gd, [{x: [1,2,3,4,5], y: [15,11,12,13,14]}], {yaxis: {type: 'category'}});
expect(gd._fullLayout.xaxis.categoryorder).toBe(undefined);
expect(gd._fullLayout.yaxis.categorarray).toBe(undefined);
expect(gd._fullLayout.yaxis.categoryorder).toBe('trace');
expect(gd._fullLayout.yaxis.categorarray).toBe(undefined);
});

});
Expand All @@ -426,20 +431,23 @@ describe('Test axes', function() {
xaxis: {type: 'category', categoryorder: 'array', categoryarray: ['b','a','d','e','c']}
});
expect(gd._fullLayout.xaxis.categoryorder).toBe('array');
expect(gd._fullLayout.xaxis.categoryarray).toEqual(['b','a','d','e','c']);
});

it('should switch categoryorder on "array" if it is not supplied but categoryarray is supplied', function() {
PlotlyInternal.plot(gd, [{x: ['c','a','e','b','d'], y: [15,11,12,13,14]}], {
xaxis: {type: 'category', categoryarray: ['b','a','d','e','c']}
});
expect(gd._fullLayout.xaxis.categoryorder).toBe('array');
expect(gd._fullLayout.xaxis.categoryarray).toEqual(['b','a','d','e','c']);
});

it('should revert categoryorder to "trace" if "array" is supplied but there is no list', function() {
PlotlyInternal.plot(gd, [{x: ['c','a','e','b','d'], y: [15,11,12,13,14]}], {
xaxis: {type: 'category', categoryorder: 'array'}
});
expect(gd._fullLayout.xaxis.categoryorder).toBe('trace');
expect(gd._fullLayout.xaxis.categorarray).toBe(undefined);
});

});
Expand All @@ -451,13 +459,15 @@ describe('Test axes', function() {
xaxis: {type: 'category', categoryorder: 'array', categoryarray: []}
});
expect(gd._fullLayout.xaxis.categoryorder).toBe('trace');
expect(gd._fullLayout.xaxis.categoryarray).toEqual([]);
});

it('should not switch categoryorder on "array" if categoryarray is supplied but empty', function() {
PlotlyInternal.plot(gd, [{x: ['c','a','e','b','d'], y: [15,11,12,13,14]}], {
xaxis: {type: 'category', categoryarray: []}
});
expect(gd._fullLayout.xaxis.categoryorder).toBe('trace');
expect(gd._fullLayout.xaxis.categoryarray).toEqual(undefined);
});
});

Expand All @@ -468,20 +478,23 @@ describe('Test axes', function() {
xaxis: {type: 'category', categoryorder: 'trace', categoryarray: ['b','a','d','e','c']}
});
expect(gd._fullLayout.xaxis.categoryorder).toBe('trace');
expect(gd._fullLayout.xaxis.categoryarray).toBe(undefined);
});

it('should use specified categoryorder if it is supplied even if categoryarray exists', function() {
PlotlyInternal.plot(gd, [{x: ['c','a','e','b','d'], y: [15,11,12,13,14]}], {
xaxis: {type: 'category', categoryorder: 'category ascending', categoryarray: ['b','a','d','e','c']}
});
expect(gd._fullLayout.xaxis.categoryorder).toBe('category ascending');
expect(gd._fullLayout.xaxis.categoryarray).toBe(undefined);
});

it('should use specified categoryorder if it is supplied even if categoryarray exists', function() {
PlotlyInternal.plot(gd, [{x: ['c','a','e','b','d'], y: [15,11,12,13,14]}], {
xaxis: {type: 'category', categoryorder: 'category descending', categoryarray: ['b','a','d','e','c']}
});
expect(gd._fullLayout.xaxis.categoryorder).toBe('category descending');
expect(gd._fullLayout.xaxis.categoryarray).toBe(undefined);
});

});
Expand All @@ -493,13 +506,15 @@ describe('Test axes', function() {
xaxis: {type: 'category', categoryorder: 'invalid value'}
});
expect(gd._fullLayout.xaxis.categoryorder).toBe('trace');
expect(gd._fullLayout.xaxis.categoryarray).toBe(undefined);
});

it('should switch categoryorder to "array" if mode is supplied but invalid and list is supplied', function() {
PlotlyInternal.plot(gd, [{x: ['c','a','e','b','d'], y: [15,11,12,13,14]}], {
xaxis: {type: 'category', categoryorder: 'invalid value', categoryarray: ['b','a','d','e','c']}
});
expect(gd._fullLayout.xaxis.categoryorder).toBe('array');
expect(gd._fullLayout.xaxis.categoryarray).toEqual(['b','a','d','e','c']);
});

});
Expand Down
54 changes: 54 additions & 0 deletions test/jasmine/tests/cartesian_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,57 @@ describe('zoom box element', function() {
.toEqual(0);
});
});

describe('relayout', function() {

describe('axis category attributes', function() {
var mock = require('@mocks/basic_bar.json');

var gd, mockCopy;

beforeEach(function() {
mockCopy = Lib.extendDeep({}, mock);
gd = createGraphDiv();
});

afterEach(destroyGraphDiv);

it('should response to \'categoryarray\' and \'categoryorder\' updates', function(done) {
function assertCategories(list) {
d3.selectAll('g.xtick').each(function(_, i) {
var tick = d3.select(this).select('text');
expect(tick.html()).toEqual(list[i]);
});
}

Plotly.plot(gd, mockCopy.data, mockCopy.layout).then(function() {
assertCategories(['giraffes', 'orangutans', 'monkeys']);

return Plotly.relayout(gd, 'xaxis.categoryorder', 'category descending');
}).then(function() {
var list = ['orangutans', 'monkeys', 'giraffes'];

expect(gd._fullLayout.xaxis._initialCategories).toEqual(list);
assertCategories(list);

return Plotly.relayout(gd, 'xaxis.categoryorder', null);
}).then(function() {
assertCategories(['giraffes', 'orangutans', 'monkeys']);

return Plotly.relayout(gd, {
'xaxis.categoryarray': ['monkeys', 'giraffes', 'orangutans']
});
}).then(function() {
var list = ['monkeys', 'giraffes', 'orangutans'];

expect(gd.layout.xaxis.categoryarray).toEqual(list);
expect(gd._fullLayout.xaxis.categoryarray).toEqual(list);
expect(gd._fullLayout.xaxis._initialCategories).toEqual(list);
assertCategories(list);

done();
});
});
});

});