plotly.js
Version:
The open source javascript graphing library that powers plotly
1,366 lines (1,174 loc) • 127 kB
JavaScript
/**
* Copyright 2012-2020, Plotly, Inc.
* All rights reserved.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
'use strict';
var d3 = require('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 MINUS_SIGN = constants.MINUS_SIGN;
var BADNUM = constants.BADNUM;
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);
};
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(Array.isArray(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]);
}
}
// TODO what to do for transforms?
} 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._rangeInitial === undefined);
var hasChanged = isNew || !(
ax.range[0] === ax._rangeInitial[0] &&
ax.range[1] === ax._rangeInitial[1]
);
if((isNew && ax.autorange === false) || (overwrite && hasChanged)) {
ax._rangeInitial = ax.range.slice();
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 tick0, dtick, and tick rounding calculated
axes.prepTicks = function(ax, opts) {
var rng = Lib.simpleMap(ax.range, ax.r2l, undefined, undefined, opts);
ax._dtickInit = ax.dtick;
ax._tick0Init = ax.tick0;
// 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 ? (ax.tickfont.size || 12) * 1.2 : 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;
}
// 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) {
axes.prepTicks(ax, opts);
var rng = Lib.simpleMap(ax.range, ax.r2l, undefined, undefined, 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(ax.tickmode === 'array') return arrayTicks(ax);
// 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];
// check for reversed axis
var axrev = (rng[1] < rng[0]);
var minRange = Math.min(rng[0], rng[1]);
var maxRange = Math.max(rng[0], rng[1]);
var isDLog = (ax.type === 'log') && !(isNumeric(ax.dtick) || ax.dtick.charAt(0) === 'L');
var isPeriod = ax.ticklabelmode === 'period';
// find the first tick
ax._tmin = axes.tickFirst(ax, opts);
// No visible ticks? Quit.
// I've only seen this on category axes with all categories off the edge.
if((ax._tmin < startTick) !== axrev) return [];
// return the full set of tick vals
if(ax.type === 'category' || ax.type === 'multicategory') {
endTick = (axrev) ? Math.max(-0.5, endTick) :
Math.min(ax._categories.length - 0.5, endTick);
}
var x = ax._tmin;
if(ax.rangebreaks && ax._tick0Init !== ax.tick0) {
// adjust tick0
x = moveOutsideBreak(x, ax);
if(!axrev) {
x = axes.tickIncrement(x, ax.dtick, !axrev, ax.calendar);
}
}
if(isPeriod) {
// add one item to label period before tick0
x = axes.tickIncrement(x, ax.dtick, !axrev, ax.calendar);
}
var maxTicks = Math.max(1000, ax._length || 0);
var tickVals = [];
var xPrevious = null;
for(;
(axrev) ? (x >= endTick) : (x <= endTick);
x = axes.tickIncrement(x, ax.dtick, axrev, ax.calendar)
) {
if(ax.rangebreaks) {
if(!axrev) {
if(x < startTick) continue;
if(ax.maskBreaks(x) === BADNUM && moveOutsideBreak(x, ax) >= 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 === xPrevious) break;
xPrevious = x;
var minor = false;
if(isDLog && (x !== (x | 0))) {
minor = true;
}
tickVals.push({
minor: minor,
value: x
});
}
if(isPeriod) positionPeriodTicks(tickVals, 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 ticksOut = [];
var t, p;
for(i = 0; i < tickVals.length; i++) {
var _minor = tickVals[i].minor;
var _value = tickVals[i].value;
t = axes.tickText(
ax,
_value,
false, // hover
_minor // 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;
t.text = ' '; // don't use an empty string here which can confuse automargin (issue 5132)
ax._prevDateHead = '';
}
}
ticksOut.push(t);
}
ax._inCalcTicks = false;
return ticksOut;
};
function arrayTicks(ax) {
var vals = ax.tickvals;
var text = ax.ticktext;
var ticksOut = new Array(vals.length);
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]);
var j = 0;
// without a text array, just format the given values as any other ticks
// except with more precision to the numbers
if(!Array.isArray(text)) text = [];
// 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);
}
for(var i = 0; i < vals.length; i++) {
var vali = tickVal2l(vals[i]);
if(vali > tickMin && vali < tickMax) {
if(text[i] === undefined) ticksOut[j] = axes.tickText(ax, vali);
else ticksOut[j] = tickTextObj(ax, vali, String(text[i]));
j++;
}
}
if(j < vals.length) ticksOut.splice(j, vals.length - j);
if(ax.rangebreaks) {
// remove ticks falling inside rangebreaks
ticksOut = ticksOut.filter(function(d) {
return ax.maskBreaks(d.x) !== BADNUM;
});
}
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 1, 2000 for dates
// dtick: the actual, nice round tick spacing, usually a little larger than roughDTick
// if the ticks are spaced linearly (linear scale, categories,
// log with only full powers, date ticks < month),
// this will just be a number
// months: M#
// years: M# where # is 12*number of years
// log with linear ticks: L# where # is the linear tick spacing
// log showing powers plus some intermediates:
// D1 shows all digits, D2 shows 2 and 5
axes.autoTicks = function(ax, roughDTick) {
var base;
function getBase(v) {
return Math.pow(v, Math.floor(Math.log(roughDTick) / Math.LN10));
}
if(ax.type === 'date') {
ax.tick0 = Lib.dateTick0(ax.calendar, 0);
// the criteria below are all based on the rough spacing we calculate
// being > half of the final unit - so precalculate twice the rough val
var roughX2 = 2 * roughDTick;
if(roughX2 > ONEAVGYEAR) {
roughDTick /= ONEAVGYEAR;
base = getBase(10);
ax.dtick = 'M' + (12 * roundDTick(roughDTick, base, roundBase10));
} else if(roughX2 > ONEAVGMONTH) {
roughDTick /= ONEAVGMONTH;
ax.dtick = 'M' + roundDTick(roughDTick, 1, roundBase24);
} else if(roughX2 > ONEDAY) {
ax.dtick = roundDTick(roughDTick, ONEDAY, ax._hasDayOfWeekBreaks ? [1, 2, 7, 14] : roundDays);
// get week ticks on sunday
// this will also move the base tick off 2000-01-01 if dtick is
// 2 or 3 days... but that's a weird enough case that we'll ignore it.
var tickformat = axes.getTickFormat(ax);
var isPeriod = ax.ticklabelmode === 'period';
if(isPeriod) ax._rawTick0 = ax.tick0;
if(/%[uVW]/.test(tickformat)) {
ax.tick0 = Lib.dateTick0(ax.calendar, 2); // Monday
} else {
ax.tick0 = Lib.dateTick0(ax.calendar, 1); // Sunday
}
if(isPeriod) ax._dowTick0 = ax.tick0;
} else if(roughX2 > ONEHOUR) {
ax.dtick = roundDTick(roughDTick, ONEHOUR, roundBase24);
} else if(roughX2 > ONEMIN) {
ax.dtick = roundDTick(roughDTick, ONEMIN, roundBase60);
} else if(roughX2 > ONESEC) {
ax.dtick = roundDTick(roughDTick, ONESEC, roundBase60);
} else {
// milliseconds
base = getBase(10);
ax.dtick = roundDTick(roughDTick, base, roundBase10);
}
} else if(ax.type === 'log') {
ax.tick0 = 0;
var rng = Lib.simpleMap(ax.range, ax.r2l);
if(roughDTick > 0.7) {
// only show powers of 10
ax.dtick = Math.ceil(roughDTick);
} else if(Math.abs(rng[1] - rng[0]) < 1) {
// span is less than one power of 10
var nt = 1.5 * Math.abs((rng[1] - rng[0]) / roughDTick);
// ticks on a linear scale, labeled fully
roughDTick = Math.abs(Math.pow(10, rng[1]) -
Math.pow(10, rng[0])) / nt;
base = getBase(10);
ax.dtick = 'L' + roundDTick(roughDTick, base, roundBase10);
} else {
// include intermediates between powers of 10,
// labeled with small digits
// ax.dtick = "D2" (show 2 and 5) or "D1" (show all digits)
ax.dtick = (roughDTick > 0.3) ? 'D2' : 'D1';
}
} else if(ax.type === 'category' || ax.type === 'multicategory') {
ax.tick0 = 0;
ax.dtick = Math.ceil(Math.max(roughDTick, 1));
} else if(isAngular(ax)) {
ax.tick0 = 0;
base = 1;
ax.dtick = roundDTick(roughDTick, base, roundAngles);
} else {
// auto ticks always start at 0
ax.tick0 = 0;
base = getBase(10);
ax.dtick = roundDTick(roughDTick, base, roundBase10);
}
// prevent infinite loops
if(ax.dtick === 0) ax.dtick = 1;
// TODO: this is from log axis histograms with autorange off
if(!isNumeric(ax.dtick) && typeof ax.dtick !== 'string') {
var olddtick = ax.dtick;
ax.dtick = 1;
throw 'ax.dtick error: ' + String(olddtick);
}
};
// after dtick is already known, find tickround = precision
// to display in tick labels
// for numeric ticks, integer # digits after . to round to
// for date ticks, the last date part to show (y,m,d,H,M,S)
// or an integer # digits past seconds
function autoTickRound(ax) {
var dtick = ax.dtick;
ax._tickexponent = 0;
if(!isNumeric(dtick) && typeof dtick !== 'string') {
dtick = 1;
}
if(ax.type === 'category' || ax.type === 'multicategory') {
ax._tickround = null;
}
if(ax.type === 'date') {
// If tick0 is unusual, give tickround a bit more information
// not necessarily *all* the information in tick0 though, if it's really odd
// minimal string length for tick0: 'd' is 10, 'M' is 16, 'S' is 19
// take off a leading minus (year < 0) and i (intercalary month) so length is consistent
var tick0ms = ax.r2l(ax.tick0);
var tick0str = ax.l2r(tick0ms).replace(/(^-|i)/g, '');
var tick0len = tick0str.length;
if(String(dtick).charAt(0) === 'M') {
// any tick0 more specific than a year: alway show the full date
if(tick0len > 10 || tick0str.substr(5) !== '01-01') ax._tickround = 'd';
// show the month unless ticks are full multiples of a year
else ax._tickround = (+(dtick.substr(1)) % 12 === 0) ? 'y' : 'm';
} else if((dtick >= ONEDAY && tick0len <= 10) || (dtick >= ONEDAY * 15)) ax._tickround = 'd';
else if((dtick >= ONEMIN && tick0len <= 16) || (dtick >= ONEHOUR)) ax._tickround = 'M';
else if((dtick >= ONESEC && tick0len <= 19) || (dtick >= ONEMIN)) ax._tickround = 'S';
else {
// tickround is a number of digits of fractional seconds
// of any two adjacent ticks, at least one will have the maximum fractional digits
// of all possible ticks - so take the max. length of tick0 and the next one
var tick1len = ax.l2r(tick0ms + dtick).replace(/^-/, '').length;
ax._tickround = Math.max(tick0len, tick1len) - 20;
// We shouldn't get here... but in case there's a situation I'm
// not thinking of where tick0str and tick1str are identical or
// something, fall back on maximum precision
if(ax._tickround < 0) ax._tickround = 4;
}
} else if(isNumeric(dtick) || dtick.charAt(0) === 'L') {
// linear or log (except D1, D2)
var rng = ax.range.map(ax.r2d || Number);
if(!isNumeric(dtick)) dtick = Number(dtick.substr(1));
// 2 digits past largest digit of dtick
ax._tickround = 2 - Math.floor(Math.log(dtick) / Math.LN10 + 0.01);
var maxend = Math.max(Math.abs(rng[0]), Math.abs(rng[1]));
var rangeexp = Math.floor(Math.log(maxend) / Math.LN10 + 0.01);
var minexponent = ax.minexponent === undefined ? 3 : ax.minexponent;
if(Math.abs(rangeexp) > minexponent) {
if(isSIFormat(ax.exponentformat) && !beyondSI(rangeexp)) {
ax._tickexponent = 3 * Math.round((rangeexp - 1) / 3);
} else ax._tickexponent = rangeexp;
}
} else {
// D1 or D2 (log)
ax._tickround = null;
}
}
// months and years don't have constant millisecond values
// (but a year is always 12 months so we only need months)
// log-scale ticks are also not consistently spaced, except
// for pure powers of 10
// numeric ticks always have constant differences, other datetime ticks
// can all be calculated as constant number of milliseconds
axes.tickIncrement = function(x, dtick, axrev, calendar) {
var axSign = axrev ? -1 : 1;
// includes linear, all dates smaller than month, and pure 10^n in log
if(isNumeric(dtick)) return Lib.increment(x, axSign * dtick);
// everything else is a string, one character plus a number
var tType = dtick.charAt(0);
var dtSigned = axSign * Number(dtick.substr(1));
// Dates: months (or years - see Lib.incrementMonth)
if(tType === 'M') return Lib.incrementMonth(x, dtSigned, calendar);
// Log scales: Linear, Digits
if(tType === 'L') return Math.log(Math.pow(10, x) + dtSigned) / Math.LN10;
// log10 of 2,5,10, or all digits (logs just have to be
// close enough to round)
if(tType === 'D') {
var tickset = (dtick === 'D2') ? roundLog2 : roundLog1;
var x2 = x + axSign * 0.01;
var frac = Lib.roundUp(Lib.mod(x2, 1), tickset, axrev);
return Math.floor(x2) +
Math.log(d3.round(Math.pow(10, frac), 1)) / Math.LN10;
}
throw 'unrecognized dtick ' + String(dtick);
};
// calculate the first tick on an axis
axes.tickFirst = function(ax, opts) {
var r2l = ax.r2l || Number;
var rng = Lib.simpleMap(ax.range, r2l, undefined, undefined, opts);
var axrev = rng[1] < rng[0];
var sRound = axrev ? Math.floor : Math.ceil;
// add a tiny extra bit to make sure we get ticks
// that may have been rounded out
var r0 = expandRange(rng)[0];
var dtick = ax.dtick;
var tick0 = r2l(ax.tick0);
if(isNumeric(dtick)) {
var tmin = sRound((r0 - tick0) / dtick) * dtick + tick0;
// make sure no ticks outside the category list
if(ax.type === 'category' || ax.type === 'multicategory') {
tmin = Lib.constrain(tmin, 0, ax._categories.length - 1);
}
return tmin;
}
var tType = dtick.charAt(0);
var dtNum = Number(dtick.substr(1));
// Dates: months (or years)
if(tType === 'M') {
var cnt = 0;
var t0 = tick0;
var t1, mult, newDTick;
// This algorithm should work for *any* nonlinear (but close to linear!)
// tick spacing. Limit to 10 iterations, for gregorian months it's normally <=3.
while(cnt < 10) {
t1 = axes.tickIncrement(t0, dtick, axrev, ax.calendar);
if((t1 - r0) * (t0 - r0) <= 0) {
// t1 and t0 are on opposite sides of r0! we've succeeded!
if(axrev) return Math.min(t0, t1);
return Math.max(t0, t1);
}
mult = (r0 - ((t0 + t1) / 2)) / (t1 - t0);
newDTick = tType + ((Math.abs(Math.round(mult)) || 1) * dtNum);
t0 = axes.tickIncrement(t0, newDTick, mult < 0 ? !axrev : axrev, ax.calendar);
cnt++;
}
Lib.error('tickFirst did not converge', ax);
return t0;
} else if(tType === 'L') {
// Log scales: Linear, Digits
return Math.log(sRound(
(Math.pow(10, r0) - tick0) / dtNum) * dtNum + tick0) / Math.LN10;
} else if(tType === 'D') {
var tickset = (dtick === 'D2') ? roundLog2 : roundLog1;
var frac = Lib.roundUp(Lib.mod(r0, 1), tickset, axrev);
return Math.floor(r0) +
Math.log(d3.round(Math.pow(10, frac), 1)) / Math.LN10;
} else throw 'unrecognized dtick ' + String(dtick);
};
// draw the text for one tick.
// px,py are the location on gd.paper
// prefix is there so the x axis ticks can be dropped a line
// ax is the axis layout, x is the tick value
// hover is a (truthy) flag for whether to show numbers with a bit
// more precision for hovertext
axes.tickText = function(ax, x, hover, noSuffixPrefix) {
var out = tickTextObj(ax, x);
var arrayMode = ax.tickmode === 'array';
var extraPrecision = hover || arrayMode;
var axType = ax.type;
// TODO multicategory, if we allow ticktext / tickvals
var tickVal2l = axType === 'category' ? ax.d2l_noadd : ax.d2l;
var i;
if(arrayMode && Array.isArray(ax.ticktext)) {
var rng = Lib.simpleMap(ax.range, ax.r2l);
var minDiff = (Math.abs(rng[1] - rng[0]) - (ax._lBreaks || 0)) / 10000;
for(i = 0; i < ax.ticktext.length; i++) {
if(Math.abs(x - tickVal2l(ax.tickvals[i])) < minDiff) break;
}
if(i < ax.ticktext.length) {
out.text = String(ax.ticktext[i]);
return out;
}
}
function isHidden(showAttr) {
if(showAttr === undefined) return true;
if(hover) return showAttr === 'none';
var firstOrLast = {
first: ax._tmin,
last: ax._tmax
}[showAttr];
return showAttr !== 'all' && x !== firstOrLast;
}
var hideexp = hover ?
'never' :
ax.exponentformat !== 'none' && isHidden(ax.showexponent) ? 'hide' : '';
if(axType === 'date') formatDate(ax, out, hover, extraPrecision);
else if(axType === 'log') formatLog(ax, out, hover, extraPrecision, hideexp);
else if(axType === 'category') formatCategory(ax, out);
else if(axType === 'multicategory') formatMultiCategory(ax, out, hover);
else if(isAngular(ax)) formatAngle(ax, out, hover, extraPrecision, hideexp);
else formatLinear(ax, out, hover, extraPrecision, hideexp);
// add prefix and suffix
if(!noSuffixPrefix) {
if(ax.tickprefix && !isHidden(ax.showtickprefix)) out.text = ax.tickprefix + out.text;
if(ax.ticksuffix && !isHidden(ax.showticksuffix)) out.text += ax.ticksuffix;
}
// Setup ticks and grid lines boundaries
// at 1/2 a 'category' to the left/bottom
if(ax.tickson === 'boundaries' || ax.showdividers) {
var inbounds = function(v) {
var p = ax.l2p(v);
return p >= 0 && p <= ax._length ? v : null;
};
out.xbnd = [
inbounds(out.x - 0.5),
inbounds(out.x + ax.dtick - 0.5)
];
}
return out;
};
/**
* create text for a hover label on this axis, with special handling of
* log axes (where negative values can't be displayed but can appear in hover text)
*
* @param {obje