diff --git a/src/components/fx/hover.js b/src/components/fx/hover.js
index 9e65fe7386f..a108240b916 100644
--- a/src/components/fx/hover.js
+++ b/src/components/fx/hover.js
@@ -605,7 +605,7 @@ function _hover(gd, evt, subplot, noHoverEvent) {
var result = dragElement.unhoverRaw(gd, evt);
if(hasCartesian && ((spikePoints.hLinePoint !== null) || (spikePoints.vLinePoint !== null))) {
if(spikesChanged(oldspikepoints)) {
- createSpikelines(spikePoints, spikelineOpts);
+ createSpikelines(gd, spikePoints, spikelineOpts);
}
}
return result;
@@ -613,7 +613,7 @@ function _hover(gd, evt, subplot, noHoverEvent) {
if(hasCartesian) {
if(spikesChanged(oldspikepoints)) {
- createSpikelines(spikePoints, spikelineOpts);
+ createSpikelines(gd, spikePoints, spikelineOpts);
}
}
@@ -1396,9 +1396,10 @@ function cleanPoint(d, hovermode) {
return d;
}
-function createSpikelines(closestPoints, opts) {
+function createSpikelines(gd, closestPoints, opts) {
var container = opts.container;
var fullLayout = opts.fullLayout;
+ var gs = fullLayout._size;
var evt = opts.event;
var showY = !!closestPoints.hLinePoint;
var showX = !!closestPoints.vLinePoint;
@@ -1433,8 +1434,7 @@ function createSpikelines(closestPoints, opts) {
var yMode = ya.spikemode;
var yThickness = ya.spikethickness;
var yColor = ya.spikecolor || dfltHLineColor;
- var yBB = ya._boundingBox;
- var xEdge = ((yBB.left + yBB.right) / 2) < hLinePointX ? yBB.right : yBB.left;
+ var xEdge = Axes.getPxPosition(gd, ya);
var xBase, xEndSpike;
if(yMode.indexOf('toaxis') !== -1 || yMode.indexOf('across') !== -1) {
@@ -1443,8 +1443,14 @@ function createSpikelines(closestPoints, opts) {
xEndSpike = hLinePointX;
}
if(yMode.indexOf('across') !== -1) {
- xBase = ya._counterSpan[0];
- xEndSpike = ya._counterSpan[1];
+ var xAcross0 = ya._counterDomainMin;
+ var xAcross1 = ya._counterDomainMax;
+ if(ya.anchor === 'free') {
+ xAcross0 = Math.min(xAcross0, ya.position);
+ xAcross1 = Math.max(xAcross1, ya.position);
+ }
+ xBase = gs.l + xAcross0 * gs.w;
+ xEndSpike = gs.l + xAcross1 * gs.w;
}
// Foreground horizontal line (to y-axis)
@@ -1507,8 +1513,7 @@ function createSpikelines(closestPoints, opts) {
var xMode = xa.spikemode;
var xThickness = xa.spikethickness;
var xColor = xa.spikecolor || dfltVLineColor;
- var xBB = xa._boundingBox;
- var yEdge = ((xBB.top + xBB.bottom) / 2) < vLinePointY ? xBB.bottom : xBB.top;
+ var yEdge = Axes.getPxPosition(gd, xa);
var yBase, yEndSpike;
if(xMode.indexOf('toaxis') !== -1 || xMode.indexOf('across') !== -1) {
@@ -1517,8 +1522,14 @@ function createSpikelines(closestPoints, opts) {
yEndSpike = vLinePointY;
}
if(xMode.indexOf('across') !== -1) {
- yBase = xa._counterSpan[0];
- yEndSpike = xa._counterSpan[1];
+ var yAcross0 = xa._counterDomainMin;
+ var yAcross1 = xa._counterDomainMax;
+ if(xa.anchor === 'free') {
+ yAcross0 = Math.min(yAcross0, xa.position);
+ yAcross1 = Math.max(yAcross1, xa.position);
+ }
+ yBase = gs.t + (1 - yAcross1) * gs.h;
+ yEndSpike = gs.t + (1 - yAcross0) * gs.h;
}
// Foreground vertical line (to x-axis)
diff --git a/src/components/rangeslider/draw.js b/src/components/rangeslider/draw.js
index e427d610bc5..43902b66934 100644
--- a/src/components/rangeslider/draw.js
+++ b/src/components/rangeslider/draw.js
@@ -108,17 +108,14 @@ module.exports = function(gd) {
var gs = fullLayout._size;
var domain = axisOpts.domain;
- var tickHeight = opts._tickHeight;
-
- var oppBottom = opts._oppBottom;
opts._width = gs.w * (domain[1] - domain[0]);
var x = Math.round(gs.l + (gs.w * domain[0]));
var y = Math.round(
- gs.t + gs.h * (1 - oppBottom) +
- tickHeight +
+ gs.t + gs.h * (1 - axisOpts._counterDomainMin) +
+ (axisOpts.side === 'bottom' ? axisOpts._depth : 0) +
opts._offsetShift + constants.extraPad
);
diff --git a/src/components/rangeslider/helpers.js b/src/components/rangeslider/helpers.js
index 461fde83dcb..08352e0739a 100644
--- a/src/components/rangeslider/helpers.js
+++ b/src/components/rangeslider/helpers.js
@@ -9,7 +9,9 @@
'use strict';
var axisIDs = require('../../plots/cartesian/axis_ids');
+var svgTextUtils = require('../../lib/svg_text_utils');
var constants = require('./constants');
+var LINE_SPACING = require('../../constants/alignment').LINE_SPACING;
var name = constants.name;
function isVisible(ax) {
@@ -42,27 +44,30 @@ exports.makeData = function(fullLayout) {
};
exports.autoMarginOpts = function(gd, ax) {
+ var fullLayout = gd._fullLayout;
var opts = ax[name];
+ var axLetter = ax._id.charAt(0);
- var oppBottom = Infinity;
- var counterAxes = ax._counterAxes;
- for(var j = 0; j < counterAxes.length; j++) {
- var counterId = counterAxes[j];
- var oppAxis = axisIDs.getFromId(gd, counterId);
- oppBottom = Math.min(oppBottom, oppAxis.domain[0]);
+ var bottomDepth = 0;
+ var titleHeight = 0;
+ if(ax.side === 'bottom') {
+ bottomDepth = ax._depth;
+ if(ax.title.text !== fullLayout._dfltTitle[axLetter]) {
+ // as in rangeslider/draw.js
+ titleHeight = 1.5 * ax.title.font.size + 10 + opts._offsetShift;
+ // multi-line extra bump
+ var extraLines = (ax.title.text.match(svgTextUtils.BR_TAG_ALL) || []).length;
+ titleHeight += extraLines * ax.title.font.size * LINE_SPACING;
+ }
}
- opts._oppBottom = oppBottom;
-
- var tickHeight = (ax.side === 'bottom' && ax._boundingBox.height) || 0;
- opts._tickHeight = tickHeight;
return {
x: 0,
- y: oppBottom,
+ y: ax._counterDomainMin,
l: 0,
r: 0,
t: 0,
- b: opts._height + gd._fullLayout.margin.b + tickHeight,
+ b: opts._height + bottomDepth + Math.max(fullLayout.margin.b, titleHeight),
pad: constants.extraPad + opts._offsetShift * 2
};
};
diff --git a/src/lib/svg_text_utils.js b/src/lib/svg_text_utils.js
index 37d8de07458..b83bb2a92be 100644
--- a/src/lib/svg_text_utils.js
+++ b/src/lib/svg_text_utils.js
@@ -266,6 +266,7 @@ var SPLIT_TAGS = /(<[^<>]*>)/;
var ONE_TAG = /<(\/?)([^ >]*)(\s+(.*))?>/i;
var BR_TAG = /
/i;
+exports.BR_TAG_ALL = /
/gi;
/*
* style and href: pull them out of either single or double quotes. Also
diff --git a/src/plot_api/subroutines.js b/src/plot_api/subroutines.js
index ce221c8efbf..df87fbcd474 100644
--- a/src/plot_api/subroutines.js
+++ b/src/plot_api/subroutines.js
@@ -72,7 +72,7 @@ function lsInner(gd) {
// can still get here because it makes some of the SVG structure
// for shared features like selections.
if(!fullLayout._has('cartesian')) {
- return gd._promises.length && Promise.all(gd._promises);
+ return Plots.previousPromises(gd);
}
function getLinePosition(ax, counterAx, side) {
@@ -347,7 +347,7 @@ function lsInner(gd) {
Axes.makeClipPaths(gd);
- return gd._promises.length && Promise.all(gd._promises);
+ return Plots.previousPromises(gd);
}
function shouldShowLinesOrTicks(ax, subplot) {
@@ -599,9 +599,11 @@ exports.drawData = function(gd) {
// styling separate from drawing
Plots.style(gd);
- // show annotations and shapes
+ // draw components that can be drawn on axes,
+ // and that do not push the margins
Registry.getComponentMethod('shapes', 'draw')(gd);
Registry.getComponentMethod('annotations', 'draw')(gd);
+ Registry.getComponentMethod('images', 'draw')(gd);
// Mark the first render as complete
fullLayout._replotting = false;
@@ -717,9 +719,6 @@ exports.doAutoRangeAndConstraints = function(gd) {
// correctly sized and the whole plot re-margined. fullLayout._replotting must
// be set to false before these will work properly.
exports.finalDraw = function(gd) {
- Registry.getComponentMethod('shapes', 'draw')(gd);
- Registry.getComponentMethod('images', 'draw')(gd);
- Registry.getComponentMethod('annotations', 'draw')(gd);
// TODO: rangesliders really belong in marginPushers but they need to be
// drawn after data - can we at least get the margin pushing part separated
// out and done earlier?
diff --git a/src/plots/cartesian/axes.js b/src/plots/cartesian/axes.js
index 0c83a476ffa..1b6bb040cae 100644
--- a/src/plots/cartesian/axes.js
+++ b/src/plots/cartesian/axes.js
@@ -6,7 +6,6 @@
* LICENSE file in the root directory of this source tree.
*/
-
'use strict';
var d3 = require('d3');
@@ -35,6 +34,7 @@ var BADNUM = constants.BADNUM;
var MID_SHIFT = require('../../constants/alignment').MID_SHIFT;
var LINE_SPACING = require('../../constants/alignment').LINE_SPACING;
+var OPPOSITE_SIDE = require('../../constants/alignment').OPPOSITE_SIDE;
var axes = module.exports = {};
@@ -1636,6 +1636,24 @@ axes.draw = function(gd, arg, opts) {
* @param {object} ax (full) axis object
* @param {object} opts
* - @param {boolean} skipTitle (set to true to skip axis title draw call)
+ *
+ * Depends on:
+ * - ax._mainSubplot (from linkSubplots)
+ * - ax._mainAxis
+ * - ax._anchorAxis
+ * - ax._subplotsWith
+ * - ax._counterDomainMin, ax._counterDomainMax (optionally, from linkSubplots)
+ * - ax._mainLinePosition (from lsInner)
+ * - ax._mainMirrorPosition
+ * - ax._linepositions
+ *
+ * Fills in:
+ * - ax._vals:
+ * - ax._gridVals:
+ * - ax._selections:
+ * - ax._tickAngles:
+ * - ax._depth (when required only):
+ * - and calls ax.setScale
*/
axes.drawOne = function(gd, ax, opts) {
opts = opts || {};
@@ -1648,12 +1666,10 @@ axes.drawOne = function(gd, ax, opts) {
var axId = ax._id;
var axLetter = axId.charAt(0);
var counterLetter = axes.counterLetter(axId);
- var mainSubplot = ax._mainSubplot;
var mainLinePosition = ax._mainLinePosition;
var mainMirrorPosition = ax._mainMirrorPosition;
- var mainPlotinfo = fullLayout._plots[mainSubplot];
+ var mainPlotinfo = fullLayout._plots[ax._mainSubplot];
var mainAxLayer = mainPlotinfo[axLetter + 'axislayer'];
- var subplotsWithAx = ax._subplotsWith;
var vals = ax._vals = axes.calcTicks(ax);
@@ -1664,13 +1680,27 @@ axes.drawOne = function(gd, ax, opts) {
vals[i].axInfo = axInfo;
}
- if(!ax.visible) return;
-
// stash selections to avoid DOM queries e.g.
// - stash tickLabels selection, so that drawTitle can use it to scoot title
ax._selections = {};
// stash tick angle (including the computed 'auto' values) per tick-label class
ax._tickAngles = {};
+ // measure [in px] between axis position and outward-most part of bounding box
+ // (touching either the tick label or ticks)
+ // depth can be expansive to compute, so we only do so when required
+ ax._depth = null;
+
+ // calcLabelLevelBbox can be expensive,
+ // so make sure to not call it twice during the same Axes.drawOne call
+ // by stashing label-level bounding boxes per tick-label class
+ var llbboxes = {};
+ function getLabelLevelBbox(suffix) {
+ var cls = axId + (suffix || 'tick');
+ if(!llbboxes[cls]) llbboxes[cls] = calcLabelLevelBbox(ax, cls);
+ return llbboxes[cls];
+ }
+
+ if(!ax.visible) return;
var transFn = axes.makeTransFn(ax);
var tickVals;
@@ -1691,7 +1721,9 @@ axes.drawOne = function(gd, ax, opts) {
var dividerVals = getDividerVals(ax, vals);
if(!fullLayout._hasOnlyLargeSploms) {
- // keep track of which subplots (by main conteraxis) we've already
+ var subplotsWithAx = ax._subplotsWith;
+
+ // keep track of which subplots (by main counter axis) we've already
// drawn grids for, so we don't overdraw overlaying subplots
var finishedGrids = {};
@@ -1795,13 +1827,13 @@ axes.drawOne = function(gd, ax, opts) {
});
if(ax.type === 'multicategory') {
- var labelLength = 0;
var pad = {x: 2, y: 10}[axLetter];
- var sgn = tickSigns[2] * (ax.ticks === 'inside' ? -1 : 1);
+ var sgn = {l: -1, t: -1, r: 1, b: 1}[ax.side.charAt(0)];
seq.push(function() {
- labelLength += getLabelLevelSpan(ax, axId + 'tick') + pad;
- labelLength += ax._tickAngles[axId + 'tick'] ? ax.tickfont.size * LINE_SPACING : 0;
+ var bboxKey = {x: 'height', y: 'width'}[axLetter];
+ var standoff = getLabelLevelBbox()[bboxKey] + pad +
+ (ax._tickAngles[axId + 'tick'] ? ax.tickfont.size * LINE_SPACING : 0);
return axes.drawLabels(gd, ax, {
vals: getSecondaryLabelVals(ax, vals),
@@ -1810,197 +1842,128 @@ axes.drawOne = function(gd, ax, opts) {
repositionOnUpdate: true,
secondary: true,
transFn: transFn,
- labelFns: axes.makeLabelFns(ax, mainLinePosition + labelLength * sgn)
+ labelFns: axes.makeLabelFns(ax, mainLinePosition + standoff * sgn)
});
});
seq.push(function() {
- labelLength += getLabelLevelSpan(ax, axId + 'tick2');
- ax._labelLength = labelLength;
+ ax._depth = sgn * (getLabelLevelBbox('tick2')[ax.side] - mainLinePosition);
return drawDividers(gd, ax, {
vals: dividerVals,
layer: mainAxLayer,
- path: axes.makeTickPath(ax, mainLinePosition, sgn, labelLength),
+ path: axes.makeTickPath(ax, mainLinePosition, sgn, ax._depth),
transFn: transFn
});
});
}
- function extendRange(range, newRange) {
- range[0] = Math.min(range[0], newRange[0]);
- range[1] = Math.max(range[1], newRange[1]);
- }
-
- function calcBoundingBox() {
- if(ax.showticklabels) {
- var gdBB = gd.getBoundingClientRect();
- var bBox = mainAxLayer.node().getBoundingClientRect();
-
- /*
- * the way we're going to use this, the positioning that matters
- * is relative to the origin of gd. This is important particularly
- * if gd is scrollable, and may have been scrolled between the time
- * we calculate this and the time we use it
- */
-
- ax._boundingBox = {
- width: bBox.width,
- height: bBox.height,
- left: bBox.left - gdBB.left,
- right: bBox.right - gdBB.left,
- top: bBox.top - gdBB.top,
- bottom: bBox.bottom - gdBB.top
- };
- } else {
- var gs = fullLayout._size;
- var pos;
-
- // set dummy bbox for ticklabel-less axes
-
- if(axLetter === 'x') {
- pos = ax.anchor === 'free' ?
- gs.t + gs.h * (1 - ax.position) :
- gs.t + gs.h * (1 - ax._anchorAxis.domain[{bottom: 0, top: 1}[ax.side]]);
-
- ax._boundingBox = {
- top: pos,
- bottom: pos,
- left: ax._offset,
- right: ax._offset + ax._length,
- width: ax._length,
- height: 0
- };
- } else {
- pos = ax.anchor === 'free' ?
- gs.l + gs.w * ax.position :
- gs.l + gs.w * ax._anchorAxis.domain[{left: 0, right: 1}[ax.side]];
-
- ax._boundingBox = {
- left: pos,
- right: pos,
- bottom: ax._offset + ax._length,
- top: ax._offset,
- height: ax._length,
- width: 0
- };
- }
- }
-
- /*
- * for spikelines: what's the full domain of positions in the
- * opposite direction that are associated with this axis?
- * This means any axes that we make a subplot with, plus the
- * position of the axis itself if it's free.
- */
- if(subplotsWithAx) {
- var fullRange = ax._counterSpan = [Infinity, -Infinity];
-
- for(var i = 0; i < subplotsWithAx.length; i++) {
- var plotinfo = fullLayout._plots[subplotsWithAx[i]];
- var counterAxis = plotinfo[(axLetter === 'x') ? 'yaxis' : 'xaxis'];
-
- extendRange(fullRange, [
- counterAxis._offset,
- counterAxis._offset + counterAxis._length
- ]);
- }
-
- if(ax.anchor === 'free') {
- extendRange(fullRange, (axLetter === 'x') ?
- [ax._boundingBox.bottom, ax._boundingBox.top] :
- [ax._boundingBox.right, ax._boundingBox.left]);
- }
- }
- }
-
var hasRangeSlider = Registry.getComponentMethod('rangeslider', 'isVisible')(ax);
- function doAutoMargins() {
+ seq.push(function() {
var s = ax.side.charAt(0);
+ var sMirror = OPPOSITE_SIDE[ax.side].charAt(0);
+ var pos = axes.getPxPosition(gd, ax);
+ var outsideTickLen = ax.ticks === 'outside' ? ax.ticklen : 0;
+ var llbbox;
+
var push;
+ var mirrorPush;
var rangeSliderPush;
- if(hasRangeSlider) {
- rangeSliderPush = Registry.getComponentMethod('rangeslider', 'autoMarginOpts')(gd, ax);
+ if(ax.automargin || hasRangeSlider) {
+ if(ax.type === 'multicategory') {
+ llbbox = getLabelLevelBbox('tick2');
+ } else {
+ llbbox = getLabelLevelBbox();
+ if(axLetter === 'x' && s === 'b') {
+ ax._depth = Math.max(llbbox.width > 0 ? llbbox.bottom - pos : 0, outsideTickLen);
+ }
+ }
}
- Plots.autoMargin(gd, rangeSliderAutoMarginID(ax), rangeSliderPush);
- if(ax.automargin && (!hasRangeSlider || s !== 'b')) {
+ if(ax.automargin) {
push = {x: 0, y: 0, r: 0, l: 0, t: 0, b: 0};
+ var domainIndices = [0, 1];
+
+ if(axLetter === 'x') {
+ if(s === 'b') {
+ push[s] = ax._depth;
+ } else {
+ push[s] = ax._depth = Math.max(llbbox.width > 0 ? pos - llbbox.top : 0, outsideTickLen);
+ domainIndices.reverse();
+ }
- var bbox = ax._boundingBox;
- var titleOffset = getTitleOffset(gd, ax);
- var anchorAxDomainIndex;
- var offset;
-
- switch(axLetter + s) {
- case 'xb':
- anchorAxDomainIndex = 0;
- offset = bbox.top - titleOffset;
- push[s] = bbox.height;
- break;
- case 'xt':
- anchorAxDomainIndex = 1;
- offset = titleOffset - bbox.bottom;
- push[s] = bbox.height;
- break;
- case 'yl':
- anchorAxDomainIndex = 0;
- offset = titleOffset - bbox.right;
- push[s] = bbox.width;
- break;
- case 'yr':
- anchorAxDomainIndex = 1;
- offset = bbox.left - titleOffset;
- push[s] = bbox.width;
- break;
+ if(llbbox.width > 0) {
+ var rExtra = llbbox.right - (ax._offset + ax._length);
+ if(rExtra > 0) {
+ push.x = 1;
+ push.r = rExtra;
+ }
+ var lExtra = ax._offset - llbbox.left;
+ if(lExtra > 0) {
+ push.x = 0;
+ push.l = lExtra;
+ }
+ }
+ } else {
+ if(s === 'l') {
+ push[s] = ax._depth = Math.max(llbbox.height > 0 ? pos - llbbox.left : 0, outsideTickLen);
+ } else {
+ push[s] = ax._depth = Math.max(llbbox.height > 0 ? llbbox.right - pos : 0, outsideTickLen);
+ domainIndices.reverse();
+ }
+
+ if(llbbox.height > 0) {
+ var bExtra = llbbox.bottom - (ax._offset + ax._length);
+ if(bExtra > 0) {
+ push.y = 0;
+ push.b = bExtra;
+ }
+ var tExtra = ax._offset - llbbox.top;
+ if(tExtra > 0) {
+ push.y = 1;
+ push.t = tExtra;
+ }
+ }
}
push[counterLetter] = ax.anchor === 'free' ?
ax.position :
- ax._anchorAxis.domain[anchorAxDomainIndex];
-
- if(push[s] > 0) {
- push[s] += offset;
- }
+ ax._anchorAxis.domain[domainIndices[0]];
if(ax.title.text !== fullLayout._dfltTitle[axLetter]) {
- push[s] += ax.title.font.size;
+ var extraLines = (ax.title.text.match(svgTextUtils.BR_TAG_ALL) || []).length;
+ push[s] += extraLines ?
+ ax.title.font.size * (extraLines + 1) * LINE_SPACING :
+ ax.title.font.size;
}
- if(axLetter === 'x' && bbox.width > 0) {
- var rExtra = bbox.right - (ax._offset + ax._length);
- if(rExtra > 0) {
- push.x = 1;
- push.r = rExtra;
- }
- var lExtra = ax._offset - bbox.left;
- if(lExtra > 0) {
- push.x = 0;
- push.l = lExtra;
- }
- } else if(axLetter === 'y' && bbox.height > 0) {
- var bExtra = bbox.bottom - (ax._offset + ax._length);
- if(bExtra > 0) {
- push.y = 0;
- push.b = bExtra;
- }
- var tExtra = ax._offset - bbox.top;
- if(tExtra > 0) {
- push.y = 1;
- push.t = tExtra;
+ if(ax.mirror) {
+ mirrorPush = {x: 0, y: 0, r: 0, l: 0, t: 0, b: 0};
+
+ mirrorPush[sMirror] = ax.linewidth;
+ if(ax.mirror && ax.mirror !== true) mirrorPush[sMirror] += outsideTickLen;
+
+ if(ax.mirror === true || ax.mirror === 'ticks') {
+ mirrorPush[counterLetter] = ax._anchorAxis.domain[domainIndices[1]];
+ } else if(ax.mirror === 'all' || ax.mirror === 'allticks') {
+ mirrorPush[counterLetter] = [ax._counterDomainMin, ax._counterDomainMax][domainIndices[1]];
}
}
}
- Plots.autoMargin(gd, axAutoMarginID(ax), push);
- }
+ if(hasRangeSlider) {
+ rangeSliderPush = Registry.getComponentMethod('rangeslider', 'autoMarginOpts')(gd, ax);
+ }
- seq.push(calcBoundingBox, doAutoMargins);
+ Plots.autoMargin(gd, axAutoMarginID(ax), push);
+ Plots.autoMargin(gd, axMirrorAutoMarginID(ax), mirrorPush);
+ Plots.autoMargin(gd, rangeSliderAutoMarginID(ax), rangeSliderPush);
+ });
if(!opts.skipTitle &&
- !(hasRangeSlider && ax._boundingBox && ax.side === 'bottom')
+ !(hasRangeSlider && ax.side === 'bottom')
) {
seq.push(function() { return drawTitle(gd, ax); });
}
@@ -2078,27 +2041,45 @@ function getDividerVals(ax, vals) {
return out;
}
-function getLabelLevelSpan(ax, cls) {
- var axLetter = ax._id.charAt(0);
- var angle = ax._tickAngles[cls] || 0;
- var rad = Lib.deg2rad(angle);
- var sinA = Math.sin(rad);
- var cosA = Math.cos(rad);
- var maxX = 0;
- var maxY = 0;
-
- // N.B. Drawing.bBox does not take into account rotate transforms
-
- ax._selections[cls].each(function() {
- var thisLabel = selectTickLabel(this);
- var bb = Drawing.bBox(thisLabel.node());
- var w = bb.width;
- var h = bb.height;
- maxX = Math.max(maxX, cosA * w, sinA * h);
- maxY = Math.max(maxY, sinA * w, cosA * h);
- });
+function calcLabelLevelBbox(ax, cls) {
+ var top, bottom;
+ var left, right;
+
+ if(ax._selections[cls].size()) {
+ top = Infinity;
+ bottom = -Infinity;
+ left = Infinity;
+ right = -Infinity;
+ ax._selections[cls].each(function() {
+ var thisLabel = selectTickLabel(this);
+ // Use parent node , to make Drawing.bBox
+ // retrieve a bbox computed with transform info
+ //
+ // To improve perf, it would be nice to use `thisLabel.node()`
+ // (like in fixLabelOverlaps) instead and use Axes.getPxPosition
+ // together with the makeLabelFns outputs and `tickangle`
+ // to compute one bbox per (tick value x tick style)
+ var bb = Drawing.bBox(thisLabel.node().parentNode);
+ top = Math.min(top, bb.top);
+ bottom = Math.max(bottom, bb.bottom);
+ left = Math.min(left, bb.left);
+ right = Math.max(right, bb.right);
+ });
+ } else {
+ top = 0;
+ bottom = 0;
+ left = 0;
+ right = 0;
+ }
- return {x: maxY, y: maxX}[axLetter];
+ return {
+ top: top,
+ bottom: bottom,
+ left: left,
+ right: right,
+ height: bottom - top,
+ width: right - left
+ };
}
/**
@@ -2151,7 +2132,7 @@ axes.makeTransFn = function(ax) {
* - {number} ticklen
* - {number} linewidth
* @param {number} shift along direction of ticklen
- * @param {1 or -1} sng tick sign
+ * @param {1 or -1} sgn tick sign
* @param {number (optional)} len tick length
* @return {string}
*/
@@ -2655,14 +2636,28 @@ function drawDividers(gd, ax, opts) {
.attr('d', opts.path);
}
-function getTitleOffset(gd, ax) {
+/**
+ * Get axis position in px, that is the distance for the graph's
+ * top (left) edge for x (y) axes.
+ *
+ * @param {DOM element} gd
+ * @param {object} ax (full) axis object
+ * - {string} _id
+ * - {string} side
+ * if anchored:
+ * - {object} _anchorAxis
+ * Otherwise:
+ * - {number} position
+ * @return {number}
+ */
+axes.getPxPosition = function(gd, ax) {
var gs = gd._fullLayout._size;
var axLetter = ax._id.charAt(0);
var side = ax.side;
var anchorAxis;
if(ax.anchor !== 'free') {
- anchorAxis = axisIds.getFromId(gd, ax.anchor);
+ anchorAxis = ax._anchorAxis;
} else if(axLetter === 'x') {
anchorAxis = {
_offset: gs.t + (1 - (ax.position || 0)) * gs.h,
@@ -2680,7 +2675,7 @@ function getTitleOffset(gd, ax) {
} else if(side === 'bottom' || side === 'right') {
return anchorAxis._offset + anchorAxis._length;
}
-}
+};
function drawTitle(gd, ax) {
var fullLayout = gd._fullLayout;
@@ -2690,14 +2685,13 @@ function drawTitle(gd, ax) {
var titleStandoff;
if(ax.type === 'multicategory') {
- titleStandoff = ax._labelLength;
+ titleStandoff = ax._depth;
} else {
var offsetBase = 1.5;
titleStandoff = 10 + fontSize * offsetBase + (ax.linewidth ? ax.linewidth - 1 : 0);
}
- var titleOffset = getTitleOffset(gd, ax);
-
+ var pos = axes.getPxPosition(gd, ax);
var transform, x, y;
if(axLetter === 'x') {
@@ -2708,7 +2702,7 @@ function drawTitle(gd, ax) {
} else {
y = titleStandoff + fontSize * (ax.showticklabels ? 1.5 : 0.5);
}
- y += titleOffset;
+ y += pos;
} else {
y = ax._offset + ax._length / 2;
@@ -2717,7 +2711,7 @@ function drawTitle(gd, ax) {
} else {
x = -titleStandoff - fontSize * (ax.showticklabels ? 0.5 : 0);
}
- x += titleOffset;
+ x += pos;
transform = {rotate: '-90', offset: 0};
}
@@ -2866,6 +2860,9 @@ axes.allowAutoMargin = function(gd) {
var ax = axList[i];
if(ax.automargin) {
Plots.allowAutoMargin(gd, axAutoMarginID(ax));
+ if(ax.mirror) {
+ Plots.allowAutoMargin(gd, axMirrorAutoMarginID(ax));
+ }
}
if(Registry.getComponentMethod('rangeslider', 'isVisible')(ax)) {
Plots.allowAutoMargin(gd, rangeSliderAutoMarginID(ax));
@@ -2874,6 +2871,7 @@ axes.allowAutoMargin = function(gd) {
};
function axAutoMarginID(ax) { return ax._id + '.automargin'; }
+function axMirrorAutoMarginID(ax) { return axAutoMarginID(ax) + '.mirror'; }
function rangeSliderAutoMarginID(ax) { return ax._id + '.rangeslider'; }
// swap all the presentation attributes of the axes showing these traces
diff --git a/src/plots/plots.js b/src/plots/plots.js
index 705e8aa614d..fc7f66b0d6c 100644
--- a/src/plots/plots.js
+++ b/src/plots/plots.js
@@ -920,6 +920,26 @@ plots.linkSubplots = function(newFullData, newFullLayout, oldFullData, oldFullLa
ax._counterAxes.sort(axisIDs.idSort);
ax._subplotsWith.sort(Lib.subplotSort);
ax._mainSubplot = findMainSubplot(ax, newFullLayout);
+
+ // find "full" domain span of counter axes,
+ // this loop can be costly, so only compute it when required
+ if(ax._counterAxes.length && (
+ (ax.spikemode && ax.spikemode.indexOf('across') !== -1) ||
+ (ax.automargin && ax.mirror) ||
+ Registry.getComponentMethod('rangeslider', 'isVisible')(ax)
+ )) {
+ var min = 1;
+ var max = 0;
+ for(j = 0; j < ax._counterAxes.length; j++) {
+ var ax2 = axisIDs.getFromId(mockGd, ax._counterAxes[j]);
+ min = Math.min(min, ax2.domain[0]);
+ max = Math.max(max, ax2.domain[1]);
+ }
+ if(min < max) {
+ ax._counterDomainMin = min;
+ ax._counterDomainMax = max;
+ }
+ }
}
};
@@ -1645,7 +1665,6 @@ plots.purge = function(gd) {
fullLayout._glcontainer.remove();
fullLayout._glcanvas = null;
}
- if(fullLayout._geocontainer !== undefined) fullLayout._geocontainer.remove();
// remove modebar
if(fullLayout._modeBar) fullLayout._modeBar.destroy();
diff --git a/src/plots/polar/polar.js b/src/plots/polar/polar.js
index 1c450123545..7a1a5df463b 100644
--- a/src/plots/polar/polar.js
+++ b/src/plots/polar/polar.js
@@ -299,13 +299,7 @@ proto.updateLayout = function(fullLayout, polarLayout) {
};
proto.mockAxis = function(fullLayout, polarLayout, axLayout, opts) {
- var commonOpts = {
- // to get _boundingBox computation right when showticklabels is false
- anchor: 'free',
- position: 0
- };
-
- var ax = Lib.extendFlat(commonOpts, axLayout, opts);
+ var ax = Lib.extendFlat({}, axLayout, opts);
setConvertPolar(ax, polarLayout, fullLayout);
return ax;
};
diff --git a/test/image/baselines/automargin-mirror-all.png b/test/image/baselines/automargin-mirror-all.png
new file mode 100644
index 00000000000..cbd5ad2bb29
Binary files /dev/null and b/test/image/baselines/automargin-mirror-all.png differ
diff --git a/test/image/baselines/automargin-mirror-allticks.png b/test/image/baselines/automargin-mirror-allticks.png
new file mode 100644
index 00000000000..376b0a8efe8
Binary files /dev/null and b/test/image/baselines/automargin-mirror-allticks.png differ
diff --git a/test/image/baselines/automargin-multiline-titles.png b/test/image/baselines/automargin-multiline-titles.png
new file mode 100644
index 00000000000..3593c242148
Binary files /dev/null and b/test/image/baselines/automargin-multiline-titles.png differ
diff --git a/test/image/baselines/automargin-rangeslider-and-sidepush.png b/test/image/baselines/automargin-rangeslider-and-sidepush.png
new file mode 100644
index 00000000000..f107bc954df
Binary files /dev/null and b/test/image/baselines/automargin-rangeslider-and-sidepush.png differ
diff --git a/test/image/baselines/finance_multicategory.png b/test/image/baselines/finance_multicategory.png
index f7bc6c99e8c..c17e4282440 100644
Binary files a/test/image/baselines/finance_multicategory.png and b/test/image/baselines/finance_multicategory.png differ
diff --git a/test/image/baselines/finance_subplots_categories.png b/test/image/baselines/finance_subplots_categories.png
index 737202f3241..0d682eb9e86 100644
Binary files a/test/image/baselines/finance_subplots_categories.png and b/test/image/baselines/finance_subplots_categories.png differ
diff --git a/test/image/baselines/multicategory-inside-ticks.png b/test/image/baselines/multicategory-inside-ticks.png
index c01f1b712ef..d77fa4f1a2f 100644
Binary files a/test/image/baselines/multicategory-inside-ticks.png and b/test/image/baselines/multicategory-inside-ticks.png differ
diff --git a/test/image/baselines/multicategory-mirror.png b/test/image/baselines/multicategory-mirror.png
index c56a0dcd6ba..a91338dc6b8 100644
Binary files a/test/image/baselines/multicategory-mirror.png and b/test/image/baselines/multicategory-mirror.png differ
diff --git a/test/image/baselines/multicategory-sorting.png b/test/image/baselines/multicategory-sorting.png
index 421440aab83..190d0a09e2a 100644
Binary files a/test/image/baselines/multicategory-sorting.png and b/test/image/baselines/multicategory-sorting.png differ
diff --git a/test/image/baselines/multicategory-y.png b/test/image/baselines/multicategory-y.png
index 20e55ffa95b..05701726e9c 100644
Binary files a/test/image/baselines/multicategory-y.png and b/test/image/baselines/multicategory-y.png differ
diff --git a/test/image/baselines/multicategory2.png b/test/image/baselines/multicategory2.png
index d7c9115a674..03f9f4890c0 100644
Binary files a/test/image/baselines/multicategory2.png and b/test/image/baselines/multicategory2.png differ
diff --git a/test/image/baselines/multicategory_histograms.png b/test/image/baselines/multicategory_histograms.png
index a12bd2d0e64..7f8c4ac6f80 100644
Binary files a/test/image/baselines/multicategory_histograms.png and b/test/image/baselines/multicategory_histograms.png differ
diff --git a/test/image/baselines/range_slider_box.png b/test/image/baselines/range_slider_box.png
index ab16195a328..261eda9eaab 100644
Binary files a/test/image/baselines/range_slider_box.png and b/test/image/baselines/range_slider_box.png differ
diff --git a/test/image/baselines/range_slider_multiple.png b/test/image/baselines/range_slider_multiple.png
index 4d21f5ebaed..0822e6f721e 100644
Binary files a/test/image/baselines/range_slider_multiple.png and b/test/image/baselines/range_slider_multiple.png differ
diff --git a/test/image/baselines/violin_grouped_horz-multicategory.png b/test/image/baselines/violin_grouped_horz-multicategory.png
index b7ca2616b5d..4eee5c3bc69 100644
Binary files a/test/image/baselines/violin_grouped_horz-multicategory.png and b/test/image/baselines/violin_grouped_horz-multicategory.png differ
diff --git a/test/image/mocks/automargin-mirror-all.json b/test/image/mocks/automargin-mirror-all.json
new file mode 100644
index 00000000000..87de78e5a19
--- /dev/null
+++ b/test/image/mocks/automargin-mirror-all.json
@@ -0,0 +1,51 @@
+{
+ "data": [
+ {
+ "x": [1, 2, 3],
+ "y": [4, 5, 6]
+ },
+ {
+ "x": [20, 30, 40],
+ "y": [50, 60, 70],
+ "xaxis": "x2",
+ "yaxis": "y2"
+ }
+ ],
+ "layout": {
+ "showlegend": false,
+ "grid": {
+ "rows": 1,
+ "columns": 2,
+ "pattern": "independent"
+ },
+ "xaxis": {
+ "automargin": true,
+ "ticks": "outside",
+ "showline": true, "linewidth": 5,
+ "mirror": "all"
+ },
+ "xaxis2": {
+ "automargin": true,
+ "ticks": "outside",
+ "showline": true,
+ "mirror": "all"
+ },
+ "yaxis": {
+ "automargin": true,
+ "ticks": "outside",
+ "showline": true,
+ "zeroline": false,
+ "mirror": "all"
+ },
+ "yaxis2": {
+ "automargin": true,
+ "ticks": "outside",
+ "showline": true, "linewidth": 10,
+ "zeroline": false,
+ "mirror": "all"
+ },
+ "margin": {"l": 0, "b": 0, "t": 0, "r": 0},
+ "width": 500,
+ "height": 400
+ }
+}
diff --git a/test/image/mocks/automargin-mirror-allticks.json b/test/image/mocks/automargin-mirror-allticks.json
new file mode 100644
index 00000000000..d76fe3bd7f1
--- /dev/null
+++ b/test/image/mocks/automargin-mirror-allticks.json
@@ -0,0 +1,60 @@
+{
+ "data": [
+ {
+ "x": [1, 2, 3],
+ "y": [4, 5, 6]
+ },
+ {
+ "x": [20, 30, 40],
+ "y": [50, 60, 70],
+ "xaxis": "x2"
+ },
+ {
+ "x": [1, 2, 3],
+ "y": [4, 5, 6],
+ "yaxis": "y2"
+ },
+ {
+ "x": [20, 30, 40],
+ "y": [50, 60, 70],
+ "xaxis": "x2",
+ "yaxis": "y2"
+ }
+ ],
+ "layout": {
+ "showlegend": false,
+ "grid": {
+ "rows": 2,
+ "columns": 2
+ },
+ "xaxis": {
+ "automargin": true,
+ "ticks": "outside",
+ "showline": true,
+ "mirror": "all"
+ },
+ "xaxis2": {
+ "automargin": true,
+ "ticks": "outside", "ticklen": 10,
+ "showline": true,
+ "mirror": "allticks"
+ },
+ "yaxis": {
+ "automargin": true,
+ "ticks": "outside",
+ "showline": true,
+ "zeroline": false,
+ "mirror": "all"
+ },
+ "yaxis2": {
+ "automargin": true,
+ "ticks": "outside", "ticklen": 5,
+ "showline": true, "linewidth": 5,
+ "zeroline": false,
+ "mirror": "allticks"
+ },
+ "margin": {"l": 0, "b": 0, "t": 0, "r": 0},
+ "width": 500,
+ "height": 400
+ }
+}
diff --git a/test/image/mocks/automargin-multiline-titles.json b/test/image/mocks/automargin-multiline-titles.json
new file mode 100644
index 00000000000..544d029701e
--- /dev/null
+++ b/test/image/mocks/automargin-multiline-titles.json
@@ -0,0 +1,27 @@
+{
+ "data": [
+ {
+ "y": [1, 2, 1]
+ }
+ ],
+ "layout": {
+ "xaxis": {
+ "automargin": true,
+ "nticks": 3,
+ "tickfont": {"size": 30},
+ "title": {"text": "Hello
Bonjour"},
+ "zeroline": false
+ },
+ "yaxis": {
+ "automargin": true,
+ "title": {
+ "text": "Hello
Bonjour
Hola",
+ "font": {"size": 32}
+ },
+ "ticklen": 20
+ },
+ "width": 400,
+ "height": 400,
+ "margin": {"l": 0, "t": 0, "r": 0, "b": 0}
+ }
+}
diff --git a/test/image/mocks/automargin-rangeslider-and-sidepush.json b/test/image/mocks/automargin-rangeslider-and-sidepush.json
new file mode 100644
index 00000000000..cb941cdf94d
--- /dev/null
+++ b/test/image/mocks/automargin-rangeslider-and-sidepush.json
@@ -0,0 +1,24 @@
+{
+ "data": [
+ {
+ "x": ["a", "b", "c", "d", "long category", "another even longer", "the longest one yet!!!!!!"],
+ "y": [0, 10, 20, 30, 40, 50, 60]
+ }
+ ],
+ "layout": {
+ "xaxis": {
+ "title": {
+ "text": "Bottom X Axis
2nd line",
+ "font": {"size": 14}
+ },
+ "rangeslider": { "visible": true },
+ "automargin": true
+ },
+ "yaxis": {
+ "automargin": true
+ },
+ "width": 400,
+ "height": 400,
+ "margin": {"l": 0, "t": 0, "b": 0, "r": 0}
+ }
+}
diff --git a/test/image/mocks/multicategory2.json b/test/image/mocks/multicategory2.json
index 8f068be2077..5fbb13a1b96 100644
--- a/test/image/mocks/multicategory2.json
+++ b/test/image/mocks/multicategory2.json
@@ -25,10 +25,12 @@
],
"layout": {
"xaxis": {
- "title": "MULTI-CATEGORY ON TOP",
+ "title": {"text": "MULTI-CATEGORY ON TOP"},
"side": "top",
- "automargin": true,
- "tickson": "labels"
+ "tickson": "labels",
+ "ticks": "outside",
+ "ticklen": 25,
+ "automargin": true
},
"showlegend": false,
"width": 400,
diff --git a/test/jasmine/tests/hover_spikeline_test.js b/test/jasmine/tests/hover_spikeline_test.js
index 791dc28b738..80823cb07f5 100644
--- a/test/jasmine/tests/hover_spikeline_test.js
+++ b/test/jasmine/tests/hover_spikeline_test.js
@@ -156,6 +156,120 @@ describe('spikeline hover', function() {
.then(done);
});
+ it('draws lines up to x-axis position', function(done) {
+ Plotly.plot(gd, [
+ { y: [1, 2, 1] },
+ { y: [2, 1, 2], yaxis: 'y2' }
+ ], {
+ // here the x-axis is drawn at the middle of the graph
+ xaxis: { showspike: true, spikemode: 'toaxis' },
+ yaxis: { domain: [0.5, 1] },
+ yaxis2: { anchor: 'x', domain: [0, 0.5] },
+ width: 400,
+ height: 400
+ })
+ .then(function() {
+ _hover({xval: 1, yval: 2});
+ // from "y" of x-axis up to "y" of pt
+ _assert([[189, 210.5, 189, 109.25]], []);
+ })
+ .then(function() { return Plotly.relayout(gd, 'xaxis.spikemode', 'across'); })
+ .then(function() {
+ _hover({xval: 1, yval: 2});
+ // from "y" of xy subplot top, down to "y" xy2 subplot bottom
+ _assert([[189, 100, 189, 320]], []);
+ })
+ .catch(failTest)
+ .then(done);
+ });
+
+ it('draws lines up to y-axis position - anchor free case', function(done) {
+ Plotly.plot(gd, [
+ { y: [1, 2, 1] },
+ { y: [2, 1, 2], xaxis: 'x2' }
+ ], {
+ yaxis: { domain: [0.5, 1] },
+ xaxis2: {
+ anchor: 'free', position: 0, overlaying: 'x',
+ showspikes: true, spikemode: 'across'
+ },
+ width: 400,
+ height: 400,
+ showlegend: false
+ })
+ .then(function() {
+ _hover({xval: 0, yval: 2}, 'x2y');
+ // from "y" of pt, down to "y" of x2 axis
+ _assert([[95.75, 100, 95.75, 320]], []);
+ })
+ .then(function() { return Plotly.relayout(gd, 'xaxis2.position', 0.6); })
+ .then(function() {
+ _hover({xval: 0, yval: 2}, 'x2y');
+ // from "y" of pt, down to "y" of x axis (which is further down)
+ _assert([[95.75, 100, 95.75, 210]], []);
+ })
+ .catch(failTest)
+ .then(done);
+ });
+
+ it('draws lines up to y-axis position', function(done) {
+ Plotly.plot(gd, [
+ { y: [1, 2, 1] },
+ { y: [2, 1, 2], xaxis: 'x2' }
+ ], {
+ // here the y-axis is drawn at the middle of the graph,
+ // with xy subplot to the right and xy2 to the left
+ yaxis: { showspike: true, spikemode: 'toaxis' },
+ xaxis: { domain: [0.5, 1] },
+ xaxis2: { anchor: 'y', domain: [0, 0.5] },
+ width: 400,
+ height: 400,
+ showlegend: false
+ })
+ .then(function() {
+ _hover({xval: 1, yval: 2});
+ // from "x" of y-axis to "x" of pt
+ _assert([[199.5, 114.75, 260, 114.75]], []);
+ })
+ .then(function() { return Plotly.relayout(gd, 'yaxis.spikemode', 'across'); })
+ .then(function() {
+ _hover({xval: 1, yval: 2});
+ // from "x" at xy2 subplot left, to "x" at xy subplot right
+ _assert([[80, 114.75, 320, 114.75]], []);
+ })
+ .catch(failTest)
+ .then(done);
+ });
+
+ it('draws lines up to y-axis position - anchor free case', function(done) {
+ Plotly.plot(gd, [
+ { y: [1, 2, 1] },
+ { y: [2, 1, 2], yaxis: 'y2' }
+ ], {
+ xaxis: { domain: [0.5, 1] },
+ yaxis2: {
+ anchor: 'free', position: 0, overlaying: 'y',
+ showspikes: true, spikemode: 'across'
+ },
+ width: 400,
+ height: 400,
+ showlegend: false
+ })
+ .then(function() {
+ _hover({xval: 0, yval: 2}, 'xy2');
+ // from "x" of y2 axis to "x" of pt
+ _assert([[80, 114.75, 320, 114.75]], []);
+ })
+ .then(function() { return Plotly.relayout(gd, 'yaxis2.position', 0.6); })
+ .then(function() {
+ _hover({xval: 0, yval: 2}, 'xy2');
+ // from "x" of y axis (which is further left) to "x" of pt
+ _assert([[200, 114.75, 320, 114.75]], []);
+ })
+ .catch(failTest)
+ .then(done);
+ });
+
it('draws lines and markers on enabled axes in the spikesnap "cursor" mode', function(done) {
var _mock = makeMock('toaxis', 'x');