Skip to content

Fix positioning monthly tickformat when initial auto dtick is weekly #5208

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 9 commits into from
Oct 15, 2020
278 changes: 153 additions & 125 deletions src/plots/cartesian/axes.js
Original file line number Diff line number Diff line change
Expand Up @@ -575,6 +575,10 @@ axes.prepTicks = function(ax, opts) {
}
}

if(ax.ticklabelmode === 'period') {
adjustPeriodDelta(ax);
}

// check for missing tick0
if(!ax.tick0) {
ax.tick0 = (ax.type === 'date') ? '2000-01-01' : 0;
Expand All @@ -592,47 +596,18 @@ function nMonths(dtick) {
return +(dtick.substring(1));
}

// calculate the ticks: text, values, positioning
// if ticks are set to automatic, determine the right values (tick0,dtick)
// 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, opts) {
axes.prepTicks(ax, opts);
var rng = Lib.simpleMap(ax.range, ax.r2l, undefined, undefined, opts);

// 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);

// add a tiny bit so we get ticks which may have rounded out
var exRng = expandRange(rng);
var startTick = exRng[0];
var endTick = exRng[1];
// check for reversed axis
var axrev = (rng[1] < rng[0]);
var minRange = Math.min(rng[0], rng[1]);
var maxRange = Math.max(rng[0], rng[1]);

// find the first tick
ax._tmin = axes.tickFirst(ax, opts);

// No visible ticks? Quit.
// I've only seen this on category axes with all categories off the edge.
if((ax._tmin < startTick) !== axrev) return [];
function adjustPeriodDelta(ax) { // adjusts ax.dtick and sets ax._definedDelta
var definedDelta;

// return the full set of tick vals
if(ax.type === 'category' || ax.type === 'multicategory') {
endTick = (axrev) ? Math.max(-0.5, endTick) :
Math.min(ax._categories.length - 0.5, endTick);
function mDate() {
return !(
isNumeric(ax.dtick) ||
ax.dtick.charAt(0) !== 'M'
);
}

var isDLog = (ax.type === 'log') && !(isNumeric(ax.dtick) || ax.dtick.charAt(0) === 'L');
var isMDate = (ax.type === 'date') && !(isNumeric(ax.dtick) || ax.dtick.charAt(0) === 'M');

var isMDate = mDate();
var tickformat = axes.getTickFormat(ax);
var isPeriod = ax.ticklabelmode === 'period';
var definedDelta;
if(isPeriod && tickformat) {
if(tickformat) {
var noDtick = ax._dtickInit !== ax.dtick;
if(
!(/%[fLQsSMX]/.test(tickformat))
Expand Down Expand Up @@ -708,9 +683,136 @@ axes.calcTicks = function calcTicks(ax, opts) {
}
}

var maxTicks = Math.max(1000, ax._length || 0);
var tickVals = [];
var xPrevious = null;
isMDate = mDate();
if(isMDate && ax.tick0 === ax._dowTick0) {
// discard Sunday/Monday tweaks
ax.tick0 = ax._rawTick0;
}

ax._definedDelta = definedDelta;
}

function positionPeriodTicks(tickVals, ax, definedDelta) {
for(var i = 0; i < tickVals.length; i++) {
var v = tickVals[i].value;

var a = i;
var b = i + 1;
if(i < tickVals.length - 1) {
a = i;
b = i + 1;
} else if(i > 0) {
a = i - 1;
b = i;
} else {
a = i;
b = i;
}

var A = tickVals[a].value;
var B = tickVals[b].value;
var actualDelta = Math.abs(B - A);
var delta = definedDelta || actualDelta;
var periodLength = 0;

if(delta >= ONEMINYEAR) {
if(actualDelta >= ONEMINYEAR && actualDelta <= ONEMAXYEAR) {
periodLength = actualDelta;
} else {
periodLength = ONEAVGYEAR;
}
} else if(definedDelta === ONEAVGQUARTER && delta >= ONEMINQUARTER) {
if(actualDelta >= ONEMINQUARTER && actualDelta <= ONEMAXQUARTER) {
periodLength = actualDelta;
} else {
periodLength = ONEAVGQUARTER;
}
} else if(delta >= ONEMINMONTH) {
if(actualDelta >= ONEMINMONTH && actualDelta <= ONEMAXMONTH) {
periodLength = actualDelta;
} else {
periodLength = ONEAVGMONTH;
}
} else if(definedDelta === ONEWEEK && delta >= ONEWEEK) {
periodLength = ONEWEEK;
} else if(delta >= ONEDAY) {
periodLength = ONEDAY;
} else if(definedDelta === HALFDAY && delta >= HALFDAY) {
periodLength = HALFDAY;
} else if(definedDelta === ONEHOUR && delta >= ONEHOUR) {
periodLength = ONEHOUR;
}

var inBetween;
if(periodLength >= actualDelta) {
// ensure new label positions remain between ticks
periodLength = actualDelta;
inBetween = true;
}

var endPeriod = v + periodLength;
if(ax.rangebreaks && periodLength > 0) {
var nAll = 84; // highly divisible 7 * 12
var n = 0;
for(var c = 0; c < nAll; c++) {
var r = (c + 0.5) / nAll;
if(ax.maskBreaks(v * (1 - r) + r * endPeriod) !== BADNUM) n++;
}
periodLength *= n / nAll;

if(!periodLength) {
tickVals[i].drop = true;
}

if(inBetween && actualDelta > ONEWEEK) periodLength = actualDelta; // center monthly & longer periods
}

if(
periodLength > 0 || // not instant
i === 0 // taking care first tick added
) {
tickVals[i].periodX = v + periodLength / 2;
}
}
}

// calculate the ticks: text, values, positioning
// if ticks are set to automatic, determine the right values (tick0,dtick)
// 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, opts) {
axes.prepTicks(ax, opts);
var rng = Lib.simpleMap(ax.range, ax.r2l, undefined, undefined, opts);

// 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);

// add a tiny bit so we get ticks which may have rounded out
var exRng = expandRange(rng);
var startTick = exRng[0];
var endTick = exRng[1];
// check for reversed axis
var axrev = (rng[1] < rng[0]);
var minRange = Math.min(rng[0], rng[1]);
var maxRange = Math.max(rng[0], rng[1]);

var isDLog = (ax.type === 'log') && !(isNumeric(ax.dtick) || ax.dtick.charAt(0) === 'L');
var isPeriod = ax.ticklabelmode === 'period';

// find the first tick
ax._tmin = axes.tickFirst(ax, opts);

// No visible ticks? Quit.
// I've only seen this on category axes with all categories off the edge.
if((ax._tmin < startTick) !== axrev) return [];

// return the full set of tick vals
if(ax.type === 'category' || ax.type === 'multicategory') {
endTick = (axrev) ? Math.max(-0.5, endTick) :
Math.min(ax._categories.length - 0.5, endTick);
}

var x = ax._tmin;

if(ax.rangebreaks && ax._tick0Init !== ax.tick0) {
Expand All @@ -726,6 +828,9 @@ axes.calcTicks = function calcTicks(ax, opts) {
x = axes.tickIncrement(x, ax.dtick, !axrev, ax.calendar);
}

var maxTicks = Math.max(1000, ax._length || 0);
var tickVals = [];
var xPrevious = null;
for(;
(axrev) ? (x >= endTick) : (x <= endTick);
x = axes.tickIncrement(x, ax.dtick, axrev, ax.calendar)
Expand Down Expand Up @@ -753,91 +858,9 @@ axes.calcTicks = function calcTicks(ax, opts) {
});
}

var i;
if(isPeriod) {
for(i = 0; i < tickVals.length; i++) {
var v = tickVals[i].value;

var a = i;
var b = i + 1;
if(i < tickVals.length - 1) {
a = i;
b = i + 1;
} else if(i > 0) {
a = i - 1;
b = i;
} else {
a = i;
b = i;
}

var A = tickVals[a].value;
var B = tickVals[b].value;
var actualDelta = Math.abs(B - A);
var delta = definedDelta || actualDelta;
var periodLength = 0;

if(delta >= ONEMINYEAR) {
if(actualDelta >= ONEMINYEAR && actualDelta <= ONEMAXYEAR) {
periodLength = actualDelta;
} else {
periodLength = ONEAVGYEAR;
}
} else if(definedDelta === ONEAVGQUARTER && delta >= ONEMINQUARTER) {
if(actualDelta >= ONEMINQUARTER && actualDelta <= ONEMAXQUARTER) {
periodLength = actualDelta;
} else {
periodLength = ONEAVGQUARTER;
}
} else if(delta >= ONEMINMONTH) {
if(actualDelta >= ONEMINMONTH && actualDelta <= ONEMAXMONTH) {
periodLength = actualDelta;
} else {
periodLength = ONEAVGMONTH;
}
} else if(definedDelta === ONEWEEK && delta >= ONEWEEK) {
periodLength = ONEWEEK;
} else if(delta >= ONEDAY) {
periodLength = ONEDAY;
} else if(definedDelta === HALFDAY && delta >= HALFDAY) {
periodLength = HALFDAY;
} else if(definedDelta === ONEHOUR && delta >= ONEHOUR) {
periodLength = ONEHOUR;
}

var inBetween;
if(periodLength >= actualDelta) {
// ensure new label positions remain between ticks
periodLength = actualDelta;
inBetween = true;
}

var endPeriod = v + periodLength;
if(ax.rangebreaks && periodLength > 0) {
var nAll = 84; // highly divisible 7 * 12
var n = 0;
for(var c = 0; c < nAll; c++) {
var r = (c + 0.5) / nAll;
if(ax.maskBreaks(v * (1 - r) + r * endPeriod) !== BADNUM) n++;
}
periodLength *= n / nAll;

if(!periodLength) {
tickVals[i].drop = true;
}

if(inBetween && actualDelta > ONEWEEK) periodLength = actualDelta; // center monthly & longer periods
}

if(
periodLength > 0 || // not instant
i === 0 // taking care first tick added
) {
tickVals[i].periodX = v + periodLength / 2;
}
}
}
if(isPeriod) positionPeriodTicks(tickVals, ax, ax._definedDelta);

var i;
if(ax.rangebreaks) {
var flip = ax._id.charAt(0) === 'y';

Expand Down Expand Up @@ -1022,11 +1045,16 @@ axes.autoTicks = function(ax, roughDTick) {
// this will also move the base tick off 2000-01-01 if dtick is
// 2 or 3 days... but that's a weird enough case that we'll ignore it.
var tickformat = axes.getTickFormat(ax);
var isPeriod = ax.ticklabelmode === 'period';
if(isPeriod) ax._rawTick0 = ax.tick0;

if(/%[uVW]/.test(tickformat)) {
ax.tick0 = Lib.dateTick0(ax.calendar, 2); // Monday
} else {
ax.tick0 = Lib.dateTick0(ax.calendar, 1); // Sunday
}

if(isPeriod) ax._dowTick0 = ax.tick0;
} else if(roughX2 > ONEHOUR) {
ax.dtick = roundDTick(roughDTick, ONEHOUR, roundBase24);
} else if(roughX2 > ONEMIN) {
Expand Down
47 changes: 47 additions & 0 deletions test/jasmine/tests/axes_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -5681,6 +5681,53 @@ describe('Test axes', function() {
});
});

[
{
range: ['2019-12-10', '2020-01-10'],
positions: ['2019-12-16 12:00', '2020-01-10'],
labels: ['2019-Dec', ' ']
},
{
range: ['2019-12-20', '2020-01-20'],
positions: ['2019-12-20', '2020-01-16 12:00'],
labels: [' ', '2020-Jan']
},
{
range: ['2020-01-20', '2019-12-20'],
positions: ['2020-01-20', '2020-01-16 12:00'],
labels: [' ', '2020-Jan']
}
].forEach(function(t) {
it('should position labels with monthly tickformat when auto dtick is weekly | range:' + t.range, function(done) {
Plotly.newPlot(gd, {
data: [{
x: [
'2020-01-01',
'2020-01-02'
],
mode: 'lines+text',
text: [
'Jan 01',
'Jan 02'
]
}],
layout: {
width: 600,
xaxis: {
range: t.range,
ticklabelmode: 'period',
tickformat: '%Y-%b'
}
}
})
.then(function() {
_assert('', t.positions, t.labels);
})
.catch(failTest)
.then(done);
});
});

[
{
range: ['2020-12-15', '2084-12-15'],
Expand Down