diff --git a/draftlogs/7937_fix.md b/draftlogs/7937_fix.md new file mode 100644 index 00000000000..e016d31f8dd --- /dev/null +++ b/draftlogs/7937_fix.md @@ -0,0 +1 @@ +- Fix missing period label in last period when using a negative ticklabelindex [[#7937](https://github.com/plotly/plotly.js/pull/7937)] diff --git a/src/plots/cartesian/axes.js b/src/plots/cartesian/axes.js index 1661d8c115c..e60e4110aa5 100644 --- a/src/plots/cartesian/axes.js +++ b/src/plots/cartesian/axes.js @@ -1024,6 +1024,7 @@ axes.calcTicks = function calcTicks(ax, opts) { // the labels are placed at minor ticks (even if invisible) instead of major ticks. var calcMinor = hasMinor || ax._useTicklabelIndex; + var majorDtick = ax.dtick; // calc major first for(var major = 1; major >= (calcMinor ? 0 : 1); major--) { var isMinor = !major; @@ -1041,6 +1042,7 @@ axes.calcTicks = function calcTicks(ax, opts) { axes.prepMinorTicks(mockAx, ax, opts); } else { axes.prepTicks(mockAx, opts); + majorDtick = mockAx.dtick; } // now that we've figured out the auto values for formatting @@ -1067,9 +1069,11 @@ axes.calcTicks = function calcTicks(ax, opts) { var exRng = expandRange(rng); var startTick = exRng[0]; var endTick = exRng[1]; + var visibleEndTick = endTick; // for period axis, there will be an additional major tick after that + var dtick = mockAx.dtick; - var numDtick = isNumeric(mockAx.dtick); - var isDLog = (type === 'log') && !(numDtick || mockAx.dtick.charAt(0) === 'L'); + var numDtick = isNumeric(dtick); + var isDLog = (type === 'log') && !(numDtick || dtick.charAt(0) === 'L'); // find the first tick var x0 = axes.tickFirst(mockAx, opts); @@ -1113,7 +1117,6 @@ axes.calcTicks = function calcTicks(ax, opts) { ) / _dTick) - 1; } - var dtick = mockAx.dtick; var tick0l = (type === 'linear') ? mockAx.r2l(mockAx.tick0) : undefined; if(mockAx.rangebreaks && mockAx._tick0Init !== mockAx.tick0) { @@ -1125,11 +1128,11 @@ axes.calcTicks = function calcTicks(ax, opts) { } 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 - // that if a tick is labeled, there are always at least 2 ticks. - x = axes.tickIncrement(x, dtick, !axrev, calendar); + // add an additional major tick and correspondingly many minor ticks + // before the first major tick to be able to label the period before the first visible ticks. + x = axes.tickIncrement(x, majorDtick, !axrev, calendar); + // same after the last major tick to be able to label the period after the last visible tick. + if (ax._useTicklabelIndex) endTick = axes.tickIncrement(endTick, majorDtick, axrev, calendar); if (major) majorId--; } @@ -1160,6 +1163,11 @@ axes.calcTicks = function calcTicks(ax, opts) { var obj = { value: snapToGrid(x, tick0l, dtick) }; + // mark ticks that were only added for period label positioning as "noTick" so they aren't drawn. + if (axrev ? (x > startTick || x < visibleEndTick) : (x < startTick || x > visibleEndTick)) { + obj.noTick = true; + } + if(major) { // mark first major tick, for showexponent/showtickprefix/showticksuffix 'first' if(x === x0) obj.first = true; @@ -1199,14 +1207,15 @@ axes.calcTicks = function calcTicks(ax, opts) { // 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) { - ax._useTicklabelIndex = false; - } else { - const diff = (minorTickVals[2].value - minorTickVals[1].value) * (isReversed ? -1 : 1); - if(!periodCompatibleWithTickformat(diff, ax.tickformat)) { + if (ax._useTicklabelIndex) { + const visibleMinorTicks = minorTickVals.filter((minorTick) => !minorTick.noTick); + if (visibleMinorTicks.length < 2) { ax._useTicklabelIndex = false; - // remove previously added tick before tick0 for handling ticklabelindex positioning - minorTickVals = minorTickVals.slice(1); + } else { + const diff = (visibleMinorTicks[1].value - visibleMinorTicks[0].value) * (isReversed ? -1 : 1); + if(!periodCompatibleWithTickformat(diff, ax.tickformat)) { + ax._useTicklabelIndex = false; + } } } @@ -1223,10 +1232,6 @@ axes.calcTicks = function calcTicks(ax, opts) { const labelTickValsAscending = tickVals.concat(filteredMinors) .sort((a, b) => a.value - b.value); - if (isPeriod && tickVals.length) { - // first major tick was just added for period handling - tickVals[0].skipLabel = true; - } tickVals.forEach(function(majorTick) { if (!majorTick.skipLabel) { ticklabelIndex.forEach((labelIndex) => { @@ -1236,18 +1241,26 @@ axes.calcTicks = function calcTicks(ax, opts) { const labeledTickIndex = smallerTicks.length - absLabelIndex - 1; if (absLabelIndex < smallerTicks.length) { const tickToLabel = smallerTicks[labeledTickIndex]; - allTicklabelVals.push(tickToLabel); - if (isPeriod) { - tickToLabel.periodEndTick = smallerTicks[labeledTickIndex + 1]; + const periodEndTick = isPeriod ? smallerTicks[labeledTickIndex + 1] : undefined; + const labelVisible = !tickToLabel.noTick || (periodEndTick && !periodEndTick.noTick); + if (labelVisible) { + allTicklabelVals.push(tickToLabel); + if (periodEndTick && !periodEndTick.noTick) { + tickToLabel.periodEndTick = periodEndTick; + } } } } else { // labelIndex >= 0 const largerTicks = labelTickValsAscending.filter((minorTick) => minorTick.value >= majorTick.value); - if (labelIndex < largerTicks.length) { + if (labelIndex < largerTicks.length && !largerTicks[labelIndex].noTick) { const tickToLabel = largerTicks[labelIndex]; - allTicklabelVals.push(tickToLabel); - if (isPeriod && largerTicks[labelIndex + 1]) { - tickToLabel.periodEndTick = largerTicks[labelIndex + 1]; + const periodEndTick = isPeriod ? largerTicks[labelIndex + 1] : undefined; + const labelVisible = !tickToLabel.noTick || (periodEndTick && !periodEndTick.noTick); + if (labelVisible) { + allTicklabelVals.push(tickToLabel); + if (periodEndTick && !periodEndTick.noTick) { + tickToLabel.periodEndTick = periodEndTick; + } } } } @@ -1367,8 +1380,9 @@ axes.calcTicks = function calcTicks(ax, opts) { if(ax._useTicklabelIndex && allTicklabelVals.indexOf(tickVals[i]) !== -1) { t = setTickLabel(ax, tickVals[i]); } else { - t = { x: _value }; + t = { x: _value, text: ''}; } + t.noTick = tickVals[i].noTick; t.minor = true; minorTicks.push(t); } else { @@ -1377,24 +1391,14 @@ axes.calcTicks = function calcTicks(ax, opts) { if (tickVals[i].skipLabel) { hideLabel(t); } - + t.noTick = tickVals[i].noTick; ticksOut.push(t); } } - if(isPeriod && ax._useTicklabelIndex && minorTicks.length) { - // drop very first minor tick that we added to handle ticklabelindex - minorTicks[0].noTick = true; - } - ticksOut = ticksOut.concat(minorTicks); - ax._inCalcTicks = false; - if(isPeriod && ticksOut.length) { - // drop very first tick that we added to handle period - ticksOut[0].noTick = true; - } - + ticksOut = ticksOut.concat(minorTicks); return ticksOut; }; diff --git a/test/image/baselines/ticklabelindex-2.png b/test/image/baselines/ticklabelindex-2.png index 312a7f07709..9858e71441b 100644 Binary files a/test/image/baselines/ticklabelindex-2.png and b/test/image/baselines/ticklabelindex-2.png differ diff --git a/test/image/mocks/ticklabelindex-2.json b/test/image/mocks/ticklabelindex-2.json index 047fb3485a3..9321d4923ea 100644 --- a/test/image/mocks/ticklabelindex-2.json +++ b/test/image/mocks/ticklabelindex-2.json @@ -1,5 +1,13 @@ { "data": [ + { + "x": [ + "2020", "2021", "2022" + ], + "y": [ + 3, 2, 1 + ] + }, { "mode": "markers+text", "texttemplate": "%{x|Q%q %y}", @@ -9,13 +17,36 @@ ], "y": [ 2, 2, 2, 2, 2 - ] + ], + "yaxis": "y2", + "xaxis": "x2" } ], "layout": { "width": 600, - "height": 400, + "height": 800, + "grid": { + "rows": 2, + "columns": 1, + "pattern": "independent" + }, "xaxis": { + "dtick": "M12", + "insiderange": [ + "2020-03-27", + "2022-07-21" + ], + "tickformat": "%Y", + "ticklabelindex": -1, + "ticks": "outside", + "ticklen": 20, + "ticklabelmode": "period", + "type": "date", + "title": { + "text": "Should display 2 major ticks and labels 2020, 2021, 2022" + } + }, + "xaxis2": { "dtick": "M24", "insiderange": [ "2019-03-01", @@ -28,9 +59,9 @@ "ticklen": 20, "type": "date", "ticks": "outside", - "minor": { "dtick": "M12" }, + "minor": { "dtick": "M12", "ticks": "outside", "ticklen": 5 }, "title": { - "text": "Should display 1 major tick and a label 2020." + "text": "Should display 1 major tick, 1 minor tick and a label 2020 in between." } } }