Skip to content

Commit d173d7a

Browse files
authored
Merge pull request #5208 from plotly/more-period-labels
Fix positioning monthly tickformat when initial auto dtick is weekly
2 parents 3580851 + bcf2a71 commit d173d7a

File tree

2 files changed

+200
-125
lines changed

2 files changed

+200
-125
lines changed

src/plots/cartesian/axes.js

+153-125
Original file line numberDiff line numberDiff line change
@@ -575,6 +575,10 @@ axes.prepTicks = function(ax, opts) {
575575
}
576576
}
577577

578+
if(ax.ticklabelmode === 'period') {
579+
adjustPeriodDelta(ax);
580+
}
581+
578582
// check for missing tick0
579583
if(!ax.tick0) {
580584
ax.tick0 = (ax.type === 'date') ? '2000-01-01' : 0;
@@ -592,47 +596,18 @@ function nMonths(dtick) {
592596
return +(dtick.substring(1));
593597
}
594598

595-
// calculate the ticks: text, values, positioning
596-
// if ticks are set to automatic, determine the right values (tick0,dtick)
597-
// in any case, set tickround to # of digits to round tick labels to,
598-
// or codes to this effect for log and date scales
599-
axes.calcTicks = function calcTicks(ax, opts) {
600-
axes.prepTicks(ax, opts);
601-
var rng = Lib.simpleMap(ax.range, ax.r2l, undefined, undefined, opts);
602-
603-
// now that we've figured out the auto values for formatting
604-
// in case we're missing some ticktext, we can break out for array ticks
605-
if(ax.tickmode === 'array') return arrayTicks(ax);
606-
607-
// add a tiny bit so we get ticks which may have rounded out
608-
var exRng = expandRange(rng);
609-
var startTick = exRng[0];
610-
var endTick = exRng[1];
611-
// check for reversed axis
612-
var axrev = (rng[1] < rng[0]);
613-
var minRange = Math.min(rng[0], rng[1]);
614-
var maxRange = Math.max(rng[0], rng[1]);
615-
616-
// find the first tick
617-
ax._tmin = axes.tickFirst(ax, opts);
618-
619-
// No visible ticks? Quit.
620-
// I've only seen this on category axes with all categories off the edge.
621-
if((ax._tmin < startTick) !== axrev) return [];
599+
function adjustPeriodDelta(ax) { // adjusts ax.dtick and sets ax._definedDelta
600+
var definedDelta;
622601

623-
// return the full set of tick vals
624-
if(ax.type === 'category' || ax.type === 'multicategory') {
625-
endTick = (axrev) ? Math.max(-0.5, endTick) :
626-
Math.min(ax._categories.length - 0.5, endTick);
602+
function mDate() {
603+
return !(
604+
isNumeric(ax.dtick) ||
605+
ax.dtick.charAt(0) !== 'M'
606+
);
627607
}
628-
629-
var isDLog = (ax.type === 'log') && !(isNumeric(ax.dtick) || ax.dtick.charAt(0) === 'L');
630-
var isMDate = (ax.type === 'date') && !(isNumeric(ax.dtick) || ax.dtick.charAt(0) === 'M');
631-
608+
var isMDate = mDate();
632609
var tickformat = axes.getTickFormat(ax);
633-
var isPeriod = ax.ticklabelmode === 'period';
634-
var definedDelta;
635-
if(isPeriod && tickformat) {
610+
if(tickformat) {
636611
var noDtick = ax._dtickInit !== ax.dtick;
637612
if(
638613
!(/%[fLQsSMX]/.test(tickformat))
@@ -708,9 +683,136 @@ axes.calcTicks = function calcTicks(ax, opts) {
708683
}
709684
}
710685

711-
var maxTicks = Math.max(1000, ax._length || 0);
712-
var tickVals = [];
713-
var xPrevious = null;
686+
isMDate = mDate();
687+
if(isMDate && ax.tick0 === ax._dowTick0) {
688+
// discard Sunday/Monday tweaks
689+
ax.tick0 = ax._rawTick0;
690+
}
691+
692+
ax._definedDelta = definedDelta;
693+
}
694+
695+
function positionPeriodTicks(tickVals, ax, definedDelta) {
696+
for(var i = 0; i < tickVals.length; i++) {
697+
var v = tickVals[i].value;
698+
699+
var a = i;
700+
var b = i + 1;
701+
if(i < tickVals.length - 1) {
702+
a = i;
703+
b = i + 1;
704+
} else if(i > 0) {
705+
a = i - 1;
706+
b = i;
707+
} else {
708+
a = i;
709+
b = i;
710+
}
711+
712+
var A = tickVals[a].value;
713+
var B = tickVals[b].value;
714+
var actualDelta = Math.abs(B - A);
715+
var delta = definedDelta || actualDelta;
716+
var periodLength = 0;
717+
718+
if(delta >= ONEMINYEAR) {
719+
if(actualDelta >= ONEMINYEAR && actualDelta <= ONEMAXYEAR) {
720+
periodLength = actualDelta;
721+
} else {
722+
periodLength = ONEAVGYEAR;
723+
}
724+
} else if(definedDelta === ONEAVGQUARTER && delta >= ONEMINQUARTER) {
725+
if(actualDelta >= ONEMINQUARTER && actualDelta <= ONEMAXQUARTER) {
726+
periodLength = actualDelta;
727+
} else {
728+
periodLength = ONEAVGQUARTER;
729+
}
730+
} else if(delta >= ONEMINMONTH) {
731+
if(actualDelta >= ONEMINMONTH && actualDelta <= ONEMAXMONTH) {
732+
periodLength = actualDelta;
733+
} else {
734+
periodLength = ONEAVGMONTH;
735+
}
736+
} else if(definedDelta === ONEWEEK && delta >= ONEWEEK) {
737+
periodLength = ONEWEEK;
738+
} else if(delta >= ONEDAY) {
739+
periodLength = ONEDAY;
740+
} else if(definedDelta === HALFDAY && delta >= HALFDAY) {
741+
periodLength = HALFDAY;
742+
} else if(definedDelta === ONEHOUR && delta >= ONEHOUR) {
743+
periodLength = ONEHOUR;
744+
}
745+
746+
var inBetween;
747+
if(periodLength >= actualDelta) {
748+
// ensure new label positions remain between ticks
749+
periodLength = actualDelta;
750+
inBetween = true;
751+
}
752+
753+
var endPeriod = v + periodLength;
754+
if(ax.rangebreaks && periodLength > 0) {
755+
var nAll = 84; // highly divisible 7 * 12
756+
var n = 0;
757+
for(var c = 0; c < nAll; c++) {
758+
var r = (c + 0.5) / nAll;
759+
if(ax.maskBreaks(v * (1 - r) + r * endPeriod) !== BADNUM) n++;
760+
}
761+
periodLength *= n / nAll;
762+
763+
if(!periodLength) {
764+
tickVals[i].drop = true;
765+
}
766+
767+
if(inBetween && actualDelta > ONEWEEK) periodLength = actualDelta; // center monthly & longer periods
768+
}
769+
770+
if(
771+
periodLength > 0 || // not instant
772+
i === 0 // taking care first tick added
773+
) {
774+
tickVals[i].periodX = v + periodLength / 2;
775+
}
776+
}
777+
}
778+
779+
// calculate the ticks: text, values, positioning
780+
// if ticks are set to automatic, determine the right values (tick0,dtick)
781+
// in any case, set tickround to # of digits to round tick labels to,
782+
// or codes to this effect for log and date scales
783+
axes.calcTicks = function calcTicks(ax, opts) {
784+
axes.prepTicks(ax, opts);
785+
var rng = Lib.simpleMap(ax.range, ax.r2l, undefined, undefined, opts);
786+
787+
// now that we've figured out the auto values for formatting
788+
// in case we're missing some ticktext, we can break out for array ticks
789+
if(ax.tickmode === 'array') return arrayTicks(ax);
790+
791+
// add a tiny bit so we get ticks which may have rounded out
792+
var exRng = expandRange(rng);
793+
var startTick = exRng[0];
794+
var endTick = exRng[1];
795+
// check for reversed axis
796+
var axrev = (rng[1] < rng[0]);
797+
var minRange = Math.min(rng[0], rng[1]);
798+
var maxRange = Math.max(rng[0], rng[1]);
799+
800+
var isDLog = (ax.type === 'log') && !(isNumeric(ax.dtick) || ax.dtick.charAt(0) === 'L');
801+
var isPeriod = ax.ticklabelmode === 'period';
802+
803+
// find the first tick
804+
ax._tmin = axes.tickFirst(ax, opts);
805+
806+
// No visible ticks? Quit.
807+
// I've only seen this on category axes with all categories off the edge.
808+
if((ax._tmin < startTick) !== axrev) return [];
809+
810+
// return the full set of tick vals
811+
if(ax.type === 'category' || ax.type === 'multicategory') {
812+
endTick = (axrev) ? Math.max(-0.5, endTick) :
813+
Math.min(ax._categories.length - 0.5, endTick);
814+
}
815+
714816
var x = ax._tmin;
715817

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

831+
var maxTicks = Math.max(1000, ax._length || 0);
832+
var tickVals = [];
833+
var xPrevious = null;
729834
for(;
730835
(axrev) ? (x >= endTick) : (x <= endTick);
731836
x = axes.tickIncrement(x, ax.dtick, axrev, ax.calendar)
@@ -753,91 +858,9 @@ axes.calcTicks = function calcTicks(ax, opts) {
753858
});
754859
}
755860

756-
var i;
757-
if(isPeriod) {
758-
for(i = 0; i < tickVals.length; i++) {
759-
var v = tickVals[i].value;
760-
761-
var a = i;
762-
var b = i + 1;
763-
if(i < tickVals.length - 1) {
764-
a = i;
765-
b = i + 1;
766-
} else if(i > 0) {
767-
a = i - 1;
768-
b = i;
769-
} else {
770-
a = i;
771-
b = i;
772-
}
773-
774-
var A = tickVals[a].value;
775-
var B = tickVals[b].value;
776-
var actualDelta = Math.abs(B - A);
777-
var delta = definedDelta || actualDelta;
778-
var periodLength = 0;
779-
780-
if(delta >= ONEMINYEAR) {
781-
if(actualDelta >= ONEMINYEAR && actualDelta <= ONEMAXYEAR) {
782-
periodLength = actualDelta;
783-
} else {
784-
periodLength = ONEAVGYEAR;
785-
}
786-
} else if(definedDelta === ONEAVGQUARTER && delta >= ONEMINQUARTER) {
787-
if(actualDelta >= ONEMINQUARTER && actualDelta <= ONEMAXQUARTER) {
788-
periodLength = actualDelta;
789-
} else {
790-
periodLength = ONEAVGQUARTER;
791-
}
792-
} else if(delta >= ONEMINMONTH) {
793-
if(actualDelta >= ONEMINMONTH && actualDelta <= ONEMAXMONTH) {
794-
periodLength = actualDelta;
795-
} else {
796-
periodLength = ONEAVGMONTH;
797-
}
798-
} else if(definedDelta === ONEWEEK && delta >= ONEWEEK) {
799-
periodLength = ONEWEEK;
800-
} else if(delta >= ONEDAY) {
801-
periodLength = ONEDAY;
802-
} else if(definedDelta === HALFDAY && delta >= HALFDAY) {
803-
periodLength = HALFDAY;
804-
} else if(definedDelta === ONEHOUR && delta >= ONEHOUR) {
805-
periodLength = ONEHOUR;
806-
}
807-
808-
var inBetween;
809-
if(periodLength >= actualDelta) {
810-
// ensure new label positions remain between ticks
811-
periodLength = actualDelta;
812-
inBetween = true;
813-
}
814-
815-
var endPeriod = v + periodLength;
816-
if(ax.rangebreaks && periodLength > 0) {
817-
var nAll = 84; // highly divisible 7 * 12
818-
var n = 0;
819-
for(var c = 0; c < nAll; c++) {
820-
var r = (c + 0.5) / nAll;
821-
if(ax.maskBreaks(v * (1 - r) + r * endPeriod) !== BADNUM) n++;
822-
}
823-
periodLength *= n / nAll;
824-
825-
if(!periodLength) {
826-
tickVals[i].drop = true;
827-
}
828-
829-
if(inBetween && actualDelta > ONEWEEK) periodLength = actualDelta; // center monthly & longer periods
830-
}
831-
832-
if(
833-
periodLength > 0 || // not instant
834-
i === 0 // taking care first tick added
835-
) {
836-
tickVals[i].periodX = v + periodLength / 2;
837-
}
838-
}
839-
}
861+
if(isPeriod) positionPeriodTicks(tickVals, ax, ax._definedDelta);
840862

863+
var i;
841864
if(ax.rangebreaks) {
842865
var flip = ax._id.charAt(0) === 'y';
843866

@@ -1022,11 +1045,16 @@ axes.autoTicks = function(ax, roughDTick) {
10221045
// this will also move the base tick off 2000-01-01 if dtick is
10231046
// 2 or 3 days... but that's a weird enough case that we'll ignore it.
10241047
var tickformat = axes.getTickFormat(ax);
1048+
var isPeriod = ax.ticklabelmode === 'period';
1049+
if(isPeriod) ax._rawTick0 = ax.tick0;
1050+
10251051
if(/%[uVW]/.test(tickformat)) {
10261052
ax.tick0 = Lib.dateTick0(ax.calendar, 2); // Monday
10271053
} else {
10281054
ax.tick0 = Lib.dateTick0(ax.calendar, 1); // Sunday
10291055
}
1056+
1057+
if(isPeriod) ax._dowTick0 = ax.tick0;
10301058
} else if(roughX2 > ONEHOUR) {
10311059
ax.dtick = roundDTick(roughDTick, ONEHOUR, roundBase24);
10321060
} else if(roughX2 > ONEMIN) {

test/jasmine/tests/axes_test.js

+47
Original file line numberDiff line numberDiff line change
@@ -5700,6 +5700,53 @@ describe('Test axes', function() {
57005700
});
57015701
});
57025702

5703+
[
5704+
{
5705+
range: ['2019-12-10', '2020-01-10'],
5706+
positions: ['2019-12-16 12:00', '2020-01-10'],
5707+
labels: ['2019-Dec', ' ']
5708+
},
5709+
{
5710+
range: ['2019-12-20', '2020-01-20'],
5711+
positions: ['2019-12-20', '2020-01-16 12:00'],
5712+
labels: [' ', '2020-Jan']
5713+
},
5714+
{
5715+
range: ['2020-01-20', '2019-12-20'],
5716+
positions: ['2020-01-20', '2020-01-16 12:00'],
5717+
labels: [' ', '2020-Jan']
5718+
}
5719+
].forEach(function(t) {
5720+
it('should position labels with monthly tickformat when auto dtick is weekly | range:' + t.range, function(done) {
5721+
Plotly.newPlot(gd, {
5722+
data: [{
5723+
x: [
5724+
'2020-01-01',
5725+
'2020-01-02'
5726+
],
5727+
mode: 'lines+text',
5728+
text: [
5729+
'Jan 01',
5730+
'Jan 02'
5731+
]
5732+
}],
5733+
layout: {
5734+
width: 600,
5735+
xaxis: {
5736+
range: t.range,
5737+
ticklabelmode: 'period',
5738+
tickformat: '%Y-%b'
5739+
}
5740+
}
5741+
})
5742+
.then(function() {
5743+
_assert('', t.positions, t.labels);
5744+
})
5745+
.catch(failTest)
5746+
.then(done);
5747+
});
5748+
});
5749+
57035750
[
57045751
{
57055752
range: ['2020-12-15', '2084-12-15'],

0 commit comments

Comments
 (0)