diff --git a/.gitignore b/.gitignore index 0f7e76037ad..f4c03139eec 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,8 @@ build/* npm-debug.log* *.sublime* +*~ +tags .* !.circleci diff --git a/package-lock.json b/package-lock.json index 1ac9254245b..fba2c239730 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4219,6 +4219,12 @@ } } }, + "extra-iterable": { + "version": "2.5.13", + "resolved": "https://registry.npmjs.org/extra-iterable/-/extra-iterable-2.5.13.tgz", + "integrity": "sha512-6K+KLLptYou+973HnqDjGZp15joUkEO/LCbo212V0fTl/PENmMttQGQLof5EAmeX5xLNnXAu0qkV7cH/rxZuRA==", + "dev": true + }, "extract-frustum-planes": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/extract-frustum-planes/-/extract-frustum-planes-1.0.0.tgz", diff --git a/package.json b/package.json index cc5d7801cee..8379963bf14 100644 --- a/package.json +++ b/package.json @@ -138,6 +138,7 @@ "elliptic": "^6.5.3", "eslint": "^7.10.0", "espree": "^7.3.0", + "extra-iterable": "^2.5.13", "falafel": "^2.2.4", "fs-extra": "^9.0.1", "fuse.js": "^6.4.1", diff --git a/src/components/annotations/attributes.js b/src/components/annotations/attributes.js index 1e27069b618..2dd2fe8b6a3 100644 --- a/src/components/annotations/attributes.js +++ b/src/components/annotations/attributes.js @@ -12,7 +12,34 @@ var ARROWPATHS = require('./arrow_paths'); var fontAttrs = require('../../plots/font_attributes'); var cartesianConstants = require('../../plots/cartesian/constants'); var templatedArray = require('../../plot_api/plot_template').templatedArray; +var axisPlaceableObjs = require('../../constants/axis_placeable_objects'); +function arrowAxisRefDescription(axis) { + return [ + 'In order for absolute positioning of the arrow to work, *a' + axis + + 'ref* must be exactly the same as *' + axis + 'ref*, otherwise *a' + axis + + 'ref* will revert to *pixel* (explained next).', + 'For relative positioning, *a' + axis + 'ref* can be set to *pixel*,', + 'in which case the *a' + axis + '* value is specified in pixels', + 'relative to *' + axis + '*.', + 'Absolute positioning is useful', + 'for trendline annotations which should continue to indicate', + 'the correct trend when zoomed. Relative positioning is useful', + 'for specifying the text offset for an annotated point.' + ].join(' '); +} + +function arrowCoordinateDescription(axis, lower, upper) { + return [ + 'Sets the', axis, 'component of the arrow tail about the arrow head.', + 'If `a' + axis + 'ref` is `pixel`, a positive (negative)', + 'component corresponds to an arrow pointing', + 'from', upper, 'to', lower, '(' + lower, 'to', upper + ').', + 'If `a' + axis + 'ref` is not `pixel` and is exactly the same as `' + axis + 'ref`,', + 'this is an absolute value on that axis,', + 'like `' + axis + '`, specified in the same coordinates as `' + axis + 'ref`.' + ].join(' '); +} module.exports = templatedArray('annotation', { visible: { @@ -254,12 +281,7 @@ module.exports = templatedArray('annotation', { role: 'info', editType: 'calc+arraydraw', description: [ - 'Sets the x component of the arrow tail about the arrow head.', - 'If `axref` is `pixel`, a positive (negative) ', - 'component corresponds to an arrow pointing', - 'from right to left (left to right).', - 'If `axref` is an axis, this is an absolute value on that axis,', - 'like `x`, NOT a relative value.' + arrowCoordinateDescription('x', 'left', 'right') ].join(' ') }, ay: { @@ -267,12 +289,7 @@ module.exports = templatedArray('annotation', { role: 'info', editType: 'calc+arraydraw', description: [ - 'Sets the y component of the arrow tail about the arrow head.', - 'If `ayref` is `pixel`, a positive (negative) ', - 'component corresponds to an arrow pointing', - 'from bottom to top (top to bottom).', - 'If `ayref` is an axis, this is an absolute value on that axis,', - 'like `y`, NOT a relative value.' + arrowCoordinateDescription('y', 'top', 'bottom') ].join(' ') }, axref: { @@ -285,12 +302,10 @@ module.exports = templatedArray('annotation', { role: 'info', editType: 'calc', description: [ - 'Indicates in what terms the tail of the annotation (ax,ay) ', - 'is specified. If `pixel`, `ax` is a relative offset in pixels ', - 'from `x`. If set to an x axis id (e.g. *x* or *x2*), `ax` is ', - 'specified in the same terms as that axis. This is useful ', - 'for trendline annotations which should continue to indicate ', - 'the correct trend when zoomed.' + 'Indicates in what coordinates the tail of the', + 'annotation (ax,ay) is specified.', + axisPlaceableObjs.axisRefDescription('ax', 'left', 'right'), + arrowAxisRefDescription('x') ].join(' ') }, ayref: { @@ -303,12 +318,10 @@ module.exports = templatedArray('annotation', { role: 'info', editType: 'calc', description: [ - 'Indicates in what terms the tail of the annotation (ax,ay) ', - 'is specified. If `pixel`, `ay` is a relative offset in pixels ', - 'from `y`. If set to a y axis id (e.g. *y* or *y2*), `ay` is ', - 'specified in the same terms as that axis. This is useful ', - 'for trendline annotations which should continue to indicate ', - 'the correct trend when zoomed.' + 'Indicates in what coordinates the tail of the', + 'annotation (ax,ay) is specified.', + axisPlaceableObjs.axisRefDescription('ay', 'bottom', 'top'), + arrowAxisRefDescription('y') ].join(' ') }, // positioning @@ -322,11 +335,7 @@ module.exports = templatedArray('annotation', { editType: 'calc', description: [ 'Sets the annotation\'s x coordinate axis.', - 'If set to an x axis id (e.g. *x* or *x2*), the `x` position', - 'refers to an x coordinate', - 'If set to *paper*, the `x` position refers to the distance from', - 'the left side of the plotting area in normalized coordinates', - 'where 0 (1) corresponds to the left (right) side.' + axisPlaceableObjs.axisRefDescription('x', 'left', 'right'), ].join(' ') }, x: { @@ -385,11 +394,7 @@ module.exports = templatedArray('annotation', { editType: 'calc', description: [ 'Sets the annotation\'s y coordinate axis.', - 'If set to an y axis id (e.g. *y* or *y2*), the `y` position', - 'refers to an y coordinate', - 'If set to *paper*, the `y` position refers to the distance from', - 'the bottom of the plotting area in normalized coordinates', - 'where 0 (1) corresponds to the bottom (top).' + axisPlaceableObjs.axisRefDescription('y', 'bottom', 'top'), ].join(' ') }, y: { diff --git a/src/components/annotations/calc_autorange.js b/src/components/annotations/calc_autorange.js index aa47ec253e8..2c598b7ba48 100644 --- a/src/components/annotations/calc_autorange.js +++ b/src/components/annotations/calc_autorange.js @@ -34,10 +34,12 @@ function annAutorange(gd) { Lib.filterVisible(fullLayout.annotations).forEach(function(ann) { var xa = Axes.getFromId(gd, ann.xref); var ya = Axes.getFromId(gd, ann.yref); + var xRefType = Axes.getRefType(ann.xref); + var yRefType = Axes.getRefType(ann.yref); ann._extremes = {}; - if(xa) calcAxisExpansion(ann, xa); - if(ya) calcAxisExpansion(ann, ya); + if(xRefType === 'range') calcAxisExpansion(ann, xa); + if(yRefType === 'range') calcAxisExpansion(ann, ya); }); } diff --git a/src/components/annotations/defaults.js b/src/components/annotations/defaults.js index aca202ffaa2..ed791e4a1cf 100644 --- a/src/components/annotations/defaults.js +++ b/src/components/annotations/defaults.js @@ -60,7 +60,8 @@ function handleAnnotationDefaults(annIn, annOut, fullLayout) { if(showArrow) { var arrowPosAttr = 'a' + axLetter; // axref, ayref - var aaxRef = Axes.coerceRef(annIn, annOut, gdMock, arrowPosAttr, 'pixel'); + var aaxRef = Axes.coerceRef(annIn, annOut, gdMock, arrowPosAttr, 'pixel', + ['pixel', 'paper']); // for now the arrow can only be on the same axis or specified as pixels // TODO: sometime it might be interesting to allow it to be on *any* axis diff --git a/src/components/annotations/draw.js b/src/components/annotations/draw.js index f810100a420..478a12141d0 100644 --- a/src/components/annotations/draw.js +++ b/src/components/annotations/draw.js @@ -73,6 +73,31 @@ function drawOne(gd, index) { drawRaw(gd, options, index, false, xa, ya); } +// Convert pixels to the coordinates relevant for the axis referred to. For +// example, for paper it would convert to a value normalized by the dimension of +// the plot. +// axDomainRef: if true and axa defined, draws relative to axis domain, +// otherwise draws relative to data (if axa defined) or paper (if not). +function shiftPosition(axa, dAx, axLetter, gs, options) { + var optAx = options[axLetter]; + var axRef = options[axLetter + 'ref']; + var vertical = axLetter.indexOf('y') !== -1; + var axDomainRef = Axes.getRefType(axRef) === 'domain'; + var gsDim = vertical ? gs.h : gs.w; + if(axa) { + if(axDomainRef) { + // here optAx normalized to length of axis (e.g., normally in range + // 0 to 1). But dAx is in pixels. So we normalize dAx to length of + // axis before doing the math. + return optAx + (vertical ? -dAx : dAx) / axa._length; + } else { + return axa.p2r(axa.r2p(optAx) + dAx); + } + } else { + return optAx + (vertical ? -dAx : dAx) / gsDim; + } +} + /** * drawRaw: draw a single annotation, potentially with modifications * @@ -296,13 +321,14 @@ function drawRaw(gd, options, index, subplotId, xa, ya) { var alignPosition; var autoAlignFraction; var textShift; + var axRefType = Axes.getRefType(axRef); /* * calculate the *primary* pixel position * which is the arrowhead if there is one, * otherwise the text anchor point */ - if(ax) { + if(ax && (axRefType !== 'domain')) { // check if annotation is off screen, to bypass DOM manipulations var posFraction = ax.r2fraction(options[axLetter]); if(posFraction < 0 || posFraction > 1) { @@ -318,12 +344,17 @@ function drawRaw(gd, options, index, subplotId, xa, ya) { basePx = ax._offset + ax.r2p(options[axLetter]); autoAlignFraction = 0.5; } else { + var axRefTypeEqDomain = axRefType === 'domain'; if(axLetter === 'x') { alignPosition = options[axLetter]; - basePx = gs.l + gs.w * alignPosition; + basePx = axRefTypeEqDomain ? + ax._offset + ax._length * alignPosition : + basePx = gs.l + gs.w * alignPosition; } else { alignPosition = 1 - options[axLetter]; - basePx = gs.t + gs.h * alignPosition; + basePx = axRefTypeEqDomain ? + ax._offset + ax._length * alignPosition : + basePx = gs.t + gs.h * alignPosition; } autoAlignFraction = options.showarrow ? 0.5 : alignPosition; } @@ -340,8 +371,29 @@ function drawRaw(gd, options, index, subplotId, xa, ya) { annSizeFromHeight * shiftFraction(0.5, options.yanchor); if(tailRef === axRef) { - posPx.tail = ax._offset + ax.r2p(arrowLength); - // tail is data-referenced: autorange pads the text in px from the tail + // In the case tailRefType is 'domain' or 'paper', the arrow's + // position is set absolutely, which is consistent with how + // it behaves when its position is set in data ('range') + // coordinates. + var tailRefType = Axes.getRefType(tailRef); + if(tailRefType === 'domain') { + if(axLetter === 'y') { + arrowLength = 1 - arrowLength; + } + posPx.tail = ax._offset + ax._length * arrowLength; + } else if(tailRefType === 'paper') { + if(axLetter === 'y') { + arrowLength = 1 - arrowLength; + posPx.tail = gs.t + gs.h * arrowLength; + } else { + posPx.tail = gs.l + gs.w * arrowLength; + } + } else { + // assumed tailRef is range or paper referenced + posPx.tail = ax._offset + ax.r2p(arrowLength); + } + // tail is range- or domain-referenced: autorange pads the + // text in px from the tail textPadShift = textShift; } else { posPx.tail = basePx + arrowLength; @@ -562,19 +614,20 @@ function drawRaw(gd, options, index, subplotId, xa, ya) { var ycenter = annxy0[1] + dy; annTextGroupInner.call(Drawing.setTranslate, xcenter, ycenter); - modifyItem('x', xa ? - xa.p2r(xa.r2p(options.x) + dx) : - (options.x + (dx / gs.w))); - modifyItem('y', ya ? - ya.p2r(ya.r2p(options.y) + dy) : - (options.y - (dy / gs.h))); + modifyItem('x', + shiftPosition(xa, dx, 'x', gs, options)); + modifyItem('y', + shiftPosition(ya, dy, 'y', gs, options)); + // for these 2 calls to shiftPosition, it is assumed xa, ya are + // defined, so gsDim will not be used, but we put it in + // anyways for consistency if(options.axref === options.xref) { - modifyItem('ax', xa.p2r(xa.r2p(options.ax) + dx)); + modifyItem('ax', shiftPosition(xa, dx, 'ax', gs, options)); } if(options.ayref === options.yref) { - modifyItem('ay', ya.p2r(ya.r2p(options.ay) + dy)); + modifyItem('ay', shiftPosition(ya, dy, 'ay', gs, options)); } arrowGroup.attr('transform', 'translate(' + dx + ',' + dy + ')'); @@ -609,14 +662,17 @@ function drawRaw(gd, options, index, subplotId, xa, ya) { moveFn: function(dx, dy) { var csr = 'pointer'; if(options.showarrow) { + // for these 2 calls to shiftPosition, it is assumed xa, ya are + // defined, so gsDim will not be used, but we put it in + // anyways for consistency if(options.axref === options.xref) { - modifyItem('ax', xa.p2r(xa.r2p(options.ax) + dx)); + modifyItem('ax', shiftPosition(xa, dx, 'ax', gs, options)); } else { modifyItem('ax', options.ax + dx); } if(options.ayref === options.yref) { - modifyItem('ay', ya.p2r(ya.r2p(options.ay) + dy)); + modifyItem('ay', shiftPosition(ya, dy, 'ay', gs.w, options)); } else { modifyItem('ay', options.ay + dy); } @@ -625,7 +681,9 @@ function drawRaw(gd, options, index, subplotId, xa, ya) { } else if(!subplotId) { var xUpdate, yUpdate; if(xa) { - xUpdate = xa.p2r(xa.r2p(options.x) + dx); + // shiftPosition will not execute code where xa was + // undefined, so we use to calculate xUpdate too + xUpdate = shiftPosition(xa, dx, 'x', gs, options); } else { var widthFraction = options._xsize / gs.w; var xLeft = options.x + (options._xshift - options.xshift) / gs.w - widthFraction / 2; @@ -635,7 +693,9 @@ function drawRaw(gd, options, index, subplotId, xa, ya) { } if(ya) { - yUpdate = ya.p2r(ya.r2p(options.y) + dy); + // shiftPosition will not execute code where ya was + // undefined, so we use to calculate yUpdate too + yUpdate = shiftPosition(ya, dy, 'y', gs, options); } else { var heightFraction = options._ysize / gs.h; var yBottom = options.y - (options._yshift + options.yshift) / gs.h - heightFraction / 2; diff --git a/src/components/images/attributes.js b/src/components/images/attributes.js index 1018355839e..c14afa9e7d8 100644 --- a/src/components/images/attributes.js +++ b/src/components/images/attributes.js @@ -10,6 +10,7 @@ var cartesianConstants = require('../../plots/cartesian/constants'); var templatedArray = require('../../plot_api/plot_template').templatedArray; +var axisPlaceableObjs = require('../../constants/axis_placeable_objects'); module.exports = templatedArray('image', { @@ -57,7 +58,9 @@ module.exports = templatedArray('image', { 'Sets the image container size horizontally.', 'The image will be sized based on the `position` value.', 'When `xref` is set to `paper`, units are sized relative', - 'to the plot width.' + 'to the plot width.', + 'When `xref` ends with ` domain`, units are sized relative', + 'to the axis width.', ].join(' ') }, @@ -70,7 +73,9 @@ module.exports = templatedArray('image', { 'Sets the image container size vertically.', 'The image will be sized based on the `position` value.', 'When `yref` is set to `paper`, units are sized relative', - 'to the plot height.' + 'to the plot height.', + 'When `yref` ends with ` domain`, units are sized relative', + 'to the axis height.' ].join(' ') }, @@ -150,11 +155,7 @@ module.exports = templatedArray('image', { editType: 'arraydraw', description: [ 'Sets the images\'s x coordinate axis.', - 'If set to a x axis id (e.g. *x* or *x2*), the `x` position', - 'refers to an x data coordinate', - 'If set to *paper*, the `x` position refers to the distance from', - 'the left of plot in normalized coordinates', - 'where *0* (*1*) corresponds to the left (right).' + axisPlaceableObjs.axisRefDescription('x', 'left', 'right'), ].join(' ') }, @@ -169,11 +170,7 @@ module.exports = templatedArray('image', { editType: 'arraydraw', description: [ 'Sets the images\'s y coordinate axis.', - 'If set to a y axis id (e.g. *y* or *y2*), the `y` position', - 'refers to a y data coordinate.', - 'If set to *paper*, the `y` position refers to the distance from', - 'the bottom of the plot in normalized coordinates', - 'where *0* (*1*) corresponds to the bottom (top).' + axisPlaceableObjs.axisRefDescription('y', 'bottom', 'top'), ].join(' ') }, editType: 'arraydraw' diff --git a/src/components/images/defaults.js b/src/components/images/defaults.js index 0c08874c441..7e85e6508f8 100644 --- a/src/components/images/defaults.js +++ b/src/components/images/defaults.js @@ -49,7 +49,7 @@ function imageDefaults(imageIn, imageOut, fullLayout) { for(var i = 0; i < 2; i++) { // 'paper' is the fallback axref var axLetter = axLetters[i]; - var axRef = Axes.coerceRef(imageIn, imageOut, gdMock, axLetter, 'paper'); + var axRef = Axes.coerceRef(imageIn, imageOut, gdMock, axLetter, 'paper', undefined); if(axRef !== 'paper') { var ax = Axes.getFromId(gdMock, axRef); diff --git a/src/components/images/draw.js b/src/components/images/draw.js index c1d93e3f3c5..50378e37802 100644 --- a/src/components/images/draw.js +++ b/src/components/images/draw.js @@ -11,6 +11,7 @@ var d3 = require('d3'); var Drawing = require('../drawing'); var Axes = require('../../plots/cartesian/axes'); +var axisIds = require('../../plots/cartesian/axis_ids'); var xmlnsNamespaces = require('../../constants/xmlns_namespaces'); module.exports = function draw(gd) { @@ -27,7 +28,7 @@ module.exports = function draw(gd) { if(img.visible) { if(img.layer === 'below' && img.xref !== 'paper' && img.yref !== 'paper') { - subplot = img.xref + img.yref; + subplot = axisIds.ref2id(img.xref) + axisIds.ref2id(img.yref); var plotinfo = fullLayout._plots[subplot]; @@ -130,10 +131,25 @@ module.exports = function draw(gd) { // Axes if specified var xa = Axes.getFromId(gd, d.xref); var ya = Axes.getFromId(gd, d.yref); + var xIsDomain = Axes.getRefType(d.xref) === 'domain'; + var yIsDomain = Axes.getRefType(d.yref) === 'domain'; var size = fullLayout._size; - var width = xa ? Math.abs(xa.l2p(d.sizex) - xa.l2p(0)) : d.sizex * size.w; - var height = ya ? Math.abs(ya.l2p(d.sizey) - ya.l2p(0)) : d.sizey * size.h; + var width, height; + if(xa !== undefined) { + width = ((typeof(d.xref) === 'string') && xIsDomain) ? + xa._length * d.sizex : + Math.abs(xa.l2p(d.sizex) - xa.l2p(0)); + } else { + width = d.sizex * size.w; + } + if(ya !== undefined) { + height = ((typeof(d.yref) === 'string') && yIsDomain) ? + ya._length * d.sizey : + Math.abs(ya.l2p(d.sizey) - ya.l2p(0)); + } else { + height = d.sizey * size.h; + } // Offsets for anchor positioning var xOffset = width * anchors.x[d.xanchor].offset; @@ -142,8 +158,25 @@ module.exports = function draw(gd) { var sizing = anchors.x[d.xanchor].sizing + anchors.y[d.yanchor].sizing; // Final positions - var xPos = (xa ? xa.r2p(d.x) + xa._offset : d.x * size.w + size.l) + xOffset; - var yPos = (ya ? ya.r2p(d.y) + ya._offset : size.h - d.y * size.h + size.t) + yOffset; + var xPos, yPos; + if(xa !== undefined) { + xPos = ((typeof(d.xref) === 'string') && xIsDomain) ? + xa._length * d.x + xa._offset : + xa.r2p(d.x) + xa._offset; + } else { + xPos = d.x * size.w + size.l; + } + xPos += xOffset; + if(ya !== undefined) { + yPos = ((typeof(d.yref) === 'string') && yIsDomain) ? + // consistent with "paper" yref value, where positive values + // move up the page + ya._length * (1 - d.y) + ya._offset : + ya.r2p(d.y) + ya._offset; + } else { + yPos = size.h - d.y * size.h + size.t; + } + yPos += yOffset; // Construct the proper aspectRatio attribute switch(d.sizing) { @@ -167,8 +200,8 @@ module.exports = function draw(gd) { // Set proper clipping on images - var xId = xa ? xa._id : ''; - var yId = ya ? ya._id : ''; + var xId = xa && (Axes.getRefType(d.xref) !== 'domain') ? xa._id : ''; + var yId = ya && (Axes.getRefType(d.yref) !== 'domain') ? ya._id : ''; var clipAxes = xId + yId; Drawing.setClipUrl( diff --git a/src/components/shapes/attributes.js b/src/components/shapes/attributes.js index 1b8b0ff2665..9e445831897 100644 --- a/src/components/shapes/attributes.js +++ b/src/components/shapes/attributes.js @@ -13,6 +13,7 @@ var scatterLineAttrs = require('../../traces/scatter/attributes').line; var dash = require('../drawing/attributes').dash; var extendFlat = require('../../lib/extend').extendFlat; var templatedArray = require('../../plot_api/plot_template').templatedArray; +var axisPlaceableObjs = require('../../constants/axis_placeable_objects'); module.exports = templatedArray('shape', { visible: { @@ -63,11 +64,7 @@ module.exports = templatedArray('shape', { xref: extendFlat({}, annAttrs.xref, { description: [ 'Sets the shape\'s x coordinate axis.', - 'If set to an x axis id (e.g. *x* or *x2*), the `x` position', - 'refers to an x coordinate.', - 'If set to *paper*, the `x` position refers to the distance from', - 'the left side of the plotting area in normalized coordinates', - 'where *0* (*1*) corresponds to the left (right) side.', + axisPlaceableObjs.axisRefDescription('x', 'left', 'right'), 'If the axis `type` is *log*, then you must take the', 'log of your desired range.', 'If the axis `type` is *date*, then you must convert', @@ -126,11 +123,7 @@ module.exports = templatedArray('shape', { yref: extendFlat({}, annAttrs.yref, { description: [ 'Sets the annotation\'s y coordinate axis.', - 'If set to an y axis id (e.g. *y* or *y2*), the `y` position', - 'refers to an y coordinate', - 'If set to *paper*, the `y` position refers to the distance from', - 'the bottom of the plotting area in normalized coordinates', - 'where *0* (*1*) corresponds to the bottom (top).' + axisPlaceableObjs.axisRefDescription('y', 'bottom', 'top'), ].join(' ') }), ysizemode: { diff --git a/src/components/shapes/calc_autorange.js b/src/components/shapes/calc_autorange.js index 6aae1d05fe1..b9e022b3b84 100644 --- a/src/components/shapes/calc_autorange.js +++ b/src/components/shapes/calc_autorange.js @@ -25,9 +25,12 @@ module.exports = function calcAutorange(gd) { var shape = shapeList[i]; shape._extremes = {}; - var ax, bounds; + var ax; var bounds; + var xRefType = Axes.getRefType(shape.xref); + var yRefType = Axes.getRefType(shape.yref); - if(shape.xref !== 'paper') { + // paper and axis domain referenced shapes don't affect autorange + if(shape.xref !== 'paper' && xRefType !== 'domain') { var vx0 = shape.xsizemode === 'pixel' ? shape.xanchor : shape.x0; var vx1 = shape.xsizemode === 'pixel' ? shape.xanchor : shape.x1; ax = Axes.getFromId(gd, shape.xref); @@ -38,7 +41,7 @@ module.exports = function calcAutorange(gd) { } } - if(shape.yref !== 'paper') { + if(shape.yref !== 'paper' && yRefType !== 'domain') { var vy0 = shape.ysizemode === 'pixel' ? shape.yanchor : shape.y0; var vy1 = shape.ysizemode === 'pixel' ? shape.yanchor : shape.y1; ax = Axes.getFromId(gd, shape.yref); diff --git a/src/components/shapes/defaults.js b/src/components/shapes/defaults.js index 85b18ad3463..c6ab8ea3b6c 100644 --- a/src/components/shapes/defaults.js +++ b/src/components/shapes/defaults.js @@ -63,9 +63,11 @@ function handleShapeDefaults(shapeIn, shapeOut, fullLayout) { var r2pos; // xref, yref - var axRef = Axes.coerceRef(shapeIn, shapeOut, gdMock, axLetter, '', 'paper'); + var axRef = Axes.coerceRef(shapeIn, shapeOut, gdMock, axLetter, undefined, + 'paper'); + var axRefType = Axes.getRefType(axRef); - if(axRef !== 'paper') { + if(axRefType === 'range') { ax = Axes.getFromId(gdMock, axRef); ax._shapeIndices.push(shapeOut._index); r2pos = helpers.rangeToShapePosition(ax); diff --git a/src/components/shapes/draw.js b/src/components/shapes/draw.js index b519a654a8b..14f3faef6f7 100644 --- a/src/components/shapes/draw.js +++ b/src/components/shapes/draw.js @@ -183,7 +183,10 @@ function setClipPath(shapePath, gd, shapeOptions) { // note that for layer="below" the clipAxes can be different from the // subplot we're drawing this in. This could cause problems if the shape // spans two subplots. See https://github.com/plotly/plotly.js/issues/1452 - var clipAxes = (shapeOptions.xref + shapeOptions.yref).replace(/paper/g, ''); + // + // if axis is 'paper' or an axis with " domain" appended, then there is no + // clip axis + var clipAxes = (shapeOptions.xref + shapeOptions.yref).replace(/paper/g, '').replace(/[xyz][1-9]* *domain/g, ''); Drawing.setClipUrl( shapePath, @@ -209,11 +212,13 @@ function setupDragElement(gd, shapePath, shapeOptions, index, shapeLayer, editHe // setup conversion functions var xa = Axes.getFromId(gd, shapeOptions.xref); + var xRefType = Axes.getRefType(shapeOptions.xref); var ya = Axes.getFromId(gd, shapeOptions.yref); - var x2p = helpers.getDataToPixel(gd, xa); - var y2p = helpers.getDataToPixel(gd, ya, true); - var p2x = helpers.getPixelToData(gd, xa); - var p2y = helpers.getPixelToData(gd, ya, true); + var yRefType = Axes.getRefType(shapeOptions.yref); + var x2p = helpers.getDataToPixel(gd, xa, false, xRefType); + var y2p = helpers.getDataToPixel(gd, ya, true, yRefType); + var p2x = helpers.getPixelToData(gd, xa, false, xRefType); + var p2y = helpers.getPixelToData(gd, ya, true, yRefType); var sensoryElement = obtainSensoryElement(); var dragOptions = { @@ -584,6 +589,8 @@ function setupDragElement(gd, shapePath, shapeOptions, index, shapeLayer, editHe function getPathString(gd, options) { var type = options.type; + var xRefType = Axes.getRefType(options.xref); + var yRefType = Axes.getRefType(options.yref); var xa = Axes.getFromId(gd, options.xref); var ya = Axes.getFromId(gd, options.yref); var gs = gd._fullLayout._size; @@ -591,15 +598,23 @@ function getPathString(gd, options) { var x0, x1, y0, y1; if(xa) { - x2r = helpers.shapePositionToRange(xa); - x2p = function(v) { return xa._offset + xa.r2p(x2r(v, true)); }; + if(xRefType === 'domain') { + x2p = function(v) { return xa._offset + xa._length * v; }; + } else { + x2r = helpers.shapePositionToRange(xa); + x2p = function(v) { return xa._offset + xa.r2p(x2r(v, true)); }; + } } else { x2p = function(v) { return gs.l + gs.w * v; }; } if(ya) { - y2r = helpers.shapePositionToRange(ya); - y2p = function(v) { return ya._offset + ya.r2p(y2r(v, true)); }; + if(yRefType === 'domain') { + y2p = function(v) { return ya._offset + ya._length * (1 - v); }; + } else { + y2r = helpers.shapePositionToRange(ya); + y2p = function(v) { return ya._offset + ya.r2p(y2r(v, true)); }; + } } else { y2p = function(v) { return gs.t + gs.h * (1 - v); }; } diff --git a/src/components/shapes/helpers.js b/src/components/shapes/helpers.js index fb5416a70ef..b22ea96a60d 100644 --- a/src/components/shapes/helpers.js +++ b/src/components/shapes/helpers.js @@ -58,18 +58,24 @@ exports.extractPathCoords = function(path, paramsToUse) { return extractedCoordinates; }; -exports.getDataToPixel = function(gd, axis, isVertical) { +exports.getDataToPixel = function(gd, axis, isVertical, refType) { var gs = gd._fullLayout._size; var dataToPixel; if(axis) { - var d2r = exports.shapePositionToRange(axis); - - dataToPixel = function(v) { - return axis._offset + axis.r2p(d2r(v, true)); - }; - - if(axis.type === 'date') dataToPixel = exports.decodeDate(dataToPixel); + if(refType === 'domain') { + dataToPixel = function(v) { + return axis._length * (isVertical ? (1 - v) : v) + axis._offset; + }; + } else { + var d2r = exports.shapePositionToRange(axis); + + dataToPixel = function(v) { + return axis._offset + axis.r2p(d2r(v, true)); + }; + + if(axis.type === 'date') dataToPixel = exports.decodeDate(dataToPixel); + } } else if(isVertical) { dataToPixel = function(v) { return gs.t + gs.h * (1 - v); }; } else { @@ -79,13 +85,20 @@ exports.getDataToPixel = function(gd, axis, isVertical) { return dataToPixel; }; -exports.getPixelToData = function(gd, axis, isVertical) { +exports.getPixelToData = function(gd, axis, isVertical, opt) { var gs = gd._fullLayout._size; var pixelToData; if(axis) { - var r2d = exports.rangeToShapePosition(axis); - pixelToData = function(p) { return r2d(axis.p2r(p - axis._offset)); }; + if(opt === 'domain') { + pixelToData = function(p) { + var q = (p - axis._offset) / axis._length; + return isVertical ? 1 - q : q; + }; + } else { + var r2d = exports.rangeToShapePosition(axis); + pixelToData = function(p) { return r2d(axis.p2r(p - axis._offset)); }; + } } else if(isVertical) { pixelToData = function(p) { return 1 - (p - gs.t) / gs.h; }; } else { diff --git a/src/constants/axis_placeable_objects.js b/src/constants/axis_placeable_objects.js new file mode 100644 index 00000000000..1982b035a42 --- /dev/null +++ b/src/constants/axis_placeable_objects.js @@ -0,0 +1,30 @@ +/** +* Copyright 2012-2020, Plotly, Inc. +* All rights reserved. +* +* This source code is licensed under the MIT license found in the +* LICENSE file in the root directory of this source tree. +*/ + + +'use strict'; + +module.exports = { + axisRefDescription: function(axisname, lower, upper) { + return [ + 'If set to a', axisname, 'axis id (e.g. *' + axisname + '* or', + '*' + axisname + '2*), the `' + axisname + '` position refers to a', + axisname, 'coordinate. If set to *paper*, the `' + axisname + '`', + 'position refers to the distance from the', lower, 'of the plotting', + 'area in normalized coordinates where *0* (*1*) corresponds to the', + lower, '(' + upper + '). If set to a', axisname, 'axis ID followed by', + '*domain* (separated by a space), the position behaves like for', + '*paper*, but refers to the distance in fractions of the domain', + 'length from the', lower, 'of the domain of that axis: e.g.,', + '*' + axisname + '2 domain* refers to the domain of the second', + axisname, ' axis and a', axisname, 'position of 0.5 refers to the', + 'point between the', lower, 'and the', upper, 'of the domain of the', + 'second', axisname, 'axis.', + ].join(' '); + } +}; diff --git a/src/plot_api/helpers.js b/src/plot_api/helpers.js index 230b313a8b7..dc688ce8600 100644 --- a/src/plot_api/helpers.js +++ b/src/plot_api/helpers.js @@ -174,6 +174,16 @@ exports.cleanLayout = function(layout) { cleanAxRef(shape, 'yref'); } + var imagesLen = Array.isArray(layout.images) ? layout.images.length : 0; + for(i = 0; i < imagesLen; i++) { + var image = layout.images[i]; + + if(!Lib.isPlainObject(image)) continue; + + cleanAxRef(image, 'xref'); + cleanAxRef(image, 'yref'); + } + var legend = layout.legend; if(legend) { // check for old-style legend positioning (x or y is +/- 100) @@ -218,7 +228,7 @@ function cleanAxRef(container, attr) { var valIn = container[attr]; var axLetter = attr.charAt(0); if(valIn && valIn !== 'paper') { - container[attr] = cleanId(valIn, axLetter); + container[attr] = cleanId(valIn, axLetter, true); } } diff --git a/src/plots/cartesian/axes.js b/src/plots/cartesian/axes.js index 40a1e1e5e96..1c4937e1812 100644 --- a/src/plots/cartesian/axes.js +++ b/src/plots/cartesian/axes.js @@ -91,14 +91,17 @@ axes.coerceRef = function(containerIn, containerOut, gd, attr, dflt, extraOption var refAttr = attr + 'ref'; var attrDef = {}; - if(!dflt) dflt = axlist[0] || extraOption; + if(!dflt) dflt = axlist[0] || (typeof extraOption === 'string' ? extraOption : extraOption[0]); if(!extraOption) extraOption = dflt; + axlist = axlist.concat(axlist.map(function(x) { return x + ' domain'; })); // data-ref annotations are not supported in gl2d yet attrDef[refAttr] = { valType: 'enumerated', - values: axlist.concat(extraOption ? [extraOption] : []), + values: axlist.concat(extraOption ? + (typeof extraOption === 'string' ? [extraOption] : extraOption) : + []), dflt: dflt }; @@ -106,6 +109,21 @@ axes.coerceRef = function(containerIn, containerOut, gd, attr, dflt, extraOption return Lib.coerce(containerIn, containerOut, attrDef, refAttr); }; +/* + * Get the type of an axis reference. This can be 'range', 'domain', or 'paper'. + * This assumes ar is a valid axis reference and returns 'range' if it doesn't + * match the patterns for 'paper' or 'domain'. + * + * ar: the axis reference string + * + */ +axes.getRefType = function(ar) { + if(ar === undefined) { return ar; } + if(ar === 'paper') { return 'paper'; } + if(ar === 'pixel') { return 'pixel'; } + if(/( domain)$/.test(ar)) { return 'domain'; } else { return 'range'; } +}; + /* * coerce position attributes (range-type) that can be either on axes or absolute * (paper or pixel) referenced. The biggest complication here is that we don't know @@ -130,8 +148,8 @@ axes.coerceRef = function(containerIn, containerOut, gd, attr, dflt, extraOption */ axes.coercePosition = function(containerOut, gd, coerce, axRef, attr, dflt) { var cleanPos, pos; - - if(axRef === 'paper' || axRef === 'pixel') { + var axRefType = axes.getRefType(axRef); + if(axRefType !== 'range') { cleanPos = Lib.ensureNumber; pos = coerce(attr, dflt); } else { @@ -140,7 +158,6 @@ axes.coercePosition = function(containerOut, gd, coerce, axRef, attr, dflt) { pos = coerce(attr, dflt); cleanPos = ax.cleanPos; } - containerOut[attr] = cleanPos(pos); }; diff --git a/src/plots/cartesian/axis_ids.js b/src/plots/cartesian/axis_ids.js index 5306b32452d..dff56784fe3 100644 --- a/src/plots/cartesian/axis_ids.js +++ b/src/plots/cartesian/axis_ids.js @@ -18,7 +18,7 @@ var constants = require('./constants'); // completely in favor of just 'x' if it weren't ingrained in the API etc. exports.id2name = function id2name(id) { if(typeof id !== 'string' || !id.match(constants.AX_ID_PATTERN)) return; - var axNum = id.substr(1); + var axNum = id.split(' ')[0].substr(1); if(axNum === '1') axNum = ''; return id.charAt(0) + 'axis' + axNum; }; @@ -30,13 +30,20 @@ exports.name2id = function name2id(name) { return name.charAt(0) + axNum; }; -exports.cleanId = function cleanId(id, axLetter) { +/* + * Cleans up the number of an axis, e.g., 'x002'->'x2', 'x0'->'x', 'x1' -> 'x', + * etc. + * If domainId is true, then id could be a domain reference and if it is, the + * ' domain' part is kept at the end of the axis ID string. + */ +exports.cleanId = function cleanId(id, axLetter, domainId) { + var domainTest = /( domain)$/.test(id); if(typeof id !== 'string' || !id.match(constants.AX_ID_PATTERN)) return; if(axLetter && id.charAt(0) !== axLetter) return; - - var axNum = id.substr(1).replace(/^0+/, ''); + if(domainTest && (!domainId)) return; + var axNum = id.split(' ')[0].substr(1).replace(/^0+/, ''); if(axNum === '1') axNum = ''; - return id.charAt(0) + axNum; + return id.charAt(0) + axNum + (domainTest && domainId ? ' domain' : ''); }; // get all axis objects, as restricted in listNames @@ -82,6 +89,8 @@ exports.listIds = function(gd, axLetter) { // optionally, id can be a subplot (ie 'x2y3') and type gets x or y from it exports.getFromId = function(gd, id, type) { var fullLayout = gd._fullLayout; + // remove "domain" suffix + id = ((id === undefined) || (typeof(id) !== 'string')) ? id : id.replace(' domain', ''); if(type === 'x') id = id.replace(/y[0-9]*/, ''); else if(type === 'y') id = id.replace(/x[0-9]*/, ''); @@ -123,3 +132,17 @@ exports.getAxisGroup = function getAxisGroup(fullLayout, axId) { } return axId; }; + +/* + * An axis reference (e.g., the contents at the 'xref' key of an object) might + * have extra information appended. Extract the axis ID only. + * + * ar: the axis reference string + * + */ +exports.ref2id = function(ar) { + // This assumes ar has been coerced via coerceRef, and uses the shortcut of + // checking if the first letter matches [xyz] to determine if it should + // return the axis ID. Otherwise it returns false. + return (/^[xyz]/.test(ar)) ? ar.split(' ')[0] : false; +}; diff --git a/src/plots/cartesian/constants.js b/src/plots/cartesian/constants.js index 98ef0bc9f18..c6a523fe2df 100644 --- a/src/plots/cartesian/constants.js +++ b/src/plots/cartesian/constants.js @@ -12,8 +12,8 @@ var counterRegex = require('../../lib/regex').counter; module.exports = { idRegex: { - x: counterRegex('x'), - y: counterRegex('y') + x: counterRegex('x', '( domain)?'), + y: counterRegex('y', '( domain)?') }, attrRegex: counterRegex('[xy]axis'), @@ -25,7 +25,7 @@ module.exports = { // pattern matching axis ids and names // note that this is more permissive than counterRegex, as // id2name, name2id, and cleanId accept "x1" etc - AX_ID_PATTERN: /^[xyz][0-9]*$/, + AX_ID_PATTERN: /^[xyz][0-9]*( domain)?$/, AX_NAME_PATTERN: /^[xyz]axis[0-9]*$/, // and for 2D subplots diff --git a/src/plots/cartesian/include_components.js b/src/plots/cartesian/include_components.js index cd817b55464..e8738b42c1d 100644 --- a/src/plots/cartesian/include_components.js +++ b/src/plots/cartesian/include_components.js @@ -11,6 +11,7 @@ var Registry = require('../../registry'); var Lib = require('../../lib'); +var axisIds = require('./axis_ids'); /** * Factory function for checking component arrays for subplot references. @@ -40,8 +41,10 @@ module.exports = function makeIncludeComponents(containerArrayName) { var itemi = array[i]; if(!Lib.isPlainObject(itemi)) continue; - var xref = itemi.xref; - var yref = itemi.yref; + // call cleanId because if xref, or yref has something appended + // (e.g., ' domain') this will get removed. + var xref = axisIds.cleanId(itemi.xref, 'x', false); + var yref = axisIds.cleanId(itemi.yref, 'y', false); var hasXref = idRegex.x.test(xref); var hasYref = idRegex.y.test(yref); diff --git a/test/image/baselines/domain_ref_axis_types.png b/test/image/baselines/domain_ref_axis_types.png new file mode 100644 index 00000000000..88a884fe3ea Binary files /dev/null and b/test/image/baselines/domain_ref_axis_types.png differ diff --git a/test/image/baselines/domain_refs.png b/test/image/baselines/domain_refs.png new file mode 100644 index 00000000000..7347652f223 Binary files /dev/null and b/test/image/baselines/domain_refs.png differ diff --git a/test/image/mocks/domain_ref_axis_types.json b/test/image/mocks/domain_ref_axis_types.json new file mode 100644 index 00000000000..7f32664abcc --- /dev/null +++ b/test/image/mocks/domain_ref_axis_types.json @@ -0,0 +1,1234 @@ +{ + "data": [ + { + "type": "bar", + "x": [ + 1, + 2, + 3, + 4 + ], + "xaxis": "x", + "y": [ + 3, + 1, + 6, + 10 + ], + "yaxis": "y" + }, + { + "type": "bar", + "x": [ + 1, + 2, + 3, + 4 + ], + "xaxis": "x2", + "y": [ + 3, + 1, + 6, + 10 + ], + "yaxis": "y2" + }, + { + "type": "bar", + "x": [ + 0, + 86400000, + 172800000, + 259200000 + ], + "xaxis": "x3", + "y": [ + 3, + 1, + 6, + 10 + ], + "yaxis": "y3" + }, + { + "type": "bar", + "x": [ + "dog", + "cat", + "bear", + "alex" + ], + "xaxis": "x4", + "y": [ + 3, + 1, + 6, + 10 + ], + "yaxis": "y4" + }, + { + "type": "bar", + "x": [ + [ + "A", + "B", + "B", + "C" + ], + [ + "table", + "chair", + "piano", + "mojtaba" + ] + ], + "xaxis": "x5", + "y": [ + 3, + 1, + 6, + 10 + ], + "yaxis": "y5" + }, + { + "orientation": "h", + "type": "bar", + "x": [ + 3, + 1, + 6, + 10 + ], + "xaxis": "x6", + "y": [ + 1, + 2, + 3, + 4 + ], + "yaxis": "y6" + }, + { + "orientation": "h", + "type": "bar", + "x": [ + 3, + 1, + 6, + 10 + ], + "xaxis": "x7", + "y": [ + 1, + 2, + 3, + 4 + ], + "yaxis": "y7" + }, + { + "orientation": "h", + "type": "bar", + "x": [ + 3, + 1, + 6, + 10 + ], + "xaxis": "x8", + "y": [ + 0, + 86400000, + 172800000, + 259200000 + ], + "yaxis": "y8" + }, + { + "orientation": "h", + "type": "bar", + "x": [ + 3, + 1, + 6, + 10 + ], + "xaxis": "x9", + "y": [ + "dog", + "cat", + "bear", + "alex" + ], + "yaxis": "y9" + }, + { + "orientation": "h", + "type": "bar", + "x": [ + 3, + 1, + 6, + 10 + ], + "xaxis": "x10", + "y": [ + [ + "A", + "B", + "B", + "C" + ], + [ + "table", + "chair", + "piano", + "mojtaba" + ] + ], + "yaxis": "y10" + } + ], + "layout": { + "annotations": [ + { + "font": { + "size": 16 + }, + "showarrow": false, + "text": "linear", + "x": 0.08399999999999999, + "xanchor": "center", + "xref": "paper", + "y": 1.0, + "yanchor": "bottom", + "yref": "paper" + }, + { + "font": { + "size": 16 + }, + "showarrow": false, + "text": "log", + "x": 0.292, + "xanchor": "center", + "xref": "paper", + "y": 1.0, + "yanchor": "bottom", + "yref": "paper" + }, + { + "font": { + "size": 16 + }, + "showarrow": false, + "text": "date", + "x": 0.5, + "xanchor": "center", + "xref": "paper", + "y": 1.0, + "yanchor": "bottom", + "yref": "paper" + }, + { + "font": { + "size": 16 + }, + "showarrow": false, + "text": "category", + "x": 0.708, + "xanchor": "center", + "xref": "paper", + "y": 1.0, + "yanchor": "bottom", + "yref": "paper" + }, + { + "font": { + "size": 16 + }, + "showarrow": false, + "text": "multicategory", + "x": 0.9159999999999999, + "xanchor": "center", + "xref": "paper", + "y": 1.0, + "yanchor": "bottom", + "yref": "paper" + } + ], + "shapes": [ + { + "type": "line", + "x0": 3, + "x1": 3, + "xref": "x", + "y0": 0, + "y1": 1, + "yref": "y domain" + }, + { + "type": "line", + "x0": 4, + "x1": 4, + "xref": "x", + "y0": 0, + "y1": 1, + "yref": "y domain" + }, + { + "type": "line", + "x0": 0, + "x1": 1, + "xref": "x domain", + "y0": 2, + "y1": 2, + "yref": "y" + }, + { + "type": "rect", + "x0": 1, + "x1": 2, + "xref": "x", + "y0": 0, + "y1": 1, + "yref": "y domain" + }, + { + "type": "rect", + "x0": 0, + "x1": 1, + "xref": "x domain", + "y0": 3, + "y1": 5, + "yref": "y" + }, + { + "type": "line", + "x0": 3, + "x1": 3, + "xref": "x2", + "y0": 0, + "y1": 1, + "yref": "y2 domain" + }, + { + "type": "line", + "x0": 4, + "x1": 4, + "xref": "x2", + "y0": 0, + "y1": 1, + "yref": "y2 domain" + }, + { + "type": "line", + "x0": 0, + "x1": 1, + "xref": "x2 domain", + "y0": 2, + "y1": 2, + "yref": "y2" + }, + { + "type": "rect", + "x0": 1, + "x1": 2, + "xref": "x2", + "y0": 0, + "y1": 1, + "yref": "y2 domain" + }, + { + "type": "rect", + "x0": 0, + "x1": 1, + "xref": "x2 domain", + "y0": 3, + "y1": 5, + "yref": "y2" + }, + { + "type": "line", + "x0": 172800000, + "x1": 172800000, + "xref": "x3", + "y0": 0, + "y1": 1, + "yref": "y3 domain" + }, + { + "type": "line", + "x0": 259200000, + "x1": 259200000, + "xref": "x3", + "y0": 0, + "y1": 1, + "yref": "y3 domain" + }, + { + "type": "line", + "x0": 0, + "x1": 1, + "xref": "x3 domain", + "y0": 2, + "y1": 2, + "yref": "y3" + }, + { + "type": "rect", + "x0": 0, + "x1": 86400000, + "xref": "x3", + "y0": 0, + "y1": 1, + "yref": "y3 domain" + }, + { + "type": "rect", + "x0": 0, + "x1": 1, + "xref": "x3 domain", + "y0": 3, + "y1": 5, + "yref": "y3" + }, + { + "type": "line", + "x0": "bear", + "x1": "bear", + "xref": "x4", + "y0": 0, + "y1": 1, + "yref": "y4 domain" + }, + { + "type": "line", + "x0": "alex", + "x1": "alex", + "xref": "x4", + "y0": 0, + "y1": 1, + "yref": "y4 domain" + }, + { + "type": "line", + "x0": 0, + "x1": 1, + "xref": "x4 domain", + "y0": 2, + "y1": 2, + "yref": "y4" + }, + { + "type": "rect", + "x0": "dog", + "x1": "cat", + "xref": "x4", + "y0": 0, + "y1": 1, + "yref": "y4 domain" + }, + { + "type": "rect", + "x0": 0, + "x1": 1, + "xref": "x4 domain", + "y0": 3, + "y1": 5, + "yref": "y4" + }, + { + "type": "line", + "x0": [ + "B", + "piano" + ], + "x1": [ + "B", + "piano" + ], + "xref": "x5", + "y0": 0, + "y1": 1, + "yref": "y5 domain" + }, + { + "type": "line", + "x0": [ + "C", + "mojtaba" + ], + "x1": [ + "C", + "mojtaba" + ], + "xref": "x5", + "y0": 0, + "y1": 1, + "yref": "y5 domain" + }, + { + "type": "line", + "x0": 0, + "x1": 1, + "xref": "x5 domain", + "y0": 2, + "y1": 2, + "yref": "y5" + }, + { + "type": "rect", + "x0": [ + "A", + "table" + ], + "x1": [ + "B", + "chair" + ], + "xref": "x5", + "y0": 0, + "y1": 1, + "yref": "y5 domain" + }, + { + "type": "rect", + "x0": 0, + "x1": 1, + "xref": "x5 domain", + "y0": 3, + "y1": 5, + "yref": "y5" + }, + { + "type": "line", + "x0": 0, + "x1": 1, + "xref": "x6 domain", + "y0": 3, + "y1": 3, + "yref": "y6" + }, + { + "type": "line", + "x0": 0, + "x1": 1, + "xref": "x6 domain", + "y0": 4, + "y1": 4, + "yref": "y6" + }, + { + "type": "line", + "x0": 2, + "x1": 2, + "xref": "x6", + "y0": 0, + "y1": 1, + "yref": "y6 domain" + }, + { + "type": "rect", + "x0": 0, + "x1": 1, + "xref": "x6 domain", + "y0": 1, + "y1": 2, + "yref": "y6" + }, + { + "type": "rect", + "x0": 3, + "x1": 5, + "xref": "x6", + "y0": 0, + "y1": 1, + "yref": "y6 domain" + }, + { + "type": "line", + "x0": 0, + "x1": 1, + "xref": "x7 domain", + "y0": 3, + "y1": 3, + "yref": "y7" + }, + { + "type": "line", + "x0": 0, + "x1": 1, + "xref": "x7 domain", + "y0": 4, + "y1": 4, + "yref": "y7" + }, + { + "type": "line", + "x0": 2, + "x1": 2, + "xref": "x7", + "y0": 0, + "y1": 1, + "yref": "y7 domain" + }, + { + "type": "rect", + "x0": 0, + "x1": 1, + "xref": "x7 domain", + "y0": 1, + "y1": 2, + "yref": "y7" + }, + { + "type": "rect", + "x0": 3, + "x1": 5, + "xref": "x7", + "y0": 0, + "y1": 1, + "yref": "y7 domain" + }, + { + "type": "line", + "x0": 0, + "x1": 1, + "xref": "x8 domain", + "y0": 172800000, + "y1": 172800000, + "yref": "y8" + }, + { + "type": "line", + "x0": 0, + "x1": 1, + "xref": "x8 domain", + "y0": 259200000, + "y1": 259200000, + "yref": "y8" + }, + { + "type": "line", + "x0": 2, + "x1": 2, + "xref": "x8", + "y0": 0, + "y1": 1, + "yref": "y8 domain" + }, + { + "type": "rect", + "x0": 0, + "x1": 1, + "xref": "x8 domain", + "y0": 0, + "y1": 86400000, + "yref": "y8" + }, + { + "type": "rect", + "x0": 3, + "x1": 5, + "xref": "x8", + "y0": 0, + "y1": 1, + "yref": "y8 domain" + }, + { + "type": "line", + "x0": 0, + "x1": 1, + "xref": "x9 domain", + "y0": "bear", + "y1": "bear", + "yref": "y9" + }, + { + "type": "line", + "x0": 0, + "x1": 1, + "xref": "x9 domain", + "y0": "alex", + "y1": "alex", + "yref": "y9" + }, + { + "type": "line", + "x0": 2, + "x1": 2, + "xref": "x9", + "y0": 0, + "y1": 1, + "yref": "y9 domain" + }, + { + "type": "rect", + "x0": 0, + "x1": 1, + "xref": "x9 domain", + "y0": "dog", + "y1": "cat", + "yref": "y9" + }, + { + "type": "rect", + "x0": 3, + "x1": 5, + "xref": "x9", + "y0": 0, + "y1": 1, + "yref": "y9 domain" + }, + { + "type": "line", + "x0": 0, + "x1": 1, + "xref": "x10 domain", + "y0": [ + "B", + "piano" + ], + "y1": [ + "B", + "piano" + ], + "yref": "y10" + }, + { + "type": "line", + "x0": 0, + "x1": 1, + "xref": "x10 domain", + "y0": [ + "C", + "mojtaba" + ], + "y1": [ + "C", + "mojtaba" + ], + "yref": "y10" + }, + { + "type": "line", + "x0": 2, + "x1": 2, + "xref": "x10", + "y0": 0, + "y1": 1, + "yref": "y10 domain" + }, + { + "type": "rect", + "x0": 0, + "x1": 1, + "xref": "x10 domain", + "y0": [ + "A", + "table" + ], + "y1": [ + "B", + "chair" + ], + "yref": "y10" + }, + { + "type": "rect", + "x0": 3, + "x1": 5, + "xref": "x10", + "y0": 0, + "y1": 1, + "yref": "y10 domain" + } + ], + "template": { + "data": { + "bar": [ + { + "error_x": { + "color": "#2a3f5f" + }, + "error_y": { + "color": "#2a3f5f" + }, + "marker": { + "line": { + "color": "#E5ECF6", + "width": 0.5 + } + }, + "type": "bar" + } + ] + }, + "layout": { + "coloraxis": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "colorscale": { + "diverging": [ + [ + 0, + "#8e0152" + ], + [ + 0.1, + "#c51b7d" + ], + [ + 0.2, + "#de77ae" + ], + [ + 0.3, + "#f1b6da" + ], + [ + 0.4, + "#fde0ef" + ], + [ + 0.5, + "#f7f7f7" + ], + [ + 0.6, + "#e6f5d0" + ], + [ + 0.7, + "#b8e186" + ], + [ + 0.8, + "#7fbc41" + ], + [ + 0.9, + "#4d9221" + ], + [ + 1, + "#276419" + ] + ], + "sequential": [ + [ + 0.0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1.0, + "#f0f921" + ] + ], + "sequentialminus": [ + [ + 0.0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1.0, + "#f0f921" + ] + ] + }, + "colorway": [ + "#636efa", + "#EF553B", + "#00cc96", + "#ab63fa", + "#FFA15A", + "#19d3f3", + "#FF6692", + "#B6E880", + "#FF97FF", + "#FECB52" + ], + "font": { + "color": "#2a3f5f" + }, + "hoverlabel": { + "align": "left" + }, + "hovermode": "closest", + "paper_bgcolor": "white", + "plot_bgcolor": "#E5ECF6", + "polar": { + "angularaxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + }, + "bgcolor": "#E5ECF6", + "radialaxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + } + }, + "scene": { + "xaxis": { + "backgroundcolor": "#E5ECF6", + "gridcolor": "white", + "gridwidth": 2, + "linecolor": "white", + "showbackground": true, + "ticks": "", + "zerolinecolor": "white" + }, + "yaxis": { + "backgroundcolor": "#E5ECF6", + "gridcolor": "white", + "gridwidth": 2, + "linecolor": "white", + "showbackground": true, + "ticks": "", + "zerolinecolor": "white" + }, + "zaxis": { + "backgroundcolor": "#E5ECF6", + "gridcolor": "white", + "gridwidth": 2, + "linecolor": "white", + "showbackground": true, + "ticks": "", + "zerolinecolor": "white" + } + }, + "shapedefaults": { + "line": { + "color": "#2a3f5f" + } + }, + "ternary": { + "aaxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + }, + "baxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + }, + "bgcolor": "#E5ECF6", + "caxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + } + }, + "title": { + "x": 0.05 + }, + "xaxis": { + "automargin": true, + "gridcolor": "white", + "linecolor": "white", + "ticks": "", + "title": { + "standoff": 15 + }, + "zerolinecolor": "white", + "zerolinewidth": 2 + }, + "yaxis": { + "automargin": true, + "gridcolor": "white", + "linecolor": "white", + "ticks": "", + "title": { + "standoff": 15 + }, + "zerolinecolor": "white", + "zerolinewidth": 2 + } + } + }, + "xaxis": { + "anchor": "y", + "domain": [ + 0.0, + 0.16799999999999998 + ], + "tickfont": { + "size": 8 + }, + "type": "linear" + }, + "xaxis10": { + "anchor": "y10", + "domain": [ + 0.832, + 1.0 + ], + "tickfont": { + "size": 8 + } + }, + "xaxis2": { + "anchor": "y2", + "domain": [ + 0.208, + 0.376 + ], + "tickfont": { + "size": 8 + }, + "type": "log" + }, + "xaxis3": { + "anchor": "y3", + "domain": [ + 0.416, + 0.584 + ], + "tickfont": { + "size": 8 + }, + "type": "date" + }, + "xaxis4": { + "anchor": "y4", + "domain": [ + 0.624, + 0.792 + ], + "tickfont": { + "size": 8 + }, + "type": "category" + }, + "xaxis5": { + "anchor": "y5", + "domain": [ + 0.832, + 1.0 + ], + "tickfont": { + "size": 8 + }, + "type": "multicategory" + }, + "xaxis6": { + "anchor": "y6", + "domain": [ + 0.0, + 0.16799999999999998 + ], + "tickfont": { + "size": 8 + } + }, + "xaxis7": { + "anchor": "y7", + "domain": [ + 0.208, + 0.376 + ], + "tickfont": { + "size": 8 + } + }, + "xaxis8": { + "anchor": "y8", + "domain": [ + 0.416, + 0.584 + ], + "tickfont": { + "size": 8 + } + }, + "xaxis9": { + "anchor": "y9", + "domain": [ + 0.624, + 0.7248 + ], + "tickfont": { + "size": 8 + } + }, + "yaxis": { + "anchor": "x", + "domain": [ + 0.625, + 1.0 + ], + "tickfont": { + "size": 8 + } + }, + "yaxis10": { + "anchor": "x10", + "domain": [ + 0.0, + 0.375 + ], + "tickfont": { + "size": 8 + }, + "type": "multicategory" + }, + "yaxis2": { + "anchor": "x2", + "domain": [ + 0.625, + 1.0 + ], + "tickfont": { + "size": 8 + } + }, + "yaxis3": { + "anchor": "x3", + "domain": [ + 0.625, + 1.0 + ], + "tickfont": { + "size": 8 + } + }, + "yaxis4": { + "anchor": "x4", + "domain": [ + 0.625, + 1.0 + ], + "tickfont": { + "size": 8 + } + }, + "yaxis5": { + "anchor": "x5", + "domain": [ + 0.625, + 1.0 + ], + "tickfont": { + "size": 8 + } + }, + "yaxis6": { + "anchor": "x6", + "domain": [ + 0.0, + 0.375 + ], + "tickfont": { + "size": 8 + }, + "type": "linear" + }, + "yaxis7": { + "anchor": "x7", + "domain": [ + 0.0, + 0.375 + ], + "tickfont": { + "size": 8 + }, + "type": "log" + }, + "yaxis8": { + "anchor": "x8", + "domain": [ + 0.0, + 0.375 + ], + "tickfont": { + "size": 8 + }, + "type": "date" + }, + "yaxis9": { + "anchor": "x9", + "domain": [ + 0.0, + 0.375 + ], + "tickfont": { + "size": 8 + }, + "type": "category" + } + } +} diff --git a/test/image/mocks/domain_refs.json b/test/image/mocks/domain_refs.json new file mode 100644 index 00000000000..af56005cc3a --- /dev/null +++ b/test/image/mocks/domain_refs.json @@ -0,0 +1,221 @@ +{ + "data": [{ + "type": "scatter", + "x": [], + "xaxis": "x", + "y": [], + "yaxis": "y" + }, + { + "type": "scatter", + "x": [], + "xaxis": "x2", + "y": [], + "yaxis": "y2" + }, + { + "type": "scatter", + "x": [], + "xaxis": "x3", + "y": [], + "yaxis": "y3" + }, + { + "type": "scatter", + "x": [], + "xaxis": "x4", + "y": [], + "yaxis": "y4" + } + ], + "layout": { + "xaxis": { + "anchor": "y", + "domain": [ + 0.0, + 0.45 + ], + "range": [ + 1, + 10 + ] + }, + "xaxis2": { + "anchor": "y2", + "type": "log", + "domain": [ + 0.55, + 1.0 + ], + "range": [ + 0, + 1 + ] + }, + "xaxis3": { + "anchor": "y3", + "domain": [ + 0.0, + 0.45 + ], + "range": [ + 1, + 10 + ] + }, + "xaxis4": { + "anchor": "y4", + "type": "log", + "domain": [ + 0.55, + 1.0 + ], + "range": [ + 0, + 1 + ] + }, + "yaxis": { + "anchor": "x", + "type": "log", + "domain": [ + 0.575, + 1 + ], + "range": [ + 0, + 1 + ] + }, + "yaxis2": { + "anchor": "x2", + "domain": [ + 0.575, + 1 + ], + "range": [ + 1, + 10 + ] + }, + "yaxis3": { + "anchor": "x3", + "type": "log", + "domain": [ + 0.0, + 0.425 + ], + "range": [ + 0, + 1 + ] + }, + "yaxis4": { + "anchor": "x4", + "domain": [ + 0.0, + 0.425 + ], + "range": [ + 1, + 10 + ] + }, + "shapes": [{ + "type": "line", + "xref": "x3", + "yref": "y3 domain", + "x0": 6, + "y0": 0, + "x1": 6, + "y1": 1, + "line": { + "color": "rgb(10, 20, 30)" + } + }, { + "type": "line", + "xref": "x4 domain", + "yref": "y4", + "x0": 0, + "y0": 8, + "x1": 1, + "y1": 8, + "line": { + "color": "rgb(10, 20, 31)" + } + },{ + "type": "rect", + "xref": "x domain", + "yref": "y", + "x0": 0, + "y0": 4, + "x1": 1, + "y1": 9, + "line": { + "color": "rgb(10, 20, 32)" + } + }, { + "type": "rect", + "xref": "x2", + "yref": "y2 domain", + "x0": 1, + "y0": 0, + "x1": 3, + "y1": 1, + "line": { + "color": "rgb(10, 20, 33)" + } + }], + "annotations": [{ + "text": "A", + "bordercolor": "rgb(100, 200, 232)", + "xref": "x3 domain", + "yref": "y3 domain", + "x": 0, + "y": 0, + "axref": "x3 domain", + "ayref": "y3 domain", + "ax": 0.5, + "ay": 0.5, + "arrowcolor": "rgb(231, 200, 100)" + }, { + "text": "B", + "bordercolor": "rgb(200, 200, 232)", + "xref": "x4 domain", + "yref": "y4 domain", + "x": 1, + "y": 1, + "axref": "x4 domain", + "ayref": "y4 domain", + "ax": 0.5, + "ay": 0.5, + "arrowcolor": "rgb(231, 200, 200)" + }], + "images": [{ + "source": "https://images.plot.ly/language-icons/api-home/js-logo.png", + "xref": "x domain", + "yref": "y domain", + "x": 0, + "y": 0, + "xanchor": "left", + "yanchor": "bottom", + "sizex": 0.5, + "sizey": 0.5, + "sizing": "stretch" + }, { + "source": "https://images.plot.ly/language-icons/api-home/python-logo.png", + "xref": "x2 domain", + "yref": "y2 domain", + "x": 1, + "y": 1, + "xanchor": "right", + "yanchor": "top", + "sizex": 0.5, + "sizey": 0.5, + "sizing": "stretch" + }] + }, + "config": { + "editable": true + } +} diff --git a/test/jasmine/assets/domain_ref/components.js b/test/jasmine/assets/domain_ref/components.js new file mode 100644 index 00000000000..bb481cf7842 --- /dev/null +++ b/test/jasmine/assets/domain_ref/components.js @@ -0,0 +1,855 @@ +// Test the placement of Axis Referencing Objects (AROs) +// Tools that can be wrapped in Jasmine tests. +// +// TODO: To make it work with Jasmine, we need to return a list of promises, +// one for each combination in the combo. When we're debugging / exploring, we +// want to be able to call the promise from the browser. When in a jasmine +// test, we need a description of the test and the promise doing the test +// itself. In this case, it needs to tell jasmine if it passed or failed, so we +// pass in an assert function that the promise can call. Then in jasmine, the +// promise is followed by .catch(failTest).then(done) +'use strict'; + +var Plotly = require('../../../../lib/index'); +var d3 = require('d3'); +var pixelCalc = require('../../assets/pixel_calc'); +var getSVGElemScreenBBox = require('../../assets/get_svg_elem_screen_bbox'); +// var SVGTools = require('../../assets/svg_tools'); +var Lib = require('../../../../src/lib'); +var Axes = require('../../../../src/plots/cartesian/axes'); +var axisIds = require('../../../../src/plots/cartesian/axis_ids'); +var testImage = 'https://images.plot.ly/language-icons/api-home/js-logo.png'; +var iterable = require('extra-iterable'); + +var testMock = require('./domain_ref_base.json'); + +// NOTE: this tolerance is in pixels +var EQUALITY_TOLERANCE = 1e-2; + +// Make an array from a finite iterable (for environments not having +// Array.from) +function iterToArray(iter) { + var a = []; + var v; + // when done is true v.value is undefined + for(v = iter.next(); !v.done; v = iter.next()) { + a.push(v.value); + } + return a; +} + +// some made-up values for testing +// NOTE: The pixel values are intentionally set so that 2*pixel is never greater +// than the mock's margin. This is so that annotations are not unintentionally +// clipped out because they exceed the plotting area. The reason for using twice +// the pixel value is because the annotation test requires plotting 2 +// annotations, the second having arrow components twice as long as the first. +var aroPositionsX = [{ + // aros referring to data + ref: 'range', + value: [2, 3], + // for objects that need a size (i.e., images) + size: 1.5, + // for the case when annotations specifies arrow in pixels, this value + // is read instead of value[1] + pixel: 25 +}, +{ + // aros referring to domains + ref: 'domain', + value: [0.2, 0.75], + size: 0.3, + pixel: 30 +}, +{ + // aros referring to paper + ref: 'paper', + value: [0.25, 0.8], + size: 0.35, + pixel: 35 +}, +]; +var aroPositionsY = [{ + // aros referring to data + ref: 'range', + // two values for rects + value: [1, 2], + pixel: 30, + size: 1.2 +}, +{ + // aros referring to domains + ref: 'domain', + value: [0.25, 0.7], + pixel: 40, + size: 0.2 +}, +{ + // aros referring to paper + ref: 'paper', + value: [0.2, 0.85], + pixel: 45, + size: 0.3 +} +]; + +var axisTypes = ['linear', 'log']; +// Test on 'x', 'y', 'x2', 'y2' axes +// TODO the 'paper' position references are tested twice when once would +// suffice. +var axisPairs = [ + ['x', 'y'], + ['x2', 'y2'] +]; +// For annotations: if arrow coordinate is in the same coordinate system 's', if +// pixel then 'p' +var arrowAxis = [ + ['s', 's'], + ['p', 's'], + ['s', 'p'], + ['p', 'p'] +]; +// only test the shapes line and rect for now +var shapeTypes = ['line', 'rect']; +// anchor positions for images +var xAnchors = ['left', 'center', 'right']; +var yAnchors = ['top', 'middle', 'bottom']; +// this color chosen so it can easily be found with d3 +// NOTE: for images color cannot be set but it will be the only image in the +// plot so you can use d3.select('g image').node() +var aroColor = 'rgb(50, 100, 150)'; + +// acts on an Object representing a aro which could be a line or a rect +// DEPRECATED +function aroFromAROPos(aro, axletter, axnum, aropos) { + aro[axletter + '0'] = aropos.value[0]; + aro[axletter + '1'] = aropos.value[1]; + if(aropos.ref === 'range') { + aro[axletter + 'ref'] = axletter + axnum; + } else if(aropos.ref === 'domain') { + aro[axletter + 'ref'] = axletter + axnum + ' domain'; + } else if(aropos.ref === 'paper') { + aro[axletter + 'ref'] = 'paper'; + } +} + +// {axid} is the axis id, e.g., x2, y, etc. +// {ref} is ['range'|'domain'|'paper'] +function makeAxRef(axid, ref) { + var axref; + switch(ref) { + case 'range': + axref = axid; + break; + case 'domain': + axref = axid + ' domain'; + break; + case 'paper': + axref = 'paper'; + break; + default: + throw 'Bad axis type (ref): ' + ref; + } + return axref; +} + +// shape, annotation and image all take x0, y0, xref, yref, color parameters +// x0, y0 are numerical values, xref, yref are strings that could be passed to +// the xref field of an ANO (e.g., 'x2 domain' or 'paper'), color should be +// specified using the 'rgb(r, g, b)' syntax +// arotype can be 'shape', 'annotation', or 'image' +// shapes take type=[line|rect], x1, y1 +// annotations take ax, ay, axref, ayref, (text is just set to "A" and xanchor +// and yanchor are always set to left because these are text attributes which we +// don't test) +// images take xsize, ysize, xanchor, yanchor (sizing is set to stretch for simplicity +// in computing the bounding box and source is something predetermined) +function aroFromParams(arotype, x0, y0, xref, yref, color, opts) { + // fill with common values + var aro = { + xref: xref, + yref: yref + }; + switch(arotype) { + case 'shape': + aro.x0 = x0; + aro.y0 = y0; + aro.x1 = opts.x1; + aro.y1 = opts.y1; + aro.type = opts.type; + aro.line = { + color: color + }; + break; + case 'annotation': + aro.x = x0; + aro.y = y0; + aro.text = 'A'; + aro.ax = opts.ax; + aro.ay = opts.ay; + aro.axref = opts.axref; + aro.ayref = opts.ayref; + aro.showarrow = true; + aro.arrowhead = 0; + aro.arrowcolor = color; + break; + case 'image': + aro.x = x0; + aro.y = y0; + aro.sizex = opts.sizex; + aro.sizey = opts.sizey; + aro.xanchor = opts.xanchor; + aro.yanchor = opts.yanchor; + aro.sizing = 'stretch'; + aro.source = testImage; + break; + default: + throw 'Bad arotype: ' + arotype; + } + return aro; +} + +// Calculate the ax value of an annotation given a particular desired scaling K +// This also works with log axes by taking logs of each part of the sum, so that +// the length in pixels is multiplied by the scalar +function annaxscale(ac, c0) { + var ret; + ret = c0 + 2 * (ac - c0); + return ret; +} + +// This tests to see that an annotation was drawn correctly. +// Determinining the length of the arrow seems complicated due to the +// rectangle containing the text, so we draw 2 annotations, one K times the +// length of the other, and solve for the desired arrow length from the +// length measured on the screen. This works because multiplying the length +// of the arrow doesn't change where the arrow meets the text box. +// xaxistype can be linear|log, only used if xref has type 'range' or 'domain', +// same for yaxistype and yref +function annotationTest(gd, layout, opt) { + var x0 = opt.x0; + var y0 = opt.y0; + var ax = opt.ax; + var ay = opt.ay; + var xref = opt.xref; + var yref = opt.yref; + var axref = opt.axref; + var ayref = opt.ayref; + var xaxistype = opt.xaxistype; + var yaxistype = opt.yaxistype; + var xid = opt.xid; + var yid = opt.yid; + + // Take the log of values corresponding to log axes. This is because the + // test is designed to make predicting the pixel positions easy, and it's + // easiest when we work with the logarithm of values on log axes (doubling + // the log value doubles the pixel value, etc.). + var xreftype = Axes.getRefType(xref); + var yreftype = Axes.getRefType(yref); + var axreftype = Axes.getRefType(axref); + var ayreftype = Axes.getRefType(ayref); + x0 = xreftype === 'range' && xaxistype === 'log' ? Math.log10(x0) : x0; + ax = axreftype === 'range' && xaxistype === 'log' ? Math.log10(ax) : ax; + y0 = yreftype === 'range' && yaxistype === 'log' ? Math.log10(y0) : y0; + ay = ayreftype === 'range' && yaxistype === 'log' ? Math.log10(ay) : ay; + // if xref != axref or axref === 'pixel' then ax is a value relative to + // x0 but in pixels. Same for yref + var axpixels = false; + if((axreftype === 'pixel') || (axreftype !== xreftype)) { + axpixels = true; + } + var aypixels = false; + if((ayreftype === 'pixel') || (ayreftype !== yreftype)) { + aypixels = true; + } + logAxisIfAxType(gd.layout, layout, xid, xaxistype); + logAxisIfAxType(gd.layout, layout, yid, yaxistype); + var xpixels; + var ypixels; + var opts0 = { + ax: ax, + ay: ay, + axref: axref, + ayref: ayref, + }; + var opts1 = { + ax: axpixels ? 2 * ax : annaxscale(ax, x0), + ay: aypixels ? 2 * ay : annaxscale(ay, y0), + axref: axref, + ayref: ayref, + }; + // 2 colors so we can extract each annotation individually + var color0 = 'rgb(10, 20, 30)'; + var color1 = 'rgb(10, 20, 31)'; + var anno0 = aroFromParams('annotation', x0, y0, xref, yref, color0, opts0); + var anno1 = aroFromParams('annotation', x0, y0, xref, yref, color1, opts1); + layout.annotations = [anno0, anno1]; + return Plotly.relayout(gd, layout) + .then(function(gd) { + // the choice of anno1 or anno0 is arbitrary + var xabspixels = mapAROCoordToPixel(gd.layout, 'xref', anno1, 'x', 0, true); + var yabspixels = mapAROCoordToPixel(gd.layout, 'yref', anno1, 'y', 0, true); + if(axpixels) { + // no need to map the specified values to pixels (because that's what + // they are already) + xpixels = ax; + } else { + xpixels = mapAROCoordToPixel(gd.layout, 'xref', anno0, 'ax', 0, true) - + xabspixels; + } + if(aypixels) { + // no need to map the specified values to pixels (because that's what + // they are already) + ypixels = ay; + } else { + ypixels = mapAROCoordToPixel(gd.layout, 'yref', anno0, 'ay', 0, true) - + yabspixels; + } + var annobbox0 = getSVGElemScreenBBox(findAROByColor(color0)); + var annobbox1 = getSVGElemScreenBBox(findAROByColor(color1)); + // solve for the arrow length's x coordinate + var arrowLenX = ((annobbox1.x + annobbox1.width) - (annobbox0.x + annobbox0 + .width)); + var arrowLenY; + var yabspixelscmp; + if(aypixels) { + // for annotations whose arrows are specified in relative pixels, + // positive pixel values on the y axis mean moving down the page like + // SVG coordinates, so we have to add height + arrowLenY = (annobbox1.y + annobbox1.height) - + (annobbox0.y + annobbox0.height); + yabspixelscmp = annobbox0.y; + } else { + arrowLenY = annobbox1.y - annobbox0.y; + yabspixelscmp = annobbox0.y + annobbox0.height; + } + var ret = coordsEq(arrowLenX, xpixels) && + coordsEq(arrowLenY, ypixels) && + coordsEq(xabspixels, annobbox0.x) && + coordsEq(yabspixels, yabspixelscmp); + return ret; + }); +} + +// axid is e.g., 'x', 'y2' etc. +// if nologrange is true, log of range is not taken +function logAxisIfAxType(layoutIn, layoutOut, axid, axtype, nologrange) { + var axname = axisIds.id2name(axid); + if((axtype === 'log') && (axid !== undefined)) { + var axis = Lib.extendDeep({}, layoutIn[axname]); + axis.type = 'log'; + axis.range = nologrange ? axis.range : axis.range.map(Math.log10); + layoutOut[axname] = axis; + } +} + +// {layout} is required to map to pixels using its domain, range and size +// {axref} can be xref or yref +// {aro} is the components object where c and axref will be looked up +// {c} can be x0, x1, y0, y1 +// {offset} allows adding something to the coordinate before converting, say if +// you want to map the point on the other side of a square +// {nolog} if set to true, the log of a range value will not be taken before +// computing its pixel position. This is useful for components whose positions +// are specified in log coordinates (i.e., images and annotations). +// You can tell I first wrote this function for shapes only and then learned +// later this was the case for images and annotations :'). +function mapAROCoordToPixel(layout, axref, aro, c, offset, nolog) { + var reftype = Axes.getRefType(aro[axref]); + var ret; + offset = (offset === undefined) ? 0 : offset; + var val = aro[c] + offset; + var axis; + if(reftype === 'range') { + axis = axisIds.id2name(aro[axref]); + ret = pixelCalc.mapRangeToPixel(layout, axis, val, nolog); + } else if(reftype === 'domain') { + axis = axisIds.id2name(aro[axref]); + ret = pixelCalc.mapDomainToPixel(layout, axis, val); + } else if(reftype === 'paper') { + axis = axref[0]; + ret = pixelCalc.mapPaperToPixel(layout, axis, val); + } + return ret; +} + +// compute the bounding box of the shape so that it can be compared with the SVG +// bounding box +function shapeToBBox(layout, aro) { + var bbox = {}; + var x1; + var y1; + // map x coordinates + bbox.x = mapAROCoordToPixel(layout, 'xref', aro, 'x0'); + x1 = mapAROCoordToPixel(layout, 'xref', aro, 'x1'); + // SVG bounding boxes have x,y referring to top left corner, but here we are + // specifying aros where y0 refers to the bottom left corner like + // Plotly.js, so we swap y0 and y1 + bbox.y = mapAROCoordToPixel(layout, 'yref', aro, 'y1'); + y1 = mapAROCoordToPixel(layout, 'yref', aro, 'y0'); + bbox.width = x1 - bbox.x; + bbox.height = y1 - bbox.y; + return bbox; +} + +function imageToBBox(layout, img) { + var bbox = {}; + // these will be pixels from the bottom of the plot and will be manipulated + // below to be compatible with the SVG bounding box + var x0; + var x1; + var y0; + var y1; + switch(img.xanchor) { + case 'left': + x0 = mapAROCoordToPixel(layout, 'xref', img, 'x', undefined, true); + x1 = mapAROCoordToPixel(layout, 'xref', img, 'x', img.sizex, true); + break; + case 'right': + x0 = mapAROCoordToPixel(layout, 'xref', img, 'x', -img.sizex, true); + x1 = mapAROCoordToPixel(layout, 'xref', img, 'x', undefined, true); + break; + case 'center': + x0 = mapAROCoordToPixel(layout, 'xref', img, 'x', -img.sizex * 0.5, true); + x1 = mapAROCoordToPixel(layout, 'xref', img, 'x', img.sizex * 0.5, true); + break; + default: + throw 'Bad xanchor: ' + img.xanchor; + } + switch(img.yanchor) { + case 'bottom': + y0 = mapAROCoordToPixel(layout, 'yref', img, 'y', undefined, true); + y1 = mapAROCoordToPixel(layout, 'yref', img, 'y', img.sizey, true); + break; + case 'top': + y0 = mapAROCoordToPixel(layout, 'yref', img, 'y', -img.sizey, true); + y1 = mapAROCoordToPixel(layout, 'yref', img, 'y', undefined, true); + break; + case 'middle': + y0 = mapAROCoordToPixel(layout, 'yref', img, 'y', -img.sizey * 0.5, true); + y1 = mapAROCoordToPixel(layout, 'yref', img, 'y', img.sizey * 0.5, true); + break; + default: + throw 'Bad yanchor: ' + img.yanchor; + } + bbox.x = x0; + bbox.width = x1 - x0; + // done this way because the pixel value of y1 will be smaller than the + // pixel value x0 if y1 > y0 (because of how SVG draws relative to the top + // of the screen) + bbox.y = y1; + bbox.height = y0 - y1; + return bbox; +} + +function coordsEq(a, b) { + if(a && b) { + return Math.abs(a - b) < EQUALITY_TOLERANCE; + } + return false; +} + +function compareBBoxes(a, b) { + return ['x', 'y', 'width', 'height'].map( + function(k) { return coordsEq(a[k], b[k]); }).reduce( + function(l, r) { return l && r; }, + true); +} + +function findAROByColor(color, id, type, colorAttribute) { + id = (id === undefined) ? '' : id + ' '; + type = (type === undefined) ? 'path' : type; + colorAttribute = (colorAttribute === undefined) ? 'stroke' : colorAttribute; + var selector = id + type; + var ret = d3.selectAll(selector).filter(function() { + return this.style[colorAttribute] === color; + }).node(); + return ret; +} + +function findImage(id) { + id = (id === undefined) ? '' : id + ' '; + var selector = id + 'g image'; + var ret = d3.select(selector).node(); + return ret; +} + +function imageTest(gd, layout, opt) { + var xaxtype = opt.xaxtype; + var yaxtype = opt.yaxtype; + var x = opt.x; + var y = opt.y; + var sizex = opt.sizex; + var sizey = opt.sizey; + var xanchor = opt.xanchor; + var yanchor = opt.yanchor; + var xref = opt.xref; + var yref = opt.yref; + var xid = opt.xid; + var yid = opt.yid; + + var image = { + x: x, + y: y, + sizex: sizex, + sizey: sizey, + source: testImage, + xanchor: xanchor, + yanchor: yanchor, + xref: xref, + yref: yref, + sizing: 'stretch' + }; + var ret; + // we pass xid, yid because we possibly want to change some axes to log, + // even if we refer to paper in the end + logAxisIfAxType(gd.layout, layout, xid, xaxtype, true); + logAxisIfAxType(gd.layout, layout, yid, yaxtype, true); + layout.images = [image]; + return Plotly.relayout(gd, layout) + .then(function(gd) { + var imageElem = findImage('#' + gd.id); + var svgImageBBox = getSVGElemScreenBBox(imageElem); + var imageBBox = imageToBBox(gd.layout, image); + ret = compareBBoxes(svgImageBBox, imageBBox); + return ret; + }); +} + +// gets the SVG bounding box of the aro and checks it against what mapToPixel +// gives +function checkAROPosition(gd, aro) { + var aroPath = findAROByColor(aro.line.color, '#' + gd.id); + var aroPathBBox = getSVGElemScreenBBox(aroPath); + var aroBBox = shapeToBBox(gd.layout, aro); + var ret = compareBBoxes(aroBBox, aroPathBBox); + // console.log('aroBBox: ' + JSON.stringify(aroBBox)); + // console.log('aroPathBBox: ' + JSON.stringify(SVGTools.svgRectToObj(aroPathBBox))); + return ret; +} + +function shapeTest( + gd, + opt) { + var xAxNum = opt.xAxNum; + var xaxisType = opt.xaxisType; + var xaroPos = opt.xaroPos; + var yAxNum = opt.yAxNum; + var yaxisType = opt.yaxisType; + var yaroPos = opt.yaroPos; + var aroType = opt.aroType; + + // console.log('gd.layout: ', JSON.stringify(gd.layout)); + var aro = { + type: aroType, + line: { + color: aroColor + } + }; + aroFromAROPos(aro, 'x', xAxNum, xaroPos); + aroFromAROPos(aro, 'y', yAxNum, yaroPos); + var layout = { + shapes: [aro] + }; + // change to log axes if need be + logAxisIfAxType(gd.layout, layout, 'x' + xAxNum, xaxisType); + logAxisIfAxType(gd.layout, layout, 'y' + yAxNum, yaxisType); + // console.log('layout: ', JSON.stringify(layout)); + return Plotly.relayout(gd, layout) + .then(function(gd) { + return checkAROPosition(gd, aro); + }); +} + +function describeShapeComboTest(combo) { + var xaxisType = combo[0]; + var yaxisType = combo[1]; + var axispair = combo[2]; + var xaroPos = combo[3]; + var yaroPos = combo[4]; + var shapeType = combo[5]; + var gdId = combo[6]; + var xid = axispair[0]; + var yid = axispair[1]; + return [ + '#', gdId, + 'should create a plot with parameters:', '\n', + 'x-axis type:', xaxisType, '\n', + 'y-axis type:', yaxisType, '\n', + 'axis pair:', xid, yid, '\n', + 'ARO position x:', JSON.stringify(xaroPos), '\n', + 'ARO position y:', JSON.stringify(yaroPos), '\n', + 'shape type:', shapeType, '\n', + ].join(' '); +} + +function testShapeCombo(combo, assert, gd) { + var xaxisType = combo[0]; + var yaxisType = combo[1]; + var axispair = combo[2]; + var xaroPos = combo[3]; + var yaroPos = combo[4]; + var shapeType = combo[5]; + var xAxNum = axispair[0].substr(1); + var yAxNum = axispair[1].substr(1); + return Plotly.newPlot(gd, Lib.extendDeep({}, testMock)) + .then(function(gd) { + return shapeTest(gd, + {xAxNum: xAxNum, + xaxisType: xaxisType, + xaroPos: xaroPos, + yAxNum: yAxNum, + yaxisType: yaxisType, + yaroPos: yaroPos, + aroType: shapeType, + }); + }).then(function(testRet) { + assert(testRet); + }); +} + +function describeImageComboTest(combo) { + var axistypex = combo[0]; + var axistypey = combo[1]; + var axispair = combo[2]; + var aroposx = combo[3]; + var aroposy = combo[4]; + var xanchor = combo[5]; + var yanchor = combo[6]; + var gdId = combo[7]; + var xid = axispair[0]; + var yid = axispair[1]; + var xref = makeAxRef(xid, aroposx.ref); + var yref = makeAxRef(yid, aroposy.ref); + // TODO Add image combo test description + return [ + '#', gdId, + 'should create a plot with parameters:', '\n', + 'x-axis type:', axistypex, '\n', + 'y-axis type:', axistypey, '\n', + 'axis pair:', xid, yid, '\n', + 'ARO position x:', JSON.stringify(aroposx), '\n', + 'ARO position y:', JSON.stringify(aroposy), '\n', + 'xanchor:', xanchor, '\n', + 'yanchor:', yanchor, '\n', + 'xref:', xref, '\n', + 'yref:', yref, '\n', + ].join(' '); +} + +function testImageCombo(combo, assert, gd) { + var axistypex = combo[0]; + var axistypey = combo[1]; + var axispair = combo[2]; + var aroposx = combo[3]; + var aroposy = combo[4]; + var xanchor = combo[5]; + var yanchor = combo[6]; + var xid = axispair[0]; + var yid = axispair[1]; + var xref = makeAxRef(xid, aroposx.ref); + var yref = makeAxRef(yid, aroposy.ref); + return Plotly.newPlot(gd, Lib.extendDeep({}, testMock)) + .then(function(gd) { + return imageTest(gd, {}, + { + xaxtype: axistypex, + yaxtype: axistypey, + x: aroposx.value[0], + y: aroposy.value[0], + sizex: aroposx.size, + sizey: aroposy.size, + xanchor: xanchor, + yanchor: yanchor, + xref: xref, + yref: yref, + xid: xid, + yid: yid, + }); + }).then(function(testRet) { + assert(testRet); + }); +} + +function describeAnnotationComboTest(combo) { + var axistypex = combo[0]; + var axistypey = combo[1]; + var axispair = combo[2]; + var aroposx = combo[3]; + var aroposy = combo[4]; + var arrowaxispair = combo[5]; + var gdId = combo[6]; + var xid = axispair[0]; + var yid = axispair[1]; + var xref = makeAxRef(xid, aroposx.ref); + var yref = makeAxRef(yid, aroposy.ref); + return [ + '#', gdId, + 'should create a plot with parameters:', '\n', + 'x-axis type:', axistypex, '\n', + 'y-axis type:', axistypey, '\n', + 'axis pair:', xid, yid, '\n', + 'ARO position x:', JSON.stringify(aroposx), '\n', + 'ARO position y:', JSON.stringify(aroposy), '\n', + 'arrow axis pair:', arrowaxispair, '\n', + 'xref:', xref, '\n', + 'yref:', yref, '\n', + ].join(' '); +} + +function testAnnotationCombo(combo, assert, gd) { + var axistypex = combo[0]; + var axistypey = combo[1]; + var axispair = combo[2]; + var aroposx = combo[3]; + var aroposy = combo[4]; + var arrowaxispair = combo[5]; + var xid = axispair[0]; + var yid = axispair[1]; + var xref = makeAxRef(xid, aroposx.ref); + var yref = makeAxRef(yid, aroposy.ref); + var axref = arrowaxispair[0] === 'p' ? 'pixel' : xref; + var ayref = arrowaxispair[1] === 'p' ? 'pixel' : yref; + var x0 = aroposx.value[0]; + var y0 = aroposy.value[0]; + var ax = axref === 'pixel' ? aroposx.pixel : aroposx.value[1]; + var ay = ayref === 'pixel' ? aroposy.pixel : aroposy.value[1]; + return Plotly.newPlot(gd, Lib.extendDeep({}, testMock)) + .then(function(gd) { + return annotationTest(gd, {}, + { + x0: x0, + y0: y0, + ax: ax, + ay: ay, + xref: xref, + yref: yref, + axref: axref, + ayref: ayref, + axistypex: axistypex, + axistypey: axistypey, + xid: xid, + yid: yid, + }); + }).then(function(testRet) { + assert(testRet); + }); +} + +// return a list of functions, each returning a promise that executes a +// particular test. This function takes the keepGraphDiv argument, which if true +// will prevent destroying the generated graph after the test is executed, and +// an assert argument, which is a function that will be passed true if the test +// passed. +// {testCombos} is a list of combinations each of which will be passed to the +// test function +// {test} is the function returning a Promise that executes this test +function comboTests(testCombos, test) { + var ret = testCombos.map(function(combo) { + return function(assert, gd) { + return test(combo, assert, gd); + }; + }); + return ret; +} + +// return a list of strings, each describing a corresponding test +// describe is a function taking a combination and returning a description of +// the test +function comboTestDescriptions(testCombos, desribe) { + var ret = testCombos.map(desribe); + return ret; +} + +function annotationTestCombos() { + var testCombos = iterToArray(iterable.cartesianProduct([ + axisTypes, axisTypes, axisPairs, aroPositionsX, aroPositionsY, arrowAxis + ])); + testCombos = testCombos.map( + function(c, i) { + return c.concat(['graph-' + i]); + } + ); + return testCombos; +} + +function annotationTests() { + var testCombos = annotationTestCombos(); + return comboTests(testCombos, testAnnotationCombo); +} + +function annotationTestDescriptions() { + var testCombos = annotationTestCombos(); + return comboTestDescriptions(testCombos, describeAnnotationComboTest); +} + +function imageTestCombos() { + var testCombos = iterToArray(iterable.cartesianProduct( + [ + axisTypes, axisTypes, axisPairs, + // axis reference types are contained in here + aroPositionsX, aroPositionsY, + xAnchors, yAnchors + ] + )); + testCombos = testCombos.map( + function(c, i) { + return c.concat(['graph-' + i]); + } + ); + return testCombos; +} + +function imageTests() { + var testCombos = imageTestCombos(); + return comboTests(testCombos, testImageCombo); +} + +function imageTestDescriptions() { + var testCombos = imageTestCombos(); + return comboTestDescriptions(testCombos, describeImageComboTest); +} + +function shapeTestCombos() { + var testCombos = iterToArray(iterable.cartesianProduct( + [ + axisTypes, axisTypes, axisPairs, + // axis reference types are contained in here + aroPositionsX, aroPositionsY, shapeTypes + ] + )); + testCombos = testCombos.map( + function(c, i) { + return c.concat(['graph-' + i]); + } + ); + return testCombos; +} + +function shapeTests() { + var testCombos = shapeTestCombos(); + return comboTests(testCombos, testShapeCombo); +} + +function shapeTestDescriptions() { + var testCombos = shapeTestCombos(); + return comboTestDescriptions(testCombos, describeShapeComboTest); +} + +module.exports = { + // tests + annotations: { + descriptions: annotationTestDescriptions, + tests: annotationTests, + }, + images: { + descriptions: imageTestDescriptions, + tests: imageTests, + }, + shapes: { + descriptions: shapeTestDescriptions, + tests: shapeTests + }, + // utilities + findAROByColor: findAROByColor +}; diff --git a/test/jasmine/assets/domain_ref/domain_ref_base.json b/test/jasmine/assets/domain_ref/domain_ref_base.json new file mode 100644 index 00000000000..8e759334d43 --- /dev/null +++ b/test/jasmine/assets/domain_ref/domain_ref_base.json @@ -0,0 +1,41 @@ +{ + "data": [{ + "x": [1, 2], + "y": [3, 1], + "type": "scatter", + "xaxis": "x", + "yaxis": "y", + "line": { "color": "rgb(127,127,127)" } + },{ + "x": [1, 3], + "y": [1, 2], + "type": "scatter", + "xaxis": "x2", + "yaxis": "y2", + "line": { "color": "rgb(127,127,127)" } + } + ], + "layout": { + "xaxis": { + "domain": [0,0.75], + "range": [1, 4] + }, + "yaxis": { + "domain": [0,0.4], + "range": [1, 4] + }, + "xaxis2": { + "domain": [0.75,1], + "range": [1, 5], + "anchor": "y2" + }, + "yaxis2": { + "domain": [0.4,1], + "range": [1, 3], + "anchor": "x2" + }, + "margin": { "l": 100, "r": 100, "t": 100, "b": 100, "autoexpand": false }, + "width": 400, + "height": 400 + } +} diff --git a/test/jasmine/assets/domain_ref/domain_refs_editable.json b/test/jasmine/assets/domain_ref/domain_refs_editable.json new file mode 100644 index 00000000000..f5cb8f1b56e --- /dev/null +++ b/test/jasmine/assets/domain_ref/domain_refs_editable.json @@ -0,0 +1,159 @@ +{ + "data": [ + { + "type": "scatter", + "x": [], + "xaxis": "x", + "y": [], + "yaxis": "y" + }, + { + "type": "scatter", + "x": [], + "xaxis": "x2", + "y": [], + "yaxis": "y2" + }, + { + "type": "scatter", + "x": [], + "xaxis": "x3", + "y": [], + "yaxis": "y3" + }, + { + "type": "scatter", + "x": [], + "xaxis": "x4", + "y": [], + "yaxis": "y4" + } + ], + "layout": { + "xaxis": { + "anchor": "y", + "domain": [ + 0.0, + 0.45 + ] + }, + "xaxis2": { + "anchor": "y2", + "domain": [ + 0.55, + 1.0 + ] + }, + "xaxis3": { + "anchor": "y3", + "domain": [ + 0.0, + 0.45 + ], + "range": [ + 1, + 10 + ] + }, + "xaxis4": { + "type": "log", + "anchor": "y4", + "domain": [ + 0.55, + 1.0 + ], + "range": [ + 0, + 1 + ] + }, + "yaxis": { + "anchor": "x", + "domain": [ + 0.575, + 1.0 + ] + }, + "yaxis2": { + "anchor": "x2", + "domain": [ + 0.575, + 1.0 + ] + }, + "yaxis3": { + "type": "log", + "anchor": "x3", + "domain": [ + 0.0, + 0.425 + ], + "range": [ + 0, + 1 + ] + }, + "yaxis4": { + "anchor": "x4", + "domain": [ + 0.0, + 0.425 + ], + "range": [ + 1, + 10 + ] + }, + "shapes": [{ + "type": "rect", + "xref": "x3 domain", + "yref": "y3 domain", + "x0": 0.1, + "y0": 0.2, + "x1": 0.3, + "y1": 0.4, + "line": { "color": "rgb(10, 20, 30)" } + },{ + "type": "rect", + "xref": "x4 domain", + "yref": "y4 domain", + "x0": 0.4, + "y0": 0.5, + "x1": 0.6, + "y1": 0.7, + "line": { "color": "rgb(10, 20, 31)" } + }], + "annotations": [{ + "text": "A", + "bordercolor": "rgb(100, 200, 232)", + "xref": "x3 domain", + "yref": "y3 domain", + "x": 0.4, + "y": 0.6, + "axref": "x3 domain", + "ayref": "y3 domain", + "ax": 0.7, + "ay": 0.8, + "arrowcolor": "rgb(231, 200, 100)", + "arrowwidth": 3 + },{ + "text": "B", + "bordercolor": "rgb(200, 200, 232)", + "xref": "x4 domain", + "yref": "y4 domain", + "x": 0.3, + "y": 0.4, + "axref": "x4 domain", + "ayref": "y4 domain", + "ax": 0.1, + "ay": 0.2, + "arrowcolor": "rgb(231, 200, 200)", + "arrowwidth": 3 + }], + "dragmode": "drawopenpath" + }, + "config": { + "editable": true, + "modeBarButtonsToAdd": ["drawopenpath"] + } +} diff --git a/test/jasmine/assets/get_rect_center.js b/test/jasmine/assets/get_rect_center.js index 1117383354b..fec124acfe7 100644 --- a/test/jasmine/assets/get_rect_center.js +++ b/test/jasmine/assets/get_rect_center.js @@ -1,5 +1,7 @@ 'use strict'; +var SVGTools = require('./svg_tools'); + /** * Get the screen coordinates of the center of @@ -18,7 +20,7 @@ module.exports = function getRectCenter(rect) { // Taken from: http://stackoverflow.com/a/5835212/4068492 function getRectScreenCoords(rect) { - var svg = findParentSVG(rect); + var svg = SVGTools.findParentSVG(rect); var pt = svg.createSVGPoint(); var corners = {}; var matrix = rect.getScreenCTM(); @@ -35,13 +37,3 @@ function getRectScreenCoords(rect) { return corners; } - -function findParentSVG(node) { - var parentNode = node.parentNode; - - if(parentNode.tagName === 'svg') { - return parentNode; - } else { - return findParentSVG(parentNode); - } -} diff --git a/test/jasmine/assets/get_svg_elem_screen_bbox.js b/test/jasmine/assets/get_svg_elem_screen_bbox.js new file mode 100644 index 00000000000..a6d95d0001e --- /dev/null +++ b/test/jasmine/assets/get_svg_elem_screen_bbox.js @@ -0,0 +1,29 @@ +'use strict'; + +var SVGTools = require('./svg_tools'); + +/** + * Get the bounding box in screen coordinates of an SVG element. + * + * @param {elem} SVG element's node. + */ +module.exports = getSVGElemScreenBBox; + +// Get the screen coordinates of an SVG Element's bounding box +// Based off of this: +// https://stackoverflow.com/questions/26049488/how-to-get-absolute-coordinates-of-object-inside-a-g-group +function getSVGElemScreenBBox(elem) { + var svg = SVGTools.findParentSVG(elem); + var rect = svg.createSVGRect(); + var pt = svg.createSVGPoint(); + var ctm = elem.getScreenCTM(); + var bbox = elem.getBBox(); + pt.x = bbox.x; + pt.y = bbox.y; + rect.width = bbox.width; + rect.height = bbox.height; + pt = pt.matrixTransform(ctm); + rect.x = pt.x; + rect.y = pt.y; + return rect; +} diff --git a/test/jasmine/assets/pixel_calc.js b/test/jasmine/assets/pixel_calc.js new file mode 100644 index 00000000000..d214c56826b --- /dev/null +++ b/test/jasmine/assets/pixel_calc.js @@ -0,0 +1,69 @@ +'use strict'; + +// Calculate the pixel values from various objects + +// {layout} is where the margins are obtained from +// {axis} can be x or y and is used to extract the values for making the +// calculation. If axis is y then d is converted to 1 - d to be consistent with +// how the y axis in Plotly.js works. +// {domain} depends on the application: if we're converting from the paper domain +// to pixels then domain can just be [0,1]. If we're converting from an axis +// domain to pixels, then domain[0] is the start of its domain and domain[1] the +// end (as obtained from layout.xaxis.domain for example). +// {d} Is normalized to domain length, so 0 is the beginning of the domain and 1 +// the end, but it can have values beyond this (e.g., -2 is twice the domain +// length in the opposite direction). For the case where you want to convert +// from range to pixels, convert the range to a normalized using the range for +// that axis (e.g., layout.xaxis.range) +function mapToPixelHelper(layout, axis, domain, d) { + var dim; + var lower; + var upper; + if(axis === 'x') { + dim = 'width'; + lower = 'l'; + upper = 'r'; + } else if(axis === 'y') { + dim = 'height'; + lower = 'b'; + upper = 't'; + } else { + throw 'Bad axis letter: ' + axis; + } + var plotwidth = layout[dim] - layout.margin[lower] - layout.margin[upper]; + var domwidth = (domain[1] - domain[0]) * plotwidth; + if(dim === 'height') { + // y-axes relative to bottom of plot in plotly.js + return layout[dim] - (layout.margin[lower] + domain[0] * plotwidth + domwidth * d); + } + return layout.margin[lower] + domain[0] * plotwidth + domwidth * d; +} + +// axis must be single letter, e.g., x or y +function mapPaperToPixel(layout, axis, d) { + return mapToPixelHelper(layout, axis, [0, 1], d); +} + +// Here axis must have the same form as in layout, e.g., xaxis, yaxis2, etc. +function mapDomainToPixel(layout, axis, d) { + return mapToPixelHelper(layout, axis[0], layout[axis].domain, d); +} + +// Here axis must have the same form as in layout, e.g., xaxis, yaxis2, etc. +// nolog is provided to avoid taking the log of the value even if the axis is a +// log axis. This is used in the case of layout images, whose corner coordinates +// and dimensions are specified in powers of 10, e.g., if the corner's x +// coordinate is at data 10, then the x value passed is 1 +function mapRangeToPixel(layout, axis, r, nolog) { + if((!nolog) && (layout[axis].type === 'log')) { + r = Math.log10(r); + } + var d = (r - layout[axis].range[0]) / (layout[axis].range[1] - layout[axis].range[0]); + return mapDomainToPixel(layout, axis, d); +} + +module.exports = { + mapPaperToPixel: mapPaperToPixel, + mapDomainToPixel: mapDomainToPixel, + mapRangeToPixel: mapRangeToPixel +}; diff --git a/test/jasmine/assets/svg_tools.js b/test/jasmine/assets/svg_tools.js new file mode 100644 index 00000000000..72ba4e44f86 --- /dev/null +++ b/test/jasmine/assets/svg_tools.js @@ -0,0 +1,25 @@ +'use strict'; + +module.exports = { + findParentSVG: findParentSVG, + svgRectToObj: svgRectToObj +}; + +function findParentSVG(node) { + var parentNode = node.parentNode; + + if(parentNode.tagName === 'svg') { + return parentNode; + } else { + return findParentSVG(parentNode); + } +} + +function svgRectToObj(svgrect) { + var obj = {}; + obj.x = svgrect.x; + obj.y = svgrect.y; + obj.width = svgrect.width; + obj.height = svgrect.height; + return obj; +} diff --git a/test/jasmine/bundle_tests/plotschema_test.js b/test/jasmine/bundle_tests/plotschema_test.js index 36149adb965..7011aa61a8e 100644 --- a/test/jasmine/bundle_tests/plotschema_test.js +++ b/test/jasmine/bundle_tests/plotschema_test.js @@ -14,6 +14,7 @@ var gl3dAttrs = require('@src/plots/gl3d').layoutAttributes; var polarLayoutAttrs = require('@src/plots/polar/legacy/axis_attributes'); var annotationAttrs = require('@src/components/annotations').layoutAttributes; var updatemenuAttrs = require('@src/components/updatemenus').layoutAttributes; +var cartesianIdRegex = require('@src/plots/cartesian/constants').idRegex; describe('plot schema', function() { 'use strict'; @@ -363,9 +364,9 @@ describe('plot schema', function() { var splomAttrs = plotSchema.traces.splom.attributes; expect(typeof splomAttrs.xaxes.items.regex).toBe('string'); - expect(splomAttrs.xaxes.items.regex).toBe('/^x([2-9]|[1-9][0-9]+)?$/'); + expect(splomAttrs.xaxes.items.regex).toBe(cartesianIdRegex.x.toString()); expect(typeof splomAttrs.yaxes.items.regex).toBe('string'); - expect(splomAttrs.yaxes.items.regex).toBe('/^y([2-9]|[1-9][0-9]+)?$/'); + expect(splomAttrs.yaxes.items.regex).toBe(cartesianIdRegex.y.toString()); }); it('should prune unsupported global-level trace attributes', function() { diff --git a/test/jasmine/karma.conf.js b/test/jasmine/karma.conf.js index cde3011b4eb..113f3185ade 100644 --- a/test/jasmine/karma.conf.js +++ b/test/jasmine/karma.conf.js @@ -246,7 +246,13 @@ func.defaultConfig = { }, _Firefox: { base: 'Firefox', - flags: ['--width=' + argv.width, '--height=' + argv.height] + flags: ['--width=' + argv.width, '--height=' + argv.height], + prefs: { + 'devtools.toolbox.zoomValue': '1.5', + 'devtools.toolbox.host': 'window', + 'devtools.toolbox.previousHost': 'bottom', + 'devtools.command-button-rulers.enabled': true + } } }, diff --git a/test/jasmine/tests/domain_ref_interact_test.js b/test/jasmine/tests/domain_ref_interact_test.js new file mode 100644 index 00000000000..6d79887ca2d --- /dev/null +++ b/test/jasmine/tests/domain_ref_interact_test.js @@ -0,0 +1,192 @@ +'use strict'; +var failTest = require('../assets/fail_test'); +var domainRefComponents = require('../assets/domain_ref/components'); +var createGraphDiv = require('../assets/create_graph_div'); +var destroyGraphDiv = require('../assets/destroy_graph_div'); +var Plotly = require('../../../lib/index'); +var Lib = require('../../../src/lib'); +var getSVGElemScreenBBox = require( + '../assets/get_svg_elem_screen_bbox'); +var testMock = require('../assets/domain_ref/domain_refs_editable.json'); +var delay = require('../assets/delay'); +var mouseEvent = require('../assets/mouse_event'); +// we have to use drag to move annotations for some reason +var drag = require('../assets/drag'); +// var SVGTools = require('../assets/svg_tools'); + +// color of the rectangles +var rectColor1 = 'rgb(10, 20, 30)'; +var rectColor2 = 'rgb(10, 20, 31)'; +var rectColor3 = 'rgb(100, 200, 232)'; +var rectColor4 = 'rgb(200, 200, 232)'; +var arrowColor1 = 'rgb(231, 200, 100)'; +var arrowColor2 = 'rgb(231, 200, 200)'; + +var DELAY_TIME = 10; + +// function svgRectToJSON(svgrect) { +// return JSON.stringify(SVGTools.svgRectToObj(svgrect)); +// } + +function checkBBox(bboxBefore, bboxAfter, moveX, moveY) { + // We print out the objects for sanity, because sometimes Jasmine says a + // test passed when it actually did nothing! + // console.log('bboxBefore', svgRectToJSON(bboxBefore)); + // console.log('bboxAfter', svgRectToJSON(bboxAfter)); + // console.log('moveX', moveX); + // console.log('moveY', moveY); + expect(bboxAfter.x).toBeCloseTo(bboxBefore.x + moveX, 2); + expect(bboxAfter.y).toBeCloseTo(bboxBefore.y + moveY, 2); +} + +function testObjectMove(objectColor, moveX, moveY, type) { + var bboxBefore = getSVGElemScreenBBox( + domainRefComponents.findAROByColor(objectColor, undefined, type) + ); + var pos = { + mouseStartX: bboxBefore.x + bboxBefore.width * 0.5, + mouseStartY: bboxBefore.y + bboxBefore.height * 0.5, + }; + pos.mouseEndX = pos.mouseStartX + moveX; + pos.mouseEndY = pos.mouseStartY + moveY; + mouseEvent('mousemove', pos.mouseStartX, pos.mouseStartY); + mouseEvent('mousedown', pos.mouseStartX, pos.mouseStartY); + mouseEvent('mousemove', pos.mouseEndX, pos.mouseEndY); + mouseEvent('mouseup', pos.mouseEndX, pos.mouseEndY); + var bboxAfter = getSVGElemScreenBBox( + domainRefComponents.findAROByColor(objectColor, undefined, type) + ); + checkBBox(bboxBefore, bboxAfter, moveX, moveY); +} + +function dragPos0(bbox, corner) { + if(corner === 'bl') { + return [ bbox.x + bbox.width * 0.5, + bbox.y + bbox.height * 0.5 - 10 ]; + } else if(corner === 'tr') { + return [ bbox.x + bbox.width * 0.5, + bbox.y + bbox.height * 0.5 + 10 ]; + } else { + return [ bbox.x + bbox.width * 0.5, + bbox.y + bbox.height * 0.5]; + } +} + +// Tests moving the annotation label +function testAnnotationMoveLabel(objectColor, moveX, moveY) { + var bboxAfter; + // Get where the text box (label) is before dragging it + var bboxBefore = getSVGElemScreenBBox( + domainRefComponents.findAROByColor(objectColor, undefined, 'rect') + ); + // we have to use drag to move annotations for some reason + var optLabelDrag = { + pos0: dragPos0(bboxBefore) + }; + optLabelDrag.dpos = [moveX, moveY]; + // console.log('optLabelDrag', optLabelDrag); + // drag the label, this will make the arrow rotate around the arrowhead + return (new Promise(function(resolve) { + drag(optLabelDrag); resolve(); + })) + .then(delay(DELAY_TIME)) + .then(function() { + // then check it's position + bboxAfter = getSVGElemScreenBBox( + domainRefComponents.findAROByColor(objectColor, undefined, 'rect') + ); + checkBBox(bboxBefore, bboxAfter, moveX, moveY); + }) + .then(delay(DELAY_TIME)); +} + +// Tests moving the whole annotation +function testAnnotationMoveWhole(objectColor, arrowColor, moveX, moveY, corner) { + var bboxAfter; + var arrowBBoxAfter; + // Get where the text box (label) is before dragging it + var bboxBefore = getSVGElemScreenBBox( + domainRefComponents.findAROByColor(objectColor, undefined, 'rect') + ); + var arrowBBoxBefore = getSVGElemScreenBBox( + domainRefComponents.findAROByColor(arrowColor, undefined, 'path', 'fill') + ); + var optArrowDrag = { + pos0: dragPos0(arrowBBoxBefore, corner) + }; + optArrowDrag.dpos = [moveX, moveY]; + // console.log('optArrowDrag', optArrowDrag); + // drag the whole annotation + (new Promise(function(resolve) { + drag(optArrowDrag); resolve(); + })) + .then(delay(DELAY_TIME)) + .then(function() { + // check the new position of the arrow and label + arrowBBoxAfter = getSVGElemScreenBBox( + domainRefComponents.findAROByColor(arrowColor, undefined, 'path', 'fill') + ); + bboxAfter = getSVGElemScreenBBox( + domainRefComponents.findAROByColor(objectColor, undefined, 'rect') + ); + checkBBox(arrowBBoxBefore, arrowBBoxAfter, moveX, moveY); + checkBBox(bboxBefore, bboxAfter, moveX, moveY); + }) + .then(delay(DELAY_TIME)); +} + +describe('Shapes referencing domain', function() { + var gd; + beforeEach(function() { + gd = createGraphDiv(); + }); + afterEach(function() { + Plotly.purge(gd); + destroyGraphDiv(gd); + gd = null; + }); + function testObjectMoveItFun(color, x, y, type) { + return function(done) { + Plotly.newPlot(gd, Lib.extendDeep({}, testMock)) + .then(delay(DELAY_TIME)) + .then(function() { + testObjectMove(color, x, y, type); + }) + .then(delay(DELAY_TIME)) + .catch(failTest) + .then(done); + }; + } + function testAnnotationMoveLabelItFun(color, x, y) { + return function(done) { + Plotly.newPlot(gd, Lib.extendDeep({}, testMock)) + .then(delay(DELAY_TIME)) + .then(testAnnotationMoveLabel(color, x, y)) + .then(delay(DELAY_TIME)) + .catch(failTest) + .then(done); + }; + } + function testAnnotationMoveWholeItFun(color, arrowColor, x, y, corner) { + return function(done) { + Plotly.newPlot(gd, Lib.extendDeep({}, testMock)) + .then(delay(DELAY_TIME)) + .then(testAnnotationMoveWhole(color, arrowColor, x, y, corner)) + .then(delay(DELAY_TIME)) + .catch(failTest) + .then(done); + }; + } + it('should move box on linear x axis and log y to the proper position', + testObjectMoveItFun(rectColor1, 100, -300, 'path')); + it('should move box on log x axis and linear y to the proper position', + testObjectMoveItFun(rectColor2, -400, -200, 'path')); + it('should move annotation label on linear x axis and log y to the proper position', + testAnnotationMoveLabelItFun(rectColor3, 50, -100, 'rect')); + it('should move annotation label on log x axis and linear y to the proper position', + testAnnotationMoveLabelItFun(rectColor4, -75, -150, 'rect')); + it('should move whole annotation on linear x axis and log y to the proper position', + testAnnotationMoveWholeItFun(rectColor3, arrowColor1, 50, -100, 'bl')); + it('should move whole annotation on log x axis and linear y to the proper position', + testAnnotationMoveWholeItFun(rectColor4, arrowColor2, -75, -150, 'tr')); +}); diff --git a/test/jasmine/tests/domain_ref_test.js b/test/jasmine/tests/domain_ref_test.js new file mode 100644 index 00000000000..79ab84b101e --- /dev/null +++ b/test/jasmine/tests/domain_ref_test.js @@ -0,0 +1,48 @@ +'use strict'; +var failTest = require('../assets/fail_test'); +var domainRefComponents = require('../assets/domain_ref/components'); +var createGraphDiv = require('../assets/create_graph_div'); +var destroyGraphDiv = require('../assets/destroy_graph_div'); +var Plotly = require('../../../lib/index'); +// optionally specify a test number to run just a single test +var testNumber; + +function makeTests(component, filter) { + return function() { + filter = filter === undefined ? function() { + return true; + } : filter; + var descriptions = component.descriptions().filter(filter); + var tests = component.tests().filter(filter); + var gd; + beforeEach(function() { + gd = createGraphDiv(); + }); + afterEach(function() { + Plotly.purge(gd); + destroyGraphDiv(gd); + gd = null; + }); + descriptions.forEach(function(d, i) { + it(d, function(done) { + // console.log('testing ' + d); + gd.id = 'graph-' + i; + tests[i](function(v) { + expect(v).toBe(true); + }, gd) + .catch(failTest) + .then(done); + }); + }); + }; +} + +['annotations', 'images', 'shapes'].forEach(function(componentName) { + describe('Test ' + componentName, makeTests(domainRefComponents[componentName], + function(f, i) { + if(testNumber === undefined) { + return true; + } + return i === testNumber; + })); +}); diff --git a/test/jasmine/tests/gl2d_plot_interact_test.js b/test/jasmine/tests/gl2d_plot_interact_test.js index 60eca4fdfe4..945345b51d0 100644 --- a/test/jasmine/tests/gl2d_plot_interact_test.js +++ b/test/jasmine/tests/gl2d_plot_interact_test.js @@ -21,7 +21,7 @@ describe('Test removal of gl contexts', function() { var gd; beforeEach(function() { - jasmine.DEFAULT_TIMEOUT_INTERVAL = 5000; + jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000; gd = createGraphDiv(); }); @@ -94,7 +94,7 @@ describe('Test gl plot side effects', function() { var gd; beforeEach(function() { - jasmine.DEFAULT_TIMEOUT_INTERVAL = 5000; + jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000; gd = createGraphDiv(); }); @@ -389,7 +389,7 @@ describe('Test gl2d plot interactions:', function() { var mock = require('@mocks/gl2d_10.json'); beforeEach(function() { - jasmine.DEFAULT_TIMEOUT_INTERVAL = 5000; + jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000; gd = createGraphDiv(); }); diff --git a/test/jasmine/tests/gl3d_hover_click_test.js b/test/jasmine/tests/gl3d_hover_click_test.js index 49df2b4c4df..7cfcb7b645a 100644 --- a/test/jasmine/tests/gl3d_hover_click_test.js +++ b/test/jasmine/tests/gl3d_hover_click_test.js @@ -35,7 +35,7 @@ describe('Test gl3d trace click/hover:', function() { beforeEach(function() { gd = createGraphDiv(); ptData = {}; - jasmine.DEFAULT_TIMEOUT_INTERVAL = 6000; + jasmine.DEFAULT_TIMEOUT_INTERVAL = 12000; }); afterEach(function() { diff --git a/test/jasmine/tests/gl3d_plot_interact_test.js b/test/jasmine/tests/gl3d_plot_interact_test.js index b77003fb8d1..eee0e2a2395 100644 --- a/test/jasmine/tests/gl3d_plot_interact_test.js +++ b/test/jasmine/tests/gl3d_plot_interact_test.js @@ -19,7 +19,7 @@ describe('Test gl3d before/after plot', function() { var mock = require('@mocks/gl3d_marker-arrays.json'); beforeEach(function() { - jasmine.DEFAULT_TIMEOUT_INTERVAL = 4000; + jasmine.DEFAULT_TIMEOUT_INTERVAL = 8000; }); afterEach(function() { @@ -144,7 +144,7 @@ describe('Test gl3d plots', function() { beforeEach(function() { gd = createGraphDiv(); - jasmine.DEFAULT_TIMEOUT_INTERVAL = 6000; + jasmine.DEFAULT_TIMEOUT_INTERVAL = 12000; }); afterEach(function() { @@ -918,7 +918,7 @@ describe('Test gl3d drag and wheel interactions', function() { beforeEach(function() { gd = createGraphDiv(); - jasmine.DEFAULT_TIMEOUT_INTERVAL = 3000; + jasmine.DEFAULT_TIMEOUT_INTERVAL = 6000; }); afterEach(function() { diff --git a/test/jasmine/tests/mock_test.js b/test/jasmine/tests/mock_test.js index 1be09bf830f..00ead5f8508 100644 --- a/test/jasmine/tests/mock_test.js +++ b/test/jasmine/tests/mock_test.js @@ -260,6 +260,7 @@ var list = [ 'date_histogram', 'dendrogram', 'display-text_zero-number', + 'domain_refs', 'earth_heatmap', 'electric_heatmap', 'empty', @@ -1322,6 +1323,7 @@ figs['date_axes_period_breaks_automargin'] = require('@mocks/date_axes_period_br figs['date_histogram'] = require('@mocks/date_histogram'); // figs['dendrogram'] = require('@mocks/dendrogram'); figs['display-text_zero-number'] = require('@mocks/display-text_zero-number'); +figs['domain_refs'] = require('@mocks/domain_refs'); figs['earth_heatmap'] = require('@mocks/earth_heatmap'); figs['electric_heatmap'] = require('@mocks/electric_heatmap'); figs['empty'] = require('@mocks/empty'); diff --git a/test/jasmine/tests/parcats_test.js b/test/jasmine/tests/parcats_test.js index 9255740ba6e..eb5828ef87b 100644 --- a/test/jasmine/tests/parcats_test.js +++ b/test/jasmine/tests/parcats_test.js @@ -12,7 +12,7 @@ var delay = require('../assets/delay'); var customAssertions = require('../assets/custom_assertions'); var assertHoverLabelContent = customAssertions.assertHoverLabelContent; -var CALLBACK_DELAY = 500; +var CALLBACK_DELAY = 3000; // Testing constants // ================= diff --git a/test/jasmine/tests/polar_test.js b/test/jasmine/tests/polar_test.js index e087f2495e4..4262b87e41d 100644 --- a/test/jasmine/tests/polar_test.js +++ b/test/jasmine/tests/polar_test.js @@ -675,7 +675,7 @@ describe('Test polar interactions:', function() { ]; beforeEach(function() { - jasmine.DEFAULT_TIMEOUT_INTERVAL = 5000; + jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000; eventData = ''; eventCnts = {}; gd = createGraphDiv(); @@ -1367,7 +1367,7 @@ describe('Test polar *gridshape linear* interactions', function() { var gd; beforeEach(function() { - jasmine.DEFAULT_TIMEOUT_INTERVAL = 5000; + jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000; gd = createGraphDiv(); }); diff --git a/test/jasmine/tests/scatter3d_test.js b/test/jasmine/tests/scatter3d_test.js index abe1a04bde4..27cdea117bf 100644 --- a/test/jasmine/tests/scatter3d_test.js +++ b/test/jasmine/tests/scatter3d_test.js @@ -106,7 +106,7 @@ describe('Test scatter3d interactions:', function() { beforeEach(function() { gd = createGraphDiv(); - jasmine.DEFAULT_TIMEOUT_INTERVAL = 6000; + jasmine.DEFAULT_TIMEOUT_INTERVAL = 12000; }); afterEach(function() { diff --git a/test/jasmine/tests/scattergl_test.js b/test/jasmine/tests/scattergl_test.js index 6dc35e2cb36..4e1c60d10d9 100644 --- a/test/jasmine/tests/scattergl_test.js +++ b/test/jasmine/tests/scattergl_test.js @@ -624,7 +624,7 @@ describe('Test scattergl autorange:', function() { var gd; beforeEach(function() { - jasmine.DEFAULT_TIMEOUT_INTERVAL = 5000; + jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000; gd = createGraphDiv(); }); @@ -675,7 +675,7 @@ describe('Test scattergl autorange:', function() { var gd; beforeEach(function() { - jasmine.DEFAULT_TIMEOUT_INTERVAL = 5000; + jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000; gd = createGraphDiv(); // to avoid expansive draw calls (which could be problematic on CI) spyOn(ScatterGl, 'plot').and.callFake(function(gd) {