-
-
Notifications
You must be signed in to change notification settings - Fork 2k
fix: Format date tick labels with minor-tick precision when using ticklabelindex
#7935
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
base: main
Are you sure you want to change the base?
Changes from all commits
5047961
c59fa9f
9fbd6c4
b12f71a
c1ecb06
65dbb62
b1703cb
77d7d26
6ebf050
140c33b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| - Fix incorrect positioning of tick labels when using ticklabelindex on period axes [[#7934](https://github.com/plotly/plotly.js/pull/7934)] |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| - Fix tick label format sometimes being incompatible with the labeled tick when using ticklabelindex [[#7934](https://github.com/plotly/plotly.js/pull/7935)] | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -716,6 +716,13 @@ axes.prepMinorTicks = function(mockAx, ax, opts) { | |
| // ensure identical tick0 | ||
| mockAx.tick0 = ax.tick0; | ||
| } | ||
| if (ax._useTicklabelIndex) { | ||
| // this could always be done but the additional information on the minor | ||
| // axis is just necessary when using ticklabelindex. | ||
| autoTickRound(mockAx); | ||
| delete mockAx.minor; // prevent self-reference | ||
| Lib.extendFlat(ax.minor, mockAx); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What are you trying to copy over on this line? I think it might be better to save the specific keys you're looking for than adding the entirety of |
||
| } | ||
| }; | ||
|
|
||
| function isMultiple(bigger, smaller) { | ||
|
|
@@ -882,25 +889,41 @@ function adjustPeriodDelta(ax) { // adjusts ax.dtick and sets ax._definedDelta | |
| ax._definedDelta = definedDelta; | ||
| } | ||
|
|
||
| function positionPeriodTicks(tickVals, ax, definedDelta) { | ||
| /** | ||
| * Calculates the period label position for each tick in tickVals. | ||
| * @param {array} tickVals: the list of ticks for which to position the period labels | ||
| * @param {object} ax: the axis of the ticks | ||
| * @param {number} definedDelta: the defined distance between two ticks | ||
| * @param {array (optional)} periodEndTicks: the optional list of neighboring ticks | ||
| * for each tick in tickVals. If not provided, the function will use the next | ||
| * tick in tickVals as the neighbor. Useful if labeled periods have unlabeled | ||
| * periods between each other which can happen when using ticklabelindex. | ||
| */ | ||
| function positionPeriodTicks(tickVals, ax, definedDelta, periodEndTicks) { | ||
| 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; | ||
| var A, B; | ||
| if (periodEndTicks != null) { | ||
| A = tickVals[i].value; | ||
| B = periodEndTicks[i].value; | ||
| } else { | ||
| a = i; | ||
| b = i; | ||
| 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; | ||
| } | ||
|
|
||
| A = tickVals[a].value; | ||
| B = tickVals[b].value; | ||
| } | ||
|
|
||
| var A = tickVals[a].value; | ||
| var B = tickVals[b].value; | ||
| var actualDelta = Math.abs(B - A); | ||
| var delta = definedDelta || actualDelta; | ||
| var periodLength = 0; | ||
|
|
@@ -978,6 +1001,7 @@ axes.calcTicks = function calcTicks(ax, opts) { | |
| var isReversed = ax.range[0] > ax.range[1]; | ||
| var ticklabelIndex = (!ax.ticklabelindex || Lib.isArrayOrTypedArray(ax.ticklabelindex)) ? | ||
| ax.ticklabelindex : [ax.ticklabelindex]; | ||
| ax._useTicklabelIndex = ticklabelIndex != null && ticklabelIndex !== 0; | ||
| var rng = Lib.simpleMap(ax.range, ax.r2l, undefined, undefined, opts); | ||
| var axrev = (rng[1] < rng[0]); | ||
| var minRange = Math.min(rng[0], rng[1]); | ||
|
|
@@ -993,11 +1017,14 @@ axes.calcTicks = function calcTicks(ax, opts) { | |
| // all ticks for which labels are drawn which is not necessarily the major ticks when | ||
| // `ticklabelindex` is set. | ||
| var allTicklabelVals = []; | ||
| // for period label positioning when using `ticklabelindex`: | ||
| // for each tick in `allTicklabelVals` holds the neighboring period end tick | ||
| var periodEndTicks; | ||
|
|
||
| var hasMinor = ax.minor && (ax.minor.ticks || ax.minor.showgrid); | ||
| // minor ticks should be calculated if they are visible or if ticklabelindex is set because then | ||
| // the labels are placed at minor ticks (even if invisible) instead of major ticks. | ||
| var calcMinor = hasMinor || ticklabelIndex; | ||
| var calcMinor = hasMinor || ax._useTicklabelIndex; | ||
|
|
||
| // calc major first | ||
| for(var major = 1; major >= (calcMinor ? 0 : 1); major--) { | ||
|
|
@@ -1099,7 +1126,7 @@ axes.calcTicks = function calcTicks(ax, opts) { | |
| } | ||
| } | ||
|
|
||
| if((major || ticklabelIndex) && isPeriod) { | ||
| if((major || ax._useTicklabelIndex) && isPeriod) { | ||
| // if major: add one item to label period before tick0 | ||
| // if minor: add one item for ticklabelindex positioning. positionPeriodTicks requires | ||
| // at least 2 ticks to calculate the period length, so we add a dummy tick, ensuring | ||
|
|
@@ -1153,93 +1180,109 @@ axes.calcTicks = function calcTicks(ax, opts) { | |
| } | ||
| } | ||
|
|
||
| function findOverlappingTick(minorTick, majorTicks) { | ||
| const majorTick = majorTicks.find((majorTick) => majorTick.value === minorTick.value); | ||
| if (majorTick != null) { | ||
| return majorTick; | ||
| } | ||
| // add 10e6 to eliminate problematic digits | ||
| const epsilon = 10e6; | ||
| for (var i = 0; i < majorTicks.length; i++) { | ||
| if (epsilon + majorTicks[i].value === epsilon + minorTick.value) { | ||
| return majorTicks[i]; | ||
| } | ||
| } | ||
| return null; | ||
| }; | ||
|
|
||
| // check if ticklabelIndex makes sense, otherwise ignore it. | ||
| // It makes sense if in addition to the always present dummy, there are at least 2 minor ticks | ||
| // with the required distance to each other. | ||
| if(!minorTickVals || minorTickVals.length < 3) { | ||
| ticklabelIndex = false; | ||
| ax._useTicklabelIndex = false; | ||
| } else { | ||
| var diff = (minorTickVals[2].value - minorTickVals[1].value) * (isReversed ? -1 : 1); | ||
| if(!periodCompatibleWithTickformat(diff, ax.tickformat)) { | ||
| ticklabelIndex = false; | ||
| ax._useTicklabelIndex = false; | ||
| // remove previously added tick before tick0 for handling ticklabelindex positioning | ||
| minorTickVals = minorTickVals.slice(1); | ||
| } | ||
| } | ||
|
|
||
| // Determine for which ticks to draw labels | ||
| if(!ticklabelIndex) { | ||
| if(!ax._useTicklabelIndex) { | ||
| allTicklabelVals = tickVals; | ||
| } else { | ||
| // Collect and sort all major and minor ticks, to find the minor ticks `ticklabelIndex` | ||
| // steps away from each major tick. For those minor ticks we want to draw the label. | ||
| // For each major tick, find the minor tick `ticklabelIndex` steps away. | ||
| // This minor tick will be labeled instead of the major tick. | ||
| if (isPeriod) periodEndTicks = []; // for each minor tick at the start of a labeled period this will hold the neighboring period end tick. | ||
|
|
||
| const labelTickValsAscending = minorTickVals | ||
| .map((minor) => { | ||
| // if there is a major tick at the same position, prefer it over the minor tick because overlapping minor ticks are stripped away | ||
| // if both minor and major ticks are drawn on the same side. | ||
| const major = findOverlappingTick(minor, tickVals); | ||
| if (major) { | ||
| return major; | ||
| } | ||
| return minor; | ||
| }) | ||
| .toSorted((a, b) => a.value - b.value); | ||
|
|
||
| var allTickVals = tickVals.concat(minorTickVals); | ||
| if(isPeriod && tickVals.length) { | ||
| if (isPeriod && tickVals.length) { | ||
| // first major tick was just added for period handling | ||
| allTickVals = allTickVals.slice(1); | ||
| tickVals[0].skipLabel = true; | ||
| } | ||
|
|
||
| allTickVals = | ||
| allTickVals | ||
| .sort(function(a, b) { return a.value - b.value; }) | ||
| .filter(function(tick, index, self) { | ||
| return index === 0 || tick.value !== self[index - 1].value; | ||
| }); | ||
|
|
||
| var majorTickIndices = | ||
| allTickVals | ||
| .map(function(item, index) { | ||
| return item.minor === undefined && !item.skipLabel ? index : null; | ||
| }) | ||
| .filter(function(index) { return index !== null; }); | ||
|
|
||
| majorTickIndices.forEach(function(majorIdx) { | ||
| ticklabelIndex.map(function(nextLabelIdx) { | ||
| var minorIdx = majorIdx + nextLabelIdx; | ||
| if(minorIdx >= 0 && minorIdx < allTickVals.length) { | ||
| Lib.pushUnique(allTicklabelVals, allTickVals[minorIdx]); | ||
| } | ||
| }); | ||
| }); | ||
| tickVals.forEach(function(tick) { | ||
| tick.skipLabel = allTicklabelVals.indexOf(tick) === -1; | ||
| tickVals.forEach(function(majorTick) { | ||
| if (!majorTick.skipLabel) { | ||
| ticklabelIndex.forEach((labelIndex) => { | ||
| if (labelIndex < 0) { | ||
| const smallerTicks = labelTickValsAscending.filter((minorTick) => minorTick.value <= majorTick.value); | ||
| const absLabelIndex = Math.abs(labelIndex); | ||
| const labeledTickIndex = smallerTicks.length - absLabelIndex - 1; | ||
| if (absLabelIndex <= smallerTicks.length - 1) { | ||
| allTicklabelVals.push(smallerTicks[labeledTickIndex]); | ||
| if (isPeriod) periodEndTicks.push(smallerTicks[labeledTickIndex + 1]); | ||
| } | ||
| } else { // labelIndex >= 0 | ||
| const largerTicks = labelTickValsAscending.filter((minorTick) => minorTick.value >= majorTick.value); | ||
| if (labelIndex < largerTicks.length - 1) { | ||
| allTicklabelVals.push(largerTicks[labelIndex]); | ||
| if (isPeriod) periodEndTicks.push(largerTicks[labelIndex + 1]); | ||
| } | ||
| } | ||
| majorTick.skipLabel = majorTick.skipLabel !== false; | ||
| }); | ||
| } | ||
| }); | ||
| allTicklabelVals.forEach((t) => t.skipLabel = false); | ||
| } | ||
|
|
||
| if(hasMinor) { | ||
| var canOverlap = | ||
| var allowedToOverlap = | ||
| (ax.minor.ticks === 'inside' && ax.ticks === 'outside') || | ||
| (ax.minor.ticks === 'outside' && ax.ticks === 'inside'); | ||
|
|
||
| if(!canOverlap) { | ||
| if(!allowedToOverlap) { | ||
| // remove duplicate minors | ||
|
|
||
| var majorValues = tickVals.map(function(d) { return d.value; }); | ||
|
|
||
| var list = []; | ||
| for(var k = 0; k < minorTickVals.length; k++) { | ||
| var T = minorTickVals[k]; | ||
| var v = T.value; | ||
| if(majorValues.indexOf(v) !== -1) { | ||
| continue; | ||
| if (findOverlappingTick(minorTickVals[k], tickVals) == null) { | ||
| list.push(minorTickVals[k]); | ||
| } | ||
| var found = false; | ||
| for(var q = 0; !found && (q < tickVals.length); q++) { | ||
| if( | ||
| // add 10e6 to eliminate problematic digits | ||
| 10e6 + tickVals[q].value === | ||
| 10e6 + v | ||
| ) { | ||
| found = true; | ||
| } | ||
| } | ||
| if(!found) list.push(T); | ||
| } | ||
| minorTickVals = list; | ||
| } | ||
| } | ||
|
|
||
| if(isPeriod) positionPeriodTicks(allTicklabelVals, ax, ax._definedDelta); | ||
| if(isPeriod) { | ||
| if (ax._useTicklabelIndex) { | ||
| var periodDefinedDelta = ax._useTicklabelIndex ? ax.minor._definedDelta : ax._definedDelta; | ||
| positionPeriodTicks(allTicklabelVals, ax, ax.minor._definedDelta, periodEndTicks); | ||
| } else { | ||
| positionPeriodTicks(allTicklabelVals, ax, ax._definedDelta); | ||
| } | ||
| } | ||
|
|
||
| var i; | ||
| if(ax.rangebreaks) { | ||
|
|
@@ -1323,7 +1366,7 @@ axes.calcTicks = function calcTicks(ax, opts) { | |
| var _value = tickVals[i].value; | ||
|
|
||
| if(_minor) { | ||
| if(ticklabelIndex && allTicklabelVals.indexOf(tickVals[i]) !== -1) { | ||
| if(ax._useTicklabelIndex && allTicklabelVals.indexOf(tickVals[i]) !== -1) { | ||
| t = setTickLabel(ax, tickVals[i]); | ||
| } else { | ||
| t = { x: _value }; | ||
|
|
@@ -1341,7 +1384,7 @@ axes.calcTicks = function calcTicks(ax, opts) { | |
| } | ||
| } | ||
|
|
||
| if(isPeriod && ticklabelIndex && minorTicks.length) { | ||
| if(isPeriod && ax._useTicklabelIndex && minorTicks.length) { | ||
| // drop very first minor tick that we added to handle ticklabelindex | ||
| minorTicks[0].noTick = true; | ||
| } | ||
|
|
@@ -1890,9 +1933,8 @@ function tickTextObj(ax, x, text) { | |
| } | ||
|
|
||
| function formatDate(ax, out, hover, extraPrecision) { | ||
| var tr = ax._tickround; | ||
| var fmt = (hover && ax.hoverformat) || axes.getTickFormat(ax); | ||
|
|
||
| var tr = ax._useTicklabelIndex ? ax.minor._tickround : ax._tickround; | ||
| var fmt = ax._useTicklabelIndex ? "" : (hover && ax.hoverformat) || axes.getTickFormat(ax); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why do you use an empty string for a |
||
| // Only apply extra precision if no explicit format was provided. | ||
| extraPrecision = !fmt && extraPrecision; | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| { | ||
| "data": [ | ||
| { | ||
| "mode": "markers+text", | ||
| "texttemplate": "%{x|Q%q %y}", | ||
| "textposition": "top center", | ||
| "x": [ | ||
| "2019-07-01", "2019-10-01", "2020-01-01", "2020-04-01", "2020-07-01" | ||
| ], | ||
| "y": [ | ||
| 2, 2, 2, 2, 2 | ||
| ] | ||
| } | ||
| ], | ||
| "layout": { | ||
| "width": 600, | ||
| "height": 400, | ||
| "xaxis": { | ||
| "dtick": "M24", | ||
| "insiderange": [ | ||
| "2019-03-01", | ||
| "2021-01-01" | ||
| ], | ||
| "tick0": "2021-01-01", | ||
| "tickformat": "%Y", | ||
| "ticklabelindex": -1, | ||
| "ticklabelmode": "period", | ||
| "ticklen": 20, | ||
| "type": "date", | ||
| "ticks": "outside", | ||
| "minor": { "dtick": "M12" }, | ||
| "title": { | ||
| "text": "Should display 1 major tick and a label 2020." | ||
| } | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.