diff --git a/src/plot_api/subroutines.js b/src/plot_api/subroutines.js index 15aa35162a5..e36be0c3ed0 100644 --- a/src/plot_api/subroutines.js +++ b/src/plot_api/subroutines.js @@ -221,183 +221,11 @@ function lsInner(gd) { } } - var xLinesXLeft, xLinesXRight, xLinesYBottom, xLinesYTop, - leftYLineWidth, rightYLineWidth; - var yLinesYBottom, yLinesYTop, yLinesXLeft, yLinesXRight, - connectYBottom, connectYTop; - var extraSubplot; - - function xLinePath(y) { - return 'M' + xLinesXLeft + ',' + y + 'H' + xLinesXRight; - } - - function xLinePathFree(y) { - return 'M' + xa._offset + ',' + y + 'h' + xa._length; - } - - function yLinePath(x) { - return 'M' + x + ',' + yLinesYTop + 'V' + yLinesYBottom; - } - - function yLinePathFree(x) { - if(typeof(ya.shift) === 'number') { - x += ya.shift; - } else if(ya._shift !== undefined) { - x += ya._shift; - } - return 'M' + x + ',' + ya._offset + 'v' + ya._length; - } - - function mainPath(ax, pathFn, pathFnFree) { - if(!ax.showline || subplot !== ax._mainSubplot) return ''; - if(!ax._anchorAxis) return pathFnFree(ax._mainLinePosition); - var out = pathFn(ax._mainLinePosition); - if(ax.mirror) out += pathFn(ax._mainMirrorPosition); - return out; - } - - for(subplot in fullLayout._plots) { - plotinfo = fullLayout._plots[subplot]; - xa = plotinfo.xaxis; - ya = plotinfo.yaxis; - - /* - * x lines get longer where they meet y lines, to make a crisp corner. - * The x lines get the padding (margin.pad) plus the y line width to - * fill up the corner nicely. Free x lines are excluded - they always - * span exactly the data area of the plot - * - * | XXXXX - * | XXXXX - * | - * +------ - * x1 - * ----- - * x2 - */ - var xPath = 'M0,0'; - if(shouldShowLinesOrTicks(xa, subplot)) { - leftYLineWidth = findCounterAxisLineWidth(xa, 'left', ya, axList); - xLinesXLeft = xa._offset - (leftYLineWidth ? (pad + leftYLineWidth) : 0); - rightYLineWidth = findCounterAxisLineWidth(xa, 'right', ya, axList); - xLinesXRight = xa._offset + xa._length + (rightYLineWidth ? (pad + rightYLineWidth) : 0); - xLinesYBottom = getLinePosition(xa, ya, 'bottom'); - xLinesYTop = getLinePosition(xa, ya, 'top'); - - // save axis line positions for extra ticks to reference - // each subplot that gets ticks from "allticks" gets an entry: - // [left or bottom, right or top] - extraSubplot = (!xa._anchorAxis || subplot !== xa._mainSubplot); - if(extraSubplot && (xa.mirror === 'allticks' || xa.mirror === 'all')) { - xa._linepositions[subplot] = [xLinesYBottom, xLinesYTop]; - } - - xPath = mainPath(xa, xLinePath, xLinePathFree); - if(extraSubplot && xa.showline && (xa.mirror === 'all' || xa.mirror === 'allticks')) { - xPath += xLinePath(xLinesYBottom) + xLinePath(xLinesYTop); - } - - plotinfo.xlines - .style('stroke-width', xa._lw + 'px') - .call(Color.stroke, xa.showline ? - xa.linecolor : 'rgba(0,0,0,0)'); - } - plotinfo.xlines.attr('d', xPath); - - /* - * y lines that meet x axes get longer only by margin.pad, because - * the x axes fill in the corner space. Free y axes, like free x axes, - * always span exactly the data area of the plot - * - * | | XXXX - * y2| y1| XXXX - * | | XXXX - * | - * +----- - */ - var yPath = 'M0,0'; - if(shouldShowLinesOrTicks(ya, subplot)) { - connectYBottom = findCounterAxisLineWidth(ya, 'bottom', xa, axList); - yLinesYBottom = ya._offset + ya._length + (connectYBottom ? pad : 0); - connectYTop = findCounterAxisLineWidth(ya, 'top', xa, axList); - yLinesYTop = ya._offset - (connectYTop ? pad : 0); - yLinesXLeft = getLinePosition(ya, xa, 'left'); - yLinesXRight = getLinePosition(ya, xa, 'right'); - - extraSubplot = (!ya._anchorAxis || subplot !== ya._mainSubplot); - if(extraSubplot && (ya.mirror === 'allticks' || ya.mirror === 'all')) { - ya._linepositions[subplot] = [yLinesXLeft, yLinesXRight]; - } - - yPath = mainPath(ya, yLinePath, yLinePathFree); - if(extraSubplot && ya.showline && (ya.mirror === 'all' || ya.mirror === 'allticks')) { - yPath += yLinePath(yLinesXLeft) + yLinePath(yLinesXRight); - } - - plotinfo.ylines - .style('stroke-width', ya._lw + 'px') - .call(Color.stroke, ya.showline ? - ya.linecolor : 'rgba(0,0,0,0)'); - } - plotinfo.ylines.attr('d', yPath); - } - Axes.makeClipPaths(gd); return Plots.previousPromises(gd); } -function shouldShowLinesOrTicks(ax, subplot) { - return (ax.ticks || ax.showline) && - (subplot === ax._mainSubplot || ax.mirror === 'all' || ax.mirror === 'allticks'); -} - -/* - * should we draw a line on counterAx at this side of ax? - * It's assumed that counterAx is known to overlay the subplot we're working on - * but it may not be its main axis. - */ -function shouldShowLineThisSide(ax, side, counterAx) { - // does counterAx get a line at all? - if(!counterAx.showline || !counterAx._lw) return false; - - // are we drawing *all* lines for counterAx? - if(counterAx.mirror === 'all' || counterAx.mirror === 'allticks') return true; - - var anchorAx = counterAx._anchorAxis; - - // is this a free axis? free axes can only have a subplot side-line with all(ticks)? mirroring - if(!anchorAx) return false; - - // in order to handle cases where the user forgot to anchor this axis correctly - // (because its default anchor has the same domain on the relevant end) - // check whether the relevant position is the same. - var sideIndex = alignmentConstants.FROM_BL[side]; - if(counterAx.side === side) { - return anchorAx.domain[sideIndex] === ax.domain[sideIndex]; - } - return counterAx.mirror && anchorAx.domain[1 - sideIndex] === ax.domain[1 - sideIndex]; -} - -/* - * Is there another axis intersecting `side` end of `ax`? - * First look at `counterAx` (the axis for this subplot), - * then at all other potential counteraxes on or overlaying this subplot. - * Take the line width from the first one that has a line. - */ -function findCounterAxisLineWidth(ax, side, counterAx, axList) { - if(shouldShowLineThisSide(ax, side, counterAx)) { - return counterAx._lw; - } - for(var i = 0; i < axList.length; i++) { - var axi = axList[i]; - if(axi._mainAxis === counterAx._mainAxis && shouldShowLineThisSide(ax, side, axi)) { - return axi._lw; - } - } - return 0; -} - exports.drawMainTitle = function(gd) { var fullLayout = gd._fullLayout; diff --git a/src/plots/cartesian/axes.js b/src/plots/cartesian/axes.js index ae0d56e8cbd..a34bd0611b0 100644 --- a/src/plots/cartesian/axes.js +++ b/src/plots/cartesian/axes.js @@ -2526,6 +2526,132 @@ axes.drawOne = function(gd, ax, opts) { } } + var xLinesXLeft, xLinesXRight, xLinesYBottom, xLinesYTop, + leftYLineWidth, rightYLineWidth; + var yLinesYBottom, yLinesYTop, yLinesXLeft, yLinesXRight, + connectYBottom, connectYTop; + var extraSubplot; + + var axList = axes.list(gd, '', true); + + function xLinePath(y) { + return 'M' + xLinesXLeft + ',' + y + 'H' + xLinesXRight; + } + + function xLinePathFree(y) { + return 'M' + xa._offset + ',' + y + 'h' + xa._length; + } + + function yLinePath(x) { + return 'M' + x + ',' + yLinesYTop + 'V' + yLinesYBottom; + } + + function yLinePathFree(x) { + if(typeof(ya.shift) === 'number') { + x += ya.shift; + } else if(ya._shift !== undefined) { + x += ya._shift; + } + return 'M' + x + ',' + ya._offset + 'v' + ya._length; + } + + for(var subplot in fullLayout._plots) { + var plotinfo = fullLayout._plots[subplot]; + var pad = gd._fullLayout._size.p + var xa = plotinfo.xaxis; + var ya = plotinfo.yaxis; + + + if(axLetter === 'y') { + if(ax._id === ya._id) { + /* + * y lines that meet x axes get longer only by margin.pad, because + * the x axes fill in the corner space. Free y axes, like free x axes, + * always span exactly the data area of the plot + * + * | | XXXX + * y2| y1| XXXX + * | | XXXX + * | + * +----- + */ + var yPath = 'M0,0'; + if(shouldShowLinesOrTicks(ya, subplot)) { + connectYBottom = findCounterAxisLineWidth(ya, 'bottom', xa, axList); + yLinesYBottom = ya._offset + ya._length + (connectYBottom ? pad : 0); + connectYTop = findCounterAxisLineWidth(ya, 'top', xa, axList); + yLinesYTop = ya._offset - (connectYTop ? pad : 0); + yLinesXLeft = getLinePosition(gd, ya, xa, 'left'); + yLinesXRight = getLinePosition(gd, ya, xa, 'right'); + + extraSubplot = (!ya._anchorAxis || subplot !== ya._mainSubplot); + if(extraSubplot && (ya.mirror === 'allticks' || ya.mirror === 'all')) { + ya._linepositions[subplot] = [yLinesXLeft, yLinesXRight]; + } + + yPath = mainPath(ya, yLinePath, yLinePathFree, subplot); + if(extraSubplot && ya.showline && (ya.mirror === 'all' || ya.mirror === 'allticks')) { + yPath += yLinePath(yLinesXLeft) + yLinePath(yLinesXRight); + } + + plotinfo.ylines + .style('stroke-width', ya._lw + 'px') + .call(Color.stroke, ya.showline ? + ya.linecolor : 'rgba(0,0,0,0)'); + } + plotinfo.ylines.attr('d', yPath); + break; + } + } else if(axLetter === 'x') { + if(ax._id === xa._id) { + /* + * x lines get longer where they meet y lines, to make a crisp corner. + * The x lines get the padding (margin.pad) plus the y line width to + * fill up the corner nicely. Free x lines are excluded - they always + * span exactly the data area of the plot + * + * | XXXXX + * | XXXXX + * | + * +------ + * x1 + * ----- + * x2 + */ + var xPath = 'M0,0'; + if(shouldShowLinesOrTicks(xa, subplot)) { + leftYLineWidth = findCounterAxisLineWidth(xa, 'left', ya, axList); + xLinesXLeft = xa._offset - (leftYLineWidth ? (pad + leftYLineWidth) : 0); + rightYLineWidth = findCounterAxisLineWidth(xa, 'right', ya, axList); + xLinesXRight = xa._offset + xa._length + (rightYLineWidth ? (pad + rightYLineWidth) : 0); + xLinesYBottom = getLinePosition(gd, xa, ya, 'bottom'); + xLinesYTop = getLinePosition(gd, xa, ya, 'top'); + + // save axis line positions for extra ticks to reference + // each subplot that gets ticks from "allticks" gets an entry: + // [left or bottom, right or top] + extraSubplot = (!xa._anchorAxis || subplot !== xa._mainSubplot); + if(extraSubplot && (xa.mirror === 'allticks' || xa.mirror === 'all')) { + xa._linepositions[subplot] = [xLinesYBottom, xLinesYTop]; + } + + xPath = mainPath(xa, xLinePath, xLinePathFree, subplot); + if(extraSubplot && xa.showline && (xa.mirror === 'all' || xa.mirror === 'allticks')) { + xPath += xLinePath(xLinesYBottom) + xLinePath(xLinesYTop); + } + + plotinfo.xlines + .style('stroke-width', xa._lw + 'px') + .call(Color.stroke, xa.showline ? + xa.linecolor : 'rgba(0,0,0,0)'); + } + plotinfo.xlines.attr('d', xPath); + break; + } + } + + } + var seq = []; // tick labels - for now just the main labels. @@ -4292,3 +4418,80 @@ function setShiftVal(ax, axShifts) { axShifts[ax.overlaying][ax.side] : (ax.shift || 0); } + +function mainPath(ax, pathFn, pathFnFree, subplot) { + if(!ax.showline || subplot !== ax._mainSubplot) return ''; + if(!ax._anchorAxis) return pathFnFree(ax._mainLinePosition); + var out = pathFn(ax._mainLinePosition); + if(ax.mirror) out += pathFn(ax._mainMirrorPosition); + return out; +} + +function shouldShowLinesOrTicks(ax, subplot) { + return (ax.ticks || ax.showline) && + (subplot === ax._mainSubplot || ax.mirror === 'all' || ax.mirror === 'allticks'); +} + +/* + * Is there another axis intersecting `side` end of `ax`? + * First look at `counterAx` (the axis for this subplot), + * then at all other potential counteraxes on or overlaying this subplot. + * Take the line width from the first one that has a line. + */ +function findCounterAxisLineWidth(ax, side, counterAx, axList) { + if(shouldShowLineThisSide(ax, side, counterAx)) { + return counterAx._lw; + } + for(var i = 0; i < axList.length; i++) { + var axi = axList[i]; + if(axi._mainAxis === counterAx._mainAxis && shouldShowLineThisSide(ax, side, axi)) { + return axi._lw; + } + } + return 0; +} + + +/* + * should we draw a line on counterAx at this side of ax? + * It's assumed that counterAx is known to overlay the subplot we're working on + * but it may not be its main axis. + */ +function shouldShowLineThisSide(ax, side, counterAx) { + // does counterAx get a line at all? + if(!counterAx.showline || !counterAx._lw) return false; + + // are we drawing *all* lines for counterAx? + if(counterAx.mirror === 'all' || counterAx.mirror === 'allticks') return true; + + var anchorAx = counterAx._anchorAxis; + + // is this a free axis? free axes can only have a subplot side-line with all(ticks)? mirroring + if(!anchorAx) return false; + + // in order to handle cases where the user forgot to anchor this axis correctly + // (because its default anchor has the same domain on the relevant end) + // check whether the relevant position is the same. + var sideIndex = alignmentConstants.FROM_BL[side]; + if(counterAx.side === side) { + return anchorAx.domain[sideIndex] === ax.domain[sideIndex]; + } + return counterAx.mirror && anchorAx.domain[1 - sideIndex] === ax.domain[1 - sideIndex]; +} + +// TODO: This is repeated from plot_api/subroutines.js +function getLinePosition(gd, ax, counterAx, side) { + var lwHalf = ax._lw / 2; + var gs = gd._fullLayout._size; + var pad = gs.p; + + if(ax._id.charAt(0) === 'x') { + if(!counterAx) return gs.t + gs.h * (1 - (ax.position || 0)) + (lwHalf % 1); + else if(side === 'top') return counterAx._offset - pad - lwHalf; + return counterAx._offset + counterAx._length + pad + lwHalf; + } + + if(!counterAx) return gs.l + gs.w * (ax.position || 0) + (lwHalf % 1); + else if(side === 'right') return counterAx._offset + counterAx._length + pad + lwHalf; + return counterAx._offset - pad - lwHalf; +} \ No newline at end of file diff --git a/test/image/mocks/zz-mult-yaxes-no-redraw.json b/test/image/mocks/zz-mult-yaxes-no-redraw.json new file mode 100644 index 00000000000..0481dd54b47 --- /dev/null +++ b/test/image/mocks/zz-mult-yaxes-no-redraw.json @@ -0,0 +1,96 @@ +{ + "data": [ + { + "x": [ + 1, + 2, + 3 + ], + "y": [ + 4, + 5, + 6 + ], + "name": "yaxis1 data", + "type": "scatter" + }, + { + "x": [ + 2, + 3, + 4 + ], + "y": [ + 4, + 5, + 6 + ], + "name": "yaxis2 data", + "yaxis": "y2", + "type": "scatter" + }, + { + "x": [ + 3, + 4, + 5 + ], + "y": [ + 4, + 5, + 6 + ], + "name": "yaxis3 data", + "yaxis": "y3", + "type": "scatter" + }, + { + "x": [ + 4, + 5, + 6 + ], + "y": [ + 1, + 2, + 3 + ], + "name": "yaxis4 data", + "yaxis": "y4", + "type": "scatter" + } + ], + "layout": { + "title": { + "text": "multiple y-axes example" + }, + "xaxis": {"domain": [0.25, 0.75]}, + "width": 800, + "yaxis": { + "showline": true, + "title": {"text": "axis 1"} + }, + "yaxis2": { + "overlaying": "y", + "showline": true, + "side": "right", + "title": {"text": "axis 2"} + }, + "yaxis3": { + "anchor": "free", + "overlaying": "y", + "showline": true, + "shift": true, + "side": "right", + "title": {"text": "axis 3"} + }, + "yaxis4": { + "anchor": "free", + "overlaying": "y", + "showline": true, + "shift": true, + "side": "left", + "title": {"text": "axis 4"} + } + } +} diff --git a/test/image/mocks/zz-mult-yaxes-subplots-stacked.json b/test/image/mocks/zz-mult-yaxes-subplots-stacked.json index 1922e2a1831..38f9025f650 100644 --- a/test/image/mocks/zz-mult-yaxes-subplots-stacked.json +++ b/test/image/mocks/zz-mult-yaxes-subplots-stacked.json @@ -83,17 +83,19 @@ }, "grid": {"rows": 2, "columns": 1, "pattern": "independent"}, "width": 800, - "yaxis": {"showline": true, "title": {"text": "yaxis title"}}, + "yaxis": {"showline": true, "title": {"text": "yaxis title"}, "mirror": true}, "yaxis2": { "title": {"text": "yaxis2 title"}, - "showline": true + "showline": true, + "mirror": true }, "yaxis3": { "title": {"text": "yaxis3 title"}, "anchor": "free", "overlaying": "y", "showline": true, - "shift": true + "shift": true, + "mirror": true }, "yaxis4": { "title": {"text": "yaxis4 title"},