UNPKG

plotly.js

Version:

The open source javascript graphing library that powers plotly

1,392 lines (1,200 loc) 162 kB
'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