Skip to content

Commit 9e27955

Browse files
committed
switch date axes to use date strings for range and tick0
and introduce new 'r' conversion mode for ranges: can be the same as 'd' (for linear and date), 'l' (for log, for now!) or 'c' (for category, this will have to stay different from 'd' forever so it's continuous)
1 parent d4bb89d commit 9e27955

File tree

9 files changed

+346
-206
lines changed

9 files changed

+346
-206
lines changed

src/components/rangeselector/get_update_object.js

+6-4
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111

1212
var d3 = require('d3');
1313

14+
var Lib = require('../../lib');
15+
1416

1517
module.exports = function getUpdateObject(axisLayout, buttonLayout) {
1618
var axName = axisLayout._name;
@@ -31,7 +33,7 @@ module.exports = function getUpdateObject(axisLayout, buttonLayout) {
3133

3234
function getXRange(axisLayout, buttonLayout) {
3335
var currentRange = axisLayout.range;
34-
var base = new Date(currentRange[1]);
36+
var base = new Date(Lib.dateTime2ms(currentRange[1]));
3537

3638
var step = buttonLayout.step,
3739
count = buttonLayout.count;
@@ -40,13 +42,13 @@ function getXRange(axisLayout, buttonLayout) {
4042

4143
switch(buttonLayout.stepmode) {
4244
case 'backward':
43-
range0 = d3.time[step].offset(base, -count).getTime();
45+
range0 = Lib.ms2DateTime(d3.time[step].offset(base, -count));
4446
break;
4547

4648
case 'todate':
47-
var base2 = d3.time[step].offset(base, -(count - 1));
49+
var base2 = d3.time[step].offset(base, -count);
4850

49-
range0 = d3.time[step].floor(base2).getTime();
51+
range0 = Lib.ms2DateTime(d3.time[step].ceil(base2));
5052
break;
5153
}
5254

src/plots/cartesian/axes.js

+85-45
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,19 @@ axes.minDtick = function(ax, newDiff, newFirst, allow) {
132132
}
133133
};
134134

135+
// Find the autorange for this axis
136+
//
137+
// assumes ax._min and ax._max have already been set by calling axes.expand
138+
// using calcdata from all traces. These are arrays of:
139+
// {val: calcdata value, pad: extra pixels beyond this value}
140+
//
141+
// Returns an array of [min, max]. These are calcdata for log and category axes
142+
// and data for linear and date axes.
143+
//
144+
// TODO: we want to change log to data as well, but it's hard to do this
145+
// maintaining backward compatibility. category will always have to use calcdata
146+
// though, because otherwise values between categories (or outside all categories)
147+
// would be impossible.
135148
axes.getAutoRange = function(ax) {
136149
var newRange = [];
137150

@@ -150,7 +163,12 @@ axes.getAutoRange = function(ax) {
150163

151164
var j, minpt, maxpt, minbest, maxbest, dp, dv,
152165
mbest = 0,
153-
axReverse = (ax.range && ax.range[1] < ax.range[0]);
166+
axReverse = false;
167+
168+
if(ax.range) {
169+
var rng = ax.range.map(ax.r2l);
170+
axReverse = rng[1] < rng[0];
171+
}
154172

155173
// one-time setting to easily reverse the axis
156174
// when plotting from code
@@ -239,11 +257,9 @@ axes.getAutoRange = function(ax) {
239257
}
240258

241259
// maintain reversal
242-
if(axReverse) {
243-
newRange.reverse();
244-
}
260+
if(axReverse) newRange.reverse();
245261

246-
return newRange;
262+
return newRange.map(ax.l2r || Number);
247263
};
248264

249265
axes.doAutoRange = function(ax) {
@@ -444,10 +460,22 @@ axes.autoBin = function(data, ax, nbins, is2d) {
444460
}
445461

446462
// piggyback off autotick code to make "nice" bin sizes
447-
var dummyax = {
448-
type: ax.type === 'log' ? 'linear' : ax.type,
449-
range: [datamin, datamax]
450-
};
463+
var dummyax;
464+
if(ax.type === 'log') {
465+
dummyax = {
466+
type: 'linear',
467+
range: [datamin, datamax]
468+
};
469+
}
470+
else {
471+
dummyax = {
472+
type: ax.type,
473+
// conversion below would be ax.c2r but that's only different from l2r
474+
// for log, and this is the only place (so far?) we would want c2r.
475+
range: [datamin, datamax].map(ax.l2r)
476+
};
477+
}
478+
451479
axes.autoTicks(dummyax, size0);
452480
var binstart = axes.tickIncrement(
453481
axes.tickFirst(dummyax), dummyax.dtick, 'reverse'),
@@ -529,6 +557,8 @@ axes.autoBin = function(data, ax, nbins, is2d) {
529557
axes.calcTicks = function calcTicks(ax) {
530558
if(ax.tickmode === 'array') return arrayTicks(ax);
531559

560+
var rng = ax.range.map(ax.r2l);
561+
532562
// calculate max number of (auto) ticks to display based on plot size
533563
if(ax.tickmode === 'auto' || !ax.dtick) {
534564
var nt = ax.nticks,
@@ -543,7 +573,7 @@ axes.calcTicks = function calcTicks(ax) {
543573
nt = Lib.constrain(ax._length / minPx, 4, 9) + 1;
544574
}
545575
}
546-
axes.autoTicks(ax, Math.abs(ax.range[1] - ax.range[0]) / nt);
576+
axes.autoTicks(ax, Math.abs(rng[1] - rng[0]) / nt);
547577
// check for a forced minimum dtick
548578
if(ax._minDtick > 0 && ax.dtick < ax._minDtick * 2) {
549579
ax.dtick = ax._minDtick;
@@ -553,8 +583,7 @@ axes.calcTicks = function calcTicks(ax) {
553583

554584
// check for missing tick0
555585
if(!ax.tick0) {
556-
ax.tick0 = (ax.type === 'date') ?
557-
new Date(2000, 0, 1).getTime() : 0;
586+
ax.tick0 = (ax.type === 'date') ? '2000-01-01' : 0;
558587
}
559588

560589
// now figure out rounding of tick values
@@ -564,12 +593,12 @@ axes.calcTicks = function calcTicks(ax) {
564593
ax._tmin = axes.tickFirst(ax);
565594

566595
// check for reversed axis
567-
var axrev = (ax.range[1] < ax.range[0]);
596+
var axrev = (rng[1] < rng[0]);
568597

569598
// return the full set of tick vals
570599
var vals = [],
571600
// add a tiny bit so we get ticks which may have rounded out
572-
endtick = ax.range[1] * 1.0001 - ax.range[0] * 0.0001;
601+
endtick = rng[1] * 1.0001 - rng[0] * 0.0001;
573602
if(ax.type === 'category') {
574603
endtick = (axrev) ? Math.max(-0.5, endtick) :
575604
Math.min(ax._categories.length - 0.5, endtick);
@@ -597,15 +626,15 @@ function arrayTicks(ax) {
597626
var vals = ax.tickvals,
598627
text = ax.ticktext,
599628
ticksOut = new Array(vals.length),
600-
r0expanded = ax.range[0] * 1.0001 - ax.range[1] * 0.0001,
601-
r1expanded = ax.range[1] * 1.0001 - ax.range[0] * 0.0001,
629+
rng = ax.range.map(ax.r2l),
630+
r0expanded = rng[0] * 1.0001 - rng[1] * 0.0001,
631+
r1expanded = rng[1] * 1.0001 - rng[0] * 0.0001,
602632
tickMin = Math.min(r0expanded, r1expanded),
603633
tickMax = Math.max(r0expanded, r1expanded),
604634
vali,
605635
i,
606636
j = 0;
607637

608-
609638
// without a text array, just format the given values as any other ticks
610639
// except with more precision to the numbers
611640
if(!Array.isArray(text)) text = [];
@@ -658,7 +687,7 @@ axes.autoTicks = function(ax, roughDTick) {
658687
var base;
659688

660689
if(ax.type === 'date') {
661-
ax.tick0 = new Date(2000, 0, 1).getTime();
690+
ax.tick0 = '2000-01-01';
662691

663692
if(roughDTick > 15778800000) {
664693
// years if roughDTick > 6mo
@@ -675,7 +704,7 @@ axes.autoTicks = function(ax, roughDTick) {
675704
// days if roughDTick > 12h
676705
ax.dtick = roundDTick(roughDTick, 86400000, roundDays);
677706
// get week ticks on sunday
678-
ax.tick0 = new Date(2000, 0, 2).getTime();
707+
ax.tick0 = '2000-01-01';
679708
}
680709
else if(roughDTick > 1800000) {
681710
// hours if roughDTick > 30m
@@ -697,16 +726,19 @@ axes.autoTicks = function(ax, roughDTick) {
697726
}
698727
else if(ax.type === 'log') {
699728
ax.tick0 = 0;
729+
var rng = ax.range.map(ax.r2l);
700730

701-
// only show powers of 10
702-
if(roughDTick > 0.7) ax.dtick = Math.ceil(roughDTick);
703-
else if(Math.abs(ax.range[1] - ax.range[0]) < 1) {
731+
if(roughDTick > 0.7) {
732+
// only show powers of 10
733+
ax.dtick = Math.ceil(roughDTick);
734+
}
735+
else if(Math.abs(rng[1] - rng[0]) < 1) {
704736
// span is less than one power of 10
705-
var nt = 1.5 * Math.abs((ax.range[1] - ax.range[0]) / roughDTick);
737+
var nt = 1.5 * Math.abs((rng[1] - rng[0]) / roughDTick);
706738

707739
// ticks on a linear scale, labeled fully
708-
roughDTick = Math.abs(Math.pow(10, ax.range[1]) -
709-
Math.pow(10, ax.range[0])) / nt;
740+
roughDTick = Math.abs(Math.pow(10, rng[1]) -
741+
Math.pow(10, rng[0])) / nt;
710742
base = Math.pow(10, Math.floor(Math.log(roughDTick) / Math.LN10));
711743
ax.dtick = 'L' + roundDTick(roughDTick, base, roundBase10);
712744
}
@@ -745,13 +777,16 @@ axes.autoTicks = function(ax, roughDTick) {
745777
// for date ticks, the last date part to show (y,m,d,H,M,S)
746778
// or an integer # digits past seconds
747779
function autoTickRound(ax) {
748-
var dtick = ax.dtick,
749-
maxend;
780+
var dtick = ax.dtick;
750781

751782
ax._tickexponent = 0;
752-
if(!isNumeric(dtick) && typeof dtick !== 'string') dtick = 1;
783+
if(!isNumeric(dtick) && typeof dtick !== 'string') {
784+
dtick = 1;
785+
}
753786

754-
if(ax.type === 'category') ax._tickround = null;
787+
if(ax.type === 'category') {
788+
ax._tickround = null;
789+
}
755790
else if(isNumeric(dtick) || dtick.charAt(0) === 'L') {
756791
if(ax.type === 'date') {
757792
if(dtick >= 86400000) ax._tickround = 'd';
@@ -761,14 +796,13 @@ function autoTickRound(ax) {
761796
else ax._tickround = 3 - Math.round(Math.log(dtick / 2) / Math.LN10);
762797
}
763798
else {
799+
// linear or log
800+
var rng = ax.range.map(ax.r2d || Number);
764801
if(!isNumeric(dtick)) dtick = Number(dtick.substr(1));
765802
// 2 digits past largest digit of dtick
766803
ax._tickround = 2 - Math.floor(Math.log(dtick) / Math.LN10 + 0.01);
767804

768-
if(ax.type === 'log') {
769-
maxend = Math.pow(10, Math.max(ax.range[0], ax.range[1]));
770-
}
771-
else maxend = Math.max(Math.abs(ax.range[0]), Math.abs(ax.range[1]));
805+
var maxend = Math.max(Math.abs(rng[0]), Math.abs(rng[1]));
772806

773807
var rangeexp = Math.floor(Math.log(maxend) / Math.LN10 + 0.01);
774808
if(Math.abs(rangeexp) > 3) {
@@ -824,13 +858,16 @@ axes.tickIncrement = function(x, dtick, axrev) {
824858

825859
// calculate the first tick on an axis
826860
axes.tickFirst = function(ax) {
827-
var axrev = ax.range[1] < ax.range[0],
861+
var r2l = ax.r2l || Number,
862+
rng = ax.range.map(r2l),
863+
axrev = rng[1] < rng[0],
828864
sRound = axrev ? Math.floor : Math.ceil,
829865
// add a tiny extra bit to make sure we get ticks
830866
// that may have been rounded out
831-
r0 = ax.range[0] * 1.0001 - ax.range[1] * 0.0001,
867+
r0 = rng[0] * 1.0001 - rng[1] * 0.0001,
832868
dtick = ax.dtick,
833-
tick0 = ax.tick0;
869+
tick0 = r2l(ax.tick0);
870+
834871
if(isNumeric(dtick)) {
835872
var tmin = sRound((r0 - tick0) / dtick) * dtick + tick0;
836873

@@ -916,7 +953,8 @@ axes.tickText = function(ax, x, hover) {
916953
i;
917954

918955
if(arrayMode && Array.isArray(ax.ticktext)) {
919-
var minDiff = Math.abs(ax.range[1] - ax.range[0]) / 10000;
956+
var rng = ax.range.map(ax.r2l),
957+
minDiff = Math.abs(rng[1] - rng[0]) / 10000;
920958
for(i = 0; i < ax.ticktext.length; i++) {
921959
if(Math.abs(x - ax.d2l(ax.tickvals[i])) < minDiff) break;
922960
}
@@ -1106,7 +1144,7 @@ function numFormat(v, ax, fmtoverride, hover) {
11061144
(isNumeric(v) ? Math.abs(v) || 1 : 1),
11071145
// if not showing any exponents, don't change the exponent
11081146
// from what we calculate
1109-
range: ax.showexponent === 'none' ? ax.range : [0, v || 1]
1147+
range: ax.showexponent === 'none' ? ax.range.map(ax.r2d) : [0, v || 1]
11101148
};
11111149
autoTickRound(ah);
11121150
tickRound = (Number(ah._tickround) || 0) + 4;
@@ -1354,8 +1392,9 @@ axes.makeClipPaths = function(gd) {
13541392
// doTicks: draw ticks, grids, and tick labels
13551393
// axid: 'x', 'y', 'x2' etc,
13561394
// blank to do all,
1357-
// 'redraw' to force full redraw, and reset ax._r
1358-
// (stored range for use by zoom/pan)
1395+
// 'redraw' to force full redraw, and reset:
1396+
// ax._r (stored range for use by zoom/pan)
1397+
// ax._rl (stored linearized range for use by zoom/pan)
13591398
// or can pass in an axis object directly
13601399
axes.doTicks = function(gd, axid, skipTitle) {
13611400
var fullLayout = gd._fullLayout,
@@ -1393,7 +1432,10 @@ axes.doTicks = function(gd, axid, skipTitle) {
13931432
return function() {
13941433
if(!ax._id) return;
13951434
var axDone = axes.doTicks(gd, ax._id);
1396-
if(axid === 'redraw') ax._r = ax.range.slice();
1435+
if(axid === 'redraw') {
1436+
ax._r = ax.range.slice();
1437+
ax._rl = ax._r.map(ax.r2l);
1438+
}
13971439
return axDone;
13981440
};
13991441
}));
@@ -1411,9 +1453,6 @@ axes.doTicks = function(gd, axid, skipTitle) {
14111453
}
14121454
}
14131455

1414-
// in case a val turns into string somehow
1415-
ax.range = [+ax.range[0], +ax.range[1]];
1416-
14171456
// set scaling to pixels
14181457
ax.setScale();
14191458

@@ -1802,7 +1841,8 @@ axes.doTicks = function(gd, axid, skipTitle) {
18021841
break;
18031842
}
18041843
}
1805-
var showZl = (ax.range[0] * ax.range[1] <= 0) && ax.zeroline &&
1844+
var rng = ax.range.map(ax.r2l),
1845+
showZl = (rng[0] * rng[1] <= 0) && ax.zeroline &&
18061846
(ax.type === 'linear' || ax.type === '-') && gridvals.length &&
18071847
(hasBarsOrFill || clipEnds({x: 0}) || !ax.showline);
18081848
var zl = zlcontainer.selectAll('path.' + zcls)

src/plots/cartesian/axis_defaults.js

+8-1
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ var setConvert = require('./set_convert');
2525
var orderedCategories = require('./ordered_categories');
2626
var cleanDatum = require('./clean_datum');
2727
var axisIds = require('./axis_ids');
28+
var constants = require('./constants');
2829

2930

3031
/**
@@ -96,7 +97,13 @@ module.exports = function handleAxisDefaults(containerIn, containerOut, coerce,
9697
var autoRange = coerce('autorange', !validRange);
9798

9899
if(autoRange) coerce('rangemode');
99-
var range = coerce('range', [-1, letter === 'x' ? 6 : 4]);
100+
101+
var dfltRange;
102+
if(axType === 'date') dfltRange = constants.DFLTRANGEDATE;
103+
else if(letter === 'x') dfltRange = constants.DFLTRANGEX;
104+
else dfltRange = constants.DFLTRANGEY;
105+
106+
var range = coerce('range', dfltRange.slice());
100107
if(range[0] === range[1]) {
101108
containerOut.range = [range[0] - 1, range[0] + 1];
102109
}

src/plots/cartesian/constants.js

+6-1
Original file line numberDiff line numberDiff line change
@@ -64,5 +64,10 @@ module.exports = {
6464
BENDPX: 1.5,
6565

6666
// delay before a redraw (relayout) after smooth panning and zooming
67-
REDRAWDELAY: 50
67+
REDRAWDELAY: 50,
68+
69+
// last resort axis ranges for x, y, and date axes if we have no data
70+
DFLTRANGEX: [-1, 6],
71+
DFLTRANGEY: [-1, 4],
72+
DFLTRANGEDATE: ['2000-01-01', '2001-01-01'],
6873
};

0 commit comments

Comments
 (0)