diff --git a/src/components/annotations/draw.js b/src/components/annotations/draw.js index 7669ab2a2da..ed18530d558 100644 --- a/src/components/annotations/draw.js +++ b/src/components/annotations/draw.js @@ -584,7 +584,7 @@ function drawRaw(gd, options, index, subplotId, xa, ya) { }); }, doneFn: function() { - Registry.call('relayout', gd, getUpdateObj()); + Registry.call('_guiRelayout', gd, getUpdateObj()); var notesBox = document.querySelector('.js-notes-box-panel'); if(notesBox) notesBox.redraw(notesBox.selectedObj); } @@ -667,7 +667,7 @@ function drawRaw(gd, options, index, subplotId, xa, ya) { }, doneFn: function() { setCursor(annTextGroupInner); - Registry.call('relayout', gd, getUpdateObj()); + Registry.call('_guiRelayout', gd, getUpdateObj()); var notesBox = document.querySelector('.js-notes-box-panel'); if(notesBox) notesBox.redraw(notesBox.selectedObj); } @@ -691,7 +691,7 @@ function drawRaw(gd, options, index, subplotId, xa, ya) { modifyBase(ya._name + '.autorange', true); } - Registry.call('relayout', gd, getUpdateObj()); + Registry.call('_guiRelayout', gd, getUpdateObj()); }); } else annText.call(textLayout); diff --git a/src/components/colorbar/draw.js b/src/components/colorbar/draw.js index 18d776a2873..71c5b9dd7ff 100644 --- a/src/components/colorbar/draw.js +++ b/src/components/colorbar/draw.js @@ -627,7 +627,7 @@ module.exports = function draw(gd, id) { setCursor(container); if(xf !== undefined && yf !== undefined) { - Registry.call('restyle', + Registry.call('_guiRestyle', gd, {'colorbar.x': xf, 'colorbar.y': yf}, getTrace().index diff --git a/src/components/legend/draw.js b/src/components/legend/draw.js index 30d3c66aa7e..9b444268489 100644 --- a/src/components/legend/draw.js +++ b/src/components/legend/draw.js @@ -339,7 +339,7 @@ module.exports = function draw(gd) { }, doneFn: function() { if(xf !== undefined && yf !== undefined) { - Registry.call('relayout', gd, {'legend.x': xf, 'legend.y': yf}); + Registry.call('_guiRelayout', gd, {'legend.x': xf, 'legend.y': yf}); } }, clickFn: function(numClicks, e) { @@ -446,7 +446,7 @@ function drawTexts(g, gd, maxLength) { update.name = newName; } - return Registry.call('restyle', gd, update, traceIndex); + return Registry.call('_guiRestyle', gd, update, traceIndex); }); } else { textLayout(textEl); diff --git a/src/components/legend/handle_click.js b/src/components/legend/handle_click.js index b12bf50e8e3..608c4a272da 100644 --- a/src/components/legend/handle_click.js +++ b/src/components/legend/handle_click.js @@ -111,7 +111,7 @@ module.exports = function handleClick(g, gd, numClicks) { } } - Registry.call('relayout', gd, 'hiddenlabels', hiddenSlices); + Registry.call('_guiRelayout', gd, 'hiddenlabels', hiddenSlices); } else { var hasLegendgroup = legendgroup && legendgroup.length; var traceIndicesInGroup = []; @@ -217,6 +217,6 @@ module.exports = function handleClick(g, gd, numClicks) { } } - Registry.call('restyle', gd, attrUpdate, attrIndices); + Registry.call('_guiRestyle', gd, attrUpdate, attrIndices); } }; diff --git a/src/components/modebar/buttons.js b/src/components/modebar/buttons.js index 6d1ce4a6369..eca5d398ead 100644 --- a/src/components/modebar/buttons.js +++ b/src/components/modebar/buttons.js @@ -261,7 +261,7 @@ function handleCartesian(gd, ev) { aobj[astr] = val; } - Registry.call('relayout', gd, aobj); + Registry.call('_guiRelayout', gd, aobj); } modeBarButtons.zoom3d = { @@ -317,7 +317,7 @@ function handleDrag3d(gd, ev) { var val2d = (val === 'pan') ? val : 'zoom'; layoutUpdate.dragmode = val2d; - Registry.call('relayout', gd, layoutUpdate); + Registry.call('_guiRelayout', gd, layoutUpdate); } modeBarButtons.resetCameraDefault3d = { @@ -356,7 +356,7 @@ function handleCamera3d(gd, ev) { } } - Registry.call('relayout', gd, aobj); + Registry.call('_guiRelayout', gd, aobj); } modeBarButtons.hoverClosest3d = { @@ -411,7 +411,7 @@ function getNextHover3d(gd, ev) { function handleHover3d(gd, ev) { var layoutUpdate = getNextHover3d(gd, ev); - Registry.call('relayout', gd, layoutUpdate); + Registry.call('_guiRelayout', gd, layoutUpdate); } modeBarButtons.zoomInGeo = { @@ -467,7 +467,7 @@ function handleGeo(gd, ev) { var scale = geoLayout.projection.scale; var newScale = (val === 'in') ? 2 * scale : 0.5 * scale; - Registry.call('relayout', gd, id + '.projection.scale', newScale); + Registry.call('_guiRelayout', gd, id + '.projection.scale', newScale); } else if(attr === 'reset') { resetView(gd, 'geo'); } @@ -508,7 +508,7 @@ function getNextHover(gd) { function toggleHover(gd) { var newHover = getNextHover(gd); - Registry.call('relayout', gd, 'hovermode', newHover); + Registry.call('_guiRelayout', gd, 'hovermode', newHover); } // buttons when more then one plot types are present @@ -525,7 +525,7 @@ modeBarButtons.toggleHover = { var layoutUpdate = getNextHover3d(gd, ev); layoutUpdate.hovermode = getNextHover(gd); - Registry.call('relayout', gd, layoutUpdate); + Registry.call('_guiRelayout', gd, layoutUpdate); } }; @@ -561,7 +561,7 @@ modeBarButtons.toggleSpikelines = { var aobj = setSpikelineVisibility(gd); - Registry.call('relayout', gd, aobj); + Registry.call('_guiRelayout', gd, aobj); } }; @@ -608,5 +608,5 @@ function resetView(gd, subplotType) { } } - Registry.call('relayout', gd, aObj); + Registry.call('_guiRelayout', gd, aObj); } diff --git a/src/components/rangeselector/draw.js b/src/components/rangeselector/draw.js index aa717d663b5..f3bbe3e5505 100644 --- a/src/components/rangeselector/draw.js +++ b/src/components/rangeselector/draw.js @@ -69,7 +69,7 @@ module.exports = function draw(gd) { button.on('click', function() { if(gd._dragged) return; - Registry.call('relayout', gd, update); + Registry.call('_guiRelayout', gd, update); }); button.on('mouseover', function() { diff --git a/src/components/rangeslider/draw.js b/src/components/rangeslider/draw.js index e1d0b9f609c..5d0bf72f23e 100644 --- a/src/components/rangeslider/draw.js +++ b/src/components/rangeslider/draw.js @@ -286,7 +286,10 @@ function setDataRange(rangeSlider, gd, axisOpts, opts) { dataMax = clamp(opts.p2d(opts._pixelMax)); window.requestAnimationFrame(function() { - Registry.call('relayout', gd, axisOpts._name + '.range', [dataMin, dataMax]); + var update = {}; + update[axisOpts._name + '.range[0]'] = dataMin; + update[axisOpts._name + '.range[1]'] = dataMax; + Registry.call('_guiRelayout', gd, update); }); } diff --git a/src/components/shapes/draw.js b/src/components/shapes/draw.js index 793536597a1..ca24704caa3 100644 --- a/src/components/shapes/draw.js +++ b/src/components/shapes/draw.js @@ -306,7 +306,7 @@ function setupDragElement(gd, shapePath, shapeOptions, index, shapeLayer) { // Don't rely on clipPath being activated during re-layout setClipPath(shapePath, gd, shapeOptions); - Registry.call('relayout', gd, editHelpers.getUpdateObj()); + Registry.call('_guiRelayout', gd, editHelpers.getUpdateObj()); } function abortDrag() { diff --git a/src/components/titles/index.js b/src/components/titles/index.js index e2211dd88df..8fed7ba9684 100644 --- a/src/components/titles/index.js +++ b/src/components/titles/index.js @@ -237,9 +237,9 @@ function draw(gd, titleClass, options) { el.call(svgTextUtils.makeEditable, {gd: gd}) .on('edit', function(text) { if(traceIndex !== undefined) { - Registry.call('restyle', gd, prop, text, traceIndex); + Registry.call('_guiRestyle', gd, prop, text, traceIndex); } else { - Registry.call('relayout', gd, prop, text); + Registry.call('_guiRelayout', gd, prop, text); } }) .on('cancel', function() { diff --git a/src/core.js b/src/core.js index 1025c37d4a7..04a026f7d6c 100644 --- a/src/core.js +++ b/src/core.js @@ -29,7 +29,8 @@ var plotApi = require('./plot_api'); var methodNames = Object.keys(plotApi); for(var i = 0; i < methodNames.length; i++) { var name = methodNames[i]; - exports[name] = plotApi[name]; + // _ -> private API methods, but still registered for internal use + if(name.charAt(0) !== '_') exports[name] = plotApi[name]; register({ moduleType: 'apiMethod', name: name, diff --git a/src/plot_api/index.js b/src/plot_api/index.js index ac81c327b05..410b2da6f87 100644 --- a/src/plot_api/index.js +++ b/src/plot_api/index.js @@ -16,6 +16,10 @@ exports.restyle = main.restyle; exports.relayout = main.relayout; exports.redraw = main.redraw; exports.update = main.update; +exports._guiRestyle = main._guiRestyle; +exports._guiRelayout = main._guiRelayout; +exports._guiUpdate = main._guiUpdate; +exports._storeDirectGUIEdit = main._storeDirectGUIEdit; exports.react = main.react; exports.extendTraces = main.extendTraces; exports.prependTraces = main.prependTraces; diff --git a/src/plot_api/plot_api.js b/src/plot_api/plot_api.js index bf9ba602080..f9c907fa8e5 100644 --- a/src/plot_api/plot_api.js +++ b/src/plot_api/plot_api.js @@ -1315,7 +1315,7 @@ exports.moveTraces = function moveTraces(gd, currentIndices, newIndices) { * If the array is too short, it will wrap around (useful for * style files that want to specify cyclical default values). */ -exports.restyle = function restyle(gd, astr, val, _traces) { +function restyle(gd, astr, val, _traces) { gd = Lib.getGraphDiv(gd); helpers.clearPromiseQueue(gd); @@ -1385,7 +1385,8 @@ exports.restyle = function restyle(gd, astr, val, _traces) { gd.emit('plotly_restyle', specs.eventData); return gd; }); -}; +} +exports.restyle = restyle; // for undo: undefined initial vals must be turned into nulls // so that we unset rather than ignore them @@ -1394,11 +1395,74 @@ function undefinedToNull(val) { return val; } +/** + * modified Lib.nestedProperty to also record GUI edits + */ +function npWithGuiEdits(container, attr, preGUI, guiEditFlag) { + var np = nestedProperty(container, attr); + + var npSet = np.set; + function setWithGuiEdits(val) { + // preGUI is not nested, it's flat with attribute strings + // flatten it the rest of the way, even if currentVal is nested + storeCurrent(attr, np.get(), val, preGUI); + npSet(val); + } + + if(guiEditFlag) { + np.set = setWithGuiEdits; + } + + return np; +} + +function storeCurrent(attr, val, newVal, preGUI) { + if(Array.isArray(val) || Array.isArray(newVal)) { + var arrayVal = Array.isArray(val) ? val : []; + var arrayNew = Array.isArray(newVal) ? newVal : []; + var maxLen = Math.max(arrayVal.length, arrayNew.length); + for(var i = 0; i < maxLen; i++) { + storeCurrent(attr + '[' + i + ']', arrayVal[i], arrayNew[i], preGUI); + } + } + else if(Lib.isPlainObject(val) || Lib.isPlainObject(newVal)) { + var objVal = Lib.isPlainObject(val) ? val : {}; + var objNew = Lib.isPlainObject(newVal) ? newVal : {}; + var objBoth = Lib.extendFlat({}, objVal, objNew); + for(var key in objBoth) { + storeCurrent(attr + '.' + key, objVal[key], objNew[key], preGUI); + } + } + else if(preGUI[attr] === undefined) { + preGUI[attr] = undefinedToNull(val); + } +} + +/* + * storeDirectGUIEdit: for routines that skip restyle/relayout and mock it + * by emitting a plotly_restyle or plotly_relayout event, this routine + * applies the changes to the input objects and keeps track of the initial state + * in _preGUI for use by uirevision + * + * @param {object} container: the input attributes container (eg `layout` or a `trace`) + * @param {object} fullContainer: the full partner to `container` + * @param {object} edits: the {attr: val} object as normally passed to `relayout` etc + * @param {boolean} apply: should we apply these changes to the input object? + */ +exports._storeDirectGUIEdit = function(container, preGUI, edits, apply) { + for(var attr in edits) { + var np = nestedProperty(container, attr); + storeCurrent(attr, np.get(), edits[attr], preGUI); + if(apply) np.set(edits[attr]); + } +}; + function _restyle(gd, aobj, traces) { - var fullLayout = gd._fullLayout, - fullData = gd._fullData, - data = gd.data, - i; + var fullLayout = gd._fullLayout; + var fullData = gd._fullData; + var data = gd.data; + var guiEditFlag = fullLayout._guiEditing; + var i; // initialize flags var flags = editTypes.traceFlags(); @@ -1447,9 +1511,11 @@ function _restyle(gd, aobj, traces) { var extraparam; if(attr.substr(0, 6) === 'LAYOUT') { - extraparam = Lib.nestedProperty(gd.layout, attr.replace('LAYOUT', '')); + extraparam = npWithGuiEdits(gd.layout, attr.replace('LAYOUT', ''), fullLayout._preGUI, guiEditFlag); } else { - extraparam = Lib.nestedProperty(data[traces[i]], attr); + var tracei = traces[i]; + var preGUI = fullLayout._tracePreGUI[getFullTrace(tracei)._fullInput.uid]; + extraparam = npWithGuiEdits(data[tracei], attr, preGUI, guiEditFlag); } if(!(attr in undoit)) { @@ -1504,7 +1570,7 @@ function _restyle(gd, aobj, traces) { redoit[ai] = vi; if(ai.substr(0, 6) === 'LAYOUT') { - param = Lib.nestedProperty(gd.layout, ai.replace('LAYOUT', '')); + param = npWithGuiEdits(gd.layout, ai.replace('LAYOUT', ''), fullLayout._preGUI, guiEditFlag); undoit[ai] = [undefinedToNull(param.get())]; // since we're allowing val to be an array, allow it here too, // even though that's meaningless @@ -1520,7 +1586,8 @@ function _restyle(gd, aobj, traces) { for(i = 0; i < traces.length; i++) { cont = data[traces[i]]; contFull = getFullTrace(traces[i]); - param = Lib.nestedProperty(cont, ai); + var preGUI = fullLayout._tracePreGUI[contFull._fullInput.uid]; + param = npWithGuiEdits(cont, ai, preGUI, guiEditFlag); oldVal = param.get(); newVal = Array.isArray(vi) ? vi[i % vi.length] : vi; @@ -1718,7 +1785,7 @@ function _restyle(gd, aobj, traces) { * attribute object `{astr1: val1, astr2: val2 ...}` * allows setting multiple attributes simultaneously */ -exports.relayout = function relayout(gd, astr, val) { +function relayout(gd, astr, val) { gd = Lib.getGraphDiv(gd); helpers.clearPromiseQueue(gd); @@ -1781,7 +1848,8 @@ exports.relayout = function relayout(gd, astr, val) { gd.emit('plotly_relayout', specs.eventData); return gd; }); -}; +} +exports.relayout = relayout; // Optimization mostly for large splom traces where // Plots.supplyDefaults can take > 100ms @@ -1826,14 +1894,14 @@ var AX_AUTORANGE_RE = /^[xyz]axis[0-9]*\.autorange$/; var AX_DOMAIN_RE = /^[xyz]axis[0-9]*\.domain(\[[0|1]\])?$/; function _relayout(gd, aobj) { - var layout = gd.layout, - fullLayout = gd._fullLayout, - keys = Object.keys(aobj), - axes = Axes.list(gd), - arrayEdits = {}, - arrayStr, - i, - j; + var layout = gd.layout; + var fullLayout = gd._fullLayout; + var guiEditFlag = fullLayout._guiEditing; + var keys = Object.keys(aobj); + var axes = Axes.list(gd); + var arrayEdits = {}; + + var arrayStr, i, j; // look for 'allaxes', split out into all axes // in case of 3D the axis are nested within a scene which is held in _id @@ -1873,7 +1941,7 @@ function _relayout(gd, aobj) { // via a parent) do not override with this auto-generated extra if(attr in aobj || helpers.hasParent(aobj, attr)) return; - var p = Lib.nestedProperty(layout, attr); + var p = npWithGuiEdits(layout, attr, fullLayout._preGUI, guiEditFlag); if(!(attr in undoit)) { undoit[attr] = undefinedToNull(p.get()); } @@ -1898,7 +1966,7 @@ function _relayout(gd, aobj) { throw new Error('cannot set ' + ai + 'and a parent attribute simultaneously'); } - var p = Lib.nestedProperty(layout, ai); + var p = npWithGuiEdits(layout, ai, fullLayout._preGUI, guiEditFlag); var vi = aobj[ai]; var plen = p.parts.length; // p.parts may end with an index integer if the property is an array @@ -2194,7 +2262,7 @@ function updateAutosize(gd) { * integer or array of integers for the traces to alter (all if omitted) * */ -exports.update = function update(gd, traceUpdate, layoutUpdate, _traces) { +function update(gd, traceUpdate, layoutUpdate, _traces) { gd = Lib.getGraphDiv(gd); helpers.clearPromiseQueue(gd); @@ -2274,7 +2342,25 @@ exports.update = function update(gd, traceUpdate, layoutUpdate, _traces) { return gd; }); -}; +} +exports.update = update; + +/* + * internal-use-only restyle/relayout/update variants that record the initial + * values in (fullLayout|fullTrace)._preGUI so changes can be persisted across + * Plotly.react data updates, dependent on uirevision attributes + */ +function guiEdit(func) { + return function wrappedEdit(gd) { + gd._fullLayout._guiEditing = true; + var p = func.apply(null, arguments); + gd._fullLayout._guiEditing = false; + return p; + }; +} +exports._guiRestyle = guiEdit(restyle); +exports._guiRelayout = guiEdit(relayout); +exports._guiUpdate = guiEdit(update); /** * Plotly.react: diff --git a/src/plots/cartesian/dragbox.js b/src/plots/cartesian/dragbox.js index 6f8d03c8cc2..817bd4d1ad7 100644 --- a/src/plots/cartesian/dragbox.js +++ b/src/plots/cartesian/dragbox.js @@ -271,7 +271,7 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) { .on('edit', function(text) { var v = ax.d2r(text); if(v !== undefined) { - Registry.call('relayout', gd, attrStr, v); + Registry.call('_guiRelayout', gd, attrStr, v); } }); } @@ -712,7 +712,7 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) { } gd.emit('plotly_doubleclick', null); - Registry.call('relayout', gd, attrs); + Registry.call('_guiRelayout', gd, attrs); } // dragTail - finish a drag event with a redraw @@ -726,7 +726,7 @@ 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('relayout', gd, updates); } + function() { Registry.call('_guiRelayout', gd, updates); } ], gd); } diff --git a/src/plots/geo/geo.js b/src/plots/geo/geo.js index 83153e0dfe3..af681ea1f8e 100644 --- a/src/plots/geo/geo.js +++ b/src/plots/geo/geo.js @@ -367,7 +367,7 @@ proto.updateFx = function(fullLayout, geoLayout) { updateObj[_this.id + '.' + k] = viewInitial[k]; } - Registry.call('relayout', gd, updateObj); + Registry.call('_guiRelayout', gd, updateObj); gd.emit('plotly_doubleclick', null); } diff --git a/src/plots/geo/zoom.js b/src/plots/geo/zoom.js index f55e123f202..247ed1553c4 100644 --- a/src/plots/geo/zoom.js +++ b/src/plots/geo/zoom.js @@ -11,6 +11,7 @@ var d3 = require('d3'); var Lib = require('../../lib'); +var Registry = require('../../registry'); var radians = Math.PI / 180; var degrees = 180 / Math.PI; @@ -47,8 +48,10 @@ function initZoom(geo, projection) { function sync(geo, projection, cb) { var id = geo.id; var gd = geo.graphDiv; - var userOpts = gd.layout[id]; - var fullOpts = gd._fullLayout[id]; + var layout = gd.layout; + var userOpts = layout[id]; + var fullLayout = gd._fullLayout; + var fullOpts = fullLayout[id]; var eventData = {}; @@ -64,6 +67,7 @@ function sync(geo, projection, cb) { cb(set); set('projection.scale', projection.scale() / geo.fitScale); + Registry.call('_storeDirectGUIEdit', layout, fullLayout._preGUI, eventData); gd.emit('plotly_relayout', eventData); } diff --git a/src/plots/gl2d/scene2d.js b/src/plots/gl2d/scene2d.js index 56e12929508..d406125e4bf 100644 --- a/src/plots/gl2d/scene2d.js +++ b/src/plots/gl2d/scene2d.js @@ -313,27 +313,21 @@ proto.updateRefs = function(newFullLayout) { }; proto.relayoutCallback = function() { - var graphDiv = this.graphDiv, - xaxis = this.xaxis, - yaxis = this.yaxis, - layout = graphDiv.layout; - - // update user layout - layout.xaxis.autorange = xaxis.autorange; - layout.xaxis.range = xaxis.range.slice(0); - layout.yaxis.autorange = yaxis.autorange; - layout.yaxis.range = yaxis.range.slice(0); - - // make a meaningful value to be passed on to the possible 'plotly_relayout' subscriber(s) - // scene.camera has no many useful projection or scale information - // helps determine which one is the latest input (if async) - var update = { - lastInputTime: this.camera.lastInputTime - }; + var graphDiv = this.graphDiv; + var xaxis = this.xaxis; + var yaxis = this.yaxis; + + // make a meaningful value to be passed on to possible 'plotly_relayout' subscriber(s) + var update = {}; + update[xaxis._name + '.range'] = xaxis.range.slice(); + update[yaxis._name + '.range'] = yaxis.range.slice(); + update[xaxis._name + '.autorange'] = xaxis.autorange; + update[yaxis._name + '.autorange'] = yaxis.autorange; - update[xaxis._name] = xaxis.range.slice(0); - update[yaxis._name] = yaxis.range.slice(0); + Registry.call('_storeDirectGUIEdit', graphDiv.layout, graphDiv._fullLayout._preGUI, update, true); + // lastInputTime helps determine which one is the latest input (if async) + update.lastInputTime = this.camera.lastInputTime; graphDiv.emit('plotly_relayout', update); }; diff --git a/src/plots/gl3d/scene.js b/src/plots/gl3d/scene.js index 4911a4967ff..8b1e7eb8ed8 100644 --- a/src/plots/gl3d/scene.js +++ b/src/plots/gl3d/scene.js @@ -222,6 +222,7 @@ function initializeGLPlot(scene, fullLayout, canvas, gl) { var update = {}; update[scene.id + '.camera'] = getLayoutCamera(scene.camera); + Registry.call('_storeDirectGUIEdit', gd.layout, gd._fullLayout._preGUI, update); scene.saveCamera(gd.layout); scene.graphDiv.emit('plotly_relayout', update); }; diff --git a/src/plots/mapbox/mapbox.js b/src/plots/mapbox/mapbox.js index 77da3871017..f3b745cacdd 100644 --- a/src/plots/mapbox/mapbox.js +++ b/src/plots/mapbox/mapbox.js @@ -13,6 +13,7 @@ var mapboxgl = require('mapbox-gl'); var Fx = require('../../components/fx'); var Lib = require('../../lib'); +var Registry = require('../../registry'); var dragElement = require('../../components/dragelement'); var prepSelect = require('../cartesian/select').prepSelect; var selectOnClick = require('../cartesian/select').selectOnClick; @@ -210,6 +211,7 @@ proto.createMap = function(calcData, fullLayout, resolve, reject) { for(var k in view) { evtData[id + '.' + k] = view[k]; } + Registry.call('_storeDirectGUIEdit', gd.layout, gd._fullLayout._preGUI, evtData); gd.emit('plotly_relayout', evtData); } diff --git a/src/plots/plots.js b/src/plots/plots.js index a6d222da003..c053b8c9c6f 100644 --- a/src/plots/plots.js +++ b/src/plots/plots.js @@ -479,6 +479,24 @@ plots.supplyDefaults = function(gd, opts) { // relink functions and _ attributes to promote consistency between plots relinkPrivateKeys(newFullLayout, oldFullLayout); + // For persisting GUI-driven changes in layout + // _preGUI and _tracePreGUI were already copied over in relinkPrivateKeys + if(!newFullLayout._preGUI) newFullLayout._preGUI = {}; + // track trace GUI changes by uid rather than by trace index + if(!newFullLayout._tracePreGUI) newFullLayout._tracePreGUI = {}; + var tracePreGUI = newFullLayout._tracePreGUI; + var uids = {}; + var uid; + for(uid in tracePreGUI) uids[uid] = 'old'; + for(i = 0; i < newFullData.length; i++) { + uid = newFullData[i]._fullInput.uid; + if(!uids[uid]) tracePreGUI[uid] = {}; + uids[uid] = 'new'; + } + for(uid in uids) { + if(uids[uid] === 'old') delete tracePreGUI[uid]; + } + // TODO may return a promise plots.doAutoMargin(gd); diff --git a/src/plots/polar/polar.js b/src/plots/polar/polar.js index 97142b3a31d..02347fd474a 100644 --- a/src/plots/polar/polar.js +++ b/src/plots/polar/polar.js @@ -864,7 +864,7 @@ proto.updateMainDrag = function(fullLayout) { rl[0] + (r0 - innerRadius) * m, rl[0] + (r1 - innerRadius) * m ]; - Registry.call('relayout', gd, _this.id + '.radialaxis.range', newRng); + Registry.call('_guiRelayout', gd, _this.id + '.radialaxis.range', newRng); } function zoomClick(numClicks, evt) { @@ -880,7 +880,7 @@ proto.updateMainDrag = function(fullLayout) { } gd.emit('plotly_doubleclick', null); - Registry.call('relayout', gd, updateObj); + Registry.call('_guiRelayout', gd, updateObj); } if(clickMode.indexOf('select') > -1 && numClicks === 1) { @@ -1007,9 +1007,9 @@ proto.updateRadialDrag = function(fullLayout, polarLayout, rngIndex) { function doneFn() { if(angle1 !== null) { - Registry.call('relayout', gd, _this.id + '.radialaxis.angle', angle1); + Registry.call('_guiRelayout', gd, _this.id + '.radialaxis.angle', angle1); } else if(rprime !== null) { - Registry.call('relayout', gd, _this.id + '.radialaxis.range[' + rngIndex + ']', rprime); + Registry.call('_guiRelayout', gd, _this.id + '.radialaxis.range[' + rngIndex + ']', rprime); } } @@ -1221,7 +1221,7 @@ proto.updateAngularDrag = function(fullLayout) { updateObj[_this.id + '.radialaxis.angle'] = rrot1; } - Registry.call('relayout', gd, updateObj); + Registry.call('_guiRelayout', gd, updateObj); } dragOpts.prepFn = function(evt, startX, startY) { diff --git a/src/plots/ternary/ternary.js b/src/plots/ternary/ternary.js index a4ec4a7427c..f5563eb8f13 100644 --- a/src/plots/ternary/ternary.js +++ b/src/plots/ternary/ternary.js @@ -527,18 +527,22 @@ proto.initInteractions = function() { var x0, y0, mins0, span0, mins, lum, path0, dimmed, zb, corners; + function makeUpdate(_mins) { + var attrs = {}; + attrs[_this.id + '.aaxis.min'] = _mins.a; + attrs[_this.id + '.baxis.min'] = _mins.b; + attrs[_this.id + '.caxis.min'] = _mins.c; + return attrs; + } + function clickZoomPan(numClicks, evt) { var clickMode = gd._fullLayout.clickmode; removeZoombox(gd); if(numClicks === 2) { - var attrs = {}; - attrs[_this.id + '.aaxis.min'] = 0; - attrs[_this.id + '.baxis.min'] = 0; - attrs[_this.id + '.caxis.min'] = 0; gd.emit('plotly_doubleclick', null); - Registry.call('relayout', gd, attrs); + Registry.call('_guiRelayout', gd, makeUpdate({a: 0, b: 0, c: 0})); } if(clickMode.indexOf('select') > -1 && numClicks === 1) { @@ -642,12 +646,7 @@ proto.initInteractions = function() { if(mins === mins0) return; - var attrs = {}; - attrs[_this.id + '.aaxis.min'] = mins.a; - attrs[_this.id + '.baxis.min'] = mins.b; - attrs[_this.id + '.caxis.min'] = mins.c; - - Registry.call('relayout', gd, attrs); + Registry.call('_guiRelayout', gd, makeUpdate(mins)); if(SHOWZOOMOUTTIP && gd.data && gd._context.showTips) { Lib.notifier(_(gd, 'Double-click to zoom back out'), 'long'); @@ -721,12 +720,7 @@ proto.initInteractions = function() { } function dragDone() { - var attrs = {}; - attrs[_this.id + '.aaxis.min'] = mins.a; - attrs[_this.id + '.baxis.min'] = mins.b; - attrs[_this.id + '.caxis.min'] = mins.c; - - Registry.call('relayout', gd, attrs); + Registry.call('_guiRelayout', gd, makeUpdate(mins)); } // finally, set up hover and click diff --git a/src/traces/parcoords/plot.js b/src/traces/parcoords/plot.js index aef8c9581c4..3d280905c9b 100644 --- a/src/traces/parcoords/plot.js +++ b/src/traces/parcoords/plot.js @@ -8,6 +8,8 @@ 'use strict'; +var Registry = require('../../registry'); + var parcoords = require('./parcoords'); var prepareRegl = require('../../lib/prepare_regl'); @@ -22,12 +24,17 @@ module.exports = function plot(gd, cdparcoords) { var gdDimensions = {}; var gdDimensionsOriginalOrder = {}; + var fullIndices = {}; + var inputIndices = {}; var size = fullLayout._size; cdparcoords.forEach(function(d, i) { - gdDimensions[i] = gd.data[i].dimensions; - gdDimensionsOriginalOrder[i] = gd.data[i].dimensions.slice(); + var trace = d[0].trace; + fullIndices[i] = trace.index; + var iIn = inputIndices[i] = trace._fullInput.index; + gdDimensions[i] = gd.data[iIn].dimensions; + gdDimensionsOriginalOrder[i] = gd.data[iIn].dimensions.slice(); }); var filterChanged = function(i, originalDimensionIndex, newRanges) { @@ -48,10 +55,19 @@ module.exports = function plot(gd, cdparcoords) { newConstraints = [newConstraints]; } - var restyleData = {}; var aStr = 'dimensions[' + originalDimensionIndex + '].constraintrange'; + + var editData = {}; + editData[aStr] = newConstraints && newConstraints[0]; + Registry.call('_storeDirectGUIEdit', + gd.data[inputIndices[i]], + fullLayout._tracePreGUI[gd._fullData[fullIndices[i]]._fullInput.uid], + editData + ); + + var restyleData = {}; restyleData[aStr] = newConstraints; - gd.emit('plotly_restyle', [restyleData, [i]]); + gd.emit('plotly_restyle', [restyleData, [inputIndices[i]]]); }; var hover = function(eventData) { @@ -103,7 +119,17 @@ module.exports = function plot(gd, cdparcoords) { gdDimensions[i].splice(gdDimensionsOriginalOrder[i].indexOf(d), 0, d); // insert at original index }); - gd.emit('plotly_restyle'); + // TODO: we can't really store this part of the interaction state + // directly as below, since it incudes data arrays. If we want to + // persist column order we may have to do something special for this + // case to just store the order itself. + // Registry.call('_storeDirectGUIEdit', + // gd.data[inputIndices[i]], + // fullLayout._tracePreGUI[gd._fullData[fullIndices[i]]._fullInput.uid], + // {dimensions: gdDimensions[i]} + // ); + + gd.emit('plotly_restyle', [{dimensions: [gdDimensions[i]]}, [inputIndices[i]]]); }; parcoords( diff --git a/src/traces/table/plot.js b/src/traces/table/plot.js index 0ac8f2a50db..5e177cb3631 100644 --- a/src/traces/table/plot.js +++ b/src/traces/table/plot.js @@ -588,6 +588,9 @@ function columnMoved(gd, calcdata, indices) { calcdata.columnorder = indices; + // TODO: there's no data here, but also this reordering is not reflected + // in gd.data or even gd._fullData. + // For now I will not attempt to persist this in _preGUI gd.emit('plotly_restyle'); }