diff --git a/src/plots/cartesian/axes.js b/src/plots/cartesian/axes.js index 0b5389d3716..83c93e5c8e4 100644 --- a/src/plots/cartesian/axes.js +++ b/src/plots/cartesian/axes.js @@ -36,6 +36,7 @@ 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' }; @@ -540,13 +541,113 @@ function autoShiftMonthBins(binStart, data, dtick, dataMin, calendar) { // 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); - ax._dtickInit = ax.dtick; - ax._tick0Init = ax.tick0; - // calculate max number of (auto) ticks to display based on plot size if(ax.tickmode === 'auto' || !ax.dtick) { var nt = ax.nticks; @@ -566,10 +667,11 @@ axes.prepTicks = function(ax, opts) { if(ax._name === 'radialaxis') nt *= 2; } - // add a couple of extra digits for filling in ticks when we - // have explicit tickvals without tick text - if(ax.tickmode === 'array') nt *= 100; - + 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); @@ -787,111 +889,201 @@ function positionPeriodTicks(tickVals, ax, definedDelta) { // in any case, set tickround to # of digits to round tick labels to, // or codes to this effect for log and date scales axes.calcTicks = function calcTicks(ax, opts) { - axes.prepTicks(ax, opts); - var rng = Lib.simpleMap(ax.range, ax.r2l, undefined, undefined, opts); - - // now that we've figured out the auto values for formatting - // in case we're missing some ticktext, we can break out for array ticks - if(ax.tickmode === 'array') return arrayTicks(ax); + var type = ax.type; + var calendar = ax.calendar; + var ticklabelstep = ax.ticklabelstep; + var isPeriod = ax.ticklabelmode === 'period'; - // add a tiny bit so we get ticks which may have rounded out - var exRng = expandRange(rng); - var startTick = exRng[0]; - var endTick = exRng[1]; - // check for reversed axis + var 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 numDtick = isNumeric(ax.dtick); - var isDLog = (ax.type === 'log') && !(numDtick || ax.dtick.charAt(0) === 'L'); - var isPeriod = ax.ticklabelmode === 'period'; + var maxTicks = Math.max(1000, ax._length || 0); - // find the first tick - ax._tmin = axes.tickFirst(ax, opts); + var ticksOut = []; + var minorTicks = []; - // No visible ticks? Quit. - // I've only seen this on category axes with all categories off the edge. - if((ax._tmin < startTick) !== axrev) return []; + var tickVals = []; + var minorTickVals = []; - // return the full set of tick vals - if(ax.type === 'category' || ax.type === 'multicategory') { - endTick = (axrev) ? Math.max(-0.5, endTick) : - Math.min(ax._categories.length - 0.5, endTick); - } + var hasMinor = ax.minor && (ax.minor.ticks || ax.minor.showgrid); - var x = ax._tmin; + // calc major first + for(var major = 1; major >= (hasMinor ? 0 : 1); major--) { + var isMinor = !major; - if(ax.rangebreaks && ax._tick0Init !== ax.tick0) { - // adjust tick0 - x = moveOutsideBreak(x, ax); - if(!axrev) { - x = axes.tickIncrement(x, ax.dtick, !axrev, ax.calendar); + if(major) { + ax._dtickInit = ax.dtick; + ax._tick0Init = ax.tick0; + } else { + ax.minor._dtickInit = ax.minor.dtick; + ax.minor._tick0Init = ax.minor.tick0; } - } - if(isPeriod) { - // add one item to label period before tick0 - x = axes.tickIncrement(x, ax.dtick, !axrev, ax.calendar); - } + var mockAx = major ? ax : Lib.extendFlat({}, ax, ax.minor); - var ticklabelstep = ax.ticklabelstep; + if(isMinor) { + axes.prepMinorTicks(mockAx, ax, opts); + } else { + axes.prepTicks(mockAx, opts); + } - var maxTicks = Math.max(1000, ax._length || 0); - var tickVals = []; - var xPrevious = null; + // 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); + } else { + minorTickVals = []; + minorTicks = arrayTicks(ax); + } + continue; + } - var dTick; - if(numDtick) { - dTick = ax.dtick; - } else { - if(ax.type === 'date') { - if(typeof ax.dtick === 'string' && ax.dtick.charAt(0) === 'M') { - dTick = ONEAVGMONTH * ax.dtick.substring(1); + // 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); } - } else { - dTick = ax._roughDTick; } - } - var id = Math.round(( - ax.r2l(x) - - ax.r2l(ax.tick0) - ) / dTick) - 1; + var prevX = null; + var x = x0; + var majorId; - for(; - (axrev) ? (x >= endTick) : (x <= endTick); - x = axes.tickIncrement(x, ax.dtick, axrev, ax.calendar) - ) { - id++; + 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(ax.rangebreaks) { + if(mockAx.rangebreaks && mockAx._tick0Init !== mockAx.tick0) { + // adjust tick0 + x = moveOutsideBreak(x, ax); if(!axrev) { - if(x < startTick) continue; - if(ax.maskBreaks(x) === BADNUM && moveOutsideBreak(x, ax) >= maxRange) break; + x = axes.tickIncrement(x, dtick, !axrev, calendar); } } - // prevent infinite loops - no more than one tick per pixel, - // and make sure each value is different from the previous - if(tickVals.length > maxTicks || x === xPrevious) break; - xPrevious = x; - - var minor = false; - if(isDLog && (x !== (x | 0))) { - minor = true; + if(major && isPeriod) { + // add one item to label period before tick0 + x = axes.tickIncrement(x, dtick, !axrev, calendar); + majorId--; } - var obj = { - minor: minor, - value: x - }; + for(; + axrev ? + (x >= endTick) : + (x <= endTick); + x = axes.tickIncrement( + x, + dtick, + axrev, + calendar + ) + ) { + if(major) majorId++; - if(ticklabelstep > 1 && id % ticklabelstep) { - obj.skipLabel = true; + 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); + } } + } + + 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; }); - tickVals.push(obj); + 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(tickVals, ax, ax._definedDelta); @@ -950,38 +1142,47 @@ axes.calcTicks = function calcTicks(ax, opts) { ax._prevDateHead = lastVisibleHead; }; - var ticksOut = []; + tickVals = tickVals.concat(minorTickVals); + var t, p; for(i = 0; i < tickVals.length; i++) { var _minor = tickVals[i].minor; var _value = tickVals[i].value; - lastVisibleHead = ax._prevDateHead; + if(_minor) { + minorTicks.push({ + x: _value, + minor: true + }); + } else { + lastVisibleHead = ax._prevDateHead; - t = axes.tickText( - ax, - _value, - false, // hover - _minor // noSuffixPrefix - ); + t = axes.tickText( + ax, + _value, + false, // hover + tickVals[i].simpleLabel // noSuffixPrefix + ); - p = tickVals[i].periodX; - if(p !== undefined) { - t.periodX = p; - if(p > maxRange || p < minRange) { // hide label if outside the range - if(p > maxRange) t.periodX = maxRange; - if(p < minRange) t.periodX = minRange; + p = tickVals[i].periodX; + if(p !== undefined) { + t.periodX = p; + if(p > maxRange || p < minRange) { // hide label if outside the range + if(p > maxRange) t.periodX = maxRange; + if(p < minRange) t.periodX = minRange; + hideLabel(t); + } + } + + if(tickVals[i].skipLabel) { hideLabel(t); } - } - if(tickVals[i].skipLabel) { - hideLabel(t); + ticksOut.push(t); } - - ticksOut.push(t); } + ticksOut = ticksOut.concat(minorTicks); ax._inCalcTicks = false; @@ -994,18 +1195,10 @@ axes.calcTicks = function calcTicks(ax, opts) { }; function arrayTicks(ax) { - var vals = ax.tickvals; - var text = ax.ticktext; - var ticksOut = new Array(vals.length); var rng = Lib.simpleMap(ax.range, ax.r2l); var exRng = expandRange(rng); var tickMin = Math.min(exRng[0], exRng[1]); var tickMax = Math.max(exRng[0], exRng[1]); - var j = 0; - - // without a text array, just format the given values as any other ticks - // except with more precision to the numbers - if(!Array.isArray(text)) text = []; // make sure showing ticks doesn't accidentally add new categories // TODO multicategory, if we allow ticktext / tickvals @@ -1017,17 +1210,36 @@ function arrayTicks(ax) { ax.dtick = 'L' + Math.pow(10, Math.floor(Math.min(ax.range[0], ax.range[1])) - 1); } - for(var i = 0; i < vals.length; i++) { - var vali = tickVal2l(vals[i]); - if(vali > tickMin && vali < tickMax) { - if(text[i] === undefined) ticksOut[j] = axes.tickText(ax, vali); - else ticksOut[j] = tickTextObj(ax, vali, String(text[i])); - j++; + var ticksOut = []; + for(var isMinor = 0; isMinor <= 1; isMinor++) { + 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(!Array.isArray(text)) text = []; + + for(var i = 0; i < vals.length; i++) { + var vali = tickVal2l(vals[i]); + if(vali > tickMin && vali < tickMax) { + var obj = text[i] === undefined ? + axes.tickText(ax, vali) : + tickTextObj(ax, vali, String(text[i])); + + if(isMinor) { + obj.minor = true; + obj.text = ''; + } + + ticksOut.push(obj); + } } } - if(j < vals.length) ticksOut.splice(j, vals.length - j); - if(ax.rangebreaks) { // remove ticks falling inside rangebreaks ticksOut = ticksOut.filter(function(d) { @@ -1070,7 +1282,7 @@ function roundDTick(roughDTick, base, roundingSet) { // 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) { +axes.autoTicks = function(ax, roughDTick, isMinor) { var base; function getBase(v) { @@ -1093,20 +1305,22 @@ axes.autoTicks = function(ax, roughDTick) { ax.dtick = 'M' + roundDTick(roughDTick, 1, roundBase24); } else if(roughX2 > ONEDAY) { ax.dtick = roundDTick(roughDTick, ONEDAY, ax._hasDayOfWeekBreaks ? [1, 2, 7, 14] : roundDays); - // get week ticks on sunday - // this will also move the base tick off 2000-01-01 if dtick is - // 2 or 3 days... but that's a weird enough case that we'll ignore it. - var tickformat = axes.getTickFormat(ax); - var isPeriod = ax.ticklabelmode === 'period'; - if(isPeriod) ax._rawTick0 = ax.tick0; - - if(/%[uVW]/.test(tickformat)) { - ax.tick0 = Lib.dateTick0(ax.calendar, 2); // Monday - } else { - ax.tick0 = Lib.dateTick0(ax.calendar, 1); // Sunday - } + if(!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; + if(isPeriod) ax._dowTick0 = ax.tick0; + } } else if(roughX2 > ONEHOUR) { ax.dtick = roundDTick(roughDTick, ONEHOUR, roundBase24); } else if(roughX2 > ONEMIN) { @@ -1121,7 +1335,12 @@ axes.autoTicks = function(ax, roughDTick) { } 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); @@ -2008,6 +2227,7 @@ axes.draw = function(gd, arg, opts) { 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(); @@ -2161,6 +2381,7 @@ axes.drawOne = function(gd, ax, opts) { vals: gridVals, counterAxis: counterAxis, layer: plotinfo.gridlayer.select('.' + axId), + minorLayer: plotinfo.minorGridlayer.select('.' + axId), path: gridPath, transFn: transTickFn }); @@ -2173,21 +2394,32 @@ axes.drawOne = function(gd, ax, opts) { } } - var tickSigns = axes.getTickSigns(ax); - var tickPath; - if(ax.ticks) { - var mainTickPath; - var mirrorTickPath; - var fullTickPath; - mainTickPath = axes.makeTickPath(ax, mainLinePosition, tickSigns[2]); + var majorTickSigns = axes.getTickSigns(ax); + var minorTickSigns = axes.getTickSigns(ax, 'minor'); + + if(ax.ticks || (ax.minor && ax.minor.ticks)) { + var majorTickPath = axes.makeTickPath(ax, mainLinePosition, majorTickSigns[2]); + var minorTickPath = axes.makeTickPath(ax, mainLinePosition, minorTickSigns[2], { minor: true }); + + var mirrorMajorTickPath; + var mirrorMinorTickPath; + + var fullMajorTickPath; + var fullMinorTickPath; + if(ax._anchorAxis && ax.mirror && ax.mirror !== true) { - mirrorTickPath = axes.makeTickPath(ax, mainMirrorPosition, tickSigns[3]); - fullTickPath = mainTickPath + mirrorTickPath; + mirrorMajorTickPath = axes.makeTickPath(ax, mainMirrorPosition, majorTickSigns[3]); + mirrorMinorTickPath = axes.makeTickPath(ax, mainMirrorPosition, minorTickSigns[3], { minor: true }); + + fullMajorTickPath = majorTickPath + mirrorMajorTickPath; + fullMinorTickPath = minorTickPath + mirrorMinorTickPath; } else { - mirrorTickPath = ''; - fullTickPath = mainTickPath; + mirrorMajorTickPath = ''; + mirrorMinorTickPath = ''; + fullMajorTickPath = majorTickPath; + fullMinorTickPath = minorTickPath; } if(ax.showdividers && outsideTicks && ax.tickson === 'boundaries') { @@ -2196,10 +2428,12 @@ axes.drawOne = function(gd, ax, opts) { dividerLookup[dividerVals[i].x] = 1; } tickPath = function(d) { - return dividerLookup[d.x] ? mirrorTickPath : fullTickPath; + return dividerLookup[d.x] ? mirrorMajorTickPath : fullMajorTickPath; }; } else { - tickPath = fullTickPath; + tickPath = function(d) { + return d.minor ? fullMinorTickPath : fullMajorTickPath; + }; } } @@ -2218,8 +2452,20 @@ axes.drawOne = function(gd, ax, opts) { plotinfo = fullLayout._plots[sp]; // [bottom or left, top or right], free and main are handled above var linepositions = ax._linepositions[sp] || []; - var spTickPath = axes.makeTickPath(ax, linepositions[0], tickSigns[0]) + - axes.makeTickPath(ax, linepositions[1], tickSigns[1]); + + 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, @@ -2260,23 +2506,23 @@ axes.drawOne = function(gd, ax, opts) { repositionOnUpdate: true, secondary: true, transFn: transTickFn, - labelFns: axes.makeLabelFns(ax, mainLinePosition + standoff * tickSigns[4]) + labelFns: axes.makeLabelFns(ax, mainLinePosition + standoff * majorTickSigns[4]) }); }); seq.push(function() { - ax._depth = tickSigns[4] * (getLabelLevelBbox('tick2')[ax.side] - mainLinePosition); + ax._depth = majorTickSigns[4] * (getLabelLevelBbox('tick2')[ax.side] - mainLinePosition); return drawDividers(gd, ax, { vals: dividerVals, layer: mainAxLayer, - path: axes.makeTickPath(ax, mainLinePosition, tickSigns[4], ax._depth), + path: axes.makeTickPath(ax, mainLinePosition, majorTickSigns[4], { len: ax._depth }), transFn: transTickFn }); }); } else if(ax.title.hasOwnProperty('standoff')) { seq.push(function() { - ax._depth = tickSigns[4] * (getLabelLevelBbox()[ax.side] - mainLinePosition); + ax._depth = majorTickSigns[4] * (getLabelLevelBbox()[ax.side] - mainLinePosition); }); } @@ -2517,13 +2763,15 @@ function calcLabelLevelBbox(ax, cls) { * - [3]: sign for ticks mirroring 'ax.side' * - [4]: sign of arrow starting at axis pointing towards margin */ -axes.getTickSigns = function(ax) { +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 - if((ax.ticks !== 'inside') === (axLetter === 'x')) { + + 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 @@ -2627,11 +2875,17 @@ function getTickLabelUV(ax) { * - {number} linewidth * @param {number} shift along direction of ticklen * @param {1 or -1} sgn tick sign - * @param {number (optional)} len tick length + * @param {object} opts + * - {number (optional)} len tick length * @return {string} */ -axes.makeTickPath = function(ax, shift, sgn, len) { - len = len !== undefined ? len : ax.ticklen; +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; @@ -2838,10 +3092,20 @@ axes.drawTicks = function(gd, ax, opts) { var cls = ax._id + 'tick'; - var vals = opts.vals.filter(function(e) { return !e.noTick; }); + 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(ax.ticks ? vals : [], tickDataFn); + .data(vals, tickDataFn); ticks.exit().remove(); @@ -2849,8 +3113,16 @@ axes.drawTicks = function(gd, ax, opts) { .classed(cls, 1) .classed('ticks', 1) .classed('crisp', opts.crisp !== false) - .call(Color.stroke, ax.tickcolor) - .style('stroke-width', Drawing.crispRound(gd, ax.tickwidth, 1) + 'px') + .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 @@ -2885,16 +3157,18 @@ axes.drawGrid = function(gd, ax, opts) { opts = opts || {}; var cls = ax._id + 'grid'; - var vals = opts.vals; + + 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(ax.showgrid === false) { - vals = []; - } else if(counterAx && axes.shouldShowZeroLine(gd, ax, counterAx)) { + if(counterAx && axes.shouldShowZeroLine(gd, ax, counterAx)) { var isArrayMode = ax.tickmode === 'array'; - for(var i = 0; i < vals.length; i++) { - var xi = vals[i].x; + for(var i = 0; i < majorVals.length; i++) { + var xi = majorVals[i].x; if(isArrayMode ? !xi : (Math.abs(xi) < ax.dtick / 100)) { - vals = vals.slice(0, i).concat(vals.slice(i + 1)); + 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. @@ -2904,27 +3178,50 @@ axes.drawGrid = function(gd, ax, opts) { } } - var grid = opts.layer.selectAll('path.' + cls) - .data(vals, tickDataFn); + ax._gw = + Drawing.crispRound(gd, ax.gridwidth, 1); - grid.exit().remove(); + var wMinor = !hasMinor ? 0 : + Drawing.crispRound(gd, ax.minor.gridwidth, 1); - grid.enter().append('path') - .classed(cls, 1) - .classed('crisp', opts.crisp !== false); + var majorLayer = opts.layer; + var minorLayer = opts.minorLayer; + for(var major = 1; major >= 0; major--) { + var layer = major ? majorLayer : minorLayer; + if(!layer) continue; - ax._gw = Drawing.crispRound(gd, ax.gridwidth, 1); + var grid = layer.selectAll('path.' + cls) + .data(major ? majorVals : minorVals, tickDataFn); - grid.attr('transform', opts.transFn) - .attr('d', opts.path) - .call(Color.stroke, ax.gridcolor || '#ddd') - .style('stroke-dasharray', Drawing.dashStyle(ax.griddash, ax.gridwidth)) - .style('stroke-width', ax._gw + 'px') - .style('display', null); // visible + grid.exit().remove(); + + grid.enter().append('path') + .classed(cls, 1) + .classed('crisp', opts.crisp !== false); - hideCounterAxisInsideTickLabels(ax, [GRID_PATH]); + 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); + } - if(typeof opts.path === 'function') grid.attr('d', opts.path); + hideCounterAxisInsideTickLabels(ax, [GRID_PATH, MINORGRID_PATH]); }; /** @@ -3009,7 +3306,7 @@ axes.drawLabels = function(gd, ax, opts) { var axLetter = axId.charAt(0); var cls = opts.cls || axId + 'tick'; - var vals = opts.vals.filter(function(e) { return e.text; }); + var vals = opts.vals.filter(function(d) { return d.text; }); var labelFns = opts.labelFns; var tickAngle = opts.secondary ? 0 : ax.tickangle; @@ -3191,6 +3488,7 @@ axes.drawLabels = function(gd, ax, opts) { if(anchorAx && insideTicklabelposition(anchorAx)) { (partialOpts || [ ZERO_PATH, + MINORGRID_PATH, GRID_PATH, TICK_PATH, TICK_TEXT @@ -3204,6 +3502,7 @@ axes.drawLabels = function(gd, ax, opts) { 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']; diff --git a/src/plots/cartesian/axis_defaults.js b/src/plots/cartesian/axis_defaults.js index 434b076a845..969fee18e7f 100644 --- a/src/plots/cartesian/axis_defaults.js +++ b/src/plots/cartesian/axis_defaults.js @@ -4,6 +4,7 @@ var isNumeric = require('fast-isnumeric'); var Registry = require('../../registry'); var Lib = require('../../lib'); +var Template = require('../../plot_api/plot_template'); var handleArrayContainerDefaults = require('../array_container_defaults'); @@ -121,16 +122,45 @@ module.exports = function handleAxisDefaults(containerIn, containerOut, coerce, color: dfltFontColor }); + // major ticks handleTickValueDefaults(containerIn, containerOut, coerce, axType); + + var hasMinor = options.hasMinor; + if(hasMinor) { + // minor ticks + Template.newContainer(containerOut, 'minor'); + handleTickValueDefaults(containerIn, containerOut, coerce, axType, { isMinor: true }); + } + handleTickLabelDefaults(containerIn, containerOut, coerce, axType, options); + + // major and minor ticks handleTickMarkDefaults(containerIn, containerOut, coerce, options); + if(hasMinor) { + var keepIsMinor = options.isMinor; + options.isMinor = true; + handleTickMarkDefaults(containerIn, containerOut, coerce, options); + options.isMinor = keepIsMinor; + } + handleLineGridDefaults(containerIn, containerOut, coerce, { dfltColor: dfltColor, bgColor: options.bgColor, showGrid: options.showGrid, + hasMinor: hasMinor, attributes: layoutAttributes }); + // delete minor when no minor ticks or gridlines + if( + hasMinor && + !containerOut.minor.ticks && + !containerOut.minor.showgrid + ) { + delete containerOut.minor; + } + + // mirror if(containerOut.showline || containerOut.ticks) coerce('mirror'); if(options.automargin) coerce('automargin'); diff --git a/src/plots/cartesian/index.js b/src/plots/cartesian/index.js index 591f85c2bf5..e57c5572d01 100644 --- a/src/plots/cartesian/index.js +++ b/src/plots/cartesian/index.js @@ -461,6 +461,7 @@ function makeSubplotLayer(gd, plotinfo) { plotinfo.shapelayer = ensureSingle(backLayer, 'g', 'shapelayer'); plotinfo.imagelayer = ensureSingle(backLayer, 'g', 'imagelayer'); + plotinfo.minorGridlayer = ensureSingle(plotgroup, 'g', 'minor-gridlayer'); plotinfo.gridlayer = ensureSingle(plotgroup, 'g', 'gridlayer'); plotinfo.zerolinelayer = ensureSingle(plotgroup, 'g', 'zerolinelayer'); @@ -500,6 +501,7 @@ function makeSubplotLayer(gd, plotinfo) { // their other components to the corresponding // extra groups of their main plots. + plotinfo.minorGridlayer = mainplotinfo.minorGridlayer; plotinfo.gridlayer = mainplotinfo.gridlayer; plotinfo.zerolinelayer = mainplotinfo.zerolinelayer; @@ -525,6 +527,12 @@ function makeSubplotLayer(gd, plotinfo) { // common attributes for all subplots, overlays or not if(!hasOnlyLargeSploms) { + ensureSingleAndAddDatum(plotinfo.minorGridlayer, 'g', plotinfo.xaxis._id); + ensureSingleAndAddDatum(plotinfo.minorGridlayer, 'g', plotinfo.yaxis._id); + plotinfo.minorGridlayer.selectAll('g') + .map(function(d) { return d[0]; }) + .sort(axisIds.idSort); + ensureSingleAndAddDatum(plotinfo.gridlayer, 'g', plotinfo.xaxis._id); ensureSingleAndAddDatum(plotinfo.gridlayer, 'g', plotinfo.yaxis._id); plotinfo.gridlayer.selectAll('g') diff --git a/src/plots/cartesian/layout_attributes.js b/src/plots/cartesian/layout_attributes.js index 219e2b314e9..9810aca3370 100644 --- a/src/plots/cartesian/layout_attributes.js +++ b/src/plots/cartesian/layout_attributes.js @@ -12,6 +12,167 @@ var constants = require('./constants'); var HOUR = constants.HOUR_PATTERN; var DAY_OF_WEEK = constants.WEEKDAY_PATTERN; +var tickmode = { + valType: 'enumerated', + values: ['auto', 'linear', 'array'], + editType: 'ticks', + impliedEdits: {tick0: undefined, dtick: undefined}, + description: [ + 'Sets the tick mode for this axis.', + 'If *auto*, the number of ticks is set via `nticks`.', + 'If *linear*, the placement of the ticks is determined by', + 'a starting position `tick0` and a tick step `dtick`', + '(*linear* is the default value if `tick0` and `dtick` are provided).', + 'If *array*, the placement of the ticks is set via `tickvals`', + 'and the tick text is `ticktext`.', + '(*array* is the default value if `tickvals` is provided).' + ].join(' ') +}; + +function makeNticks(minor) { + return { + valType: 'integer', + min: 0, + dflt: minor ? 5 : 0, + editType: 'ticks', + description: [ + 'Specifies the maximum number of ticks for the particular axis.', + 'The actual number of ticks will be chosen automatically to be', + 'less than or equal to `nticks`.', + 'Has an effect only if `tickmode` is set to *auto*.' + ].join(' ') + }; +} + +var tick0 = { + valType: 'any', + editType: 'ticks', + impliedEdits: {tickmode: 'linear'}, + description: [ + 'Sets the placement of the first tick on this axis.', + 'Use with `dtick`.', + 'If the axis `type` is *log*, then you must take the log of your starting tick', + '(e.g. to set the starting tick to 100, set the `tick0` to 2)', + 'except when `dtick`=*L* (see `dtick` for more info).', + 'If the axis `type` is *date*, it should be a date string, like date data.', + 'If the axis `type` is *category*, it should be a number, using the scale where', + 'each category is assigned a serial number from zero in the order it appears.' + ].join(' ') +}; + +var dtick = { + valType: 'any', + editType: 'ticks', + impliedEdits: {tickmode: 'linear'}, + description: [ + 'Sets the step in-between ticks on this axis. Use with `tick0`.', + 'Must be a positive number, or special strings available to *log* and *date* axes.', + 'If the axis `type` is *log*, then ticks are set every 10^(n*dtick) where n', + 'is the tick number. For example,', + 'to set a tick mark at 1, 10, 100, 1000, ... set dtick to 1.', + 'To set tick marks at 1, 100, 10000, ... set dtick to 2.', + 'To set tick marks at 1, 5, 25, 125, 625, 3125, ... set dtick to log_10(5), or 0.69897000433.', + '*log* has several special values; *L*, where `f` is a positive number,', + 'gives ticks linearly spaced in value (but not position).', + 'For example `tick0` = 0.1, `dtick` = *L0.5* will put ticks at 0.1, 0.6, 1.1, 1.6 etc.', + 'To show powers of 10 plus small digits between, use *D1* (all digits) or *D2* (only 2 and 5).', + '`tick0` is ignored for *D1* and *D2*.', + 'If the axis `type` is *date*, then you must convert the time to milliseconds.', + 'For example, to set the interval between ticks to one day,', + 'set `dtick` to 86400000.0.', + '*date* also has special values *M* gives ticks spaced by a number of months.', + '`n` must be a positive integer.', + 'To set ticks on the 15th of every third month, set `tick0` to *2000-01-15* and `dtick` to *M3*.', + 'To set ticks every 4 years, set `dtick` to *M48*' + ].join(' ') +}; + +var tickvals = { + valType: 'data_array', + editType: 'ticks', + description: [ + 'Sets the values at which ticks on this axis appear.', + 'Only has an effect if `tickmode` is set to *array*.', + 'Used with `ticktext`.' + ].join(' ') +}; + +var ticks = { + valType: 'enumerated', + values: ['outside', 'inside', ''], + editType: 'ticks', + description: [ + 'Determines whether ticks are drawn or not.', + 'If **, this axis\' ticks are not drawn.', + 'If *outside* (*inside*), this axis\' are drawn outside (inside)', + 'the axis lines.' + ].join(' ') +}; + +function makeTicklen(minor) { + var obj = { + valType: 'number', + min: 0, + editType: 'ticks', + description: 'Sets the tick length (in px).' + }; + + if(!minor) obj.dflt = 5; + + return obj; +} + +function makeTickwidth(minor) { + var obj = { + valType: 'number', + min: 0, + editType: 'ticks', + description: 'Sets the tick width (in px).' + }; + + if(!minor) obj.dflt = 1; + + return obj; +} + +var tickcolor = { + valType: 'color', + dflt: colorAttrs.defaultLine, + editType: 'ticks', + description: 'Sets the tick color.' +}; + +var gridcolor = { + valType: 'color', + dflt: colorAttrs.lightLine, + editType: 'ticks', + description: 'Sets the color of the grid lines.' +}; + +function makeGridwidth(minor) { + var obj = { + valType: 'number', + min: 0, + editType: 'ticks', + description: 'Sets the width (in px) of the grid lines.' + }; + + if(!minor) obj.dflt = 1; + + return obj; +} + +var griddash = extendFlat({}, dash, {editType: 'ticks'}); + +var showgrid = { + valType: 'boolean', + editType: 'ticks', + description: [ + 'Determines whether or not grid lines are drawn.', + 'If *true*, the grid lines are drawn at every tick mark.' + ].join(' ') +}; + module.exports = { visible: { valType: 'boolean', @@ -343,75 +504,10 @@ module.exports = { }), // ticks - tickmode: { - valType: 'enumerated', - values: ['auto', 'linear', 'array'], - editType: 'ticks', - impliedEdits: {tick0: undefined, dtick: undefined}, - description: [ - 'Sets the tick mode for this axis.', - 'If *auto*, the number of ticks is set via `nticks`.', - 'If *linear*, the placement of the ticks is determined by', - 'a starting position `tick0` and a tick step `dtick`', - '(*linear* is the default value if `tick0` and `dtick` are provided).', - 'If *array*, the placement of the ticks is set via `tickvals`', - 'and the tick text is `ticktext`.', - '(*array* is the default value if `tickvals` is provided).' - ].join(' ') - }, - nticks: { - valType: 'integer', - min: 0, - dflt: 0, - editType: 'ticks', - description: [ - 'Specifies the maximum number of ticks for the particular axis.', - 'The actual number of ticks will be chosen automatically to be', - 'less than or equal to `nticks`.', - 'Has an effect only if `tickmode` is set to *auto*.' - ].join(' ') - }, - tick0: { - valType: 'any', - editType: 'ticks', - impliedEdits: {tickmode: 'linear'}, - description: [ - 'Sets the placement of the first tick on this axis.', - 'Use with `dtick`.', - 'If the axis `type` is *log*, then you must take the log of your starting tick', - '(e.g. to set the starting tick to 100, set the `tick0` to 2)', - 'except when `dtick`=*L* (see `dtick` for more info).', - 'If the axis `type` is *date*, it should be a date string, like date data.', - 'If the axis `type` is *category*, it should be a number, using the scale where', - 'each category is assigned a serial number from zero in the order it appears.' - ].join(' ') - }, - dtick: { - valType: 'any', - editType: 'ticks', - impliedEdits: {tickmode: 'linear'}, - description: [ - 'Sets the step in-between ticks on this axis. Use with `tick0`.', - 'Must be a positive number, or special strings available to *log* and *date* axes.', - 'If the axis `type` is *log*, then ticks are set every 10^(n*dtick) where n', - 'is the tick number. For example,', - 'to set a tick mark at 1, 10, 100, 1000, ... set dtick to 1.', - 'To set tick marks at 1, 100, 10000, ... set dtick to 2.', - 'To set tick marks at 1, 5, 25, 125, 625, 3125, ... set dtick to log_10(5), or 0.69897000433.', - '*log* has several special values; *L*, where `f` is a positive number,', - 'gives ticks linearly spaced in value (but not position).', - 'For example `tick0` = 0.1, `dtick` = *L0.5* will put ticks at 0.1, 0.6, 1.1, 1.6 etc.', - 'To show powers of 10 plus small digits between, use *D1* (all digits) or *D2* (only 2 and 5).', - '`tick0` is ignored for *D1* and *D2*.', - 'If the axis `type` is *date*, then you must convert the time to milliseconds.', - 'For example, to set the interval between ticks to one day,', - 'set `dtick` to 86400000.0.', - '*date* also has special values *M* gives ticks spaced by a number of months.', - '`n` must be a positive integer.', - 'To set ticks on the 15th of every third month, set `tick0` to *2000-01-15* and `dtick` to *M3*.', - 'To set ticks every 4 years, set `dtick` to *M48*' - ].join(' ') - }, + tickmode: tickmode, + nticks: makeNticks(), + tick0: tick0, + dtick: dtick, ticklabelstep: { valType: 'integer', min: 1, @@ -426,15 +522,7 @@ module.exports = { 'Not implemented for axes with `type` *log* or *multicategory*, or when `tickmode` is *array*.' ].join(' ') }, - tickvals: { - valType: 'data_array', - editType: 'ticks', - description: [ - 'Sets the values at which ticks on this axis appear.', - 'Only has an effect if `tickmode` is set to *array*.', - 'Used with `ticktext`.' - ].join(' ') - }, + tickvals: tickvals, ticktext: { valType: 'data_array', editType: 'ticks', @@ -444,17 +532,7 @@ module.exports = { 'Used with `tickvals`.' ].join(' ') }, - ticks: { - valType: 'enumerated', - values: ['outside', 'inside', ''], - editType: 'ticks', - description: [ - 'Determines whether ticks are drawn or not.', - 'If **, this axis\' ticks are not drawn.', - 'If *outside* (*inside*), this axis\' are drawn outside (inside)', - 'the axis lines.' - ].join(' ') - }, + ticks: ticks, tickson: { valType: 'enumerated', values: ['labels', 'boundaries'], @@ -537,26 +615,9 @@ module.exports = { 'on all shared-axes subplots.' ].join(' ') }, - ticklen: { - valType: 'number', - min: 0, - dflt: 5, - editType: 'ticks', - description: 'Sets the tick length (in px).' - }, - tickwidth: { - valType: 'number', - min: 0, - dflt: 1, - editType: 'ticks', - description: 'Sets the tick width (in px).' - }, - tickcolor: { - valType: 'color', - dflt: colorAttrs.defaultLine, - editType: 'ticks', - description: 'Sets the tick color.' - }, + ticklen: makeTicklen(), + tickwidth: makeTickwidth(), + tickcolor: tickcolor, showticklabels: { valType: 'boolean', dflt: true, @@ -776,28 +837,11 @@ module.exports = { editType: 'ticks+layoutstyle', description: 'Sets the width (in px) of the axis line.' }, - showgrid: { - valType: 'boolean', - editType: 'ticks', - description: [ - 'Determines whether or not grid lines are drawn.', - 'If *true*, the grid lines are drawn at every tick mark.' - ].join(' ') - }, - gridcolor: { - valType: 'color', - dflt: colorAttrs.lightLine, - editType: 'ticks', - description: 'Sets the color of the grid lines.' - }, - gridwidth: { - valType: 'number', - min: 0, - dflt: 1, - editType: 'ticks', - description: 'Sets the width (in px) of the grid lines.' - }, - griddash: extendFlat({}, dash, {editType: 'ticks'}), + showgrid: showgrid, + gridcolor: gridcolor, + gridwidth: makeGridwidth(), + griddash: griddash, + zeroline: { valType: 'boolean', editType: 'ticks', @@ -899,6 +943,26 @@ module.exports = { 'axis will be visible.' ].join(' ') }, + + minor: { + tickmode: tickmode, + nticks: makeNticks('minor'), + tick0: tick0, + dtick: dtick, + tickvals: tickvals, + ticks: ticks, + ticklen: makeTicklen('minor'), + tickwidth: makeTickwidth('minor'), + tickcolor: tickcolor, + + gridcolor: gridcolor, + gridwidth: makeGridwidth('minor'), + griddash: griddash, + showgrid: showgrid, + + editType: 'ticks' + }, + layer: { valType: 'enumerated', values: ['above traces', 'below traces'], diff --git a/src/plots/cartesian/layout_defaults.js b/src/plots/cartesian/layout_defaults.js index dc703752eb7..c24a7c34f1a 100644 --- a/src/plots/cartesian/layout_defaults.js +++ b/src/plots/cartesian/layout_defaults.js @@ -229,6 +229,7 @@ module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, fullData) { )); var defaultOptions = { + hasMinor: true, letter: axLetter, font: layoutOut.font, outerTicks: outerTicks[axName], diff --git a/src/plots/cartesian/line_grid_defaults.js b/src/plots/cartesian/line_grid_defaults.js index fe04c508c8f..062c5f2493c 100644 --- a/src/plots/cartesian/line_grid_defaults.js +++ b/src/plots/cartesian/line_grid_defaults.js @@ -1,7 +1,7 @@ 'use strict'; var colorMix = require('tinycolor2').mix; -var lightFraction = require('../../components/color/attributes').lightFraction; +var colorAttrs = require('../../components/color/attributes'); var Lib = require('../../lib'); /** @@ -32,11 +32,15 @@ module.exports = function handleLineGridDefaults(containerIn, containerOut, coer delete containerOut.linewidth; } - var gridColorDflt = colorMix(dfltColor, opts.bgColor, opts.blend || lightFraction).toRgbString(); + var gridColorDflt = colorMix(dfltColor, opts.bgColor, opts.blend || colorAttrs.lightFraction).toRgbString(); var gridColor = coerce2('gridcolor', gridColorDflt); var gridWidth = coerce2('gridwidth'); var gridDash = coerce2('griddash'); - var showGridLines = coerce('showgrid', opts.showGrid || !!gridColor || !!gridWidth || !!gridDash); + var showGridLines = coerce('showgrid', opts.showGrid || + !!gridColor || + !!gridWidth || + !!gridDash + ); if(!showGridLines) { delete containerOut.gridcolor; @@ -44,6 +48,24 @@ module.exports = function handleLineGridDefaults(containerIn, containerOut, coer delete containerOut.griddash; } + if(opts.hasMinor) { + var minorGridColorDflt = colorMix(containerOut.gridcolor, opts.bgColor, 67).toRgbString(); + var minorGridColor = coerce2('minor.gridcolor', minorGridColorDflt); + var minorGridWidth = coerce2('minor.gridwidth', containerOut.gridwidth || 1); + var minorGridDash = coerce2('minor.griddash', containerOut.griddash || 'solid'); + var minorShowGridLines = coerce('minor.showgrid', + !!minorGridColor || + !!minorGridWidth || + !!minorGridDash + ); + + if(!minorShowGridLines) { + delete containerOut.minor.gridcolor; + delete containerOut.minor.gridwidth; + delete containerOut.minor.griddash; + } + } + if(!opts.noZeroLine) { var zeroLineColor = coerce2('zerolinecolor', dfltColor); var zeroLineWidth = coerce2('zerolinewidth'); diff --git a/src/plots/cartesian/tick_mark_defaults.js b/src/plots/cartesian/tick_mark_defaults.js index 6a76c37142b..38c58deb124 100644 --- a/src/plots/cartesian/tick_mark_defaults.js +++ b/src/plots/cartesian/tick_mark_defaults.js @@ -9,14 +9,22 @@ var layoutAttributes = require('./layout_attributes'); * options: inherits outerTicks from axes.handleAxisDefaults */ module.exports = function handleTickMarkDefaults(containerIn, containerOut, coerce, options) { - var tickLen = Lib.coerce2(containerIn, containerOut, layoutAttributes, 'ticklen'); - var tickWidth = Lib.coerce2(containerIn, containerOut, layoutAttributes, 'tickwidth'); - var tickColor = Lib.coerce2(containerIn, containerOut, layoutAttributes, 'tickcolor', containerOut.color); - var showTicks = coerce('ticks', (options.outerTicks || tickLen || tickWidth || tickColor) ? 'outside' : ''); + var isMinor = options.isMinor; + var cIn = isMinor ? containerIn.minor || {} : containerIn; + var cOut = isMinor ? containerOut.minor : containerOut; + var lAttr = isMinor ? layoutAttributes.minor : layoutAttributes; + var prefix = isMinor ? 'minor.' : ''; + + var tickLen = Lib.coerce2(cIn, cOut, lAttr, 'ticklen', isMinor ? ((containerOut.ticklen || 5) * 0.6) : undefined); + var tickWidth = Lib.coerce2(cIn, cOut, lAttr, 'tickwidth', isMinor ? (containerOut.tickwidth || 1) : undefined); + var tickColor = Lib.coerce2(cIn, cOut, lAttr, 'tickcolor', (isMinor ? containerOut.tickcolor : undefined) || cOut.color); + var showTicks = coerce(prefix + 'ticks', ( + (!isMinor && options.outerTicks) || tickLen || tickWidth || tickColor + ) ? 'outside' : ''); if(!showTicks) { - delete containerOut.ticklen; - delete containerOut.tickwidth; - delete containerOut.tickcolor; + delete cOut.ticklen; + delete cOut.tickwidth; + delete cOut.tickcolor; } }; diff --git a/src/plots/cartesian/tick_value_defaults.js b/src/plots/cartesian/tick_value_defaults.js index 1b6a06902b3..e6ffb8f6a5d 100644 --- a/src/plots/cartesian/tick_value_defaults.js +++ b/src/plots/cartesian/tick_value_defaults.js @@ -3,12 +3,18 @@ var cleanTicks = require('./clean_ticks'); var isArrayOrTypedArray = require('../../lib').isArrayOrTypedArray; -module.exports = function handleTickValueDefaults(containerIn, containerOut, coerce, axType) { +module.exports = function handleTickValueDefaults(containerIn, containerOut, coerce, axType, opts) { + if(!opts) opts = {}; + var isMinor = opts.isMinor; + var cIn = isMinor ? containerIn.minor || {} : containerIn; + var cOut = isMinor ? containerOut.minor : containerOut; + var prefix = isMinor ? 'minor.' : ''; + function readInput(attr) { - var v = containerIn[attr]; + var v = cIn[attr]; return ( v !== undefined - ) ? v : (containerOut._template || {})[attr]; + ) ? v : (cOut._template || {})[attr]; } var _tick0 = readInput('tick0'); @@ -18,20 +24,21 @@ module.exports = function handleTickValueDefaults(containerIn, containerOut, coe var tickmodeDefault = isArrayOrTypedArray(_tickvals) ? 'array' : _dtick ? 'linear' : 'auto'; - var tickmode = coerce('tickmode', tickmodeDefault); + var tickmode = coerce(prefix + 'tickmode', tickmodeDefault); - if(tickmode === 'auto') coerce('nticks'); - else if(tickmode === 'linear') { + if(tickmode === 'auto') { + coerce(prefix + 'nticks'); + } else if(tickmode === 'linear') { // dtick is usually a positive number, but there are some // special strings available for log or date axes // tick0 also has special logic - var dtick = containerOut.dtick = cleanTicks.dtick( + var dtick = cOut.dtick = cleanTicks.dtick( _dtick, axType); - containerOut.tick0 = cleanTicks.tick0( + cOut.tick0 = cleanTicks.tick0( _tick0, axType, containerOut.calendar, dtick); } else if(axType !== 'multicategory') { - var tickvals = coerce('tickvals'); - if(tickvals === undefined) containerOut.tickmode = 'auto'; - else coerce('ticktext'); + var tickvals = coerce(prefix + 'tickvals'); + if(tickvals === undefined) cOut.tickmode = 'auto'; + else if(!isMinor) coerce('ticktext'); } }; diff --git a/test/image/baselines/11.png b/test/image/baselines/11.png index dcdd4a2ca6c..5f238ccfffd 100644 Binary files a/test/image/baselines/11.png and b/test/image/baselines/11.png differ diff --git a/test/image/baselines/14.png b/test/image/baselines/14.png index a8bf354b7c3..ce59b527c1b 100644 Binary files a/test/image/baselines/14.png and b/test/image/baselines/14.png differ diff --git a/test/image/baselines/15.png b/test/image/baselines/15.png index 07a0869e350..126d8d74c85 100644 Binary files a/test/image/baselines/15.png and b/test/image/baselines/15.png differ diff --git a/test/image/baselines/17.png b/test/image/baselines/17.png index fcfd9d95da9..3b1d48b8e77 100644 Binary files a/test/image/baselines/17.png and b/test/image/baselines/17.png differ diff --git a/test/image/baselines/19.png b/test/image/baselines/19.png index 6325afa746d..e98f113080f 100644 Binary files a/test/image/baselines/19.png and b/test/image/baselines/19.png differ diff --git a/test/image/baselines/24.png b/test/image/baselines/24.png index c2081da4df9..664db11f2c4 100644 Binary files a/test/image/baselines/24.png and b/test/image/baselines/24.png differ diff --git a/test/image/baselines/28.png b/test/image/baselines/28.png index 28c054d5b30..6e2f8ac543a 100644 Binary files a/test/image/baselines/28.png and b/test/image/baselines/28.png differ diff --git a/test/image/baselines/32.png b/test/image/baselines/32.png index 0899b6b8d06..73c3ea4fdab 100644 Binary files a/test/image/baselines/32.png and b/test/image/baselines/32.png differ diff --git a/test/image/baselines/date_axes_period2.png b/test/image/baselines/date_axes_period2.png index 2265edae798..1606f5d87e5 100644 Binary files a/test/image/baselines/date_axes_period2.png and b/test/image/baselines/date_axes_period2.png differ diff --git a/test/image/baselines/h-colorbar01.png b/test/image/baselines/h-colorbar01.png index 69b3bae0fcc..3d19cb619ed 100644 Binary files a/test/image/baselines/h-colorbar01.png and b/test/image/baselines/h-colorbar01.png differ diff --git a/test/image/baselines/h-colorbar02.png b/test/image/baselines/h-colorbar02.png index f6d4f1e2c90..12109468a8d 100644 Binary files a/test/image/baselines/h-colorbar02.png and b/test/image/baselines/h-colorbar02.png differ diff --git a/test/image/baselines/h-colorbar03.png b/test/image/baselines/h-colorbar03.png index a2428f96621..616a8cd344c 100644 Binary files a/test/image/baselines/h-colorbar03.png and b/test/image/baselines/h-colorbar03.png differ diff --git a/test/image/baselines/h-colorbar04.png b/test/image/baselines/h-colorbar04.png index a3b2fb729ed..cb960586896 100644 Binary files a/test/image/baselines/h-colorbar04.png and b/test/image/baselines/h-colorbar04.png differ diff --git a/test/image/baselines/h-colorbar_airfoil.png b/test/image/baselines/h-colorbar_airfoil.png index 77ca9b3609d..ce648549155 100644 Binary files a/test/image/baselines/h-colorbar_airfoil.png and b/test/image/baselines/h-colorbar_airfoil.png differ diff --git a/test/image/baselines/heatmap_xyz-dates-and-categories.png b/test/image/baselines/heatmap_xyz-dates-and-categories.png index 29be7d0fcd5..8b0f24b5de9 100644 Binary files a/test/image/baselines/heatmap_xyz-dates-and-categories.png and b/test/image/baselines/heatmap_xyz-dates-and-categories.png differ diff --git a/test/image/baselines/legend_horizontal_autowrap.png b/test/image/baselines/legend_horizontal_autowrap.png index e108f39d57e..c7f92fb58e1 100644 Binary files a/test/image/baselines/legend_horizontal_autowrap.png and b/test/image/baselines/legend_horizontal_autowrap.png differ diff --git a/test/image/baselines/mirror-all-vs-allticks.png b/test/image/baselines/mirror-all-vs-allticks.png index a05a50f116a..6b5a48acde4 100644 Binary files a/test/image/baselines/mirror-all-vs-allticks.png and b/test/image/baselines/mirror-all-vs-allticks.png differ diff --git a/test/image/baselines/ticklabelposition-0.png b/test/image/baselines/ticklabelposition-0.png index 960b14b28ca..d1caded77d2 100644 Binary files a/test/image/baselines/ticklabelposition-0.png and b/test/image/baselines/ticklabelposition-0.png differ diff --git a/test/image/baselines/ticklabelposition-1.png b/test/image/baselines/ticklabelposition-1.png index a01cd612ea7..60747200880 100644 Binary files a/test/image/baselines/ticklabelposition-1.png and b/test/image/baselines/ticklabelposition-1.png differ diff --git a/test/image/baselines/ticklabelposition-2.png b/test/image/baselines/ticklabelposition-2.png index 0082752718c..d8192ec8e82 100644 Binary files a/test/image/baselines/ticklabelposition-2.png and b/test/image/baselines/ticklabelposition-2.png differ diff --git a/test/image/baselines/ticklabelposition-5.png b/test/image/baselines/ticklabelposition-5.png index 60f4bd86f23..b899794805a 100644 Binary files a/test/image/baselines/ticklabelposition-5.png and b/test/image/baselines/ticklabelposition-5.png differ diff --git a/test/image/mocks/11.json b/test/image/mocks/11.json index b2091806455..dc6197eb91d 100644 --- a/test/image/mocks/11.json +++ b/test/image/mocks/11.json @@ -176,6 +176,7 @@ "autorange": true }, "yaxis": { + "minor": { "showgrid": true }, "title": { "text": "Length", "font": { diff --git a/test/image/mocks/14.json b/test/image/mocks/14.json index 2eb9f882110..eb8bd66436b 100644 --- a/test/image/mocks/14.json +++ b/test/image/mocks/14.json @@ -156,6 +156,7 @@ "autorange": true }, "yaxis": { + "minor": { "showgrid": true }, "title": { "text": "Cost ($/WP)" }, diff --git a/test/image/mocks/15.json b/test/image/mocks/15.json index d19e2f40a27..3b47e6fe28b 100644 --- a/test/image/mocks/15.json +++ b/test/image/mocks/15.json @@ -417,6 +417,7 @@ "width": 800, "height": 420, "xaxis": { + "minor": { "ticks": "outside" }, "title": { "text": "Year" }, diff --git a/test/image/mocks/17.json b/test/image/mocks/17.json index c4ef06418bd..e397ab43374 100644 --- a/test/image/mocks/17.json +++ b/test/image/mocks/17.json @@ -584,6 +584,10 @@ "width": 800, "height": 440, "xaxis": { + "minor": { + "ticks": "inside", + "showgrid": true + }, "title": { "text": "Time (s)" }, @@ -630,6 +634,10 @@ "autorange": false }, "yaxis": { + "minor": { + "ticks": "outside", + "showgrid": true + }, "title": { "text": "Velocity (m/s)" }, diff --git a/test/image/mocks/19.json b/test/image/mocks/19.json index f179b563b18..007d414dc63 100644 --- a/test/image/mocks/19.json +++ b/test/image/mocks/19.json @@ -45,6 +45,7 @@ "width": 1152, "height": 481, "xaxis": { + "minor": { "showgrid": true }, "title": { "text": "Click to enter X axis title" }, @@ -91,6 +92,7 @@ "autorange": true }, "yaxis": { + "minor": { "showgrid": true }, "title": { "text": "Click to enter Y axis title" }, diff --git a/test/image/mocks/24.json b/test/image/mocks/24.json index ab2ad0513bc..c5287bcfee5 100644 --- a/test/image/mocks/24.json +++ b/test/image/mocks/24.json @@ -73,6 +73,14 @@ "width": 1276, "height": 506, "xaxis": { + "minor": { + "ticklen": 8, + "tickwidth": 4, + "tickcolor": "blue", + "griddash": "dash", + "gridwidth": 2, + "gridcolor": "cyan" + }, "title": { "text": "Click to enter X axis title" }, @@ -119,6 +127,14 @@ "autorange": true }, "yaxis": { + "minor": { + "ticklen": 6, + "tickwidth": 3, + "tickcolor": "red", + "griddash": "6px", + "gridwidth": 3, + "gridcolor": "yellow" + }, "title": { "text": "Click to enter Y axis title" }, diff --git a/test/image/mocks/28.json b/test/image/mocks/28.json index 355b19b6827..218bebf3c78 100644 --- a/test/image/mocks/28.json +++ b/test/image/mocks/28.json @@ -886,6 +886,7 @@ "width": 606, "height": 400, "xaxis": { + "minor": { "ticks": "outside" }, "title": { "text": "Note: Data are for cash income. Source: Piketty and Saez (2007)", "font": { @@ -936,6 +937,7 @@ "autorange": false }, "yaxis": { + "minor": { "ticks": "outside" }, "title": { "text": "Tax Rate (%)" }, diff --git a/test/image/mocks/32.json b/test/image/mocks/32.json index e3ab3de6d14..a0ac4b93c3e 100644 --- a/test/image/mocks/32.json +++ b/test/image/mocks/32.json @@ -1023,6 +1023,7 @@ "width": 1366, "height": 713, "xaxis": { + "minor": { "ticks": "outside" }, "title": { "text": "Click to enter X axis title" }, @@ -1069,6 +1070,7 @@ "autorange": true }, "yaxis": { + "minor": { "ticks": "outside" }, "title": { "text": "Click to enter Y axis title" }, diff --git a/test/image/mocks/date_axes_period2.json b/test/image/mocks/date_axes_period2.json index a1f3fbcc05b..eb62505a49c 100644 --- a/test/image/mocks/date_axes_period2.json +++ b/test/image/mocks/date_axes_period2.json @@ -130,6 +130,7 @@ ] }, "xaxis": { + "minor": { "ticks": "inside" }, "tickcolor": "black", "gridcolor": "orange", "range": [ @@ -140,6 +141,7 @@ "tickformat": "%b %d, %Y" }, "xaxis2": { + "minor": { "ticks": "inside" }, "tickcolor": "black", "gridcolor": "orange", "range": [ @@ -150,6 +152,7 @@ "anchor": "y2" }, "xaxis3": { + "minor": { "ticks": "inside" }, "tickcolor": "black", "gridcolor": "orange", "range": [ @@ -160,6 +163,7 @@ "anchor": "y3" }, "xaxis4": { + "minor": { "ticks": "inside" }, "tickcolor": "black", "gridcolor": "orange", "range": [ @@ -170,6 +174,7 @@ "anchor": "y4" }, "xaxis5": { + "minor": { "ticks": "inside" }, "tickcolor": "black", "gridcolor": "orange", "range": [ @@ -180,6 +185,7 @@ "anchor": "y5" }, "xaxis6": { + "minor": { "ticks": "inside" }, "tickcolor": "black", "gridcolor": "orange", "range": [ @@ -190,6 +196,7 @@ "anchor": "y6" }, "xaxis7": { + "minor": { "ticks": "inside" }, "tickcolor": "black", "gridcolor": "orange", "range": [ diff --git a/test/image/mocks/h-colorbar01.json b/test/image/mocks/h-colorbar01.json index bcc18cfa530..1b476d9913b 100644 --- a/test/image/mocks/h-colorbar01.json +++ b/test/image/mocks/h-colorbar01.json @@ -35,6 +35,16 @@ "font": { "size": 24 } + }, + "xaxis": { + "minor": { + "showgrid": true + } + }, + "yaxis": { + "minor": { + "showgrid": true + } } } } diff --git a/test/image/mocks/h-colorbar02.json b/test/image/mocks/h-colorbar02.json index 278c3158f2f..f22b6e38194 100644 --- a/test/image/mocks/h-colorbar02.json +++ b/test/image/mocks/h-colorbar02.json @@ -41,6 +41,32 @@ "font": { "size": 24 } + }, + "xaxis": { + "zerolinecolor": "white", + "zerolinewidth": "2", + "gridwidth": "2", + "gridcolor": "black", + "minor": { + "griddash": "dot", + "ticks": "inside", + "ticklen": 12, + "tickwidth": 3, + "tickcolor": "green" + } + }, + "yaxis": { + "zerolinecolor": "white", + "zerolinewidth": "2", + "gridwidth": "2", + "gridcolor": "black", + "minor": { + "griddash": "dot", + "ticks": "outside", + "ticklen": 12, + "tickwidth": 3, + "tickcolor": "green" + } } } } diff --git a/test/image/mocks/h-colorbar03.json b/test/image/mocks/h-colorbar03.json index 424130a2ad9..fd2c742d267 100644 --- a/test/image/mocks/h-colorbar03.json +++ b/test/image/mocks/h-colorbar03.json @@ -42,6 +42,22 @@ "font": { "size": 24 } + }, + "xaxis": { + "gridwidth": 3, + "gridcolor": "gray", + "tickvals": [0.25, 0.75], + "minor": { + "griddash": "dash" + } + }, + "yaxis": { + "gridwidth": 3, + "gridcolor": "green", + "minor": { + "tickvals": [0.25, 0.75], + "griddash": "dash" + } } } } diff --git a/test/image/mocks/h-colorbar04.json b/test/image/mocks/h-colorbar04.json index 95724bb7503..dd66a9d6aaa 100644 --- a/test/image/mocks/h-colorbar04.json +++ b/test/image/mocks/h-colorbar04.json @@ -43,6 +43,20 @@ "font": { "size": 24 } + }, + "xaxis": { + "gridwidth": 2, + "gridcolor": "gray", + "minor": { + "showgrid": true + } + }, + "yaxis": { + "gridwidth": 2, + "gridcolor": "gray", + "minor": { + "showgrid": true + } } } } diff --git a/test/image/mocks/h-colorbar_airfoil.json b/test/image/mocks/h-colorbar_airfoil.json index 9593301e393..b9fb60721d9 100644 --- a/test/image/mocks/h-colorbar_airfoil.json +++ b/test/image/mocks/h-colorbar_airfoil.json @@ -7,6 +7,7 @@ "orientation": "h" }, "yaxis":{ + "minor": { "ticks": "outside" }, "zeroline":false, "range":[ -1.800, @@ -17,6 +18,7 @@ "dragmode":"pan", "height":700, "xaxis":{ + "minor": { "ticks": "outside" }, "zeroline":false, "scaleratio":1, "scaleanchor":"y", diff --git a/test/image/mocks/heatmap_xyz-dates-and-categories.json b/test/image/mocks/heatmap_xyz-dates-and-categories.json index d0c7aad9be4..350342bd441 100644 --- a/test/image/mocks/heatmap_xyz-dates-and-categories.json +++ b/test/image/mocks/heatmap_xyz-dates-and-categories.json @@ -282,6 +282,7 @@ } ], "layout": { + "xaxis": { "minor": { "ticks": "outside" } }, "title": {"text": "heatmap xyz with date & categories"} } } diff --git a/test/image/mocks/legend_horizontal_autowrap.json b/test/image/mocks/legend_horizontal_autowrap.json index 8f6d5fa950c..3b2db63cf65 100644 --- a/test/image/mocks/legend_horizontal_autowrap.json +++ b/test/image/mocks/legend_horizontal_autowrap.json @@ -440,6 +440,16 @@ "autorange": false, "tickmode": "array", "range": [0.4, 4.6], + "minor": { + "tickvals": [1.5, 2.5, 3.5], + "ticks": "inside", + "tickwidth": 100, + "ticklen": 5, + "tickcolor": "orange", + "gridcolor": "yellow", + "gridwidth": 100, + "griddash": "5px" + }, "ticktext": ["2012", "2013", "2014", "2015"], "tickvals": [1, 2, 3, 4], "ticks": "", diff --git a/test/image/mocks/mirror-all-vs-allticks.json b/test/image/mocks/mirror-all-vs-allticks.json index d48df50b5db..17c3bd1cabe 100644 --- a/test/image/mocks/mirror-all-vs-allticks.json +++ b/test/image/mocks/mirror-all-vs-allticks.json @@ -28,18 +28,21 @@ "columns": 2 }, "xaxis": { + "minor": {"ticks": "inside"}, "title": {"text": "mirror:all"}, "ticks": "outside", "showline": true, "mirror": "all" }, "xaxis2": { + "minor": {"ticks": "inside"}, "title": {"text": "mirror:allticks"}, "ticks": "outside", "showline": true, "mirror": "allticks" }, "yaxis": { + "minor": {"ticks": "inside"}, "title": {"text": "mirror:all"}, "ticks": "outside", "showline": true, @@ -47,6 +50,7 @@ "mirror": "all" }, "yaxis2": { + "minor": {"ticks": "inside"}, "title": {"text": "mirror:allticks"}, "ticks": "outside", "showline": true, diff --git a/test/image/mocks/ticklabelposition-0.json b/test/image/mocks/ticklabelposition-0.json index 4845e55b7d5..6f3d8f35bad 100644 --- a/test/image/mocks/ticklabelposition-0.json +++ b/test/image/mocks/ticklabelposition-0.json @@ -22,6 +22,7 @@ }], "layout": { "xaxis": { + "minor": { "showgrid": true }, "anchor": "y", "domain": [0, 0.475], "side": "bottom", @@ -34,6 +35,7 @@ "gridcolor": "white" }, "yaxis": { + "minor": { "showgrid": true }, "anchor": "x", "domain": [0, 0.475], "autorange": "reversed", @@ -48,6 +50,7 @@ "gridcolor": "white" }, "xaxis2": { + "minor": { "showgrid": true }, "anchor": "y2", "domain": [0.525, 1], "autorange": "reversed", @@ -61,6 +64,7 @@ "gridcolor": "white" }, "yaxis2": { + "minor": { "showgrid": true }, "anchor": "x2", "domain": [0, 0.475], "type": "log", @@ -74,6 +78,7 @@ "gridcolor": "white" }, "xaxis3": { + "minor": { "showgrid": true }, "anchor": "y3", "domain": [0.525, 1], "side": "top", @@ -86,6 +91,7 @@ "gridcolor": "white" }, "yaxis3": { + "minor": { "showgrid": true }, "anchor": "x3", "domain": [0.525, 1], "autorange": "reversed", @@ -100,6 +106,7 @@ "gridcolor": "white" }, "xaxis4": { + "minor": { "showgrid": true }, "anchor": "y4", "domain": [0, 0.475], "autorange": "reversed", @@ -114,6 +121,7 @@ "gridcolor": "white" }, "yaxis4": { + "minor": { "showgrid": true }, "anchor": "x4", "domain": [0.525, 1], "type": "log", diff --git a/test/image/mocks/ticklabelposition-1.json b/test/image/mocks/ticklabelposition-1.json index a96b7af9114..17a52853c6c 100644 --- a/test/image/mocks/ticklabelposition-1.json +++ b/test/image/mocks/ticklabelposition-1.json @@ -22,6 +22,7 @@ }], "layout": { "xaxis": { + "minor": { "showgrid": true }, "anchor": "y", "domain": [0, 0.475], "type": "date", @@ -36,6 +37,7 @@ "gridcolor": "white" }, "yaxis": { + "minor": { "showgrid": true }, "anchor": "x", "domain": [0, 0.475], "autorange": "reversed", @@ -50,6 +52,7 @@ "gridcolor": "white" }, "xaxis2": { + "minor": { "showgrid": true }, "anchor": "y2", "domain": [0.525, 1], "autorange": "reversed", @@ -64,6 +67,7 @@ "gridcolor": "white" }, "yaxis2": { + "minor": { "showgrid": true }, "anchor": "x2", "domain": [0, 0.475], "type": "log", @@ -77,6 +81,7 @@ "gridcolor": "white" }, "xaxis3": { + "minor": { "showgrid": true }, "anchor": "y3", "domain": [0.525, 1], "type": "date", @@ -90,6 +95,7 @@ "gridcolor": "white" }, "yaxis3": { + "minor": { "showgrid": true }, "anchor": "x3", "domain": [0.525, 1], "autorange": "reversed", @@ -104,6 +110,7 @@ "gridcolor": "white" }, "xaxis4": { + "minor": { "showgrid": true }, "anchor": "y4", "domain": [0, 0.475], "autorange": "reversed", @@ -119,6 +126,7 @@ "gridcolor": "white" }, "yaxis4": { + "minor": { "showgrid": true }, "anchor": "x4", "domain": [0.525, 1], "type": "log", diff --git a/test/image/mocks/ticklabelposition-2.json b/test/image/mocks/ticklabelposition-2.json index 982e1899cf9..4c6eab154c5 100644 --- a/test/image/mocks/ticklabelposition-2.json +++ b/test/image/mocks/ticklabelposition-2.json @@ -22,6 +22,7 @@ }], "layout": { "yaxis": { + "minor": { "showgrid": true }, "anchor": "x", "domain": [0, 0.475], "type": "date", @@ -38,6 +39,7 @@ "gridcolor": "rgb(191,191,191)" }, "xaxis": { + "minor": { "showgrid": true }, "anchor": "y", "domain": [0, 0.475], "autorange": "reversed", @@ -54,6 +56,7 @@ "gridcolor": "rgb(191,191,191)" }, "yaxis2": { + "minor": { "showgrid": true }, "anchor": "x2", "domain": [0.525, 1], "autorange": "reversed", @@ -70,6 +73,7 @@ "gridcolor": "rgb(191,191,191)" }, "xaxis2": { + "minor": { "showgrid": true }, "anchor": "y2", "domain": [0, 0.475], "type": "log", @@ -85,6 +89,7 @@ "gridcolor": "rgb(191,191,191)" }, "yaxis3": { + "minor": { "showgrid": true }, "anchor": "x3", "domain": [0.525, 1], "type": "date", @@ -100,6 +105,7 @@ "gridcolor": "rgb(191,191,191)" }, "xaxis3": { + "minor": { "showgrid": true }, "anchor": "y3", "domain": [0.525, 1], "autorange": "reversed", @@ -134,6 +140,7 @@ "gridcolor": "rgb(191,191,191)" }, "xaxis4": { + "minor": { "showgrid": true }, "anchor": "y4", "domain": [0.525, 1], "type": "log", diff --git a/test/image/mocks/ticklabelposition-5.json b/test/image/mocks/ticklabelposition-5.json index 6342f69f89b..bc9ac23d003 100644 --- a/test/image/mocks/ticklabelposition-5.json +++ b/test/image/mocks/ticklabelposition-5.json @@ -110,6 +110,7 @@ }], "layout": { "xaxis": { + "minor": { "showgrid": true }, "rangemode": "nonnegative", "anchor": "y", "domain": [0, 0.475], @@ -118,6 +119,7 @@ "gridcolor": "white" }, "yaxis": { + "minor": { "showgrid": true }, "anchor": "x", "domain": [0, 0.475], "side": "left", @@ -125,6 +127,7 @@ "gridcolor": "white" }, "xaxis2": { + "minor": { "showgrid": true }, "rangemode": "nonnegative", "anchor": "y2", "domain": [0.525, 1], @@ -133,6 +136,7 @@ "gridcolor": "white" }, "yaxis2": { + "minor": { "showgrid": true }, "anchor": "x2", "domain": [0, 0.475], "side": "right", @@ -140,6 +144,7 @@ "gridcolor": "white" }, "xaxis3": { + "minor": { "showgrid": true }, "anchor": "y3", "domain": [0.525, 1], "side": "top", @@ -147,6 +152,7 @@ "gridcolor": "white" }, "yaxis3": { + "minor": { "showgrid": true }, "rangemode": "nonnegative", "anchor": "x3", "domain": [0.525, 1], @@ -155,6 +161,7 @@ "gridcolor": "white" }, "xaxis4": { + "minor": { "showgrid": true }, "anchor": "y4", "domain": [0, 0.475], "side": "top", diff --git a/test/jasmine/tests/axes_test.js b/test/jasmine/tests/axes_test.js index 6793b526397..9033adb6b40 100644 --- a/test/jasmine/tests/axes_test.js +++ b/test/jasmine/tests/axes_test.js @@ -6416,6 +6416,728 @@ describe('Test axes', function() { }); }); }); + + describe('minor ticks"', function() { + var gd; + + beforeEach(function() { + gd = createGraphDiv(); + }); + + afterEach(destroyGraphDiv); + + function _assert(expPositions) { + var ax = gd._fullLayout.xaxis; + + // minor positions + var positions = + ax._vals + .filter(function(d) { return d.minor; }) + .map(function(d) { return d.x; }); + + expect(positions).toEqual(expPositions); + } + + it('minor tickvals', function(done) { + Plotly.newPlot(gd, { + data: [{ + x: [10, 100] + }], + layout: { + width: 1000, + xaxis: { + minor: { + tickvals: [12, 34, 56, 78], + showgrid: true + } + } + } + }) + .then(function() { + _assert([ 12, 34, 56, 78 ]); + }) + .then(done, done.fail); + }); + + it('linear auto', function(done) { + Plotly.newPlot(gd, { + data: [{ + x: [10, 100] + }], + layout: { + width: 1000, + xaxis: { + minor: { + showgrid: true + } + } + } + }) + .then(function() { + _assert([ 5, 10, 15, 25, 30, 35, 45, 50, 55, 65, 70, 75, 85, 90, 95, 105 ]); + }) + .then(done, done.fail); + }); + + it('linear auto with defined minor nticks: 2', function(done) { + Plotly.newPlot(gd, { + data: [{ + x: [10, 100] + }], + layout: { + width: 1000, + xaxis: { + minor: { + nticks: 2, + showgrid: true + } + } + } + }) + .then(function() { + _assert([ 10, 30, 50, 70, 90 ]); + }) + .then(done, done.fail); + }); + + it('linear auto with defined minor nticks: 7', function(done) { + Plotly.newPlot(gd, { + data: [{ + x: [10, 100] + }], + layout: { + width: 1000, + xaxis: { + minor: { + nticks: 7, + showgrid: true + } + } + } + }) + .then(function() { + _assert([ 5, 10, 15, 25, 30, 35, 45, 50, 55, 65, 70, 75, 85, 90, 95, 105 ]); + }) + .then(done, done.fail); + }); + + it('linear auto with defined minor nticks: 10', function(done) { + Plotly.newPlot(gd, { + data: [{ + x: [10, 100] + }], + layout: { + width: 1000, + xaxis: { + minor: { + nticks: 10, + showgrid: true + } + } + } + }) + .then(function() { + _assert([ 6, 8, 10, 12, 14, 16, 18, 22, 24, 26, 28, 30, 32, 34, 36, 38, 42, 44, 46, 48, 50, 52, 54, 56, 58, 62, 64, 66, 68, 70, 72, 74, 76, 78, 82, 84, 86, 88, 90, 92, 94, 96, 98, 102, 104 ]); + }) + .then(done, done.fail); + }); + + it('linear auto with defined minor dtick: 2.5', function(done) { + Plotly.newPlot(gd, { + data: [{ + x: [10, 100] + }], + layout: { + width: 1000, + xaxis: { + minor: { + dtick: 2.5, + showgrid: true + } + } + } + }) + .then(function() { + _assert([ 5, 7.5, 10, 12.5, 15, 17.5, 22.5, 25, 27.5, 30, 32.5, 35, 37.5, 42.5, 45, 47.5, 50, 52.5, 55, 57.5, 62.5, 65, 67.5, 70, 72.5, 75, 77.5, 82.5, 85, 87.5, 90, 92.5, 95, 97.5, 102.5, 105 ]); + }) + .then(done, done.fail); + }); + + it('linear auto with defined major dtick: 10', function(done) { + Plotly.newPlot(gd, { + data: [{ + x: [10, 100] + }], + layout: { + width: 1000, + xaxis: { + dtick: 10, + minor: { + showgrid: true + } + } + } + }) + .then(function() { + _assert([ 6, 8, 12, 14, 16, 18, 22, 24, 26, 28, 32, 34, 36, 38, 42, 44, 46, 48, 52, 54, 56, 58, 62, 64, 66, 68, 72, 74, 76, 78, 82, 84, 86, 88, 92, 94, 96, 98, 102, 104 ]); + }) + .then(done, done.fail); + }); + + it('linear with defined major & minor dtick', function(done) { + Plotly.newPlot(gd, { + data: [{ + x: [10, 100] + }], + layout: { + width: 1000, + xaxis: { + dtick: 10, + minor: { + dtick: 5, + showgrid: true + } + } + } + }) + .then(function() { + _assert([ 5, 15, 25, 35, 45, 55, 65, 75, 85, 95, 105 ]); + }) + .then(done, done.fail); + }); + + it('log auto - case 1', function(done) { + Plotly.newPlot(gd, { + data: [{ + x: [1, 10e20] + }], + layout: { + width: 1000, + xaxis: { + type: 'log', + minor: { + showgrid: true + } + } + } + }) + .then(function() { + _assert([ -1, 1, 2, 4, 5, 7, 8, 10, 11, 13, 14, 16, 17, 19, 20, 22 ]); + }) + .then(done, done.fail); + }); + + it('log auto - case 2', function(done) { + Plotly.newPlot(gd, { + data: [{ + x: [1, 10e10] + }], + layout: { + width: 1000, + xaxis: { + type: 'log', + minor: { + showgrid: true + } + } + } + }) + .then(function() { + _assert([ 1, 3, 5, 7, 9, 11 ]); + }) + .then(done, done.fail); + }); + + it('log auto - case 3', function(done) { + Plotly.newPlot(gd, { + data: [{ + x: [1, 10e3] + }], + layout: { + width: 1000, + xaxis: { + type: 'log', + minor: { + showgrid: true + } + } + } + }) + .then(function() { + _assert([ -0.22184874961635648, -0.1549019599857433, -0.09691001300805657, -0.04575749056067513, 0.4771212547196623, 0.6020599913279623, 0.7781512503836435, 0.8450980400142567, 0.9030899869919434, 0.9542425094393249, 1.4771212547196624, 1.6020599913279623, 1.7781512503836434, 1.8450980400142567, 1.9030899869919433, 1.9542425094393248, 2.477121254719662, 2.6020599913279625, 2.7781512503836434, 2.845098040014257, 2.9030899869919433, 2.9542425094393248, 3.477121254719662, 3.6020599913279625, 3.7781512503836434, 3.845098040014257, 3.9030899869919433, 3.9542425094393248 ]); + }) + .then(done, done.fail); + }); + + it('log auto - case 4', function(done) { + Plotly.newPlot(gd, { + data: [{ + x: [1, 10] + }], + layout: { + width: 1000, + xaxis: { + type: 'log', + minor: { + showgrid: true + } + } + } + }) + .then(function() { + _assert([]); + }) + .then(done, done.fail); + }); + + it('log auto - case 5', function(done) { + Plotly.newPlot(gd, { + data: [{ + x: [1, 2] + }], + layout: { + width: 1000, + xaxis: { + type: 'log', + minor: { + showgrid: true + } + } + } + }) + .then(function() { + _assert([ -0.017728766960431602, -0.00877392430750515, 0.008600171761917567, 0.017033339298780367, 0.025305865264770258, 0.03342375548694973, 0.049218022670181646, 0.056904851336472634, 0.06445798922691853, 0.07188200730612543, 0.08635983067474828, 0.09342168516223513, 0.10037054511756296, 0.10720996964786844, 0.12057393120584996, 0.1271047983648077, 0.13353890837021762, 0.13987908640123659, 0.15228834438305658, 0.15836249209524975, 0.1643528557844372, 0.17026171539495752, 0.18184358794477265, 0.18752072083646318, 0.1931245983544617, 0.1986570869544227, 0.20951501454263102, 0.21484384804769796, 0.22010808804005513, 0.22530928172586287, 0.23552844690754896, 0.24054924828259974, 0.24551266781414988, 0.250420002308894, 0.2600713879850748, 0.2648178230095364, 0.26951294421791616, 0.2741578492636797, 0.28330122870354935, 0.2878017299302258, 0.29225607135647574, 0.29666519026153076, 0.30535136944662333, 0.3096301674258983, 0.31386722036915293, 0.31806333496276107 ]); + }) + .then(done, done.fail); + }); + + it('log with defined minor dtick: D2', function(done) { + Plotly.newPlot(gd, { + data: [{ + x: [1, 10e6] + }], + layout: { + width: 1000, + xaxis: { + type: 'log', + minor: { + dtick: 'D2', + showgrid: true + } + } + } + }) + .then(function() { + _assert([ -0.30102999566398125, 0.30102999566398114, 0.6989700043360187, 1.3010299956639813, 1.6989700043360187, 2.3010299956639813, 2.6989700043360187, 3.3010299956639813, 3.6989700043360187, 4.301029995663981, 4.698970004336019, 5.301029995663981, 5.698970004336019, 6.301029995663981, 6.698970004336019, 7.301029995663981 ]); + }) + .then(done, done.fail); + }); + + it('log with defined minor dtick: L0.5', function(done) { + Plotly.newPlot(gd, { + data: [{ + x: [1, 10] + }], + layout: { + width: 1000, + xaxis: { + type: 'log', + minor: { + dtick: 'L0.5', + showgrid: true + } + } + } + }) + .then(function() { + _assert([ 0.17609125905568124, 0.3979400086720376, 0.5440680443502756, 0.6532125137753436, 0.7403626894942437, 0.8129133566428552, 0.8750612633916998, 0.9294189257142923, 0.9777236052888472, 1.0211892990699374, 1.0413926851582243, 1.0606978403536107 ]); + }) + .then(done, done.fail); + }); + + it('date auto - case of 10 years major dtick', function(done) { + Plotly.newPlot(gd, { + data: [{ + x: ['2000-01', '2050-01'] + }], + layout: { + width: 1000, + xaxis: { + minor: { + showgrid: true + } + } + } + }) + .then(function() { + _assert([ 883612800000, 1009843200000, 1072915200000, 1136073600000, 1199145600000, 1325376000000, 1388534400000, 1451606400000, 1514764800000, 1640995200000, 1704067200000, 1767225600000, 1830297600000, 1956528000000, 2019686400000, 2082758400000, 2145916800000, 2272147200000, 2335219200000, 2398377600000, 2461449600000, 2587680000000 ]); + }) + .then(done, done.fail); + }); + + it('date auto - case of 5 years major dtick', function(done) { + Plotly.newPlot(gd, { + data: [{ + x: ['2000-01', '2020-01'] + }], + layout: { + width: 1000, + xaxis: { + minor: { + showgrid: true + } + } + } + }) + .then(function() { + _assert([ 915148800000, 978307200000, 1009843200000, 1041379200000, 1072915200000, 1136073600000, 1167609600000, 1199145600000, 1230768000000, 1293840000000, 1325376000000, 1356998400000, 1388534400000, 1451606400000, 1483228800000, 1514764800000, 1546300800000, 1609459200000 ]); + }) + .then(done, done.fail); + }); + + it('date auto - case of 2 years major dtick', function(done) { + Plotly.newPlot(gd, { + data: [{ + x: ['2000-01', '2010-01'] + }], + layout: { + width: 1000, + xaxis: { + minor: { + showgrid: true + } + } + } + }) + .then(function() { + _assert([ 930787200000, 962409600000, 978307200000, 993945600000, 1025481600000, 1041379200000, 1057017600000, 1088640000000, 1104537600000, 1120176000000, 1151712000000, 1167609600000, 1183248000000, 1214870400000, 1230768000000, 1246406400000, 1277942400000 ]); + }) + .then(done, done.fail); + }); + + it('date auto - case of 1 year major dtick', function(done) { + Plotly.newPlot(gd, { + data: [{ + x: ['2000-01', '2005-01'] + }], + layout: { + width: 1000, + xaxis: { + minor: { + showgrid: true + } + } + } + }) + .then(function() { + _assert([ 938736000000, 954547200000, 962409600000, 970358400000, 986083200000, 993945600000, 1001894400000, 1017619200000, 1025481600000, 1033430400000, 1049155200000, 1057017600000, 1064966400000, 1080777600000, 1088640000000, 1096588800000, 1112313600000 ]); + }) + .then(done, done.fail); + }); + + it('date auto - case of M6 major dtick', function(done) { + Plotly.newPlot(gd, { + data: [{ + x: ['2000-01', '2003-01'] + }], + layout: { + width: 1000, + xaxis: { + minor: { + showgrid: true + } + } + } + }) + .then(function() { + _assert([ 941414400000, 951868800000, 957139200000, 967766400000, 973036800000, 983404800000, 988675200000, 999302400000, 1004572800000, 1014940800000, 1020211200000, 1030838400000, 1036108800000, 1046476800000 ]); + }) + .then(done, done.fail); + }); + + it('date auto - case of M3 major dtick', function(done) { + Plotly.newPlot(gd, { + data: [{ + x: ['2000-01', '2002-01'] + }], + layout: { + width: 1000, + xaxis: { + minor: { + showgrid: true + } + } + } + }) + .then(function() { + _assert([ 944006400000, 949363200000, 951868800000, 957139200000, 959817600000, 965088000000, 967766400000, 973036800000, 975628800000, 980985600000, 983404800000, 988675200000, 991353600000, 996624000000, 999302400000, 1004572800000, 1007164800000, 1012521600000 ]); + }) + .then(done, done.fail); + }); + + it('date auto - case of M2 major dtick', function(done) { + Plotly.newPlot(gd, { + data: [{ + x: ['2000-01', '2001-01'] + }], + layout: { + width: 1000, + xaxis: { + minor: { + showgrid: true + } + } + } + }) + .then(function() { + _assert([ 949363200000, 954547200000, 959817600000, 965088000000, 970358400000, 975628800000 ]); + }) + .then(done, done.fail); + }); + + it('date auto - case of M1 major dtick', function(done) { + Plotly.newPlot(gd, { + data: [{ + x: ['2000-01', '2000-06'] + }], + layout: { + width: 1000, + xaxis: { + minor: { + showgrid: true + } + } + } + }) + .then(function() { + _assert([]); + }) + .then(done, done.fail); + }); + + it('date auto - case of 2 weeks major dtick', function(done) { + Plotly.newPlot(gd, { + data: [{ + x: ['2000-01', '2000-04'] + }], + layout: { + width: 1000, + xaxis: { + minor: { + showgrid: true + } + } + } + }) + .then(function() { + _assert([ 947376000000, 948585600000, 949795200000, 951004800000, 952214400000, 953424000000, 954633600000 ]); + }) + .then(done, done.fail); + }); + + it('date auto - case of 1 week major dtick', function(done) { + Plotly.newPlot(gd, { + data: [{ + x: ['2000-01', '2000-02'] + }], + layout: { + width: 1000, + xaxis: { + minor: { + showgrid: true + } + } + } + }) + .then(function() { + _assert([ 946598400000, 946684800000, 946857600000, 946944000000, 947030400000, 947116800000, 947203200000, 947289600000, 947462400000, 947548800000, 947635200000, 947721600000, 947808000000, 947894400000, 948067200000, 948153600000, 948240000000, 948326400000, 948412800000, 948499200000, 948672000000, 948758400000, 948844800000, 948931200000, 949017600000, 949104000000, 949276800000, 949363200000, 949449600000 ]); + }) + .then(done, done.fail); + }); + + it('date auto - case of 3 days major dtick', function(done) { + Plotly.newPlot(gd, { + data: [{ + x: ['2000-01-01', '2000-01-21'] + }], + layout: { + width: 1000, + xaxis: { + minor: { + showgrid: true + } + } + } + }) + .then(function() { + _assert([ 946598400000, 946684800000, 946857600000, 946944000000, 947116800000, 947203200000, 947376000000, 947462400000, 947635200000, 947721600000, 947894400000, 947980800000, 948153600000, 948240000000, 948412800000, 948499200000 ]); + }) + .then(done, done.fail); + }); + + it('date auto - case of 2 days major dtick', function(done) { + Plotly.newPlot(gd, { + data: [{ + x: ['2000-01-01', '2000-01-11'] + }], + layout: { + width: 1000, + xaxis: { + minor: { + showgrid: true + } + } + } + }) + .then(function() { + _assert([ 946641600000, 946684800000, 946728000000, 946814400000, 946857600000, 946900800000, 946987200000, 947030400000, 947073600000, 947160000000, 947203200000, 947246400000, 947332800000, 947376000000, 947419200000, 947505600000, 947548800000, 947592000000 ]); + }) + .then(done, done.fail); + }); + + it('date auto - case of 1 day major dtick', function(done) { + Plotly.newPlot(gd, { + data: [{ + x: ['2000-01-01', '2000-01-06'] + }], + layout: { + width: 1000, + xaxis: { + minor: { + showgrid: true + } + } + } + }) + .then(function() { + _assert([ 946663200000, 946706400000, 946728000000, 946749600000, 946792800000, 946814400000, 946836000000, 946879200000, 946900800000, 946922400000, 946965600000, 946987200000, 947008800000, 947052000000, 947073600000, 947095200000, 947138400000 ]); + }) + .then(done, done.fail); + }); + + it('date auto - case of 12 hours major dtick', function(done) { + Plotly.newPlot(gd, { + data: [{ + x: ['2000-01-01', '2000-01-03'] + }], + layout: { + width: 1000, + xaxis: { + minor: { + showgrid: true + } + } + } + }) + .then(function() { + _assert([ 946677600000, 946692000000, 946699200000, 946713600000, 946720800000, 946735200000, 946742400000, 946756800000, 946764000000, 946778400000, 946785600000, 946800000000, 946807200000, 946821600000, 946828800000, 946843200000, 946850400000, 946864800000 ]); + }) + .then(done, done.fail); + }); + + it('date auto - case of 6 hours major dtick', function(done) { + Plotly.newPlot(gd, { + data: [{ + x: ['2000-01-01', '2000-01-02 12:00'] + }], + layout: { + width: 1000, + xaxis: { + minor: { + showgrid: true + } + } + } + }) + .then(function() { + _assert([ 946677600000, 946692000000, 946699200000, 946713600000, 946720800000, 946735200000, 946742400000, 946756800000, 946764000000, 946778400000, 946785600000, 946800000000, 946807200000, 946821600000 ]); + }) + .then(done, done.fail); + }); + + it('date auto - case of 3 hours major dtick', function(done) { + Plotly.newPlot(gd, { + data: [{ + x: ['2000-01-01', '2000-01-02'] + }], + layout: { + width: 1000, + xaxis: { + minor: { + showgrid: true + } + } + } + }) + .then(function() { + _assert([ 946681200000, 946688400000, 946692000000, 946699200000, 946702800000, 946710000000, 946713600000, 946720800000, 946724400000, 946731600000, 946735200000, 946742400000, 946746000000, 946753200000, 946756800000, 946764000000, 946767600000, 946774800000 ]); + }) + .then(done, done.fail); + }); + + it('date auto - case of 2 hours major dtick', function(done) { + Plotly.newPlot(gd, { + data: [{ + x: ['2000-01-01', '2000-01-01 10:00'] + }], + layout: { + width: 1000, + xaxis: { + minor: { + showgrid: true + } + } + } + }) + .then(function() { + _assert([ 946683000000, 946686600000, 946688400000, 946690200000, 946693800000, 946695600000, 946697400000, 946701000000, 946702800000, 946704600000, 946708200000, 946710000000, 946711800000, 946715400000, 946717200000, 946719000000, 946722600000 ]); + }) + .then(done, done.fail); + }); + + it('date auto - case of M1 major dtick with weekly minor', function(done) { + Plotly.newPlot(gd, { + data: [{ + x: ['2000-01', '2000-06'] + }], + layout: { + width: 1000, + xaxis: { + minor: { + dtick: ONEWEEK, + showgrid: true + } + } + } + }) + .then(function() { + _assert([ 946166400000, 946771200000, 947376000000, 947980800000, 948585600000, 949190400000, 949795200000, 950400000000, 951004800000, 951609600000, 952214400000, 952819200000, 953424000000, 954028800000, 954633600000, 955238400000, 955843200000, 956448000000, 957052800000, 957657600000, 958262400000, 958867200000, 959472000000, 960076800000 ]); + }) + .then(done, done.fail); + }); + + it('date auto - case of M1 major dtick with weekly minor start on Sunday', function(done) { + Plotly.newPlot(gd, { + data: [{ + x: ['2000-01', '2000-06'] + }], + layout: { + width: 1000, + xaxis: { + minor: { + tick0: '2001-01-02', + dtick: ONEWEEK, + showgrid: true + } + } + } + }) + .then(function() { + _assert([ 946339200000, 946944000000, 947548800000, 948153600000, 948758400000, 949968000000, 950572800000, 951177600000, 951782400000, 952387200000, 952992000000, 953596800000, 954201600000, 954806400000, 955411200000, 956016000000, 956620800000, 957225600000, 957830400000, 958435200000, 959040000000, 959644800000, 960249600000 ]); + }) + .then(done, done.fail); + }); + }); }); function getZoomInButton(gd) { diff --git a/test/jasmine/tests/splom_test.js b/test/jasmine/tests/splom_test.js index ad9c1c997aa..656d4ced184 100644 --- a/test/jasmine/tests/splom_test.js +++ b/test/jasmine/tests/splom_test.js @@ -807,7 +807,7 @@ describe('Test splom interactions:', function() { .then(function() { _assert({ subplotCnt: 25, - innerSubplotNodeCnt: 17, + innerSubplotNodeCnt: 18, hasSplomGrid: false, bgCnt: 0 }); @@ -826,8 +826,8 @@ describe('Test splom interactions:', function() { // from large -> small splom: // grid layer would be above xaxis layer, // if we didn't clear subplot children. - expect(gridIndex).toBe(1, ' index'); - expect(xaxisIndex).toBe(14, ' index'); + expect(gridIndex).toBe(2, ' index'); + expect(xaxisIndex).toBe(15, ' index'); return Plotly.restyle(gd, 'dimensions', [dimsLarge]); }) @@ -839,7 +839,7 @@ describe('Test splom interactions:', function() { // new subplots though have reduced number of children. innerSubplotNodeCnt: function(d) { var p = d.match(SUBPLOT_PATTERN); - return (p[1] > 5 || p[2] > 5) ? 4 : 17; + return (p[1] > 5 || p[2] > 5) ? 4 : 18; }, hasSplomGrid: true, bgCnt: 0 diff --git a/test/plot-schema.json b/test/plot-schema.json index a27fa752f29..c31d50b475d 100644 --- a/test/plot-schema.json +++ b/test/plot-schema.json @@ -9963,6 +9963,113 @@ "min": 0, "valType": "number" }, + "minor": { + "dtick": { + "description": "Sets the step in-between ticks on this axis. Use with `tick0`. Must be a positive number, or special strings available to *log* and *date* axes. If the axis `type` is *log*, then ticks are set every 10^(n*dtick) where n is the tick number. For example, to set a tick mark at 1, 10, 100, 1000, ... set dtick to 1. To set tick marks at 1, 100, 10000, ... set dtick to 2. To set tick marks at 1, 5, 25, 125, 625, 3125, ... set dtick to log_10(5), or 0.69897000433. *log* has several special values; *L*, where `f` is a positive number, gives ticks linearly spaced in value (but not position). For example `tick0` = 0.1, `dtick` = *L0.5* will put ticks at 0.1, 0.6, 1.1, 1.6 etc. To show powers of 10 plus small digits between, use *D1* (all digits) or *D2* (only 2 and 5). `tick0` is ignored for *D1* and *D2*. If the axis `type` is *date*, then you must convert the time to milliseconds. For example, to set the interval between ticks to one day, set `dtick` to 86400000.0. *date* also has special values *M* gives ticks spaced by a number of months. `n` must be a positive integer. To set ticks on the 15th of every third month, set `tick0` to *2000-01-15* and `dtick` to *M3*. To set ticks every 4 years, set `dtick` to *M48*", + "editType": "ticks", + "impliedEdits": { + "tickmode": "linear" + }, + "valType": "any" + }, + "editType": "ticks", + "gridcolor": { + "description": "Sets the color of the grid lines.", + "dflt": "#eee", + "editType": "ticks", + "valType": "color" + }, + "griddash": { + "description": "Sets the dash style of lines. Set to a dash type string (*solid*, *dot*, *dash*, *longdash*, *dashdot*, or *longdashdot*) or a dash length list in px (eg *5px,10px,2px,2px*).", + "dflt": "solid", + "editType": "ticks", + "valType": "string", + "values": [ + "solid", + "dot", + "dash", + "longdash", + "dashdot", + "longdashdot" + ] + }, + "gridwidth": { + "description": "Sets the width (in px) of the grid lines.", + "editType": "ticks", + "min": 0, + "valType": "number" + }, + "nticks": { + "description": "Specifies the maximum number of ticks for the particular axis. The actual number of ticks will be chosen automatically to be less than or equal to `nticks`. Has an effect only if `tickmode` is set to *auto*.", + "dflt": 5, + "editType": "ticks", + "min": 0, + "valType": "integer" + }, + "role": "object", + "showgrid": { + "description": "Determines whether or not grid lines are drawn. If *true*, the grid lines are drawn at every tick mark.", + "editType": "ticks", + "valType": "boolean" + }, + "tick0": { + "description": "Sets the placement of the first tick on this axis. Use with `dtick`. If the axis `type` is *log*, then you must take the log of your starting tick (e.g. to set the starting tick to 100, set the `tick0` to 2) except when `dtick`=*L* (see `dtick` for more info). If the axis `type` is *date*, it should be a date string, like date data. If the axis `type` is *category*, it should be a number, using the scale where each category is assigned a serial number from zero in the order it appears.", + "editType": "ticks", + "impliedEdits": { + "tickmode": "linear" + }, + "valType": "any" + }, + "tickcolor": { + "description": "Sets the tick color.", + "dflt": "#444", + "editType": "ticks", + "valType": "color" + }, + "ticklen": { + "description": "Sets the tick length (in px).", + "editType": "ticks", + "min": 0, + "valType": "number" + }, + "tickmode": { + "description": "Sets the tick mode for this axis. If *auto*, the number of ticks is set via `nticks`. If *linear*, the placement of the ticks is determined by a starting position `tick0` and a tick step `dtick` (*linear* is the default value if `tick0` and `dtick` are provided). If *array*, the placement of the ticks is set via `tickvals` and the tick text is `ticktext`. (*array* is the default value if `tickvals` is provided).", + "editType": "ticks", + "impliedEdits": {}, + "valType": "enumerated", + "values": [ + "auto", + "linear", + "array" + ] + }, + "ticks": { + "description": "Determines whether ticks are drawn or not. If **, this axis' ticks are not drawn. If *outside* (*inside*), this axis' are drawn outside (inside) the axis lines.", + "editType": "ticks", + "valType": "enumerated", + "values": [ + "outside", + "inside", + "" + ] + }, + "tickvals": { + "description": "Sets the values at which ticks on this axis appear. Only has an effect if `tickmode` is set to *array*. Used with `ticktext`.", + "editType": "ticks", + "valType": "data_array" + }, + "tickvalssrc": { + "description": "Sets the source reference on Chart Studio Cloud for `tickvals`.", + "editType": "none", + "valType": "string" + }, + "tickwidth": { + "description": "Sets the tick width (in px).", + "editType": "ticks", + "min": 0, + "valType": "number" + } + }, "mirror": { "description": "Determines if the axis lines or/and ticks are mirrored to the opposite side of the plotting area. If *true*, the axis lines are mirrored. If *ticks*, the axis lines and ticks are mirrored. If *false*, mirroring is disable. If *all*, axis lines are mirrored on all shared-axes subplots. If *allticks*, axis lines and ticks are mirrored on all shared-axes subplots.", "dflt": false, @@ -11095,6 +11202,113 @@ "min": 0, "valType": "number" }, + "minor": { + "dtick": { + "description": "Sets the step in-between ticks on this axis. Use with `tick0`. Must be a positive number, or special strings available to *log* and *date* axes. If the axis `type` is *log*, then ticks are set every 10^(n*dtick) where n is the tick number. For example, to set a tick mark at 1, 10, 100, 1000, ... set dtick to 1. To set tick marks at 1, 100, 10000, ... set dtick to 2. To set tick marks at 1, 5, 25, 125, 625, 3125, ... set dtick to log_10(5), or 0.69897000433. *log* has several special values; *L*, where `f` is a positive number, gives ticks linearly spaced in value (but not position). For example `tick0` = 0.1, `dtick` = *L0.5* will put ticks at 0.1, 0.6, 1.1, 1.6 etc. To show powers of 10 plus small digits between, use *D1* (all digits) or *D2* (only 2 and 5). `tick0` is ignored for *D1* and *D2*. If the axis `type` is *date*, then you must convert the time to milliseconds. For example, to set the interval between ticks to one day, set `dtick` to 86400000.0. *date* also has special values *M* gives ticks spaced by a number of months. `n` must be a positive integer. To set ticks on the 15th of every third month, set `tick0` to *2000-01-15* and `dtick` to *M3*. To set ticks every 4 years, set `dtick` to *M48*", + "editType": "ticks", + "impliedEdits": { + "tickmode": "linear" + }, + "valType": "any" + }, + "editType": "ticks", + "gridcolor": { + "description": "Sets the color of the grid lines.", + "dflt": "#eee", + "editType": "ticks", + "valType": "color" + }, + "griddash": { + "description": "Sets the dash style of lines. Set to a dash type string (*solid*, *dot*, *dash*, *longdash*, *dashdot*, or *longdashdot*) or a dash length list in px (eg *5px,10px,2px,2px*).", + "dflt": "solid", + "editType": "ticks", + "valType": "string", + "values": [ + "solid", + "dot", + "dash", + "longdash", + "dashdot", + "longdashdot" + ] + }, + "gridwidth": { + "description": "Sets the width (in px) of the grid lines.", + "editType": "ticks", + "min": 0, + "valType": "number" + }, + "nticks": { + "description": "Specifies the maximum number of ticks for the particular axis. The actual number of ticks will be chosen automatically to be less than or equal to `nticks`. Has an effect only if `tickmode` is set to *auto*.", + "dflt": 5, + "editType": "ticks", + "min": 0, + "valType": "integer" + }, + "role": "object", + "showgrid": { + "description": "Determines whether or not grid lines are drawn. If *true*, the grid lines are drawn at every tick mark.", + "editType": "ticks", + "valType": "boolean" + }, + "tick0": { + "description": "Sets the placement of the first tick on this axis. Use with `dtick`. If the axis `type` is *log*, then you must take the log of your starting tick (e.g. to set the starting tick to 100, set the `tick0` to 2) except when `dtick`=*L* (see `dtick` for more info). If the axis `type` is *date*, it should be a date string, like date data. If the axis `type` is *category*, it should be a number, using the scale where each category is assigned a serial number from zero in the order it appears.", + "editType": "ticks", + "impliedEdits": { + "tickmode": "linear" + }, + "valType": "any" + }, + "tickcolor": { + "description": "Sets the tick color.", + "dflt": "#444", + "editType": "ticks", + "valType": "color" + }, + "ticklen": { + "description": "Sets the tick length (in px).", + "editType": "ticks", + "min": 0, + "valType": "number" + }, + "tickmode": { + "description": "Sets the tick mode for this axis. If *auto*, the number of ticks is set via `nticks`. If *linear*, the placement of the ticks is determined by a starting position `tick0` and a tick step `dtick` (*linear* is the default value if `tick0` and `dtick` are provided). If *array*, the placement of the ticks is set via `tickvals` and the tick text is `ticktext`. (*array* is the default value if `tickvals` is provided).", + "editType": "ticks", + "impliedEdits": {}, + "valType": "enumerated", + "values": [ + "auto", + "linear", + "array" + ] + }, + "ticks": { + "description": "Determines whether ticks are drawn or not. If **, this axis' ticks are not drawn. If *outside* (*inside*), this axis' are drawn outside (inside) the axis lines.", + "editType": "ticks", + "valType": "enumerated", + "values": [ + "outside", + "inside", + "" + ] + }, + "tickvals": { + "description": "Sets the values at which ticks on this axis appear. Only has an effect if `tickmode` is set to *array*. Used with `ticktext`.", + "editType": "ticks", + "valType": "data_array" + }, + "tickvalssrc": { + "description": "Sets the source reference on Chart Studio Cloud for `tickvals`.", + "editType": "none", + "valType": "string" + }, + "tickwidth": { + "description": "Sets the tick width (in px).", + "editType": "ticks", + "min": 0, + "valType": "number" + } + }, "mirror": { "description": "Determines if the axis lines or/and ticks are mirrored to the opposite side of the plotting area. If *true*, the axis lines are mirrored. If *ticks*, the axis lines and ticks are mirrored. If *false*, mirroring is disable. If *all*, axis lines are mirrored on all shared-axes subplots. If *allticks*, axis lines and ticks are mirrored on all shared-axes subplots.", "dflt": false,