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;