diff --git a/src/plots/cartesian/axes.js b/src/plots/cartesian/axes.js
index bd86bb76b06..cb07516caae 100644
--- a/src/plots/cartesian/axes.js
+++ b/src/plots/cartesian/axes.js
@@ -611,8 +611,6 @@ axes.autoBin = function(data, ax, nbins, is2d) {
// in any case, set tickround to # of digits to round tick labels to,
// or codes to this effect for log and date scales
axes.calcTicks = function calcTicks(ax) {
- if(ax.tickmode === 'array') return arrayTicks(ax);
-
var rng = ax.range.map(ax.r2l);
// calculate max number of (auto) ticks to display based on plot size
@@ -629,6 +627,11 @@ axes.calcTicks = function calcTicks(ax) {
nt = Lib.constrain(ax._length / minPx, 4, 9) + 1;
}
}
+
+ // add a couple of extra digits for filling in ticks when we
+ // have explicit tickvals without tick text
+ if(ax.tickmode === 'array') nt *= 100;
+
axes.autoTicks(ax, Math.abs(rng[1] - rng[0]) / nt);
// check for a forced minimum dtick
if(ax._minDtick > 0 && ax.dtick < ax._minDtick * 2) {
@@ -645,6 +648,10 @@ axes.calcTicks = function calcTicks(ax) {
// now figure out rounding of tick values
autoTickRound(ax);
+ // now that we've figured out the auto values for formatting
+ // in case we're missing some ticktext, we can break out for array ticks
+ if(ax.tickmode === 'array') return arrayTicks(ax);
+
// find the first tick
ax._tmin = axes.tickFirst(ax);
@@ -672,11 +679,11 @@ axes.calcTicks = function calcTicks(ax) {
// show the exponent only on the last one
ax._tmax = vals[vals.length - 1];
- // for showing date suffixes: ax._prevSuffix holds what we showed most
- // recently. Start with it cleared and mark that we're in calcTicks (ie
- // calculating a whole string of these so we should care what the previous
- // suffix was!)
- ax._prevSuffix = '';
+ // for showing the rest of a date when the main tick label is only the
+ // latter part: ax._prevDateHead holds what we showed most recently.
+ // Start with it cleared and mark that we're in calcTicks (ie calculating a
+ // whole string of these so we should care what the previous date head was!)
+ ax._prevDateHead = '';
ax._inCalcTicks = true;
var ticksOut = new Array(vals.length);
@@ -704,8 +711,17 @@ function arrayTicks(ax) {
// except with more precision to the numbers
if(!Array.isArray(text)) text = [];
+ // make sure showing ticks doesn't accidentally add new categories
+ var tickVal2l = ax.type === 'category' ? ax.d2l_noadd : ax.d2l;
+
+ // array ticks on log axes always show the full number
+ // (if no explicit ticktext overrides it)
+ if(ax.type === 'log' && String(ax.dtick).charAt(0) !== 'L') {
+ ax.dtick = 'L' + Math.pow(10, Math.floor(Math.min(ax.range[0], ax.range[1])) - 1);
+ }
+
for(i = 0; i < vals.length; i++) {
- vali = ax.d2l(vals[i]);
+ vali = tickVal2l(vals[i]);
if(vali > tickMin && vali < tickMax) {
if(text[i] === undefined) ticksOut[j] = axes.tickText(ax, vali);
else ticksOut[j] = tickTextObj(ax, vali, String(text[i]));
@@ -1030,13 +1046,14 @@ axes.tickText = function(ax, x, hover) {
hideexp,
arrayMode = ax.tickmode === 'array',
extraPrecision = hover || arrayMode,
- i;
+ i,
+ tickVal2l = ax.type === 'category' ? ax.d2l_noadd : ax.d2l;
if(arrayMode && Array.isArray(ax.ticktext)) {
var rng = ax.range.map(ax.r2l),
minDiff = Math.abs(rng[1] - rng[0]) / 10000;
for(i = 0; i < ax.ticktext.length; i++) {
- if(Math.abs(x - ax.d2l(ax.tickvals[i])) < minDiff) break;
+ if(Math.abs(x - tickVal2l(ax.tickvals[i])) < minDiff) break;
}
if(i < ax.ticktext.length) {
out.text = String(ax.ticktext[i]);
@@ -1089,12 +1106,11 @@ function tickTextObj(ax, x, text) {
function formatDate(ax, out, hover, extraPrecision) {
var x = out.x,
tr = ax._tickround,
- trOriginal = tr,
d = new Date(x),
- // suffix completes the full date info, to be included
+ // headPart completes the full date info, to be included
// with only the first tick or if any info before what's
// shown has changed
- suffix,
+ headPart,
tt;
if(hover && ax.hoverformat) {
tt = modDateFormat(ax.hoverformat, x);
@@ -1113,12 +1129,12 @@ function formatDate(ax, out, hover, extraPrecision) {
else if(tr === 'm') tt = monthFormat(d);
else {
if(tr === 'd') {
- if(!hover) suffix = '
' + yearFormat(d);
+ headPart = yearFormat(d);
tt = dayFormat(d);
}
else {
- if(!hover) suffix = '
' + yearMonthDayFormat(d);
+ headPart = yearMonthDayFormat(d);
tt = minuteFormat(d);
if(tr !== 'M') {
@@ -1128,17 +1144,34 @@ function formatDate(ax, out, hover, extraPrecision) {
.substr(1);
}
}
- else if(trOriginal === 'd') {
- // for hover on axes with day ticks, minuteFormat (which
- // only includes %H:%M) isn't enough, you want the date too
- tt = dayFormat(d) + ' ' + tt;
- }
}
}
}
- if(suffix && (!ax._inCalcTicks || (suffix !== ax._prevSuffix))) {
- tt += suffix;
- ax._prevSuffix = suffix;
+ if(hover || ax.tickmode === 'array') {
+ // we get extra precision in array mode or hover,
+ // but it may be useless, strip it off
+ if(tt === '00:00:00' || tt === '00:00') {
+ tt = headPart;
+ headPart = '';
+ }
+ else if(tt.length === 8) {
+ // strip off seconds if they're zero (zero fractional seconds
+ // are already omitted)
+ tt = tt.replace(/:00$/, '');
+ }
+ }
+
+ if(headPart) {
+ if(hover) {
+ // hover puts it all on one line, so headPart works best up front
+ // except for year headPart: turn this into "Jan 1, 2000" etc.
+ if(tr === 'd') tt += ', ' + headPart;
+ else tt = headPart + (tt ? ', ' + tt : '');
+ }
+ else if(!ax._inCalcTicks || (headPart !== ax._prevDateHead)) {
+ tt += '
' + headPart;
+ ax._prevDateHead = headPart;
+ }
}
out.text = tt;
}
diff --git a/src/plots/cartesian/set_convert.js b/src/plots/cartesian/set_convert.js
index e4fae71290f..69a24545b40 100644
--- a/src/plots/cartesian/set_convert.js
+++ b/src/plots/cartesian/set_convert.js
@@ -313,6 +313,14 @@ module.exports = function setConvert(ax) {
return c === -1 ? BADNUM : c;
};
+ ax.d2l_noadd = function(v) {
+ // d2c variant that that won't add categories but will also
+ // allow numbers to be mapped to the linearized axis positions
+ var index = ax._categories.indexOf(v);
+ if(index !== -1) return index;
+ if(typeof v === 'number') return v;
+ };
+
ax.d2l = ax.d2c;
ax.r2l = num;
ax.l2r = num;
diff --git a/test/jasmine/tests/axes_test.js b/test/jasmine/tests/axes_test.js
index 90433bd53af..897e9512626 100644
--- a/test/jasmine/tests/axes_test.js
+++ b/test/jasmine/tests/axes_test.js
@@ -1393,7 +1393,7 @@ describe('Test axes', function() {
});
});
- describe('calcTicks', function() {
+ describe('calcTicks and tickText', function() {
function mockCalc(ax) {
Axes.setConvert(ax);
ax.tickfont = {};
@@ -1401,14 +1401,28 @@ describe('Test axes', function() {
return Axes.calcTicks(ax).map(function(v) { return v.text; });
}
+ function mockHoverText(ax, x) {
+ var xCalc = (ax.d2l_noadd || ax.d2c)(x);
+ var tickTextObj = Axes.tickText(ax, xCalc, true);
+ return tickTextObj.text;
+ }
+
+ function checkHovers(ax, specArray) {
+ specArray.forEach(function(v) {
+ expect(mockHoverText(ax, v[0]))
+ .toBe(v[1], ax.dtick + ' - ' + v[0]);
+ });
+ }
+
it('provides a new date suffix whenever the suffix changes', function() {
- var textOut = mockCalc({
+ var ax = {
type: 'date',
tickmode: 'linear',
tick0: '2000-01-01',
dtick: 14 * 24 * 3600 * 1000, // 14 days
range: ['1999-12-01', '2000-02-15']
- });
+ };
+ var textOut = mockCalc(ax);
var expectedText = [
'Dec 4
1999',
@@ -1419,14 +1433,17 @@ describe('Test axes', function() {
'Feb 12'
];
expect(textOut).toEqual(expectedText);
+ expect(mockHoverText(ax, ax.d2c('1999-12-18 15:34:33.3')))
+ .toBe('Dec 18, 1999, 15:34');
- textOut = mockCalc({
+ ax = {
type: 'date',
tickmode: 'linear',
tick0: '2000-01-01',
dtick: 12 * 3600 * 1000, // 12 hours
range: ['2000-01-03 11:00', '2000-01-06']
- });
+ };
+ textOut = mockCalc(ax);
expectedText = [
'12:00
Jan 3, 2000',
@@ -1437,14 +1454,17 @@ describe('Test axes', function() {
'00:00
Jan 6, 2000'
];
expect(textOut).toEqual(expectedText);
+ expect(mockHoverText(ax, ax.d2c('2000-01-04 15:34:33.3')))
+ .toBe('Jan 4, 2000, 15:34:33');
- textOut = mockCalc({
+ ax = {
type: 'date',
tickmode: 'linear',
tick0: '2000-01-01',
dtick: 1000, // 1 sec
range: ['2000-02-03 23:59:57', '2000-02-04 00:00:02']
- });
+ };
+ textOut = mockCalc(ax);
expectedText = [
'23:59:57
Feb 3, 2000',
@@ -1455,16 +1475,21 @@ describe('Test axes', function() {
'00:00:02'
];
expect(textOut).toEqual(expectedText);
+ expect(mockHoverText(ax, ax.d2c('2000-02-04 00:00:00.123456')))
+ .toBe('Feb 4, 2000, 00:00:00.1235');
+ expect(mockHoverText(ax, ax.d2c('2000-02-04 00:00:00')))
+ .toBe('Feb 4, 2000');
});
it('should give dates extra precision if tick0 is weird', function() {
- var textOut = mockCalc({
+ var ax = {
type: 'date',
tickmode: 'linear',
tick0: '2000-01-01 00:05',
dtick: 14 * 24 * 3600 * 1000, // 14 days
range: ['1999-12-01', '2000-02-15']
- });
+ };
+ var textOut = mockCalc(ax);
var expectedText = [
'00:05
Dec 4, 1999',
@@ -1475,16 +1500,21 @@ describe('Test axes', function() {
'00:05
Feb 12, 2000'
];
expect(textOut).toEqual(expectedText);
+ expect(mockHoverText(ax, ax.d2c('2000-02-04 00:00:00.123456')))
+ .toBe('Feb 4, 2000');
+ expect(mockHoverText(ax, ax.d2c('2000-02-04 00:00:05.123456')))
+ .toBe('Feb 4, 2000, 00:00:05');
});
it('should never give dates more than 100 microsecond precision', function() {
- var textOut = mockCalc({
+ var ax = {
type: 'date',
tickmode: 'linear',
tick0: '2000-01-01',
dtick: 1.1333,
range: ['2000-01-01', '2000-01-01 00:00:00.01']
- });
+ };
+ var textOut = mockCalc(ax);
var expectedText = [
'00:00:00
Jan 1, 2000',
@@ -1499,5 +1529,185 @@ describe('Test axes', function() {
];
expect(textOut).toEqual(expectedText);
});
+
+ it('should handle edge cases with dates and tickvals', function() {
+ var ax = {
+ type: 'date',
+ tickmode: 'array',
+ tickvals: [
+ '2012-01-01',
+ new Date(2012, 2, 1).getTime(),
+ '2012-08-01 00:00:00',
+ '2012-10-01 12:00:00',
+ new Date(2013, 0, 1, 0, 0, 1).getTime(),
+ '2010-01-01', '2014-01-01' // off the axis
+ ],
+ // only the first two have text
+ ticktext: ['New year', 'February'],
+
+ // required to get calcTicks to run
+ range: ['2011-12-10', '2013-01-23'],
+ nticks: 10
+ };
+ var textOut = mockCalc(ax);
+
+ var expectedText = [
+ 'New year',
+ 'February',
+ 'Aug 1, 2012',
+ '12:00
Oct 1, 2012',
+ '00:00:01
Jan 1, 2013'
+ ];
+ expect(textOut).toEqual(expectedText);
+ expect(mockHoverText(ax, ax.d2c('2012-01-01')))
+ .toBe('New year');
+ expect(mockHoverText(ax, ax.d2c('2012-01-01 12:34:56.1234')))
+ .toBe('Jan 1, 2012, 12:34:56');
+ });
+
+ it('should handle tickvals edge cases with linear and log axes', function() {
+ ['linear', 'log'].forEach(function(axType) {
+ var ax = {
+ type: axType,
+ tickmode: 'array',
+ tickvals: [1, 1.5, 2.6999999, 30, 39.999, 100, 0.1],
+ ticktext: ['One', '...and a half'],
+ // I'll be so happy when I can finally get rid of this switch!
+ range: axType === 'log' ? [-0.2, 1.8] : [0.5, 50],
+ nticks: 10
+ };
+ var textOut = mockCalc(ax);
+
+ var expectedText = [
+ 'One',
+ '...and a half', // the first two get explicit labels
+ '2.7', // 2.6999999 gets rounded to 2.7
+ '30',
+ '39.999' // 39.999 does not get rounded
+ // 10 and 0.1 are off scale
+ ];
+ expect(textOut).toEqual(expectedText, axType);
+ expect(mockHoverText(ax, ax.c2l(1))).toBe('One');
+ expect(mockHoverText(ax, ax.c2l(19.999))).toBe('19.999');
+ });
+ });
+
+ it('should handle tickvals edge cases with category axes', function() {
+ var ax = {
+ type: 'category',
+ _categories: ['a', 'b', 'c', 'd'],
+ tickmode: 'array',
+ tickvals: ['a', 1, 1.5, 'c', 2.7, 3, 'e', 4, 5, -2],
+ ticktext: ['A!', 'B?', 'B->C'],
+ range: [-0.5, 4.5],
+ nticks: 10
+ };
+ var textOut = mockCalc(ax);
+
+ var expectedText = [
+ 'A!', // category position, explicit text
+ 'B?', // integer position, explicit text
+ 'B->C', // non-integer position, explicit text
+ 'c', // category position, no text: use category
+ 'd', // non-integer position, no text: use closest category
+ 'd', // integer position, no text: use category
+ '' // 4: number with no close category: leave blank
+ // but still include it so we get a tick mark & grid
+ // 'e', 5, -2: bad category and numbers out of range: omitted
+ ];
+ expect(textOut).toEqual(expectedText);
+ expect(mockHoverText(ax, 0)).toBe('A!');
+ expect(mockHoverText(ax, 2)).toBe('c');
+ expect(mockHoverText(ax, 4)).toBe('');
+
+ // make sure we didn't add any more categories accidentally
+ expect(ax._categories).toEqual(['a', 'b', 'c', 'd']);
+ });
+
+ it('should always start at year for date axis hover', function() {
+ var ax = {
+ type: 'date',
+ tickmode: 'linear',
+ tick0: '2000-01-01',
+ dtick: 'M1200',
+ range: ['1000-01-01', '3000-01-01'],
+ nticks: 10
+ };
+ mockCalc(ax);
+
+ checkHovers(ax, [
+ ['2000-01-01', 'Jan 2000'],
+ ['2000-01-01 11:00', 'Jan 2000'],
+ ['2000-01-01 11:14', 'Jan 2000'],
+ ['2000-01-01 11:00:15', 'Jan 2000'],
+ ['2000-01-01 11:00:00.1', 'Jan 2000'],
+ ['2000-01-01 11:00:00.0001', 'Jan 2000']
+ ]);
+
+ ax.dtick = 'M1';
+ ax.range = ['1999-06-01', '2000-06-01'];
+ mockCalc(ax);
+
+ checkHovers(ax, [
+ ['2000-01-01', 'Jan 1, 2000'],
+ ['2000-01-01 11:00', 'Jan 1, 2000'],
+ ['2000-01-01 11:14', 'Jan 1, 2000'],
+ ['2000-01-01 11:00:15', 'Jan 1, 2000'],
+ ['2000-01-01 11:00:00.1', 'Jan 1, 2000'],
+ ['2000-01-01 11:00:00.0001', 'Jan 1, 2000']
+ ]);
+
+ ax.dtick = 24 * 3600000; // one day
+ ax.range = ['1999-12-15', '2000-01-15'];
+ mockCalc(ax);
+
+ checkHovers(ax, [
+ ['2000-01-01', 'Jan 1, 2000'],
+ ['2000-01-01 11:00', 'Jan 1, 2000, 11:00'],
+ ['2000-01-01 11:14', 'Jan 1, 2000, 11:14'],
+ ['2000-01-01 11:00:15', 'Jan 1, 2000, 11:00'],
+ ['2000-01-01 11:00:00.1', 'Jan 1, 2000, 11:00'],
+ ['2000-01-01 11:00:00.0001', 'Jan 1, 2000, 11:00']
+ ]);
+
+ ax.dtick = 3600000; // one hour
+ ax.range = ['1999-12-31', '2000-01-02'];
+ mockCalc(ax);
+
+ checkHovers(ax, [
+ ['2000-01-01', 'Jan 1, 2000'],
+ ['2000-01-01 11:00', 'Jan 1, 2000, 11:00'],
+ ['2000-01-01 11:14', 'Jan 1, 2000, 11:14'],
+ ['2000-01-01 11:00:15', 'Jan 1, 2000, 11:00:15'],
+ ['2000-01-01 11:00:00.1', 'Jan 1, 2000, 11:00'],
+ ['2000-01-01 11:00:00.0001', 'Jan 1, 2000, 11:00']
+ ]);
+
+ ax.dtick = 60000; // one minute
+ ax.range = ['1999-12-31 23:00', '2000-01-01 01:00'];
+ mockCalc(ax);
+
+ checkHovers(ax, [
+ ['2000-01-01', 'Jan 1, 2000'],
+ ['2000-01-01 11:00', 'Jan 1, 2000, 11:00'],
+ ['2000-01-01 11:14', 'Jan 1, 2000, 11:14'],
+ ['2000-01-01 11:00:15', 'Jan 1, 2000, 11:00:15'],
+ ['2000-01-01 11:00:00.1', 'Jan 1, 2000, 11:00'],
+ ['2000-01-01 11:00:00.0001', 'Jan 1, 2000, 11:00']
+ ]);
+
+ ax.dtick = 1000; // one second
+ ax.range = ['1999-12-31 23:59', '2000-01-01 00:01'];
+ mockCalc(ax);
+
+ checkHovers(ax, [
+ ['2000-01-01', 'Jan 1, 2000'],
+ ['2000-01-01 11:00', 'Jan 1, 2000, 11:00'],
+ ['2000-01-01 11:14', 'Jan 1, 2000, 11:14'],
+ ['2000-01-01 11:00:15', 'Jan 1, 2000, 11:00:15'],
+ ['2000-01-01 11:00:00.1', 'Jan 1, 2000, 11:00:00.1'],
+ ['2000-01-01 11:00:00.0001', 'Jan 1, 2000, 11:00:00.0001']
+ ]);
+ });
});
});