Skip to content

Commit 2e9dbad

Browse files
committed
fix date and log axis manual tick0/dtick handling
1 parent e63ea3b commit 2e9dbad

File tree

3 files changed

+129
-10
lines changed

3 files changed

+129
-10
lines changed

src/plots/cartesian/layout_attributes.js

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -137,34 +137,43 @@ module.exports = {
137137
].join(' ')
138138
},
139139
tick0: {
140-
valType: 'number',
140+
valType: 'any',
141141
dflt: 0,
142142
role: 'style',
143143
description: [
144144
'Sets the placement of the first tick on this axis.',
145145
'Use with `dtick`.',
146146
'If the axis `type` is *log*, then you must take the log of your starting tick',
147-
'(e.g. to set the starting tick to 100, set the `tick0` to 2).',
147+
'(e.g. to set the starting tick to 100, set the `tick0` to 2)',
148+
'except when `dtick`=*L<f>* (see `dtick` for more info).',
148149
'If the axis `type` is *date*, it should be a date string, like date data.',
149150
'If the axis `type` is *category*, it should be a number, using the scale where',
150151
'each category is assigned a serial number from zero in the order it appears.'
151152
].join(' ')
152153
},
153154
dtick: {
154155
valType: 'any',
155-
dflt: 1,
156156
role: 'style',
157157
description: [
158-
'Sets the step in-between ticks on this axis',
159-
'Use with `tick0`.',
158+
'Sets the step in-between ticks on this axis. Use with `tick0`.',
159+
'Must be a positive number, or special strings available to *log* and *date* axes.',
160160
'If the axis `type` is *log*, then ticks are set every 10^(n*dtick) where n',
161161
'is the tick number. For example,',
162162
'to set a tick mark at 1, 10, 100, 1000, ... set dtick to 1.',
163163
'To set tick marks at 1, 100, 10000, ... set dtick to 2.',
164164
'To set tick marks at 1, 5, 25, 125, 625, 3125, ... set dtick to log_10(5), or 0.69897000433.',
165+
'*log* has several special values; *L<f>*, where `f` is a positive number,',
166+
'gives ticks linearly spaced in value (but not position).',
167+
'For example `tick0` = 0.1, `dtick` = *L0.5* will put ticks at 0.1, 0.6, 1.1, 1.6 etc.',
168+
'To show powers of 10 plus small digits between, use *D1* (all digits) or *D2* (only 2 and 5).',
169+
'`tick0` is ignored for *D1* and *D2*.',
165170
'If the axis `type` is *date*, then you must convert the time to milliseconds.',
166171
'For example, to set the interval between ticks to one day,',
167-
'set `dtick` to 86400000.0.'
172+
'set `dtick` to 86400000.0.',
173+
'*date* also has special values *M<n>* gives ticks spaced by a number of months.',
174+
'`n` must be a positive integer.',
175+
'To set ticks on the 15th of every third month, set `tick0` to *2000-01-15* and `dtick` to *M3*.',
176+
'To set ticks every 4 years, set `dtick` to *M48*'
168177
].join(' ')
169178
},
170179
tickvals: {

src/plots/cartesian/tick_value_defaults.js

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,22 +22,53 @@ module.exports = function handleTickValueDefaults(containerIn, containerOut, coe
2222
}
2323

2424
if(Array.isArray(containerIn.tickvals)) tickmodeDefault = 'array';
25-
else if(containerIn.dtick && isNumeric(containerIn.dtick)) {
25+
else if(containerIn.dtick) {
2626
tickmodeDefault = 'linear';
2727
}
2828
var tickmode = coerce('tickmode', tickmodeDefault);
2929

3030
if(tickmode === 'auto') coerce('nticks');
3131
else if(tickmode === 'linear') {
32-
var tick0 = coerce('tick0');
33-
coerce('dtick');
32+
// dtick is usually a positive number, but there are some
33+
// special strings available for log or date axes
34+
// default is 1 day for dates, otherwise 1
35+
var dtickDflt = (axType === 'date') ? 86400000 : 1;
36+
var dtick = coerce('dtick', dtickDflt);
37+
if(isNumeric(dtick)) {
38+
containerOut.dtick = (dtick > 0) ? Number(dtick) : dtickDflt;
39+
}
40+
else if(typeof dtick !== 'string') {
41+
containerOut.dtick = dtickDflt;
42+
}
43+
else {
44+
// date and log special cases are all one character plus a number
45+
var prefix = dtick.charAt(0),
46+
dtickNum = dtick.substr(1);
47+
48+
dtickNum = isNumeric(dtickNum) ? Number(dtickNum) : 0;
49+
if((dtickNum <= 0) || !(
50+
// "M<n>" gives ticks every (integer) n months
51+
(axType === 'date' && prefix === 'M' && dtickNum === Math.round(dtickNum)) ||
52+
// "L<f>" gives ticks linearly spaced in data (not in position) every (float) f
53+
(axType === 'log' && prefix === 'L') ||
54+
// "D1" gives powers of 10 with all small digits between, "D2" gives only 2 and 5
55+
(axType === 'log' && prefix === 'D' && (dtickNum === 1 || dtickNum === 2))
56+
)) {
57+
containerOut.dtick = dtickDflt;
58+
}
59+
}
3460

3561
// tick0 can have different valType for different axis types, so
3662
// validate that now. Also for dates, change milliseconds to date strings
63+
var tick0 = coerce('tick0');
3764
if(axType === 'date') {
3865
containerOut.tick0 = Lib.cleanDate(tick0, '2000-01-01');
3966
}
40-
else if(!isNumeric(tick0)) {
67+
// Aside from date axes, dtick must be numeric; D1 and D2 modes ignore tick0 entirely
68+
else if(isNumeric(tick0) && dtick !== 'D1' && dtick !== 'D2') {
69+
containerOut.tick0 = Number(tick0);
70+
}
71+
else {
4172
containerOut.tick0 = 0;
4273
}
4374
}

test/jasmine/tests/axes_test.js

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -676,6 +676,85 @@ describe('Test axes', function() {
676676
expect(axOut.dtick).toBe(0.00159);
677677
});
678678

679+
it('should handle tick0 and dtick for date axes', function() {
680+
var someMs = 123456789,
681+
someMsDate = Lib.ms2DateTime(someMs),
682+
oneDay = 24 * 3600 * 1000,
683+
axIn = {tick0: someMs, dtick: String(3 * oneDay)},
684+
axOut = {};
685+
mockSupplyDefaults(axIn, axOut, 'date');
686+
expect(axOut.tick0).toBe(someMsDate);
687+
expect(axOut.dtick).toBe(3 * oneDay);
688+
689+
var someDate = '2011-12-15 13:45:56';
690+
axIn = {tick0: someDate, dtick: 'M15'};
691+
axOut = {};
692+
mockSupplyDefaults(axIn, axOut, 'date');
693+
expect(axOut.tick0).toBe(someDate);
694+
expect(axOut.dtick).toBe('M15');
695+
696+
// now some stuff that shouldn't work, should give defaults
697+
[
698+
['next thursday', -1],
699+
['123-45', 'L1'],
700+
['', 'M0.5'],
701+
['', 'M-1'],
702+
['', '2000-01-01']
703+
].forEach(function(v) {
704+
axIn = {tick0: v[0], dtick: v[1]};
705+
axOut = {};
706+
mockSupplyDefaults(axIn, axOut, 'date');
707+
expect(axOut.tick0).toBe('2000-01-01');
708+
expect(axOut.dtick).toBe(oneDay);
709+
});
710+
});
711+
712+
it('should handle tick0 and dtick for log axes', function() {
713+
var axIn = {tick0: '0.2', dtick: 0.3},
714+
axOut = {};
715+
mockSupplyDefaults(axIn, axOut, 'log');
716+
expect(axOut.tick0).toBe(0.2);
717+
expect(axOut.dtick).toBe(0.3);
718+
719+
['D1', 'D2'].forEach(function(v) {
720+
axIn = {tick0: -1, dtick: v};
721+
axOut = {};
722+
mockSupplyDefaults(axIn, axOut, 'log');
723+
// tick0 gets ignored for D<n>
724+
expect(axOut.tick0).toBe(0);
725+
expect(axOut.dtick).toBe(v);
726+
});
727+
728+
[
729+
[-1, 'L3'],
730+
['0.2', 'L0.3'],
731+
[-1, 3],
732+
['0.1234', '0.69238473']
733+
].forEach(function(v) {
734+
axIn = {tick0: v[0], dtick: v[1]};
735+
axOut = {};
736+
mockSupplyDefaults(axIn, axOut, 'log');
737+
expect(axOut.tick0).toBe(Number(v[0]));
738+
expect(axOut.dtick).toBe((+v[1]) ? Number(v[1]) : v[1]);
739+
});
740+
741+
// now some stuff that should not work, should give defaults
742+
[
743+
['', -1],
744+
['D1', 'D3'],
745+
['', 'D0'],
746+
['2011-01-01', 'L0'],
747+
['', 'L-1']
748+
].forEach(function(v) {
749+
axIn = {tick0: v[0], dtick: v[1]};
750+
axOut = {};
751+
mockSupplyDefaults(axIn, axOut, 'log');
752+
expect(axOut.tick0).toBe(0);
753+
expect(axOut.dtick).toBe(1);
754+
});
755+
756+
});
757+
679758
it('should set tickvals and ticktext iff tickmode=array', function() {
680759
var axIn = {tickmode: 'auto', tickvals: [1, 2, 3], ticktext: ['4', '5', '6']},
681760
axOut = {};

0 commit comments

Comments
 (0)