diff --git a/src/components/shapes/draw.js b/src/components/shapes/draw.js index 271e37c4861..7e1bb7e1305 100644 --- a/src/components/shapes/draw.js +++ b/src/components/shapes/draw.js @@ -42,7 +42,11 @@ function draw(gd) { // Remove previous shapes before drawing new in shapes in fullLayout.shapes fullLayout._shapeUpperLayer.selectAll('path').remove(); fullLayout._shapeLowerLayer.selectAll('path').remove(); - fullLayout._shapeSubplotLayers.selectAll('path').remove(); + + for(var k in fullLayout._plots) { + var shapelayer = fullLayout._plots[k].shapelayer; + if(shapelayer) shapelayer.selectAll('path').remove(); + } for(var i = 0; i < fullLayout.shapes.length; i++) { if(fullLayout.shapes[i].visible) { diff --git a/src/plot_api/edit_types.js b/src/plot_api/edit_types.js index 24701a9a443..ea6defdc2c5 100644 --- a/src/plot_api/edit_types.js +++ b/src/plot_api/edit_types.js @@ -35,7 +35,7 @@ var layoutOpts = { valType: 'flaglist', extras: ['none'], flags: [ - 'calc', 'calcIfAutorange', 'plot', 'legend', 'ticks', 'margins', + 'calc', 'calcIfAutorange', 'plot', 'legend', 'ticks', 'axrange', 'margins', 'layoutstyle', 'modebar', 'camera', 'arraydraw' ], description: [ @@ -48,6 +48,7 @@ var layoutOpts = { '*legend* only redraws the legend.', '*ticks* only redraws axis ticks, labels, and gridlines.', '*margins* recomputes ticklabel automargins.', + '*axrange* minimal sequence when updating axis ranges.', '*layoutstyle* reapplies global and SVG cartesian axis styles.', '*modebar* just updates the modebar.', '*camera* just updates the camera settings for gl3d scenes.', diff --git a/src/plot_api/plot_api.js b/src/plot_api/plot_api.js index 89f2a7b7d2d..21ae2ea2cb5 100644 --- a/src/plot_api/plot_api.js +++ b/src/plot_api/plot_api.js @@ -22,14 +22,13 @@ var Registry = require('../registry'); var PlotSchema = require('./plot_schema'); var Plots = require('../plots/plots'); var Polar = require('../plots/polar/legacy'); -var initInteractions = require('../plots/cartesian/graph_interact'); var Axes = require('../plots/cartesian/axes'); var Drawing = require('../components/drawing'); var Color = require('../components/color'); +var initInteractions = require('../plots/cartesian/graph_interact').initInteractions; var xmlnsNamespaces = require('../constants/xmlns_namespaces'); var svgTextUtils = require('../lib/svg_text_utils'); -var clearGlCanvases = require('../lib/clear_gl_canvases'); var defaultConfig = require('./plot_config'); var manageArrays = require('./manage_arrays'); @@ -37,11 +36,7 @@ var helpers = require('./helpers'); var subroutines = require('./subroutines'); var editTypes = require('./edit_types'); -var cartesianConstants = require('../plots/cartesian/constants'); -var axisConstraints = require('../plots/cartesian/constraints'); -var enforceAxisConstraints = axisConstraints.enforce; -var cleanAxisConstraints = axisConstraints.clean; -var doAutoRange = require('../plots/cartesian/autorange').doAutoRange; +var AX_NAME_PATTERN = require('../plots/cartesian/constants').AX_NAME_PATTERN; var numericNameWarningCount = 0; var numericNameWarningCountLimit = 5; @@ -331,15 +326,7 @@ exports.plot = function(gd, data, layout, config) { function doAutoRangeAndConstraints() { if(gd._transitioning) return; - var axList = Axes.list(gd, '', true); - for(var i = 0; i < axList.length; i++) { - var ax = axList[i]; - cleanAxisConstraints(gd, ax); - - doAutoRange(ax); - } - - enforceAxisConstraints(gd); + subroutines.doAutoRangeAndConstraints(gd); // store initial ranges *after* enforcing constraints, otherwise // we will never look like we're at the initial ranges @@ -351,83 +338,6 @@ exports.plot = function(gd, data, layout, config) { return Axes.doTicks(gd, graphWasEmpty ? '' : 'redraw'); } - // Now plot the data - function drawData() { - var calcdata = gd.calcdata, - i, - rangesliderContainers = fullLayout._infolayer.selectAll('g.rangeslider-container'); - - // in case of traces that were heatmaps or contour maps - // previously, remove them and their colorbars explicitly - for(i = 0; i < calcdata.length; i++) { - var trace = calcdata[i][0].trace, - isVisible = (trace.visible === true), - uid = trace.uid; - - if(!isVisible || !Registry.traceIs(trace, '2dMap')) { - var query = ( - '.hm' + uid + - ',.contour' + uid + - ',#clip' + uid - ); - - fullLayout._paper - .selectAll(query) - .remove(); - - rangesliderContainers - .selectAll(query) - .remove(); - } - - if(!isVisible || !trace._module.colorbar) { - fullLayout._infolayer.selectAll('.cb' + uid).remove(); - } - } - - // TODO does this break or slow down parcoords?? - clearGlCanvases(gd); - - // loop over the base plot modules present on graph - var basePlotModules = fullLayout._basePlotModules; - for(i = 0; i < basePlotModules.length; i++) { - basePlotModules[i].plot(gd); - } - - // keep reference to shape layers in subplots - var layerSubplot = fullLayout._paper.selectAll('.layer-subplot'); - fullLayout._shapeSubplotLayers = layerSubplot.selectAll('.shapelayer'); - - // styling separate from drawing - Plots.style(gd); - - // show annotations and shapes - Registry.getComponentMethod('shapes', 'draw')(gd); - Registry.getComponentMethod('annotations', 'draw')(gd); - - // source links - Plots.addLinks(gd); - - // Mark the first render as complete - fullLayout._replotting = false; - - return Plots.previousPromises(gd); - } - - // An initial paint must be completed before these components can be - // correctly sized and the whole plot re-margined. fullLayout._replotting must - // be set to false before these will work properly. - function finalDraw() { - Registry.getComponentMethod('shapes', 'draw')(gd); - Registry.getComponentMethod('images', 'draw')(gd); - Registry.getComponentMethod('annotations', 'draw')(gd); - Registry.getComponentMethod('legend', 'draw')(gd); - Registry.getComponentMethod('rangeslider', 'draw')(gd); - Registry.getComponentMethod('rangeselector', 'draw')(gd); - Registry.getComponentMethod('sliders', 'draw')(gd); - Registry.getComponentMethod('updatemenus', 'draw')(gd); - } - var seq = [ Plots.previousPromises, addFrames, @@ -439,9 +349,10 @@ exports.plot = function(gd, data, layout, config) { seq.push(subroutines.layoutStyles); if(hasCartesian) seq.push(drawAxes); seq.push( - drawData, - finalDraw, + subroutines.drawData, + subroutines.finalDraw, initInteractions, + Plots.addLinks, Plots.rehover, Plots.previousPromises ); @@ -1385,8 +1296,8 @@ exports.restyle = function restyle(gd, astr, val, _traces) { var traces = helpers.coerceTraceIndices(gd, _traces); - var specs = _restyle(gd, aobj, traces), - flags = specs.flags; + var specs = _restyle(gd, aobj, traces); + var flags = specs.flags; // clear calcdata and/or axis types if required so they get regenerated if(flags.clearCalc) gd.calcdata = undefined; @@ -1750,8 +1661,8 @@ exports.relayout = function relayout(gd, astr, val) { if(Object.keys(aobj).length) gd.changed = true; - var specs = _relayout(gd, aobj), - flags = specs.flags; + var specs = _relayout(gd, aobj); + var flags = specs.flags; // clear calcdata if required if(flags.calc) gd.calcdata = undefined; @@ -1772,6 +1683,30 @@ exports.relayout = function relayout(gd, astr, val) { if(flags.legend) seq.push(subroutines.doLegend); if(flags.layoutstyle) seq.push(subroutines.layoutStyles); + + if(flags.axrange) { + // N.B. leave as sequence of subroutines (for now) instead of + // subroutine of its own so that finalDraw always gets + // executed after drawData + seq.push( + // TODO + // no test fail when commenting out doAutoRangeAndConstraints, + // but I think we do need this (maybe just the enforce part?) + // Am I right? + // More info in: + // https://github.com/plotly/plotly.js/issues/2540 + subroutines.doAutoRangeAndConstraints, + // TODO + // can target specific axes, + // do not have to redraw all axes here + // See: + // https://github.com/plotly/plotly.js/issues/2547 + subroutines.doTicksRelayout, + subroutines.drawData, + subroutines.finalDraw + ); + } + if(flags.ticks) seq.push(subroutines.doTicksRelayout); if(flags.modebar) seq.push(subroutines.doModeBar); if(flags.camera) seq.push(subroutines.doCamera); @@ -1992,7 +1927,7 @@ function _relayout(gd, aobj) { } Lib.nestedProperty(fullLayout, ptrunk + '._inputRange').set(null); } - else if(pleaf.match(cartesianConstants.AX_NAME_PATTERN)) { + else if(pleaf.match(AX_NAME_PATTERN)) { var fullProp = Lib.nestedProperty(fullLayout, ai).get(), newType = (vi || {}).type; @@ -2045,8 +1980,9 @@ function _relayout(gd, aobj) { if(checkForAutorange && (refAutorange(gd, objToAutorange, 'x') || refAutorange(gd, objToAutorange, 'y'))) { flags.calc = true; } - else editTypes.update(flags, updateValObject); - + else { + editTypes.update(flags, updateValObject); + } // prepare the edits object we'll send to applyContainerArrayChanges if(!arrayEdits[arrayStr]) arrayEdits[arrayStr] = {}; @@ -2197,11 +2133,11 @@ exports.update = function update(gd, traceUpdate, layoutUpdate, _traces) { var traces = helpers.coerceTraceIndices(gd, _traces); - var restyleSpecs = _restyle(gd, Lib.extendFlat({}, traceUpdate), traces), - restyleFlags = restyleSpecs.flags; + var restyleSpecs = _restyle(gd, Lib.extendFlat({}, traceUpdate), traces); + var restyleFlags = restyleSpecs.flags; - var relayoutSpecs = _relayout(gd, Lib.extendFlat({}, layoutUpdate)), - relayoutFlags = relayoutSpecs.flags; + var relayoutSpecs = _relayout(gd, Lib.extendFlat({}, layoutUpdate)); + var relayoutFlags = relayoutSpecs.flags; // clear calcdata and/or axis types if required if(restyleFlags.clearCalc || relayoutFlags.calc) gd.calcdata = undefined; @@ -2236,6 +2172,14 @@ exports.update = function update(gd, traceUpdate, layoutUpdate, _traces) { if(restyleFlags.colorbars) seq.push(subroutines.doColorBars); if(relayoutFlags.legend) seq.push(subroutines.doLegend); if(relayoutFlags.layoutstyle) seq.push(subroutines.layoutStyles); + if(relayoutFlags.axrange) { + seq.push( + subroutines.doAutoRangeAndConstraints, + subroutines.doTicksRelayout, + subroutines.drawData, + subroutines.finalDraw + ); + } if(relayoutFlags.ticks) seq.push(subroutines.doTicksRelayout); if(relayoutFlags.modebar) seq.push(subroutines.doModeBar); if(relayoutFlags.camera) seq.push(subroutines.doCamera); @@ -2388,6 +2332,14 @@ exports.react = function(gd, data, layout, config) { if(restyleFlags.colorbars) seq.push(subroutines.doColorBars); if(relayoutFlags.legend) seq.push(subroutines.doLegend); if(relayoutFlags.layoutstyle) seq.push(subroutines.layoutStyles); + if(relayoutFlags.axrange) { + seq.push( + subroutines.doAutoRangeAndConstraints, + subroutines.doTicksRelayout, + subroutines.drawData, + subroutines.finalDraw + ); + } if(relayoutFlags.ticks) seq.push(subroutines.doTicksRelayout); if(relayoutFlags.modebar) seq.push(subroutines.doModeBar); if(relayoutFlags.camera) seq.push(subroutines.doCamera); diff --git a/src/plot_api/subroutines.js b/src/plot_api/subroutines.js index 6ddf17409ea..7256c48ca2f 100644 --- a/src/plot_api/subroutines.js +++ b/src/plot_api/subroutines.js @@ -11,16 +11,22 @@ var d3 = require('d3'); var Registry = require('../registry'); var Plots = require('../plots/plots'); + var Lib = require('../lib'); +var clearGlCanvases = require('../lib/clear_gl_canvases'); var Color = require('../components/color'); var Drawing = require('../components/drawing'); var Titles = require('../components/titles'); var ModeBar = require('../components/modebar'); + var Axes = require('../plots/cartesian/axes'); -var initInteractions = require('../plots/cartesian/graph_interact'); var cartesianConstants = require('../plots/cartesian/constants'); var alignmentConstants = require('../constants/alignment'); +var axisConstraints = require('../plots/cartesian/constraints'); +var enforceAxisConstraints = axisConstraints.enforce; +var cleanAxisConstraints = axisConstraints.clean; +var doAutoRange = require('../plots/cartesian/autorange').doAutoRange; exports.layoutStyles = function(gd) { return Lib.syncOrAsync([Plots.doAutoMargin, exports.lsInner], gd); @@ -173,7 +179,7 @@ exports.lsInner = function(gd) { .append('rect'); }); - plotClip.select('rect').attr({ + plotinfo.clipRect = plotClip.select('rect').attr({ width: xa._length, height: ya._length }); @@ -464,7 +470,6 @@ exports.doModeBar = function(gd) { var fullLayout = gd._fullLayout; ModeBar.manage(gd); - initInteractions(gd); for(var i = 0; i < fullLayout._basePlotModules.length; i++) { var updateFx = fullLayout._basePlotModules[i].updateFx; @@ -485,3 +490,84 @@ exports.doCamera = function(gd) { scene.setCamera(sceneLayout.camera); } }; + +exports.drawData = function(gd) { + var fullLayout = gd._fullLayout; + var calcdata = gd.calcdata; + var rangesliderContainers = fullLayout._infolayer.selectAll('g.rangeslider-container'); + var i; + + // in case of traces that were heatmaps or contour maps + // previously, remove them and their colorbars explicitly + for(i = 0; i < calcdata.length; i++) { + var trace = calcdata[i][0].trace; + var isVisible = (trace.visible === true); + var uid = trace.uid; + + if(!isVisible || !Registry.traceIs(trace, '2dMap')) { + var query = ( + '.hm' + uid + + ',.contour' + uid + + ',#clip' + uid + ); + + fullLayout._paper + .selectAll(query) + .remove(); + + rangesliderContainers + .selectAll(query) + .remove(); + } + + if(!isVisible || !trace._module.colorbar) { + fullLayout._infolayer.selectAll('.cb' + uid).remove(); + } + } + + clearGlCanvases(gd); + + // loop over the base plot modules present on graph + var basePlotModules = fullLayout._basePlotModules; + for(i = 0; i < basePlotModules.length; i++) { + basePlotModules[i].plot(gd); + } + + // styling separate from drawing + Plots.style(gd); + + // show annotations and shapes + Registry.getComponentMethod('shapes', 'draw')(gd); + Registry.getComponentMethod('annotations', 'draw')(gd); + + // Mark the first render as complete + fullLayout._replotting = false; + + return Plots.previousPromises(gd); +}; + +exports.doAutoRangeAndConstraints = function(gd) { + var axList = Axes.list(gd, '', true); + + for(var i = 0; i < axList.length; i++) { + var ax = axList[i]; + cleanAxisConstraints(gd, ax); + doAutoRange(ax); + } + + enforceAxisConstraints(gd); +}; + +// An initial paint must be completed before these components can be +// 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); + Registry.getComponentMethod('legend', 'draw')(gd); + Registry.getComponentMethod('rangeslider', 'draw')(gd); + Registry.getComponentMethod('rangeselector', 'draw')(gd); + Registry.getComponentMethod('sliders', 'draw')(gd); + Registry.getComponentMethod('updatemenus', 'draw')(gd); +}; diff --git a/src/plots/cartesian/axes.js b/src/plots/cartesian/axes.js index 29414f278dd..c44dd3db8f9 100644 --- a/src/plots/cartesian/axes.js +++ b/src/plots/cartesian/axes.js @@ -2188,7 +2188,7 @@ axes.doTicks = function(gd, axid, skipTitle) { } drawTicks(mainPlotinfo[axLetter + 'axislayer'], tickpath); - tickSubplots = Object.keys(ax._linepositions); + tickSubplots = Object.keys(ax._linepositions || {}); } tickSubplots.map(function(subplot) { diff --git a/src/plots/cartesian/dragbox.js b/src/plots/cartesian/dragbox.js index f89468b128a..1f2bbd9a5cd 100644 --- a/src/plots/cartesian/dragbox.js +++ b/src/plots/cartesian/dragbox.js @@ -55,66 +55,74 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) { // within DBLCLICKDELAY so we can check for click or doubleclick events // dragged stores whether a drag has occurred, so we don't have to // redraw unnecessarily, ie if no move bigger than MINDRAG or MINZOOM px - var fullLayout = gd._fullLayout; var zoomlayer = gd._fullLayout._zoomlayer; var isMainDrag = (ns + ew === 'nsew'); var singleEnd = (ns + ew).length === 1; - var subplots, xa, ya, xs, ys, pw, ph, xActive, yActive, cursor, - isSubplotConstrained, xaLinked, yaLinked; + // main subplot x and y (i.e. found in plotinfo - the main ones) + var xa0, ya0; + // {ax._id: ax} hash objects + var xaHash, yaHash; + // xaHash/yaHash values (arrays) + var xaxes, yaxes; + // main axis offsets + var xs, ys; + // main axis lengths + var pw, ph; + // contains keys 'xaHash', 'yaHash', 'xaxes', and 'yaxes' + // which are the x/y {ax._id: ax} hash objects and their values + // for linked axis relative to this subplot + var links; + // set to ew/ns val when active, set to '' when inactive + var xActive, yActive; + // are all axes in this subplot are fixed? + var allFixedRanges; + // is subplot constrained? + var isSubplotConstrained; + // do we need to edit x/y ranges? + var editX, editY; function recomputeAxisLists() { - xa = [plotinfo.xaxis]; - ya = [plotinfo.yaxis]; - var xa0 = xa[0]; - var ya0 = ya[0]; + xa0 = plotinfo.xaxis; + ya0 = plotinfo.yaxis; pw = xa0._length; ph = ya0._length; + xs = xa0._offset; + ys = ya0._offset; - var constraintGroups = fullLayout._axisConstraintGroups; - var xIDs = [xa0._id]; - var yIDs = [ya0._id]; + xaHash = {}; + xaHash[xa0._id] = xa0; + yaHash = {}; + yaHash[ya0._id] = ya0; // if we're dragging two axes at once, also drag overlays - subplots = [plotinfo].concat((ns && ew) ? plotinfo.overlays : []); - - for(var i = 1; i < subplots.length; i++) { - var subplotXa = subplots[i].xaxis, - subplotYa = subplots[i].yaxis; - - if(xa.indexOf(subplotXa) === -1) { - xa.push(subplotXa); - xIDs.push(subplotXa._id); - } - - if(ya.indexOf(subplotYa) === -1) { - ya.push(subplotYa); - yIDs.push(subplotYa._id); + if(ns && ew) { + var overlays = plotinfo.overlays; + for(var i = 0; i < overlays.length; i++) { + var xa = overlays[i].xaxis; + xaHash[xa._id] = xa; + var ya = overlays[i].yaxis; + yaHash[ya._id] = ya; } } - xActive = isDirectionActive(xa, ew); - yActive = isDirectionActive(ya, ns); - cursor = getDragCursor(yActive + xActive, fullLayout.dragmode); - xs = xa0._offset; - ys = ya0._offset; - - var links = calcLinks(constraintGroups, xIDs, yIDs); - isSubplotConstrained = links.xy; + xaxes = hashValues(xaHash); + yaxes = hashValues(yaHash); + xActive = isDirectionActive(xaxes, ew); + yActive = isDirectionActive(yaxes, ns); + allFixedRanges = !yActive && !xActive; - // finally make the list of axis objects to link - xaLinked = []; - for(var xLinkID in links.x) { xaLinked.push(getFromId(gd, xLinkID)); } - yaLinked = []; - for(var yLinkID in links.y) { yaLinked.push(getFromId(gd, yLinkID)); } + links = calcLinks(gd, xaHash, yaHash); + isSubplotConstrained = links.isSubplotConstrained; + editX = ew || isSubplotConstrained; + editY = ns || isSubplotConstrained; } recomputeAxisLists(); + var cursor = getDragCursor(yActive + xActive, gd._fullLayout.dragmode, isMainDrag); var dragger = makeRectDragger(plotinfo, ns + ew + 'drag', cursor, x, y, w, h); - var allFixedRanges = !yActive && !xActive; - // still need to make the element if the axes are disabled // but nuke its events (except for maindrag which needs them for hover) // and stop there @@ -131,6 +139,8 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) { prepFn: function(e, startX, startY) { var dragModeNow = gd._fullLayout.dragmode; + recomputeAxisLists(); + if(!allFixedRanges) { if(isMainDrag) { // main dragger handles all drag modes, and changes @@ -151,8 +161,8 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) { else dragOptions.minDrag = undefined; if(isSelectOrLasso(dragModeNow)) { - dragOptions.xaxes = xa; - dragOptions.yaxes = ya; + dragOptions.xaxes = xaxes; + dragOptions.yaxes = yaxes; prepSelect(e, startX, startY, dragOptions, dragModeNow); } else if(allFixedRanges) { @@ -184,7 +194,7 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) { Fx.click(gd, evt, plotinfo.id); } else if(numClicks === 1 && singleEnd) { - var ax = ns ? ya[0] : xa[0], + var ax = ns ? ya0 : xa0, end = (ns === 's' || ew === 'w') ? 0 : 1, attrStr = ax._name + '.range[' + end + ']', initialText = getEndText(ax, end), @@ -204,7 +214,7 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) { .call(svgTextUtils.makeEditable, { gd: gd, immediate: true, - background: fullLayout.paper_bgcolor, + background: gd._fullLayout.paper_bgcolor, text: String(initialText), fill: ax.tickfont ? ax.tickfont.color : '#444', horizontalAlign: hAlign, @@ -334,8 +344,12 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) { } // TODO: edit linked axes in zoomAxRanges and in dragTail - if(zoomMode === 'xy' || zoomMode === 'x') zoomAxRanges(xa, box.l / pw, box.r / pw, updates, xaLinked); - if(zoomMode === 'xy' || zoomMode === 'y') zoomAxRanges(ya, (ph - box.b) / ph, (ph - box.t) / ph, updates, yaLinked); + if(zoomMode === 'xy' || zoomMode === 'x') { + zoomAxRanges(xaxes, box.l / pw, box.r / pw, updates, links.xaxes); + } + if(zoomMode === 'xy' || zoomMode === 'y') { + zoomAxRanges(yaxes, (ph - box.b) / ph, (ph - box.t) / ph, updates, links.yaxes); + } removeZoombox(gd); dragTail(); @@ -347,14 +361,13 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) { // wait a little after scrolling before redrawing var redrawTimer = null; var REDRAWDELAY = constants.REDRAWDELAY; - var mainplot = plotinfo.mainplot ? - fullLayout._plots[plotinfo.mainplot] : plotinfo; + var mainplot = plotinfo.mainplot ? gd._fullLayout._plots[plotinfo.mainplot] : plotinfo; function zoomWheel(e) { // deactivate mousewheel scrolling on embedded graphs // devs can override this with layout._enablescrollzoom, // but _ ensures this setting won't leave their page - if(!gd._context.scrollZoom && !fullLayout._enablescrollzoom) { + if(!gd._context.scrollZoom && !gd._fullLayout._enablescrollzoom) { return; } @@ -405,20 +418,24 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) { ax.range = axRange.map(doZoom); } - if(ew || isSubplotConstrained) { + if(editX) { // if we're only zooming this axis because of constraints, // zoom it about the center if(!ew) xfrac = 0.5; - for(i = 0; i < xa.length; i++) zoomWheelOneAxis(xa[i], xfrac, zoom); + for(i = 0; i < xaxes.length; i++) { + zoomWheelOneAxis(xaxes[i], xfrac, zoom); + } scrollViewBox[2] *= zoom; scrollViewBox[0] += scrollViewBox[2] * xfrac * (1 / zoom - 1); } - if(ns || isSubplotConstrained) { + if(editY) { if(!ns) yfrac = 0.5; - for(i = 0; i < ya.length; i++) zoomWheelOneAxis(ya[i], yfrac, zoom); + for(i = 0; i < yaxes.length; i++) { + zoomWheelOneAxis(yaxes[i], yfrac, zoom); + } scrollViewBox[3] *= zoom; scrollViewBox[1] += scrollViewBox[3] * (1 - yfrac) * (1 / zoom - 1); @@ -456,11 +473,9 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) { return; } - recomputeAxisLists(); - if(xActive === 'ew' || yActive === 'ns') { - if(xActive) dragAxList(xa, dx); - if(yActive) dragAxList(ya, dy); + if(xActive) dragAxList(xaxes, dx); + if(yActive) dragAxList(yaxes, dy); updateSubplots([xActive ? -dx : 0, yActive ? -dy : 0, pw, ph]); ticksAndAnnotations(yActive, xActive); return; @@ -500,12 +515,12 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) { dy = dxySign * dxyFraction * ph; } - if(xActive === 'w') dx = dz(xa, 0, dx); - else if(xActive === 'e') dx = dz(xa, 1, -dx); + if(xActive === 'w') dx = dz(xaxes, 0, dx); + else if(xActive === 'e') dx = dz(xaxes, 1, -dx); else if(!xActive) dx = 0; - if(yActive === 'n') dy = dz(ya, 1, dy); - else if(yActive === 's') dy = dz(ya, 0, -dy); + if(yActive === 'n') dy = dz(yaxes, 1, dy); + else if(yActive === 's') dy = dz(yaxes, 0, -dy); else if(!yActive) dy = 0; var x0 = (xActive === 'w') ? dx : 0; @@ -516,17 +531,17 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) { if(!xActive && yActive.length === 1) { // dragging one end of the y axis of a constrained subplot // scale the other axis the same about its middle - for(i = 0; i < xa.length; i++) { - xa[i].range = xa[i]._r.slice(); - scaleZoom(xa[i], 1 - dy / ph); + for(i = 0; i < xaxes.length; i++) { + xaxes[i].range = xaxes[i]._r.slice(); + scaleZoom(xaxes[i], 1 - dy / ph); } dx = dy * pw / ph; x0 = dx / 2; } if(!yActive && xActive.length === 1) { - for(i = 0; i < ya.length; i++) { - ya[i].range = ya[i]._r.slice(); - scaleZoom(ya[i], 1 - dx / pw); + for(i = 0; i < yaxes.length; i++) { + yaxes[i].range = yaxes[i]._r.slice(); + scaleZoom(yaxes[i], 1 - dx / pw); } dy = dx * ph / pw; y0 = dy / 2; @@ -534,7 +549,6 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) { } updateSubplots([x0, y0, pw - dx, ph - dy]); - ticksAndAnnotations(yActive, xActive); } @@ -550,13 +564,13 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) { } } - if(ew || isSubplotConstrained) { - pushActiveAxIds(xa); - pushActiveAxIds(xaLinked); + if(editX) { + pushActiveAxIds(xaxes); + pushActiveAxIds(links.xaxes); } - if(ns || isSubplotConstrained) { - pushActiveAxIds(ya); - pushActiveAxIds(yaLinked); + if(editY) { + pushActiveAxIds(yaxes); + pushActiveAxIds(links.yaxes); } updates = {}; @@ -584,16 +598,16 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) { // annotations and shapes 'draw' method is slow, // use the finer-grained 'drawOne' method instead - redrawObjs(fullLayout.annotations || [], Registry.getComponentMethod('annotations', 'drawOne')); - redrawObjs(fullLayout.shapes || [], Registry.getComponentMethod('shapes', 'drawOne')); - redrawObjs(fullLayout.images || [], Registry.getComponentMethod('images', 'draw'), true); + redrawObjs(gd._fullLayout.annotations || [], Registry.getComponentMethod('annotations', 'drawOne')); + redrawObjs(gd._fullLayout.shapes || [], Registry.getComponentMethod('shapes', 'drawOne')); + redrawObjs(gd._fullLayout.images || [], Registry.getComponentMethod('images', 'draw'), true); } function doubleClick() { if(gd._transitioningWithDuration) return; var doubleClickConfig = gd._context.doubleClick, - axList = (xActive ? xa : []).concat(yActive ? ya : []), + axList = (xActive ? xaxes : []).concat(yActive ? yaxes : []), attrs = {}; var ax, i, rangeInitial; @@ -632,12 +646,12 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) { else if(doubleClickConfig === 'reset') { // when we're resetting, reset all linked axes too, so we get back // to the fully-auto-with-constraints situation - if(xActive || isSubplotConstrained) axList = axList.concat(xaLinked); - if(yActive && !isSubplotConstrained) axList = axList.concat(yaLinked); + if(xActive || isSubplotConstrained) axList = axList.concat(links.xaxes); + if(yActive && !isSubplotConstrained) axList = axList.concat(links.yaxes); if(isSubplotConstrained) { - if(!xActive) axList = axList.concat(xa); - else if(!yActive) axList = axList.concat(ya); + if(!xActive) axList = axList.concat(xaxes); + else if(!yActive) axList = axList.concat(yaxes); } for(i = 0; i < axList.length; i++) { @@ -675,122 +689,154 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) { // updateSubplots - find all plot viewboxes that should be // affected by this drag, and update them. look for all plots - // sharing an affected axis (including the one being dragged) + // sharing an affected axis (including the one being dragged), + // includes also scattergl and splom logic. function updateSubplots(viewBox) { + var fullLayout = gd._fullLayout; var plotinfos = fullLayout._plots; - var subplots = Object.keys(plotinfos); - var xScaleFactor = viewBox[2] / xa[0]._length; - var yScaleFactor = viewBox[3] / ya[0]._length; - var editX = ew || isSubplotConstrained; - var editY = ns || isSubplotConstrained; - - var i, xScaleFactor2, yScaleFactor2, clipDx, clipDy; - - // Find the appropriate scaling for this axis, if it's linked to the - // dragged axes by constraints. 0 is special, it means this axis shouldn't - // ever be scaled (will be converted to 1 if the other axis is scaled) - function getLinkedScaleFactor(ax) { - if(ax.fixedrange) return 0; - - if(editX && xaLinked.indexOf(ax) !== -1) { - return xScaleFactor; - } - if(editY && (isSubplotConstrained ? xaLinked : yaLinked).indexOf(ax) !== -1) { - return yScaleFactor; - } - return 0; - } + var subplots = fullLayout._subplots.cartesian; - function scaleAndGetShift(ax, scaleFactor) { - if(scaleFactor) { - ax.range = ax._r.slice(); - scaleZoom(ax, scaleFactor); - return getShift(ax, scaleFactor); - } - return 0; + // TODO can we move these to outer scope? + var hasScatterGl = fullLayout._has('scattergl'); + var hasOnlyLargeSploms = fullLayout._hasOnlyLargeSploms; + var hasSplom = hasOnlyLargeSploms || fullLayout._has('splom'); + var hasSVG = fullLayout._has('svg'); + var hasDraggedPts = fullLayout._has('draggedPts'); + + var i, sp, xa, ya; + + if(hasSplom || hasScatterGl) { + clearGlCanvases(gd); } - function getShift(ax, scaleFactor) { - return ax._length * (1 - scaleFactor) * FROM_TL[ax.constraintoward || 'middle']; + if(hasSplom) { + Registry.subplotsRegistry.splom.drag(gd); + if(hasOnlyLargeSploms) return; } - clearGlCanvases(gd); - - for(i = 0; i < subplots.length; i++) { - var subplot = plotinfos[subplots[i]], - xa2 = subplot.xaxis, - ya2 = subplot.yaxis, - editX2 = editX && !xa2.fixedrange && (xa.indexOf(xa2) !== -1), - editY2 = editY && !ya2.fixedrange && (ya.indexOf(ya2) !== -1); - - // scattergl translate - if(subplot._scene && subplot._scene.update) { - // FIXME: possibly we could update axis internal _r and _rl here - var xaRange = Lib.simpleMap(xa2.range, xa2.r2l); - var yaRange = Lib.simpleMap(ya2.range, ya2.r2l); - subplot._scene.update( - {range: [xaRange[0], yaRange[0], xaRange[1], yaRange[1]]} - ); + if(hasScatterGl) { + // loop over all subplots (w/o exceptions) here, + // as we cleared the gl canvases above + for(i = 0; i < subplots.length; i++) { + sp = plotinfos[subplots[i]]; + xa = sp.xaxis; + ya = sp.yaxis; + + var scene = sp._scene; + if(scene) { + // FIXME: possibly we could update axis internal _r and _rl here + var xrng = Lib.simpleMap(xa.range, xa.r2l); + var yrng = Lib.simpleMap(ya.range, ya.r2l); + scene.update({range: [xrng[0], yrng[0], xrng[1], yrng[1]]}); + } } + } - if(editX2) { - xScaleFactor2 = xScaleFactor; - clipDx = ew ? viewBox[0] : getShift(xa2, xScaleFactor2); - } - else { - xScaleFactor2 = getLinkedScaleFactor(xa2); - clipDx = scaleAndGetShift(xa2, xScaleFactor2); - } + if(hasSVG) { + var xScaleFactor = viewBox[2] / xa0._length; + var yScaleFactor = viewBox[3] / ya0._length; - if(editY2) { - yScaleFactor2 = yScaleFactor; - clipDy = ns ? viewBox[1] : getShift(ya2, yScaleFactor2); - } - else { - yScaleFactor2 = getLinkedScaleFactor(ya2); - clipDy = scaleAndGetShift(ya2, yScaleFactor2); - } + for(i = 0; i < subplots.length; i++) { + sp = plotinfos[subplots[i]]; + xa = sp.xaxis; + ya = sp.yaxis; - // don't scale at all if neither axis is scalable here - if(!xScaleFactor2 && !yScaleFactor2) { - continue; - } + var editX2 = editX && !xa.fixedrange && xaHash[xa._id]; + var editY2 = editY && !ya.fixedrange && yaHash[ya._id]; - // but if only one is, reset the other axis scaling - if(!xScaleFactor2) xScaleFactor2 = 1; - if(!yScaleFactor2) yScaleFactor2 = 1; + var xScaleFactor2, yScaleFactor2; + var clipDx, clipDy; - var plotDx = xa2._offset - clipDx / xScaleFactor2, - plotDy = ya2._offset - clipDy / yScaleFactor2; + if(editX2) { + xScaleFactor2 = xScaleFactor; + clipDx = ew ? viewBox[0] : getShift(xa, xScaleFactor2); + } else { + xScaleFactor2 = getLinkedScaleFactor(xa, xScaleFactor, yScaleFactor); + clipDx = scaleAndGetShift(xa, xScaleFactor2); + } - fullLayout._defs.select('#' + subplot.clipId + '> rect') - .call(Drawing.setTranslate, clipDx, clipDy) - .call(Drawing.setScale, xScaleFactor2, yScaleFactor2); + if(editY2) { + yScaleFactor2 = yScaleFactor; + clipDy = ns ? viewBox[1] : getShift(ya, yScaleFactor2); + } else { + yScaleFactor2 = getLinkedScaleFactor(ya, xScaleFactor, yScaleFactor); + clipDy = scaleAndGetShift(ya, yScaleFactor2); + } - var traceGroups = subplot.plot - .selectAll('.scatterlayer .trace, .boxlayer .trace, .violinlayer .trace'); + // don't scale at all if neither axis is scalable here + if(!xScaleFactor2 && !yScaleFactor2) { + continue; + } - subplot.plot - .call(Drawing.setTranslate, plotDx, plotDy) - .call(Drawing.setScale, 1 / xScaleFactor2, 1 / yScaleFactor2); + // but if only one is, reset the other axis scaling + if(!xScaleFactor2) xScaleFactor2 = 1; + if(!yScaleFactor2) yScaleFactor2 = 1; + + var plotDx = xa._offset - clipDx / xScaleFactor2; + var plotDy = ya._offset - clipDy / yScaleFactor2; + + // TODO could be more efficient here: + // setTranslate and setScale do a lot of extra work + // when working independently, should perhaps combine + // them into a single routine. + sp.clipRect + .call(Drawing.setTranslate, clipDx, clipDy) + .call(Drawing.setScale, xScaleFactor2, yScaleFactor2); + + sp.plot + .call(Drawing.setTranslate, plotDx, plotDy) + .call(Drawing.setScale, 1 / xScaleFactor2, 1 / yScaleFactor2); + + // TODO move these selectAll calls out of here + // and stash them somewhere nice, see: + // https://github.com/plotly/plotly.js/issues/2548 + if(hasDraggedPts) { + var traceGroups = sp.plot + .selectAll('.scatterlayer .trace, .boxlayer .trace, .violinlayer .trace'); + + // This is specifically directed at marker points in scatter, box and violin traces, + // applying an inverse scale to individual points to counteract + // the scale of the trace as a whole: + traceGroups.selectAll('.point') + .call(Drawing.setPointGroupScale, xScaleFactor2, yScaleFactor2); + traceGroups.selectAll('.textpoint') + .call(Drawing.setTextPointsScale, xScaleFactor2, yScaleFactor2); + traceGroups + .call(Drawing.hideOutsideRangePoints, sp); + + sp.plot.selectAll('.barlayer .trace') + .call(Drawing.hideOutsideRangePoints, sp, '.bartext'); + } + } + } + } - // This is specifically directed at marker points in scatter, box and violin traces, - // applying an inverse scale to individual points to counteract - // the scale of the trace as a whole: - traceGroups.selectAll('.point') - .call(Drawing.setPointGroupScale, xScaleFactor2, yScaleFactor2); - traceGroups.selectAll('.textpoint') - .call(Drawing.setTextPointsScale, xScaleFactor2, yScaleFactor2); - traceGroups - .call(Drawing.hideOutsideRangePoints, subplot); + // Find the appropriate scaling for this axis, if it's linked to the + // dragged axes by constraints. 0 is special, it means this axis shouldn't + // ever be scaled (will be converted to 1 if the other axis is scaled) + function getLinkedScaleFactor(ax, xScaleFactor, yScaleFactor) { + if(ax.fixedrange) return 0; - subplot.plot.selectAll('.barlayer .trace') - .call(Drawing.hideOutsideRangePoints, subplot, '.bartext'); + if(editX && links.xaHash[ax._id]) { + return xScaleFactor; + } + if(editY && (isSubplotConstrained ? links.xaHash : links.yaHash)[ax._id]) { + return yScaleFactor; } + return 0; + } - if(Registry.subplotsRegistry.splom) { - Registry.subplotsRegistry.splom.drag(gd); + function scaleAndGetShift(ax, scaleFactor) { + if(scaleFactor) { + ax.range = ax._r.slice(); + scaleZoom(ax, scaleFactor); + return getShift(ax, scaleFactor); } + return 0; + } + + function getShift(ax, scaleFactor) { + return ax._length * (1 - scaleFactor) * FROM_TL[ax.constraintoward || 'middle']; } return dragger; @@ -892,9 +938,12 @@ function dZoom(d) { 1 / (1 / Math.max(d, -0.3) + 3.222)); } -function getDragCursor(nsew, dragmode) { +function getDragCursor(nsew, dragmode, isMainDrag) { if(!nsew) return 'pointer'; if(nsew === 'nsew') { + // in this case here, clear cursor and + // use the cursor style set on + if(isMainDrag) return ''; if(dragmode === 'pan') return 'move'; return 'crosshair'; } @@ -997,40 +1046,40 @@ function xyCorners(box) { 'h' + clen + 'v3h-' + (clen + 3) + 'Z'; } -function calcLinks(constraintGroups, xIDs, yIDs) { +function calcLinks(gd, xaHash, yaHash) { + var constraintGroups = gd._fullLayout._axisConstraintGroups; var isSubplotConstrained = false; var xLinks = {}; var yLinks = {}; - var i, j, k; + var xID, yID, xLinkID, yLinkID; - var group, xLinkID, yLinkID; - for(i = 0; i < constraintGroups.length; i++) { - group = constraintGroups[i]; + for(var i = 0; i < constraintGroups.length; i++) { + var group = constraintGroups[i]; // check if any of the x axes we're dragging is in this constraint group - for(j = 0; j < xIDs.length; j++) { - if(group[xIDs[j]]) { + for(xID in xaHash) { + if(group[xID]) { // put the rest of these axes into xLinks, if we're not already // dragging them, so we know to scale these axes automatically too // to match the changes in the dragged x axes for(xLinkID in group) { - if((xLinkID.charAt(0) === 'x' ? xIDs : yIDs).indexOf(xLinkID) === -1) { + if(!(xLinkID.charAt(0) === 'x' ? xaHash : yaHash)[xLinkID]) { xLinks[xLinkID] = 1; } } // check if the x and y axes of THIS drag are linked - for(k = 0; k < yIDs.length; k++) { - if(group[yIDs[k]]) isSubplotConstrained = true; + for(yID in yaHash) { + if(group[yID]) isSubplotConstrained = true; } } } // now check if any of the y axes we're dragging is in this constraint group // only look for outside links, as we've already checked for links within the dragger - for(j = 0; j < yIDs.length; j++) { - if(group[yIDs[j]]) { + for(yID in yaHash) { + if(group[yID]) { for(yLinkID in group) { - if((yLinkID.charAt(0) === 'x' ? xIDs : yIDs).indexOf(yLinkID) === -1) { + if(!(yLinkID.charAt(0) === 'x' ? xaHash : yaHash)[yLinkID]) { yLinks[yLinkID] = 1; } } @@ -1045,10 +1094,29 @@ function calcLinks(constraintGroups, xIDs, yIDs) { Lib.extendFlat(xLinks, yLinks); yLinks = {}; } + + var xaHashLinked = {}; + var xaxesLinked = []; + for(xLinkID in xLinks) { + var xa = getFromId(gd, xLinkID); + xaxesLinked.push(xa); + xaHashLinked[xa._id] = xa; + } + + var yaHashLinked = {}; + var yaxesLinked = []; + for(yLinkID in yLinks) { + var ya = getFromId(gd, yLinkID); + yaxesLinked.push(ya); + yaHashLinked[ya._id] = ya; + } + return { - x: xLinks, - y: yLinks, - xy: isSubplotConstrained + xaHash: xaHashLinked, + yaHash: yaHashLinked, + xaxes: xaxesLinked, + yaxes: yaxesLinked, + isSubplotConstrained: isSubplotConstrained }; } @@ -1070,6 +1138,12 @@ function attachWheelEventHandler(element, handler) { } } +function hashValues(hash) { + var out = []; + for(var k in hash) out.push(hash[k]); + return out; +} + module.exports = { makeDragBox: makeDragBox, diff --git a/src/plots/cartesian/graph_interact.js b/src/plots/cartesian/graph_interact.js index 7508b288ae7..f872ddef16e 100644 --- a/src/plots/cartesian/graph_interact.js +++ b/src/plots/cartesian/graph_interact.js @@ -13,11 +13,12 @@ var d3 = require('d3'); var Fx = require('../../components/fx'); var dragElement = require('../../components/dragelement'); +var setCursor = require('../../lib/setcursor'); -var constants = require('./constants'); var makeDragBox = require('./dragbox').makeDragBox; +var DRAGGERSIZE = require('./constants').DRAGGERSIZE; -module.exports = function initInteractions(gd) { +exports.initInteractions = function initInteractions(gd) { var fullLayout = gd._fullLayout; if(gd._context.staticPlot) { @@ -43,12 +44,9 @@ module.exports = function initInteractions(gd) { subplots.forEach(function(subplot) { var plotinfo = fullLayout._plots[subplot]; - var xa = plotinfo.xaxis; var ya = plotinfo.yaxis; - var DRAGGERSIZE = constants.DRAGGERSIZE; - // main and corner draggers need not be repeated for // overlaid subplots - these draggers drag them all if(!plotinfo.mainplot) { @@ -139,17 +137,29 @@ module.exports = function initInteractions(gd) { var hoverLayer = fullLayout._hoverlayer.node(); hoverLayer.onmousemove = function(evt) { - evt.target = fullLayout._lasthover; + evt.target = gd._fullLayout._lasthover; Fx.hover(gd, evt, fullLayout._hoversubplot); }; hoverLayer.onclick = function(evt) { - evt.target = fullLayout._lasthover; + evt.target = gd._fullLayout._lasthover; Fx.click(gd, evt); }; // also delegate mousedowns... TODO: does this actually work? hoverLayer.onmousedown = function(evt) { - fullLayout._lasthover.onmousedown(evt); + gd._fullLayout._lasthover.onmousedown(evt); }; + + exports.updateFx(fullLayout); +}; + +// Minimal set of update needed on 'modebar' edits. +// We only need to update the cursor style. +// +// Note that changing the axis configuration and/or the fixedrange attribute +// should trigger a full initInteractions. +exports.updateFx = function(fullLayout) { + var cursor = fullLayout.dragmode === 'pan' ? 'move' : 'crosshair'; + setCursor(fullLayout._draggers, cursor); }; diff --git a/src/plots/cartesian/index.js b/src/plots/cartesian/index.js index 16fed7c3f88..eca498b809b 100644 --- a/src/plots/cartesian/index.js +++ b/src/plots/cartesian/index.js @@ -548,3 +548,5 @@ exports.toSVG = function(gd) { canvases.each(canvasToImage); }; + +exports.updateFx = require('./graph_interact').updateFx; diff --git a/src/plots/cartesian/layout_attributes.js b/src/plots/cartesian/layout_attributes.js index 8334ab5d0ec..25bfda44393 100644 --- a/src/plots/cartesian/layout_attributes.js +++ b/src/plots/cartesian/layout_attributes.js @@ -100,10 +100,10 @@ module.exports = { valType: 'info_array', role: 'info', items: [ - {valType: 'any', editType: 'plot+margins', impliedEdits: {'^autorange': false}}, - {valType: 'any', editType: 'plot+margins', impliedEdits: {'^autorange': false}} + {valType: 'any', editType: 'axrange+margins', impliedEdits: {'^autorange': false}}, + {valType: 'any', editType: 'axrange+margins', impliedEdits: {'^autorange': false}} ], - editType: 'plot+margins', + editType: 'axrange+margins', impliedEdits: {'autorange': false}, description: [ 'Sets the range of this axis.', diff --git a/src/plots/cartesian/transition_axes.js b/src/plots/cartesian/transition_axes.js index 281b9964b9b..c8d306d5e0e 100644 --- a/src/plots/cartesian/transition_axes.js +++ b/src/plots/cartesian/transition_axes.js @@ -233,7 +233,7 @@ module.exports = function transitionAxes(gd, newLayout, transitionOpts, makeOnCo var plotDx = xa2._offset - fracDx, plotDy = ya2._offset - fracDy; - fullLayout._defs.select('#' + subplot.clipId + '> rect') + subplot.clipRect .call(Drawing.setTranslate, clipDx, clipDy) .call(Drawing.setScale, 1 / xScaleFactor, 1 / yScaleFactor); diff --git a/src/plots/plots.js b/src/plots/plots.js index 74c9ab8b872..7becaef923d 100644 --- a/src/plots/plots.js +++ b/src/plots/plots.js @@ -630,24 +630,22 @@ plots.createTransitionData = function(gd) { // whether a certain plot type is present on plot // or trace has a category plots._hasPlotType = function(category) { - // check plot - var basePlotModules = this._basePlotModules || []; var i; + // check base plot modules + var basePlotModules = this._basePlotModules || []; for(i = 0; i < basePlotModules.length; i++) { - var _module = basePlotModules[i]; - - if(_module.name === category) return true; + if(basePlotModules[i].name === category) return true; } - // check trace + // check trace modules var modules = this._modules || []; - for(i = 0; i < modules.length; i++) { - var modulei = modules[i]; - if(modulei.categories && modulei.categories.indexOf(category) >= 0) { - return true; - } + var name = modules[i].name; + if(name === category) return true; + // N.B. this is modules[i] along with 'categories' as a hash object + var _module = Registry.modules[name]; + if(_module && _module.categories[category]) return true; } return false; diff --git a/src/traces/bar/index.js b/src/traces/bar/index.js index 6ff430354b8..1798b2a7c88 100644 --- a/src/traces/bar/index.js +++ b/src/traces/bar/index.js @@ -27,7 +27,7 @@ Bar.selectPoints = require('./select'); Bar.moduleType = 'trace'; Bar.name = 'bar'; Bar.basePlotModule = require('../../plots/cartesian'); -Bar.categories = ['cartesian', 'bar', 'oriented', 'markerColorscale', 'errorBarsOK', 'showLegend']; +Bar.categories = ['cartesian', 'svg', 'bar', 'oriented', 'markerColorscale', 'errorBarsOK', 'showLegend', 'draggedPts']; Bar.meta = { description: [ 'The data visualized by the span of the bars is set in `y`', diff --git a/src/traces/box/index.js b/src/traces/box/index.js index 5395dd0af66..ad32d7000aa 100644 --- a/src/traces/box/index.js +++ b/src/traces/box/index.js @@ -24,7 +24,7 @@ Box.selectPoints = require('./select'); Box.moduleType = 'trace'; Box.name = 'box'; Box.basePlotModule = require('../../plots/cartesian'); -Box.categories = ['cartesian', 'symbols', 'oriented', 'box-violin', 'showLegend']; +Box.categories = ['cartesian', 'svg', 'symbols', 'oriented', 'box-violin', 'showLegend', 'draggedPts']; Box.meta = { description: [ 'In vertical (horizontal) box plots,', diff --git a/src/traces/candlestick/index.js b/src/traces/candlestick/index.js index dff5c003935..ef94d47bc17 100644 --- a/src/traces/candlestick/index.js +++ b/src/traces/candlestick/index.js @@ -14,7 +14,7 @@ module.exports = { moduleType: 'trace', name: 'candlestick', basePlotModule: require('../../plots/cartesian'), - categories: ['cartesian', 'showLegend', 'candlestick'], + categories: ['cartesian', 'svg', 'showLegend', 'candlestick'], meta: { description: [ 'The candlestick is a style of financial chart describing', diff --git a/src/traces/carpet/index.js b/src/traces/carpet/index.js index e97fc2c1789..0a4c6a36b3f 100644 --- a/src/traces/carpet/index.js +++ b/src/traces/carpet/index.js @@ -20,7 +20,7 @@ Carpet.animatable = true; Carpet.moduleType = 'trace'; Carpet.name = 'carpet'; Carpet.basePlotModule = require('../../plots/cartesian'); -Carpet.categories = ['cartesian', 'carpet', 'carpetAxis', 'notLegendIsolatable']; +Carpet.categories = ['cartesian', 'svg', 'carpet', 'carpetAxis', 'notLegendIsolatable']; Carpet.meta = { description: [ 'The data describing carpet axis layout is set in `y` and (optionally)', diff --git a/src/traces/contour/index.js b/src/traces/contour/index.js index f56f61cd7ec..f498cf78d98 100644 --- a/src/traces/contour/index.js +++ b/src/traces/contour/index.js @@ -22,7 +22,7 @@ Contour.hoverPoints = require('./hover'); Contour.moduleType = 'trace'; Contour.name = 'contour'; Contour.basePlotModule = require('../../plots/cartesian'); -Contour.categories = ['cartesian', '2dMap', 'contour', 'showLegend']; +Contour.categories = ['cartesian', 'svg', '2dMap', 'contour', 'showLegend']; Contour.meta = { description: [ 'The data from which contour lines are computed is set in `z`.', diff --git a/src/traces/contourcarpet/index.js b/src/traces/contourcarpet/index.js index 7853dae6fc1..1529594f1bc 100644 --- a/src/traces/contourcarpet/index.js +++ b/src/traces/contourcarpet/index.js @@ -20,7 +20,7 @@ ContourCarpet.style = require('../contour/style'); ContourCarpet.moduleType = 'trace'; ContourCarpet.name = 'contourcarpet'; ContourCarpet.basePlotModule = require('../../plots/cartesian'); -ContourCarpet.categories = ['cartesian', 'carpet', 'contour', 'symbols', 'showLegend', 'hasLines', 'carpetDependent']; +ContourCarpet.categories = ['cartesian', 'svg', 'carpet', 'contour', 'symbols', 'showLegend', 'hasLines', 'carpetDependent']; ContourCarpet.meta = { hrName: 'contour_carpet', description: [ diff --git a/src/traces/heatmap/index.js b/src/traces/heatmap/index.js index d50b941e377..12ccc878755 100644 --- a/src/traces/heatmap/index.js +++ b/src/traces/heatmap/index.js @@ -22,7 +22,7 @@ Heatmap.hoverPoints = require('./hover'); Heatmap.moduleType = 'trace'; Heatmap.name = 'heatmap'; Heatmap.basePlotModule = require('../../plots/cartesian'); -Heatmap.categories = ['cartesian', '2dMap']; +Heatmap.categories = ['cartesian', 'svg', '2dMap']; Heatmap.meta = { description: [ 'The data that describes the heatmap value-to-color mapping', diff --git a/src/traces/histogram/index.js b/src/traces/histogram/index.js index f1c107e7555..c0949a06447 100644 --- a/src/traces/histogram/index.js +++ b/src/traces/histogram/index.js @@ -41,7 +41,7 @@ Histogram.eventData = require('./event_data'); Histogram.moduleType = 'trace'; Histogram.name = 'histogram'; Histogram.basePlotModule = require('../../plots/cartesian'); -Histogram.categories = ['cartesian', 'bar', 'histogram', 'oriented', 'errorBarsOK', 'showLegend']; +Histogram.categories = ['cartesian', 'svg', 'bar', 'histogram', 'oriented', 'errorBarsOK', 'showLegend']; Histogram.meta = { description: [ 'The sample data from which statistics are computed is set in `x`', diff --git a/src/traces/histogram2d/index.js b/src/traces/histogram2d/index.js index 324a952598e..c8e9695b8dc 100644 --- a/src/traces/histogram2d/index.js +++ b/src/traces/histogram2d/index.js @@ -23,7 +23,7 @@ Histogram2D.eventData = require('../histogram/event_data'); Histogram2D.moduleType = 'trace'; Histogram2D.name = 'histogram2d'; Histogram2D.basePlotModule = require('../../plots/cartesian'); -Histogram2D.categories = ['cartesian', '2dMap', 'histogram']; +Histogram2D.categories = ['cartesian', 'svg', '2dMap', 'histogram']; Histogram2D.meta = { hrName: 'histogram_2d', description: [ diff --git a/src/traces/histogram2dcontour/index.js b/src/traces/histogram2dcontour/index.js index 9c3d5f2e2da..7953ff48354 100644 --- a/src/traces/histogram2dcontour/index.js +++ b/src/traces/histogram2dcontour/index.js @@ -22,7 +22,7 @@ Histogram2dContour.hoverPoints = require('../contour/hover'); Histogram2dContour.moduleType = 'trace'; Histogram2dContour.name = 'histogram2dcontour'; Histogram2dContour.basePlotModule = require('../../plots/cartesian'); -Histogram2dContour.categories = ['cartesian', '2dMap', 'contour', 'histogram']; +Histogram2dContour.categories = ['cartesian', 'svg', '2dMap', 'contour', 'histogram']; Histogram2dContour.meta = { hrName: 'histogram_2d_contour', description: [ diff --git a/src/traces/ohlc/index.js b/src/traces/ohlc/index.js index d8d2f12f650..cabac0d8568 100644 --- a/src/traces/ohlc/index.js +++ b/src/traces/ohlc/index.js @@ -14,7 +14,7 @@ module.exports = { moduleType: 'trace', name: 'ohlc', basePlotModule: require('../../plots/cartesian'), - categories: ['cartesian', 'showLegend'], + categories: ['cartesian', 'svg', 'showLegend'], meta: { description: [ 'The ohlc (short for Open-High-Low-Close) is a style of financial chart describing', diff --git a/src/traces/scatter/index.js b/src/traces/scatter/index.js index 8bca686de6a..3808dcf7628 100644 --- a/src/traces/scatter/index.js +++ b/src/traces/scatter/index.js @@ -34,7 +34,7 @@ Scatter.animatable = true; Scatter.moduleType = 'trace'; Scatter.name = 'scatter'; Scatter.basePlotModule = require('../../plots/cartesian'); -Scatter.categories = ['cartesian', 'symbols', 'markerColorscale', 'errorBarsOK', 'showLegend', 'scatter-like']; +Scatter.categories = ['cartesian', 'svg', 'symbols', 'markerColorscale', 'errorBarsOK', 'showLegend', 'scatter-like', 'draggedPts']; Scatter.meta = { description: [ 'The scatter trace type encompasses line charts, scatter charts, text charts, and bubble charts.', diff --git a/src/traces/scattercarpet/index.js b/src/traces/scattercarpet/index.js index 689f4cedf5f..c8864a6c45d 100644 --- a/src/traces/scattercarpet/index.js +++ b/src/traces/scattercarpet/index.js @@ -23,7 +23,7 @@ ScatterCarpet.eventData = require('./event_data'); ScatterCarpet.moduleType = 'trace'; ScatterCarpet.name = 'scattercarpet'; ScatterCarpet.basePlotModule = require('../../plots/cartesian'); -ScatterCarpet.categories = ['carpet', 'symbols', 'markerColorscale', 'showLegend', 'carpetDependent']; +ScatterCarpet.categories = ['svg', 'carpet', 'symbols', 'markerColorscale', 'showLegend', 'carpetDependent', 'draggedPts']; ScatterCarpet.meta = { hrName: 'scatter_carpet', description: [ diff --git a/src/traces/violin/index.js b/src/traces/violin/index.js index c97355078d2..26e39f644f6 100644 --- a/src/traces/violin/index.js +++ b/src/traces/violin/index.js @@ -23,7 +23,7 @@ module.exports = { moduleType: 'trace', name: 'violin', basePlotModule: require('../../plots/cartesian'), - categories: ['cartesian', 'symbols', 'oriented', 'box-violin', 'showLegend'], + categories: ['cartesian', 'svg', 'symbols', 'oriented', 'box-violin', 'showLegend', 'draggedPts'], meta: { description: [ 'In vertical (horizontal) violin plots,', diff --git a/test/jasmine/tests/cartesian_interact_test.js b/test/jasmine/tests/cartesian_interact_test.js index bb9d6c3073b..d105bb5b4a3 100644 --- a/test/jasmine/tests/cartesian_interact_test.js +++ b/test/jasmine/tests/cartesian_interact_test.js @@ -530,6 +530,30 @@ describe('axis zoom/pan and main plot zoom', function() { .catch(failTest) .then(done); }); + + it('updates linked axes when there are constraints (axes_scaleanchor mock)', function(done) { + var fig = Lib.extendDeep({}, require('@mocks/axes_scaleanchor.json')); + + function _assert(y3rng, y4rng) { + expect(gd._fullLayout.yaxis3.range).toBeCloseToArray(y3rng, 2, 'y3 rng'); + expect(gd._fullLayout.yaxis4.range).toBeCloseToArray(y4rng, 2, 'y3 rng'); + } + + Plotly.plot(gd, fig) + .then(function() { + _assert([-0.36, 4.36], [-0.36, 4.36]); + }) + .then(doDrag('x2y3', 'nsew', 0, 100)) + .then(function() { + _assert([-0.36, 2], [0.82, 3.18]); + }) + .then(doDrag('x2y4', 'nsew', 0, 50)) + .then(function() { + _assert([0.41, 1.23], [1.18, 2]); + }) + .catch(failTest) + .then(done); + }); }); describe('Event data:', function() { diff --git a/test/jasmine/tests/fx_test.js b/test/jasmine/tests/fx_test.js index 293d482e63d..50b2172c9e9 100644 --- a/test/jasmine/tests/fx_test.js +++ b/test/jasmine/tests/fx_test.js @@ -201,13 +201,12 @@ describe('relayout', function() { afterEach(destroyGraphDiv); it('should update main drag with correct', function(done) { - function assertMainDrag(cursor, isActive) { expect(d3.selectAll('rect.nsewdrag').size()).toEqual(1, 'number of nodes'); - var mainDrag = d3.select('rect.nsewdrag'), - node = mainDrag.node(); + var mainDrag = d3.select('rect.nsewdrag'); + var node = mainDrag.node(); - expect(mainDrag.classed('cursor-' + cursor)).toBe(true, 'cursor ' + cursor); + expect(window.getComputedStyle(node).cursor).toBe(cursor, 'cursor ' + cursor); expect(node.style.pointerEvents).toEqual('all', 'pointer event'); expect(!!node.onmousedown).toBe(isActive, 'mousedown handler'); } diff --git a/test/jasmine/tests/plot_api_test.js b/test/jasmine/tests/plot_api_test.js index 7fe0c98693e..688d5b2d892 100644 --- a/test/jasmine/tests/plot_api_test.js +++ b/test/jasmine/tests/plot_api_test.js @@ -504,7 +504,10 @@ describe('Test plot api', function() { 'layoutStyles', 'doTicksRelayout', 'doModeBar', - 'doCamera' + 'doCamera', + 'doAutoRangeAndConstraints', + 'drawData', + 'finalDraw' ]; var gd; @@ -613,6 +616,46 @@ describe('Test plot api', function() { expectReplot(attr); } }); + + it('should trigger minimal sequence for cartesian axis range updates', function() { + var seq = ['doAutoRangeAndConstraints', 'doTicksRelayout', 'drawData', 'finalDraw']; + + function _assert(msg) { + expect(gd.calcdata).toBeDefined(); + mockedMethods.forEach(function(m) { + expect(subroutines[m].calls.count()).toBe( + seq.indexOf(m) === -1 ? 0 : 1, + '# of ' + m + ' calls - ' + msg + ); + }); + } + + var trace = {y: [1, 2, 1]}; + + var specs = [ + ['relayout', ['xaxis.range[0]', 0]], + ['relayout', ['xaxis.range[1]', 3]], + ['relayout', ['xaxis.range', [-1, 5]]], + ['update', [{}, {'xaxis.range': [-1, 10]}]], + ['react', [[trace], {xaxis: {range: [0, 1]}}]] + ]; + + specs.forEach(function(s) { + // create 'real' div for Plotly.react to work + gd = createGraphDiv(); + Plotly.plot(gd, [trace], {xaxis: {range: [1, 2]}}); + mock(gd); + + Plotly[s[0]](gd, s[1][0], s[1][1]); + + _assert([ + 'Plotly.', s[0], + '(gd, ', JSON.stringify(s[1][0]), ', ', JSON.stringify(s[1][1]), ')' + ].join('')); + + destroyGraphDiv(); + }); + }); }); describe('Plotly.restyle subroutines switchboard', function() {