Skip to content

Commit a554920

Browse files
committed
Fix period label sometimes showing incorrect format when ticklabelindex is used.
If tickformat isn't set, the old code just used the major dtick to determine the tickformat but when using ticklabelindex we need to look at the minor dtick. Only add additional ticks to the end if labels are drawn left of ticks.
1 parent 553dc51 commit a554920

1 file changed

Lines changed: 108 additions & 90 deletions

File tree

src/plots/cartesian/axes.js

Lines changed: 108 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -694,6 +694,11 @@ axes.prepMinorTicks = function(mockAx, ax, opts) {
694694
// ensure identical tick0
695695
mockAx.tick0 = ax.tick0;
696696
}
697+
autoTickRound(mockAx);
698+
if(ax.ticklabelmode === 'period') {
699+
mockAx._definedDelta = definedDeltaForTickformat(axes.getTickFormat(mockAx));
700+
}
701+
Lib.extendFlat(ax.minor, mockAx);
697702
};
698703

699704
function isMultiple(bigger, smaller) {
@@ -764,90 +769,96 @@ function nMonths(dtick) {
764769
return +(dtick.substring(1));
765770
}
766771

767-
function adjustPeriodDelta(ax) { // adjusts ax.dtick and sets ax._definedDelta
768-
var definedDelta;
772+
// Maps a tick format string to the period length its labels represent.
773+
// Returns undefined if the format has no (or a sub-second/minute) period unit,
774+
// in which case the period length is derived from the actual tick spacing.
775+
function definedDeltaForTickformat(tickformat) {
776+
if(
777+
!tickformat ||
778+
/%[fLQsSMX]/.test(tickformat)
779+
// %f: microseconds as a decimal number [000000, 999999]
780+
// %L: milliseconds as a decimal number [000, 999]
781+
// %Q: milliseconds since UNIX epoch
782+
// %s: seconds since UNIX epoch
783+
// %S: second as a decimal number [00,61]
784+
// %M: minute as a decimal number [00,59]
785+
// %X: the locale’s time, such as %-I:%M:%S %p
786+
) return undefined;
787+
788+
if(
789+
/%[HI]/.test(tickformat)
790+
// %H: hour (24-hour clock) as a decimal number [00,23]
791+
// %I: hour (12-hour clock) as a decimal number [01,12]
792+
) return ONEHOUR;
793+
if(
794+
/%p/.test(tickformat) // %p: either AM or PM
795+
) return HALFDAY;
796+
if(
797+
/%[Aadejuwx]/.test(tickformat)
798+
// %A: full weekday name
799+
// %a: abbreviated weekday name
800+
// %d: zero-padded day of the month as a decimal number [01,31]
801+
// %e: space-padded day of the month as a decimal number [ 1,31]
802+
// %j: day of the year as a decimal number [001,366]
803+
// %u: Monday-based (ISO 8601) weekday as a decimal number [1,7]
804+
// %w: Sunday-based weekday as a decimal number [0,6]
805+
// %x: the locale’s date, such as %-m/%-d/%Y
806+
) return ONEDAY;
807+
if(
808+
/%[UVW]/.test(tickformat)
809+
// %U: Sunday-based week of the year as a decimal number [00,53]
810+
// %V: ISO 8601 week of the year as a decimal number [01, 53]
811+
// %W: Monday-based week of the year as a decimal number [00,53]
812+
) return ONEWEEK;
813+
if(
814+
/%[Bbm]/.test(tickformat)
815+
// %B: full month name
816+
// %b: abbreviated month name
817+
// %m: month as a decimal number [01,12]
818+
) return ONEAVGMONTH;
819+
if(
820+
/%[q]/.test(tickformat)
821+
// %q: quarter of the year as a decimal number [1,4]
822+
) return ONEAVGQUARTER;
823+
if(
824+
/%[Yy]/.test(tickformat)
825+
// %Y: year with century as a decimal number, such as 1999
826+
// %y: year without century as a decimal number [00,99]
827+
) return ONEAVGYEAR;
828+
return undefined;
829+
}
769830

831+
function adjustPeriodDelta(ax) { // adjusts ax.dtick and sets ax._definedDelta
770832
function mDate() {
771833
return !(
772834
isNumeric(ax.dtick) ||
773835
ax.dtick.charAt(0) !== 'M'
774836
);
775837
}
776838
var isMDate = mDate();
777-
var tickformat = axes.getTickFormat(ax);
778-
if(tickformat) {
839+
var definedDelta = definedDeltaForTickformat(axes.getTickFormat(ax));
840+
if(definedDelta !== undefined) {
779841
var noDtick = ax._dtickInit !== ax.dtick;
780-
if(
781-
!(/%[fLQsSMX]/.test(tickformat))
782-
// %f: microseconds as a decimal number [000000, 999999]
783-
// %L: milliseconds as a decimal number [000, 999]
784-
// %Q: milliseconds since UNIX epoch
785-
// %s: seconds since UNIX epoch
786-
// %S: second as a decimal number [00,61]
787-
// %M: minute as a decimal number [00,59]
788-
// %X: the locale’s time, such as %-I:%M:%S %p
789-
) {
790-
if(
791-
/%[HI]/.test(tickformat)
792-
// %H: hour (24-hour clock) as a decimal number [00,23]
793-
// %I: hour (12-hour clock) as a decimal number [01,12]
794-
) {
795-
definedDelta = ONEHOUR;
796-
if(noDtick && !isMDate && ax.dtick < ONEHOUR) ax.dtick = ONEHOUR;
797-
} else if(
798-
/%p/.test(tickformat) // %p: either AM or PM
799-
) {
800-
definedDelta = HALFDAY;
801-
if(noDtick && !isMDate && ax.dtick < HALFDAY) ax.dtick = HALFDAY;
802-
} else if(
803-
/%[Aadejuwx]/.test(tickformat)
804-
// %A: full weekday name
805-
// %a: abbreviated weekday name
806-
// %d: zero-padded day of the month as a decimal number [01,31]
807-
// %e: space-padded day of the month as a decimal number [ 1,31]
808-
// %j: day of the year as a decimal number [001,366]
809-
// %u: Monday-based (ISO 8601) weekday as a decimal number [1,7]
810-
// %w: Sunday-based weekday as a decimal number [0,6]
811-
// %x: the locale’s date, such as %-m/%-d/%Y
812-
) {
813-
definedDelta = ONEDAY;
814-
if(noDtick && !isMDate && ax.dtick < ONEDAY) ax.dtick = ONEDAY;
815-
} else if(
816-
/%[UVW]/.test(tickformat)
817-
// %U: Sunday-based week of the year as a decimal number [00,53]
818-
// %V: ISO 8601 week of the year as a decimal number [01, 53]
819-
// %W: Monday-based week of the year as a decimal number [00,53]
820-
) {
821-
definedDelta = ONEWEEK;
822-
if(noDtick && !isMDate && ax.dtick < ONEWEEK) ax.dtick = ONEWEEK;
823-
} else if(
824-
/%[Bbm]/.test(tickformat)
825-
// %B: full month name
826-
// %b: abbreviated month name
827-
// %m: month as a decimal number [01,12]
828-
) {
829-
definedDelta = ONEAVGMONTH;
830-
if(noDtick && (
831-
isMDate ? nMonths(ax.dtick) < 1 : ax.dtick < ONEMINMONTH)
832-
) ax.dtick = 'M1';
833-
} else if(
834-
/%[q]/.test(tickformat)
835-
// %q: quarter of the year as a decimal number [1,4]
836-
) {
837-
definedDelta = ONEAVGQUARTER;
838-
if(noDtick && (
839-
isMDate ? nMonths(ax.dtick) < 3 : ax.dtick < ONEMINQUARTER)
840-
) ax.dtick = 'M3';
841-
} else if(
842-
/%[Yy]/.test(tickformat)
843-
// %Y: year with century as a decimal number, such as 1999
844-
// %y: year without century as a decimal number [00,99]
845-
) {
846-
definedDelta = ONEAVGYEAR;
847-
if(noDtick && (
848-
isMDate ? nMonths(ax.dtick) < 12 : ax.dtick < ONEMINYEAR)
849-
) ax.dtick = 'M12';
850-
}
842+
if(definedDelta === ONEHOUR) {
843+
if(noDtick && !isMDate && ax.dtick < ONEHOUR) ax.dtick = ONEHOUR;
844+
} else if(definedDelta === HALFDAY) {
845+
if(noDtick && !isMDate && ax.dtick < HALFDAY) ax.dtick = HALFDAY;
846+
} else if(definedDelta === ONEDAY) {
847+
if(noDtick && !isMDate && ax.dtick < ONEDAY) ax.dtick = ONEDAY;
848+
} else if(definedDelta === ONEWEEK) {
849+
if(noDtick && !isMDate && ax.dtick < ONEWEEK) ax.dtick = ONEWEEK;
850+
} else if(definedDelta === ONEAVGMONTH) {
851+
if(noDtick && (
852+
isMDate ? nMonths(ax.dtick) < 1 : ax.dtick < ONEMINMONTH)
853+
) ax.dtick = 'M1';
854+
} else if(definedDelta === ONEAVGQUARTER) {
855+
if(noDtick && (
856+
isMDate ? nMonths(ax.dtick) < 3 : ax.dtick < ONEMINQUARTER)
857+
) ax.dtick = 'M3';
858+
} else if(definedDelta === ONEAVGYEAR) {
859+
if(noDtick && (
860+
isMDate ? nMonths(ax.dtick) < 12 : ax.dtick < ONEMINYEAR)
861+
) ax.dtick = 'M12';
851862
}
852863
}
853864

@@ -971,6 +982,7 @@ axes.calcTicks = function calcTicks(ax, opts) {
971982
var isReversed = ax.range[0] > ax.range[1];
972983
var ticklabelIndex = (!ax.ticklabelindex || Lib.isArrayOrTypedArray(ax.ticklabelindex)) ?
973984
ax.ticklabelindex : [ax.ticklabelindex];
985+
ax._useTicklabelIndex = ticklabelIndex != null && ticklabelIndex !== 0;
974986
var rng = Lib.simpleMap(ax.range, ax.r2l, undefined, undefined, opts);
975987
var axrev = (rng[1] < rng[0]);
976988
var minRange = Math.min(rng[0], rng[1]);
@@ -993,7 +1005,7 @@ axes.calcTicks = function calcTicks(ax, opts) {
9931005
var hasMinor = ax.minor && (ax.minor.ticks || ax.minor.showgrid);
9941006
// minor ticks should be calculated if they are visible or if ticklabelindex is set because then
9951007
// the labels are placed at minor ticks (even if invisible) instead of major ticks.
996-
var calcMinor = hasMinor || (ticklabelIndex != null && ticklabelIndex !== 0);
1008+
var calcMinor = hasMinor || ax._useTicklabelIndex;
9971009

9981010
var majorDtick = ax.dtick;
9991011
// calc major first
@@ -1098,10 +1110,10 @@ axes.calcTicks = function calcTicks(ax, opts) {
10981110

10991111
if (isPeriod) {
11001112
// add an additional major tick and correspondingly many minor ticks
1101-
// before the first and after the last major tick
1102-
// to be able to label a period before the first and last visible ticks.
1113+
// before the first major tick to be able to label the period before the first visible ticks.
11031114
x = axes.tickIncrement(x, majorDtick, !axrev, calendar);
1104-
endTick = axes.tickIncrement(endTick, majorDtick, axrev, calendar);
1115+
// same after the last major tick to be able to label the period after the last visible tick.
1116+
if (ax._useTicklabelIndex) endTick = axes.tickIncrement(endTick, majorDtick, axrev, calendar);
11051117
if (major) majorId--;
11061118
}
11071119

@@ -1174,17 +1186,19 @@ axes.calcTicks = function calcTicks(ax, opts) {
11741186
// It makes sense if in addition to the always present dummy, there are at least 2 minor ticks
11751187
// with the required distance to each other.
11761188
const visibleMinorTicks = minorTickVals.filter((minorTick) => minorTick.noTick !== true);
1177-
if(!visibleMinorTicks || visibleMinorTicks.length < 2) {
1178-
ticklabelIndex = false;
1179-
} else {
1180-
var diff = (visibleMinorTicks[1].value - visibleMinorTicks[0].value) * (isReversed ? -1 : 1);
1181-
if(!periodCompatibleWithTickformat(diff, ax.tickformat)) {
1182-
ticklabelIndex = false;
1189+
if (ax._useTicklabelIndex) {
1190+
if(!visibleMinorTicks || visibleMinorTicks.length < 2) {
1191+
ax._useTicklabelIndex = false;
1192+
} else {
1193+
var diff = (visibleMinorTicks[1].value - visibleMinorTicks[0].value) * (isReversed ? -1 : 1);
1194+
if(!periodCompatibleWithTickformat(diff, axes.getTickFormat(ax.minor))) {
1195+
ax._useTicklabelIndex = false;
1196+
}
11831197
}
11841198
}
11851199

11861200
// Determine for which ticks to draw labels
1187-
if(!ticklabelIndex) {
1201+
if(!ax._useTicklabelIndex) {
11881202
allTicklabelVals = tickVals;
11891203
} else {
11901204
// For each major tick, find the minor tick `ticklabelIndex` steps away.
@@ -1248,7 +1262,10 @@ axes.calcTicks = function calcTicks(ax, opts) {
12481262
minorTickVals = list;
12491263
}
12501264
}
1251-
if(isPeriod) positionPeriodTicks(allTicklabelVals, ax, ax._definedDelta, periodEndTicks);
1265+
if(isPeriod) {
1266+
var periodDefinedDelta = ax._useTicklabelIndex ? ax.minor._definedDelta : ax._definedDelta;
1267+
positionPeriodTicks(allTicklabelVals, ax, periodDefinedDelta, periodEndTicks);
1268+
}
12521269

12531270
var i;
12541271
if(ax.rangebreaks) {
@@ -1332,7 +1349,7 @@ axes.calcTicks = function calcTicks(ax, opts) {
13321349
var _value = tickVals[i].value;
13331350

13341351
if(_minor) {
1335-
if(ticklabelIndex && allTicklabelVals.indexOf(tickVals[i]) !== -1) {
1352+
if(ax._useTicklabelIndex && allTicklabelVals.indexOf(tickVals[i]) !== -1) {
13361353
t = setTickLabel(ax, tickVals[i]);
13371354
} else {
13381355
t = { x: _value, text: ''};
@@ -1890,8 +1907,9 @@ function tickTextObj(ax, x, text) {
18901907
}
18911908

18921909
function formatDate(ax, out, hover, extraPrecision) {
1893-
var tr = ax._tickround;
1894-
var fmt = (hover && ax.hoverformat) || axes.getTickFormat(ax);
1910+
var periodAxis = ax._useTicklabelIndex ? ax.minor : ax;
1911+
var tr = periodAxis._tickround;
1912+
var fmt = (hover && ax.hoverformat) || axes.getTickFormat(periodAxis);
18951913

18961914
// Only apply extra precision if no explicit format was provided.
18971915
extraPrecision = !fmt && extraPrecision;

0 commit comments

Comments
 (0)