plotly.js
Version:
The open source javascript graphing library that powers plotly
1,392 lines (1,200 loc) • 162 kB
JavaScript
'use strict';
var d3 = require('@plotly/d3');
var isNumeric = require('fast-isnumeric');
var Plots = require('../../plots/plots');
var Registry = require('../../registry');
var Lib = require('../../lib');
var strTranslate = Lib.strTranslate;
var svgTextUtils = require('../../lib/svg_text_utils');
var Titles = require('../../components/titles');
var Color = require('../../components/color');
var Drawing = require('../../components/drawing');
var axAttrs = require('./layout_attributes');
var cleanTicks = require('./clean_ticks');
var constants = require('../../constants/numerical');
var ONEMAXYEAR = constants.ONEMAXYEAR;
var ONEAVGYEAR = constants.ONEAVGYEAR;
var ONEMINYEAR = constants.ONEMINYEAR;
var ONEMAXQUARTER = constants.ONEMAXQUARTER;
var ONEAVGQUARTER = constants.ONEAVGQUARTER;
var ONEMINQUARTER = constants.ONEMINQUARTER;
var ONEMAXMONTH = constants.ONEMAXMONTH;
var ONEAVGMONTH = constants.ONEAVGMONTH;
var ONEMINMONTH = constants.ONEMINMONTH;
var ONEWEEK = constants.ONEWEEK;
var ONEDAY = constants.ONEDAY;
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;
var ZERO_PATH = { K: 'zeroline' };
var GRID_PATH = { K: 'gridline', L: 'path' };
var MINORGRID_PATH = { K: 'minor-gridline', L: 'path' };
var TICK_PATH = { K: 'tick', L: 'path' };
var TICK_TEXT = { K: 'tick', L: 'text' };
var MARGIN_MAPPING = {
width: ['x', 'r', 'l', 'xl', 'xr'],
height: ['y', 't', 'b', 'yt', 'yb'],
right: ['r', 'xr'],
left: ['l', 'xl'],
top: ['t', 'yt'],
bottom: ['b', 'yb']
};
var alignmentConstants = require('../../constants/alignment');
var MID_SHIFT = alignmentConstants.MID_SHIFT;
var CAP_SHIFT = alignmentConstants.CAP_SHIFT;
var LINE_SPACING = alignmentConstants.LINE_SPACING;
var OPPOSITE_SIDE = alignmentConstants.OPPOSITE_SIDE;
var TEXTPAD = 3;
var axes = module.exports = {};
axes.setConvert = require('./set_convert');
var autoType = require('./axis_autotype');
var axisIds = require('./axis_ids');
var idSort = axisIds.idSort;
var isLinked = axisIds.isLinked;
// tight coupling to chart studio
axes.id2name = axisIds.id2name;
axes.name2id = axisIds.name2id;
axes.cleanId = axisIds.cleanId;
axes.list = axisIds.list;
axes.listIds = axisIds.listIds;
axes.getFromId = axisIds.getFromId;
axes.getFromTrace = axisIds.getFromTrace;
var autorange = require('./autorange');
axes.getAutoRange = autorange.getAutoRange;
axes.findExtremes = autorange.findExtremes;
var epsilon = 0.0001;
function expandRange(range) {
var delta = (range[1] - range[0]) * epsilon;
return [
range[0] - delta,
range[1] + delta
];
}
/*
* find the list of possible axes to reference with an xref or yref attribute
* and coerce it to that list
*
* attr: the attribute we're generating a reference for. Should end in 'x' or 'y'
* but can be prefixed, like 'ax' for annotation's arrow x
* dflt: the default to coerce to, or blank to use the first axis (falling back on
* extraOption if there is no axis)
* extraOption: aside from existing axes with this letter, what non-axis value is allowed?
* Only required if it's different from `dflt`
*/
axes.coerceRef = function(containerIn, containerOut, gd, attr, dflt, extraOption) {
var axLetter = attr.charAt(attr.length - 1);
var axlist = gd._fullLayout._subplots[axLetter + 'axis'];
var refAttr = attr + 'ref';
var attrDef = {};
if(!dflt) dflt = axlist[0] || (typeof extraOption === 'string' ? extraOption : extraOption[0]);
if(!extraOption) extraOption = dflt;
axlist = axlist.concat(axlist.map(function(x) { return x + ' domain'; }));
// data-ref annotations are not supported in gl2d yet
attrDef[refAttr] = {
valType: 'enumerated',
values: axlist.concat(extraOption ?
(typeof extraOption === 'string' ? [extraOption] : extraOption) :
[]),
dflt: dflt
};
// xref, yref
return Lib.coerce(containerIn, containerOut, attrDef, refAttr);
};
/*
* Get the type of an axis reference. This can be 'range', 'domain', or 'paper'.
* This assumes ar is a valid axis reference and returns 'range' if it doesn't
* match the patterns for 'paper' or 'domain'.
*
* ar: the axis reference string
*
*/
axes.getRefType = function(ar) {
if(ar === undefined) { return ar; }
if(ar === 'paper') { return 'paper'; }
if(ar === 'pixel') { return 'pixel'; }
if(/( domain)$/.test(ar)) { return 'domain'; } else { return 'range'; }
};
/*
* coerce position attributes (range-type) that can be either on axes or absolute
* (paper or pixel) referenced. The biggest complication here is that we don't know
* before looking at the axis whether the value must be a number or not (it may be
* a date string), so we can't use the regular valType='number' machinery
*
* axRef (string): the axis this position is referenced to, or:
* paper: fraction of the plot area
* pixel: pixels relative to some starting position
* attr (string): the attribute in containerOut we are coercing
* dflt (number): the default position, as a fraction or pixels. If the attribute
* is to be axis-referenced, this will be converted to an axis data value
*
* Also cleans the values, since the attribute definition itself has to say
* valType: 'any' to handle date axes. This allows us to accept:
* - for category axes: category names, and convert them here into serial numbers.
* Note that this will NOT work for axis range endpoints, because we don't know
* the category list yet (it's set by ax.makeCalcdata during calc)
* but it works for component (note, shape, images) positions.
* - for date axes: JS Dates or milliseconds, and convert to date strings
* - for other types: coerce them to numbers
*/
axes.coercePosition = function(containerOut, gd, coerce, axRef, attr, dflt) {
var cleanPos, pos;
var axRefType = axes.getRefType(axRef);
if(axRefType !== 'range') {
cleanPos = Lib.ensureNumber;
pos = coerce(attr, dflt);
} else {
var ax = axes.getFromId(gd, axRef);
dflt = ax.fraction2r(dflt);
pos = coerce(attr, dflt);
cleanPos = ax.cleanPos;
}
containerOut[attr] = cleanPos(pos);
};
axes.cleanPosition = function(pos, gd, axRef) {
var cleanPos = (axRef === 'paper' || axRef === 'pixel') ?
Lib.ensureNumber :
axes.getFromId(gd, axRef).cleanPos;
return cleanPos(pos);
};
axes.redrawComponents = function(gd, axIds) {
axIds = axIds ? axIds : axes.listIds(gd);
var fullLayout = gd._fullLayout;
function _redrawOneComp(moduleName, methodName, stashName, shortCircuit) {
var method = Registry.getComponentMethod(moduleName, methodName);
var stash = {};
for(var i = 0; i < axIds.length; i++) {
var ax = fullLayout[axes.id2name(axIds[i])];
var indices = ax[stashName];
for(var j = 0; j < indices.length; j++) {
var ind = indices[j];
if(!stash[ind]) {
method(gd, ind);
stash[ind] = 1;
// once is enough for images (which doesn't use the `i` arg anyway)
if(shortCircuit) return;
}
}
}
}
// annotations and shapes 'draw' method is slow,
// use the finer-grained 'drawOne' method instead
_redrawOneComp('annotations', 'drawOne', '_annIndices');
_redrawOneComp('shapes', 'drawOne', '_shapeIndices');
_redrawOneComp('images', 'draw', '_imgIndices', true);
_redrawOneComp('selections', 'drawOne', '_selectionIndices');
};
var getDataConversions = axes.getDataConversions = function(gd, trace, target, targetArray) {
var ax;
// If target points to an axis, use the type we already have for that
// axis to find the data type. Otherwise use the values to autotype.
var d2cTarget = (target === 'x' || target === 'y' || target === 'z') ?
target :
targetArray;
// In the case of an array target, make a mock data array
// and call supplyDefaults to the data type and
// setup the data-to-calc method.
if(Lib.isArrayOrTypedArray(d2cTarget)) {
ax = {
type: autoType(targetArray, undefined, {
autotypenumbers: gd._fullLayout.autotypenumbers
}),
_categories: []
};
axes.setConvert(ax);
// build up ax._categories (usually done during ax.makeCalcdata()
if(ax.type === 'category') {
for(var i = 0; i < targetArray.length; i++) {
ax.d2c(targetArray[i]);
}
}
} else {
ax = axes.getFromTrace(gd, trace, d2cTarget);
}
// if 'target' has corresponding axis
// -> use setConvert method
if(ax) return {d2c: ax.d2c, c2d: ax.c2d};
// special case for 'ids'
// -> cast to String
if(d2cTarget === 'ids') return {d2c: toString, c2d: toString};
// otherwise (e.g. numeric-array of 'marker.color' or 'marker.size')
// -> cast to Number
return {d2c: toNum, c2d: toNum};
};
function toNum(v) { return +v; }
function toString(v) { return String(v); }
axes.getDataToCoordFunc = function(gd, trace, target, targetArray) {
return getDataConversions(gd, trace, target, targetArray).d2c;
};
// get counteraxis letter for this axis (name or id)
// this can also be used as the id for default counter axis
axes.counterLetter = function(id) {
var axLetter = id.charAt(0);
if(axLetter === 'x') return 'y';
if(axLetter === 'y') return 'x';
};
// incorporate a new minimum difference and first tick into
// forced
// note that _forceTick0 is linearized, so needs to be turned into
// a range value for setting tick0
axes.minDtick = function(ax, newDiff, newFirst, allow) {
// doesn't make sense to do forced min dTick on log or category axes,
// and the plot itself may decide to cancel (ie non-grouped bars)
if(['log', 'category', 'multicategory'].indexOf(ax.type) !== -1 || !allow) {
ax._minDtick = 0;
} else if(ax._minDtick === undefined) {
// undefined means there's nothing there yet
ax._minDtick = newDiff;
ax._forceTick0 = newFirst;
} else if(ax._minDtick) {
if((ax._minDtick / newDiff + 1e-6) % 1 < 2e-6 &&
// existing minDtick is an integer multiple of newDiff
// (within rounding err)
// and forceTick0 can be shifted to newFirst
(((newFirst - ax._forceTick0) / newDiff % 1) +
1.000001) % 1 < 2e-6) {
ax._minDtick = newDiff;
ax._forceTick0 = newFirst;
} else if((newDiff / ax._minDtick + 1e-6) % 1 > 2e-6 ||
// if the converse is true (newDiff is a multiple of minDtick and
// newFirst can be shifted to forceTick0) then do nothing - same
// forcing stands. Otherwise, cancel forced minimum
(((newFirst - ax._forceTick0) / ax._minDtick % 1) +
1.000001) % 1 > 2e-6) {
ax._minDtick = 0;
}
}
};
// save a copy of the initial axis ranges in fullLayout
// use them in mode bar and dblclick events
axes.saveRangeInitial = function(gd, overwrite) {
var axList = axes.list(gd, '', true);
var hasOneAxisChanged = false;
for(var i = 0; i < axList.length; i++) {
var ax = axList[i];
var isNew =
ax._rangeInitial0 === undefined &&
ax._rangeInitial1 === undefined;
var hasChanged = isNew || (
ax.range[0] !== ax._rangeInitial0 ||
ax.range[1] !== ax._rangeInitial1
);
var autorange = ax.autorange;
if((isNew && autorange !== true) || (overwrite && hasChanged)) {
ax._rangeInitial0 = (autorange === 'min' || autorange === 'max reversed') ? undefined : ax.range[0];
ax._rangeInitial1 = (autorange === 'max' || autorange === 'min reversed') ? undefined : ax.range[1];
ax._autorangeInitial = autorange;
hasOneAxisChanged = true;
}
}
return hasOneAxisChanged;
};
// save a copy of the initial spike visibility
axes.saveShowSpikeInitial = function(gd, overwrite) {
var axList = axes.list(gd, '', true);
var hasOneAxisChanged = false;
var allSpikesEnabled = 'on';
for(var i = 0; i < axList.length; i++) {
var ax = axList[i];
var isNew = (ax._showSpikeInitial === undefined);
var hasChanged = isNew || !(ax.showspikes === ax._showspikes);
if(isNew || (overwrite && hasChanged)) {
ax._showSpikeInitial = ax.showspikes;
hasOneAxisChanged = true;
}
if(allSpikesEnabled === 'on' && !ax.showspikes) {
allSpikesEnabled = 'off';
}
}
gd._fullLayout._cartesianSpikesEnabled = allSpikesEnabled;
return hasOneAxisChanged;
};
axes.autoBin = function(data, ax, nbins, is2d, calendar, size) {
var dataMin = Lib.aggNums(Math.min, null, data);
var dataMax = Lib.aggNums(Math.max, null, data);
if(ax.type === 'category' || ax.type === 'multicategory') {
return {
start: dataMin - 0.5,
end: dataMax + 0.5,
size: Math.max(1, Math.round(size) || 1),
_dataSpan: dataMax - dataMin,
};
}
if(!calendar) calendar = ax.calendar;
// piggyback off tick code to make "nice" bin sizes and edges
var dummyAx;
if(ax.type === 'log') {
dummyAx = {
type: 'linear',
range: [dataMin, dataMax]
};
} else {
dummyAx = {
type: ax.type,
range: Lib.simpleMap([dataMin, dataMax], ax.c2r, 0, calendar),
calendar: calendar
};
}
axes.setConvert(dummyAx);
size = size && cleanTicks.dtick(size, dummyAx.type);
if(size) {
dummyAx.dtick = size;
dummyAx.tick0 = cleanTicks.tick0(undefined, dummyAx.type, calendar);
} else {
var size0;
if(nbins) size0 = ((dataMax - dataMin) / nbins);
else {
// totally auto: scale off std deviation so the highest bin is
// somewhat taller than the total number of bins, but don't let
// the size get smaller than the 'nice' rounded down minimum
// difference between values
var distinctData = Lib.distinctVals(data);
var msexp = Math.pow(10, Math.floor(
Math.log(distinctData.minDiff) / Math.LN10));
var minSize = msexp * Lib.roundUp(
distinctData.minDiff / msexp, [0.9, 1.9, 4.9, 9.9], true);
size0 = Math.max(minSize, 2 * Lib.stdev(data) /
Math.pow(data.length, is2d ? 0.25 : 0.4));
// fallback if ax.d2c output BADNUMs
// e.g. when user try to plot categorical bins
// on a layout.xaxis.type: 'linear'
if(!isNumeric(size0)) size0 = 1;
}
axes.autoTicks(dummyAx, size0);
}
var finalSize = dummyAx.dtick;
var binStart = axes.tickIncrement(
axes.tickFirst(dummyAx), finalSize, 'reverse', calendar);
var binEnd, bincount;
// check for too many data points right at the edges of bins
// (>50% within 1% of bin edges) or all data points integral
// and offset the bins accordingly
if(typeof finalSize === 'number') {
binStart = autoShiftNumericBins(binStart, data, dummyAx, dataMin, dataMax);
bincount = 1 + Math.floor((dataMax - binStart) / finalSize);
binEnd = binStart + bincount * finalSize;
} else {
// month ticks - should be the only nonlinear kind we have at this point.
// dtick (as supplied by axes.autoTick) only has nonlinear values on
// date and log axes, but even if you display a histogram on a log axis
// we bin it on a linear axis (which one could argue against, but that's
// a separate issue)
if(dummyAx.dtick.charAt(0) === 'M') {
binStart = autoShiftMonthBins(binStart, data, finalSize, dataMin, calendar);
}
// calculate the endpoint for nonlinear ticks - you have to
// just increment until you're done
binEnd = binStart;
bincount = 0;
while(binEnd <= dataMax) {
binEnd = axes.tickIncrement(binEnd, finalSize, false, calendar);
bincount++;
}
}
return {
start: ax.c2r(binStart, 0, calendar),
end: ax.c2r(binEnd, 0, calendar),
size: finalSize,
_dataSpan: dataMax - dataMin
};
};
function autoShiftNumericBins(binStart, data, ax, dataMin, dataMax) {
var edgecount = 0;
var midcount = 0;
var intcount = 0;
var blankCount = 0;
function nearEdge(v) {
// is a value within 1% of a bin edge?
return (1 + (v - binStart) * 100 / ax.dtick) % 100 < 2;
}
for(var i = 0; i < data.length; i++) {
if(data[i] % 1 === 0) intcount++;
else if(!isNumeric(data[i])) blankCount++;
if(nearEdge(data[i])) edgecount++;
if(nearEdge(data[i] + ax.dtick / 2)) midcount++;
}
var dataCount = data.length - blankCount;
if(intcount === dataCount && ax.type !== 'date') {
if(ax.dtick < 1) {
// all integers: if bin size is <1, it's because
// that was specifically requested (large nbins)
// so respect that... but center the bins containing
// integers on those integers
binStart = dataMin - 0.5 * ax.dtick;
} else {
// otherwise start half an integer down regardless of
// the bin size, just enough to clear up endpoint
// ambiguity about which integers are in which bins.
binStart -= 0.5;
if(binStart + ax.dtick < dataMin) binStart += ax.dtick;
}
} else if(midcount < dataCount * 0.1) {
if(edgecount > dataCount * 0.3 ||
nearEdge(dataMin) || nearEdge(dataMax)) {
// lots of points at the edge, not many in the middle
// shift half a bin
var binshift = ax.dtick / 2;
binStart += (binStart + binshift < dataMin) ? binshift : -binshift;
}
}
return binStart;
}
function autoShiftMonthBins(binStart, data, dtick, dataMin, calendar) {
var stats = Lib.findExactDates(data, calendar);
// number of data points that needs to be an exact value
// to shift that increment to (near) the bin center
var threshold = 0.8;
if(stats.exactDays > threshold) {
var numMonths = Number(dtick.substr(1));
if((stats.exactYears > threshold) && (numMonths % 12 === 0)) {
// The exact middle of a non-leap-year is 1.5 days into July
// so if we start the bins here, all but leap years will
// get hover-labeled as exact years.
binStart = axes.tickIncrement(binStart, 'M6', 'reverse') + ONEDAY * 1.5;
} else if(stats.exactMonths > threshold) {
// Months are not as clean, but if we shift half the *longest*
// month (31/2 days) then 31-day months will get labeled exactly
// and shorter months will get labeled with the correct month
// but shifted 12-36 hours into it.
binStart = axes.tickIncrement(binStart, 'M1', 'reverse') + ONEDAY * 15.5;
} else {
// Shifting half a day is exact, but since these are month bins it
// will always give a somewhat odd-looking label, until we do something
// smarter like showing the bin boundaries (or the bounds of the actual
// data in each bin)
binStart -= HALFDAY;
}
var nextBinStart = axes.tickIncrement(binStart, dtick);
if(nextBinStart <= dataMin) return nextBinStart;
}
return binStart;
}
// ----------------------------------------------------
// Ticks and grids
// ----------------------------------------------------
// ensure we have minor tick0 and dtick calculated
axes.prepMinorTicks = function(mockAx, ax, opts) {
if(!ax.minor.dtick) {
delete mockAx.dtick;
var hasMajor = ax.dtick && isNumeric(ax._tmin);
var mockMinorRange;
if(hasMajor) {
var tick2 = axes.tickIncrement(ax._tmin, ax.dtick, true);
// mock range a tiny bit smaller than one major tick interval
mockMinorRange = [ax._tmin, tick2 * 0.99 + ax._tmin * 0.01];
} else {
var rl = Lib.simpleMap(ax.range, ax.r2l);
// If we don't have a major dtick, the concept of minor ticks is a little
// ambiguous - just take a stab and say minor.nticks should span 1/5 the axis
mockMinorRange = [rl[0], 0.8 * rl[0] + 0.2 * rl[1]];
}
mockAx.range = Lib.simpleMap(mockMinorRange, ax.l2r);
mockAx._isMinor = true;
axes.prepTicks(mockAx, opts);
if(hasMajor) {
var numericMajor = isNumeric(ax.dtick);
var numericMinor = isNumeric(mockAx.dtick);
var majorNum = numericMajor ? ax.dtick : +ax.dtick.substring(1);
var minorNum = numericMinor ? mockAx.dtick : +mockAx.dtick.substring(1);
if(numericMajor && numericMinor) {
if(!isMultiple(majorNum, minorNum)) {
// give up on minor ticks - outside the below exceptions,
// this can only happen if minor.nticks is smaller than two jumps
// in the auto-tick scale and the first jump is not an even multiple
// (5 -> 2 or for dates 3 ->2, 15 -> 10 etc) or if you provided
// an explicit dtick, in which case it's fine to give up,
// you can provide an explicit minor.dtick.
if((majorNum === 2 * ONEWEEK) && (minorNum === 3 * ONEDAY)) {
mockAx.dtick = ONEWEEK;
} else if(majorNum === ONEWEEK && !(ax._input.minor || {}).nticks) {
// minor.nticks defaults to 5, but in this one case we want 7,
// so the minor ticks show on all days of the week
mockAx.dtick = ONEDAY;
} else if(isClose(majorNum / minorNum, 2.5)) {
// 5*10^n -> 2*10^n and you've set nticks < 5
// quarters are pretty common, we don't do this by default as it
// would add an extra digit to display, but minor has no labels
mockAx.dtick = majorNum / 2;
} else {
mockAx.dtick = majorNum;
}
} else if(majorNum === 2 * ONEWEEK && minorNum === 2 * ONEDAY) {
// this is a weird one: we don't want to automatically choose
// 2-day minor ticks for 2-week major, even though it IS an even multiple,
// because people would expect to see the weeks clearly
mockAx.dtick = ONEWEEK;
}
} else if(String(ax.dtick).charAt(0) === 'M') {
if(numericMinor) {
mockAx.dtick = 'M1';
} else {
if(!isMultiple(majorNum, minorNum)) {
// unless you provided an explicit ax.dtick (in which case
// it's OK for us to give up, you can provide an explicit
// minor.dtick too), this can only happen with:
// minor.nticks < 3 and dtick === M3, or
// minor.nticks < 5 and dtick === 5 * 10^n years
// so in all cases we just give up.
mockAx.dtick = ax.dtick;
} else if((majorNum >= 12) && (minorNum === 2)) {
// another special carve-out: for year major ticks, don't show
// 2-month minor ticks, bump to quarters
mockAx.dtick = 'M3';
}
}
} else if(String(mockAx.dtick).charAt(0) === 'L') {
if(String(ax.dtick).charAt(0) === 'L') {
if(!isMultiple(majorNum, minorNum)) {
mockAx.dtick = isClose(majorNum / minorNum, 2.5) ? (ax.dtick / 2) : ax.dtick;
}
} else {
mockAx.dtick = 'D1';
}
} else if(mockAx.dtick === 'D2' && +ax.dtick > 1) {
// the D2 log axis tick spacing is confusing for unlabeled minor ticks if
// the major dtick is more than one order of magnitude.
mockAx.dtick = 1;
}
}
// put back the original range, to use to find the full set of minor ticks
mockAx.range = ax.range;
}
if(ax.minor._tick0Init === undefined) {
// ensure identical tick0
mockAx.tick0 = ax.tick0;
}
};
function isMultiple(bigger, smaller) {
return Math.abs((bigger / smaller + 0.5) % 1 - 0.5) < 0.001;
}
function isClose(a, b) {
return Math.abs((a / b) - 1) < 0.001;
}
// ensure we have tick0, dtick, and tick rounding calculated
axes.prepTicks = function(ax, opts) {
var rng = Lib.simpleMap(ax.range, ax.r2l, undefined, undefined, opts);
// calculate max number of (auto) ticks to display based on plot size
if(ax.tickmode === 'auto' || !ax.dtick) {
var nt = ax.nticks;
var minPx;
if(!nt) {
if(ax.type === 'category' || ax.type === 'multicategory') {
minPx = ax.tickfont ? Lib.bigFont(ax.tickfont.size || 12) : 15;
nt = ax._length / minPx;
} else {
minPx = ax._id.charAt(0) === 'y' ? 40 : 80;
nt = Lib.constrain(ax._length / minPx, 4, 9) + 1;
}
// radial axes span half their domain,
// multiply nticks value by two to get correct number of auto ticks.
if(ax._name === 'radialaxis') nt *= 2;
}
if(!(ax.minor && ax.minor.tickmode !== 'array')) {
// add a couple of extra digits for filling in ticks when we
// have explicit tickvals without tick text
if(ax.tickmode === 'array') nt *= 100;
}
ax._roughDTick = Math.abs(rng[1] - rng[0]) / nt;
axes.autoTicks(ax, ax._roughDTick);
// check for a forced minimum dtick
if(ax._minDtick > 0 && ax.dtick < ax._minDtick * 2) {
ax.dtick = ax._minDtick;
ax.tick0 = ax.l2r(ax._forceTick0);
}
}
if(ax.ticklabelmode === 'period') {
adjustPeriodDelta(ax);
}
// check for missing tick0
if(!ax.tick0) {
ax.tick0 = (ax.type === 'date') ? '2000-01-01' : 0;
}
// ensure we don't try to make ticks below our minimum precision
// see https://github.com/plotly/plotly.js/issues/2892
if(ax.type === 'date' && ax.dtick < 0.1) ax.dtick = 0.1;
// now figure out rounding of tick values
autoTickRound(ax);
};
function nMonths(dtick) {
return +(dtick.substring(1));
}
function adjustPeriodDelta(ax) { // adjusts ax.dtick and sets ax._definedDelta
var definedDelta;
function mDate() {
return !(
isNumeric(ax.dtick) ||
ax.dtick.charAt(0) !== 'M'
);
}
var isMDate = mDate();
var tickformat = axes.getTickFormat(ax);
if(tickformat) {
var noDtick = ax._dtickInit !== ax.dtick;
if(
!(/%[fLQsSMX]/.test(tickformat))
// %f: microseconds as a decimal number [000000, 999999]
// %L: milliseconds as a decimal number [000, 999]
// %Q: milliseconds since UNIX epoch
// %s: seconds since UNIX epoch
// %S: second as a decimal number [00,61]
// %M: minute as a decimal number [00,59]
// %X: the locale’s time, such as %-I:%M:%S %p
) {
if(
/%[HI]/.test(tickformat)
// %H: hour (24-hour clock) as a decimal number [00,23]
// %I: hour (12-hour clock) as a decimal number [01,12]
) {
definedDelta = ONEHOUR;
if(noDtick && !isMDate && ax.dtick < ONEHOUR) ax.dtick = ONEHOUR;
} else if(
/%p/.test(tickformat) // %p: either AM or PM
) {
definedDelta = HALFDAY;
if(noDtick && !isMDate && ax.dtick < HALFDAY) ax.dtick = HALFDAY;
} else if(
/%[Aadejuwx]/.test(tickformat)
// %A: full weekday name
// %a: abbreviated weekday name
// %d: zero-padded day of the month as a decimal number [01,31]
// %e: space-padded day of the month as a decimal number [ 1,31]
// %j: day of the year as a decimal number [001,366]
// %u: Monday-based (ISO 8601) weekday as a decimal number [1,7]
// %w: Sunday-based weekday as a decimal number [0,6]
// %x: the locale’s date, such as %-m/%-d/%Y
) {
definedDelta = ONEDAY;
if(noDtick && !isMDate && ax.dtick < ONEDAY) ax.dtick = ONEDAY;
} else if(
/%[UVW]/.test(tickformat)
// %U: Sunday-based week of the year as a decimal number [00,53]
// %V: ISO 8601 week of the year as a decimal number [01, 53]
// %W: Monday-based week of the year as a decimal number [00,53]
) {
definedDelta = ONEWEEK;
if(noDtick && !isMDate && ax.dtick < ONEWEEK) ax.dtick = ONEWEEK;
} else if(
/%[Bbm]/.test(tickformat)
// %B: full month name
// %b: abbreviated month name
// %m: month as a decimal number [01,12]
) {
definedDelta = ONEAVGMONTH;
if(noDtick && (
isMDate ? nMonths(ax.dtick) < 1 : ax.dtick < ONEMINMONTH)
) ax.dtick = 'M1';
} else if(
/%[q]/.test(tickformat)
// %q: quarter of the year as a decimal number [1,4]
) {
definedDelta = ONEAVGQUARTER;
if(noDtick && (
isMDate ? nMonths(ax.dtick) < 3 : ax.dtick < ONEMINQUARTER)
) ax.dtick = 'M3';
} else if(
/%[Yy]/.test(tickformat)
// %Y: year with century as a decimal number, such as 1999
// %y: year without century as a decimal number [00,99]
) {
definedDelta = ONEAVGYEAR;
if(noDtick && (
isMDate ? nMonths(ax.dtick) < 12 : ax.dtick < ONEMINYEAR)
) ax.dtick = 'M12';
}
}
}
isMDate = mDate();
if(isMDate && ax.tick0 === ax._dowTick0) {
// discard Sunday/Monday tweaks
ax.tick0 = ax._rawTick0;
}
ax._definedDelta = definedDelta;
}
function positionPeriodTicks(tickVals, ax, definedDelta) {
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;
} else {
a = i;
b = i;
}
var A = tickVals[a].value;
var B = tickVals[b].value;
var actualDelta = Math.abs(B - A);
var delta = definedDelta || actualDelta;
var periodLength = 0;
if(delta >= ONEMINYEAR) {
if(actualDelta >= ONEMINYEAR && actualDelta <= ONEMAXYEAR) {
periodLength = actualDelta;
} else {
periodLength = ONEAVGYEAR;
}
} else if(definedDelta === ONEAVGQUARTER && delta >= ONEMINQUARTER) {
if(actualDelta >= ONEMINQUARTER && actualDelta <= ONEMAXQUARTER) {
periodLength = actualDelta;
} else {
periodLength = ONEAVGQUARTER;
}
} else if(delta >= ONEMINMONTH) {
if(actualDelta >= ONEMINMONTH && actualDelta <= ONEMAXMONTH) {
periodLength = actualDelta;
} else {
periodLength = ONEAVGMONTH;
}
} else if(definedDelta === ONEWEEK && delta >= ONEWEEK) {
periodLength = ONEWEEK;
} else if(delta >= ONEDAY) {
periodLength = ONEDAY;
} else if(definedDelta === HALFDAY && delta >= HALFDAY) {
periodLength = HALFDAY;
} else if(definedDelta === ONEHOUR && delta >= ONEHOUR) {
periodLength = ONEHOUR;
}
var inBetween;
if(periodLength >= actualDelta) {
// ensure new label positions remain between ticks
periodLength = actualDelta;
inBetween = true;
}
var endPeriod = v + periodLength;
if(ax.rangebreaks && periodLength > 0) {
var nAll = 84; // highly divisible 7 * 12
var n = 0;
for(var c = 0; c < nAll; c++) {
var r = (c + 0.5) / nAll;
if(ax.maskBreaks(v * (1 - r) + r * endPeriod) !== BADNUM) n++;
}
periodLength *= n / nAll;
if(!periodLength) {
tickVals[i].drop = true;
}
if(inBetween && actualDelta > ONEWEEK) periodLength = actualDelta; // center monthly & longer periods
}
if(
periodLength > 0 || // not instant
i === 0 // taking care first tick added
) {
tickVals[i].periodX = v + periodLength / 2;
}
}
}
// calculate the ticks: text, values, positioning
// if ticks are set to automatic, determine the right values (tick0,dtick)
// in any case, set tickround to # of digits to round tick labels to,
// or codes to this effect for log and date scales
axes.calcTicks = function calcTicks(ax, opts) {
var type = ax.type;
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]);
var maxRange = Math.max(rng[0], rng[1]);
var maxTicks = Math.max(1000, ax._length || 0);
var ticksOut = [];
var minorTicks = [];
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);
// calc major first
for(var major = 1; major >= (hasMinor ? 0 : 1); major--) {
var isMinor = !major;
if(major) {
ax._dtickInit = ax.dtick;
ax._tick0Init = ax.tick0;
} else {
ax.minor._dtickInit = ax.minor.dtick;
ax.minor._tick0Init = ax.minor.tick0;
}
var mockAx = major ? ax : Lib.extendFlat({}, ax, ax.minor);
if(isMinor) {
axes.prepMinorTicks(mockAx, ax, opts);
} else {
axes.prepTicks(mockAx, opts);
}
// now that we've figured out the auto values for formatting
// in case we're missing some ticktext, we can break out for array ticks
if(mockAx.tickmode === 'array') {
if(major) {
tickVals = [];
ticksOut = arrayTicks(ax, !isMinor);
} else {
minorTickVals = [];
minorTicks = arrayTicks(ax, !isMinor);
}
continue;
}
// fill tickVals based on overlaying axis
if(mockAx.tickmode === 'sync') {
tickVals = [];
ticksOut = syncTicks(ax);
continue;
}
// add a tiny bit so we get ticks which may have rounded out
var exRng = expandRange(rng);
var startTick = exRng[0];
var endTick = exRng[1];
var numDtick = isNumeric(mockAx.dtick);
var isDLog = (type === 'log') && !(numDtick || mockAx.dtick.charAt(0) === 'L');
// find the first tick
var x0 = axes.tickFirst(mockAx, opts);
if(major) {
ax._tmin = x0;
// No visible ticks? Quit.
// I've only seen this on category axes with all categories off the edge.
if((x0 < startTick) !== axrev) break;
// return the full set of tick vals
if(type === 'category' || type === 'multicategory') {
endTick = (axrev) ? Math.max(-0.5, endTick) :
Math.min(ax._categories.length - 0.5, endTick);
}
}
var prevX = null;
var x = x0;
var majorId;
if(major) {
// ids for ticklabelstep
var _dTick;
if(numDtick) {
_dTick = ax.dtick;
} else {
if(type === 'date') {
if(typeof ax.dtick === 'string' && ax.dtick.charAt(0) === 'M') {
_dTick = ONEAVGMONTH * ax.dtick.substring(1);
}
} else {
_dTick = ax._roughDTick;
}
}
majorId = Math.round((
ax.r2l(x) -
ax.r2l(ax.tick0)
) / _dTick) - 1;
}
var dtick = mockAx.dtick;
if(mockAx.rangebreaks && mockAx._tick0Init !== mockAx.tick0) {
// adjust tick0
x = moveOutsideBreak(x, ax);
if(!axrev) {
x = axes.tickIncrement(x, dtick, !axrev, calendar);
}
}
if(major && isPeriod) {
// add one item to label period before tick0
x = axes.tickIncrement(x, dtick, !axrev, calendar);
majorId--;
}
for(;
axrev ?
(x >= endTick) :
(x <= endTick);
x = axes.tickIncrement(
x,
dtick,
axrev,
calendar
)
) {
if(major) majorId++;
if(mockAx.rangebreaks) {
if(!axrev) {
if(x < startTick) continue;
if(mockAx.maskBreaks(x) === BADNUM && moveOutsideBreak(x, mockAx) >= maxRange) break;
}
}
// prevent infinite loops - no more than one tick per pixel,
// and make sure each value is different from the previous
if(tickVals.length > maxTicks || x === prevX) break;
prevX = x;
var obj = { value: x };
if(major) {
if(isDLog && (x !== (x | 0))) {
obj.simpleLabel = true;
}
if(ticklabelstep > 1 && majorId % ticklabelstep) {
obj.skipLabel = true;
}
tickVals.push(obj);
} else {
obj.minor = true;
minorTickVals.push(obj);
}
}
}
// 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') ||
(ax.minor.ticks === 'outside' && ax.ticks === 'inside');
if(!canOverlap) {
// 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;
}
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);
var i;
if(ax.rangebreaks) {
var flip = ax._id.charAt(0) === 'y';
var fontSize = 1; // one pixel minimum
if(ax.tickmode === 'auto') {
fontSize = ax.tickfont ? ax.tickfont.size : 12;
}
var prevL = NaN;
for(i = tickVals.length - 1; i > -1; i--) {
if(tickVals[i].drop) {
tickVals.splice(i, 1);
continue;
}
tickVals[i].value = moveOutsideBreak(tickVals[i].value, ax);
// avoid overlaps
var l = ax.c2p(tickVals[i].value);
if(flip ?
(prevL > l - fontSize) :
(prevL < l + fontSize)
) { // ensure one pixel minimum
tickVals.splice(axrev ? i + 1 : i, 1);
} else {
prevL = l;
}
}
}
// If same angle over a full circle, the last tick vals is a duplicate.
// TODO must do something similar for angular date axes.
if(isAngular(ax) && Math.abs(rng[1] - rng[0]) === 360) {
tickVals.pop();
}
// save the last tick as well as first, so we can
// show the exponent only on the last one
ax._tmax = (tickVals[tickVals.length - 1] || {}).value;
// for showing the rest of a date when the main tick label is only the
// latter part: ax._prevDateHead holds what we showed most recently.
// Start with it cleared and mark that we're in calcTicks (ie calculating a
// whole string of these so we should care what the previous date head was!)
ax._prevDateHead = '';
ax._inCalcTicks = true;
var lastVisibleHead;
var hideLabel = function(tick) {
tick.text = '';
ax._prevDateHead = lastVisibleHead;
};
tickVals = tickVals.concat(minorTickVals);
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) {
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 = setTickLabel(ax, tickVals[i]);
if(tickVals[i].skipLabel ||
ticklabelIndex && allTicklabelVals.indexOf(tickVals[i]) === -1) {
hideLabel(t);
}
ticksOut.push(t);
}
}
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;
}
return ticksOut;
};
function filterRangeBreaks(ax, ticksOut) {
if(ax.rangebreaks) {
// remove ticks falling inside rangebreaks
ticksOut = ticksOut.filter(function(d) {
return ax.maskBreaks(d.x) !== BADNUM;
});
}
return ticksOut;
}
function syncTicks(ax) {
// get the overlaying axis
var baseAxis = ax._mainAxis;
var ticksOut = [];
if(baseAxis._vals) {
for(var i = 0; i < baseAxis._vals.length; i++) {
// filter vals with noTick flag
if(baseAxis._vals[i].noTick) {
continue;
}
// get the position of the every tick
var pos = baseAxis.l2p(baseAxis._vals[i].x);
// get the tick for the current axis based on position
var vali = ax.p2l(pos);
var obj = axes.tickText(ax, vali);
// assign minor ticks
if(baseAxis._vals[i].minor) {
obj.minor = true;
obj.text = '';
}
ticksOut.push(obj);
}
}
ticksOut = filterRangeBreaks(ax, ticksOut);
return ticksOut;
}
function arrayTicks(ax, majorOnly) {
var rng = Lib.simpleMap(ax.range, ax.r2l);
var exRng = expandRange(rng);
var tickMin = Math.min(exRng[0], exRng[1]);
var tickMax = Math.max(exRng[0], exRng[1]);
// make sure showing ticks doesn't accidentally add new categories
// TODO multicategory, if we allow ticktext / tickvals
var tickVal2l = ax.type === 'category' ? ax.d2l_noadd : ax.d2l;
// array ticks on log axes always show the full number
// (if no explicit ticktext overrides it)
if(ax.type === 'log' && String(ax.dtick).charAt(0) !== 'L') {
ax.dtick = 'L' + Math.pow(10, Math.floor(Math.min(ax.range[0], ax.range[1])) - 1);
}
var ticksOut = [];
for(var isMinor = 0; isMinor <= 1; isMinor++) {
if((majorOnly !== undefined) && ((majorOnly && isMinor) || (majorOnly === false && !isMinor))) continue;
if(isMinor && !ax.minor) continue;
var vals = !isMinor ? ax.tickvals : ax.minor.tickvals;
var text = !isMinor ? ax.ticktext : [];
if(!vals) continue;
// without a text array, just format the given values as any other ticks
// except with more precision to the numbers
if(!Lib.isArrayOrTypedArray(text)) text = [];
for(var i = 0; i < vals.length; i++) {
var vali = tickVal2l(vals[i]);
if(vali > tickMin && vali < tickMax) {
var obj = axes.tickText(ax, vali, false, String(text[i]));
if(isMinor) {
obj.minor = true;
obj.text = '';
}
ticksOut.push(obj);
}
}
}
ticksOut = filterRangeBreaks(ax, ticksOut);
return ticksOut;
}
var roundBase10 = [2, 5, 10];
var roundBase24 = [1, 2, 3, 6, 12];
var roundBase60 = [1, 2, 5, 10, 15, 30];
// 2&3 day ticks are weird, but need something btwn 1&7
var roundDays = [1, 2, 3, 7, 14];
// approx. tick positions for log axes, showing all (1) and just 1, 2, 5 (2)
// these don't have to be exact, just close enough to round to the right value
var roundLog1 = [-0.046, 0, 0.301, 0.477, 0.602, 0.699, 0.778, 0.845, 0.903, 0.954, 1];
var roundLog2 = [-0.301, 0, 0.301, 0.699, 1];
// N.B. `thetaunit; 'radians' angular axes must be converted to degrees
var roundAngles = [15, 30, 45, 90, 180];
function roundDTick(roughDTick, base, roundingSet) {
return base * Lib.roundUp(roughDTick / base, roundingSet);
}
// autoTicks: calculate best guess at pleasant ticks for this axis
// inputs:
// ax - an axis object
// roughDTick - rough tick spacing (to be turned into a nice round number)
// outputs (into ax):
// tick0: starting point for ticks (not necessarily on the graph)
// usually 0 for numeric (=10^0=1 for log) or jan