diff --git a/src/components/legend/defaults.js b/src/components/legend/defaults.js index ada758b9955..5b653c02884 100644 --- a/src/components/legend/defaults.js +++ b/src/components/legend/defaults.js @@ -86,7 +86,7 @@ module.exports = function legendDefaults(layoutIn, layoutOut, fullData) { coerce('orientation'); if(containerOut.orientation === 'h') { var xaxis = layoutIn.xaxis; - if(xaxis && xaxis.rangeslider && xaxis.rangeslider.visible) { + if(Registry.getComponentMethod('rangeslider', 'isVisible')(xaxis)) { defaultX = 0; defaultXAnchor = 'left'; defaultY = 1.1; diff --git a/src/components/rangeslider/draw.js b/src/components/rangeslider/draw.js index 76ef7a04a78..1a7fe176df6 100644 --- a/src/components/rangeslider/draw.js +++ b/src/components/rangeslider/draw.js @@ -19,7 +19,7 @@ var Color = require('../color'); var Titles = require('../titles'); var Cartesian = require('../../plots/cartesian'); -var Axes = require('../../plots/cartesian/axes'); +var axisIDs = require('../../plots/cartesian/axis_ids'); var dragElement = require('../dragelement'); var setCursor = require('../../lib/setcursor'); @@ -27,8 +27,13 @@ var setCursor = require('../../lib/setcursor'); var constants = require('./constants'); module.exports = function(gd) { - var fullLayout = gd._fullLayout, - rangeSliderData = makeRangeSliderData(fullLayout); + var fullLayout = gd._fullLayout; + var rangeSliderData = fullLayout._rangeSliderData; + for(var i = 0; i < rangeSliderData.length; i++) { + var opts = rangeSliderData[i][constants.name]; + // fullLayout._uid may not exist when we call makeData + opts._clipId = opts._id + '-' + fullLayout._uid; + } /* * @@ -55,10 +60,6 @@ module.exports = function(gd) { .selectAll('g.' + constants.containerClassName) .data(rangeSliderData, keyFunction); - rangeSliders.enter().append('g') - .classed(constants.containerClassName, true) - .attr('pointer-events', 'all'); - // remove exiting sliders and their corresponding clip paths rangeSliders.exit().each(function(axisOpts) { var opts = axisOpts[constants.name]; @@ -68,12 +69,16 @@ module.exports = function(gd) { // return early if no range slider is visible if(rangeSliderData.length === 0) return; + rangeSliders.enter().append('g') + .classed(constants.containerClassName, true) + .attr('pointer-events', 'all'); + // for all present range sliders rangeSliders.each(function(axisOpts) { - var rangeSlider = d3.select(this), - opts = axisOpts[constants.name], - oppAxisOpts = fullLayout[Axes.id2name(axisOpts.anchor)], - oppAxisRangeOpts = opts[Axes.id2name(axisOpts.anchor)]; + var rangeSlider = d3.select(this); + var opts = axisOpts[constants.name]; + var oppAxisOpts = fullLayout[axisIDs.id2name(axisOpts.anchor)]; + var oppAxisRangeOpts = opts[axisIDs.id2name(axisOpts.anchor)]; // update range // Expand slider range to the axis range @@ -97,19 +102,9 @@ module.exports = function(gd) { var domain = axisOpts.domain; var tickHeight = (axisOpts._boundingBox || {}).height || 0; - var oppBottom = Infinity; - var subplotData = Axes.getSubplots(gd, axisOpts); - for(var i = 0; i < subplotData.length; i++) { - var oppAxis = Axes.getFromId(gd, subplotData[i].substr(subplotData[i].indexOf('y'))); - oppBottom = Math.min(oppBottom, oppAxis.domain[0]); - } - - opts._id = constants.name + axisOpts._id; - opts._clipId = opts._id + '-' + fullLayout._uid; + var oppBottom = opts._oppBottom; opts._width = graphSize.w * (domain[1] - domain[0]); - opts._height = (fullLayout.height - margin.b - margin.t) * opts.thickness; - opts._offsetShift = Math.floor(opts.borderwidth / 2); var x = Math.round(margin.l + (graphSize.w * domain[0])); @@ -177,36 +172,9 @@ module.exports = function(gd) { } }); } - - // update margins - Plots.autoMargin(gd, opts._id, { - x: domain[0], - y: oppBottom, - l: 0, - r: 0, - t: 0, - b: opts._height + margin.b + tickHeight, - pad: constants.extraPad + opts._offsetShift * 2 - }); }); }; -function makeRangeSliderData(fullLayout) { - var axes = Axes.list({ _fullLayout: fullLayout }, 'x', true), - name = constants.name, - out = []; - - if(fullLayout._has('gl2d')) return out; - - for(var i = 0; i < axes.length; i++) { - var ax = axes[i]; - - if(ax[name] && ax[name].visible) out.push(ax); - } - - return out; -} - function setupDragElement(rangeSlider, gd, axisOpts, opts) { var slideBox = rangeSlider.select('rect.' + constants.slideBoxClassName).node(), grabAreaMin = rangeSlider.select('rect.' + constants.grabAreaMinClassName).node(), @@ -393,11 +361,10 @@ function addClipPath(rangeSlider, gd, axisOpts, opts) { } function drawRangePlot(rangeSlider, gd, axisOpts, opts) { - var subplotData = Axes.getSubplots(gd, axisOpts), - calcData = gd.calcdata; + var calcData = gd.calcdata; var rangePlots = rangeSlider.selectAll('g.' + constants.rangePlotClassName) - .data(subplotData, Lib.identity); + .data(axisOpts._subplotsWith, Lib.identity); rangePlots.enter().append('g') .attr('class', function(id) { return constants.rangePlotClassName + ' ' + id; }) @@ -413,7 +380,7 @@ function drawRangePlot(rangeSlider, gd, axisOpts, opts) { var plotgroup = d3.select(this), isMainPlot = (i === 0); - var oppAxisOpts = Axes.getFromId(gd, id, 'y'), + var oppAxisOpts = axisIDs.getFromId(gd, id, 'y'), oppAxisName = oppAxisOpts._name, oppAxisRangeOpts = opts[oppAxisName]; @@ -445,6 +412,11 @@ function drawRangePlot(rangeSlider, gd, axisOpts, opts) { var xa = mockFigure._fullLayout.xaxis; var ya = mockFigure._fullLayout[oppAxisName]; + xa.clearCalc(); + xa.setScale(); + ya.clearCalc(); + ya.setScale(); + var plotinfo = { id: id, plotgroup: plotgroup, diff --git a/src/components/rangeslider/helpers.js b/src/components/rangeslider/helpers.js new file mode 100644 index 00000000000..6009d0d51a1 --- /dev/null +++ b/src/components/rangeslider/helpers.js @@ -0,0 +1,67 @@ +/** +* Copyright 2012-2018, 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'; + +var axisIDs = require('../../plots/cartesian/axis_ids'); +var constants = require('./constants'); +var name = constants.name; + +function isVisible(ax) { + var rangeSlider = ax && ax[name]; + return rangeSlider && rangeSlider.visible; +} +exports.isVisible = isVisible; + +exports.makeData = function(fullLayout) { + var axes = axisIDs.list({ _fullLayout: fullLayout }, 'x', true); + var margin = fullLayout.margin; + var rangeSliderData = []; + + if(!fullLayout._has('gl2d')) { + for(var i = 0; i < axes.length; i++) { + var ax = axes[i]; + + if(isVisible(ax)) { + rangeSliderData.push(ax); + + var opts = ax[name]; + opts._id = name + ax._id; + opts._height = (fullLayout.height - margin.b - margin.t) * opts.thickness; + opts._offsetShift = Math.floor(opts.borderwidth / 2); + } + } + } + + fullLayout._rangeSliderData = rangeSliderData; +}; + +exports.autoMarginOpts = function(gd, ax) { + var opts = ax[name]; + + 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]); + } + opts._oppBottom = oppBottom; + + var tickHeight = (ax.side === 'bottom' && ax._boundingBox.height) || 0; + + return { + x: 0, + y: oppBottom, + l: 0, + r: 0, + t: 0, + b: opts._height + gd._fullLayout.margin.b + tickHeight, + pad: constants.extraPad + opts._offsetShift * 2 + }; +}; diff --git a/src/components/rangeslider/index.js b/src/components/rangeslider/index.js index 2983d72c58e..fd3395ff114 100644 --- a/src/components/rangeslider/index.js +++ b/src/components/rangeslider/index.js @@ -11,6 +11,7 @@ var Lib = require('../../lib'); var attrs = require('./attributes'); var oppAxisAttrs = require('./oppaxis_attributes'); +var helpers = require('./helpers'); module.exports = { moduleType: 'component', @@ -29,5 +30,8 @@ module.exports = { layoutAttributes: require('./attributes'), handleDefaults: require('./defaults'), calcAutorange: require('./calc_autorange'), - draw: require('./draw') + draw: require('./draw'), + isVisible: helpers.isVisible, + makeData: helpers.makeData, + autoMarginOpts: helpers.autoMarginOpts }; diff --git a/src/plot_api/plot_api.js b/src/plot_api/plot_api.js index 828797411f2..d95a31ca72e 100644 --- a/src/plot_api/plot_api.js +++ b/src/plot_api/plot_api.js @@ -332,8 +332,7 @@ exports.plot = function(gd, data, layout, config) { return Lib.syncOrAsync([ Registry.getComponentMethod('shapes', 'calcAutorange'), Registry.getComponentMethod('annotations', 'calcAutorange'), - doAutoRangeAndConstraints, - Registry.getComponentMethod('rangeslider', 'calcAutorange') + doAutoRangeAndConstraints ], gd); } @@ -345,6 +344,11 @@ exports.plot = function(gd, data, layout, config) { // store initial ranges *after* enforcing constraints, otherwise // we will never look like we're at the initial ranges if(graphWasEmpty) Axes.saveRangeInitial(gd); + + // this one is different from shapes/annotations calcAutorange + // the others incorporate those components into ax._extremes, + // this one actually sets the ranges in rangesliders. + Registry.getComponentMethod('rangeslider', 'calcAutorange')(gd); } // draw ticks, titles, and calculate axis scaling (._b, ._m) diff --git a/src/plot_api/subroutines.js b/src/plot_api/subroutines.js index 68874a4eae7..3033462e785 100644 --- a/src/plot_api/subroutines.js +++ b/src/plot_api/subroutines.js @@ -55,7 +55,7 @@ function lsInner(gd) { var gs = fullLayout._size; var pad = gs.p; var axList = Axes.list(gd, '', true); - var i, subplot, plotinfo, xa, ya; + var i, subplot, plotinfo, ax, xa, ya; fullLayout._paperdiv.style({ width: (gd._context.responsive && fullLayout.autosize && !gd._context._hasZeroWidth && !gd.layout.width) ? '100%' : fullLayout.width + 'px', @@ -91,10 +91,7 @@ function lsInner(gd) { // some preparation of axis position info for(i = 0; i < axList.length; i++) { - var ax = axList[i]; - - // reset scale in case the margins have changed - ax.setScale(); + ax = axList[i]; var counterAx = ax._anchorAxis; @@ -113,11 +110,6 @@ function lsInner(gd) { ax._mainMirrorPosition = (ax.mirror && counterAx) ? getLinePosition(ax, counterAx, alignmentConstants.OPPOSITE_SIDE[ax.side]) : null; - - // Figure out which subplot to draw ticks, labels, & axis lines on - // do this as a separate loop so we already have all the - // _mainAxis and _anchorAxis links set - ax._mainSubplot = findMainSubplot(ax, fullLayout); } // figure out which backgrounds we need to draw, @@ -358,48 +350,6 @@ function lsInner(gd) { return gd._promises.length && Promise.all(gd._promises); } -function findMainSubplot(ax, fullLayout) { - var subplotList = fullLayout._subplots; - var ids = subplotList.cartesian.concat(subplotList.gl2d || []); - var mockGd = {_fullLayout: fullLayout}; - - var isX = ax._id.charAt(0) === 'x'; - var anchorAx = ax._mainAxis._anchorAxis; - var mainSubplotID = ''; - var nextBestMainSubplotID = ''; - var anchorID = ''; - - // First try the main ID with the anchor - if(anchorAx) { - anchorID = anchorAx._mainAxis._id; - mainSubplotID = isX ? (ax._id + anchorID) : (anchorID + ax._id); - } - - // Then look for a subplot with the counteraxis overlaying the anchor - // If that fails just use the first subplot including this axis - if(!mainSubplotID || !fullLayout._plots[mainSubplotID]) { - mainSubplotID = ''; - - for(var j = 0; j < ids.length; j++) { - var id = ids[j]; - var yIndex = id.indexOf('y'); - var idPart = isX ? id.substr(0, yIndex) : id.substr(yIndex); - var counterPart = isX ? id.substr(yIndex) : id.substr(0, yIndex); - - if(idPart === ax._id) { - if(!nextBestMainSubplotID) nextBestMainSubplotID = id; - var counterAx = Axes.getFromId(mockGd, counterPart); - if(anchorID && counterAx.overlaying === anchorID) { - mainSubplotID = id; - break; - } - } - } - } - - return mainSubplotID || nextBestMainSubplotID; -} - function shouldShowLinesOrTicks(ax, subplot) { return (ax.ticks || ax.showline) && (subplot === ax._mainSubplot || ax.mirror === 'all' || ax.mirror === 'allticks'); @@ -752,6 +702,8 @@ exports.doAutoRangeAndConstraints = function(gd) { for(var i = 0; i < axList.length; i++) { var ax = axList[i]; cleanAxisConstraints(gd, ax); + // in case margins changed, update scale + ax.setScale(); doAutoRange(gd, ax); } diff --git a/src/plots/cartesian/autorange.js b/src/plots/cartesian/autorange.js index 3fe613d5e7d..c77914096f7 100644 --- a/src/plots/cartesian/autorange.js +++ b/src/plots/cartesian/autorange.js @@ -237,10 +237,6 @@ function concatExtremes(gd, ax) { } function doAutoRange(gd, ax) { - if(!ax._length) ax.setScale(); - - var axIn; - if(ax.autorange) { ax.range = getAutoRange(gd, ax); @@ -250,7 +246,7 @@ function doAutoRange(gd, ax) { // doAutoRange will get called on fullLayout, // but we want to report its results back to layout - axIn = ax._input; + var axIn = ax._input; // before we edit _input, store preGUI values var edits = {}; @@ -262,15 +258,16 @@ function doAutoRange(gd, ax) { axIn.autorange = ax.autorange; } - if(ax._anchorAxis && ax._anchorAxis.rangeslider) { - var axeRangeOpts = ax._anchorAxis.rangeslider[ax._name]; + var anchorAx = ax._anchorAxis; + + if(anchorAx && anchorAx.rangeslider) { + var axeRangeOpts = anchorAx.rangeslider[ax._name]; if(axeRangeOpts) { if(axeRangeOpts.rangemode === 'auto') { axeRangeOpts.range = getAutoRange(gd, ax); } } - axIn = ax._anchorAxis._input; - axIn.rangeslider[ax._name] = Lib.extendFlat({}, axeRangeOpts); + anchorAx._input.rangeslider[ax._name] = Lib.extendFlat({}, axeRangeOpts); } } diff --git a/src/plots/cartesian/axes.js b/src/plots/cartesian/axes.js index 00b4ca78dbf..f413741bc49 100644 --- a/src/plots/cartesian/axes.js +++ b/src/plots/cartesian/axes.js @@ -1467,6 +1467,9 @@ axes.getTickFormat = function(ax) { // as an array of items like 'xy', 'x2y', 'x2y2'... // sorted by x (x,x2,x3...) then y // optionally restrict to only subplots containing axis object ax +// +// NOTE: this is currently only used OUTSIDE plotly.js (toolpanel, webapp) +// ideally we get rid of it there (or just copy this there) and remove it here axes.getSubplots = function(gd, ax) { var subplotObj = gd._fullLayout._subplots; var allSubplots = subplotObj.cartesian.concat(subplotObj.gl2d || []); @@ -1485,6 +1488,8 @@ axes.getSubplots = function(gd, ax) { }; // find all subplots with axis 'ax' +// NOTE: this is only used in axes.getSubplots (only used outside plotly.js) and +// gl2d/convert (where it restricts axis subplots to only those with gl2d) axes.findSubplotsWithAxis = function(subplots, ax) { var axMatch = new RegExp( (ax._id.charAt(0) === 'x') ? ('^' + ax._id + 'y') : (ax._id + '$') @@ -1592,7 +1597,7 @@ axes.draw = function(gd, arg, opts) { var axList = (!arg || arg === 'redraw') ? axes.listIds(gd) : arg; - Lib.syncOrAsync(axList.map(function(axId) { + return Lib.syncOrAsync(axList.map(function(axId) { return function() { if(!axId) return; @@ -1628,12 +1633,20 @@ axes.drawOne = function(gd, ax, opts) { var counterLetter = axes.counterLetter(axId); var mainSubplot = ax._mainSubplot; var mainLinePosition = ax._mainLinePosition; + var mainMirrorPosition = ax._mainMirrorPosition; var mainPlotinfo = fullLayout._plots[mainSubplot]; var mainAxLayer = mainPlotinfo[axLetter + 'axislayer']; - var subplotsWithAx = axes.getSubplots(gd, ax); + var subplotsWithAx = ax._subplotsWith; var vals = ax._vals = axes.calcTicks(ax); + // Add a couple of axis properties that should cause us to recreate + // elements. Used in d3 data function. + var axInfo = [ax.mirror, mainLinePosition, mainMirrorPosition].join('_'); + for(i = 0; i < vals.length; i++) { + vals[i].axInfo = axInfo; + } + if(!ax.visible) return; // stash selections to avoid DOM queries e.g. @@ -1679,6 +1692,7 @@ axes.drawOne = function(gd, ax, opts) { axes.drawGrid(gd, ax, { vals: gridVals, + counterAxis: counterAxis, layer: plotinfo.gridlayer.select('.' + axId), path: gridPath, transFn: transFn @@ -1698,7 +1712,7 @@ axes.drawOne = function(gd, ax, opts) { if(ax.ticks) { var mainTickPath = axes.makeTickPath(ax, mainLinePosition, tickSigns[2]); if(ax._anchorAxis && ax.mirror && ax.mirror !== true) { - mainTickPath += axes.makeTickPath(ax, ax._mainMirrorPosition, tickSigns[3]); + mainTickPath += axes.makeTickPath(ax, mainMirrorPosition, tickSigns[3]); } axes.drawTicks(gd, ax, { @@ -1866,38 +1880,42 @@ axes.drawOne = function(gd, ax, opts) { } } + var hasRangeSlider = Registry.getComponentMethod('rangeslider', 'isVisible')(ax); + function doAutoMargins() { - var pushKey = ax._name + '.automargin'; + var push, rangeSliderPush; - if(!ax.automargin) { - Plots.autoMargin(gd, pushKey); - return; + if(hasRangeSlider) { + rangeSliderPush = Registry.getComponentMethod('rangeslider', 'autoMarginOpts')(gd, ax); } + Plots.autoMargin(gd, rangeSliderAutoMarginID(ax), rangeSliderPush); var s = ax.side.charAt(0); - var push = {x: 0, y: 0, r: 0, l: 0, t: 0, b: 0}; + if(ax.automargin && (!hasRangeSlider || s !== 'b')) { + push = {x: 0, y: 0, r: 0, l: 0, t: 0, b: 0}; - if(axLetter === 'x') { - push.y = (ax.anchor === 'free' ? ax.position : - ax._anchorAxis.domain[s === 't' ? 1 : 0]); - push[s] += ax._boundingBox.height; - } else { - push.x = (ax.anchor === 'free' ? ax.position : - ax._anchorAxis.domain[s === 'r' ? 1 : 0]); - push[s] += ax._boundingBox.width; - } + if(axLetter === 'x') { + push.y = (ax.anchor === 'free' ? ax.position : + ax._anchorAxis.domain[s === 't' ? 1 : 0]); + push[s] += ax._boundingBox.height; + } else { + push.x = (ax.anchor === 'free' ? ax.position : + ax._anchorAxis.domain[s === 'r' ? 1 : 0]); + push[s] += ax._boundingBox.width; + } - if(ax.title.text !== fullLayout._dfltTitle[axLetter]) { - push[s] += ax.title.font.size; + if(ax.title.text !== fullLayout._dfltTitle[axLetter]) { + push[s] += ax.title.font.size; + } } - Plots.autoMargin(gd, pushKey, push); + Plots.autoMargin(gd, axAutoMarginID(ax), push); } seq.push(calcBoundingBox, doAutoMargins); if(!opts.skipTitle && - !((ax.rangeslider || {}).visible && ax._boundingBox && ax.side === 'bottom') + !(hasRangeSlider && ax._boundingBox && ax.side === 'bottom') ) { seq.push(function() { return drawTitle(gd, ax); }); } @@ -2142,10 +2160,8 @@ axes.makeLabelFns = function(ax, shift, angle) { return out; }; -function makeDataFn(ax) { - return function(d) { - return [d.text, d.x, ax.mirror, d.font, d.fontSize, d.fontColor].join('_'); - }; +function tickDataFn(d) { + return [d.text, d.x, d.axInfo, d.font, d.fontSize, d.fontColor].join('_'); } /** @@ -2170,7 +2186,7 @@ axes.drawTicks = function(gd, ax, opts) { var cls = ax._id + 'tick'; var ticks = opts.layer.selectAll('path.' + cls) - .data(ax.ticks ? opts.vals : [], makeDataFn(ax)); + .data(ax.ticks ? opts.vals : [], tickDataFn); ticks.exit().remove(); @@ -2200,6 +2216,8 @@ axes.drawTicks = function(gd, ax, opts) { * @param {object} opts * - {array of object} vals (calcTicks output-like) * - {d3 selection} layer + * - {object} counterAxis (full axis object corresponding to counter axis) + * optional - only required if this axis supports zero lines * - {string or fn} path * - {fn} transFn * - {boolean} crisp (set to false to unset crisp-edge SVG rendering) @@ -2208,26 +2226,39 @@ axes.drawGrid = function(gd, ax, opts) { opts = opts || {}; var cls = ax._id + 'grid'; + var vals = opts.vals; + var counterAx = opts.counterAxis; + if(ax.showgrid === false) { + vals = []; + } + else if(counterAx && axes.shouldShowZeroLine(gd, ax, counterAx)) { + var isArrayMode = ax.tickmode === 'array'; + for(var i = 0; i < vals.length; i++) { + var xi = vals[i].x; + if(isArrayMode ? !xi : (Math.abs(xi) < ax.dtick / 100)) { + vals = vals.slice(0, i).concat(vals.slice(i + 1)); + // In array mode you can in principle have multiple + // ticks at 0, so test them all. Otherwise once we found + // one we can stop. + if(isArrayMode) i--; + else break; + } + } + } var grid = opts.layer.selectAll('path.' + cls) - .data((ax.showgrid === false) ? [] : opts.vals, makeDataFn(ax)); + .data(vals, tickDataFn); grid.exit().remove(); grid.enter().append('path') .classed(cls, 1) - .classed('crisp', opts.crisp !== false) - .attr('d', opts.path) - .each(function(d) { - if(ax.zeroline && (ax.type === 'linear' || ax.type === '-') && - Math.abs(d.x) < ax.dtick / 100) { - d3.select(this).remove(); - } - }); + .classed('crisp', opts.crisp !== false); ax._gw = Drawing.crispRound(gd, ax.gridwidth, 1); grid.attr('transform', opts.transFn) + .attr('d', opts.path) .call(Color.stroke, ax.gridcolor || '#ddd') .style('stroke-width', ax._gw + 'px'); @@ -2266,7 +2297,6 @@ axes.drawZeroLine = function(gd, ax, opts) { .classed(cls, 1) .classed('zl', 1) .classed('crisp', opts.crisp !== false) - .attr('d', opts.path) .each(function() { // use the fact that only one element can enter to trigger a sort. // If several zerolines enter at the same time we will sort once per, @@ -2277,6 +2307,7 @@ axes.drawZeroLine = function(gd, ax, opts) { }); zl.attr('transform', opts.transFn) + .attr('d', opts.path) .call(Color.stroke, ax.zerolinecolor || Color.defaultLine) .style('stroke-width', Drawing.crispRound(gd, ax.zerolinewidth, ax._gw || 1) + 'px'); }; @@ -2316,7 +2347,7 @@ axes.drawLabels = function(gd, ax, opts) { var lastAngle = (ax._tickAngles || {})[cls]; var tickLabels = opts.layer.selectAll('g.' + cls) - .data(ax.showticklabels ? vals : [], makeDataFn(ax)); + .data(ax.showticklabels ? vals : [], tickDataFn); var labelsReady = []; @@ -2533,7 +2564,7 @@ function drawDividers(gd, ax, opts) { var vals = opts.vals; var dividers = opts.layer.selectAll('path.' + cls) - .data(vals, makeDataFn(ax)); + .data(vals, tickDataFn); dividers.exit().remove(); @@ -2738,14 +2769,17 @@ axes.allowAutoMargin = function(gd) { for(var i = 0; i < axList.length; i++) { var ax = axList[i]; if(ax.automargin) { - Plots.allowAutoMargin(gd, ax._name + '.automargin'); + Plots.allowAutoMargin(gd, axAutoMarginID(ax)); } - if(ax.rangeslider && ax.rangeslider.visible) { - Plots.allowAutoMargin(gd, 'rangeslider' + ax._id); + if(Registry.getComponentMethod('rangeslider', 'isVisible')(ax)) { + Plots.allowAutoMargin(gd, rangeSliderAutoMarginID(ax)); } } }; +function axAutoMarginID(ax) { return ax._id + '.automargin'; } +function rangeSliderAutoMarginID(ax) { return ax._id + '.rangeslider'; } + // swap all the presentation attributes of the axes showing these traces axes.swap = function(gd, traces) { var axGroups = makeAxisGroups(gd, traces); diff --git a/src/plots/cartesian/constraints.js b/src/plots/cartesian/constraints.js index 70859a2ad0b..36c27ec08b3 100644 --- a/src/plots/cartesian/constraints.js +++ b/src/plots/cartesian/constraints.js @@ -139,7 +139,6 @@ exports.enforce = function enforceAxisConstraints(gd) { var getPad = makePadFn(ax); updateDomain(ax, factor); - ax.setScale(); var m = Math.abs(ax._m); var extremes = concatExtremes(gd, ax); var minArray = extremes.min; @@ -206,4 +205,5 @@ function updateDomain(ax, factor) { center + (inputDomain[0] - center) / factor, center + (inputDomain[1] - center) / factor ]; + ax.setScale(); } diff --git a/src/plots/cartesian/dragbox.js b/src/plots/cartesian/dragbox.js index 6cc75dc0443..8b91a5dab5b 100644 --- a/src/plots/cartesian/dragbox.js +++ b/src/plots/cartesian/dragbox.js @@ -516,6 +516,9 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) { return; } + // prevent axis drawing from monkeying with margins until we're done + gd._fullLayout._replotting = true; + if(xActive === 'ew' || yActive === 'ns') { if(xActive) dragAxList(xaxes, dx); if(yActive) dragAxList(yaxes, dy); @@ -726,7 +729,10 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) { // accumulated MathJax promises - wait for them before we relayout. Lib.syncOrAsync([ Plots.previousPromises, - function() { Registry.call('_guiRelayout', gd, updates); } + function() { + gd._fullLayout._replotting = false; + Registry.call('_guiRelayout', gd, updates); + } ], gd); } diff --git a/src/plots/cartesian/layout_defaults.js b/src/plots/cartesian/layout_defaults.js index e0e6e18dc08..6bf2f652fc6 100644 --- a/src/plots/cartesian/layout_defaults.js +++ b/src/plots/cartesian/layout_defaults.js @@ -156,6 +156,8 @@ module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, fullData) { axLayoutOut._traceIndices = traces.map(function(t) { return t._expandedIndex; }); axLayoutOut._annIndices = []; axLayoutOut._shapeIndices = []; + axLayoutOut._subplotsWith = []; + axLayoutOut._counterAxes = []; // set up some private properties axLayoutOut._name = axLayoutOut._attr = axName; @@ -239,11 +241,7 @@ module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, fullData) { var anchoredAxis = layoutOut[id2name(axLayoutOut.anchor)]; - var fixedRangeDflt = ( - anchoredAxis && - anchoredAxis.rangeslider && - anchoredAxis.rangeslider.visible - ); + var fixedRangeDflt = getComponentMethod('rangeslider', 'isVisible')(anchoredAxis); coerce('fixedrange', fixedRangeDflt); } diff --git a/src/plots/cartesian/set_convert.js b/src/plots/cartesian/set_convert.js index b07d5d02b11..57118325b8d 100644 --- a/src/plots/cartesian/set_convert.js +++ b/src/plots/cartesian/set_convert.js @@ -419,11 +419,6 @@ module.exports = function setConvert(ax, fullLayout) { ax.setScale = function(usePrivateRange) { var gs = fullLayout._size; - // TODO cleaner way to handle this case - if(!ax._categories) ax._categories = []; - // Add a map to optimize the performance of category collection - if(!ax._categoriesMap) ax._categoriesMap = {}; - // make sure we have a domain (pull it in from the axis // this one is overlaying if necessary) if(ax.overlaying) { diff --git a/src/plots/plots.js b/src/plots/plots.js index e2a28e87a39..aa52e9e05ef 100644 --- a/src/plots/plots.js +++ b/src/plots/plots.js @@ -497,15 +497,11 @@ plots.supplyDefaults = function(gd, opts) { if(uids[uid] === 'old') delete tracePreGUI[uid]; } - // TODO may return a promise - plots.doAutoMargin(gd); + // set up containers for margin calculations + initMargins(newFullLayout); - // set scale after auto margin routine - var axList = axisIDs.list(gd); - for(i = 0; i < axList.length; i++) { - var ax = axList[i]; - ax.setScale(); - } + // collect and do some initial calculations for rangesliders + Registry.getComponentMethod('rangeslider', 'makeData')(newFullLayout); // update object references in calcdata if(!skipUpdateCalc && oldCalcdata.length === newFullData.length) { @@ -815,6 +811,12 @@ plots.linkSubplots = function(newFullData, newFullLayout, oldFullData, oldFullLa plotinfo.id = id; } + // add these axis ids to each others' subplot lists + xaxis._counterAxes.push(yaxis._id); + yaxis._counterAxes.push(xaxis._id); + xaxis._subplotsWith.push(id); + yaxis._subplotsWith.push(id); + // update x and y axis layout object refs plotinfo.xaxis = xaxis; plotinfo.yaxis = yaxis; @@ -842,8 +844,9 @@ plots.linkSubplots = function(newFullData, newFullLayout, oldFullData, oldFullLa // while we're at it, link overlaying axes to their main axes and // anchored axes to the axes they're anchored to var axList = axisIDs.list(mockGd, null, true); + var ax; for(i = 0; i < axList.length; i++) { - var ax = axList[i]; + ax = axList[i]; var mainAx = null; if(ax.overlaying) { @@ -871,8 +874,53 @@ plots.linkSubplots = function(newFullData, newFullLayout, oldFullData, oldFullLa null : axisIDs.getFromId(mockGd, ax.anchor); } + + // finally, we can find the main subplot for each axis + // (on which the ticks & labels are drawn) + for(i = 0; i < axList.length; i++) { + ax = axList[i]; + ax._counterAxes.sort(axisIDs.idSort); + ax._subplotsWith.sort(Lib.subplotSort); + ax._mainSubplot = findMainSubplot(ax, newFullLayout); + } }; +function findMainSubplot(ax, fullLayout) { + var mockGd = {_fullLayout: fullLayout}; + + var isX = ax._id.charAt(0) === 'x'; + var anchorAx = ax._mainAxis._anchorAxis; + var mainSubplotID = ''; + var nextBestMainSubplotID = ''; + var anchorID = ''; + + // First try the main ID with the anchor + if(anchorAx) { + anchorID = anchorAx._mainAxis._id; + mainSubplotID = isX ? (ax._id + anchorID) : (anchorID + ax._id); + } + + // Then look for a subplot with the counteraxis overlaying the anchor + // If that fails just use the first subplot including this axis + if(!mainSubplotID || !fullLayout._plots[mainSubplotID]) { + mainSubplotID = ''; + + var counterIDs = ax._counterAxes; + for(var j = 0; j < counterIDs.length; j++) { + var counterPart = counterIDs[j]; + var id = isX ? (ax._id + counterPart) : (counterPart + ax._id); + if(!nextBestMainSubplotID) nextBestMainSubplotID = id; + var counterAx = axisIDs.getFromId(mockGd, counterPart); + if(anchorID && counterAx.overlaying === anchorID) { + mainSubplotID = id; + break; + } + } + } + + return mainSubplotID || nextBestMainSubplotID; +} + // This function clears any trace attributes with valType: color and // no set dflt filed in the plot schema. This is needed because groupby (which // is the only transform for which this currently applies) supplies parent @@ -1686,7 +1734,20 @@ plots.allowAutoMargin = function(gd, id) { gd._fullLayout._pushmarginIds[id] = 1; }; -function setupAutoMargin(fullLayout) { +function initMargins(fullLayout) { + var margin = fullLayout.margin; + + if(!fullLayout._size) { + var gs = fullLayout._size = { + l: Math.round(margin.l), + r: Math.round(margin.r), + t: Math.round(margin.t), + b: Math.round(margin.b), + p: Math.round(margin.pad) + }; + gs.w = Math.round(fullLayout.width) - gs.l - gs.r; + gs.h = Math.round(fullLayout.height) - gs.t - gs.b; + } if(!fullLayout._pushmargin) fullLayout._pushmargin = {}; if(!fullLayout._pushmarginIds) fullLayout._pushmarginIds = {}; } @@ -1709,8 +1770,6 @@ function setupAutoMargin(fullLayout) { plots.autoMargin = function(gd, id, o) { var fullLayout = gd._fullLayout; - setupAutoMargin(fullLayout); - var pushMargin = fullLayout._pushmargin; var pushMarginIds = fullLayout._pushmarginIds; @@ -1754,18 +1813,19 @@ plots.autoMargin = function(gd, id, o) { plots.doAutoMargin = function(gd) { var fullLayout = gd._fullLayout; if(!fullLayout._size) fullLayout._size = {}; - setupAutoMargin(fullLayout); + initMargins(fullLayout); - var gs = fullLayout._size, - oldmargins = JSON.stringify(gs); + var gs = fullLayout._size; + var oldmargins = JSON.stringify(gs); + var margin = fullLayout.margin; // adjust margins for outside components // fullLayout.margin is the requested margin, // fullLayout._size has margins and plotsize after adjustment - var ml = Math.max(fullLayout.margin.l || 0, 0); - var mr = Math.max(fullLayout.margin.r || 0, 0); - var mt = Math.max(fullLayout.margin.t || 0, 0); - var mb = Math.max(fullLayout.margin.b || 0, 0); + var ml = margin.l; + var mr = margin.r; + var mt = margin.t; + var mb = margin.b; var pushMargin = fullLayout._pushmargin; var pushMarginIds = fullLayout._pushmarginIds; @@ -1835,7 +1895,7 @@ plots.doAutoMargin = function(gd) { gs.r = Math.round(mr); gs.t = Math.round(mt); gs.b = Math.round(mb); - gs.p = Math.round(fullLayout.margin.pad); + gs.p = Math.round(margin.pad); gs.w = Math.round(fullLayout.width) - gs.l - gs.r; gs.h = Math.round(fullLayout.height) - gs.t - gs.b; diff --git a/test/image/baselines/range_slider_rangemode.png b/test/image/baselines/range_slider_rangemode.png index 0915d37b06d..40b03a5eb77 100644 Binary files a/test/image/baselines/range_slider_rangemode.png and b/test/image/baselines/range_slider_rangemode.png differ diff --git a/test/jasmine/tests/heatmap_test.js b/test/jasmine/tests/heatmap_test.js index 277c0b33abf..7152e38429d 100644 --- a/test/jasmine/tests/heatmap_test.js +++ b/test/jasmine/tests/heatmap_test.js @@ -298,6 +298,13 @@ describe('heatmap calc', function() { fullTrace._extremes = {}; + // we used to call ax.setScale during supplyDefaults, and this had a + // fallback to provide _categories and _categoriesMap. Now neither of + // those is true... anyway the right way to do this though is + // ax.clearCalc. + fullLayout.xaxis.clearCalc(); + fullLayout.yaxis.clearCalc(); + var out = Heatmap.calc(gd, fullTrace)[0]; out._xcategories = fullLayout.xaxis._categories; out._ycategories = fullLayout.yaxis._categories; diff --git a/test/jasmine/tests/plot_api_react_test.js b/test/jasmine/tests/plot_api_react_test.js index 812eb6f63e5..4dbf48ddc8a 100644 --- a/test/jasmine/tests/plot_api_react_test.js +++ b/test/jasmine/tests/plot_api_react_test.js @@ -468,6 +468,51 @@ describe('@noCIdep Plotly.react', function() { .then(done); }); + it('can change from scatter to category scatterpolar and back', function(done) { + function scatter() { + return { + data: [{x: ['a', 'b'], y: [1, 2]}], + layout: {width: 400, height: 400, margin: {r: 80, t: 20}} + }; + } + + function scatterpolar() { + return { + // the bug https://github.com/plotly/plotly.js/issues/3255 + // required all of this to change: + // - type -> scatterpolar + // - category theta + // - margins changed + data: [{type: 'scatterpolar', r: [1, 2, 3], theta: ['a', 'b', 'c']}], + layout: {width: 400, height: 400, margin: {r: 80, t: 50}} + }; + } + + function countTraces(scatterTraces, polarTraces) { + expect(document.querySelectorAll('.scatter').length) + .toBe(scatterTraces + polarTraces); + expect(document.querySelectorAll('.xy .scatter').length) + .toBe(scatterTraces); + expect(document.querySelectorAll('.polar .scatter').length) + .toBe(polarTraces); + } + + Plotly.newPlot(gd, scatter()) + .then(function() { + countTraces(1, 0); + return Plotly.react(gd, scatterpolar()); + }) + .then(function() { + countTraces(0, 1); + return Plotly.react(gd, scatter()); + }) + .then(function() { + countTraces(1, 0); + }) + .catch(failTest) + .then(done); + }); + it('can change data in candlesticks multiple times', function(done) { // test that we've fixed the original issue in // https://github.com/plotly/plotly.js/issues/2510 diff --git a/test/jasmine/tests/plots_test.js b/test/jasmine/tests/plots_test.js index 49c7c33ed54..11db2d50a96 100644 --- a/test/jasmine/tests/plots_test.js +++ b/test/jasmine/tests/plots_test.js @@ -83,9 +83,9 @@ describe('Test Plots', function() { expect(gd._fullLayout.someFunc).toBe(oldFullLayout.someFunc); expect(gd._fullLayout.xaxis.c2p) - .not.toBe(oldFullLayout.xaxis.c2p, '(set during ax.setScale'); + .not.toBe(oldFullLayout.xaxis.c2p, '(set during setConvert)'); expect(gd._fullLayout.yaxis._m) - .not.toBe(oldFullLayout.yaxis._m, '(set during ax.setScale'); + .toBe(oldFullLayout.yaxis._m, '(we don\'t run ax.setScale here)'); }); it('should include the correct reference to user data', function() { diff --git a/test/jasmine/tests/range_slider_test.js b/test/jasmine/tests/range_slider_test.js index 6ba00809e06..a6246a251f4 100644 --- a/test/jasmine/tests/range_slider_test.js +++ b/test/jasmine/tests/range_slider_test.js @@ -28,9 +28,7 @@ function getRangeSliderChild(index) { } function countRangeSliderClipPaths() { - return d3.selectAll('defs').selectAll('*').filter(function() { - return this.id.indexOf('rangeslider') !== -1; - }).size(); + return document.querySelectorAll('defs [id*=rangeslider]').length; } function testTranslate1D(node, val) { diff --git a/test/jasmine/tests/splom_test.js b/test/jasmine/tests/splom_test.js index 3c4d81911b3..3e09444efd9 100644 --- a/test/jasmine/tests/splom_test.js +++ b/test/jasmine/tests/splom_test.js @@ -820,11 +820,11 @@ describe('Test splom interactions:', function() { function _assert(msg, exp) { var splomScenes = gd._fullLayout._splomScenes; - var ids = Object.keys(splomScenes); + var ids = gd._fullData.map(function(trace) { return trace.uid; }); for(var i = 0; i < 3; i++) { var drawFn = splomScenes[ids[i]].draw; - expect(drawFn).toHaveBeenCalledTimes(exp[i], msg + ' - trace ' + i); + expect(drawFn.calls.count()).toBe(exp[i], msg + ' - trace ' + i); drawFn.calls.reset(); } } @@ -869,7 +869,7 @@ describe('Test splom interactions:', function() { methods.forEach(function(m) { spyOn(Plots, m).and.callThrough(); }); - function assetsFnCall(msg, exp) { + function assertFnCall(msg, exp) { methods.forEach(function(m) { expect(Plots[m]).toHaveBeenCalledTimes(exp[m], msg); Plots[m].calls.reset(); @@ -879,7 +879,7 @@ describe('Test splom interactions:', function() { spyOn(Lib, 'log'); Plotly.plot(gd, fig).then(function() { - assetsFnCall('base', { + assertFnCall('base', { cleanPlot: 1, // called once from inside Plots.supplyDefaults supplyDefaults: 1, doCalcdata: 1 @@ -892,9 +892,9 @@ describe('Test splom interactions:', function() { return Plotly.relayout(gd, {width: 4810, height: 3656}); }) .then(function() { - assetsFnCall('after', { - cleanPlot: 4, // 3 three from supplyDefaults, once in drawFramework - supplyDefaults: 3, // 1 from relayout, 1 from automargin, 1 in drawFramework + assertFnCall('after', { + cleanPlot: 3, // 2 from supplyDefaults, once in drawFramework + supplyDefaults: 2, // 1 from relayout, 1 in drawFramework doCalcdata: 1 // once in drawFramework }); assertDims('after', 4810, 3656);