Skip to content

Commit 236e43f

Browse files
authored
Merge pull request #7934 from my-tien/7876-ticklabelindex-positioning
fix: Center period labels over their own period when using ticklabelindex
2 parents b6684b7 + e520e67 commit 236e43f

6 files changed

Lines changed: 141 additions & 73 deletions

File tree

‎draftlogs/7934_fix.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
- Fix incorrect positioning of tick labels when using `ticklabelindex` on period axes [[#7934](https://github.com/plotly/plotly.js/pull/7934)], with thanks to @my-tien for the contribution!

‎src/plots/cartesian/axes.js‎

Lines changed: 103 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -882,30 +882,48 @@ function adjustPeriodDelta(ax) { // adjusts ax.dtick and sets ax._definedDelta
882882
ax._definedDelta = definedDelta;
883883
}
884884

885+
/**
886+
* Calculates the period label position for each tick in tickVals.
887+
* @param {array} tickVals: the list of ticks for which to position the period labels
888+
* @param {object} ax: the axis of the ticks
889+
* @param {number} definedDelta: the defined distance between two ticks
890+
*/
885891
function positionPeriodTicks(tickVals, ax, definedDelta) {
886892
for(var i = 0; i < tickVals.length; i++) {
887893
var v = tickVals[i].value;
888-
889-
var a = i;
890-
var b = i + 1;
891-
if(i < tickVals.length - 1) {
892-
a = i;
893-
b = i + 1;
894-
} else if(i > 0) {
895-
a = i - 1;
896-
b = i;
894+
var A, B;
895+
if (tickVals[i].periodEndTick != null) {
896+
// Will be set when ticklabelindex is used because labeled periods can
897+
// have unlabeled periods between each other
898+
A = tickVals[i].value;
899+
B = tickVals[i].periodEndTick.value;
897900
} else {
898-
a = i;
899-
b = i;
901+
// Use the next tick in tickVals as the period end tick
902+
var a = i;
903+
var b = i + 1;
904+
if(i < tickVals.length - 1) {
905+
a = i;
906+
b = i + 1;
907+
} else if(i > 0) {
908+
a = i - 1;
909+
b = i;
910+
} else {
911+
a = i;
912+
b = i;
913+
}
914+
915+
A = tickVals[a].value;
916+
B = tickVals[b].value;
900917
}
901918

902-
var A = tickVals[a].value;
903-
var B = tickVals[b].value;
904919
var actualDelta = Math.abs(B - A);
905920
var delta = definedDelta || actualDelta;
906921
var periodLength = 0;
907922

908-
if(delta >= ONEMINYEAR) {
923+
if (tickVals[i].periodEndTick != null) {
924+
// the period end is known exactly, so there is nothing to infer
925+
periodLength = actualDelta;
926+
} else if(delta >= ONEMINYEAR) {
909927
if(actualDelta >= ONEMINYEAR && actualDelta <= ONEMAXYEAR) {
910928
periodLength = actualDelta;
911929
} else {
@@ -978,6 +996,7 @@ axes.calcTicks = function calcTicks(ax, opts) {
978996
var isReversed = ax.range[0] > ax.range[1];
979997
var ticklabelIndex = (!ax.ticklabelindex || Lib.isArrayOrTypedArray(ax.ticklabelindex)) ?
980998
ax.ticklabelindex : [ax.ticklabelindex];
999+
ax._useTicklabelIndex = ticklabelIndex != null && ticklabelIndex !== 0;
9811000
var rng = Lib.simpleMap(ax.range, ax.r2l, undefined, undefined, opts);
9821001
var axrev = (rng[1] < rng[0]);
9831002
var minRange = Math.min(rng[0], rng[1]);
@@ -997,7 +1016,7 @@ axes.calcTicks = function calcTicks(ax, opts) {
9971016
var hasMinor = ax.minor && (ax.minor.ticks || ax.minor.showgrid);
9981017
// minor ticks should be calculated if they are visible or if ticklabelindex is set because then
9991018
// the labels are placed at minor ticks (even if invisible) instead of major ticks.
1000-
var calcMinor = hasMinor || ticklabelIndex;
1019+
var calcMinor = hasMinor || ax._useTicklabelIndex;
10011020

10021021
// calc major first
10031022
for(var major = 1; major >= (calcMinor ? 0 : 1); major--) {
@@ -1099,7 +1118,7 @@ axes.calcTicks = function calcTicks(ax, opts) {
10991118
}
11001119
}
11011120

1102-
if((major || ticklabelIndex) && isPeriod) {
1121+
if((major || ax._useTicklabelIndex) && isPeriod) {
11031122
// if major: add one item to label period before tick0
11041123
// if minor: add one item for ticklabelindex positioning. positionPeriodTicks requires
11051124
// at least 2 ticks to calculate the period length, so we add a dummy tick, ensuring
@@ -1156,93 +1175,104 @@ axes.calcTicks = function calcTicks(ax, opts) {
11561175
}
11571176
}
11581177

1178+
function findOverlappingTick(minorTick, majorTicks) {
1179+
const majorTick = majorTicks.find((majorTick) => majorTick.value === minorTick.value);
1180+
if (majorTick != null) {
1181+
return majorTick;
1182+
}
1183+
// add 10e6 to eliminate problematic digits
1184+
const epsilon = 10e6;
1185+
for (let i = 0; i < majorTicks.length; i++) {
1186+
if (epsilon + majorTicks[i].value === epsilon + minorTick.value) {
1187+
return majorTicks[i];
1188+
}
1189+
}
1190+
return null;
1191+
};
1192+
11591193
// check if ticklabelIndex makes sense, otherwise ignore it.
11601194
// It makes sense if in addition to the always present dummy, there are at least 2 minor ticks
11611195
// with the required distance to each other.
11621196
if(!minorTickVals || minorTickVals.length < 3) {
1163-
ticklabelIndex = false;
1197+
ax._useTicklabelIndex = false;
11641198
} else {
1165-
var diff = (minorTickVals[2].value - minorTickVals[1].value) * (isReversed ? -1 : 1);
1199+
const diff = (minorTickVals[2].value - minorTickVals[1].value) * (isReversed ? -1 : 1);
11661200
if(!periodCompatibleWithTickformat(diff, ax.tickformat)) {
1167-
ticklabelIndex = false;
1201+
ax._useTicklabelIndex = false;
11681202
// remove previously added tick before tick0 for handling ticklabelindex positioning
11691203
minorTickVals = minorTickVals.slice(1);
11701204
}
11711205
}
1206+
11721207
// Determine for which ticks to draw labels
1173-
if(!ticklabelIndex) {
1208+
if(!ax._useTicklabelIndex) {
11741209
allTicklabelVals = tickVals;
11751210
} else {
1176-
// Collect and sort all major and minor ticks, to find the minor ticks `ticklabelIndex`
1177-
// steps away from each major tick. For those minor ticks we want to draw the label.
1211+
// For each major tick, find the minor tick `ticklabelIndex` steps away.
1212+
// This minor tick will be labeled instead of the major tick.
11781213

1179-
var allTickVals = tickVals.concat(minorTickVals);
1180-
if(isPeriod && tickVals.length) {
1214+
// if a minor tick coincides with a major tick, drop the minor tick and keep the major one
1215+
// because overlapping minor ticks are stripped away if both minor and major ticks are drawn on the same side.
1216+
const filteredMinors = minorTickVals.filter((minor) => findOverlappingTick(minor, tickVals) == null);
1217+
const labelTickValsAscending = tickVals.concat(filteredMinors)
1218+
.sort((a, b) => a.value - b.value);
1219+
1220+
if (isPeriod && tickVals.length) {
11811221
// first major tick was just added for period handling
1182-
allTickVals = allTickVals.slice(1);
1222+
tickVals[0].skipLabel = true;
11831223
}
1184-
1185-
allTickVals =
1186-
allTickVals
1187-
.sort(function(a, b) { return a.value - b.value; })
1188-
.filter(function(tick, index, self) {
1189-
return index === 0 || tick.value !== self[index - 1].value;
1190-
});
1191-
1192-
var majorTickIndices =
1193-
allTickVals
1194-
.map(function(item, index) {
1195-
return item.minor === undefined && !item.skipLabel ? index : null;
1196-
})
1197-
.filter(function(index) { return index !== null; });
1198-
1199-
majorTickIndices.forEach(function(majorIdx) {
1200-
ticklabelIndex.map(function(nextLabelIdx) {
1201-
var minorIdx = majorIdx + nextLabelIdx;
1202-
if(minorIdx >= 0 && minorIdx < allTickVals.length) {
1203-
Lib.pushUnique(allTicklabelVals, allTickVals[minorIdx]);
1204-
}
1205-
});
1206-
});
1207-
tickVals.forEach(function(tick) {
1208-
tick.skipLabel = allTicklabelVals.indexOf(tick) === -1;
1224+
tickVals.forEach(function(majorTick) {
1225+
if (!majorTick.skipLabel) {
1226+
ticklabelIndex.forEach((labelIndex) => {
1227+
if (labelIndex < 0) {
1228+
const smallerTicks = labelTickValsAscending.filter((minorTick) => minorTick.value <= majorTick.value);
1229+
const absLabelIndex = Math.abs(labelIndex);
1230+
const labeledTickIndex = smallerTicks.length - absLabelIndex - 1;
1231+
if (absLabelIndex < smallerTicks.length) {
1232+
const tickToLabel = smallerTicks[labeledTickIndex];
1233+
allTicklabelVals.push(tickToLabel);
1234+
if (isPeriod) {
1235+
tickToLabel.periodEndTick = smallerTicks[labeledTickIndex + 1];
1236+
}
1237+
}
1238+
} else { // labelIndex >= 0
1239+
const largerTicks = labelTickValsAscending.filter((minorTick) => minorTick.value >= majorTick.value);
1240+
if (labelIndex < largerTicks.length) {
1241+
const tickToLabel = largerTicks[labelIndex];
1242+
allTicklabelVals.push(tickToLabel);
1243+
if (isPeriod && largerTicks[labelIndex + 1]) {
1244+
tickToLabel.periodEndTick = largerTicks[labelIndex + 1];
1245+
}
1246+
}
1247+
}
1248+
});
1249+
// Skip the major tick label since the label moved to a minor tick
1250+
majorTick.skipLabel = true;
1251+
}
12091252
});
1253+
allTicklabelVals.forEach((t) => t.skipLabel = false);
12101254
}
12111255

12121256
if(hasMinor) {
1213-
var canOverlap =
1257+
var allowedToOverlap =
12141258
(ax.minor.ticks === 'inside' && ax.ticks === 'outside') ||
12151259
(ax.minor.ticks === 'outside' && ax.ticks === 'inside');
12161260

1217-
if(!canOverlap) {
1261+
if(!allowedToOverlap) {
12181262
// remove duplicate minors
1219-
1220-
var majorValues = tickVals.map(function(d) { return d.value; });
1221-
12221263
var list = [];
12231264
for(var k = 0; k < minorTickVals.length; k++) {
1224-
var T = minorTickVals[k];
1225-
var v = T.value;
1226-
if(majorValues.indexOf(v) !== -1) {
1227-
continue;
1228-
}
1229-
var found = false;
1230-
for(var q = 0; !found && (q < tickVals.length); q++) {
1231-
if(
1232-
// add 10e6 to eliminate problematic digits
1233-
10e6 + tickVals[q].value ===
1234-
10e6 + v
1235-
) {
1236-
found = true;
1237-
}
1265+
if (findOverlappingTick(minorTickVals[k], tickVals) == null) {
1266+
list.push(minorTickVals[k]);
12381267
}
1239-
if(!found) list.push(T);
12401268
}
12411269
minorTickVals = list;
12421270
}
12431271
}
12441272

1245-
if(isPeriod) positionPeriodTicks(allTicklabelVals, ax, ax._definedDelta);
1273+
if(isPeriod) {
1274+
positionPeriodTicks(allTicklabelVals, ax, ax._definedDelta);
1275+
}
12461276

12471277
var i;
12481278
if(ax.rangebreaks) {
@@ -1328,7 +1358,7 @@ axes.calcTicks = function calcTicks(ax, opts) {
13281358
var _value = tickVals[i].value;
13291359

13301360
if(_minor) {
1331-
if(ticklabelIndex && allTicklabelVals.indexOf(tickVals[i]) !== -1) {
1361+
if(ax._useTicklabelIndex && allTicklabelVals.indexOf(tickVals[i]) !== -1) {
13321362
t = setTickLabel(ax, tickVals[i]);
13331363
} else {
13341364
t = { x: _value };
@@ -1346,7 +1376,7 @@ axes.calcTicks = function calcTicks(ax, opts) {
13461376
}
13471377
}
13481378

1349-
if(isPeriod && ticklabelIndex && minorTicks.length) {
1379+
if(isPeriod && ax._useTicklabelIndex && minorTicks.length) {
13501380
// drop very first minor tick that we added to handle ticklabelindex
13511381
minorTicks[0].noTick = true;
13521382
}
361 Bytes
Loading
791 Bytes
Loading
15.8 KB
Loading
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
{
2+
"data": [
3+
{
4+
"mode": "markers+text",
5+
"texttemplate": "%{x|Q%q %y}",
6+
"textposition": "top center",
7+
"x": [
8+
"2019-07-01", "2019-10-01", "2020-01-01", "2020-04-01", "2020-07-01"
9+
],
10+
"y": [
11+
2, 2, 2, 2, 2
12+
]
13+
}
14+
],
15+
"layout": {
16+
"width": 600,
17+
"height": 400,
18+
"xaxis": {
19+
"dtick": "M24",
20+
"insiderange": [
21+
"2019-03-01",
22+
"2021-01-01"
23+
],
24+
"tick0": "2021-01-01",
25+
"tickformat": "%Y",
26+
"ticklabelindex": -1,
27+
"ticklabelmode": "period",
28+
"ticklen": 20,
29+
"type": "date",
30+
"ticks": "outside",
31+
"minor": { "dtick": "M12" },
32+
"title": {
33+
"text": "Should display 1 major tick and a label 2020."
34+
}
35+
}
36+
}
37+
}

0 commit comments

Comments
 (0)