'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]); } } // 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._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 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, isMinor) { 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); if(!isMinor) { // 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(ax._isMinor) { // Log axes by default get MORE than nTicks based on the metrics below // But for minor ticks we don't want this increase, we already have // the major ticks. roughDTick *= 1.5; } 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; var inbounds = function(v) { var p = ax.l2p(v); return p >= 0 && p <= ax._length ? v : null; }; if(arrayMode && Lib.isArrayOrTypedArray(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]); out.xbnd = [ inbounds(out.x - 0.5), inbounds(out.x + ax.dtick - 0.5) ]; 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; } if(ax.labelalias && ax.labelalias.hasOwnProperty(out.text)) { var t = ax.labelalias[out.text]; if(typeof t === 'string') out.text = t; } // Setup ticks and grid lines boundaries // at 1/2 a 'category' to the left/bottom if(ax.tickson === 'boundaries' || ax.showdividers) { 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 {object} ax: the axis to format text for * @param {number or array of numbers} values: calcdata value(s) to format * @param {Optional(string)} hoverformat: trace (x|y)hoverformat to override axis.hoverformat * * @returns {string} `val` formatted as a string appropriate to this axis, or * first value and second value as a range (ie ' - ') if the second value is provided and * it's different from the first value. */ axes.hoverLabelText = function(ax, values, hoverformat) { if(hoverformat) ax = Lib.extendFlat({}, ax, {hoverformat: hoverformat}); var val = Lib.isArrayOrTypedArray(values) ? values[0] : values; var val2 = Lib.isArrayOrTypedArray(values) ? values[1] : undefined; if(val2 !== undefined && val2 !== val) { return ( axes.hoverLabelText(ax, val, hoverformat) + ' - ' + axes.hoverLabelText(ax, val2, hoverformat) ); } var logOffScale = (ax.type === 'log' && val <= 0); var tx = axes.tickText(ax, ax.c2l(logOffScale ? -val : val), 'hover').text; if(logOffScale) { return val === 0 ? '0' : MINUS_SIGN + tx; } // TODO: should we do something special if the axis calendar and // the data calendar are different? Somehow display both dates with // their system names? Right now it will just display in the axis calendar // but users could add the other one as text. return tx; }; function tickTextObj(ax, x, text) { var tf = ax.tickfont || {}; return { x: x, dx: 0, dy: 0, text: text || '', fontSize: tf.size, font: tf.family, fontWeight: tf.weight, fontStyle: tf.style, fontVariant: tf.variant, fontTextcase: tf.textcase, fontLineposition: tf.lineposition, fontShadow: tf.shadow, fontColor: tf.color }; } function formatDate(ax, out, hover, extraPrecision) { var tr = ax._tickround; var fmt = (hover && ax.hoverformat) || axes.getTickFormat(ax); // Only apply extra precision if no explicit format was provided. extraPrecision = !fmt && extraPrecision; if(extraPrecision) { // second or sub-second precision: extra always shows max digits. // for other fields, extra precision just adds one field. if(isNumeric(tr)) tr = 4; else tr = {y: 'm', m: 'd', d: 'M', M: 'S', S: 4}[tr]; } var dateStr = Lib.formatDate(out.x, fmt, tr, ax._dateFormat, ax.calendar, ax._extraFormat); var headStr; var splitIndex = dateStr.indexOf('\n'); if(splitIndex !== -1) { headStr = dateStr.substr(splitIndex + 1); dateStr = dateStr.substr(0, splitIndex); } if(extraPrecision) { // if extraPrecision led to trailing zeros, strip them off // actually, this can lead to removing even more zeros than // in the original rounding, but that's fine because in these // contexts uniformity is not so important (if there's even // anything to be uniform with!) // can we remove the whole time part? if(headStr !== undefined && (dateStr === '00:00:00' || dateStr === '00:00')) { dateStr = headStr; headStr = ''; } else if(dateStr.length === 8) { // strip off seconds if they're zero (zero fractional seconds // are already omitted) // but we never remove minutes and leave just hours dateStr = dateStr.replace(/:00$/, ''); } } if(headStr) { if(hover) { // hover puts it all on one line, so headPart works best up front // except for year headPart: turn this into "Jan 1, 2000" etc. if(tr === 'd') dateStr += ', ' + headStr; else dateStr = headStr + (dateStr ? ', ' + dateStr : ''); } else { if( !ax._inCalcTicks || ax._prevDateHead !== headStr ) { ax._prevDateHead = headStr; dateStr += '
' + headStr; } else { var isInside = insideTicklabelposition(ax); var side = ax._trueSide || ax.side; // polar mocks the side of the radial axis if( (!isInside && side === 'top') || (isInside && side === 'bottom') ) { dateStr += '
'; } } } } out.text = dateStr; } function formatLog(ax, out, hover, extraPrecision, hideexp) { var dtick = ax.dtick; var x = out.x; var tickformat = ax.tickformat; var dtChar0 = typeof dtick === 'string' && dtick.charAt(0); if(hideexp === 'never') { // If this is a hover label, then we must *never* hide the exponent // for the sake of display, which could give the wrong value by // potentially many orders of magnitude. If hideexp was 'never', then // it's now succeeded by preventing the other condition from automating // this choice. Thus we can unset it so that the axis formatting takes // precedence. hideexp = ''; } if(extraPrecision && (dtChar0 !== 'L')) { dtick = 'L3'; dtChar0 = 'L'; } if(tickformat || (dtChar0 === 'L')) { out.text = numFormat(Math.pow(10, x), ax, hideexp, extraPrecision); } else if(isNumeric(dtick) || ((dtChar0 === 'D') && (Lib.mod(x + 0.01, 1) < 0.1))) { var p = Math.round(x); var absP = Math.abs(p); var exponentFormat = ax.exponentformat; if(exponentFormat === 'power' || (isSIFormat(exponentFormat) && beyondSI(p))) { if(p === 0) out.text = 1; else if(p === 1) out.text = '10'; else out.text = '10' + (p > 1 ? '' : MINUS_SIGN) + absP + ''; out.fontSize *= 1.25; } else if((exponentFormat === 'e' || exponentFormat === 'E') && absP > 2) { out.text = '1' + exponentFormat + (p > 0 ? '+' : MINUS_SIGN) + absP; } else { out.text = numFormat(Math.pow(10, x), ax, '', 'fakehover'); if(dtick === 'D1' && ax._id.charAt(0) === 'y') { out.dy -= out.fontSize / 6; } } } else if(dtChar0 === 'D') { out.text = String(Math.round(Math.pow(10, Lib.mod(x, 1)))); out.fontSize *= 0.75; } else throw 'unrecognized dtick ' + String(dtick); // if 9's are printed on log scale, move the 10's away a bit if(ax.dtick === 'D1') { var firstChar = String(out.text).charAt(0); if(firstChar === '0' || firstChar === '1') { if(ax._id.charAt(0) === 'y') { out.dx -= out.fontSize / 4; } else { out.dy += out.fontSize / 2; out.dx += (ax.range[1] > ax.range[0] ? 1 : -1) * out.fontSize * (x < 0 ? 0.5 : 0.25); } } } } function formatCategory(ax, out) { var tt = ax._categories[Math.round(out.x)]; if(tt === undefined) tt = ''; out.text = String(tt); } function formatMultiCategory(ax, out, hover) { var v = Math.round(out.x); var cats = ax._categories[v] || []; var tt = cats[1] === undefined ? '' : String(cats[1]); var tt2 = cats[0] === undefined ? '' : String(cats[0]); if(hover) { // TODO is this what we want? out.text = tt2 + ' - ' + tt; } else { // setup for secondary labels out.text = tt; out.text2 = tt2; } } function formatLinear(ax, out, hover, extraPrecision, hideexp) { if(hideexp === 'never') { // If this is a hover label, then we must *never* hide the exponent // for the sake of display, which could give the wrong value by // potentially many orders of magnitude. If hideexp was 'never', then // it's now succeeded by preventing the other condition from automating // this choice. Thus we can unset it so that the axis formatting takes // precedence. hideexp = ''; } else if(ax.showexponent === 'all' && Math.abs(out.x / ax.dtick) < 1e-6) { // don't add an exponent to zero if we're showing all exponents // so the only reason you'd show an exponent on zero is if it's the // ONLY tick to get an exponent (first or last) hideexp = 'hide'; } out.text = numFormat(out.x, ax, hideexp, extraPrecision); } function formatAngle(ax, out, hover, extraPrecision, hideexp) { if(ax.thetaunit === 'radians' && !hover) { var num = out.x / 180; if(num === 0) { out.text = '0'; } else { var frac = num2frac(num); if(frac[1] >= 100) { out.text = numFormat(Lib.deg2rad(out.x), ax, hideexp, extraPrecision); } else { var isNeg = out.x < 0; if(frac[1] === 1) { if(frac[0] === 1) out.text = 'π'; else out.text = frac[0] + 'π'; } else { out.text = [ '', frac[0], '', '⁄', '', frac[1], '', 'π' ].join(''); } if(isNeg) out.text = MINUS_SIGN + out.text; } } } else { out.text = numFormat(out.x, ax, hideexp, extraPrecision); } } // inspired by // https://github.com/yisibl/num2fraction/blob/master/index.js function num2frac(num) { function almostEq(a, b) { return Math.abs(a - b) <= 1e-6; } function findGCD(a, b) { return almostEq(b, 0) ? a : findGCD(b, a % b); } function findPrecision(n) { var e = 1; while(!almostEq(Math.round(n * e) / e, n)) { e *= 10; } return e; } var precision = findPrecision(num); var number = num * precision; var gcd = Math.abs(findGCD(number, precision)); return [ // numerator Math.round(number / gcd), // denominator Math.round(precision / gcd) ]; } // format a number (tick value) according to the axis settings // new, more reliable procedure than d3.round or similar: // add half the rounding increment, then stringify and truncate // also automatically switch to sci. notation var SIPREFIXES = ['f', 'p', 'n', 'μ', 'm', '', 'k', 'M', 'G', 'T']; function isSIFormat(exponentFormat) { return exponentFormat === 'SI' || exponentFormat === 'B'; } // are we beyond the range of common SI prefixes? // 10^-16 -> 1x10^-16 // 10^-15 -> 1f // ... // 10^14 -> 100T // 10^15 -> 1x10^15 // 10^16 -> 1x10^16 function beyondSI(exponent) { return exponent > 14 || exponent < -15; } function numFormat(v, ax, fmtoverride, hover) { var isNeg = v < 0; // max number of digits past decimal point to show var tickRound = ax._tickround; var exponentFormat = fmtoverride || ax.exponentformat || 'B'; var exponent = ax._tickexponent; var tickformat = axes.getTickFormat(ax); var separatethousands = ax.separatethousands; // special case for hover: set exponent just for this value, and // add a couple more digits of precision over tick labels if(hover) { // make a dummy axis obj to get the auto rounding and exponent var ah = { exponentformat: exponentFormat, minexponent: ax.minexponent, dtick: ax.showexponent === 'none' ? ax.dtick : (isNumeric(v) ? Math.abs(v) || 1 : 1), // if not showing any exponents, don't change the exponent // from what we calculate range: ax.showexponent === 'none' ? ax.range.map(ax.r2d) : [0, v || 1] }; autoTickRound(ah); tickRound = (Number(ah._tickround) || 0) + 4; exponent = ah._tickexponent; if(ax.hoverformat) tickformat = ax.hoverformat; } if(tickformat) return ax._numFormat(tickformat)(v).replace(/-/g, MINUS_SIGN); // 'epsilon' - rounding increment var e = Math.pow(10, -tickRound) / 2; // exponentFormat codes: // 'e' (1.2e+6, default) // 'E' (1.2E+6) // 'SI' (1.2M) // 'B' (same as SI except 10^9=B not G) // 'none' (1200000) // 'power' (1.2x10^6) // 'hide' (1.2, use 3rd argument=='hide' to eg // only show exponent on last tick) if(exponentFormat === 'none') exponent = 0; // take the sign out, put it back manually at the end // - makes cases easier v = Math.abs(v); if(v < e) { // 0 is just 0, but may get exponent if it's the last tick v = '0'; isNeg = false; } else { v += e; // take out a common exponent, if any if(exponent) { v *= Math.pow(10, -exponent); tickRound += exponent; } // round the mantissa if(tickRound === 0) v = String(Math.floor(v)); else if(tickRound < 0) { v = String(Math.round(v)); v = v.substr(0, v.length + tickRound); for(var i = tickRound; i < 0; i++) v += '0'; } else { v = String(v); var dp = v.indexOf('.') + 1; if(dp) v = v.substr(0, dp + tickRound).replace(/\.?0+$/, ''); } // insert appropriate decimal point and thousands separator v = Lib.numSeparate(v, ax._separators, separatethousands); } // add exponent if(exponent && exponentFormat !== 'hide') { if(isSIFormat(exponentFormat) && beyondSI(exponent)) exponentFormat = 'power'; var signedExponent; if(exponent < 0) signedExponent = MINUS_SIGN + -exponent; else if(exponentFormat !== 'power') signedExponent = '+' + exponent; else signedExponent = String(exponent); if(exponentFormat === 'e' || exponentFormat === 'E') { v += exponentFormat + signedExponent; } else if(exponentFormat === 'power') { v += '×10' + signedExponent + ''; } else if(exponentFormat === 'B' && exponent === 9) { v += 'B'; } else if(isSIFormat(exponentFormat)) { v += SIPREFIXES[exponent / 3 + 5]; } } // put sign back in and return // replace standard minus character (which is technically a hyphen) // with a true minus sign if(isNeg) return MINUS_SIGN + v; return v; } axes.getTickFormat = function(ax) { var i; function convertToMs(dtick) { return typeof dtick !== 'string' ? dtick : Number(dtick.replace('M', '')) * ONEAVGMONTH; } function compareLogTicks(left, right) { var priority = ['L', 'D']; if(typeof left === typeof right) { if(typeof left === 'number') { return left - right; } else { var leftPriority = priority.indexOf(left.charAt(0)); var rightPriority = priority.indexOf(right.charAt(0)); if(leftPriority === rightPriority) { return Number(left.replace(/(L|D)/g, '')) - Number(right.replace(/(L|D)/g, '')); } else { return leftPriority - rightPriority; } } } else { return typeof left === 'number' ? 1 : -1; } } function isProperStop(dtick, range, convert) { var convertFn = convert || function(x) { return x;}; var leftDtick = range[0]; var rightDtick = range[1]; return ((!leftDtick && typeof leftDtick !== 'number') || convertFn(leftDtick) <= convertFn(dtick)) && ((!rightDtick && typeof rightDtick !== 'number') || convertFn(rightDtick) >= convertFn(dtick)); } function isProperLogStop(dtick, range) { var isLeftDtickNull = range[0] === null; var isRightDtickNull = range[1] === null; var isDtickInRangeLeft = compareLogTicks(dtick, range[0]) >= 0; var isDtickInRangeRight = compareLogTicks(dtick, range[1]) <= 0; return (isLeftDtickNull || isDtickInRangeLeft) && (isRightDtickNull || isDtickInRangeRight); } var tickstop, stopi; if(ax.tickformatstops && ax.tickformatstops.length > 0) { switch(ax.type) { case 'date': case 'linear': { for(i = 0; i < ax.tickformatstops.length; i++) { stopi = ax.tickformatstops[i]; if(stopi.enabled && isProperStop(ax.dtick, stopi.dtickrange, convertToMs)) { tickstop = stopi; break; } } break; } case 'log': { for(i = 0; i < ax.tickformatstops.length; i++) { stopi = ax.tickformatstops[i]; if(stopi.enabled && isProperLogStop(ax.dtick, stopi.dtickrange)) { tickstop = stopi; break; } } break; } default: } } return tickstop ? tickstop.value : ax.tickformat; }; // getSubplots - extract all subplot IDs we need // as an array of items like 'xy', 'x2y', 'x2y2'... // sorted by x (x,x2,x3...) then y // optionally restrict to only subplots containing axis object ax // // NOTE: this is currently only used OUTSIDE plotly.js (toolpanel, webapp) // ideally we get rid of it there (or just copy this there) and remove it here axes.getSubplots = function(gd, ax) { var subplotObj = gd._fullLayout._subplots; var allSubplots = subplotObj.cartesian.concat(subplotObj.gl2d || []); var out = ax ? axes.findSubplotsWithAxis(allSubplots, ax) : allSubplots; out.sort(function(a, b) { var aParts = a.substr(1).split('y'); var bParts = b.substr(1).split('y'); if(aParts[0] === bParts[0]) return +aParts[1] - +bParts[1]; return +aParts[0] - +bParts[0]; }); return out; }; // find all subplots with axis 'ax' // NOTE: this is only used in axes.getSubplots (only used outside plotly.js) and // gl2d/convert (where it restricts axis subplots to only those with gl2d) axes.findSubplotsWithAxis = function(subplots, ax) { var axMatch = new RegExp( (ax._id.charAt(0) === 'x') ? ('^' + ax._id + 'y') : (ax._id + '$') ); var subplotsWithAx = []; for(var i = 0; i < subplots.length; i++) { var sp = subplots[i]; if(axMatch.test(sp)) subplotsWithAx.push(sp); } return subplotsWithAx; }; // makeClipPaths: prepare clipPaths for all single axes and all possible xy pairings axes.makeClipPaths = function(gd) { var fullLayout = gd._fullLayout; // for more info: https://github.com/plotly/plotly.js/issues/2595 if(fullLayout._hasOnlyLargeSploms) return; var fullWidth = {_offset: 0, _length: fullLayout.width, _id: ''}; var fullHeight = {_offset: 0, _length: fullLayout.height, _id: ''}; var xaList = axes.list(gd, 'x', true); var yaList = axes.list(gd, 'y', true); var clipList = []; var i, j; for(i = 0; i < xaList.length; i++) { clipList.push({x: xaList[i], y: fullHeight}); for(j = 0; j < yaList.length; j++) { if(i === 0) clipList.push({x: fullWidth, y: yaList[j]}); clipList.push({x: xaList[i], y: yaList[j]}); } } // selectors don't work right with camelCase tags, // have to use class instead // https://groups.google.com/forum/#!topic/d3-js/6EpAzQ2gU9I var axClips = fullLayout._clips.selectAll('.axesclip') .data(clipList, function(d) { return d.x._id + d.y._id; }); axClips.enter().append('clipPath') .classed('axesclip', true) .attr('id', function(d) { return 'clip' + fullLayout._uid + d.x._id + d.y._id; }) .append('rect'); axClips.exit().remove(); axClips.each(function(d) { d3.select(this).select('rect').attr({ x: d.x._offset || 0, y: d.y._offset || 0, width: d.x._length || 1, height: d.y._length || 1 }); }); }; /** * Main multi-axis drawing routine! * * @param {DOM element} gd : graph div * @param {string or array of strings} arg : polymorphic argument * @param {object} opts: * - @param {boolean} skipTitle : optional flag to skip axis title draw/update * * Signature 1: Axes.draw(gd, 'redraw') * use this to clear and redraw all axes on graph * * Signature 2: Axes.draw(gd, '') * use this to draw all axes on graph w/o the selectAll().remove() * of the 'redraw' signature * * Signature 3: Axes.draw(gd, [axId, axId2, ...]) * where the items are axis id string, * use this to update multiple axes in one call * * N.B draw updates: * - ax._r (stored range for use by zoom/pan) * - ax._rl (stored linearized range for use by zoom/pan) */ axes.draw = function(gd, arg, opts) { var fullLayout = gd._fullLayout; if(arg === 'redraw') { fullLayout._paper.selectAll('g.subplot').each(function(d) { var id = d[0]; var plotinfo = fullLayout._plots[id]; if(plotinfo) { var xa = plotinfo.xaxis; var ya = plotinfo.yaxis; plotinfo.xaxislayer.selectAll('.' + xa._id + 'tick').remove(); plotinfo.yaxislayer.selectAll('.' + ya._id + 'tick').remove(); plotinfo.xaxislayer.selectAll('.' + xa._id + 'tick2').remove(); plotinfo.yaxislayer.selectAll('.' + ya._id + 'tick2').remove(); plotinfo.xaxislayer.selectAll('.' + xa._id + 'divider').remove(); plotinfo.yaxislayer.selectAll('.' + ya._id + 'divider').remove(); if(plotinfo.minorGridlayer) plotinfo.minorGridlayer.selectAll('path').remove(); if(plotinfo.gridlayer) plotinfo.gridlayer.selectAll('path').remove(); if(plotinfo.zerolinelayer) plotinfo.zerolinelayer.selectAll('path').remove(); fullLayout._infolayer.select('.g-' + xa._id + 'title').remove(); fullLayout._infolayer.select('.g-' + ya._id + 'title').remove(); } }); } var axList = (!arg || arg === 'redraw') ? axes.listIds(gd) : arg; var fullAxList = axes.list(gd); // Get the list of the overlaying axis for all 'shift' axes var overlayingShiftedAx = fullAxList.filter(function(ax) { return ax.autoshift; }).map(function(ax) { return ax.overlaying; }); // order axes that have dependency to other axes axList.map(function(axId) { var ax = axes.getFromId(gd, axId); if(ax.tickmode === 'sync' && ax.overlaying) { var overlayingIndex = axList.findIndex(function(axis) {return axis === ax.overlaying;}); if(overlayingIndex >= 0) { axList.unshift(axList.splice(overlayingIndex, 1).shift()); } } }); var axShifts = {false: {left: 0, right: 0}}; return Lib.syncOrAsync(axList.map(function(axId) { return function() { if(!axId) return; var ax = axes.getFromId(gd, axId); if(!opts) opts = {}; opts.axShifts = axShifts; opts.overlayingShiftedAx = overlayingShiftedAx; var axDone = axes.drawOne(gd, ax, opts); if(ax._shiftPusher) { incrementShift(ax, ax._fullDepth || 0, axShifts, true); } ax._r = ax.range.slice(); ax._rl = Lib.simpleMap(ax._r, ax.r2l); return axDone; }; })); }; /** * Draw one cartesian axis * * @param {DOM element} gd * @param {object} ax (full) axis object * @param {object} opts * - @param {boolean} skipTitle (set to true to skip axis title draw call) * * Depends on: * - ax._mainSubplot (from linkSubplots) * - ax._mainAxis * - ax._anchorAxis * - ax._subplotsWith * - ax._counterDomainMin, ax._counterDomainMax (optionally, from linkSubplots) * - ax._tickAngles (on redraw only, old value relinked during supplyDefaults) * - ax._mainLinePosition (from lsInner) * - ax._mainMirrorPosition * - ax._linepositions * * Fills in: * - ax._vals: * - ax._gridVals: * - ax._selections: * - ax._tickAngles: * - ax._depth (when required only): * - and calls ax.setScale */ axes.drawOne = function(gd, ax, opts) { opts = opts || {}; var axShifts = opts.axShifts || {}; var overlayingShiftedAx = opts.overlayingShiftedAx || []; var i, sp, plotinfo; ax.setScale(); var fullLayout = gd._fullLayout; var axId = ax._id; var axLetter = axId.charAt(0); var counterLetter = axes.counterLetter(axId); var mainPlotinfo = fullLayout._plots[ax._mainSubplot]; // this happens when updating matched group with 'missing' axes if(!mainPlotinfo) return; ax._shiftPusher = ax.autoshift || overlayingShiftedAx.indexOf(ax._id) !== -1 || overlayingShiftedAx.indexOf(ax.overlaying) !== -1; // An axis is also shifted by 1/2 of its own linewidth and inside tick length if applicable // as well as its manually specified `shift` val if we're in the context of `autoshift` if(ax._shiftPusher & ax.anchor === 'free') { var selfPush = (ax.linewidth / 2 || 0); if(ax.ticks === 'inside') { selfPush += ax.ticklen; } incrementShift(ax, selfPush, axShifts, true); incrementShift(ax, (ax.shift || 0), axShifts, false); } // Somewhat inelegant way of making sure that the shift value is only updated when the // Axes.DrawOne() function is called from the right context. An issue when redrawing the // axis as result of using the dragbox, for example. if(opts.skipTitle !== true || ax._shift === undefined) ax._shift = setShiftVal(ax, axShifts); var mainAxLayer = mainPlotinfo[axLetter + 'axislayer']; var mainLinePosition = ax._mainLinePosition; var mainLinePositionShift = mainLinePosition += ax._shift; var mainMirrorPosition = ax._mainMirrorPosition; var vals = ax._vals = axes.calcTicks(ax); // Add a couple of axis properties that should cause us to recreate // elements. Used in d3 data function. var axInfo = [ax.mirror, mainLinePositionShift, mainMirrorPosition].join('_'); for(i = 0; i < vals.length; i++) { vals[i].axInfo = axInfo; } // stash selections to avoid DOM queries e.g. // - stash tickLabels selection, so that drawTitle can use it to scoot title ax._selections = {}; // stash tick angle (including the computed 'auto' values) per tick-label class // linkup 'previous' tick angles on redraws if(ax._tickAngles) ax._prevTickAngles = ax._tickAngles; ax._tickAngles = {}; // measure [in px] between axis position and outward-most part of bounding box // (touching either the tick label or ticks) // depth can be expansive to compute, so we only do so when required ax._depth = null; // calcLabelLevelBbox can be expensive, // so make sure to not call it twice during the same Axes.drawOne call // by stashing label-level bounding boxes per tick-label class var llbboxes = {}; function getLabelLevelBbox(suffix) { var cls = axId + (suffix || 'tick'); if(!llbboxes[cls]) llbboxes[cls] = calcLabelLevelBbox(ax, cls, mainLinePositionShift); return llbboxes[cls]; } if(!ax.visible) return; var transTickFn = axes.makeTransTickFn(ax); var transTickLabelFn = axes.makeTransTickLabelFn(ax); var tickVals; // We remove zero lines, grid lines, and inside ticks if they're within 1px of the end // The key case here is removing zero lines when the axis bound is zero var valsClipped; var insideTicks = ax.ticks === 'inside'; var outsideTicks = ax.ticks === 'outside'; if(ax.tickson === 'boundaries') { var boundaryVals = getBoundaryVals(ax, vals); valsClipped = axes.clipEnds(ax, boundaryVals); tickVals = insideTicks ? valsClipped : boundaryVals; } else { valsClipped = axes.clipEnds(ax, vals); tickVals = (insideTicks && ax.ticklabelmode !== 'period') ? valsClipped : vals; } var gridVals = ax._gridVals = valsClipped; var dividerVals = getDividerVals(ax, vals); if(!fullLayout._hasOnlyLargeSploms) { var subplotsWithAx = ax._subplotsWith; // keep track of which subplots (by main counter axis) we've already // drawn grids for, so we don't overdraw overlaying subplots var finishedGrids = {}; for(i = 0; i < subplotsWithAx.length; i++) { sp = subplotsWithAx[i]; plotinfo = fullLayout._plots[sp]; var counterAxis = plotinfo[counterLetter + 'axis']; var mainCounterID = counterAxis._mainAxis._id; if(finishedGrids[mainCounterID]) continue; finishedGrids[mainCounterID] = 1; var gridPath = axLetter === 'x' ? 'M0,' + counterAxis._offset + 'v' + counterAxis._length : 'M' + counterAxis._offset + ',0h' + counterAxis._length; axes.drawGrid(gd, ax, { vals: gridVals, counterAxis: counterAxis, layer: plotinfo.gridlayer.select('.' + axId), minorLayer: plotinfo.minorGridlayer.select('.' + axId), path: gridPath, transFn: transTickFn }); axes.drawZeroLine(gd, ax, { counterAxis: counterAxis, layer: plotinfo.zerolinelayer, path: gridPath, transFn: transTickFn }); } } var tickPath; var majorTickSigns = axes.getTickSigns(ax); var minorTickSigns = axes.getTickSigns(ax, 'minor'); if(ax.ticks || (ax.minor && ax.minor.ticks)) { var majorTickPath = axes.makeTickPath(ax, mainLinePositionShift, majorTickSigns[2]); var minorTickPath = axes.makeTickPath(ax, mainLinePositionShift, minorTickSigns[2], { minor: true }); var mirrorMajorTickPath; var mirrorMinorTickPath; var fullMajorTickPath; var fullMinorTickPath; if(ax._anchorAxis && ax.mirror && ax.mirror !== true) { mirrorMajorTickPath = axes.makeTickPath(ax, mainMirrorPosition, majorTickSigns[3]); mirrorMinorTickPath = axes.makeTickPath(ax, mainMirrorPosition, minorTickSigns[3], { minor: true }); fullMajorTickPath = majorTickPath + mirrorMajorTickPath; fullMinorTickPath = minorTickPath + mirrorMinorTickPath; } else { mirrorMajorTickPath = ''; mirrorMinorTickPath = ''; fullMajorTickPath = majorTickPath; fullMinorTickPath = minorTickPath; } if(ax.showdividers && outsideTicks && ax.tickson === 'boundaries') { var dividerLookup = {}; for(i = 0; i < dividerVals.length; i++) { dividerLookup[dividerVals[i].x] = 1; } tickPath = function(d) { return dividerLookup[d.x] ? mirrorMajorTickPath : fullMajorTickPath; }; } else { tickPath = function(d) { return d.minor ? fullMinorTickPath : fullMajorTickPath; }; } } axes.drawTicks(gd, ax, { vals: tickVals, layer: mainAxLayer, path: tickPath, transFn: transTickFn }); if(ax.mirror === 'allticks') { var tickSubplots = Object.keys(ax._linepositions || {}); for(i = 0; i < tickSubplots.length; i++) { sp = tickSubplots[i]; plotinfo = fullLayout._plots[sp]; // [bottom or left, top or right], free and main are handled above var linepositions = ax._linepositions[sp] || []; var p0 = linepositions[0]; var p1 = linepositions[1]; var isMinor = linepositions[2]; var spTickPath = axes.makeTickPath(ax, p0, isMinor ? majorTickSigns[0] : minorTickSigns[0], { minor: isMinor } ) + axes.makeTickPath(ax, p1, isMinor ? majorTickSigns[1] : minorTickSigns[1], { minor: isMinor } ); axes.drawTicks(gd, ax, { vals: tickVals, layer: plotinfo[axLetter + 'axislayer'], path: spTickPath, transFn: transTickFn }); } } var seq = []; // tick labels - for now just the main labels. // TODO: mirror labels, esp for subplots seq.push(function() { return axes.drawLabels(gd, ax, { vals: vals, layer: mainAxLayer, plotinfo: plotinfo, transFn: transTickLabelFn, labelFns: axes.makeLabelFns(ax, mainLinePositionShift) }); }); if(ax.type === 'multicategory') { var pad = {x: 2, y: 10}[axLetter]; seq.push(function() { var bboxKey = {x: 'height', y: 'width'}[axLetter]; var standoff = getLabelLevelBbox()[bboxKey] + pad + (ax._tickAngles[axId + 'tick'] ? ax.tickfont.size * LINE_SPACING : 0); return axes.drawLabels(gd, ax, { vals: getSecondaryLabelVals(ax, vals), layer: mainAxLayer, cls: axId + 'tick2', repositionOnUpdate: true, secondary: true, transFn: transTickFn, labelFns: axes.makeLabelFns(ax, mainLinePositionShift + standoff * majorTickSigns[4]) }); }); seq.push(function() { ax._depth = majorTickSigns[4] * (getLabelLevelBbox('tick2')[ax.side] - mainLinePositionShift); return drawDividers(gd, ax, { vals: dividerVals, layer: mainAxLayer, path: axes.makeTickPath(ax, mainLinePositionShift, majorTickSigns[4], { len: ax._depth }), transFn: transTickFn }); }); } else if(ax.title.hasOwnProperty('standoff')) { seq.push(function() { ax._depth = majorTickSigns[4] * (getLabelLevelBbox()[ax.side] - mainLinePositionShift); }); } var hasRangeSlider = Registry.getComponentMethod('rangeslider', 'isVisible')(ax); if(!opts.skipTitle && !(hasRangeSlider && ax.side === 'bottom') ) { seq.push(function() { return drawTitle(gd, ax); }); } seq.push(function() { var s = ax.side.charAt(0); var sMirror = OPPOSITE_SIDE[ax.side].charAt(0); var pos = axes.getPxPosition(gd, ax); var outsideTickLen = outsideTicks ? ax.ticklen : 0; var llbbox; var push; var mirrorPush; var rangeSliderPush; if(ax.automargin || hasRangeSlider || ax._shiftPusher) { if(ax.type === 'multicategory') { llbbox = getLabelLevelBbox('tick2'); } else { llbbox = getLabelLevelBbox(); if(axLetter === 'x' && s === 'b') { ax._depth = Math.max(llbbox.width > 0 ? llbbox.bottom - pos : 0, outsideTickLen); } } } var axDepth = 0; var titleDepth = 0; if(ax._shiftPusher) { axDepth = Math.max( outsideTickLen, llbbox.height > 0 ? (s === 'l' ? pos - llbbox.left : llbbox.right - pos) : 0 ); if(ax.title.text !== fullLayout._dfltTitle[axLetter]) { titleDepth = (ax._titleStandoff || 0) + (ax._titleScoot || 0); if(s === 'l') { titleDepth += approxTitleDepth(ax); } } ax._fullDepth = Math.max(axDepth, titleDepth); } if(ax.automargin) { push = {x: 0, y: 0, r: 0, l: 0, t: 0, b: 0}; var domainIndices = [0, 1]; var shift = typeof ax._shift === 'number' ? ax._shift : 0; if(axLetter === 'x') { if(s === 'b') { push[s] = ax._depth; } else { push[s] = ax._depth = Math.max(llbbox.width > 0 ? pos - llbbox.top : 0, outsideTickLen); domainIndices.reverse(); } if(llbbox.width > 0) { var rExtra = llbbox.right - (ax._offset + ax._length); if(rExtra > 0) { push.xr = 1; push.r = rExtra; } var lExtra = ax._offset - llbbox.left; if(lExtra > 0) { push.xl = 0; push.l = lExtra; } } } else { if(s === 'l') { ax._depth = Math.max(llbbox.height > 0 ? pos - llbbox.left : 0, outsideTickLen); push[s] = ax._depth - shift; } else { ax._depth = Math.max(llbbox.height > 0 ? llbbox.right - pos : 0, outsideTickLen); push[s] = ax._depth + shift; domainIndices.reverse(); } if(llbbox.height > 0) { var bExtra = llbbox.bottom - (ax._offset + ax._length); if(bExtra > 0) { push.yb = 0; push.b = bExtra; } var tExtra = ax._offset - llbbox.top; if(tExtra > 0) { push.yt = 1; push.t = tExtra; } } } push[counterLetter] = ax.anchor === 'free' ? ax.position : ax._anchorAxis.domain[domainIndices[0]]; if(ax.title.text !== fullLayout._dfltTitle[axLetter]) { push[s] += approxTitleDepth(ax) + (ax.title.standoff || 0); } if(ax.mirror && ax.anchor !== 'free') { mirrorPush = {x: 0, y: 0, r: 0, l: 0, t: 0, b: 0}; mirrorPush[sMirror] = ax.linewidth; if(ax.mirror && ax.mirror !== true) mirrorPush[sMirror] += outsideTickLen; if(ax.mirror === true || ax.mirror === 'ticks') { mirrorPush[counterLetter] = ax._anchorAxis.domain[domainIndices[1]]; } else if(ax.mirror === 'all' || ax.mirror === 'allticks') { mirrorPush[counterLetter] = [ax._counterDomainMin, ax._counterDomainMax][domainIndices[1]]; } } } if(hasRangeSlider) { rangeSliderPush = Registry.getComponentMethod('rangeslider', 'autoMarginOpts')(gd, ax); } if(typeof ax.automargin === 'string') { filterPush(push, ax.automargin); filterPush(mirrorPush, ax.automargin); } Plots.autoMargin(gd, axAutoMarginID(ax), push); Plots.autoMargin(gd, axMirrorAutoMarginID(ax), mirrorPush); Plots.autoMargin(gd, rangeSliderAutoMarginID(ax), rangeSliderPush); }); return Lib.syncOrAsync(seq); }; function filterPush(push, automargin) { if(!push) return; var keepMargin = Object.keys(MARGIN_MAPPING).reduce(function(data, nextKey) { if(automargin.indexOf(nextKey) !== -1) { MARGIN_MAPPING[nextKey].forEach(function(key) { data[key] = 1;}); } return data; }, {}); Object.keys(push).forEach(function(key) { if(!keepMargin[key]) { if(key.length === 1) push[key] = 0; else delete push[key]; } }); } function getBoundaryVals(ax, vals) { var out = []; var i; // boundaryVals are never used for labels; // no need to worry about the other tickTextObj keys var _push = function(d, bndIndex) { var xb = d.xbnd[bndIndex]; if(xb !== null) { out.push(Lib.extendFlat({}, d, {x: xb})); } }; if(vals.length) { for(i = 0; i < vals.length; i++) { _push(vals[i], 0); } _push(vals[i - 1], 1); } return out; } function getSecondaryLabelVals(ax, vals) { var out = []; var lookup = {}; for(var i = 0; i < vals.length; i++) { var d = vals[i]; if(lookup[d.text2]) { lookup[d.text2].push(d.x); } else { lookup[d.text2] = [d.x]; } } for(var k in lookup) { out.push(tickTextObj(ax, Lib.interp(lookup[k], 0.5), k)); } return out; } function getDividerVals(ax, vals) { var out = []; var i, current; var reversed = (vals.length && vals[vals.length - 1].x < vals[0].x); // never used for labels; // no need to worry about the other tickTextObj keys var _push = function(d, bndIndex) { var xb = d.xbnd[bndIndex]; if(xb !== null) { out.push(Lib.extendFlat({}, d, {x: xb})); } }; if(ax.showdividers && vals.length) { for(i = 0; i < vals.length; i++) { var d = vals[i]; if(d.text2 !== current) { _push(d, reversed ? 1 : 0); } current = d.text2; } _push(vals[i - 1], reversed ? 0 : 1); } return out; } function calcLabelLevelBbox(ax, cls, mainLinePositionShift) { var top, bottom; var left, right; if(ax._selections[cls].size()) { top = Infinity; bottom = -Infinity; left = Infinity; right = -Infinity; ax._selections[cls].each(function() { var thisLabel = selectTickLabel(this); // Use parent node , to make Drawing.bBox // retrieve a bbox computed with transform info // // To improve perf, it would be nice to use `thisLabel.node()` // (like in fixLabelOverlaps) instead and use Axes.getPxPosition // together with the makeLabelFns outputs and `tickangle` // to compute one bbox per (tick value x tick style) var bb = Drawing.bBox(thisLabel.node().parentNode); top = Math.min(top, bb.top); bottom = Math.max(bottom, bb.bottom); left = Math.min(left, bb.left); right = Math.max(right, bb.right); }); } else { var dummyCalc = axes.makeLabelFns(ax, mainLinePositionShift); top = bottom = dummyCalc.yFn({dx: 0, dy: 0, fontSize: 0}); left = right = dummyCalc.xFn({dx: 0, dy: 0, fontSize: 0}); } return { top: top, bottom: bottom, left: left, right: right, height: bottom - top, width: right - left }; } /** * Which direction do the 'ax.side' values, and free ticks go? * * @param {object} ax (full) axis object * - {string} _id (starting with 'x' or 'y') * - {string} side * - {string} ticks * @return {array} all entries are either -1 or 1 * - [0]: sign for top/right ticks (i.e. negative SVG direction) * - [1]: sign for bottom/left ticks (i.e. positive SVG direction) * - [2]: sign for ticks corresponding to 'ax.side' * - [3]: sign for ticks mirroring 'ax.side' * - [4]: sign of arrow starting at axis pointing towards margin */ axes.getTickSigns = function(ax, minor) { var axLetter = ax._id.charAt(0); var sideOpposite = {x: 'top', y: 'right'}[axLetter]; var main = ax.side === sideOpposite ? 1 : -1; var out = [-1, 1, main, -main]; // then we flip if outside XOR y axis var ticks = minor ? (ax.minor || {}).ticks : ax.ticks; if((ticks !== 'inside') === (axLetter === 'x')) { out = out.map(function(v) { return -v; }); } // independent of `ticks`; do not flip this one if(ax.side) { out.push({l: -1, t: -1, r: 1, b: 1}[ax.side.charAt(0)]); } return out; }; /** * Make axis translate transform function * * @param {object} ax (full) axis object * - {string} _id * - {number} _offset * - {fn} l2p * @return {fn} function of calcTicks items */ axes.makeTransTickFn = function(ax) { return ax._id.charAt(0) === 'x' ? function(d) { return strTranslate(ax._offset + ax.l2p(d.x), 0); } : function(d) { return strTranslate(0, ax._offset + ax.l2p(d.x)); }; }; axes.makeTransTickLabelFn = function(ax) { var uv = getTickLabelUV(ax); var shift = ax.ticklabelshift || 0; var standoff = ax.ticklabelstandoff || 0; var u = uv[0]; var v = uv[1]; var isReversed = ax.range[0] > ax.range[1]; var labelsInside = ax.ticklabelposition && ax.ticklabelposition.indexOf('inside') !== -1; var labelsOutside = !labelsInside; if(shift) { var shiftSign = isReversed ? -1 : 1; shift = shift * shiftSign; } if(standoff) { var side = ax.side; var standoffSign = ( (labelsInside && (side === 'top' || side === 'left')) || (labelsOutside && (side === 'bottom' || side === 'right')) ) ? 1 : -1; standoff = standoff * standoffSign; } return ax._id.charAt(0) === 'x' ? function(d) { return strTranslate( u + ax._offset + ax.l2p(getPosX(d)) + shift, v + standoff ); } : function(d) { return strTranslate( v + standoff, u + ax._offset + ax.l2p(getPosX(d)) + shift ); }; }; function getPosX(d) { return d.periodX !== undefined ? d.periodX : d.x; } // u is a shift along the axis, // v is a shift perpendicular to the axis function getTickLabelUV(ax) { var ticklabelposition = ax.ticklabelposition || ''; var has = function(str) { return ticklabelposition.indexOf(str) !== -1; }; var isTop = has('top'); var isLeft = has('left'); var isRight = has('right'); var isBottom = has('bottom'); var isInside = has('inside'); var isAligned = isBottom || isLeft || isTop || isRight; // early return if(!isAligned && !isInside) return [0, 0]; var side = ax.side; var u = isAligned ? (ax.tickwidth || 0) / 2 : 0; var v = TEXTPAD; var fontSize = ax.tickfont ? ax.tickfont.size : 12; if(isBottom || isTop) { u += fontSize * CAP_SHIFT; v += (ax.linewidth || 0) / 2; } if(isLeft || isRight) { u += (ax.linewidth || 0) / 2; v += TEXTPAD; } if(isInside && side === 'top') { v -= fontSize * (1 - CAP_SHIFT); } if(isLeft || isTop) u = -u; if(side === 'bottom' || side === 'right') v = -v; return [ isAligned ? u : 0, isInside ? v : 0 ]; } /** * Make axis tick path string * * @param {object} ax (full) axis object * - {string} _id * - {number} ticklen * - {number} linewidth * @param {number} shift along direction of ticklen * @param {1 or -1} sgn tick sign * @param {object} opts * - {number (optional)} len tick length * @return {string} */ axes.makeTickPath = function(ax, shift, sgn, opts) { if(!opts) opts = {}; var minor = opts.minor; if(minor && !ax.minor) return ''; var len = opts.len !== undefined ? opts.len : minor ? ax.minor.ticklen : ax.ticklen; var axLetter = ax._id.charAt(0); var pad = (ax.linewidth || 1) / 2; return axLetter === 'x' ? 'M0,' + (shift + pad * sgn) + 'v' + (len * sgn) : 'M' + (shift + pad * sgn) + ',0h' + (len * sgn); }; /** * Make axis tick label x, y and anchor functions * * @param {object} ax (full) axis object * - {string} _id * - {string} ticks * - {number} ticklen * - {string} side * - {number} linewidth * - {number} tickfont.size * - {boolean} showline * @param {number} shift * @param {number} angle [in degrees] ... * @return {object} * - {fn} xFn * - {fn} yFn * - {fn} anchorFn * - {fn} heightFn * - {number} labelStandoff (gap parallel to ticks) * - {number} labelShift (gap perpendicular to ticks) */ axes.makeLabelFns = function(ax, shift, angle) { var ticklabelposition = ax.ticklabelposition || ''; var has = function(str) { return ticklabelposition.indexOf(str) !== -1; }; var isTop = has('top'); var isLeft = has('left'); var isRight = has('right'); var isBottom = has('bottom'); var isAligned = isBottom || isLeft || isTop || isRight; var insideTickLabels = has('inside'); var labelsOverTicks = (ticklabelposition === 'inside' && ax.ticks === 'inside') || (!insideTickLabels && ax.ticks === 'outside' && ax.tickson !== 'boundaries'); var labelStandoff = 0; var labelShift = 0; var tickLen = labelsOverTicks ? ax.ticklen : 0; if(insideTickLabels) { tickLen *= -1; } else if(isAligned) { tickLen = 0; } if(labelsOverTicks) { labelStandoff += tickLen; if(angle) { var rad = Lib.deg2rad(angle); labelStandoff = tickLen * Math.cos(rad) + 1; labelShift = tickLen * Math.sin(rad); } } if(ax.showticklabels && (labelsOverTicks || ax.showline)) { labelStandoff += 0.2 * ax.tickfont.size; } labelStandoff += (ax.linewidth || 1) / 2 * (insideTickLabels ? -1 : 1); var out = { labelStandoff: labelStandoff, labelShift: labelShift }; var x0, y0, ff, flipIt; var xQ = 0; var side = ax.side; var axLetter = ax._id.charAt(0); var tickangle = ax.tickangle; var endSide; if(axLetter === 'x') { endSide = (!insideTickLabels && side === 'bottom') || (insideTickLabels && side === 'top'); flipIt = endSide ? 1 : -1; if(insideTickLabels) flipIt *= -1; x0 = labelShift * flipIt; y0 = shift + labelStandoff * flipIt; ff = endSide ? 1 : -0.2; if(Math.abs(tickangle) === 90) { if(insideTickLabels) { ff += MID_SHIFT; } else { if(tickangle === -90 && side === 'bottom') { ff = CAP_SHIFT; } else if(tickangle === 90 && side === 'top') { ff = MID_SHIFT; } else { ff = 0.5; } } xQ = (MID_SHIFT / 2) * (tickangle / 90); } out.xFn = function(d) { return d.dx + x0 + xQ * d.fontSize; }; out.yFn = function(d) { return d.dy + y0 + d.fontSize * ff; }; out.anchorFn = function(d, a) { if(isAligned) { if(isLeft) return 'end'; if(isRight) return 'start'; } if(!isNumeric(a) || a === 0 || a === 180) { return 'middle'; } return ((a * flipIt < 0) !== insideTickLabels) ? 'end' : 'start'; }; out.heightFn = function(d, a, h) { return (a < -60 || a > 60) ? -0.5 * h : ((ax.side === 'top') !== insideTickLabels) ? -h : 0; }; } else if(axLetter === 'y') { endSide = (!insideTickLabels && side === 'left') || (insideTickLabels && side === 'right'); flipIt = endSide ? 1 : -1; if(insideTickLabels) flipIt *= -1; x0 = labelStandoff; y0 = labelShift * flipIt; ff = 0; if(!insideTickLabels && Math.abs(tickangle) === 90) { if( (tickangle === -90 && side === 'left') || (tickangle === 90 && side === 'right') ) { ff = CAP_SHIFT; } else { ff = 0.5; } } if(insideTickLabels) { var ang = isNumeric(tickangle) ? +tickangle : 0; if(ang !== 0) { var rA = Lib.deg2rad(ang); xQ = Math.abs(Math.sin(rA)) * CAP_SHIFT * flipIt; ff = 0; } } out.xFn = function(d) { return d.dx + shift - (x0 + d.fontSize * ff) * flipIt + xQ * d.fontSize; }; out.yFn = function(d) { return d.dy + y0 + d.fontSize * MID_SHIFT; }; out.anchorFn = function(d, a) { if(isNumeric(a) && Math.abs(a) === 90) { return 'middle'; } return endSide ? 'end' : 'start'; }; out.heightFn = function(d, a, h) { if(ax.side === 'right') a *= -1; return a < -30 ? -h : a < 30 ? -0.5 * h : 0; }; } return out; }; function tickDataFn(d) { return [d.text, d.x, d.axInfo, d.font, d.fontSize, d.fontColor].join('_'); } /** * Draw axis ticks * * @param {DOM element} gd * @param {object} ax (full) axis object * - {string} _id * - {string} ticks * - {number} linewidth * - {string} tickcolor * @param {object} opts * - {array of object} vals (calcTicks output-like) * - {d3 selection} layer * - {string or fn} path * - {fn} transFn * - {boolean} crisp (set to false to unset crisp-edge SVG rendering) */ axes.drawTicks = function(gd, ax, opts) { opts = opts || {}; var cls = ax._id + 'tick'; var vals = [] .concat(ax.minor && ax.minor.ticks ? // minor vals opts.vals.filter(function(d) { return d.minor && !d.noTick; }) : [] ) .concat(ax.ticks ? // major vals opts.vals.filter(function(d) { return !d.minor && !d.noTick; }) : [] ); var ticks = opts.layer.selectAll('path.' + cls) .data(vals, tickDataFn); ticks.exit().remove(); ticks.enter().append('path') .classed(cls, 1) .classed('ticks', 1) .classed('crisp', opts.crisp !== false) .each(function(d) { return Color.stroke(d3.select(this), d.minor ? ax.minor.tickcolor : ax.tickcolor); }) .style('stroke-width', function(d) { return Drawing.crispRound( gd, d.minor ? ax.minor.tickwidth : ax.tickwidth, 1 ) + 'px'; }) .attr('d', opts.path) .style('display', null); // visible hideCounterAxisInsideTickLabels(ax, [TICK_PATH]); ticks.attr('transform', opts.transFn); }; /** * Draw axis grid * * @param {DOM element} gd * @param {object} ax (full) axis object * - {string} _id * - {boolean} showgrid * - {string} gridcolor * - {string} gridwidth * - {string} griddash * - {boolean} zeroline * - {string} type * - {string} dtick * @param {object} opts * - {array of object} vals (calcTicks output-like) * - {d3 selection} layer * - {object} counterAxis (full axis object corresponding to counter axis) * optional - only required if this axis supports zero lines * - {string or fn} path * - {fn} transFn * - {boolean} crisp (set to false to unset crisp-edge SVG rendering) */ axes.drawGrid = function(gd, ax, opts) { opts = opts || {}; if(ax.tickmode === 'sync') { // for tickmode sync we use the overlaying axis grid return; } var cls = ax._id + 'grid'; var hasMinor = ax.minor && ax.minor.showgrid; var minorVals = hasMinor ? opts.vals.filter(function(d) { return d.minor; }) : []; var majorVals = ax.showgrid ? opts.vals.filter(function(d) { return !d.minor; }) : []; var counterAx = opts.counterAxis; if(counterAx && axes.shouldShowZeroLine(gd, ax, counterAx)) { var isArrayMode = ax.tickmode === 'array'; for(var i = 0; i < majorVals.length; i++) { var xi = majorVals[i].x; if(isArrayMode ? !xi : (Math.abs(xi) < ax.dtick / 100)) { majorVals = majorVals.slice(0, i).concat(majorVals.slice(i + 1)); // In array mode you can in principle have multiple // ticks at 0, so test them all. Otherwise once we found // one we can stop. if(isArrayMode) i--; else break; } } } ax._gw = Drawing.crispRound(gd, ax.gridwidth, 1); var wMinor = !hasMinor ? 0 : Drawing.crispRound(gd, ax.minor.gridwidth, 1); var majorLayer = opts.layer; var minorLayer = opts.minorLayer; for(var major = 1; major >= 0; major--) { var layer = major ? majorLayer : minorLayer; if(!layer) continue; var grid = layer.selectAll('path.' + cls) .data(major ? majorVals : minorVals, tickDataFn); grid.exit().remove(); grid.enter().append('path') .classed(cls, 1) .classed('crisp', opts.crisp !== false); grid.attr('transform', opts.transFn) .attr('d', opts.path) .each(function(d) { return Color.stroke(d3.select(this), d.minor ? ax.minor.gridcolor : (ax.gridcolor || '#ddd') ); }) .style('stroke-dasharray', function(d) { return Drawing.dashStyle( d.minor ? ax.minor.griddash : ax.griddash, d.minor ? ax.minor.gridwidth : ax.gridwidth ); }) .style('stroke-width', function(d) { return (d.minor ? wMinor : ax._gw) + 'px'; }) .style('display', null); // visible if(typeof opts.path === 'function') grid.attr('d', opts.path); } hideCounterAxisInsideTickLabels(ax, [GRID_PATH, MINORGRID_PATH]); }; /** * Draw axis zero-line * * @param {DOM element} gd * @param {object} ax (full) axis object * - {string} _id * - {boolean} zeroline * - {number} zerolinewidth * - {string} zerolinecolor * - {number (optional)} _gridWidthCrispRound * @param {object} opts * - {d3 selection} layer * - {object} counterAxis (full axis object corresponding to counter axis) * - {string or fn} path * - {fn} transFn * - {boolean} crisp (set to false to unset crisp-edge SVG rendering) */ axes.drawZeroLine = function(gd, ax, opts) { opts = opts || opts; var cls = ax._id + 'zl'; var show = axes.shouldShowZeroLine(gd, ax, opts.counterAxis); var zl = opts.layer.selectAll('path.' + cls) .data(show ? [{x: 0, id: ax._id}] : []); zl.exit().remove(); zl.enter().append('path') .classed(cls, 1) .classed('zl', 1) .classed('crisp', opts.crisp !== false) .each(function() { // use the fact that only one element can enter to trigger a sort. // If several zerolines enter at the same time we will sort once per, // but generally this should be a minimal overhead. opts.layer.selectAll('path').sort(function(da, db) { return idSort(da.id, db.id); }); }); zl.attr('transform', opts.transFn) .attr('d', opts.path) .call(Color.stroke, ax.zerolinecolor || Color.defaultLine) .style('stroke-width', Drawing.crispRound(gd, ax.zerolinewidth, ax._gw || 1) + 'px') .style('display', null); // visible hideCounterAxisInsideTickLabels(ax, [ZERO_PATH]); }; /** * Draw axis tick labels * * @param {DOM element} gd * @param {object} ax (full) axis object * - {string} _id * - {boolean} showticklabels * - {number} tickangle * - {object (optional)} _selections * - {object} (optional)} _tickAngles * - {object} (optional)} _prevTickAngles * @param {object} opts * - {array of object} vals (calcTicks output-like) * - {d3 selection} layer * - {string (optional)} cls (node className) * - {boolean} repositionOnUpdate (set to true to reposition update selection) * - {boolean} secondary * - {fn} transFn * - {object} labelFns * + {fn} xFn * + {fn} yFn * + {fn} anchorFn * + {fn} heightFn */ axes.drawLabels = function(gd, ax, opts) { opts = opts || {}; var fullLayout = gd._fullLayout; var axId = ax._id; var cls = opts.cls || axId + 'tick'; var vals = opts.vals.filter(function(d) { return d.text; }); var labelFns = opts.labelFns; var tickAngle = opts.secondary ? 0 : ax.tickangle; var prevAngle = (ax._prevTickAngles || {})[cls]; var tickLabels = opts.layer.selectAll('g.' + cls) .data(ax.showticklabels ? vals : [], tickDataFn); var labelsReady = []; tickLabels.enter().append('g') .classed(cls, 1) .append('text') // only so tex has predictable alignment that we can // alter later .attr('text-anchor', 'middle') .each(function(d) { var thisLabel = d3.select(this); var newPromise = gd._promises.length; thisLabel .call(svgTextUtils.positionText, labelFns.xFn(d), labelFns.yFn(d)) .call(Drawing.font, { family: d.font, size: d.fontSize, color: d.fontColor, weight: d.fontWeight, style: d.fontStyle, variant: d.fontVariant, textcase: d.fontTextcase, lineposition: d.fontLineposition, shadow: d.fontShadow, }) .text(d.text) .call(svgTextUtils.convertToTspans, gd); if(gd._promises[newPromise]) { // if we have an async label, we'll deal with that // all here so take it out of gd._promises and // instead position the label and promise this in // labelsReady labelsReady.push(gd._promises.pop().then(function() { positionLabels(thisLabel, tickAngle); })); } else { // sync label: just position it now. positionLabels(thisLabel, tickAngle); } }); hideCounterAxisInsideTickLabels(ax, [TICK_TEXT]); tickLabels.exit().remove(); if(opts.repositionOnUpdate) { tickLabels.each(function(d) { d3.select(this).select('text') .call(svgTextUtils.positionText, labelFns.xFn(d), labelFns.yFn(d)); }); } function positionLabels(s, angle) { s.each(function(d) { var thisLabel = d3.select(this); var mathjaxGroup = thisLabel.select('.text-math-group'); var anchor = labelFns.anchorFn(d, angle); var transform = opts.transFn.call(thisLabel.node(), d) + ((isNumeric(angle) && +angle !== 0) ? (' rotate(' + angle + ',' + labelFns.xFn(d) + ',' + (labelFns.yFn(d) - d.fontSize / 2) + ')') : ''); // how much to shift a multi-line label to center it vertically. var nLines = svgTextUtils.lineCount(thisLabel); var lineHeight = LINE_SPACING * d.fontSize; var anchorHeight = labelFns.heightFn(d, isNumeric(angle) ? +angle : 0, (nLines - 1) * lineHeight); if(anchorHeight) { transform += strTranslate(0, anchorHeight); } if(mathjaxGroup.empty()) { var thisText = thisLabel.select('text'); thisText.attr({ transform: transform, 'text-anchor': anchor }); thisText.style('opacity', 1); // visible if(ax._adjustTickLabelsOverflow) { ax._adjustTickLabelsOverflow(); } } else { var mjWidth = Drawing.bBox(mathjaxGroup.node()).width; var mjShift = mjWidth * {end: -0.5, start: 0.5}[anchor]; mathjaxGroup.attr('transform', transform + strTranslate(mjShift, 0)); } }); } ax._adjustTickLabelsOverflow = function() { var ticklabeloverflow = ax.ticklabeloverflow; if(!ticklabeloverflow || ticklabeloverflow === 'allow') return; var hideOverflow = ticklabeloverflow.indexOf('hide') !== -1; var isX = ax._id.charAt(0) === 'x'; // div positions var p0 = 0; var p1 = isX ? gd._fullLayout.width : gd._fullLayout.height; if(ticklabeloverflow.indexOf('domain') !== -1) { // domain positions var rl = Lib.simpleMap(ax.range, ax.r2l); p0 = ax.l2p(rl[0]) + ax._offset; p1 = ax.l2p(rl[1]) + ax._offset; } var min = Math.min(p0, p1); var max = Math.max(p0, p1); var side = ax.side; var visibleLabelMin = Infinity; var visibleLabelMax = -Infinity; tickLabels.each(function(d) { var thisLabel = d3.select(this); var mathjaxGroup = thisLabel.select('.text-math-group'); if(mathjaxGroup.empty()) { var bb = Drawing.bBox(thisLabel.node()); var adjust = 0; if(isX) { if(bb.right > max) adjust = 1; else if(bb.left < min) adjust = 1; } else { if(bb.bottom > max) adjust = 1; else if(bb.top + (ax.tickangle ? 0 : d.fontSize / 4) < min) adjust = 1; } var t = thisLabel.select('text'); if(adjust) { if(hideOverflow) t.style('opacity', 0); // hidden } else { t.style('opacity', 1); // visible if(side === 'bottom' || side === 'right') { visibleLabelMin = Math.min(visibleLabelMin, isX ? bb.top : bb.left); } else { visibleLabelMin = -Infinity; } if(side === 'top' || side === 'left') { visibleLabelMax = Math.max(visibleLabelMax, isX ? bb.bottom : bb.right); } else { visibleLabelMax = Infinity; } } } // TODO: hide mathjax? }); for(var subplot in fullLayout._plots) { var plotinfo = fullLayout._plots[subplot]; if(ax._id !== plotinfo.xaxis._id && ax._id !== plotinfo.yaxis._id) continue; var anchorAx = isX ? plotinfo.yaxis : plotinfo.xaxis; if(anchorAx) { anchorAx['_visibleLabelMin_' + ax._id] = visibleLabelMin; anchorAx['_visibleLabelMax_' + ax._id] = visibleLabelMax; } } }; ax._hideCounterAxisInsideTickLabels = function(partialOpts) { var isX = ax._id.charAt(0) === 'x'; var anchoredAxes = []; for(var subplot in fullLayout._plots) { var plotinfo = fullLayout._plots[subplot]; if(ax._id !== plotinfo.xaxis._id && ax._id !== plotinfo.yaxis._id) continue; anchoredAxes.push(isX ? plotinfo.yaxis : plotinfo.xaxis); } anchoredAxes.forEach(function(anchorAx, idx) { if(anchorAx && insideTicklabelposition(anchorAx)) { (partialOpts || [ ZERO_PATH, MINORGRID_PATH, GRID_PATH, TICK_PATH, TICK_TEXT ]).forEach(function(e) { var isPeriodLabel = e.K === 'tick' && e.L === 'text' && ax.ticklabelmode === 'period'; var mainPlotinfo = fullLayout._plots[ax._mainSubplot]; var sel; if(e.K === ZERO_PATH.K) sel = mainPlotinfo.zerolinelayer.selectAll('.' + ax._id + 'zl'); else if(e.K === MINORGRID_PATH.K) sel = mainPlotinfo.minorGridlayer.selectAll('.' + ax._id); else if(e.K === GRID_PATH.K) sel = mainPlotinfo.gridlayer.selectAll('.' + ax._id); else sel = mainPlotinfo[ax._id.charAt(0) + 'axislayer']; sel.each(function() { var w = d3.select(this); if(e.L) w = w.selectAll(e.L); w.each(function(d) { var q = ax.l2p( isPeriodLabel ? getPosX(d) : d.x ) + ax._offset; var t = d3.select(this); if( q < ax['_visibleLabelMax_' + anchorAx._id] && q > ax['_visibleLabelMin_' + anchorAx._id] ) { t.style('display', 'none'); // hidden } else if(e.K === 'tick' && !idx) { t.style('display', null); // visible } }); }); }); } }); }; // make sure all labels are correctly positioned at their base angle // the positionLabels call above is only for newly drawn labels. // do this without waiting, using the last calculated angle to // minimize flicker, then do it again when we know all labels are // there, putting back the prescribed angle to check for overlaps. positionLabels(tickLabels, (prevAngle + 1) ? prevAngle : tickAngle); function allLabelsReady() { return labelsReady.length && Promise.all(labelsReady); } var autoangle = null; function fixLabelOverlaps() { positionLabels(tickLabels, tickAngle); // check for auto-angling if x labels overlap // don't auto-angle at all for log axes with // base and digit format if(vals.length && ax.autotickangles && (ax.type !== 'log' || String(ax.dtick).charAt(0) !== 'D') ) { autoangle = ax.autotickangles[0]; var maxFontSize = 0; var lbbArray = []; var i; var maxLines = 1; tickLabels.each(function(d) { maxFontSize = Math.max(maxFontSize, d.fontSize); var x = ax.l2p(d.x); var thisLabel = selectTickLabel(this); var bb = Drawing.bBox(thisLabel.node()); maxLines = Math.max(maxLines, svgTextUtils.lineCount(thisLabel)); lbbArray.push({ // ignore about y, just deal with x overlaps top: 0, bottom: 10, height: 10, left: x - bb.width / 2, // impose a 2px gap right: x + bb.width / 2 + 2, width: bb.width + 2 }); }); // autotickangles // if there are dividers or ticks on boundaries, the labels will be in between and // we need to prevent overlap with the next divider/tick. Else the labels will be on // the ticks and we need to prevent overlap with the next label. // TODO should secondary labels also fall into this fix-overlap regime? var preventOverlapWithTick = (ax.tickson === 'boundaries' || ax.showdividers) && !opts.secondary; var vLen = vals.length; var tickSpacing = Math.abs((vals[vLen - 1].x - vals[0].x) * ax._m) / (vLen - 1); var adjacent = preventOverlapWithTick ? tickSpacing / 2 : tickSpacing; var opposite = preventOverlapWithTick ? ax.ticklen : maxFontSize * 1.25 * maxLines; var hypotenuse = Math.sqrt(Math.pow(adjacent, 2) + Math.pow(opposite, 2)); var maxCos = adjacent / hypotenuse; var autoTickAnglesRadians = ax.autotickangles.map( function(degrees) { return degrees * Math.PI / 180; } ); var angleRadians = autoTickAnglesRadians.find( function(angle) { return Math.abs(Math.cos(angle)) <= maxCos; } ); if(angleRadians === undefined) { // no angle with smaller cosine than maxCos, just pick the angle with smallest cosine angleRadians = autoTickAnglesRadians.reduce( function(currentMax, nextAngle) { return Math.abs(Math.cos(currentMax)) < Math.abs(Math.cos(nextAngle)) ? currentMax : nextAngle; } , autoTickAnglesRadians[0] ); } var newAngle = angleRadians * (180 / Math.PI /* to degrees */); if(preventOverlapWithTick) { var gap = 2; if(ax.ticks) gap += ax.tickwidth / 2; for(i = 0; i < lbbArray.length; i++) { var xbnd = vals[i].xbnd; var lbb = lbbArray[i]; if( (xbnd[0] !== null && (lbb.left - ax.l2p(xbnd[0])) < gap) || (xbnd[1] !== null && (ax.l2p(xbnd[1]) - lbb.right) < gap) ) { autoangle = newAngle; break; } } } else { var ticklabelposition = ax.ticklabelposition || ''; var has = function(str) { return ticklabelposition.indexOf(str) !== -1; }; var isTop = has('top'); var isLeft = has('left'); var isRight = has('right'); var isBottom = has('bottom'); var isAligned = isBottom || isLeft || isTop || isRight; var pad = !isAligned ? 0 : (ax.tickwidth || 0) + 2 * TEXTPAD; for(i = 0; i < lbbArray.length - 1; i++) { if(Lib.bBoxIntersect(lbbArray[i], lbbArray[i + 1], pad)) { autoangle = newAngle; break; } } } if(autoangle) { positionLabels(tickLabels, autoangle); } } } if(ax._selections) { ax._selections[cls] = tickLabels; } var seq = [allLabelsReady]; // N.B. during auto-margin redraws, if the axis fixed its label overlaps // by rotating 90 degrees, do not attempt to re-fix its label overlaps // as this can lead to infinite redraw loops! if(ax.automargin && fullLayout._redrawFromAutoMarginCount && prevAngle === 90) { autoangle = prevAngle; seq.push(function() { positionLabels(tickLabels, prevAngle); }); } else { seq.push(fixLabelOverlaps); } // save current tick angle for future redraws if(ax._tickAngles) { seq.push(function() { ax._tickAngles[cls] = autoangle === null ? (isNumeric(tickAngle) ? tickAngle : 0) : autoangle; }); } var computeTickLabelBoundingBoxes = function() { var labelsMaxW = 0; var labelsMaxH = 0; tickLabels.each(function(d, i) { var thisLabel = selectTickLabel(this); var mathjaxGroup = thisLabel.select('.text-math-group'); if(mathjaxGroup.empty()) { var bb; if(ax._vals[i]) { bb = ax._vals[i].bb || Drawing.bBox(thisLabel.node()); ax._vals[i].bb = bb; } labelsMaxW = Math.max(labelsMaxW, bb.width); labelsMaxH = Math.max(labelsMaxH, bb.height); } }); return { labelsMaxW: labelsMaxW, labelsMaxH: labelsMaxH }; }; var anchorAx = ax._anchorAxis; if( anchorAx && (anchorAx.autorange || anchorAx.insiderange) && insideTicklabelposition(ax) && !isLinked(fullLayout, ax._id) ) { if(!fullLayout._insideTickLabelsUpdaterange) { fullLayout._insideTickLabelsUpdaterange = {}; } if(anchorAx.autorange) { fullLayout._insideTickLabelsUpdaterange[anchorAx._name + '.autorange'] = anchorAx.autorange; seq.push(computeTickLabelBoundingBoxes); } if(anchorAx.insiderange) { var BBs = computeTickLabelBoundingBoxes(); var move = ax._id.charAt(0) === 'y' ? BBs.labelsMaxW : BBs.labelsMaxH; move += 2 * TEXTPAD; if(ax.ticklabelposition === 'inside') { move += ax.ticklen || 0; } var sgn = (ax.side === 'right' || ax.side === 'top') ? 1 : -1; var index = sgn === 1 ? 1 : 0; var otherIndex = sgn === 1 ? 0 : 1; var newRange = []; newRange[otherIndex] = anchorAx.range[otherIndex]; var anchorAxRange = anchorAx.range; var p0 = anchorAx.r2p(anchorAxRange[index]); var p1 = anchorAx.r2p(anchorAxRange[otherIndex]); var _tempNewRange = fullLayout._insideTickLabelsUpdaterange[anchorAx._name + '.range']; if(_tempNewRange) { // case of having multiple anchored axes having insideticklabel var q0 = anchorAx.r2p(_tempNewRange[index]); var q1 = anchorAx.r2p(_tempNewRange[otherIndex]); var dir = sgn * (ax._id.charAt(0) === 'y' ? 1 : -1); if(dir * p0 < dir * q0) { p0 = q0; newRange[index] = anchorAxRange[index] = _tempNewRange[index]; } if(dir * p1 > dir * q1) { p1 = q1; newRange[otherIndex] = anchorAxRange[otherIndex] = _tempNewRange[otherIndex]; } } var dist = Math.abs(p1 - p0); if(dist - move > 0) { dist -= move; move *= 1 + move / dist; } else { move = 0; } if(ax._id.charAt(0) !== 'y') move = -move; newRange[index] = anchorAx.p2r( anchorAx.r2p(anchorAxRange[index]) + sgn * move ); // handle partial ranges in insiderange if( anchorAx.autorange === 'min' || anchorAx.autorange === 'max reversed' ) { newRange[0] = null; anchorAx._rangeInitial0 = undefined; anchorAx._rangeInitial1 = undefined; } else if( anchorAx.autorange === 'max' || anchorAx.autorange === 'min reversed' ) { newRange[1] = null; anchorAx._rangeInitial0 = undefined; anchorAx._rangeInitial1 = undefined; } fullLayout._insideTickLabelsUpdaterange[anchorAx._name + '.range'] = newRange; } } var done = Lib.syncOrAsync(seq); if(done && done.then) gd._promises.push(done); return done; }; /** * Draw axis dividers * * @param {DOM element} gd * @param {object} ax (full) axis object * - {string} _id * - {string} showdividers * - {number} dividerwidth * - {string} dividercolor * @param {object} opts * - {array of object} vals (calcTicks output-like) * - {d3 selection} layer * - {fn} path * - {fn} transFn */ function drawDividers(gd, ax, opts) { var cls = ax._id + 'divider'; var vals = opts.vals; var dividers = opts.layer.selectAll('path.' + cls) .data(vals, tickDataFn); dividers.exit().remove(); dividers.enter().insert('path', ':first-child') .classed(cls, 1) .classed('crisp', 1) .call(Color.stroke, ax.dividercolor) .style('stroke-width', Drawing.crispRound(gd, ax.dividerwidth, 1) + 'px'); dividers .attr('transform', opts.transFn) .attr('d', opts.path); } /** * Get axis position in px, that is the distance for the graph's * top (left) edge for x (y) axes. * * @param {DOM element} gd * @param {object} ax (full) axis object * - {string} _id * - {string} side * if anchored: * - {object} _anchorAxis * Otherwise: * - {number} position * @return {number} */ axes.getPxPosition = function(gd, ax) { var gs = gd._fullLayout._size; var axLetter = ax._id.charAt(0); var side = ax.side; var anchorAxis; if(ax.anchor !== 'free') { anchorAxis = ax._anchorAxis; } else if(axLetter === 'x') { anchorAxis = { _offset: gs.t + (1 - (ax.position || 0)) * gs.h, _length: 0 }; } else if(axLetter === 'y') { anchorAxis = { _offset: gs.l + (ax.position || 0) * gs.w + ax._shift, _length: 0 }; } if(side === 'top' || side === 'left') { return anchorAxis._offset; } else if(side === 'bottom' || side === 'right') { return anchorAxis._offset + anchorAxis._length; } }; /** * Approximate axis title depth (w/o computing its bounding box) * * @param {object} ax (full) axis object * - {string} title.text * - {number} title.font.size * - {number} title.standoff * @return {number} (in px) */ function approxTitleDepth(ax) { var fontSize = ax.title.font.size; var extraLines = (ax.title.text.match(svgTextUtils.BR_TAG_ALL) || []).length; if(ax.title.hasOwnProperty('standoff')) { return fontSize * (CAP_SHIFT + (extraLines * LINE_SPACING)); } else { return extraLines ? fontSize * (extraLines + 1) * LINE_SPACING : fontSize; } } /** * Draw axis title, compute default standoff if necessary * * @param {DOM element} gd * @param {object} ax (full) axis object * - {string} _id * - {string} _name * - {string} side * - {number} title.font.size * - {object} _selections * * - {number} _depth * - {number} title.standoff * OR * - {number} linewidth * - {boolean} showticklabels */ function drawTitle(gd, ax) { var fullLayout = gd._fullLayout; var axId = ax._id; var axLetter = axId.charAt(0); var fontSize = ax.title.font.size; var titleStandoff; var extraLines = (ax.title.text.match(svgTextUtils.BR_TAG_ALL) || []).length; if(ax.title.hasOwnProperty('standoff')) { // With ax._depth the initial drawing baseline is at the outer axis border (where the // ticklabels are drawn). Since the title text will be drawn above the baseline, // bottom/right axes must be shifted by 1 text line to draw below ticklabels instead of on // top of them, whereas for top/left axes, the first line would be drawn // before the ticklabels, but we need an offset for the descender portion of the first line // and all subsequent lines. if(ax.side === 'bottom' || ax.side === 'right') { titleStandoff = ax._depth + ax.title.standoff + fontSize * CAP_SHIFT; } else if(ax.side === 'top' || ax.side === 'left') { titleStandoff = ax._depth + ax.title.standoff + fontSize * (MID_SHIFT + (extraLines * LINE_SPACING)); } } else { var isInside = insideTicklabelposition(ax); if(ax.type === 'multicategory') { titleStandoff = ax._depth; } else { var offsetBase = 1.5 * fontSize; if(isInside) { offsetBase = 0.5 * fontSize; if(ax.ticks === 'outside') { offsetBase += ax.ticklen; } } titleStandoff = 10 + offsetBase + (ax.linewidth ? ax.linewidth - 1 : 0); } if(!isInside) { if(axLetter === 'x') { titleStandoff += ax.side === 'top' ? fontSize * (ax.showticklabels ? 1 : 0) : fontSize * (ax.showticklabels ? 1.5 : 0.5); } else { titleStandoff += ax.side === 'right' ? fontSize * (ax.showticklabels ? 1 : 0.5) : fontSize * (ax.showticklabels ? 0.5 : 0); } } } var pos = axes.getPxPosition(gd, ax); var transform, x, y; if(axLetter === 'x') { x = ax._offset + ax._length / 2; y = (ax.side === 'top') ? pos - titleStandoff : pos + titleStandoff; } else { y = ax._offset + ax._length / 2; x = (ax.side === 'right') ? pos + titleStandoff : pos - titleStandoff; transform = {rotate: '-90', offset: 0}; } var avoid; if(ax.type !== 'multicategory') { var tickLabels = ax._selections[ax._id + 'tick']; avoid = { selection: tickLabels, side: ax.side }; if(tickLabels && tickLabels.node() && tickLabels.node().parentNode) { var translation = Drawing.getTranslate(tickLabels.node().parentNode); avoid.offsetLeft = translation.x; avoid.offsetTop = translation.y; } if(ax.title.hasOwnProperty('standoff')) { avoid.pad = 0; } } ax._titleStandoff = titleStandoff; return Titles.draw(gd, axId + 'title', { propContainer: ax, propName: ax._name + '.title.text', placeholder: fullLayout._dfltTitle[axLetter], avoid: avoid, transform: transform, attributes: {x: x, y: y, 'text-anchor': 'middle'} }); } axes.shouldShowZeroLine = function(gd, ax, counterAxis) { var rng = Lib.simpleMap(ax.range, ax.r2l); return ( (rng[0] * rng[1] <= 0) && ax.zeroline && (ax.type === 'linear' || ax.type === '-') && !(ax.rangebreaks && ax.maskBreaks(0) === BADNUM) && ( clipEnds(ax, 0) || !anyCounterAxLineAtZero(gd, ax, counterAxis, rng) || hasBarsOrFill(gd, ax) ) ); }; axes.clipEnds = function(ax, vals) { return vals.filter(function(d) { return clipEnds(ax, d.x); }); }; function clipEnds(ax, l) { var p = ax.l2p(l); return (p > 1 && p < ax._length - 1); } function anyCounterAxLineAtZero(gd, ax, counterAxis, rng) { var mainCounterAxis = counterAxis._mainAxis; if(!mainCounterAxis) return; var fullLayout = gd._fullLayout; var axLetter = ax._id.charAt(0); var counterLetter = axes.counterLetter(ax._id); var zeroPosition = ax._offset + ( ((Math.abs(rng[0]) < Math.abs(rng[1])) === (axLetter === 'x')) ? 0 : ax._length ); function lineNearZero(ax2) { if(!ax2.showline || !ax2.linewidth) return false; var tolerance = Math.max((ax2.linewidth + ax.zerolinewidth) / 2, 1); function closeEnough(pos2) { return typeof pos2 === 'number' && Math.abs(pos2 - zeroPosition) < tolerance; } if(closeEnough(ax2._mainLinePosition) || closeEnough(ax2._mainMirrorPosition)) { return true; } var linePositions = ax2._linepositions || {}; for(var k in linePositions) { if(closeEnough(linePositions[k][0]) || closeEnough(linePositions[k][1])) { return true; } } } var plotinfo = fullLayout._plots[counterAxis._mainSubplot]; if(!(plotinfo.mainplotinfo || plotinfo).overlays.length) { return lineNearZero(counterAxis, zeroPosition); } var counterLetterAxes = axes.list(gd, counterLetter); for(var i = 0; i < counterLetterAxes.length; i++) { var counterAxis2 = counterLetterAxes[i]; if( counterAxis2._mainAxis === mainCounterAxis && lineNearZero(counterAxis2, zeroPosition) ) { return true; } } } function hasBarsOrFill(gd, ax) { var fullData = gd._fullData; var subplot = ax._mainSubplot; var axLetter = ax._id.charAt(0); for(var i = 0; i < fullData.length; i++) { var trace = fullData[i]; if(trace.visible === true && (trace.xaxis + trace.yaxis) === subplot) { if( Registry.traceIs(trace, 'bar-like') && trace.orientation === {x: 'h', y: 'v'}[axLetter] ) return true; if( trace.fill && trace.fill.charAt(trace.fill.length - 1) === axLetter ) return true; } } return false; } function selectTickLabel(gTick) { var s = d3.select(gTick); var mj = s.select('.text-math-group'); return mj.empty() ? s.select('text') : mj; } /** * Find all margin pushers for 2D axes and reserve them for later use * Both label and rangeslider automargin calculations happen later so * we need to explicitly allow their ids in order to not delete them. * * TODO: can we pull the actual automargin calls forward to avoid this hack? * We're probably also doing multiple redraws in this case, would be faster * if we can just do the whole calculation ahead of time and draw once. */ axes.allowAutoMargin = function(gd) { var axList = axes.list(gd, '', true); for(var i = 0; i < axList.length; i++) { var ax = axList[i]; if(ax.automargin) { Plots.allowAutoMargin(gd, axAutoMarginID(ax)); if(ax.mirror) { Plots.allowAutoMargin(gd, axMirrorAutoMarginID(ax)); } } if(Registry.getComponentMethod('rangeslider', 'isVisible')(ax)) { Plots.allowAutoMargin(gd, rangeSliderAutoMarginID(ax)); } } }; function axAutoMarginID(ax) { return ax._id + '.automargin'; } function axMirrorAutoMarginID(ax) { return axAutoMarginID(ax) + '.mirror'; } function rangeSliderAutoMarginID(ax) { return ax._id + '.rangeslider'; } // swap all the presentation attributes of the axes showing these traces axes.swap = function(gd, traces) { var axGroups = makeAxisGroups(gd, traces); for(var i = 0; i < axGroups.length; i++) { swapAxisGroup(gd, axGroups[i].x, axGroups[i].y); } }; function makeAxisGroups(gd, traces) { var groups = []; var i, j; for(i = 0; i < traces.length; i++) { var groupsi = []; var xi = gd._fullData[traces[i]].xaxis; var yi = gd._fullData[traces[i]].yaxis; if(!xi || !yi) continue; // not a 2D cartesian trace? for(j = 0; j < groups.length; j++) { if(groups[j].x.indexOf(xi) !== -1 || groups[j].y.indexOf(yi) !== -1) { groupsi.push(j); } } if(!groupsi.length) { groups.push({x: [xi], y: [yi]}); continue; } var group0 = groups[groupsi[0]]; var groupj; if(groupsi.length > 1) { for(j = 1; j < groupsi.length; j++) { groupj = groups[groupsi[j]]; mergeAxisGroups(group0.x, groupj.x); mergeAxisGroups(group0.y, groupj.y); } } mergeAxisGroups(group0.x, [xi]); mergeAxisGroups(group0.y, [yi]); } return groups; } function mergeAxisGroups(intoSet, fromSet) { for(var i = 0; i < fromSet.length; i++) { if(intoSet.indexOf(fromSet[i]) === -1) intoSet.push(fromSet[i]); } } function swapAxisGroup(gd, xIds, yIds) { var xFullAxes = []; var yFullAxes = []; var layout = gd.layout; var i, j; for(i = 0; i < xIds.length; i++) xFullAxes.push(axes.getFromId(gd, xIds[i])); for(i = 0; i < yIds.length; i++) yFullAxes.push(axes.getFromId(gd, yIds[i])); var allAxKeys = Object.keys(axAttrs); var noSwapAttrs = [ 'anchor', 'domain', 'overlaying', 'position', 'side', 'tickangle', 'editType' ]; var numericTypes = ['linear', 'log']; for(i = 0; i < allAxKeys.length; i++) { var keyi = allAxKeys[i]; var xVal = xFullAxes[0][keyi]; var yVal = yFullAxes[0][keyi]; var allEqual = true; var coerceLinearX = false; var coerceLinearY = false; if(keyi.charAt(0) === '_' || typeof xVal === 'function' || noSwapAttrs.indexOf(keyi) !== -1) { continue; } for(j = 1; j < xFullAxes.length && allEqual; j++) { var xVali = xFullAxes[j][keyi]; if(keyi === 'type' && numericTypes.indexOf(xVal) !== -1 && numericTypes.indexOf(xVali) !== -1 && xVal !== xVali) { // type is special - if we find a mixture of linear and log, // coerce them all to linear on flipping coerceLinearX = true; } else if(xVali !== xVal) allEqual = false; } for(j = 1; j < yFullAxes.length && allEqual; j++) { var yVali = yFullAxes[j][keyi]; if(keyi === 'type' && numericTypes.indexOf(yVal) !== -1 && numericTypes.indexOf(yVali) !== -1 && yVal !== yVali) { // type is special - if we find a mixture of linear and log, // coerce them all to linear on flipping coerceLinearY = true; } else if(yFullAxes[j][keyi] !== yVal) allEqual = false; } if(allEqual) { if(coerceLinearX) layout[xFullAxes[0]._name].type = 'linear'; if(coerceLinearY) layout[yFullAxes[0]._name].type = 'linear'; swapAxisAttrs(layout, keyi, xFullAxes, yFullAxes, gd._fullLayout._dfltTitle); } } // now swap x&y for any annotations anchored to these x & y for(i = 0; i < gd._fullLayout.annotations.length; i++) { var ann = gd._fullLayout.annotations[i]; if(xIds.indexOf(ann.xref) !== -1 && yIds.indexOf(ann.yref) !== -1) { Lib.swapAttrs(layout.annotations[i], ['?']); } } } function swapAxisAttrs(layout, key, xFullAxes, yFullAxes, dfltTitle) { // in case the value is the default for either axis, // look at the first axis in each list and see if // this key's value is undefined var np = Lib.nestedProperty; var xVal = np(layout[xFullAxes[0]._name], key).get(); var yVal = np(layout[yFullAxes[0]._name], key).get(); var i; if(key === 'title') { // special handling of placeholder titles if(xVal && xVal.text === dfltTitle.x) { xVal.text = dfltTitle.y; } if(yVal && yVal.text === dfltTitle.y) { yVal.text = dfltTitle.x; } } for(i = 0; i < xFullAxes.length; i++) { np(layout, xFullAxes[i]._name + '.' + key).set(yVal); } for(i = 0; i < yFullAxes.length; i++) { np(layout, yFullAxes[i]._name + '.' + key).set(xVal); } } function isAngular(ax) { return ax._id === 'angularaxis'; } function moveOutsideBreak(v, ax) { var len = ax._rangebreaks.length; for(var k = 0; k < len; k++) { var brk = ax._rangebreaks[k]; if(v >= brk.min && v < brk.max) { return brk.max; } } return v; } function insideTicklabelposition(ax) { return ((ax.ticklabelposition || '').indexOf('inside') !== -1); } function hideCounterAxisInsideTickLabels(ax, opts) { if(insideTicklabelposition(ax._anchorAxis || {})) { if(ax._hideCounterAxisInsideTickLabels) { ax._hideCounterAxisInsideTickLabels(opts); } } } function incrementShift(ax, shiftVal, axShifts, normalize) { // Need to set 'overlay' for anchored axis var overlay = ((ax.anchor !== 'free') && ((ax.overlaying === undefined) || (ax.overlaying === false))) ? ax._id : ax.overlaying; var shiftValAdj; if(normalize) { shiftValAdj = ax.side === 'right' ? shiftVal : -shiftVal; } else { shiftValAdj = shiftVal; } if(!(overlay in axShifts)) { axShifts[overlay] = {}; } if(!(ax.side in axShifts[overlay])) { axShifts[overlay][ax.side] = 0; } axShifts[overlay][ax.side] += shiftValAdj; } function setShiftVal(ax, axShifts) { return ax.autoshift ? axShifts[ax.overlaying][ax.side] : (ax.shift || 0); } /** * Checks if the given period is at least the period described by the tickformat or larger. If that * is the case, they are compatible, because then the tickformat can be used to describe the period. * E.g. it doesn't make sense to put a year label on a period spanning only a month. * @param {number} period in ms * @param {string} tickformat * @returns {boolean} */ function periodCompatibleWithTickformat(period, tickformat) { return ( /%f/.test(tickformat) ? period >= ONEMICROSEC : /%L/.test(tickformat) ? period >= ONEMILLI : /%[SX]/.test(tickformat) ? period >= ONESEC : /%M/.test(tickformat) ? period >= ONEMIN : /%[HI]/.test(tickformat) ? period >= ONEHOUR : /%p/.test(tickformat) ? period >= HALFDAY : /%[Aadejuwx]/.test(tickformat) ? period >= ONEDAY : /%[UVW]/.test(tickformat) ? period >= ONEWEEK : /%[Bbm]/.test(tickformat) ? period >= ONEMINMONTH : /%[q]/.test(tickformat) ? period >= ONEMINQUARTER : /%[Yy]/.test(tickformat) ? period >= ONEMINYEAR : true ); }