diff --git a/src/components/rangeslider/draw.js b/src/components/rangeslider/draw.js index e857030cbb4..28bd04b09c9 100644 --- a/src/components/rangeslider/draw.js +++ b/src/components/rangeslider/draw.js @@ -123,19 +123,70 @@ module.exports = function(gd) { // update data <--> pixel coordinate conversion methods - var range0 = axisOpts.r2l(opts.range[0]); - var range1 = axisOpts.r2l(opts.range[1]); - var dist = range1 - range0; + opts._rl = Lib.simpleMap(opts.range, axisOpts.r2l); + var rl0 = opts._rl[0]; + var rl1 = opts._rl[1]; + var drl = rl1 - rl0; opts.p2d = function(v) { - return (v / opts._width) * dist + range0; + return (v / opts._width) * drl + rl0; }; opts.d2p = function(v) { - return (v - range0) / dist * opts._width; + return (v - rl0) / drl * opts._width; }; - opts._rl = [range0, range1]; + if(axisOpts.breaks) { + var rsBreaks = axisOpts.locateBreaks(rl0, rl1); + + if(rsBreaks.length) { + var j, brk; + + var lBreaks = 0; + for(j = 0; j < rsBreaks.length; j++) { + brk = rsBreaks[j]; + lBreaks += (brk.max - brk.min); + } + + // TODO fix for reversed-range axes !!! + + // compute slope and piecewise offsets + var m2 = opts._width / (rl1 - rl0 - lBreaks); + var _B = [-m2 * rl0]; + for(j = 0; j < rsBreaks.length; j++) { + brk = rsBreaks[j]; + _B.push(_B[_B.length - 1] - m2 * (brk.max - brk.min)); + } + + opts.d2p = function(v) { + var b = _B[0]; + for(var j = 0; j < rsBreaks.length; j++) { + var brk = rsBreaks[j]; + if(v >= brk.max) b = _B[j + 1]; + else if(v < brk.min) break; + } + return b + m2 * v; + }; + + // fill pixel (i.e. 'p') min/max here, + // to not have to loop through the _breaks twice during `p2d` + for(j = 0; j < rsBreaks.length; j++) { + brk = rsBreaks[j]; + brk.pmin = opts.d2p(brk.min); + brk.pmax = opts.d2p(brk.max); + } + + opts.p2d = function(v) { + var b = _B[0]; + for(var j = 0; j < rsBreaks.length; j++) { + var brk = rsBreaks[j]; + if(v >= brk.pmax) b = _B[j + 1]; + else if(v < brk.pmin) break; + } + return (v - b) / m2; + }; + } + } if(oppAxisRangeOpts.rangemode !== 'match') { var range0OppAxis = oppAxisOpts.r2l(oppAxisRangeOpts.range[0]); @@ -404,6 +455,10 @@ function drawRangePlot(rangeSlider, gd, axisOpts, opts) { _context: gd._context }; + if(axisOpts.breaks) { + mockFigure.layout.xaxis.breaks = axisOpts.breaks; + } + mockFigure.layout[oppAxisName] = { type: oppAxisOpts.type, domain: [0, 1], @@ -411,6 +466,10 @@ function drawRangePlot(rangeSlider, gd, axisOpts, opts) { calendar: oppAxisOpts.calendar }; + if(oppAxisOpts.breaks) { + mockFigure.layout[oppAxisName].breaks = oppAxisOpts.breaks; + } + Plots.supplyDefaults(mockFigure); var xa = mockFigure._fullLayout.xaxis; diff --git a/src/plots/cartesian/autorange.js b/src/plots/cartesian/autorange.js index 5de73652823..db84d70b4ac 100644 --- a/src/plots/cartesian/autorange.js +++ b/src/plots/cartesian/autorange.js @@ -95,6 +95,18 @@ function getAutoRange(gd, ax) { // don't allow padding to reduce the data to < 10% of the length var minSpan = axLen / 10; + // find axis breaks in [v0,v1] and compute its length in value space + var calcBreaksLength = function(v0, v1) { + var lBreaks = 0; + if(ax.breaks) { + var breaksOut = ax.locateBreaks(v0, v1); + for(var i = 0; i < breaksOut.length; i++) { + lBreaks += (breaksOut[i].max - breaksOut[i].min); + } + } + return lBreaks; + }; + var mbest = 0; var minpt, maxpt, minbest, maxbest, dp, dv; @@ -102,7 +114,7 @@ function getAutoRange(gd, ax) { minpt = minArray[i]; for(j = 0; j < maxArray.length; j++) { maxpt = maxArray[j]; - dv = maxpt.val - minpt.val; + dv = maxpt.val - minpt.val - calcBreaksLength(minpt.val, maxpt.val); if(dv > 0) { dp = axLen - getPad(minpt) - getPad(maxpt); if(dp > minSpan) { @@ -167,7 +179,7 @@ function getAutoRange(gd, ax) { } // in case it changed again... - mbest = (maxbest.val - minbest.val) / + mbest = (maxbest.val - minbest.val - calcBreaksLength(minpt.val, maxpt.val)) / (axLen - getPad(minbest) - getPad(maxbest)); newRange = [ diff --git a/src/plots/cartesian/axes.js b/src/plots/cartesian/axes.js index af04c97d90b..d1f58fa77a1 100644 --- a/src/plots/cartesian/axes.js +++ b/src/plots/cartesian/axes.js @@ -526,7 +526,10 @@ axes.prepTicks = function(ax) { // have explicit tickvals without tick text if(ax.tickmode === 'array') nt *= 100; - axes.autoTicks(ax, Math.abs(rng[1] - rng[0]) / nt); + + ax._roughDTick = (Math.abs(rng[1] - rng[0]) - (ax._lBreaks || 0)) / nt; + axes.autoTicks(ax, ax._roughDTick); + // check for a forced minimum dtick if(ax._minDtick > 0 && ax.dtick < ax._minDtick * 2) { ax.dtick = ax._minDtick; @@ -573,7 +576,6 @@ axes.calcTicks = function calcTicks(ax) { if((ax._tmin < startTick) !== axrev) return []; // return the full set of tick vals - var tickVals = []; if(ax.type === 'category' || ax.type === 'multicategory') { endTick = (axrev) ? Math.max(-0.5, endTick) : Math.min(ax._categories.length - 0.5, endTick); @@ -581,24 +583,58 @@ axes.calcTicks = function calcTicks(ax) { var isDLog = (ax.type === 'log') && !(isNumeric(ax.dtick) || ax.dtick.charAt(0) === 'L'); - var xPrevious = null; - var maxTicks = Math.max(1000, ax._length || 0); - for(var x = ax._tmin; - (axrev) ? (x >= endTick) : (x <= endTick); - x = axes.tickIncrement(x, ax.dtick, axrev, ax.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 tickVals; + function generateTicks() { + var xPrevious = null; + var maxTicks = Math.max(1000, ax._length || 0); + tickVals = []; + for(var x = ax._tmin; + (axrev) ? (x >= endTick) : (x <= endTick); + x = axes.tickIncrement(x, ax.dtick, axrev, ax.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; + } + + tickVals.push({ + minor: minor, + value: x + }); + } + } + + generateTicks(); + + if(ax.breaks) { + var nTicksBefore = tickVals.length; + + // remove ticks falling inside breaks + tickVals = tickVals.filter(function(d) { + return ax.maskBreaks(d.value) !== BADNUM; + }); - var minor = false; - if(isDLog && (x !== (x | 0))) { - minor = true; + // if 'numerous' ticks get placed into breaks, + // increase dtick to generate more ticks, + // so that some hopefully fall between breaks + if(ax.tickmode === 'auto' && tickVals.length < nTicksBefore / 6) { + axes.autoTicks(ax, ax._roughDTick / 3); + autoTickRound(ax); + ax._tmin = axes.tickFirst(ax); + generateTicks(); + tickVals = tickVals.filter(function(d) { + return ax.maskBreaks(d.value) !== BADNUM; + }); } - tickVals.push({ - minor: minor, - value: x + // remove "overlapping" ticks (e.g. on either side of a break) + var tf2 = ax.tickfont ? 1.5 * ax.tickfont.size : 0; + tickVals = tickVals.filter(function(d, i, self) { + return !(i && Math.abs(ax.c2p(d.value) - ax.c2p(self[i - 1].value)) < tf2); }); } @@ -670,6 +706,13 @@ function arrayTicks(ax) { if(j < vals.length) ticksOut.splice(j, vals.length - j); + if(ax.breaks) { + // remove ticks falling inside breaks + ticksOut = ticksOut.filter(function(d) { + return ax.maskBreaks(d.x) !== BADNUM; + }); + } + return ticksOut; } @@ -966,7 +1009,7 @@ axes.tickText = function(ax, x, hover, noSuffixPrefix) { if(arrayMode && Array.isArray(ax.ticktext)) { var rng = Lib.simpleMap(ax.range, ax.r2l); - var minDiff = Math.abs(rng[1] - rng[0]) / 10000; + var minDiff = (Math.abs(rng[1] - rng[0]) - (ax._lBreaks || 0)) / 10000; for(i = 0; i < ax.ticktext.length; i++) { if(Math.abs(x - tickVal2l(ax.tickvals[i])) < minDiff) break; @@ -2828,6 +2871,7 @@ axes.shouldShowZeroLine = function(gd, ax, counterAxis) { (rng[0] * rng[1] <= 0) && ax.zeroline && (ax.type === 'linear' || ax.type === '-') && + !(ax.breaks && ax.maskBreaks(0) === BADNUM) && ( clipEnds(ax, 0) || !anyCounterAxLineAtZero(gd, ax, counterAxis, rng) || diff --git a/src/plots/cartesian/axis_defaults.js b/src/plots/cartesian/axis_defaults.js index 02eab325f73..d0bb2060038 100644 --- a/src/plots/cartesian/axis_defaults.js +++ b/src/plots/cartesian/axis_defaults.js @@ -11,6 +11,8 @@ var Registry = require('../../registry'); var Lib = require('../../lib'); +var handleArrayContainerDefaults = require('../array_container_defaults'); + var layoutAttributes = require('./layout_attributes'); var handleTickValueDefaults = require('./tick_value_defaults'); var handleTickMarkDefaults = require('./tick_mark_defaults'); @@ -117,5 +119,75 @@ module.exports = function handleAxisDefaults(containerIn, containerOut, coerce, } } + if(containerOut.type === 'date') { + var breaks = containerIn.breaks; + if(Array.isArray(breaks) && breaks.length) { + handleArrayContainerDefaults(containerIn, containerOut, { + name: 'breaks', + inclusionAttr: 'enabled', + handleItemDefaults: breaksDefaults + }); + setConvert(containerOut, layoutOut); + + if(layoutOut._has('scattergl') || layoutOut._has('splom')) { + for(var i = 0; i < options.data.length; i++) { + var trace = options.data[i]; + if(trace.type === 'scattergl' || trace.type === 'splom') { + trace.visible = false; + Lib.warn(trace.type + + ' traces do not work on axes with breaks.' + + ' Setting trace ' + trace.index + ' to `visible: false`.'); + } + } + } + } + } + return containerOut; }; + +function breaksDefaults(itemIn, itemOut, containerOut) { + function coerce(attr, dflt) { + return Lib.coerce(itemIn, itemOut, layoutAttributes.breaks, attr, dflt); + } + + var enabled = coerce('enabled'); + + if(enabled) { + var bnds = coerce('bounds'); + + if(bnds && bnds.length >= 2) { + if(bnds.length > 2) { + itemOut.bounds = itemOut.bounds.slice(0, 2); + } + + if(containerOut.autorange === false) { + var rng = containerOut.range; + + // if bounds are bigger than the (set) range, disable break + if(rng[0] < rng[1]) { + if(bnds[0] < rng[0] && bnds[1] > rng[1]) { + itemOut.enabled = false; + return; + } + } else if(bnds[0] > rng[0] && bnds[1] < rng[1]) { + itemOut.enabled = false; + return; + } + } + + coerce('pattern'); + } else { + var values = coerce('values'); + + if(values && values.length) { + coerce('dvalue'); + } else { + itemOut.enabled = false; + return; + } + } + + coerce('operation'); + } +} diff --git a/src/plots/cartesian/dragbox.js b/src/plots/cartesian/dragbox.js index affb0e36926..74e3c801c91 100644 --- a/src/plots/cartesian/dragbox.js +++ b/src/plots/cartesian/dragbox.js @@ -986,10 +986,20 @@ function zoomAxRanges(axList, r0Fraction, r1Fraction, updates, linkedAxes) { var axi = axList[i]; if(axi.fixedrange) continue; - var axRangeLinear0 = axi._rl[0]; - var axRangeLinearSpan = axi._rl[1] - axRangeLinear0; - updates[axi._name + '.range[0]'] = axi.l2r(axRangeLinear0 + axRangeLinearSpan * r0Fraction); - updates[axi._name + '.range[1]'] = axi.l2r(axRangeLinear0 + axRangeLinearSpan * r1Fraction); + if(axi.breaks) { + if(axi._id.charAt(0) === 'y') { + updates[axi._name + '.range[0]'] = axi.l2r(axi.p2l((1 - r0Fraction) * axi._length)); + updates[axi._name + '.range[1]'] = axi.l2r(axi.p2l((1 - r1Fraction) * axi._length)); + } else { + updates[axi._name + '.range[0]'] = axi.l2r(axi.p2l(r0Fraction * axi._length)); + updates[axi._name + '.range[1]'] = axi.l2r(axi.p2l(r1Fraction * axi._length)); + } + } else { + var axRangeLinear0 = axi._rl[0]; + var axRangeLinearSpan = axi._rl[1] - axRangeLinear0; + updates[axi._name + '.range[0]'] = axi.l2r(axRangeLinear0 + axRangeLinearSpan * r0Fraction); + updates[axi._name + '.range[1]'] = axi.l2r(axRangeLinear0 + axRangeLinearSpan * r1Fraction); + } } // zoom linked axes about their centers @@ -1003,10 +1013,23 @@ function dragAxList(axList, pix) { for(var i = 0; i < axList.length; i++) { var axi = axList[i]; if(!axi.fixedrange) { - axi.range = [ - axi.l2r(axi._rl[0] - pix / axi._m), - axi.l2r(axi._rl[1] - pix / axi._m) - ]; + if(axi.breaks) { + var p0 = 0; + var p1 = axi._length; + var d0 = axi.p2l(p0 + pix) - axi.p2l(p0); + var d1 = axi.p2l(p1 + pix) - axi.p2l(p1); + var delta = (d0 + d1) / 2; + + axi.range = [ + axi.l2r(axi._rl[0] - delta), + axi.l2r(axi._rl[1] - delta) + ]; + } else { + axi.range = [ + axi.l2r(axi._rl[0] - pix / axi._m), + axi.l2r(axi._rl[1] - pix / axi._m) + ]; + } } } } diff --git a/src/plots/cartesian/layout_attributes.js b/src/plots/cartesian/layout_attributes.js index 4e2e3697e71..7be688013fe 100644 --- a/src/plots/cartesian/layout_attributes.js +++ b/src/plots/cartesian/layout_attributes.js @@ -16,7 +16,7 @@ var templatedArray = require('../../plot_api/plot_template').templatedArray; var FORMAT_LINK = require('../../constants/docs').FORMAT_LINK; var DATE_FORMAT_LINK = require('../../constants/docs').DATE_FORMAT_LINK; - +var ONEDAY = require('../../constants/numerical').ONEDAY; var constants = require('./constants'); module.exports = { @@ -248,6 +248,135 @@ module.exports = { 'Moreover, note that matching axes must have the same `type`.' ].join(' ') }, + + breaks: templatedArray('break', { + enabled: { + valType: 'boolean', + role: 'info', + dflt: true, + editType: 'calc', + description: [ + 'Determines whether this axis break is enabled or disabled.', + 'Please note that `breaks` only work for *date* axis type.' + ].join(' ') + }, + + bounds: { + valType: 'info_array', + role: 'info', + items: [ + {valType: 'any', editType: 'calc'}, + {valType: 'any', editType: 'calc'} + ], + editType: 'calc', + description: [ + 'Sets the lower and upper bounds of this axis break.', + 'Can be used with `operation` to determine the behavior at the bounds.', + 'Can be used with `pattern`.' + ].join(' ') + }, + + pattern: { + valType: 'enumerated', + // TODO could add '%H:%M:%S' + values: ['%w', '%H', ''], + dflt: '', + role: 'info', + editType: 'calc', + description: [ + 'Determines a pattern on the time line that generates breaks.', + 'If *%w* - Sunday-based weekday as a decimal number [0, 6].', + 'If *%H* - hour (24-hour clock) as a decimal number [0, 23].', + 'These are the same directive as in `tickformat`, see', + 'https://github.com/d3/d3-time-format#locale_format', + 'for more info.', + 'Examples:', + '- { pattern: \'%w\', bounds: [6, 0], operation: \'[]\' }', + ' breaks from Saturday to Monday (i.e. skips the weekends).', + '- { pattern: \'%H\', bounds: [17, 8] }', + ' breaks from 5pm to 8am (i.e. skips non-work hours).' + ].join(' ') + }, + + values: { + valType: 'info_array', + freeLength: true, + role: 'info', + editType: 'calc', + items: { + valType: 'any', + editType: 'calc' + }, + description: [ + 'Sets the coordinate values corresponding to the breaks.', + 'An alternative to `bounds`.', + 'Use `dvalue` to set the size of the values along the axis.' + ].join(' ') + }, + dvalue: { + // TODO could become 'any' to add support for 'months', 'years' + valType: 'number', + role: 'info', + editType: 'calc', + min: 0, + dflt: ONEDAY, + description: [ + 'Sets the size of each `values` item.', + 'The default is one day in milliseconds.' + ].join(' ') + }, + + operation: { + valType: 'enumerated', + values: ['[]', '()', '[)', '(]'], + dflt: '()', + role: 'info', + editType: 'calc', + description: [ + 'Determines if we include or not the bound values within the break.', + 'Closed interval bounds (i.e. starting with *[* or ending with *]*)', + 'include the bound value within the break and thus make coordinates', + 'equal to the bound disappear.', + 'Open interval bounds (i.e. starting with *(* or ending with *)*)', + 'does not include the bound value within the break and thus keep coordinates', + 'equal to the bound on the axis.' + ].join(' ') + }, + + /* + gap: { + valType: 'number', + min: 0, + dflt: 0, // for *date* axes, maybe something else for *linear* + editType: 'calc', + role: 'info', + description: [ + 'Sets the gap distance between the start and the end of this break.', + 'Use with `gapmode` to set the unit of measurement.' + ].join(' ') + }, + gapmode: { + valType: 'enumerated', + values: ['pixels', 'fraction'], + dflt: 'pixels', + editType: 'calc', + role: 'info', + description: [ + 'Determines if the `gap` value corresponds to a pixel length', + 'or a fraction of the plot area.' + ].join(' ') + }, + */ + + // To complete https://github.com/plotly/plotly.js/issues/4210 + // we additionally need `gap` and make this work on *linear*, and + // possibly all other cartesian axis types. We possibly would also need + // some style attributes controlling the zig-zag on the corresponding + // axis. + + editType: 'calc' + }), + // ticks tickmode: { valType: 'enumerated', diff --git a/src/plots/cartesian/set_convert.js b/src/plots/cartesian/set_convert.js index c910ceed7e1..b7989f9b6fa 100644 --- a/src/plots/cartesian/set_convert.js +++ b/src/plots/cartesian/set_convert.js @@ -22,6 +22,10 @@ var numConstants = require('../../constants/numerical'); var FP_SAFE = numConstants.FP_SAFE; var BADNUM = numConstants.BADNUM; var LOG_CLIP = numConstants.LOG_CLIP; +var ONEDAY = numConstants.ONEDAY; +var ONEHOUR = numConstants.ONEHOUR; +var ONEMIN = numConstants.ONEMIN; +var ONESEC = numConstants.ONESEC; var constants = require('./constants'); var axisIds = require('./axis_ids'); @@ -170,14 +174,83 @@ module.exports = function setConvert(ax, fullLayout) { if(isNumeric(v)) return +v; } - function l2p(v) { + // include 2 fractional digits on pixel, for PDF zooming etc + function _l2p(v, m, b) { return d3.round(b + m * v, 2); } + + function _p2l(px, m, b) { return (px - b) / m; } + + var l2p = function l2p(v) { if(!isNumeric(v)) return BADNUM; + return _l2p(v, ax._m, ax._b); + }; - // include 2 fractional digits on pixel, for PDF zooming etc - return d3.round(ax._b + ax._m * v, 2); - } + var p2l = function(px) { + return _p2l(px, ax._m, ax._b); + }; - function p2l(px) { return (px - ax._b) / ax._m; } + if(ax.breaks) { + if(axLetter === 'y') { + l2p = function(v) { + if(!isNumeric(v)) return BADNUM; + if(!ax._breaks.length) return _l2p(v, ax._m, ax._b); + + var b = ax._B[0]; + for(var i = 0; i < ax._breaks.length; i++) { + var brk = ax._breaks[i]; + if(v <= brk.min) b = ax._B[i + 1]; + else if(v > brk.min && v < brk.max) { + // when v falls into break, pick offset 'closest' to it + if(v - brk.min <= brk.max - v) b = ax._B[i + 1]; + else b = ax._B[i]; + break; + } else if(v > brk.max) break; + } + return _l2p(v, -ax._m2, b); + }; + p2l = function(px) { + if(!isNumeric(px)) return BADNUM; + if(!ax._breaks.length) return _p2l(px, ax._m, ax._b); + + var b = ax._B[0]; + for(var i = 0; i < ax._breaks.length; i++) { + var brk = ax._breaks[i]; + if(px >= brk.pmin) b = ax._B[i + 1]; + else if(px < brk.pmax) break; + } + return _p2l(px, -ax._m2, b); + }; + } else { + l2p = function(v) { + if(!isNumeric(v)) return BADNUM; + if(!ax._breaks.length) return _l2p(v, ax._m, ax._b); + + var b = ax._B[0]; + for(var i = 0; i < ax._breaks.length; i++) { + var brk = ax._breaks[i]; + if(v >= brk.max) b = ax._B[i + 1]; + else if(v > brk.min && v < brk.max) { + // when v falls into break, pick offset 'closest' to it + if(v - brk.min <= brk.max - v) b = ax._B[i]; + else b = ax._B[i + 1]; + break; + } else if(v < brk.min) break; + } + return _l2p(v, ax._m2, b); + }; + p2l = function(px) { + if(!isNumeric(px)) return BADNUM; + if(!ax._breaks.length) return _p2l(px, ax._m, ax._b); + + var b = ax._B[0]; + for(var i = 0; i < ax._breaks.length; i++) { + var brk = ax._breaks[i]; + if(px >= brk.pmax) b = ax._B[i + 1]; + else if(px < brk.pmin) break; + } + return _p2l(px, ax._m2, b); + }; + } + } // conversions among c/l/p are fairly simple - do them together for all axis types ax.c2l = (ax.type === 'log') ? toLog : ensureNumber; @@ -463,7 +536,7 @@ module.exports = function setConvert(ax, fullLayout) { ax.domain = ax2.domain; } - // While transitions are occuring, occurring, we get a double-transform + // While transitions are occurring, we get a double-transform // issue if we transform the drawn layer *and* use the new axis range to // draw the data. This allows us to construct setConvert using the pre- // interaction values of the range: @@ -486,12 +559,277 @@ module.exports = function setConvert(ax, fullLayout) { ax._b = -ax._m * rl0; } + // set of "N" disjoint breaks inside the range + ax._breaks = []; + // length of these breaks in value space + ax._lBreaks = 0; + // l2p slope (same for all intervals) + ax._m2 = 0; + // set of l2p offsets (one for each of the (N+1) piecewise intervals) + ax._B = []; + + if(ax.breaks) { + var i, brk; + + ax._breaks = ax.locateBreaks( + Math.min(rl0, rl1), + Math.max(rl0, rl1) + ); + var signAx = rl0 > rl1 ? -1 : 1; + + if(ax._breaks.length) { + for(i = 0; i < ax._breaks.length; i++) { + brk = ax._breaks[i]; + ax._lBreaks += (brk.max - brk.min); + } + + ax._m2 = ax._length / (rl1 - rl0 - ax._lBreaks * signAx); + + if(axLetter === 'y') { + ax._breaks.reverse(); + // N.B. top to bottom (negative coord, positive px direction) + ax._B.push(ax._m2 * rl1); + } else { + ax._B.push(-ax._m2 * rl0); + } + + for(i = 0; i < ax._breaks.length; i++) { + brk = ax._breaks[i]; + ax._B.push(ax._B[ax._B.length - 1] - ax._m2 * (brk.max - brk.min) * signAx); + } + + if(signAx === -1) { + ax._B.reverse(); + } + + // fill pixel (i.e. 'p') min/max here, + // to not have to loop through the _breaks twice during `p2l` + for(i = 0; i < ax._breaks.length; i++) { + brk = ax._breaks[i]; + brk.pmin = l2p(brk.min); + brk.pmax = l2p(brk.max); + } + } + } + if(!isFinite(ax._m) || !isFinite(ax._b) || ax._length < 0) { fullLayout._replotting = false; throw new Error('Something went wrong with axis scaling'); } }; + ax.maskBreaks = function(v) { + var breaksIn = ax.breaks || []; + var bnds, b0, b1, vb; + + for(var i = 0; i < breaksIn.length; i++) { + var brk = breaksIn[i]; + + if(brk.enabled) { + var op = brk.operation; + var op0 = op.charAt(0); + var op1 = op.charAt(1); + + if(brk.bounds) { + var doesCrossPeriod = false; + + switch(brk.pattern) { + case '%w': + bnds = Lib.simpleMap(brk.bounds, cleanNumber); + b0 = bnds[0]; + b1 = bnds[1]; + vb = (new Date(v)).getUTCDay(); + if(bnds[0] > bnds[1]) doesCrossPeriod = true; + break; + case '%H': + bnds = Lib.simpleMap(brk.bounds, cleanNumber); + b0 = bnds[0]; + b1 = bnds[1]; + var vDate = new Date(v); + vb = vDate.getUTCHours() + ( + vDate.getUTCMinutes() * ONEMIN + + vDate.getUTCSeconds() * ONESEC + + vDate.getUTCMilliseconds() + ) / ONEDAY; + if(bnds[0] > bnds[1]) doesCrossPeriod = true; + break; + case '': + // N.B. should work on date axes as well! + // e.g. { bounds: ['2020-01-04', '2020-01-05 23:59'] } + bnds = Lib.simpleMap(brk.bounds, ax.d2c); + if(bnds[0] <= bnds[1]) { + b0 = bnds[0]; + b1 = bnds[1]; + } else { + b0 = bnds[1]; + b1 = bnds[0]; + } + // TODO should work with reversed-range axes + vb = v; + break; + } + + if(doesCrossPeriod) { + if( + (op0 === '(' ? vb > b0 : vb >= b0) || + (op1 === ')' ? vb < b1 : vb <= b1) + ) return BADNUM; + } else { + if( + (op0 === '(' ? vb > b0 : vb >= b0) && + (op1 === ')' ? vb < b1 : vb <= b1) + ) return BADNUM; + } + } else { + var vals = Lib.simpleMap(brk.values, ax.d2c).sort(Lib.sorterAsc); + var onOpenBound = false; + + for(var j = 0; j < vals.length; j++) { + b0 = vals[j]; + b1 = b0 + brk.dvalue; + if( + (op0 === '(' ? v > b0 : v >= b0) && + (op1 === ')' ? v < b1 : v <= b1) + ) return BADNUM; + + if(onOpenBound && op0 === '(' && v === b0) return BADNUM; + onOpenBound = op1 === ')' && v === b1; + } + } + } + } + return v; + }; + + ax.locateBreaks = function(r0, r1) { + var i, bnds, b0, b1; + + var breaksOut = []; + if(!ax.breaks) return breaksOut; + + var breaksIn = ax.breaks.slice().sort(function(a, b) { + if(a.pattern === '%w' && b.pattern === '%H') return -1; + else if(b.pattern === '%w' && a.pattern === '%H') return 1; + return 0; + }); + + var addBreak = function(min, max) { + min = Lib.constrain(min, r0, r1); + max = Lib.constrain(max, r0, r1); + if(min === max) return; + + var isNewBreak = true; + for(var j = 0; j < breaksOut.length; j++) { + var brkj = breaksOut[j]; + if(min > brkj.max || max < brkj.min) { + // potentially a new break + } else { + if(min < brkj.min) { + brkj.min = min; + } + if(max > brkj.max) { + brkj.max = max; + } + isNewBreak = false; + } + } + if(isNewBreak) { + breaksOut.push({min: min, max: max}); + } + }; + + for(i = 0; i < breaksIn.length; i++) { + var brk = breaksIn[i]; + + if(brk.enabled) { + var op = brk.operation; + var op0 = op.charAt(0); + var op1 = op.charAt(1); + + if(brk.bounds) { + if(brk.pattern) { + bnds = Lib.simpleMap(brk.bounds, cleanNumber); + if(bnds[0] === bnds[1] && op === '()') continue; + + // r0 value as date + var r0Date = new Date(r0); + // r0 value for break pattern + var r0Pattern; + // delta between r0 and first break in break pattern values + var r0PatternDelta; + // delta between break bounds in ms + var bndDelta; + // step in ms between breaks + var step; + // tracker to position bounds + var t; + + switch(brk.pattern) { + case '%w': + b0 = bnds[0] + (op0 === '(' ? 1 : 0); + b1 = bnds[1]; + r0Pattern = r0Date.getUTCDay(); + r0PatternDelta = b0 - r0Pattern; + bndDelta = (b1 >= b0 ? b1 - b0 : (b1 + 7) - b0) * ONEDAY; + if(op1 === ']') bndDelta += ONEDAY; + step = 7 * ONEDAY; + + t = r0 + r0PatternDelta * ONEDAY - + r0Date.getUTCHours() * ONEHOUR - + r0Date.getUTCMinutes() * ONEMIN - + r0Date.getUTCSeconds() * ONESEC - + r0Date.getUTCMilliseconds(); + break; + case '%H': + b0 = bnds[0]; + b1 = bnds[1]; + r0Pattern = r0Date.getUTCHours(); + r0PatternDelta = b0 - r0Pattern; + bndDelta = (b1 >= b0 ? b1 - b0 : (b1 + 24) - b0) * ONEHOUR; + step = ONEDAY; + + t = r0 + r0PatternDelta * ONEHOUR - + r0Date.getUTCMinutes() * ONEMIN - + r0Date.getUTCSeconds() * ONESEC - + r0Date.getUTCMilliseconds(); + break; + } + + while(t <= r1) { + // TODO we need to remove decimal (most often found + // in auto ranges) for this to work correctly, + // should this be Math.floor, Math.ceil or + // Math.round ?? + addBreak(Math.floor(t), Math.floor(t + bndDelta)); + t += step; + } + } else { + bnds = Lib.simpleMap(brk.bounds, ax.r2l); + if(bnds[0] <= bnds[1]) { + b0 = bnds[0]; + b1 = bnds[1]; + } else { + b0 = bnds[1]; + b1 = bnds[0]; + } + addBreak(b0, b1); + } + } else { + var vals = Lib.simpleMap(brk.values, ax.d2c); + for(var j = 0; j < vals.length; j++) { + b0 = vals[j]; + b1 = b0 + brk.dvalue; + addBreak(b0, b1); + } + } + } + } + + breaksOut.sort(function(a, b) { return a.min - b.min; }); + + return breaksOut; + }; + // makeCalcdata: takes an x or y array and converts it // to a position on the axis object "ax" // inputs: @@ -541,6 +879,13 @@ module.exports = function setConvert(ax, fullLayout) { } } + // mask (i.e. set to BADNUM) coords that fall inside breaks + if(ax.breaks) { + for(i = 0; i < len; i++) { + arrayOut[i] = ax.maskBreaks(arrayOut[i]); + } + } + return arrayOut; }; diff --git a/test/image/baselines/axes_breaks-bars.png b/test/image/baselines/axes_breaks-bars.png new file mode 100644 index 00000000000..11469ee14a6 Binary files /dev/null and b/test/image/baselines/axes_breaks-bars.png differ diff --git a/test/image/baselines/axes_breaks-finance.png b/test/image/baselines/axes_breaks-finance.png new file mode 100644 index 00000000000..b1d0bf56e9f Binary files /dev/null and b/test/image/baselines/axes_breaks-finance.png differ diff --git a/test/image/baselines/axes_breaks-rangeslider.png b/test/image/baselines/axes_breaks-rangeslider.png new file mode 100644 index 00000000000..eb3302b7d82 Binary files /dev/null and b/test/image/baselines/axes_breaks-rangeslider.png differ diff --git a/test/image/baselines/axes_breaks-tickvals.png b/test/image/baselines/axes_breaks-tickvals.png new file mode 100644 index 00000000000..0b9f4d87f9b Binary files /dev/null and b/test/image/baselines/axes_breaks-tickvals.png differ diff --git a/test/image/baselines/axes_breaks-values.png b/test/image/baselines/axes_breaks-values.png new file mode 100644 index 00000000000..c998de4ce71 Binary files /dev/null and b/test/image/baselines/axes_breaks-values.png differ diff --git a/test/image/baselines/axes_breaks-weekends-weeknights.png b/test/image/baselines/axes_breaks-weekends-weeknights.png new file mode 100644 index 00000000000..fec2920693b Binary files /dev/null and b/test/image/baselines/axes_breaks-weekends-weeknights.png differ diff --git a/test/image/baselines/axes_breaks-weekends_autorange-reversed.png b/test/image/baselines/axes_breaks-weekends_autorange-reversed.png new file mode 100644 index 00000000000..3c36378c7c2 Binary files /dev/null and b/test/image/baselines/axes_breaks-weekends_autorange-reversed.png differ diff --git a/test/image/baselines/axes_breaks.png b/test/image/baselines/axes_breaks.png new file mode 100644 index 00000000000..0b819065907 Binary files /dev/null and b/test/image/baselines/axes_breaks.png differ diff --git a/test/image/mocks/axes_breaks-bars.json b/test/image/mocks/axes_breaks-bars.json new file mode 100644 index 00000000000..12dc70110ce --- /dev/null +++ b/test/image/mocks/axes_breaks-bars.json @@ -0,0 +1,36 @@ +{ + "data": [ + { + "type": "bar", + "y": [ + "1970-01-01 00:00:00.000", + "1970-01-01 00:00:00.002" + ], + "x": [ 1, 2 ], + "orientation": "h", + "marker": { + "color": ["purple", "orange"], + "opacity": [1, 0.5] + } + } + ], + "layout": { + "title": { + "text": "Bars centered on open breaks bounds
should show bars even though pos -/+ dPos falls within break", + "x": 0, + "xref": "paper", + "font": {"size": 12} + }, + "yaxis": { + "breaks": [ + { + "bounds": [ + "1970-01-01 00:00:00.000", + "1970-01-01 00:00:00.002" + ], + "operation": "()" + } + ] + } + } +} diff --git a/test/image/mocks/axes_breaks-finance.json b/test/image/mocks/axes_breaks-finance.json new file mode 100644 index 00000000000..0bb9daa882d --- /dev/null +++ b/test/image/mocks/axes_breaks-finance.json @@ -0,0 +1,391 @@ +{ + "data": [ + { + "type": "ohlc", + "x": [ + "2017-01-04", + "2017-01-05", + "2017-01-06", + "2017-01-09", + "2017-01-10", + "2017-01-11", + "2017-01-12", + "2017-01-13", + "2017-01-17", + "2017-01-18", + "2017-01-19", + "2017-01-20", + "2017-01-23", + "2017-01-24", + "2017-01-25", + "2017-01-26", + "2017-01-27", + "2017-01-30", + "2017-01-31", + "2017-02-01", + "2017-02-02", + "2017-02-03", + "2017-02-06", + "2017-02-07", + "2017-02-08", + "2017-02-09", + "2017-02-10", + "2017-02-13", + "2017-02-14", + "2017-02-15" + ], + "close": [ + 116.019997, + 116.610001, + 117.910004, + 118.989998, + 119.110001, + 119.75, + 119.25, + 119.040001, + 120, + 119.989998, + 119.779999, + 120, + 120.080002, + 119.970001, + 121.879997, + 121.940002, + 121.949997, + 121.629997, + 121.349998, + 128.75, + 128.529999, + 129.080002, + 130.289993, + 131.529999, + 132.039993, + 132.419998, + 132.119995, + 133.289993, + 135.020004, + 135.509995 + ], + "decreasing": { + "line": { + "color": "#7F7F7F" + } + }, + "high": [ + 116.510002, + 116.860001, + 118.160004, + 119.43, + 119.379997, + 119.93, + 119.300003, + 119.620003, + 120.239998, + 120.5, + 120.089996, + 120.449997, + 120.809998, + 120.099998, + 122.099998, + 122.440002, + 122.349998, + 121.629997, + 121.389999, + 130.490005, + 129.389999, + 129.190002, + 130.5, + 132.089996, + 132.220001, + 132.449997, + 132.940002, + 133.820007, + 135.089996, + 136.270004 + ], + "increasing": { + "line": { + "color": "#17BECF" + } + }, + "line": { + "color": "rgba(31,119,180,1)" + }, + "low": [ + 115.75, + 115.809998, + 116.470001, + 117.940002, + 118.300003, + 118.599998, + 118.209999, + 118.809998, + 118.220001, + 119.709999, + 119.370003, + 119.730003, + 119.769997, + 119.5, + 120.279999, + 121.599998, + 121.599998, + 120.660004, + 120.620003, + 127.010002, + 127.779999, + 128.160004, + 128.899994, + 130.449997, + 131.220001, + 131.119995, + 132.050003, + 132.75, + 133.25, + 134.619995 + ], + "open": [ + 115.849998, + 115.919998, + 116.779999, + 117.949997, + 118.769997, + 118.739998, + 118.900002, + 119.110001, + 118.339996, + 120, + 119.400002, + 120.449997, + 120, + 119.550003, + 120.419998, + 121.669998, + 122.139999, + 120.93, + 121.150002, + 127.029999, + 127.980003, + 128.309998, + 129.130005, + 130.539993, + 131.350006, + 131.649994, + 132.460007, + 133.080002, + 133.470001, + 135.520004 + ] + }, + { + "type": "candlestick", + "x": [ + "2017-01-04", + "2017-01-05", + "2017-01-06", + "2017-01-09", + "2017-01-10", + "2017-01-11", + "2017-01-12", + "2017-01-13", + "2017-01-17", + "2017-01-18", + "2017-01-19", + "2017-01-20", + "2017-01-23", + "2017-01-24", + "2017-01-25", + "2017-01-26", + "2017-01-27", + "2017-01-30", + "2017-01-31", + "2017-02-01", + "2017-02-02", + "2017-02-03", + "2017-02-06", + "2017-02-07", + "2017-02-08", + "2017-02-09", + "2017-02-10", + "2017-02-13", + "2017-02-14", + "2017-02-15" + ], + "close": [ + 116.019997, + 116.610001, + 117.910004, + 118.989998, + 119.110001, + 119.75, + 119.25, + 119.040001, + 120, + 119.989998, + 119.779999, + 120, + 120.080002, + 119.970001, + 121.879997, + 121.940002, + 121.949997, + 121.629997, + 121.349998, + 128.75, + 128.529999, + 129.080002, + 130.289993, + 131.529999, + 132.039993, + 132.419998, + 132.119995, + 133.289993, + 135.020004, + 135.509995 + ], + "decreasing": { + "line": { + "color": "#7F7F7F" + } + }, + "high": [ + 116.510002, + 116.860001, + 118.160004, + 119.43, + 119.379997, + 119.93, + 119.300003, + 119.620003, + 120.239998, + 120.5, + 120.089996, + 120.449997, + 120.809998, + 120.099998, + 122.099998, + 122.440002, + 122.349998, + 121.629997, + 121.389999, + 130.490005, + 129.389999, + 129.190002, + 130.5, + 132.089996, + 132.220001, + 132.449997, + 132.940002, + 133.820007, + 135.089996, + 136.270004 + ], + "increasing": { + "line": { + "color": "#17BECF" + } + }, + "line": { + "color": "rgba(31,119,180,1)" + }, + "low": [ + 115.75, + 115.809998, + 116.470001, + 117.940002, + 118.300003, + 118.599998, + 118.209999, + 118.809998, + 118.220001, + 119.709999, + 119.370003, + 119.730003, + 119.769997, + 119.5, + 120.279999, + 121.599998, + 121.599998, + 120.660004, + 120.620003, + 127.010002, + 127.779999, + 128.160004, + 128.899994, + 130.449997, + 131.220001, + 131.119995, + 132.050003, + 132.75, + 133.25, + 134.619995 + ], + "open": [ + 115.849998, + 115.919998, + 116.779999, + 117.949997, + 118.769997, + 118.739998, + 118.900002, + 119.110001, + 118.339996, + 120, + 119.400002, + 120.449997, + 120, + 119.550003, + 120.419998, + 121.669998, + 122.139999, + 120.93, + 121.150002, + 127.029999, + 127.980003, + 128.309998, + 129.130005, + 130.539993, + 131.350006, + 131.649994, + 132.460007, + 133.080002, + 133.470001, + 135.520004 + ], + "xaxis": "x2", + "yaxis": "y2" + } + ], + "layout": { + "margin": { "r": 10, "t": 25, "b": 40, "l": 60 }, + "grid": {"rows": 1, "columns": 2, "pattern": "independent"}, + "showlegend": false, + "xaxis": { + "rangeslider": { "visible": true }, + "breaks": [ + { + "pattern": "%w", + "bounds": [ 6, 0 ], + "operation": "[]" + }, + { + "values": ["2017-01-16"] + } + ] + }, + "xaxis2": { + "rangeslider": { "visible": true }, + "breaks": [ + { + "pattern": "%w", + "bounds": [ 6, 0 ], + "operation": "[]" + }, + { + "values": ["2017-01-16"] + } + ] + }, + "width": 600, + "height": 300 + } +} diff --git a/test/image/mocks/axes_breaks-rangeslider.json b/test/image/mocks/axes_breaks-rangeslider.json new file mode 100644 index 00000000000..a0230d2d4be --- /dev/null +++ b/test/image/mocks/axes_breaks-rangeslider.json @@ -0,0 +1,2686 @@ +{ + "data": [ + { + "name": "5min", + "type": "candlestick", + "x": [ + "2017-02-06 21:00:00", + "2017-02-06 21:05:00", + "2017-02-06 21:10:00", + "2017-02-06 21:15:00", + "2017-02-06 21:20:00", + "2017-02-06 21:25:00", + "2017-02-06 21:30:00", + "2017-02-06 21:35:00", + "2017-02-06 21:40:00", + "2017-02-06 21:45:00", + "2017-02-06 21:50:00", + "2017-02-06 21:55:00", + "2017-02-06 22:00:00", + "2017-02-06 22:05:00", + "2017-02-06 22:10:00", + "2017-02-06 22:15:00", + "2017-02-06 22:20:00", + "2017-02-06 22:25:00", + "2017-02-06 22:30:00", + "2017-02-06 22:35:00", + "2017-02-06 22:40:00", + "2017-02-06 22:45:00", + "2017-02-06 22:50:00", + "2017-02-06 22:55:00", + "2017-02-06 23:00:00", + "2017-02-06 23:05:00", + "2017-02-06 23:10:00", + "2017-02-06 23:15:00", + "2017-02-06 23:20:00", + "2017-02-06 23:25:00", + "2017-02-07 09:00:00", + "2017-02-07 09:05:00", + "2017-02-07 09:10:00", + "2017-02-07 09:15:00", + "2017-02-07 09:20:00", + "2017-02-07 09:25:00", + "2017-02-07 09:30:00", + "2017-02-07 09:35:00", + "2017-02-07 09:40:00", + "2017-02-07 09:45:00", + "2017-02-07 09:50:00", + "2017-02-07 09:55:00", + "2017-02-07 10:00:00", + "2017-02-07 10:05:00", + "2017-02-07 10:10:00", + "2017-02-07 10:30:00", + "2017-02-07 10:35:00", + "2017-02-07 10:40:00", + "2017-02-07 10:45:00", + "2017-02-07 10:50:00", + "2017-02-07 10:55:00", + "2017-02-07 11:00:00", + "2017-02-07 11:05:00", + "2017-02-07 11:10:00", + "2017-02-07 11:15:00", + "2017-02-07 11:20:00", + "2017-02-07 11:25:00", + "2017-02-07 13:30:00", + "2017-02-07 13:35:00", + "2017-02-07 13:40:00", + "2017-02-07 13:45:00", + "2017-02-07 13:50:00", + "2017-02-07 13:55:00", + "2017-02-07 14:00:00", + "2017-02-07 14:05:00", + "2017-02-07 14:10:00", + "2017-02-07 14:15:00", + "2017-02-07 14:20:00", + "2017-02-07 14:25:00", + "2017-02-07 14:30:00", + "2017-02-07 14:35:00", + "2017-02-07 14:40:00", + "2017-02-07 14:45:00", + "2017-02-07 14:50:00", + "2017-02-07 14:55:00", + "2017-02-07 21:00:00", + "2017-02-07 21:05:00", + "2017-02-07 21:10:00", + "2017-02-07 21:15:00", + "2017-02-07 21:20:00", + "2017-02-07 21:25:00", + "2017-02-07 21:30:00", + "2017-02-07 21:35:00", + "2017-02-07 21:40:00", + "2017-02-07 21:45:00", + "2017-02-07 21:50:00", + "2017-02-07 21:55:00", + "2017-02-07 22:00:00", + "2017-02-07 22:05:00", + "2017-02-07 22:10:00", + "2017-02-07 22:15:00", + "2017-02-07 22:20:00", + "2017-02-07 22:25:00", + "2017-02-07 22:30:00", + "2017-02-07 22:35:00", + "2017-02-07 22:40:00", + "2017-02-07 22:45:00", + "2017-02-07 22:50:00", + "2017-02-07 22:55:00", + "2017-02-07 23:00:00", + "2017-02-07 23:05:00", + "2017-02-07 23:10:00", + "2017-02-07 23:15:00", + "2017-02-07 23:20:00", + "2017-02-07 23:25:00", + "2017-02-08 09:00:00", + "2017-02-08 09:05:00", + "2017-02-08 09:10:00", + "2017-02-08 09:15:00", + "2017-02-08 09:20:00", + "2017-02-08 09:25:00", + "2017-02-08 09:30:00", + "2017-02-08 09:35:00", + "2017-02-08 09:40:00", + "2017-02-08 09:45:00", + "2017-02-08 09:50:00", + "2017-02-08 09:55:00", + "2017-02-08 10:00:00", + "2017-02-08 10:05:00", + "2017-02-08 10:10:00", + "2017-02-08 10:30:00", + "2017-02-08 10:35:00", + "2017-02-08 10:40:00", + "2017-02-08 10:45:00", + "2017-02-08 10:50:00", + "2017-02-08 10:55:00", + "2017-02-08 11:00:00", + "2017-02-08 11:05:00", + "2017-02-08 11:10:00", + "2017-02-08 11:15:00", + "2017-02-08 11:20:00", + "2017-02-08 11:25:00", + "2017-02-08 13:30:00", + "2017-02-08 13:35:00", + "2017-02-08 13:40:00", + "2017-02-08 13:45:00", + "2017-02-08 13:50:00", + "2017-02-08 13:55:00", + "2017-02-08 14:00:00", + "2017-02-08 14:05:00", + "2017-02-08 14:10:00", + "2017-02-08 14:15:00", + "2017-02-08 14:20:00", + "2017-02-08 14:25:00", + "2017-02-08 14:30:00", + "2017-02-08 14:35:00", + "2017-02-08 14:40:00", + "2017-02-08 14:45:00", + "2017-02-08 14:50:00", + "2017-02-08 14:55:00", + "2017-02-08 21:00:00", + "2017-02-08 21:05:00", + "2017-02-08 21:10:00", + "2017-02-08 21:15:00", + "2017-02-08 21:20:00", + "2017-02-08 21:25:00", + "2017-02-08 21:30:00", + "2017-02-08 21:35:00", + "2017-02-08 21:40:00", + "2017-02-08 21:45:00", + "2017-02-08 21:50:00", + "2017-02-08 21:55:00", + "2017-02-08 22:00:00", + "2017-02-08 22:05:00", + "2017-02-08 22:10:00", + "2017-02-08 22:15:00", + "2017-02-08 22:20:00", + "2017-02-08 22:25:00", + "2017-02-08 22:30:00", + "2017-02-08 22:35:00", + "2017-02-08 22:40:00", + "2017-02-08 22:45:00", + "2017-02-08 22:50:00", + "2017-02-08 22:55:00", + "2017-02-08 23:00:00", + "2017-02-08 23:05:00", + "2017-02-08 23:10:00", + "2017-02-08 23:15:00", + "2017-02-08 23:20:00", + "2017-02-08 23:25:00", + "2017-02-09 09:00:00", + "2017-02-09 09:05:00", + "2017-02-09 09:10:00", + "2017-02-09 09:15:00", + "2017-02-09 09:20:00", + "2017-02-09 09:25:00", + "2017-02-09 09:30:00", + "2017-02-09 09:35:00", + "2017-02-09 09:40:00", + "2017-02-09 09:45:00", + "2017-02-09 09:50:00", + "2017-02-09 09:55:00", + "2017-02-09 10:00:00", + "2017-02-09 10:05:00", + "2017-02-09 10:10:00", + "2017-02-09 10:30:00", + "2017-02-09 10:35:00", + "2017-02-09 10:40:00", + "2017-02-09 10:45:00", + "2017-02-09 10:50:00", + "2017-02-09 10:55:00", + "2017-02-09 11:00:00", + "2017-02-09 11:05:00", + "2017-02-09 11:10:00", + "2017-02-09 11:15:00", + "2017-02-09 11:20:00", + "2017-02-09 11:25:00", + "2017-02-09 13:30:00", + "2017-02-09 13:35:00", + "2017-02-09 13:40:00", + "2017-02-09 13:45:00", + "2017-02-09 13:50:00", + "2017-02-09 13:55:00", + "2017-02-09 14:00:00", + "2017-02-09 14:05:00", + "2017-02-09 14:10:00", + "2017-02-09 14:15:00", + "2017-02-09 14:20:00", + "2017-02-09 14:25:00", + "2017-02-09 14:30:00", + "2017-02-09 14:35:00", + "2017-02-09 14:40:00", + "2017-02-09 14:45:00", + "2017-02-09 14:50:00", + "2017-02-09 21:00:00", + "2017-02-09 21:05:00", + "2017-02-09 21:10:00", + "2017-02-09 21:15:00", + "2017-02-09 21:20:00", + "2017-02-09 21:25:00", + "2017-02-09 21:30:00", + "2017-02-09 21:35:00", + "2017-02-09 21:40:00", + "2017-02-09 21:45:00", + "2017-02-09 21:50:00", + "2017-02-09 21:55:00", + "2017-02-09 22:00:00", + "2017-02-09 22:05:00", + "2017-02-09 22:10:00", + "2017-02-09 22:15:00", + "2017-02-09 22:20:00", + "2017-02-09 22:25:00", + "2017-02-09 22:30:00", + "2017-02-09 22:35:00", + "2017-02-09 22:40:00", + "2017-02-09 22:45:00", + "2017-02-09 22:50:00", + "2017-02-09 22:55:00", + "2017-02-09 23:00:00", + "2017-02-09 23:05:00", + "2017-02-09 23:10:00", + "2017-02-09 23:15:00", + "2017-02-09 23:20:00", + "2017-02-09 23:25:00", + "2017-02-10 09:00:00", + "2017-02-10 09:05:00", + "2017-02-10 09:10:00", + "2017-02-10 09:15:00", + "2017-02-10 09:20:00", + "2017-02-10 09:25:00", + "2017-02-10 09:30:00", + "2017-02-10 09:35:00", + "2017-02-10 09:40:00", + "2017-02-10 09:45:00", + "2017-02-10 09:50:00", + "2017-02-10 09:55:00", + "2017-02-10 10:00:00", + "2017-02-10 10:05:00", + "2017-02-10 10:10:00", + "2017-02-10 10:30:00", + "2017-02-10 10:35:00", + "2017-02-10 10:40:00", + "2017-02-10 10:45:00", + "2017-02-10 10:50:00", + "2017-02-10 10:55:00", + "2017-02-10 11:00:00", + "2017-02-10 11:05:00", + "2017-02-10 11:10:00", + "2017-02-10 11:15:00", + "2017-02-10 11:20:00", + "2017-02-10 11:25:00", + "2017-02-10 13:30:00", + "2017-02-10 13:35:00", + "2017-02-10 13:40:00", + "2017-02-10 13:45:00", + "2017-02-10 13:50:00", + "2017-02-10 13:55:00", + "2017-02-10 14:00:00", + "2017-02-10 14:05:00", + "2017-02-10 14:10:00", + "2017-02-10 14:15:00", + "2017-02-10 14:20:00", + "2017-02-10 14:25:00", + "2017-02-10 14:30:00", + "2017-02-10 14:35:00", + "2017-02-10 14:40:00", + "2017-02-10 14:45:00", + "2017-02-10 14:50:00", + "2017-02-10 14:55:00", + "2017-02-10 21:00:00", + "2017-02-10 21:05:00", + "2017-02-10 21:10:00", + "2017-02-10 21:15:00", + "2017-02-10 21:20:00", + "2017-02-10 21:25:00", + "2017-02-10 21:30:00", + "2017-02-10 21:35:00", + "2017-02-10 21:40:00", + "2017-02-10 21:45:00", + "2017-02-10 21:50:00", + "2017-02-10 21:55:00", + "2017-02-10 22:00:00", + "2017-02-10 22:05:00", + "2017-02-10 22:10:00", + "2017-02-10 22:15:00", + "2017-02-10 22:20:00", + "2017-02-10 22:25:00", + "2017-02-10 22:30:00", + "2017-02-10 22:35:00", + "2017-02-10 22:40:00", + "2017-02-10 22:45:00", + "2017-02-10 22:50:00", + "2017-02-10 22:55:00", + "2017-02-10 23:00:00", + "2017-02-10 23:05:00", + "2017-02-10 23:10:00", + "2017-02-10 23:15:00", + "2017-02-10 23:20:00", + "2017-02-10 23:25:00", + "2017-02-10 23:25:00", + "2017-02-13 09:00:00", + "2017-02-13 09:05:00", + "2017-02-13 09:10:00", + "2017-02-13 09:15:00", + "2017-02-13 09:20:00", + "2017-02-13 09:25:00", + "2017-02-13 09:30:00", + "2017-02-13 09:35:00", + "2017-02-13 09:40:00", + "2017-02-13 09:45:00", + "2017-02-13 09:50:00", + "2017-02-13 09:55:00", + "2017-02-13 10:00:00", + "2017-02-13 10:05:00", + "2017-02-13 10:10:00", + "2017-02-13 10:30:00", + "2017-02-13 10:35:00", + "2017-02-13 10:40:00", + "2017-02-13 10:45:00", + "2017-02-13 10:50:00", + "2017-02-13 10:55:00", + "2017-02-13 11:00:00", + "2017-02-13 11:05:00", + "2017-02-13 11:10:00", + "2017-02-13 11:15:00", + "2017-02-13 11:20:00", + "2017-02-13 11:25:00", + "2017-02-13 13:30:00", + "2017-02-13 13:35:00", + "2017-02-13 13:40:00", + "2017-02-13 13:45:00", + "2017-02-13 13:50:00", + "2017-02-13 13:55:00", + "2017-02-13 14:00:00", + "2017-02-13 14:05:00", + "2017-02-13 14:10:00", + "2017-02-13 14:15:00", + "2017-02-13 14:20:00", + "2017-02-13 14:25:00", + "2017-02-13 14:30:00", + "2017-02-13 14:35:00", + "2017-02-13 14:40:00", + "2017-02-13 14:45:00", + "2017-02-13 14:50:00", + "2017-02-13 14:55:00" + ], + "low": [ + 6975, + 6972, + 6969, + 6964, + 6948, + 6964, + 6967, + 6966, + 6955, + 6962, + 6962, + 6961, + 6963, + 6960, + 6959, + 6962, + 6965, + 6965, + 6966, + 6970, + 6971, + 6971, + 6972, + 6970, + 6960, + 6962, + 6962, + 6967, + 6965, + 6965, + 6949, + 6950, + 6951, + 6954, + 6954, + 6955, + 6955, + 6960, + 6960, + 6955, + 6955, + 6957, + 6958, + 6958, + 6959, + 6962, + 6962, + 6974, + 6975, + 6975, + 6977, + 6978, + 6979, + 6978, + 6973, + 6975, + 6970, + 6969, + 6970, + 6971, + 6972, + 6977, + 6975, + 6975, + 6979, + 6977, + 6975, + 6976, + 6975, + 6973, + 6974, + 6974, + 6973, + 6971, + 6970, + 6951, + 6939, + 6946, + 6945, + 6942, + 6930, + 6928, + 6928, + 6935, + 6933, + 6934, + 6931, + 6930, + 6928, + 6925, + 6931, + 6932, + 6932, + 6933, + 6933, + 6932, + 6935, + 6935, + 6941, + 6943, + 6943, + 6934, + 6934, + 6936, + 6935, + 6909, + 6909, + 6913, + 6916, + 6915, + 6912, + 6915, + 6916, + 6917, + 6918, + 6920, + 6917, + 6913, + 6913, + 6915, + 6915, + 6919, + 6920, + 6920, + 6924, + 6920, + 6920, + 6921, + 6921, + 6917, + 6917, + 6917, + 6917, + 6920, + 6923, + 6920, + 6915, + 6916, + 6916, + 6922, + 6923, + 6924, + 6922, + 6922, + 6925, + 6927, + 6922, + 6919, + 6923, + 6931, + 6949, + 6953, + 6954, + 6952, + 6954, + 6951, + 6954, + 6946, + 6947, + 6948, + 6942, + 6943, + 6944, + 6944, + 6943, + 6943, + 6942, + 6944, + 6947, + 6943, + 6945, + 6948, + 6947, + 6948, + 6948, + 6948, + 6947, + 6947, + 6944, + 6942, + 6944, + 6942, + 6934, + 6933, + 6936, + 6935, + 6935, + 6936, + 6939, + 6942, + 6940, + 6938, + 6938, + 6939, + 6941, + 6940, + 6938, + 6937, + 6942, + 6945, + 6948, + 6947, + 6945, + 6944, + 6945, + 6944, + 6946, + 6946, + 6947, + 6944, + 6936, + 6929, + 6886, + 6885, + 6887, + 6885, + 6889, + 6893, + 6895, + 6899, + 6900, + 6896, + 6890, + 6888, + 6897, + 6893, + 6878, + 6882, + 6888, + 6904, + 6901, + 6903, + 6897, + 6897, + 6898, + 6903, + 6905, + 6907, + 6907, + 6909, + 6909, + 6907, + 6910, + 6908, + 6907, + 6908, + 6906, + 6906, + 6910, + 6910, + 6911, + 6912, + 6909, + 6910, + 6896, + 6898, + 6890, + 6888, + 6896, + 6893, + 6895, + 6895, + 6900, + 6898, + 6899, + 6897, + 6893, + 6895, + 6901, + 6900, + 6901, + 6900, + 6900, + 6895, + 6896, + 6891, + 6894, + 6898, + 6900, + 6897, + 6897, + 6898, + 6903, + 6905, + 6904, + 6908, + 6906, + 6909, + 6908, + 6908, + 6911, + 6910, + 6914, + 6910, + 6911, + 6908, + 6905, + 6907, + 6905, + 6901, + 6892, + 6893, + 6898, + 6896, + 6893, + 6902, + 6902, + 6904, + 6907, + 6906, + 6906, + 6908, + 6908, + 6908, + 6903, + 6897, + 6900, + 6901, + 6913, + 6912, + 6912, + 6913, + 6912, + 6913, + 6905, + 6906, + 6906, + 6905, + 6909, + 6910, + 6908, + 6914, + 6909, + 6911, + 6915, + 6944, + 6959, + 6950, + 6955, + 6962, + 6962, + 6964, + 6973, + 6959, + 6961, + 6961, + 6963, + 6966, + 6966, + 6964, + 6964, + 6968, + 6969, + 6973, + 6972, + 6966, + 6964, + 6968, + 6963, + 6956, + 6943, + 6948, + 6945, + 6945, + 6949, + 6950, + 6952, + 6954, + 6954, + 6955, + 6957, + 6957, + 6955, + 6954, + 6947 + ], + "high": [ + 6977, + 6980, + 6979, + 6971, + 6968, + 6970, + 6971, + 6970, + 6967, + 6968, + 6966, + 6966, + 6967, + 6965, + 6965, + 6967, + 6968, + 6967, + 6975, + 6974, + 6976, + 6975, + 6977, + 6976, + 6973, + 6967, + 6968, + 6970, + 6969, + 6975, + 6960, + 6955, + 6959, + 6963, + 6961, + 6959, + 6962, + 6964, + 6963, + 6960, + 6959, + 6962, + 6963, + 6960, + 6963, + 6966, + 6977, + 6987, + 6982, + 6983, + 6983, + 6985, + 6982, + 6983, + 6982, + 6979, + 6979, + 6973, + 6975, + 6975, + 6980, + 6980, + 6979, + 6981, + 6982, + 6982, + 6979, + 6980, + 6978, + 6977, + 6976, + 6981, + 6981, + 6974, + 6976, + 6965, + 6955, + 6955, + 6950, + 6947, + 6945, + 6935, + 6939, + 6938, + 6937, + 6936, + 6937, + 6934, + 6934, + 6933, + 6934, + 6938, + 6936, + 6937, + 6938, + 6937, + 6938, + 6946, + 6946, + 6947, + 6948, + 6945, + 6940, + 6939, + 6942, + 6924, + 6917, + 6922, + 6921, + 6918, + 6920, + 6919, + 6921, + 6920, + 6922, + 6924, + 6921, + 6918, + 6917, + 6917, + 6923, + 6926, + 6923, + 6930, + 6929, + 6926, + 6924, + 6925, + 6924, + 6923, + 6921, + 6920, + 6921, + 6924, + 6928, + 6925, + 6923, + 6920, + 6923, + 6928, + 6926, + 6928, + 6925, + 6927, + 6934, + 6931, + 6930, + 6926, + 6932, + 6957, + 6964, + 6962, + 6964, + 6964, + 6957, + 6957, + 6958, + 6957, + 6952, + 6955, + 6953, + 6949, + 6949, + 6949, + 6947, + 6948, + 6945, + 6950, + 6953, + 6948, + 6950, + 6951, + 6951, + 6952, + 6952, + 6951, + 6949, + 6950, + 6949, + 6947, + 6950, + 6947, + 6946, + 6939, + 6941, + 6940, + 6939, + 6939, + 6946, + 6945, + 6943, + 6941, + 6943, + 6942, + 6943, + 6944, + 6941, + 6944, + 6949, + 6949, + 6955, + 6953, + 6950, + 6947, + 6947, + 6948, + 6949, + 6949, + 6951, + 6948, + 6946, + 6939, + 6931, + 6904, + 6895, + 6899, + 6899, + 6898, + 6903, + 6904, + 6903, + 6905, + 6897, + 6894, + 6909, + 6902, + 6895, + 6889, + 6915, + 6909, + 6906, + 6908, + 6905, + 6901, + 6907, + 6908, + 6909, + 6911, + 6913, + 6913, + 6912, + 6912, + 6915, + 6913, + 6910, + 6912, + 6911, + 6911, + 6913, + 6913, + 6913, + 6917, + 6915, + 6914, + 6906, + 6907, + 6900, + 6900, + 6902, + 6897, + 6898, + 6901, + 6904, + 6903, + 6901, + 6900, + 6900, + 6901, + 6903, + 6909, + 6906, + 6906, + 6905, + 6901, + 6899, + 6897, + 6903, + 6903, + 6904, + 6902, + 6901, + 6908, + 6909, + 6914, + 6912, + 6911, + 6911, + 6914, + 6912, + 6913, + 6914, + 6924, + 6923, + 6917, + 6916, + 6915, + 6911, + 6910, + 6911, + 6917, + 6912, + 6902, + 6904, + 6904, + 6903, + 6910, + 6907, + 6910, + 6913, + 6910, + 6909, + 6913, + 6912, + 6911, + 6912, + 6904, + 6904, + 6918, + 6925, + 6918, + 6916, + 6918, + 6916, + 6917, + 6914, + 6911, + 6910, + 6911, + 6917, + 6910, + 6923, + 6923, + 6919, + 6917, + 6948, + 6985, + 6978, + 6963, + 6962, + 6969, + 6969, + 6980, + 6988, + 6981, + 6970, + 6966, + 6972, + 6973, + 6969, + 6969, + 6971, + 6972, + 6975, + 6980, + 6982, + 6974, + 6969, + 6976, + 6970, + 6966, + 6957, + 6951, + 6951, + 6955, + 6954, + 6955, + 6957, + 6959, + 6958, + 6959, + 6960, + 6960, + 6963, + 6960, + 6957 + ], + "open": [ + 6976, + 6975, + 6977, + 6970, + 6968, + 6965, + 6968, + 6969, + 6967, + 6962, + 6966, + 6963, + 6963, + 6965, + 6964, + 6963, + 6966, + 6967, + 6966, + 6973, + 6972, + 6971, + 6975, + 6975, + 6972, + 6963, + 6966, + 6969, + 6969, + 6967, + 6959, + 6950, + 6955, + 6956, + 6955, + 6958, + 6957, + 6960, + 6962, + 6960, + 6959, + 6957, + 6961, + 6959, + 6959, + 6963, + 6962, + 6978, + 6978, + 6976, + 6981, + 6979, + 6980, + 6979, + 6981, + 6975, + 6979, + 6972, + 6972, + 6974, + 6972, + 6977, + 6979, + 6976, + 6979, + 6980, + 6978, + 6977, + 6977, + 6976, + 6974, + 6974, + 6979, + 6973, + 6972, + 6959, + 6953, + 6953, + 6946, + 6946, + 6944, + 6933, + 6929, + 6936, + 6937, + 6936, + 6935, + 6931, + 6934, + 6930, + 6932, + 6934, + 6933, + 6936, + 6935, + 6932, + 6936, + 6936, + 6944, + 6943, + 6947, + 6944, + 6934, + 6937, + 6938, + 6924, + 6912, + 6916, + 6921, + 6917, + 6916, + 6919, + 6918, + 6917, + 6920, + 6922, + 6919, + 6916, + 6917, + 6917, + 6915, + 6923, + 6921, + 6921, + 6926, + 6925, + 6921, + 6922, + 6921, + 6923, + 6920, + 6919, + 6919, + 6920, + 6923, + 6924, + 6921, + 6918, + 6918, + 6922, + 6926, + 6925, + 6925, + 6922, + 6926, + 6931, + 6929, + 6924, + 6925, + 6931, + 6950, + 6958, + 6954, + 6963, + 6956, + 6955, + 6955, + 6955, + 6948, + 6951, + 6953, + 6945, + 6948, + 6946, + 6947, + 6947, + 6944, + 6944, + 6949, + 6948, + 6946, + 6949, + 6950, + 6948, + 6950, + 6950, + 6949, + 6948, + 6948, + 6946, + 6946, + 6947, + 6945, + 6939, + 6938, + 6937, + 6936, + 6936, + 6939, + 6945, + 6942, + 6940, + 6938, + 6941, + 6942, + 6942, + 6941, + 6938, + 6944, + 6946, + 6949, + 6953, + 6949, + 6945, + 6945, + 6945, + 6948, + 6948, + 6948, + 6948, + 6945, + 6938, + 6930, + 6889, + 6893, + 6891, + 6893, + 6895, + 6896, + 6902, + 6901, + 6903, + 6897, + 6892, + 6900, + 6901, + 6894, + 6884, + 6889, + 6907, + 6906, + 6904, + 6904, + 6900, + 6898, + 6905, + 6906, + 6908, + 6910, + 6910, + 6910, + 6911, + 6910, + 6912, + 6909, + 6910, + 6908, + 6910, + 6913, + 6912, + 6912, + 6912, + 6914, + 6912, + 6906, + 6901, + 6898, + 6891, + 6897, + 6897, + 6896, + 6897, + 6901, + 6903, + 6899, + 6899, + 6899, + 6895, + 6901, + 6902, + 6906, + 6902, + 6905, + 6900, + 6897, + 6897, + 6895, + 6901, + 6900, + 6902, + 6900, + 6898, + 6906, + 6906, + 6911, + 6909, + 6908, + 6911, + 6910, + 6910, + 6912, + 6914, + 6923, + 6915, + 6911, + 6915, + 6908, + 6910, + 6908, + 6907, + 6912, + 6895, + 6900, + 6902, + 6895, + 6902, + 6907, + 6904, + 6907, + 6908, + 6908, + 6908, + 6911, + 6911, + 6909, + 6904, + 6900, + 6901, + 6915, + 6916, + 6915, + 6914, + 6915, + 6915, + 6914, + 6908, + 6910, + 6906, + 6909, + 6910, + 6914, + 6921, + 6916, + 6912, + 6916, + 6946, + 6975, + 6963, + 6956, + 6962, + 6967, + 6966, + 6978, + 6977, + 6969, + 6964, + 6964, + 6969, + 6968, + 6966, + 6967, + 6970, + 6970, + 6974, + 6980, + 6973, + 6967, + 6969, + 6969, + 6965, + 6956, + 6951, + 6950, + 6947, + 6953, + 6953, + 6954, + 6956, + 6957, + 6955, + 6959, + 6959, + 6958, + 6959, + 6957 + ], + "close": [ + 6975, + 6978, + 6970, + 6968, + 6965, + 6968, + 6968, + 6967, + 6963, + 6966, + 6963, + 6964, + 6965, + 6965, + 6963, + 6966, + 6967, + 6966, + 6973, + 6972, + 6971, + 6975, + 6975, + 6973, + 6963, + 6966, + 6968, + 6968, + 6966, + 6965, + 6949, + 6955, + 6956, + 6955, + 6959, + 6956, + 6960, + 6962, + 6960, + 6958, + 6957, + 6961, + 6959, + 6959, + 6962, + 6962, + 6977, + 6978, + 6976, + 6981, + 6979, + 6980, + 6979, + 6981, + 6975, + 6979, + 6972, + 6971, + 6974, + 6972, + 6977, + 6979, + 6976, + 6980, + 6980, + 6978, + 6977, + 6977, + 6976, + 6974, + 6974, + 6979, + 6973, + 6972, + 6972, + 6953, + 6952, + 6946, + 6945, + 6944, + 6932, + 6930, + 6937, + 6937, + 6936, + 6935, + 6931, + 6933, + 6931, + 6932, + 6934, + 6933, + 6935, + 6936, + 6933, + 6937, + 6936, + 6945, + 6944, + 6947, + 6944, + 6934, + 6938, + 6938, + 6942, + 6912, + 6916, + 6921, + 6918, + 6916, + 6919, + 6918, + 6917, + 6920, + 6922, + 6920, + 6917, + 6918, + 6917, + 6916, + 6922, + 6921, + 6922, + 6925, + 6925, + 6921, + 6922, + 6921, + 6923, + 6920, + 6919, + 6918, + 6920, + 6923, + 6924, + 6921, + 6918, + 6918, + 6922, + 6926, + 6925, + 6925, + 6922, + 6926, + 6930, + 6929, + 6924, + 6924, + 6930, + 6947, + 6958, + 6955, + 6964, + 6957, + 6955, + 6955, + 6955, + 6948, + 6950, + 6953, + 6945, + 6948, + 6946, + 6948, + 6947, + 6944, + 6944, + 6950, + 6948, + 6946, + 6949, + 6950, + 6948, + 6950, + 6950, + 6949, + 6948, + 6949, + 6945, + 6944, + 6947, + 6945, + 6939, + 6939, + 6937, + 6936, + 6937, + 6939, + 6945, + 6943, + 6940, + 6938, + 6941, + 6942, + 6942, + 6941, + 6938, + 6944, + 6946, + 6949, + 6952, + 6949, + 6946, + 6945, + 6945, + 6948, + 6948, + 6948, + 6948, + 6945, + 6938, + 6929, + 6887, + 6893, + 6890, + 6892, + 6896, + 6897, + 6902, + 6902, + 6903, + 6897, + 6892, + 6891, + 6902, + 6895, + 6883, + 6889, + 6906, + 6906, + 6904, + 6905, + 6899, + 6898, + 6905, + 6906, + 6908, + 6910, + 6910, + 6910, + 6911, + 6910, + 6912, + 6909, + 6910, + 6908, + 6910, + 6911, + 6911, + 6912, + 6913, + 6914, + 6912, + 6913, + 6901, + 6899, + 6891, + 6897, + 6897, + 6896, + 6897, + 6900, + 6903, + 6899, + 6900, + 6900, + 6895, + 6901, + 6902, + 6906, + 6902, + 6905, + 6900, + 6897, + 6897, + 6895, + 6901, + 6900, + 6902, + 6900, + 6899, + 6906, + 6906, + 6910, + 6909, + 6908, + 6910, + 6910, + 6910, + 6913, + 6914, + 6923, + 6915, + 6912, + 6915, + 6908, + 6910, + 6908, + 6909, + 6912, + 6895, + 6901, + 6902, + 6896, + 6902, + 6907, + 6904, + 6907, + 6909, + 6908, + 6908, + 6911, + 6911, + 6909, + 6904, + 6901, + 6901, + 6915, + 6916, + 6915, + 6914, + 6915, + 6915, + 6915, + 6908, + 6910, + 6906, + 6910, + 6910, + 6910, + 6921, + 6917, + 6912, + 6916, + 6947, + 6974, + 6963, + 6956, + 6962, + 6967, + 6966, + 6978, + 6977, + 6969, + 6965, + 6964, + 6969, + 6969, + 6966, + 6968, + 6970, + 6970, + 6974, + 6979, + 6974, + 6967, + 6968, + 6969, + 6965, + 6956, + 6951, + 6950, + 6947, + 6953, + 6953, + 6954, + 6956, + 6957, + 6955, + 6959, + 6959, + 6957, + 6959, + 6957, + 6952 + ] + }, + { + "name": "SAR", + "type": "scatter", + "x": [ + "2017-02-06 21:00:00", + "2017-02-06 21:05:00", + "2017-02-06 21:10:00", + "2017-02-06 21:15:00", + "2017-02-06 21:20:00", + "2017-02-06 21:25:00", + "2017-02-06 21:30:00", + "2017-02-06 21:35:00", + "2017-02-06 21:40:00", + "2017-02-06 21:45:00", + "2017-02-06 21:50:00", + "2017-02-06 21:55:00", + "2017-02-06 22:00:00", + "2017-02-06 22:05:00", + "2017-02-06 22:10:00", + "2017-02-06 22:15:00", + "2017-02-06 22:20:00", + "2017-02-06 22:25:00", + "2017-02-06 22:30:00", + "2017-02-06 22:35:00", + "2017-02-06 22:40:00", + "2017-02-06 22:45:00", + "2017-02-06 22:50:00", + "2017-02-06 22:55:00", + "2017-02-06 23:00:00", + "2017-02-06 23:05:00", + "2017-02-06 23:10:00", + "2017-02-06 23:15:00", + "2017-02-06 23:20:00", + "2017-02-06 23:25:00", + "2017-02-07 09:00:00", + "2017-02-07 09:05:00", + "2017-02-07 09:10:00", + "2017-02-07 09:15:00", + "2017-02-07 09:20:00", + "2017-02-07 09:25:00", + "2017-02-07 09:30:00", + "2017-02-07 09:35:00", + "2017-02-07 09:40:00", + "2017-02-07 09:45:00", + "2017-02-07 09:50:00", + "2017-02-07 09:55:00", + "2017-02-07 10:00:00", + "2017-02-07 10:05:00", + "2017-02-07 10:10:00", + "2017-02-07 10:30:00", + "2017-02-07 10:35:00", + "2017-02-07 10:40:00", + "2017-02-07 10:45:00", + "2017-02-07 10:50:00", + "2017-02-07 10:55:00", + "2017-02-07 11:00:00", + "2017-02-07 11:05:00", + "2017-02-07 11:10:00", + "2017-02-07 11:15:00", + "2017-02-07 11:20:00", + "2017-02-07 11:25:00", + "2017-02-07 13:30:00", + "2017-02-07 13:35:00", + "2017-02-07 13:40:00", + "2017-02-07 13:45:00", + "2017-02-07 13:50:00", + "2017-02-07 13:55:00", + "2017-02-07 14:00:00", + "2017-02-07 14:05:00", + "2017-02-07 14:10:00", + "2017-02-07 14:15:00", + "2017-02-07 14:20:00", + "2017-02-07 14:25:00", + "2017-02-07 14:30:00", + "2017-02-07 14:35:00", + "2017-02-07 14:40:00", + "2017-02-07 14:45:00", + "2017-02-07 14:50:00", + "2017-02-07 14:55:00", + "2017-02-07 21:00:00", + "2017-02-07 21:05:00", + "2017-02-07 21:10:00", + "2017-02-07 21:15:00", + "2017-02-07 21:20:00", + "2017-02-07 21:25:00", + "2017-02-07 21:30:00", + "2017-02-07 21:35:00", + "2017-02-07 21:40:00", + "2017-02-07 21:45:00", + "2017-02-07 21:50:00", + "2017-02-07 21:55:00", + "2017-02-07 22:00:00", + "2017-02-07 22:05:00", + "2017-02-07 22:10:00", + "2017-02-07 22:15:00", + "2017-02-07 22:20:00", + "2017-02-07 22:25:00", + "2017-02-07 22:30:00", + "2017-02-07 22:35:00", + "2017-02-07 22:40:00", + "2017-02-07 22:45:00", + "2017-02-07 22:50:00", + "2017-02-07 22:55:00", + "2017-02-07 23:00:00", + "2017-02-07 23:05:00", + "2017-02-07 23:10:00", + "2017-02-07 23:15:00", + "2017-02-07 23:20:00", + "2017-02-07 23:25:00", + "2017-02-08 09:00:00", + "2017-02-08 09:05:00", + "2017-02-08 09:10:00", + "2017-02-08 09:15:00", + "2017-02-08 09:20:00", + "2017-02-08 09:25:00", + "2017-02-08 09:30:00", + "2017-02-08 09:35:00", + "2017-02-08 09:40:00", + "2017-02-08 09:45:00", + "2017-02-08 09:50:00", + "2017-02-08 09:55:00", + "2017-02-08 10:00:00", + "2017-02-08 10:05:00", + "2017-02-08 10:10:00", + "2017-02-08 10:30:00", + "2017-02-08 10:35:00", + "2017-02-08 10:40:00", + "2017-02-08 10:45:00", + "2017-02-08 10:50:00", + "2017-02-08 10:55:00", + "2017-02-08 11:00:00", + "2017-02-08 11:05:00", + "2017-02-08 11:10:00", + "2017-02-08 11:15:00", + "2017-02-08 11:20:00", + "2017-02-08 11:25:00", + "2017-02-08 13:30:00", + "2017-02-08 13:35:00", + "2017-02-08 13:40:00", + "2017-02-08 13:45:00", + "2017-02-08 13:50:00", + "2017-02-08 13:55:00", + "2017-02-08 14:00:00", + "2017-02-08 14:05:00", + "2017-02-08 14:10:00", + "2017-02-08 14:15:00", + "2017-02-08 14:20:00", + "2017-02-08 14:25:00", + "2017-02-08 14:30:00", + "2017-02-08 14:35:00", + "2017-02-08 14:40:00", + "2017-02-08 14:45:00", + "2017-02-08 14:50:00", + "2017-02-08 14:55:00", + "2017-02-08 21:00:00", + "2017-02-08 21:05:00", + "2017-02-08 21:10:00", + "2017-02-08 21:15:00", + "2017-02-08 21:20:00", + "2017-02-08 21:25:00", + "2017-02-08 21:30:00", + "2017-02-08 21:35:00", + "2017-02-08 21:40:00", + "2017-02-08 21:45:00", + "2017-02-08 21:50:00", + "2017-02-08 21:55:00", + "2017-02-08 22:00:00", + "2017-02-08 22:05:00", + "2017-02-08 22:10:00", + "2017-02-08 22:15:00", + "2017-02-08 22:20:00", + "2017-02-08 22:25:00", + "2017-02-08 22:30:00", + "2017-02-08 22:35:00", + "2017-02-08 22:40:00", + "2017-02-08 22:45:00", + "2017-02-08 22:50:00", + "2017-02-08 22:55:00", + "2017-02-08 23:00:00", + "2017-02-08 23:05:00", + "2017-02-08 23:10:00", + "2017-02-08 23:15:00", + "2017-02-08 23:20:00", + "2017-02-08 23:25:00", + "2017-02-09 09:00:00", + "2017-02-09 09:05:00", + "2017-02-09 09:10:00", + "2017-02-09 09:15:00", + "2017-02-09 09:20:00", + "2017-02-09 09:25:00", + "2017-02-09 09:30:00", + "2017-02-09 09:35:00", + "2017-02-09 09:40:00", + "2017-02-09 09:45:00", + "2017-02-09 09:50:00", + "2017-02-09 09:55:00", + "2017-02-09 10:00:00", + "2017-02-09 10:05:00", + "2017-02-09 10:10:00", + "2017-02-09 10:30:00", + "2017-02-09 10:35:00", + "2017-02-09 10:40:00", + "2017-02-09 10:45:00", + "2017-02-09 10:50:00", + "2017-02-09 10:55:00", + "2017-02-09 11:00:00", + "2017-02-09 11:05:00", + "2017-02-09 11:10:00", + "2017-02-09 11:15:00", + "2017-02-09 11:20:00", + "2017-02-09 11:25:00", + "2017-02-09 13:30:00", + "2017-02-09 13:35:00", + "2017-02-09 13:40:00", + "2017-02-09 13:45:00", + "2017-02-09 13:50:00", + "2017-02-09 13:55:00", + "2017-02-09 14:00:00", + "2017-02-09 14:05:00", + "2017-02-09 14:10:00", + "2017-02-09 14:15:00", + "2017-02-09 14:20:00", + "2017-02-09 14:25:00", + "2017-02-09 14:30:00", + "2017-02-09 14:35:00", + "2017-02-09 14:40:00", + "2017-02-09 14:45:00", + "2017-02-09 14:50:00", + "2017-02-09 21:00:00", + "2017-02-09 21:05:00", + "2017-02-09 21:10:00", + "2017-02-09 21:15:00", + "2017-02-09 21:20:00", + "2017-02-09 21:25:00", + "2017-02-09 21:30:00", + "2017-02-09 21:35:00", + "2017-02-09 21:40:00", + "2017-02-09 21:45:00", + "2017-02-09 21:50:00", + "2017-02-09 21:55:00", + "2017-02-09 22:00:00", + "2017-02-09 22:05:00", + "2017-02-09 22:10:00", + "2017-02-09 22:15:00", + "2017-02-09 22:20:00", + "2017-02-09 22:25:00", + "2017-02-09 22:30:00", + "2017-02-09 22:35:00", + "2017-02-09 22:40:00", + "2017-02-09 22:45:00", + "2017-02-09 22:50:00", + "2017-02-09 22:55:00", + "2017-02-09 23:00:00", + "2017-02-09 23:05:00", + "2017-02-09 23:10:00", + "2017-02-09 23:15:00", + "2017-02-09 23:20:00", + "2017-02-09 23:25:00", + "2017-02-10 09:00:00", + "2017-02-10 09:05:00", + "2017-02-10 09:10:00", + "2017-02-10 09:15:00", + "2017-02-10 09:20:00", + "2017-02-10 09:25:00", + "2017-02-10 09:30:00", + "2017-02-10 09:35:00", + "2017-02-10 09:40:00", + "2017-02-10 09:45:00", + "2017-02-10 09:50:00", + "2017-02-10 09:55:00", + "2017-02-10 10:00:00", + "2017-02-10 10:05:00", + "2017-02-10 10:10:00", + "2017-02-10 10:30:00", + "2017-02-10 10:35:00", + "2017-02-10 10:40:00", + "2017-02-10 10:45:00", + "2017-02-10 10:50:00", + "2017-02-10 10:55:00", + "2017-02-10 11:00:00", + "2017-02-10 11:05:00", + "2017-02-10 11:10:00", + "2017-02-10 11:15:00", + "2017-02-10 11:20:00", + "2017-02-10 11:25:00", + "2017-02-10 13:30:00", + "2017-02-10 13:35:00", + "2017-02-10 13:40:00", + "2017-02-10 13:45:00", + "2017-02-10 13:50:00", + "2017-02-10 13:55:00", + "2017-02-10 14:00:00", + "2017-02-10 14:05:00", + "2017-02-10 14:10:00", + "2017-02-10 14:15:00", + "2017-02-10 14:20:00", + "2017-02-10 14:25:00", + "2017-02-10 14:30:00", + "2017-02-10 14:35:00", + "2017-02-10 14:40:00", + "2017-02-10 14:45:00", + "2017-02-10 14:50:00", + "2017-02-10 14:55:00", + "2017-02-10 21:00:00", + "2017-02-10 21:05:00", + "2017-02-10 21:10:00", + "2017-02-10 21:15:00", + "2017-02-10 21:20:00", + "2017-02-10 21:25:00", + "2017-02-10 21:30:00", + "2017-02-10 21:35:00", + "2017-02-10 21:40:00", + "2017-02-10 21:45:00", + "2017-02-10 21:50:00", + "2017-02-10 21:55:00", + "2017-02-10 22:00:00", + "2017-02-10 22:05:00", + "2017-02-10 22:10:00", + "2017-02-10 22:15:00", + "2017-02-10 22:20:00", + "2017-02-10 22:25:00", + "2017-02-10 22:30:00", + "2017-02-10 22:35:00", + "2017-02-10 22:40:00", + "2017-02-10 22:45:00", + "2017-02-10 22:50:00", + "2017-02-10 22:55:00", + "2017-02-10 23:00:00", + "2017-02-10 23:05:00", + "2017-02-10 23:10:00", + "2017-02-10 23:15:00", + "2017-02-10 23:20:00", + "2017-02-10 23:25:00", + "2017-02-10 23:25:00", + "2017-02-13 09:00:00", + "2017-02-13 09:05:00", + "2017-02-13 09:10:00", + "2017-02-13 09:15:00", + "2017-02-13 09:20:00", + "2017-02-13 09:25:00", + "2017-02-13 09:30:00", + "2017-02-13 09:35:00", + "2017-02-13 09:40:00", + "2017-02-13 09:45:00", + "2017-02-13 09:50:00", + "2017-02-13 09:55:00", + "2017-02-13 10:00:00", + "2017-02-13 10:05:00", + "2017-02-13 10:10:00", + "2017-02-13 10:30:00", + "2017-02-13 10:35:00", + "2017-02-13 10:40:00", + "2017-02-13 10:45:00", + "2017-02-13 10:50:00", + "2017-02-13 10:55:00", + "2017-02-13 11:00:00", + "2017-02-13 11:05:00", + "2017-02-13 11:10:00", + "2017-02-13 11:15:00", + "2017-02-13 11:20:00", + "2017-02-13 11:25:00", + "2017-02-13 13:30:00", + "2017-02-13 13:35:00", + "2017-02-13 13:40:00", + "2017-02-13 13:45:00", + "2017-02-13 13:50:00", + "2017-02-13 13:55:00", + "2017-02-13 14:00:00", + "2017-02-13 14:05:00", + "2017-02-13 14:10:00", + "2017-02-13 14:15:00", + "2017-02-13 14:20:00", + "2017-02-13 14:25:00", + "2017-02-13 14:30:00", + "2017-02-13 14:35:00", + "2017-02-13 14:40:00", + "2017-02-13 14:45:00", + "2017-02-13 14:50:00", + "2017-02-13 14:55:00" + ], + "y": [ + null, + 6980, + 6980, + 6980, + 6979.04, + 6976.5568, + 6974.272256, + 6972.17047552, + 6971, + 6970, + 6968.24, + 6968, + 6948, + 6948.38, + 6948.7524, + 6949.117352, + 6949.475004960001, + 6950.216004761601, + 6950.927364571136, + 6952.371722696868, + 6953.729419335056, + 6955.5110657882515, + 6957.150180525191, + 6959.135162472672, + 6977, + 6976.66, + 6976.3268, + 6976.000264, + 6975.68025872, + 6975.3666535456, + 6975.059320474687, + 6975, + 6973.96, + 6972.9616, + 6972.003135999999, + 6971.08301056, + 6970.1996901376, + 6969.351702532095, + 6968.5376344308115, + 6967.756129053579, + 6967.005883891436, + 6966.285648535778, + 6965.594222594347, + 6964.930453690573, + 6964.29323554295, + 6949, + 6949.34, + 6950.4464, + 6952.6396159999995, + 6954.70123904, + 6956.6391646976, + 6958.460814815744, + 6960.1731659268, + 6961.782775971192, + 6963.29580941292, + 6964.718060848145, + 6966.054977197256, + 6967.311678565421, + 6968.492977851496, + 6969, + 6970, + 6971, + 6971.96, + 6972.8624, + 6973.710656, + 6974.50801664, + 6987, + 6986.76, + 6986.5248, + 6986.294304, + 6985.76253184, + 6985.252030566399, + 6984.761949343743, + 6984.291471369994, + 6983.493983087794, + 6982.414464440771, + 6979.273017996694, + 6974.44025583709, + 6970.18742513664, + 6966.4449341202435, + 6963.151542025815, + 6958.510326142201, + 6953.628673959449, + 6949.528086125937, + 6946.083592345787, + 6943.190217570461, + 6940.759782759187, + 6938.718217517717, + 6937.003302714882, + 6935.562774280501, + 6925, + 6925, + 6925.52, + 6926.019200000001, + 6926.498432, + 6926.95849472, + 6927.4001549312, + 6927.824148733952, + 6928.914699809915, + 6929.9398178213205, + 6931.304632395615, + 6932.974169156054, + 6948, + 6947.72, + 6947.4456, + 6947.176688, + 6945.64962048, + 6944.1836356608, + 6942.776290234368, + 6941.425238624994, + 6940.128229079994, + 6938.883099916794, + 6937.6877759201225, + 6936.540264883318, + 6935.438654287985, + 6934.381108116465, + 6933.365863791806, + 6932.391229240134, + 6931.455580070528, + 6930.557356867707, + 6929.695062592999, + 6928.86726008928, + 6928.072569685708, + 6909, + 6909.42, + 6909.8316, + 6910.234968000001, + 6910.63026864, + 6911.0176632672, + 6911.397310001856, + 6911.769363801818, + 6912.133976525782, + 6912.491296995266, + 6912.841471055361, + 6913.184641634253, + 6913.520948801568, + 6913.850529825537, + 6914.173519229026, + 6914.4900488444455, + 6914.800247867556, + 6915.104242910205, + 6915.402158052001, + 6915.694114890961, + 6915.980232593141, + 6916.2606279412785, + 6916.970202823627, + 6917.651394710682, + 6918.305338922255, + 6918.9331253653645, + 6919, + 6921.28, + 6924.6975999999995, + 6927.841791999999, + 6930.73444864, + 6933.3956927488, + 6935.844037328896, + 6938.096514342584, + 6940.168793195177, + 6942.075289739563, + 6943.8292665603985, + 6964, + 6963.56, + 6963.1288, + 6962.7062240000005, + 6962.29209952, + 6961.8862575296, + 6961.488532379008, + 6961.098761731428, + 6960.716786496799, + 6960.342450766862, + 6959.975601751526, + 6959.6160897164955, + 6959.263767922165, + 6958.918492563722, + 6958.5801227124475, + 6958.248520258198, + 6957.923549853034, + 6957.605078855973, + 6957.292977278854, + 6956.987117733277, + 6956.687375378611, + 6956.393627871039, + 6956.105755313618, + 6955.221525101073, + 6953.888233595008, + 6952.634939579308, + 6951.456843204549, + 6950.349432612276, + 6949.30846665554, + 6948.329958656207, + 6947.410161136835, + 6946.545551468625, + 6945.7328183805075, + 6944.968849277677, + 6944.250718321016, + 6933, + 6933.22, + 6933.435600000001, + 6933.646888000001, + 6934.261012480001, + 6934.850571980801, + 6936.059537661953, + 6937.195965402236, + 6938.2642074781015, + 6939.268355029415, + 6940.21225372765, + 6941.099518503991, + 6941.933547393752, + 6942.717534550126, + 6943.454482477119, + 6955, + 6954.62, + 6953.5952, + 6949.539487999999, + 6944.376328959999, + 6939.626222643199, + 6935.256124831743, + 6931.235634845204, + 6927.536784057587, + 6924.13384133298, + 6921.003134026342, + 6918.122883304235, + 6915.473052639896, + 6913.035208428704, + 6910.792391754408, + 6909, + 6909, + 6905.9, + 6878, + 6878.74, + 6879.4652, + 6880.175896, + 6880.87237808, + 6881.5549305184, + 6882.2238319080325, + 6882.879355269872, + 6883.521768164474, + 6884.151332801185, + 6884.768306145162, + 6885.372940022258, + 6885.965481221813, + 6886.546171597377, + 6887.11524816543, + 6887.672943202121, + 6888.2194843380785, + 6888.7550946513165, + 6889.27999275829, + 6889.794392903124, + 6890.298505045062, + 6890.79253494416, + 6891.2766842452775, + 6891.751150560372, + 6892.761104537956, + 6893.730660356438, + 6894.661433942181, + 6895.554976584494, + 6917, + 6916.46, + 6915.3216, + 6914.228736, + 6913.17958656, + 6912.1724030976, + 6911.205506973696, + 6910.277286694748, + 6909.386195226958, + 6908.53074741788, + 6907.709517521164, + 6906.921136820318, + 6906.164291347505, + 6888, + 6888.42, + 6888.8316, + 6889.234968000001, + 6889.63026864, + 6890.0176632672, + 6890.397310001856, + 6890.769363801818, + 6891, + 6891.36, + 6891.712799999999, + 6892.0585439999995, + 6892.397373119999, + 6892.7294256576, + 6893.054837144447, + 6893.89264365867, + 6894.696937912323, + 6895.46906039583, + 6896.2102979799965, + 6896.921886060797, + 6897.605010618365, + 6898.26081019363, + 6898.890377785885, + 6900.396955118732, + 6901.813137811608, + 6903.144349542911, + 6904.3956885703365, + 6924, + 6923.62, + 6923.2476, + 6922.882648, + 6922.00734208, + 6920.2069015552, + 6918.514487461887, + 6916.923618214174, + 6915.428201121324, + 6914.022509054044, + 6912.701158510802, + 6911.459089000154, + 6892, + 6892.42, + 6892.8316, + 6893.234968000001, + 6893.63026864, + 6894.0176632672, + 6894.397310001856, + 6894.769363801818, + 6895.133976525782, + 6895.491296995266, + 6896.391645115455, + 6898.108146408527, + 6899.721657624015, + 6901.2383581665745, + 6902.66405667658, + 6904.004213275985, + 6925, + 6924.6, + 6924.2080000000005, + 6923.823840000001, + 6923.447363200001, + 6923.078415936001, + 6905, + 6905.36, + 6905.712799999999, + 6906.0585439999995, + 6906.397373119999, + 6908.061478195199, + 6912.6777895034875, + 6917.0171221332785, + 6921.096094805282, + 6924.9303291169645, + 6928.534509369947, + 6931.92243880775, + 6935.107092479285, + 6939.338525080942, + 6943.231443074467, + 6946.81292762851, + 6950.107893418229, + 6953.139261944771, + 6955.928120989189, + 6958.493871310054, + 6960.854361605249, + 6963.026012676829, + 6964, + 6965.92, + 6967.6864000000005, + 6988, + 6987.56, + 6986.6176000000005, + 6985.712896000001, + 6984.350122240001, + 6982.082112460801, + 6978.173901214721, + 6974.656511093249, + 6971.490859983924, + 6968.6417739855315, + 6966.077596586978, + 6963.76983692828, + 6961.692853235452, + 6959.823567911907, + 6943, + 6943.32, + 6943.9872, + 6944.6277119999995, + 6945.73004928, + 6946.7662463232 + ] + } + ], + "layout": { + "xaxis": { + "tickformat": "%d %b %H:%M", + "tickfont": {"size": 8}, + "breaks": [ + { + "pattern": "%w", + "bounds": [6, 0], + "operation": "[]" + }, + { + "pattern": "%H", + "bounds": [0, 9], + "operation": "()" + }, + { + "pattern": "%H", + "bounds": [12, 13], + "operation": "()" + }, + { + "pattern": "%H", + "bounds": [15, 21], + "operation": "()" + } + ], + "rangeslider": { "visible": true } + }, + "legend": { + "x": 0, + "xanchor": "left", + "y": 1.05, + "yanchor": "bottom" + } + } +} diff --git a/test/image/mocks/axes_breaks-tickvals.json b/test/image/mocks/axes_breaks-tickvals.json new file mode 100644 index 00000000000..c68d40e8440 --- /dev/null +++ b/test/image/mocks/axes_breaks-tickvals.json @@ -0,0 +1,51 @@ +{ + "data": [ + { + "x": [ + "1969-12-31 23:59:59.980", + "1970-01-01 00:00:00.000", + "1970-01-01 00:00:00.010", + "1970-01-01 00:00:00.050", + "1970-01-01 00:00:00.090", + "1970-01-01 00:00:00.100", + "1970-01-01 00:00:00.150", + "1970-01-01 00:00:00.190", + "1970-01-01 00:00:00.200" + ] + } + ], + "layout": { + "xaxis": { + "breaks": [ + {"bounds": [ + "1969-12-31 23:59:59.999", + "1970-01-01 00:00:00.090" + ]}, + {"bounds": [ + "1970-01-01 00:00:00.101", + "1970-01-01 00:00:00.189" + ]} + ], + "tickvals": [ + "1969-12-31 23:59:59.980", + "1969-12-31 23:59:59.990", + "1970-01-01 00:00:00.000", + "1970-01-01 00:00:00.010", + "1970-01-01 00:00:00.050", + "1970-01-01 00:00:00.090", + "1970-01-01 00:00:00.100", + "1970-01-01 00:00:00.150", + "1970-01-01 00:00:00.190", + "1970-01-01 00:00:00.200" + ], + "ticktext": [ "(-20)", "(10)", "(0)" ], + "zeroline": true + }, + "title": { + "text": "Should not show x-axis zeroline
Should mask tickvals inside breaks
Should fill in ticktext correctly", + "font": {"size": 12}, + "x": 0, + "xref": "paper" + } + } +} diff --git a/test/image/mocks/axes_breaks-values.json b/test/image/mocks/axes_breaks-values.json new file mode 100644 index 00000000000..92fb240e3fe --- /dev/null +++ b/test/image/mocks/axes_breaks-values.json @@ -0,0 +1,21 @@ +{ + "data": [ + { + "x": [ + "2020-01-02 08:00", "2020-01-02 17:00", + "2020-01-03 08:00", "2020-01-03 17:00", + "2020-01-04 08:00", "2020-01-04 17:00", + "2020-01-05 08:00", "2020-01-05 17:00", + "2020-01-06 08:00", "2020-01-06 17:00", + "2020-01-07 08:00", "2020-01-07 17:00" + ] + } + ], + "layout": { + "xaxis": { + "breaks": [ + { "values": [ "2020-01-04", "2020-01-05" ] } + ] + } + } +} diff --git a/test/image/mocks/axes_breaks-weekends-weeknights.json b/test/image/mocks/axes_breaks-weekends-weeknights.json new file mode 100644 index 00000000000..3cef36b00cf --- /dev/null +++ b/test/image/mocks/axes_breaks-weekends-weeknights.json @@ -0,0 +1,33 @@ +{ + "data": [ + { + "x": [ + "2020-01-02 08:00", "2020-01-02 16:00", + "2020-01-03 08:00", "2020-01-03 16:00", + "2020-01-04 08:00", "2020-01-04 16:00", + "2020-01-05 08:00", "2020-01-05 16:00", + "2020-01-06 08:00", "2020-01-06 16:00", + "2020-01-07 08:00", "2020-01-07 16:00" + ] + } + ], + "layout": { + "xaxis": { + "breaks": [ + { + "pattern": "%w", + "bounds": [ 6, 0 ], + "operation": "[]" + }, + { + "pattern": "%H", + "bounds": [ 16, 8 ], + "operation": "()" + } + ] + }, + "hovermode": "closest", + "width": 600, + "height": 400 + } +} diff --git a/test/image/mocks/axes_breaks-weekends_autorange-reversed.json b/test/image/mocks/axes_breaks-weekends_autorange-reversed.json new file mode 100644 index 00000000000..6b71b31d654 --- /dev/null +++ b/test/image/mocks/axes_breaks-weekends_autorange-reversed.json @@ -0,0 +1,185 @@ +{ + "data": [ + { + "type": "bar", + "x": [ + "2020-01-02 08:00", + "2020-01-02 16:00", + "2020-01-03 08:00", + "2020-01-03 16:00", + "2020-01-04 08:00", + "2020-01-04 16:00", + "2020-01-05 08:00", + "2020-01-05 16:00", + "2020-01-06 08:00", + "2020-01-06 16:00", + "2020-01-07 08:00", + "2020-01-07 16:00" + ], + "y": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12] + }, + { + "xaxis": "x2", + "yaxis": "y2", + "type": "bar", + "x": [ + "2020-01-02 08:00", + "2020-01-02 16:00", + "2020-01-03 08:00", + "2020-01-03 16:00", + "2020-01-04 08:00", + "2020-01-04 16:00", + "2020-01-05 08:00", + "2020-01-05 16:00", + "2020-01-06 08:00", + "2020-01-06 16:00", + "2020-01-07 08:00", + "2020-01-07 16:00" + ], + "y": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12] + }, + { + "xaxis": "x3", + "yaxis": "y3", + "type": "bar", + "orientation": "h", + "y": [ + "2020-01-02 08:00", + "2020-01-02 16:00", + "2020-01-03 08:00", + "2020-01-03 16:00", + "2020-01-04 08:00", + "2020-01-04 16:00", + "2020-01-05 08:00", + "2020-01-05 16:00", + "2020-01-06 08:00", + "2020-01-06 16:00", + "2020-01-07 08:00", + "2020-01-07 16:00" + ], + "x": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12] + }, + { + "xaxis": "x4", + "yaxis": "y4", + "type": "bar", + "orientation": "h", + "y": [ + "2020-01-02 08:00", + "2020-01-02 16:00", + "2020-01-03 08:00", + "2020-01-03 16:00", + "2020-01-04 08:00", + "2020-01-04 16:00", + "2020-01-05 08:00", + "2020-01-05 16:00", + "2020-01-06 08:00", + "2020-01-06 16:00", + "2020-01-07 08:00", + "2020-01-07 16:00" + ], + "x": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12] + } + ], + "layout": { + "showlegend": false, + "width": 800, + "height": 800, + "xaxis": { + "breaks": [ + { + "pattern": "%w", + "bounds": [ + 6, + 0 + ], + "operation": "[]" + } + ], + "domain": [ + 0, + 0.48 + ] + }, + "xaxis2": { + "breaks": [ + { + "pattern": "%w", + "bounds": [ + 6, + 0 + ], + "operation": "[]" + } + ], + "autorange": "reversed", + "anchor": "y2", + "domain": [ + 0.52, + 1 + ] + }, + "xaxis3": { + "anchor": "y3", + "domain": [ + 0, + 0.48 + ] + }, + "xaxis4": { + "anchor": "y4", + "domain": [ + 0.52, + 1 + ] + }, + "yaxis": { + "domain": [ + 0, + 0.48 + ] + }, + "yaxis2": { + "anchor": "x2", + "domain": [ + 0.52, + 1 + ] + }, + "yaxis3": { + "breaks": [ + { + "pattern": "%w", + "bounds": [ + 6, + 0 + ], + "operation": "[]" + } + ], + "anchor": "x3", + "domain": [ + 0.52, + 1 + ] + }, + "yaxis4": { + "breaks": [ + { + "pattern": "%w", + "bounds": [ + 6, + 0 + ], + "operation": "[]" + } + ], + "autorange": "reversed", + "anchor": "x4", + "domain": [ + 0, + 0.48 + ] + } + } +} diff --git a/test/image/mocks/axes_breaks.json b/test/image/mocks/axes_breaks.json new file mode 100644 index 00000000000..0b2ac05a290 --- /dev/null +++ b/test/image/mocks/axes_breaks.json @@ -0,0 +1,113 @@ +{ + "data": [ + { + "y": [ + "1970-01-01 00:00:00.000", + "1970-01-01 00:00:00.010", + "1970-01-01 00:00:00.050", + "1970-01-01 00:00:00.090", + "1970-01-01 00:00:00.100", + "1970-01-01 00:00:00.150", + "1970-01-01 00:00:00.190", + "1970-01-01 00:00:00.200" + ], + "xaxis": "x", + "yaxis": "y" + }, + { + "y": [ + "1970-01-01 00:00:00.000", + "1970-01-01 00:00:00.010", + "1970-01-01 00:00:00.050", + "1970-01-01 00:00:00.090", + "1970-01-01 00:00:00.100", + "1970-01-01 00:00:00.150", + "1970-01-01 00:00:00.190", + "1970-01-01 00:00:00.200" + ], + "xaxis": "x2", + "yaxis": "y2" + }, + { + "x": [ + "1970-01-01 00:00:00.000", + "1970-01-01 00:00:00.010", + "1970-01-01 00:00:00.050", + "1970-01-01 00:00:00.090", + "1970-01-01 00:00:00.100", + "1970-01-01 00:00:00.150", + "1970-01-01 00:00:00.190", + "1970-01-01 00:00:00.200" + ], + "xaxis": "x3", + "yaxis": "y3" + }, + { + "x": [ + "1970-01-01 00:00:00.000", + "1970-01-01 00:00:00.010", + "1970-01-01 00:00:00.050", + "1970-01-01 00:00:00.090", + "1970-01-01 00:00:00.100", + "1970-01-01 00:00:00.150", + "1970-01-01 00:00:00.190", + "1970-01-01 00:00:00.200" + ], + "xaxis": "x4", + "yaxis": "y4" + } + ], + "layout": { + "grid": { "rows": 2, "columns": 2, "pattern": "independent" }, + "yaxis": { + "breaks": [ + {"bounds": [ + "1970-01-01 00:00:00.011", + "1970-01-01 00:00:00.089" + ]}, + { "bounds": [ + "1970-01-01 00:00:00.101", + "1970-01-01 00:00:00.189" + ]} + ] + }, + "xaxis3": { + "breaks": [ + {"bounds": [ + "1970-01-01 00:00:00.011", + "1970-01-01 00:00:00.089" + ]}, + {"bounds": [ + "1970-01-01 00:00:00.101", + "1970-01-01 00:00:00.189" + ]} + ] + }, + "shapes": [ + { + "type": "rect", + "y0": 11, "y1": 89, "yref": "y2", + "x0": 0.56, "x1": 1, "xref": "paper" + }, + { + "type": "rect", + "y0": 101, "y1": 189, "yref": "y2", + "x0": 0.56, "x1": 1, "xref": "paper" + }, + { + "type": "rect", + "x0": 11, "x1": 89, "xref": "x4", + "y0": 0, "y1": 0.5, "yref": "paper" + }, + { + "type": "rect", + "x0": 101, "x1": 189, "xref": "x4", + "y0": 0, "y1": 0.5, "yref": "paper" + } + ], + "showlegend": false, + "hovermode": "closest", + "width": 700, + "height": 600 + } +} diff --git a/test/jasmine/tests/axes_test.js b/test/jasmine/tests/axes_test.js index 83a3298876f..9b023fb0eb6 100644 --- a/test/jasmine/tests/axes_test.js +++ b/test/jasmine/tests/axes_test.js @@ -13,6 +13,7 @@ var Axes = require('@src/plots/cartesian/axes'); var Fx = require('@src/components/fx'); var supplyLayoutDefaults = require('@src/plots/cartesian/layout_defaults'); var BADNUM = require('@src/constants/numerical').BADNUM; +var ONEDAY = require('@src/constants/numerical').ONEDAY; var createGraphDiv = require('../assets/create_graph_div'); var destroyGraphDiv = require('../assets/destroy_graph_div'); @@ -1017,6 +1018,123 @@ describe('Test axes', function() { expect(layoutOut.yaxis.range).withContext('yaxis range').toEqual([0, 4]); expect(layoutOut.yaxis2.range).withContext('yaxis2 range').toEqual([0, 4]); }); + + it('should coerce *breaks* container only on a date axis', function() { + var bounds = ['2020-01-10', '2020-01-11']; + layoutIn = { + xaxis: {breaks: [{bounds: bounds}], type: 'date'}, + xaxis2: {breaks: [{bounds: bounds}], type: '-'}, + xaxis3: {breaks: [{bounds: bounds}], type: 'linear'}, + xaxis4: {breaks: [{bounds: bounds}], type: 'log'}, + xaxis5: {breaks: [{bounds: bounds}], type: 'category'}, + xaxis6: {breaks: [{bounds: bounds}], type: 'multicategory'} + }; + layoutOut._subplots.xaxis.push('x2', 'x3', 'x4', 'x5', 'x6'); + supplyLayoutDefaults(layoutIn, layoutOut, fullData); + + expect(Array.isArray(layoutOut.xaxis.breaks) && layoutOut.xaxis.breaks.length) + .toBe(1, 'xaxis.breaks is array of length 1'); + expect(layoutOut.xaxis2.breaks).toBeUndefined(); + expect(layoutOut.xaxis3.breaks).toBeUndefined(); + expect(layoutOut.xaxis4.breaks).toBeUndefined(); + expect(layoutOut.xaxis5.breaks).toBeUndefined(); + expect(layoutOut.xaxis6.breaks).toBeUndefined(); + }); + + it('should coerce *breaks* container only when it is a non-empty array', function() { + layoutIn = { + xaxis: {type: 'date', breaks: [{bounds: ['2020-01-10', '2020-01-11']}]}, + xaxis2: {type: 'date', breaks: []}, + xaxis3: {type: 'date', breaks: false}, + xaxis4: {type: 'date'} + }; + layoutOut._subplots.xaxis.push('x2', 'x3', 'x4'); + supplyLayoutDefaults(layoutIn, layoutOut, fullData); + + expect(Array.isArray(layoutOut.xaxis.breaks) && layoutOut.xaxis.breaks.length) + .toBe(1, 'xaxis.breaks is array of length 1'); + expect(layoutOut.xaxis2.breaks).toBeUndefined(); + expect(layoutOut.xaxis3.breaks).toBeUndefined(); + expect(layoutOut.xaxis4.breaks).toBeUndefined(); + }); + + it('should set *breaks* to *enabled:false* when *bounds* have less than 2 items', function() { + layoutIn = { + xaxis: {type: 'date', breaks: [{bounds: ['2020-01-10']}]}, + xaxis2: {type: 'date', breaks: [{bounds: ['2020-01-10'], values: ['2020-01-11']}]}, + xaxis3: {type: 'date', breaks: [{bounds: ['2020-01-10'], values: {}}]}, + xaxis4: {type: 'date', breaks: [{bounds: ['2020-01-10', '2020-01-11', '2020-01-12']}]} + }; + layoutOut._subplots.xaxis.push('x2', 'x3', 'x4'); + supplyLayoutDefaults(layoutIn, layoutOut, fullData); + + expect(layoutOut.xaxis.breaks[0].enabled).toBe(false, 'invalid *bounds*'); + expect(layoutOut.xaxis2.breaks[0].enabled).toBe(true, 'invalid *bounds*, valid *values*'); + expect(layoutOut.xaxis3.breaks[0].enabled).toBe(false, 'invalid *bounds*, invalid *values*'); + expect(layoutOut.xaxis4.breaks[0].enabled && layoutOut.xaxis4.breaks[0].bounds) + .withContext('valid *bounds*, sliced to length=2').toEqual(['2020-01-10', '2020-01-11']); + }); + + it('if *breaks* *bounds* are bigger than the set *range*, disable break', function() { + layoutIn = { + xaxis: {type: 'date', range: ['2020-01-10', '2020-01-14'], breaks: [{bounds: ['2020-01-11', '2020-01-12']}]}, + xaxis2: {type: 'date', range: ['2020-01-11', '2020-01-12'], breaks: [{bounds: ['2020-01-10', '2020-01-14']}]}, + xaxis3: {type: 'date', range: ['2020-01-14', '2020-01-10'], breaks: [{bounds: ['2020-01-12', '2020-01-11']}]}, + xaxis4: {type: 'date', range: ['2020-01-12', '2020-01-11'], breaks: [{bounds: ['2020-01-14', '2020-01-10']}]} + }; + layoutOut._subplots.xaxis.push('x2', 'x3', 'x4'); + supplyLayoutDefaults(layoutIn, layoutOut, fullData); + + expect(layoutOut.xaxis.breaks[0].enabled).toBe(true, '*bounds* within set range'); + expect(layoutOut.xaxis2.breaks[0].enabled).toBe(false, '*bounds* bigger than set range'); + expect(layoutOut.xaxis3.breaks[0].enabled).toBe(true, '*bounds* within set range (reversed)'); + expect(layoutOut.xaxis4.breaks[0].enabled).toBe(false, '*bounds* bigger than set range (reversed)'); + }); + + it('should coerce *breaks* *bounds* over *values*/*dvalue* if both are present', function() { + layoutIn = { + xaxis: {type: 'date', breaks: [{bounds: ['2020-01-10', '2020-01-11']}]}, + xaxis2: {type: 'date', breaks: [{values: ['2020-01-10', '2020-01-12', '2020-01-14'], dvalue: 2}]}, + xaxis3: {type: 'date', breaks: [{bounds: ['2020-01-10', '2020-01-11'], values: ['2020-01-10', '2020-01-12', '2020-01-14'], dvalue: 2}]}, + xaxis4: {type: 'date', breaks: [{bounds: false, values: ['2020-01-10', '2020-01-12', '2020-01-14'], dvalue: 2}]}, + }; + layoutOut._subplots.xaxis.push('x2', 'x3', 'x4'); + supplyLayoutDefaults(layoutIn, layoutOut, fullData); + + var xaBreak = layoutOut.xaxis.breaks[0]; + expect(xaBreak.bounds).withContext('valid *bounds*').toEqual(['2020-01-10', '2020-01-11']); + expect(xaBreak.values).toBe(undefined, 'not coerced'); + expect(xaBreak.dvalue).toBe(undefined, 'not coerced'); + + xaBreak = layoutOut.xaxis2.breaks[0]; + expect(xaBreak.bounds).toBe(undefined, 'not set, not coerced'); + expect(xaBreak.values).withContext('valid *values*').toEqual(['2020-01-10', '2020-01-12', '2020-01-14']); + expect(xaBreak.dvalue).toBe(2, 'valid *dvalue*'); + + xaBreak = layoutOut.xaxis3.breaks[0]; + expect(xaBreak.bounds).withContext('set to valid, coerced').toEqual(['2020-01-10', '2020-01-11']); + expect(xaBreak.values).toBe(undefined, 'not coerced'); + expect(xaBreak.dvalue).toBe(undefined, 'not coerced'); + + xaBreak = layoutOut.xaxis4.breaks[0]; + expect(xaBreak.bounds).toBe(undefined, 'set but invalid, not coerced'); + expect(xaBreak.values).withContext('valid *values*').toEqual(['2020-01-10', '2020-01-12', '2020-01-14']); + expect(xaBreak.dvalue).toBe(2, 'valid *dvalue*'); + }); + + it('should only coerce breaks *pattern* with *bounds*', function() { + layoutIn = { + xaxis: {type: 'date', breaks: [{bounds: ['2020-01-04', '2020-01-05']}]}, + xaxis2: {type: 'date', breaks: [{bounds: [6, 0], pattern: '%w'}]}, + xaxis3: {type: 'date', breaks: [{values: ['2020-01-04', '2020-01-05'], pattern: 'NOP'}]}, + }; + layoutOut._subplots.xaxis.push('x2', 'x3'); + supplyLayoutDefaults(layoutIn, layoutOut, fullData); + + expect(layoutOut.xaxis.breaks[0].pattern).toBe('', 'coerced to dflt value'); + expect(layoutOut.xaxis2.breaks[0].pattern).toBe('%w', 'coerced'); + expect(layoutOut.xaxis3.breaks[0].pattern).toBe(undefined, 'not coerce, using *values*'); + }); }); describe('constraints relayout', function() { @@ -3902,6 +4020,920 @@ describe('Test axes', function() { .then(done); }); }); + + describe('*breaks*', function() { + // TODO adapt `type: 'date'` requirement !! + + describe('during doCalcdata', function() { + var gd; + + function _calc(trace, layout) { + gd = {data: [trace], layout: layout}; + supplyDefaults(gd); + Plots.doCalcdata(gd); + } + + function _assert(msg, exp) { + var cd = gd.calcdata[0]; + var xc = cd.map(function(cdi) { return cdi.x; }); + expect(xc).withContext(msg).toEqual(exp); + } + + it('should discard coords within break bounds', function() { + var x = [ + '1970-01-01 00:00:00.000', + '1970-01-01 00:00:00.010', + '1970-01-01 00:00:00.050', + '1970-01-01 00:00:00.090', + '1970-01-01 00:00:00.100', + '1970-01-01 00:00:00.150', + '1970-01-01 00:00:00.190', + '1970-01-01 00:00:00.200' + ]; + + _calc({ + x: x + }, { + xaxis: { + breaks: [ + {'operation': '()', bounds: [ + '1970-01-01 00:00:00.010', + '1970-01-01 00:00:00.090' + ]}, + {'operation': '()', bounds: [ + '1970-01-01 00:00:00.100', + '1970-01-01 00:00:00.190' + ]} + ] + } + }); + _assert('with operation:()', [0, 10, BADNUM, 90, 100, BADNUM, 190, 200]); + + _calc({ + x: x + }, { + xaxis: { + breaks: [ + {'operation': '[]', bounds: [ + '1970-01-01 00:00:00.010', + '1970-01-01 00:00:00.090' + ]}, + {'operation': '[]', bounds: [ + '1970-01-01 00:00:00.100', + '1970-01-01 00:00:00.190' + ]} + ] + } + }); + _assert('with operation:[]', [0, BADNUM, BADNUM, BADNUM, BADNUM, BADNUM, BADNUM, 200]); + + _calc({ + x: x + }, { + xaxis: { + breaks: [ + {'operation': '[)', bounds: [ + '1970-01-01 00:00:00.010', + '1970-01-01 00:00:00.090' + ]}, + {'operation': '(]', bounds: [ + '1970-01-01 00:00:00.100', + '1970-01-01 00:00:00.190' + ]} + ] + } + }); + _assert('with mixed operation values', [0, BADNUM, BADNUM, 90, 100, BADNUM, BADNUM, 200]); + }); + + it('should discard coords within break bounds - date %w case', function() { + var x = [ + // Thursday + '2020-01-02 08:00', '2020-01-02 16:00', + // Friday + '2020-01-03 08:00', '2020-01-03 16:00', + // Saturday + '2020-01-04 08:00', '2020-01-04 16:00', + // Sunday + '2020-01-05 08:00', '2020-01-05 16:00', + // Monday + '2020-01-06 08:00', '2020-01-06 16:00', + // Tuesday + '2020-01-07 08:00', '2020-01-07 16:00' + ]; + + var noWeekend = [ + 1577952000000, 1577980800000, + 1578038400000, 1578067200000, + BADNUM, BADNUM, + BADNUM, BADNUM, + 1578297600000, 1578326400000, + 1578384000000, 1578412800000 + ]; + + _calc({x: x}, { + xaxis: { + breaks: [ + {pattern: '%w', bounds: [6, 0], operation: '[]'} + ] + } + }); + _assert('[6,0]', noWeekend); + + _calc({x: x}, { + xaxis: { + breaks: [ + {pattern: '%w', bounds: [5, 1], operation: '()'} + ] + } + }); + _assert('(5,1)', noWeekend); + + _calc({x: x}, { + xaxis: { + breaks: [ + {pattern: '%w', bounds: [6, 1], operation: '[)'} + ] + } + }); + _assert('[6,1)', noWeekend); + + _calc({x: x}, { + xaxis: { + breaks: [ + {pattern: '%w', bounds: [5, 0], operation: '(]'} + ] + } + }); + _assert('(5,0]', noWeekend); + }); + + it('should discard coords within break bounds - date %H case', function() { + _calc({ + x: [ + '2020-01-02 08:00', '2020-01-02 20:00', + '2020-01-03 08:00', '2020-01-03 20:00', + '2020-01-04 08:00', '2020-01-04 20:00', + '2020-01-05 08:00', '2020-01-05 20:00', + '2020-01-06 08:00', '2020-01-06 20:00', + '2020-01-07 08:00', '2020-01-07 20:00' + ] + }, { + xaxis: { + breaks: [ + {pattern: '%H', bounds: [17, 8]} + ] + } + }); + _assert('with dflt operation', [ + 1577952000000, BADNUM, + 1578038400000, BADNUM, + 1578124800000, BADNUM, + 1578211200000, BADNUM, + 1578297600000, BADNUM, + 1578384000000, BADNUM + ]); + }); + + it('should discard coords within break bounds - date %H / high precision case', function() { + _calc({ + x: [ + '2020-01-03 17:00', + '2020-01-03 17:15', + '2020-01-03 17:30', + '2020-01-06 7:45', + '2020-01-06 8:00', + '2020-01-06 8:15', + '2020-01-06 8:30' + ] + }, { + xaxis: { + breaks: [ + {pattern: '%H', bounds: [17, 8]} + ] + } + }); + _assert('with dflt operation', [ + Lib.dateTime2ms('2020-01-03 17:00'), + BADNUM, + BADNUM, + BADNUM, + Lib.dateTime2ms('2020-01-06 8:00'), + Lib.dateTime2ms('2020-01-06 8:15'), + Lib.dateTime2ms('2020-01-06 8:30') + ]); + }); + + it('should discard coords within [values[i], values[i] + dvalue] bounds', function() { + var x = [ + // Thursday + '2020-01-02 08:00', '2020-01-02 16:00', + // Friday + '2020-01-03 08:00', '2020-01-03 16:00', + // Saturday + '2020-01-04 08:00', '2020-01-04 16:00', + // Sunday + '2020-01-05 08:00', '2020-01-05 16:00', + // Monday + '2020-01-06 08:00', '2020-01-06 16:00', + // Tuesday + '2020-01-07 08:00', '2020-01-07 16:00' + ]; + + _calc({x: x}, { + xaxis: { + breaks: [{values: ['2020-01-04', '2020-01-05'], dvalue: ONEDAY}], + } + }); + _assert('two values', [ + 1577952000000, 1577980800000, + 1578038400000, 1578067200000, + BADNUM, BADNUM, + BADNUM, BADNUM, + 1578297600000, 1578326400000, + 1578384000000, 1578412800000 + ]); + }); + + it('should discard coords equal to two consecutive open values bounds', function() { + _calc({ + x: [ + '1970-01-01 00:00:00.001', + '1970-01-01 00:00:00.002', + '1970-01-01 00:00:00.003', + '1970-01-01 00:00:00.004', + '1970-01-01 00:00:00.005' + ] + }, { + xaxis: { + breaks: [{ values: [ + '1970-01-01 00:00:00.002', + '1970-01-01 00:00:00.003' + ], dvalue: 1, operation: '()' }] + } + }); + _assert('', [1, 2, BADNUM, 4, 5]); + }); + + it('should adapt coords generated from x0/dx about breaks', function() { + _calc({ + x0: '1970-01-01 00:00:00.001', + dx: 0.5, + y: [1, 3, 5, 2, 4] + }, { + xaxis: { + breaks: [ + {bounds: [ + '1970-01-01 00:00:00.002', + '1970-01-01 00:00:00.003' + ]} + ] + } + }); + _assert('generated x=2.5 gets masked', [1, 1.5, 2, BADNUM, 3]); + }); + }); + + describe('during doAutorange', function() { + var gd; + + beforeEach(function() { + gd = createGraphDiv(); + }); + + afterEach(destroyGraphDiv); + + function _assert(msg, exp) { + expect(gd._fullLayout.xaxis.range).toEqual(exp.xrng, msg + '| x range'); + expect(gd._fullLayout.xaxis._lBreaks).toBe(exp.lBreaks, msg + '| lBreaks'); + } + + it('should adapt padding about axis breaks length', function(done) { + Plotly.plot(gd, [{ + mode: 'markers', + x: [ + '1970-01-01 00:00:00.000', + '1970-01-01 00:00:00.010', + '1970-01-01 00:00:00.050', + '1970-01-01 00:00:00.090', + '1970-01-01 00:00:00.100', + '1970-01-01 00:00:00.150', + '1970-01-01 00:00:00.190', + '1970-01-01 00:00:00.200' + ] + }], { + xaxis: { + breaks: [ + {bounds: [ + '1970-01-01 00:00:00.011', + '1970-01-01 00:00:00.089' + ]}, + {bounds: [ + '1970-01-01 00:00:00.101', + '1970-01-01 00:00:00.189' + ]} + ] + } + }) + .then(function() { + _assert('mode:markers (i.e. with padding)', { + xrng: ['1969-12-31 23:59:59.9978', '1970-01-01 00:00:00.2022'], + lBreaks: 166 + }); + }) + .then(function() { + gd.data[0].mode = 'lines'; + return Plotly.react(gd, gd.data, gd.layout); + }) + .then(function() { + _assert('mode:lines (i.e. no padding)', { + xrng: ['1970-01-01', '1970-01-01 00:00:00.2'], + lBreaks: 166 + }); + }) + .then(function() { + gd.data[0].mode = 'markers'; + gd.layout.xaxis.breaks[0].enabled = false; + return Plotly.react(gd, gd.data, gd.layout); + }) + .then(function() { + _assert('mode:markers | one of two breaks enabled', { + xrng: ['1969-12-31 23:59:59.9928', '1970-01-01 00:00:00.2072'], + lBreaks: 88 + }); + }) + .then(function() { + gd.layout.xaxis.breaks[1].enabled = false; + return Plotly.react(gd, gd.data, gd.layout); + }) + .then(function() { + _assert('mode:markers | no breaks enabled', { + xrng: ['1969-12-31 23:59:59.9871', '1970-01-01 00:00:00.2129'], + lBreaks: 0 + }); + }) + .catch(failTest) + .then(done); + }); + }); + + describe('during setConvert (once range is available)', function() { + var gd; + + beforeEach(function() { + gd = createGraphDiv(); + }); + + afterEach(destroyGraphDiv); + + function _assert(msg, axLetter, exp) { + var fullLayout = gd._fullLayout; + var ax = fullLayout[axLetter + 'axis']; + + if(exp) { + expect(ax._breaks.length) + .toBe(exp.breaks.length, msg + '| correct # of breaks'); + expect(ax._breaks.map(function(brk) { return [brk.min, brk.max]; })) + .toBeCloseTo2DArray(exp.breaks, 2, msg + '| breaks [min,max]'); + + expect(ax._m2).toBe(exp.m2, msg + '| l2p slope'); + expect(ax._B).toBeCloseToArray(exp.B, 2, msg + '| l2p piecewise offsets'); + } else { + expect(ax._breaks).withContext(msg).toEqual([]); + expect(ax._m2).toBe(0, msg); + expect(ax._B).withContext(msg).toEqual([]); + } + } + + it('should locate breaks & compute l <-> p parameters - x-axis case', function(done) { + Plotly.plot(gd, [{ + x: [ + '1970-01-01 00:00:00.000', + '1970-01-01 00:00:00.010', + '1970-01-01 00:00:00.050', + '1970-01-01 00:00:00.090', + '1970-01-01 00:00:00.100', + '1970-01-01 00:00:00.150', + '1970-01-01 00:00:00.190', + '1970-01-01 00:00:00.200' + ] + }], { + xaxis: {} + }) + .then(function() { + _assert('no set breaks', 'x', null); + }) + .then(function() { + gd.layout.xaxis.breaks = [ + {bounds: [ + '1970-01-01 00:00:00.011', + '1970-01-01 00:00:00.089' + ]}, + {bounds: [ + '1970-01-01 00:00:00.101', + '1970-01-01 00:00:00.189' + ]} + ]; + return Plotly.react(gd, gd.data, gd.layout); + }) + .then(function() { + _assert('2 disjoint breaks within range', 'x', { + breaks: [[11, 89], [101, 189]], + m2: 14.062499999998405, + B: [30.937, -1065.937, -2303.437] + }); + }) + .then(function() { + gd.layout.xaxis.breaks = [ + {bounds: [ + '1970-01-01 00:00:00.011', + '1970-01-01 00:00:00.089' + ]}, + {bounds: [ + '1970-01-01 00:00:00.070', + '1970-01-01 00:00:00.189' + ]} + ]; + return Plotly.react(gd, gd.data, gd.layout); + }) + .then(function() { + _assert('2 overlapping breaks within range', 'x', { + breaks: [[11, 189]], + m2: 21.7741935483922, + B: [30.483, -3845.322] + }); + }) + .then(function() { + gd.layout.xaxis.breaks = [ + {bounds: [ + '1969-12-31 23:59:59.990', + '1970-01-01 00:00:00.089' + ]}, + {bounds: [ + '1970-01-01 00:00:00.101', + '1970-01-01 00:00:00.189' + ]} + ]; + return Plotly.react(gd, gd.data, gd.layout); + }) + .then(function() { + _assert('break beyond xaxis.range[0]', 'x', { + breaks: [[88.6, 89], [101, 189]], + m2: 22.1311475409836, + B: [-1960.819, -1969.672, -3917.213] + }); + }) + .then(function() { + gd.layout.xaxis.breaks = [ + {bounds: [ + '1970-01-01 00:00:00.011', + '1970-01-01 00:00:00.089' + ]}, + {bounds: [ + '1970-01-01 00:00:00.101', + '1970-01-01 00:00:00.300' + ]} + ]; + return Plotly.react(gd, gd.data, gd.layout); + }) + .then(function() { + _assert('break beyond xaxis.range[1]', 'x', { + breaks: [[11, 89], [101, 101.4]], + m2: 22.131147540988888, + B: [30.983, -1695.245, -1704.098] + }); + }) + .then(function() { + gd.layout.xaxis.breaks = [ + {bounds: [ + '1969-12-31 23:59:59.989', + '1970-01-01 00:00:00.090' + ]}, + {bounds: [ + '1970-01-01 00:00:00.101', + '1970-01-01 00:00:00.300' + ]} + ]; + return Plotly.react(gd, gd.data, gd.layout); + }) + .then(function() { + _assert('both breaks beyond xaxis.range', 'x', { + breaks: [[89.4, 90]], + m2: 50.943396226415125, + B: [-4554.339622641512, -4584.9056603773615] + }); + }) + .catch(failTest) + .then(done); + }); + + it('should locate breaks & compute l <-> p parameters - y-axis case', function(done) { + Plotly.plot(gd, [{ + y: [ + '1970-01-01 00:00:00.000', + '1970-01-01 00:00:00.010', + '1970-01-01 00:00:00.050', + '1970-01-01 00:00:00.090', + '1970-01-01 00:00:00.100', + '1970-01-01 00:00:00.150', + '1970-01-01 00:00:00.190', + '1970-01-01 00:00:00.200' + ] + }], { + yaxis: {} + }) + .then(function() { + _assert('no set breaks', 'y', null); + }) + .then(function() { + gd.layout.yaxis.breaks = [ + {bounds: [ + '1970-01-01 00:00:00.011', + '1970-01-01 00:00:00.089' + ]}, + {bounds: [ + '1970-01-01 00:00:00.101', + '1970-01-01 00:00:00.189' + ]} + ]; + return Plotly.react(gd, gd.data, gd.layout); + }) + .then(function() { + _assert('2 disjoint breaks within range', 'y', { + breaks: [[101, 189], [11, 89]], + m2: 6.923076923076923, + B: [1401.923, 792.692, 252.692] + }); + }) + .then(function() { + gd.layout.yaxis.breaks = [ + {bounds: [ + '1970-01-01 00:00:00.011', + '1970-01-01 00:00:00.089' + ]}, + {bounds: [ + '1970-01-01 00:00:00.070', + '1970-01-01 00:00:00.189' + ]} + ]; + return Plotly.react(gd, gd.data, gd.layout); + }) + .then(function() { + _assert('2 overlapping breaks within range', 'y', { + breaks: [[11, 189]], + m2: 10.714285714283243, + B: [2160, 252.857] + }); + }) + .catch(failTest) + .then(done); + }); + + it('should locate breaks & compute l <-> p parameters - date axis case', function(done) { + Plotly.plot(gd, [{ + x: [ + // Thursday + '2020-01-02 08:00', '2020-01-02 17:00', + // Friday + '2020-01-03 08:00', '2020-01-03 17:00', + // Saturday + '2020-01-04 08:00', '2020-01-04 17:00', + // Sunday + '2020-01-05 08:00', '2020-01-05 17:00', + // Monday + '2020-01-06 08:00', '2020-01-06 17:00', + // Tuesday + '2020-01-07 08:00', '2020-01-07 17:00' + ] + }], { + xaxis: {} + }) + .then(function() { + _assert('no set breaks', 'x', null); + }) + .then(function() { + gd.layout.xaxis.breaks = [ + {pattern: '%w', bounds: [5, 1]} + ]; + return Plotly.react(gd, gd.data, gd.layout); + }) + .then(function() { + _assert('break over the weekend days', 'x', { + breaks: [ + ['2020-01-04', '2020-01-06'].map(Lib.dateTime2ms) + ], + m2: 0.000001640946501588664, + B: [-2589304.064, -2589587.619] + }); + }) + .then(function() { + gd.layout.xaxis.breaks = [ + {pattern: '%w', bounds: [6, 0], operation: '[]'} + ]; + return Plotly.react(gd, gd.data, gd.layout); + }) + .then(function() { + _assert('break over the weekend days (with operation:[])', 'x', { + breaks: [ + ['2020-01-04', '2020-01-06'].map(Lib.dateTime2ms) + ], + m2: 0.000001640946501588664, + B: [-2589304.064, -2589587.619] + }); + }) + .then(function() { + gd.layout.xaxis.breaks = [ + {pattern: '%w', bounds: [4, 6]} + ]; + return Plotly.react(gd, gd.data, gd.layout); + }) + .then(function() { + _assert('skip Friday', 'x', { + breaks: [ + ['2020-01-03', '2020-01-04'].map(Lib.dateTime2ms) + ], + m2: 0.0000012658730158736563, + B: [-1997456.107, -1997565.478] + }); + }) + .then(function() { + gd.layout.xaxis.breaks = [ + {pattern: '%w', bounds: [5, 5], operation: '[]'} + ]; + return Plotly.react(gd, gd.data, gd.layout); + }) + .then(function() { + _assert('skip Friday (operation:[] version)', 'x', { + breaks: [ + ['2020-01-03', '2020-01-04'].map(Lib.dateTime2ms) + ], + m2: 0.0000012658730158736563, + B: [-1997456.107, -1997565.478] + }); + }) + .then(function() { + gd.layout.xaxis.breaks = [ + {pattern: '%w', bounds: [5, 5], operation: '()'} + ]; + return Plotly.react(gd, gd.data, gd.layout); + }) + .then(function() { + _assert('bad input -> implied empty breaks', 'x', null); + }) + .then(function() { + gd.layout.xaxis.breaks = [ + {pattern: '%H', bounds: [17, 8]} + ]; + return Plotly.react(gd, gd.data, gd.layout); + }) + .then(function() { + _assert('breaks outside workday hours', 'x', { + breaks: [ + ['2020-01-02 17:00:00', '2020-01-03 08:00:00'].map(Lib.dateTime2ms), + ['2020-01-03 17:00:00', '2020-01-04 08:00:00'].map(Lib.dateTime2ms), + ['2020-01-04 17:00:00', '2020-01-05 08:00:00'].map(Lib.dateTime2ms), + ['2020-01-05 17:00:00', '2020-01-06 08:00:00'].map(Lib.dateTime2ms), + ['2020-01-06 17:00:00', '2020-01-07 08:00:00'].map(Lib.dateTime2ms), + [Lib.dateTime2ms('2020-01-07 17:00:00'), 1578428892790] + ], + m2: 0.0000026100474550128112, + B: [ + -4118496.99495763, -4118637.937520201, + -4118778.8800827716, -4118919.8226453424, + -4119060.7652079132, -4119201.707770484, + -4119234.3145452295 + ] + }); + }) + .then(function() { + gd.layout.xaxis.breaks = [ + {pattern: '%w', bounds: [5, 1]}, + {pattern: '%H', bounds: [17, 8]} + ]; + return Plotly.react(gd, gd.data, gd.layout); + }) + .then(function() { + _assert('breaks outside workday hours & weekends', 'x', { + breaks: [ + ['2020-01-02 17:00:00', '2020-01-03 08:00:00'].map(Lib.dateTime2ms), + ['2020-01-03 17:00:00', '2020-01-06 08:00:00'].map(Lib.dateTime2ms), + ['2020-01-06 17:00:00', '2020-01-07 08:00:00'].map(Lib.dateTime2ms), + [Lib.dateTime2ms('2020-01-07 17:00:00'), 1578424728526.6] + ], + m2: 0.000003915071184408763, + B: [ + -6177761.798805676, -6177973.212649634, + -6178861.150794258, -6179072.564638216, + -6179105.171412717 + ] + }); + }) + .then(function() { + gd.layout.xaxis.breaks = [ + {pattern: '%H', bounds: [17, 8]}, + {pattern: '%w', bounds: [5, 1]} + ]; + return Plotly.react(gd, gd.data, gd.layout); + }) + .then(function() { + _assert('breaks outside workday hours & weekends (reversed break order)', 'x', { + breaks: [ + ['2020-01-02 17:00:00', '2020-01-03 08:00:00'].map(Lib.dateTime2ms), + ['2020-01-03 17:00:00', '2020-01-06 08:00:00'].map(Lib.dateTime2ms), + ['2020-01-06 17:00:00', '2020-01-07 08:00:00'].map(Lib.dateTime2ms), + [Lib.dateTime2ms('2020-01-07 17:00:00'), 1578424728526.6] + ], + m2: 0.000003915071184408763, + B: [ + -6177761.798805676, -6177973.212649634, + -6178861.150794258, -6179072.564638216, + -6179105.171412717 + ] + }); + }) + .then(function() { + gd.layout.xaxis.breaks = [ + {pattern: '%H', bounds: [17, 8]} + ]; + // N.B. xaxis.range[0] falls within a break + gd.layout.xaxis.autorange = false; + gd.layout.xaxis.range = ['2020-01-01 20:00:00', '2020-01-04 20:00:00']; + return Plotly.react(gd, gd.data, gd.layout); + }) + .then(function() { + _assert('when range[0] falls within a break pattern (%H case)', 'x', { + breaks: [ + [1577908800000, Lib.dateTime2ms('2020-01-02 08:00:00')], + ['2020-01-02 17:00:00', '2020-01-03 08:00:00'].map(Lib.dateTime2ms), + ['2020-01-03 17:00:00', '2020-01-04 08:00:00'].map(Lib.dateTime2ms), + ['2020-01-04 17:00:00', '2020-01-04 20:00:00'].map(Lib.dateTime2ms) + ], + m2: 0.000005555555555555556, + B: [-8766160, -8766400, -8766700, -8767000, -8767060] + }); + }) + .then(function() { + gd.layout.xaxis.breaks = [ + {pattern: '%w', bounds: [1, 4]} + ]; + // N.B. xaxis.range[0] falls within a break + gd.layout.xaxis.autorange = false; + gd.layout.xaxis.range = ['2020-01-01', '2020-01-09']; + return Plotly.react(gd, gd.data, gd.layout); + }) + .then(function() { + _assert('when range[0] falls within a break pattern (%w case)', 'x', { + breaks: [ + ['2020-01-01 00:00:00', '2020-01-02 00:00:00'].map(Lib.dateTime2ms), + ['2020-01-07 00:00:00', '2020-01-09 00:00:00'].map(Lib.dateTime2ms) + ], + m2: 0.00000125, + B: [-1972296, -1972404, -1972620] + }); + }) + .catch(failTest) + .then(done); + }); + }); + + describe('during calcTicks', function() { + var gd; + + beforeEach(function() { + gd = createGraphDiv(); + }); + + afterEach(destroyGraphDiv); + + function _assert(msg, exp) { + var fullLayout = gd._fullLayout; + var xa = fullLayout.xaxis; + + expect(xa._vals.map(function(d) { return d.x; })) + .withContext(msg).toEqual(exp.tickVals); + } + + it('should not include ticks that fall within breaks', function(done) { + Plotly.plot(gd, [{ + x: [ + '1970-01-01 00:00:00.000', + '1970-01-01 00:00:00.010', + '1970-01-01 00:00:00.050', + '1970-01-01 00:00:00.090', + '1970-01-01 00:00:00.100', + '1970-01-01 00:00:00.150', + '1970-01-01 00:00:00.190', + '1970-01-01 00:00:00.200' + ] + }], { + xaxis: {}, + width: 500, + height: 400 + }) + .then(function() { + _assert('base', { + tickVals: [0, 50, 100, 150, 200] + }); + }) + .then(function() { + gd.layout.xaxis = { + breaks: [ + {bounds: [ + '1970-01-01 00:00:00.011', + '1970-01-01 00:00:00.089' + ]}, + {bounds: [ + '1970-01-01 00:00:00.101', + '1970-01-01 00:00:00.189' + ]} + ] + }; + return Plotly.react(gd, gd.data, gd.layout); + }) + .then(function() { + _assert('with two breaks', { + tickVals: [0, 10, 100, 200] + }); + }) + .catch(failTest) + .then(done); + }); + + it('should increase dtick when too many (auto) ticks fall into breaks', function(done) { + var fig = Lib.extendDeep({}, require('@mocks/axes_breaks-finance.json')); + // break over weekend + fig.layout.xaxis.breaks[0].enabled = false; + // break on a single holiday + fig.layout.xaxis.breaks[1].enabled = false; + + Plotly.plot(gd, fig) + .then(function() { + _assert('base', { + tickVals: [1483833600000, 1485043200000, 1486252800000] + }); + }) + .then(function() { + gd.layout.xaxis.breaks[0].enabled = true; + gd.layout.xaxis.breaks[1].enabled = true; + return Plotly.react(gd, gd.data, gd.layout); + }) + .then(function() { + _assert('with breaks enabled on x-axis', { + tickVals: [ + 1483574400000, 1484092800000, 1484611200000, 1484870400000, + 1485388800000, 1485907200000, 1486425600000, 1486684800000 + ] + }); + }) + .then(function() { + // a Saturday + gd.layout.xaxis.tick0 = '2017-01-02'; + // one week + gd.layout.xaxis.dtick = 7 + 24 * 60 * 60 * 1000; + return Plotly.react(gd, gd.data, gd.layout); + }) + .then(function() { + _assert('honor set tick0/dtick even though they result in few visible ticks', { + tickVals: [1483488000014] + }); + }) + .catch(failTest) + .then(done); + }); + }); + + it('should set visible:false in scattergl traces on axis with breaks', function(done) { + var gd = createGraphDiv(); + + spyOn(Lib, 'warn'); + + Plotly.plot(gd, [{ + type: 'scattergl', + x: [ + '2020-01-02 08:00', '2020-01-02 17:00', + '2020-01-03 08:00', '2020-01-03 17:00', + '2020-01-04 08:00', '2020-01-04 17:00', + '2020-01-05 08:00', '2020-01-05 17:00', + '2020-01-06 08:00', '2020-01-06 17:00', + '2020-01-07 08:00', '2020-01-07 17:00' + ] + }], { + xaxis: { + breaks: [{pattern: '%H', bounds: [17, 8]}] + } + }) + .then(function() { + expect(gd._fullData[0].visible).toBe(false, 'sets visible:false'); + expect(Lib.warn).toHaveBeenCalledTimes(1); + expect(Lib.warn).toHaveBeenCalledWith('scattergl traces do not work on axes with breaks. Setting trace 0 to `visible: false`.'); + }) + .catch(failTest) + .then(function() { + destroyGraphDiv(); + done(); + }); + }); + }); }); function getZoomInButton(gd) { diff --git a/test/jasmine/tests/cartesian_interact_test.js b/test/jasmine/tests/cartesian_interact_test.js index 29bb79810ef..91cb61395f9 100644 --- a/test/jasmine/tests/cartesian_interact_test.js +++ b/test/jasmine/tests/cartesian_interact_test.js @@ -2029,6 +2029,140 @@ describe('axis zoom/pan and main plot zoom', function() { .catch(failTest) .then(done); }); + + describe('with axis breaks', function() { + it('should compute correct range updates - x-axis case', function(done) { + function _assert(msg, xrng) { + expect(gd.layout.xaxis.range).toBeCloseToArray(xrng, 2, 'xrng - ' + msg); + } + + Plotly.plot(gd, [{ + mode: 'lines', + x: [ + '1970-01-01 00:00:00.000', + '1970-01-01 00:00:00.010', + '1970-01-01 00:00:00.050', + '1970-01-01 00:00:00.090', + '1970-01-01 00:00:00.100', + '1970-01-01 00:00:00.150', + '1970-01-01 00:00:00.190', + '1970-01-01 00:00:00.200' + ] + }], { + xaxis: { + breaks: [ + {bounds: [ + '1970-01-01 00:00:00.011', + '1970-01-01 00:00:00.089' + ]}, + {bounds: [ + '1970-01-01 00:00:00.101', + '1970-01-01 00:00:00.189' + ]} + ] + }, + dragmode: 'zoom' + }) + .then(function() { + _assert('base', [ + '1970-01-01', + '1970-01-01 00:00:00.2' + ]); + }) + .then(doDrag('xy', 'nsew', 50, 0)) + // x range would be ~ [100, 118] w/o breaks + .then(function() { + _assert('after x-only zoombox', [ + '1970-01-01 00:00:00.095', + '1970-01-01 00:00:00.0981' + ]); + }) + .then(doDblClick('xy', 'nsew')) + .then(function() { + _assert('back to base', [ + '1970-01-01', + '1970-01-01 00:00:00.2' + ]); + }) + .then(function() { return Plotly.relayout(gd, 'dragmode', 'pan'); }) + .then(doDrag('xy', 'nsew', 50, 0)) + // x range would be ~ [-18, 181] w/o breaks + .then(function() { + _assert('after x-only pan', [ + '1969-12-31 23:59:59.9969', + '1970-01-01 00:00:00.1969' + ]); + }) + .catch(failTest) + .then(done); + }); + + it('should compute correct range updates - y-axis case', function(done) { + function _assert(msg, yrng) { + expect(gd.layout.yaxis.range).toBeCloseToArray(yrng, 2, 'yrng - ' + msg); + } + + Plotly.plot(gd, [{ + mode: 'lines', + y: [ + '1970-01-01 00:00:00.000', + '1970-01-01 00:00:00.010', + '1970-01-01 00:00:00.050', + '1970-01-01 00:00:00.090', + '1970-01-01 00:00:00.100', + '1970-01-01 00:00:00.150', + '1970-01-01 00:00:00.190', + '1970-01-01 00:00:00.200' + ] + }], { + yaxis: { + breaks: [ + {bounds: [ + '1970-01-01 00:00:00.011', + '1970-01-01 00:00:00.089' + ]}, + {bounds: [ + '1970-01-01 00:00:00.101', + '1970-01-01 00:00:00.189' + ]} + ] + }, + dragmode: 'zoom' + }) + .then(function() { + _assert('base', [ + '1969-12-31 23:59:59.9981', + '1970-01-01 00:00:00.2019' + ]); + }) + .then(doDrag('xy', 'nsew', 0, 50)) + // y range would be ~ [62, 100] w/o breaks + .then(function() { + _assert('after y-only zoombox', [ + '1970-01-01 00:00:00.01', + '1970-01-01 00:00:00.095' + ]); + }) + .then(doDblClick('xy', 'nsew')) + .then(function() { + _assert('back to base', [ + '1969-12-31 23:59:59.9981', + '1970-01-01 00:00:00.2019' + ]); + }) + .then(function() { return Plotly.relayout(gd, 'dragmode', 'pan'); }) + .then(doDrag('xy', 'nsew', 0, 50)) + // y range would be ~ [35, 239] w/o breaks + .then(function() { + _assert('after y-only pan', [ + '1970-01-01 00:00:00.0051', + '1970-01-01 00:00:00.2089' + ]); + }) + .catch(failTest) + .then(done); + }); + }); }); describe('Event data:', function() { diff --git a/test/jasmine/tests/hover_label_test.js b/test/jasmine/tests/hover_label_test.js index c26cd8a2067..e9b95979540 100644 --- a/test/jasmine/tests/hover_label_test.js +++ b/test/jasmine/tests/hover_label_test.js @@ -2682,6 +2682,165 @@ describe('Hover on multicategory axes', function() { }); }); +describe('Hover on axes with breaks', function() { + var gd; + var eventData; + + beforeEach(function() { + gd = createGraphDiv(); + }); + + afterEach(destroyGraphDiv); + + function _hover(x, y) { + delete gd._hoverdata; + Lib.clearThrottle(); + mouseEvent('mousemove', x, y); + } + + function _assert(msg, exp) { + assertHoverLabelContent({ nums: exp.nums, axis: exp.axis }, msg + '| hover label'); + expect(eventData.x).toBe(exp.x, 'event data x'); + expect(eventData.y).toBe(exp.y, 'event data y'); + } + + it('should work when breaks are present on x-axis', function(done) { + Plotly.plot(gd, [{ + mode: 'lines', // i.e. no autorange padding + x: [ + '1970-01-01 00:00:00.000', + '1970-01-01 00:00:00.010', + '1970-01-01 00:00:00.050', + '1970-01-01 00:00:00.090', + '1970-01-01 00:00:00.095', + '1970-01-01 00:00:00.100', + '1970-01-01 00:00:00.150', + '1970-01-01 00:00:00.190', + '1970-01-01 00:00:00.200' + ] + }], { + xaxis: { + breaks: [ + {bounds: [ + '1970-01-01 00:00:00.011', + '1970-01-01 00:00:00.089' + ]}, + {bounds: [ + '1970-01-01 00:00:00.101', + '1970-01-01 00:00:00.189' + ]} + ] + }, + width: 400, + height: 400, + margin: {l: 10, t: 10, b: 10, r: 10}, + hovermode: 'x' + }) + .then(function() { + gd.on('plotly_hover', function(d) { + eventData = d.points[0]; + }); + }) + .then(function() { _hover(11, 11); }) + .then(function() { + _assert('leftmost interval', { + nums: '0', + axis: 'Jan 1, 1970', + x: '1970-01-01', + y: 0 + }); + }) + .then(function() { _hover(200, 200); }) + .then(function() { + _assert('middle interval', { + nums: '4', + axis: 'Jan 1, 1970, 00:00:00.095', + x: '1970-01-01 00:00:00.095', + y: 4 + }); + }) + .then(function() { _hover(388, 388); }) + .then(function() { + _assert('rightmost interval', { + nums: '8', + axis: 'Jan 1, 1970, 00:00:00.2', + x: '1970-01-01 00:00:00.2', + y: 8 + }); + }) + .catch(failTest) + .then(done); + }); + + it('should work when breaks are present on y-axis', function(done) { + Plotly.plot(gd, [{ + mode: 'lines', // i.e. no autorange padding + y: [ + '1970-01-01 00:00:00.000', + '1970-01-01 00:00:00.010', + '1970-01-01 00:00:00.050', + '1970-01-01 00:00:00.090', + '1970-01-01 00:00:00.095', + '1970-01-01 00:00:00.100', + '1970-01-01 00:00:00.150', + '1970-01-01 00:00:00.190', + '1970-01-01 00:00:00.200' + ] + }], { + yaxis: { + breaks: [ + {bounds: [ + '1970-01-01 00:00:00.011', + '1970-01-01 00:00:00.089' + ]}, + {bounds: [ + '1970-01-01 00:00:00.101', + '1970-01-01 00:00:00.189' + ]} + ] + }, + width: 400, + height: 400, + margin: {l: 10, t: 10, b: 10, r: 10}, + hovermode: 'y' + }) + .then(function() { + gd.on('plotly_hover', function(d) { + eventData = d.points[0]; + }); + }) + .then(function() { _hover(388, 30); }) + .then(function() { + _assert('topmost interval', { + nums: '8', + axis: 'Jan 1, 1970, 00:00:00.2', + x: 8, + y: '1970-01-01 00:00:00.2' + }); + }) + .then(function() { _hover(200, 200); }) + .then(function() { + _assert('middle interval', { + nums: '4', + axis: 'Jan 1, 1970, 00:00:00.095', + x: 4, + y: '1970-01-01 00:00:00.095' + }); + }) + .then(function() { _hover(11, 370); }) + .then(function() { + _assert('bottom interval', { + nums: '0', + axis: 'Jan 1, 1970', + x: 0, + y: '1970-01-01' + }); + }) + .catch(failTest) + .then(done); + }); +}); + describe('hover updates', function() { 'use strict'; diff --git a/test/jasmine/tests/range_slider_test.js b/test/jasmine/tests/range_slider_test.js index 36343fd0106..c719d0576dd 100644 --- a/test/jasmine/tests/range_slider_test.js +++ b/test/jasmine/tests/range_slider_test.js @@ -201,6 +201,61 @@ describe('Visible rangesliders', function() { .then(done); }); + it('should update correctly when moving slider on an axis with breaks', function(done) { + var start = 250; + var end = 300; + + Plotly.plot(gd, [{ + mode: 'lines', + x: [ + '1970-01-01 00:00:00.000', + '1970-01-01 00:00:00.010', + '1970-01-01 00:00:00.050', + '1970-01-01 00:00:00.090', + '1970-01-01 00:00:00.100', + '1970-01-01 00:00:00.150', + '1970-01-01 00:00:00.190', + '1970-01-01 00:00:00.200' + ] + }], { + xaxis: { + breaks: [ + {bounds: [ + '1970-01-01 00:00:00.011', + '1970-01-01 00:00:00.089' + ]}, + {bounds: [ + '1970-01-01 00:00:00.101', + '1970-01-01 00:00:00.189' + ]} + ], + rangeslider: {visible: true} + }, + width: 800, + hieght: 500 + }) + .then(function() { + var bb = getRangeSlider().getBoundingClientRect(); + sliderY = bb.top + bb.height / 2; + }) + .then(function() { + expect(gd._fullLayout.xaxis.range).withContext('base xrng').toEqual([ + '1970-01-01', + '1970-01-01 00:00:00.2' + ]); + }) + .then(function() { return slide(start, sliderY, end, sliderY); }) + .then(function() { + // x range would be ~ [15.625, 200] w/o breaks + expect(gd._fullLayout.xaxis.range).withContext('after xrng').toEqual([ + '1970-01-01 00:00:00.0027', + '1970-01-01 00:00:00.2' + ]); + }) + .catch(failTest) + .then(done); + }); + it('should resize the main plot when rangeslider has moved', function(done) { var start = 300; var end = 400;