Skip to content

Commit

Permalink
Merge pull request #7036 from my-tien/custom_tick_label_display
Browse files Browse the repository at this point in the history
Property `ticklabelindex` for custom tick label display
  • Loading branch information
archmoj authored Jul 10, 2024
2 parents 8b1f5bd + f8974e0 commit 7d02d9c
Show file tree
Hide file tree
Showing 19 changed files with 1,242 additions and 29 deletions.
2 changes: 1 addition & 1 deletion draftlogs/7006_add.md
Original file line number Diff line number Diff line change
@@ -1 +1 @@
- Add property `ticklabelstandoff` and `ticklabelshift` to cartesian axes to adjust positioning of tick labels [[#7006](https://github.com/plotly/plotly.js/pull/7006)]
- Add property `ticklabelstandoff` and `ticklabelshift` to cartesian axes to adjust positioning of tick labels, with thanks to @my-tien for the contribution! [[#7006](https://github.com/plotly/plotly.js/pull/7006)]
1 change: 1 addition & 0 deletions draftlogs/7036_add.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- Add axis property `ticklabelindex` for drawing the label for each minor tick n positions away from a major tick, with thanks to @my-tien for the contribution! [[#7036](https://github.com/plotly/plotly.js/pull/7036)]
3 changes: 2 additions & 1 deletion src/constants/numerical.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ module.exports = {
ONEHOUR: 3600000,
ONEMIN: 60000,
ONESEC: 1000,

ONEMILLI: 1,
ONEMICROSEC: 0.001,
/*
* For fast conversion btwn world calendars and epoch ms, the Julian Day Number
* of the unix epoch. From calendars.instance().newDate(1970, 1, 1).toJD()
Expand Down
138 changes: 111 additions & 27 deletions src/plots/cartesian/axes.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ var HALFDAY = ONEDAY / 2;
var ONEHOUR = constants.ONEHOUR;
var ONEMIN = constants.ONEMIN;
var ONESEC = constants.ONESEC;
var ONEMILLI = constants.ONEMILLI;
var ONEMICROSEC = constants.ONEMICROSEC;
var MINUS_SIGN = constants.MINUS_SIGN;
var BADNUM = constants.BADNUM;

Expand Down Expand Up @@ -908,7 +910,9 @@ axes.calcTicks = function calcTicks(ax, opts) {
var calendar = ax.calendar;
var ticklabelstep = ax.ticklabelstep;
var isPeriod = ax.ticklabelmode === 'period';

var isReversed = ax.range[0] > ax.range[1];
var ticklabelIndex = (!ax.ticklabelindex || Lib.isArrayOrTypedArray(ax.ticklabelindex)) ?
ax.ticklabelindex : [ax.ticklabelindex];
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]);
Expand All @@ -921,6 +925,9 @@ axes.calcTicks = function calcTicks(ax, opts) {

var tickVals = [];
var minorTickVals = [];
// all ticks for which labels are drawn which is not necessarily the major ticks when
// `ticklabelindex` is set.
var allTicklabelVals = [];

var hasMinor = ax.minor && (ax.minor.ticks || ax.minor.showgrid);

Expand Down Expand Up @@ -1075,6 +1082,52 @@ axes.calcTicks = function calcTicks(ax, opts) {
}
}

// check if ticklabelIndex makes sense, otherwise ignore it
if(!minorTickVals || minorTickVals.length < 2) {
ticklabelIndex = false;
} else {
var diff = (minorTickVals[1].value - minorTickVals[0].value) * (isReversed ? -1 : 1);
if(!periodCompatibleWithTickformat(diff, ax.tickformat)) {
ticklabelIndex = false;
}
}
// Determine for which ticks to draw labels
if(!ticklabelIndex) {
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.

var allTickVals = tickVals.concat(minorTickVals);
if(isPeriod && tickVals.length) {
// first major tick was just added for period handling
allTickVals = allTickVals.slice(1);
}

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]);
}
});
});
}

if(hasMinor) {
var canOverlap =
(ax.minor.ticks === 'inside' && ax.ticks === 'outside') ||
Expand Down Expand Up @@ -1108,7 +1161,7 @@ axes.calcTicks = function calcTicks(ax, opts) {
}
}

if(isPeriod) positionPeriodTicks(tickVals, ax, ax._definedDelta);
if(isPeriod) positionPeriodTicks(allTicklabelVals, ax, ax._definedDelta);

var i;
if(ax.rangebreaks) {
Expand Down Expand Up @@ -1166,38 +1219,44 @@ axes.calcTicks = function calcTicks(ax, opts) {

tickVals = tickVals.concat(minorTickVals);

var t, p;
function setTickLabel(ax, tickVal) {
var text = axes.tickText(
ax,
tickVal.value,
false, // hover
tickVal.simpleLabel // noSuffixPrefix
);
var p = tickVal.periodX;
if(p !== undefined) {
text.periodX = p;
if(p > maxRange || p < minRange) { // hide label if outside the range
if(p > maxRange) text.periodX = maxRange;
if(p < minRange) text.periodX = minRange;

hideLabel(text);
}
}
return text;
}

var t;
for(i = 0; i < tickVals.length; i++) {
var _minor = tickVals[i].minor;
var _value = tickVals[i].value;

if(_minor) {
minorTicks.push({
x: _value,
minor: true
});
if(ticklabelIndex && allTicklabelVals.indexOf(tickVals[i]) !== -1) {
t = setTickLabel(ax, tickVals[i]);
} else {
t = { x: _value };
}
t.minor = true;
minorTicks.push(t);
} else {
lastVisibleHead = ax._prevDateHead;

t = axes.tickText(
ax,
_value,
false, // hover
tickVals[i].simpleLabel // noSuffixPrefix
);

p = tickVals[i].periodX;
if(p !== undefined) {
t.periodX = p;
if(p > maxRange || p < minRange) { // hide label if outside the range
if(p > maxRange) t.periodX = maxRange;
if(p < minRange) t.periodX = minRange;

hideLabel(t);
}
}

if(tickVals[i].skipLabel) {
t = setTickLabel(ax, tickVals[i]);
if(tickVals[i].skipLabel ||
ticklabelIndex && allTicklabelVals.indexOf(tickVals[i]) === -1) {
hideLabel(t);
}

Expand Down Expand Up @@ -4542,3 +4601,28 @@ function setShiftVal(ax, axShifts) {
axShifts[ax.overlaying][ax.side] :
(ax.shift || 0);
}

/**
* Checks if the given period is at least the period described by the tickformat or larger. If that
* is the case, they are compatible, because then the tickformat can be used to describe the period.
* E.g. it doesn't make sense to put a year label on a period spanning only a month.
* @param {number} period in ms
* @param {string} tickformat
* @returns {boolean}
*/
function periodCompatibleWithTickformat(period, tickformat) {
return (
/%f/.test(tickformat) ? period >= ONEMICROSEC :
/%L/.test(tickformat) ? period >= ONEMILLI :
/%[SX]/.test(tickformat) ? period >= ONESEC :
/%M/.test(tickformat) ? period >= ONEMIN :
/%[HI]/.test(tickformat) ? period >= ONEHOUR :
/%p/.test(tickformat) ? period >= HALFDAY :
/%[Aadejuwx]/.test(tickformat) ? period >= ONEDAY :
/%[UVW]/.test(tickformat) ? period >= ONEWEEK :
/%[Bbm]/.test(tickformat) ? period >= ONEMINMONTH :
/%[q]/.test(tickformat) ? period >= ONEMINQUARTER :
/%[Yy]/.test(tickformat) ? period >= ONEMINYEAR :
true
);
}
4 changes: 4 additions & 0 deletions src/plots/cartesian/axis_defaults.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,10 @@ module.exports = function handleAxisDefaults(containerIn, containerOut, coerce,
}
}

if(!options.noTicklabelindex && (axType === 'date' || axType === 'linear')) {
coerce('ticklabelindex');
}

var ticklabelposition = '';
if(!options.noTicklabelposition || axType === 'multicategory') {
ticklabelposition = Lib.coerce(containerIn, containerOut, {
Expand Down
15 changes: 15 additions & 0 deletions src/plots/cartesian/layout_attributes.js
Original file line number Diff line number Diff line change
Expand Up @@ -721,6 +721,21 @@ module.exports = {
'outside and vice versa.'
].join(' ')
},
ticklabelindex: {
// in the future maybe add `extras: ['all', 'minor']` to allow showing labels for all ticks
// or for all minor ticks.
valType: 'integer',
arrayOk: true,
editType: 'calc',
description: [
'Only for axes with `type` *date* or *linear*.',
'Instead of drawing the major tick label, draw the label for the minor tick',
'that is n positions away from the major tick. E.g. to always draw the label for the',
'minor tick before each major tick, choose `ticklabelindex` -1. This is useful for date',
'axes with `ticklabelmode` *period* if you want to label the period that ends with each',
'major tick instead of the period that begins there.'
].join(' ')
},
mirror: {
valType: 'enumerated',
values: [true, 'ticks', false, 'all', 'allticks'],
Expand Down
1 change: 1 addition & 0 deletions src/plots/gl3d/layout/axis_defaults.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, options) {
data: options.data,
showGrid: true,
noAutotickangles: true,
noTicklabelindex: true,
noTickson: true,
noTicklabelmode: true,
noTicklabelshift: true,
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added test/image/baselines/zzz_ticklabelindex.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading

0 comments on commit 7d02d9c

Please sign in to comment.