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');