diff --git a/src/components/dragelement/index.js b/src/components/dragelement/index.js index faab912d9a7..f34e2103453 100644 --- a/src/components/dragelement/index.js +++ b/src/components/dragelement/index.js @@ -6,16 +6,13 @@ * LICENSE file in the root directory of this source tree. */ - 'use strict'; var mouseOffset = require('mouse-event-offset'); var hasHover = require('has-hover'); var supportsPassive = require('has-passive-events'); -var Registry = require('../../registry'); -var Lib = require('../../lib'); - +var removeElement = require('../../lib').removeElement; var constants = require('../../plots/cartesian/constants'); var interactConstants = require('../../constants/interactions'); @@ -28,7 +25,6 @@ var unhover = require('./unhover'); dragElement.unhover = unhover.wrapped; dragElement.unhoverRaw = unhover.raw; - /** * Abstracts click & drag interactions * @@ -106,8 +102,7 @@ dragElement.init = function init(options) { if(!supportsPassive) { element.ontouchstart = onStart; - } - else { + } else { if(element._ontouchstart) { element.removeEventListener('touchstart', element._ontouchstart); } @@ -145,8 +140,7 @@ dragElement.init = function init(options) { if(newMouseDownTime - gd._mouseDownTime < DBLCLICKDELAY) { // in a click train numClicks += 1; - } - else { + } else { // new click train numClicks = 1; gd._mouseDownTime = newMouseDownTime; @@ -157,8 +151,7 @@ dragElement.init = function init(options) { if(hasHover && !rightClick) { dragCover = coverSlip(); dragCover.style.cursor = window.getComputedStyle(element).cursor; - } - else if(!hasHover) { + } else if(!hasHover) { // document acts as a dragcover for mobile, bc we can't create dragcover dynamically dragCover = document; cursor = window.getComputedStyle(document.documentElement).cursor; @@ -191,12 +184,21 @@ dragElement.init = function init(options) { dragElement.unhover(gd); } - if(gd._dragged && options.moveFn && !rightClick) options.moveFn(dx, dy); + if(gd._dragged && options.moveFn && !rightClick) { + gd._dragdata = { + element: element, + dx: dx, + dy: dy + }; + options.moveFn(dx, dy); + } return; } function onDone(e) { + delete gd._dragdata; + if(options.dragmode !== false) { e.preventDefault(); document.removeEventListener('mousemove', onMove); @@ -207,9 +209,8 @@ dragElement.init = function init(options) { document.removeEventListener('touchend', onDone); if(hasHover) { - Lib.removeElement(dragCover); - } - else if(cursor) { + removeElement(dragCover); + } else if(cursor) { dragCover.documentElement.style.cursor = cursor; cursor = null; } @@ -228,8 +229,7 @@ dragElement.init = function init(options) { if(gd._dragged) { if(options.doneFn) options.doneFn(); - } - else { + } else { if(options.clickFn) options.clickFn(numClicks, initialEvent); // If we haven't dragged, this should be a click. But because of the @@ -258,10 +258,8 @@ dragElement.init = function init(options) { } } - finishDrag(gd); - + gd._dragging = false; gd._dragged = false; - return; } }; @@ -286,11 +284,6 @@ function coverSlip() { dragElement.coverSlip = coverSlip; -function finishDrag(gd) { - gd._dragging = false; - if(gd._replotPending) Registry.call('plot', gd); -} - function pointerOffset(e) { return mouseOffset( e.changedTouches ? e.changedTouches[0] : e, diff --git a/src/plot_api/plot_api.js b/src/plot_api/plot_api.js index b62620c0a7e..449fdb259c8 100644 --- a/src/plot_api/plot_api.js +++ b/src/plot_api/plot_api.js @@ -135,18 +135,8 @@ exports.plot = function(gd, data, layout, config) { gd.empty = false; } - if(!gd.layout || graphWasEmpty) gd.layout = helpers.cleanLayout(layout); - - // if the user is trying to drag the axes, allow new data and layout - // to come in but don't allow a replot. - if(gd._dragging && !gd._transitioning) { - // signal to drag handler that after everything else is done - // we need to replot, because something has changed - gd._replotPending = true; - return Promise.reject(); - } else { - // we're going ahead with a replot now - gd._replotPending = false; + if(!gd.layout || graphWasEmpty) { + gd.layout = helpers.cleanLayout(layout); } Plots.supplyDefaults(gd); @@ -195,7 +185,7 @@ exports.plot = function(gd, data, layout, config) { if(gd._context.responsive) { if(!gd._responsiveChartHandler) { // Keep a reference to the resize handler to purge it down the road - gd._responsiveChartHandler = function() {Plots.resize(gd);}; + gd._responsiveChartHandler = function() { Plots.resize(gd); }; // Listen to window resize window.addEventListener('resize', gd._responsiveChartHandler); @@ -242,10 +232,10 @@ exports.plot = function(gd, data, layout, config) { return 'gl-canvas gl-canvas-' + d.key.replace('Layer', ''); }) .style({ - 'position': 'absolute', - 'top': 0, - 'left': 0, - 'overflow': 'visible', + position: 'absolute', + top: 0, + left: 0, + overflow: 'visible', 'pointer-events': 'none' }); } @@ -384,6 +374,7 @@ exports.plot = function(gd, data, layout, config) { initInteractions, Plots.addLinks, Plots.rehover, + Plots.redrag, // TODO: doAutoMargin is only needed here for axis automargin, which // happens outside of marginPushers where all the other automargins are // calculated. Would be much better to separate margin calculations from @@ -1402,7 +1393,7 @@ function restyle(gd, astr, val, _traces) { seq.push(emitAfterPlot); } - seq.push(Plots.rehover); + seq.push(Plots.rehover, Plots.redrag); Queue.add(gd, restyle, [gd, specs.undoit, specs.traces], @@ -1911,7 +1902,7 @@ function relayout(gd, astr, val) { seq.push(emitAfterPlot); } - seq.push(Plots.rehover); + seq.push(Plots.rehover, Plots.redrag); Queue.add(gd, relayout, [gd, specs.undoit], @@ -1992,13 +1983,8 @@ function addAxRangeSequence(seq, rangesAltered) { return Axes.draw(gd, 'redraw'); }; - var _clearSelect = function(gd) { - var zoomlayer = gd._fullLayout._zoomlayer; - if(zoomlayer) clearSelect(zoomlayer); - }; - seq.push( - _clearSelect, + clearSelect, subroutines.doAutoRangeAndConstraints, drawAxes, subroutines.drawData, @@ -2449,7 +2435,7 @@ function update(gd, traceUpdate, layoutUpdate, _traces) { seq.push(emitAfterPlot); } - seq.push(Plots.rehover); + seq.push(Plots.rehover, Plots.redrag); Queue.add(gd, update, [gd, restyleSpecs.undoit, relayoutSpecs.undoit, restyleSpecs.traces], @@ -2852,7 +2838,7 @@ exports.react = function(gd, data, layout, config) { seq.push(emitAfterPlot); } - seq.push(Plots.rehover); + seq.push(Plots.rehover, Plots.redrag); plotDone = Lib.syncOrAsync(seq, gd); if(!plotDone || !plotDone.then) plotDone = Promise.resolve(gd); diff --git a/src/plots/cartesian/dragbox.js b/src/plots/cartesian/dragbox.js index 6096ba389d9..25be88e3862 100644 --- a/src/plots/cartesian/dragbox.js +++ b/src/plots/cartesian/dragbox.js @@ -6,7 +6,6 @@ * LICENSE file in the root directory of this source tree. */ - 'use strict'; var d3 = require('d3'); @@ -38,7 +37,6 @@ var constants = require('./constants'); var MINDRAG = constants.MINDRAG; var MINZOOM = constants.MINZOOM; - // flag for showing "doubleclick to zoom out" only at the beginning var SHOWZOOMOUTTIP = true; @@ -216,13 +214,30 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) { } } } + + gd._fullLayout._redrag = function() { + var dragDataNow = gd._dragdata; + + if(dragDataNow && dragDataNow.element === dragger) { + var dragModeNow = gd._fullLayout.dragmode; + + if(!isSelectOrLasso(dragModeNow)) { + recomputeAxisLists(); + updateSubplots([0, 0, pw, ph]); + dragOptions.moveFn(dragDataNow.dx, dragDataNow.dy); + } + + // TODO should we try to "re-select" under select/lasso modes? + // probably best to wait for https://github.com/plotly/plotly.js/issues/1851 + } + }; }; function clearAndResetSelect() { // clear selection polygon cache (if any) dragOptions.plotinfo.selection = false; // clear selection outlines - clearSelect(zoomlayer); + clearSelect(gd); } function clickFn(numClicks, evt) { @@ -587,8 +602,8 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) { else if(yActive === 's') dy = dz(yaxes, 0, -dy); else if(!yActive) dy = 0; - var x0 = (xActive === 'w') ? dx : 0; - var y0 = (yActive === 'n') ? dy : 0; + var xStart = (xActive === 'w') ? dx : 0; + var yStart = (yActive === 'n') ? dy : 0; if(links.isSubplotConstrained) { var i; @@ -600,7 +615,7 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) { scaleZoom(xaxes[i], 1 - dy / ph); } dx = dy * pw / ph; - x0 = dx / 2; + xStart = dx / 2; } if(!yActive && xActive.length === 1) { for(i = 0; i < yaxes.length; i++) { @@ -608,13 +623,13 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) { scaleZoom(yaxes[i], 1 - dx / pw); } dy = dx * ph / pw; - y0 = dy / 2; + yStart = dy / 2; } } updateMatchedAxRange('x'); updateMatchedAxRange('y'); - updateSubplots([x0, y0, pw - dx, ph - dy]); + updateSubplots([xStart, yStart, pw - dx, ph - dy]); ticksAndAnnotations(); } diff --git a/src/plots/cartesian/select.js b/src/plots/cartesian/select.js index a051adbe9ad..60653648ad9 100644 --- a/src/plots/cartesian/select.js +++ b/src/plots/cartesian/select.js @@ -20,6 +20,7 @@ var throttle = require('../../lib/throttle'); var makeEventData = require('../../components/fx/helpers').makeEventData; var getFromId = require('./axis_ids').getFromId; var clearGlCanvases = require('../../lib/clear_gl_canvases'); + var redrawReglTraces = require('../../plot_api/subroutines').redrawReglTraces; var constants = require('./constants'); @@ -465,14 +466,14 @@ function multiTester(list) { function coerceSelectionsCache(evt, gd, dragOptions) { var fullLayout = gd._fullLayout; - var zoomLayer = fullLayout._zoomlayer; var plotinfo = dragOptions.plotinfo; var selectingOnSameSubplot = ( - fullLayout._lastSelectedSubplot && - fullLayout._lastSelectedSubplot === plotinfo.id + fullLayout._lastSelectedSubplot && + fullLayout._lastSelectedSubplot === plotinfo.id ); var hasModifierKey = evt.shiftKey || evt.altKey; + if(selectingOnSameSubplot && hasModifierKey && (plotinfo.selection && plotinfo.selection.selectionDefs) && !dragOptions.selectionDefs) { // take over selection definitions from prev mode, if any @@ -484,7 +485,7 @@ function coerceSelectionsCache(evt, gd, dragOptions) { // clear selection outline when selecting a different subplot if(!selectingOnSameSubplot) { - clearSelect(zoomLayer); + clearSelect(gd); fullLayout._lastSelectedSubplot = plotinfo.id; } } @@ -669,7 +670,7 @@ function updateSelectedState(gd, searchTraces, eventData) { // before anything else, update preGUI if necessary for(i = 0; i < searchTraces.length; i++) { var fullInputTrace = searchTraces[i].cd[0].trace._fullInput; - var tracePreGUI = gd._fullLayout._tracePreGUI[fullInputTrace.uid]; + var tracePreGUI = gd._fullLayout._tracePreGUI[fullInputTrace.uid] || {}; if(tracePreGUI.selectedpoints === undefined) { tracePreGUI.selectedpoints = fullInputTrace._input.selectedpoints || null; } @@ -774,11 +775,15 @@ function fillSelectionItem(selection, searchInfo) { return selection; } -function clearSelect(zoomlayer) { - // until we get around to persistent selections, remove the outline - // here. The selection itself will be removed when the plot redraws - // at the end. - zoomlayer.selectAll('.select-outline').remove(); +// until we get around to persistent selections, remove the outline +// here. The selection itself will be removed when the plot redraws +// at the end. +function clearSelect(gd) { + var fullLayout = gd._fullLayout || {}; + var zoomlayer = fullLayout._zoomlayer; + if(zoomlayer) { + zoomlayer.selectAll('.select-outline').remove(); + } } module.exports = { diff --git a/src/plots/plots.js b/src/plots/plots.js index 466bd81af14..626699d4b5d 100644 --- a/src/plots/plots.js +++ b/src/plots/plots.js @@ -19,7 +19,7 @@ var Lib = require('../lib'); var Color = require('../components/color'); var BADNUM = require('../constants/numerical').BADNUM; -var axisIDs = require('../plots/cartesian/axis_ids'); +var axisIDs = require('./cartesian/axis_ids'); var animationAttrs = require('./animation_attributes'); var frameAttrs = require('./frame_attributes'); @@ -476,6 +476,15 @@ plots.supplyDefaults = function(gd, opts) { // clean subplots and other artifacts from previous plot calls plots.cleanPlot(newFullData, newFullLayout, oldFullData, oldFullLayout); + // clear selection outline until we implement persistent selection, + // don't clear them though when drag handlers (e.g. listening to + // `plotly_selecting`) update the graph. + // we should try to come up with a better solution when implementing + // https://github.com/plotly/plotly.js/issues/1851 + if(oldFullLayout._zoomlayer && !gd._dragging) { + oldFullLayout._zoomlayer.selectAll('.select-outline').remove(); + } + // relink functions and _ attributes to promote consistency between plots relinkPrivateKeys(newFullLayout, oldFullLayout); @@ -779,10 +788,6 @@ plots.cleanPlot = function(newFullData, newFullLayout, oldFullData, oldFullLayou oldFullLayout._infolayer.select('.cb' + oldUid).remove(); } } - - if(oldFullLayout._zoomlayer) { - oldFullLayout._zoomlayer.selectAll('.select-outline').remove(); - } }; plots.linkSubplots = function(newFullData, newFullLayout, oldFullData, oldFullLayout) { @@ -1679,10 +1684,10 @@ plots.purge = function(gd) { // themselves, but may not if there was an error delete gd._dragging; delete gd._dragged; + delete gd._dragdata; delete gd._hoverdata; delete gd._snapshotInProgress; delete gd._editing; - delete gd._replotPending; delete gd._mouseDownTime; delete gd._legendMouseDownTime; @@ -2907,6 +2912,12 @@ plots.rehover = function(gd) { } }; +plots.redrag = function(gd) { + if(gd._fullLayout._redrag) { + gd._fullLayout._redrag(); + } +}; + plots.generalUpdatePerTraceModule = function(gd, subplot, subplotCalcData, subplotLayout) { var traceHashOld = subplot.traceHash; var traceHash = {}; diff --git a/src/plots/polar/polar.js b/src/plots/polar/polar.js index f37772d2086..a6f33e2da6c 100644 --- a/src/plots/polar/polar.js +++ b/src/plots/polar/polar.js @@ -795,7 +795,7 @@ proto.updateMainDrag = function(fullLayout) { zb = dragBox.makeZoombox(zoomlayer, lum, cx, cy, path0); zb.attr('fill-rule', 'evenodd'); corners = dragBox.makeCorners(zoomlayer, cx, cy); - clearSelect(zoomlayer); + clearSelect(gd); } // N.B. this sets scoped 'r0' and 'r1' @@ -1115,7 +1115,7 @@ proto.updateRadialDrag = function(fullLayout, polarLayout, rngIndex) { dragOpts.moveFn = moveFn; dragOpts.doneFn = doneFn; - clearSelect(fullLayout._zoomlayer); + clearSelect(gd); }; dragOpts.clampFn = function(dx, dy) { @@ -1263,7 +1263,7 @@ proto.updateAngularDrag = function(fullLayout) { dragOpts.moveFn = moveFn; dragOpts.doneFn = doneFn; - clearSelect(fullLayout._zoomlayer); + clearSelect(gd); }; // I don't what we should do in this case, skip we now diff --git a/src/plots/ternary/ternary.js b/src/plots/ternary/ternary.js index f9ca0688efb..f0511d7dda5 100644 --- a/src/plots/ternary/ternary.js +++ b/src/plots/ternary/ternary.js @@ -493,7 +493,7 @@ proto.initInteractions = function() { var _this = this; var dragger = _this.layers.plotbg.select('path').node(); var gd = _this.graphDiv; - var zoomContainer = gd._fullLayout._zoomlayer; + var zoomLayer = gd._fullLayout._zoomlayer; // use plotbg for the main interactions var dragOptions = { @@ -526,7 +526,7 @@ proto.initInteractions = function() { dragOptions.clickFn = clickZoomPan; dragOptions.doneFn = dragDone; panPrep(); - clearSelect(zoomContainer); + clearSelect(gd); } else if(dragModeNow === 'select' || dragModeNow === 'lasso') { prepSelect(e, startX, startY, dragOptions, dragModeNow); @@ -578,7 +578,7 @@ proto.initInteractions = function() { path0 = 'M0,' + _this.h + 'L' + (_this.w / 2) + ', 0L' + _this.w + ',' + _this.h + 'Z'; dimmed = false; - zb = zoomContainer.append('path') + zb = zoomLayer.append('path') .attr('class', 'zoombox') .attr('transform', 'translate(' + _this.x0 + ', ' + _this.y0 + ')') .style({ @@ -587,7 +587,7 @@ proto.initInteractions = function() { }) .attr('d', path0); - corners = zoomContainer.append('path') + corners = zoomLayer.append('path') .attr('class', 'zoombox-corners') .attr('transform', 'translate(' + _this.x0 + ', ' + _this.y0 + ')') .style({ @@ -598,7 +598,7 @@ proto.initInteractions = function() { }) .attr('d', 'M0,0Z'); - clearSelect(zoomContainer); + clearSelect(gd); } function getAFrac(x, y) { return 1 - (y / _this.h); } diff --git a/test/jasmine/tests/cartesian_interact_test.js b/test/jasmine/tests/cartesian_interact_test.js index a025a253c34..8a220b999cc 100644 --- a/test/jasmine/tests/cartesian_interact_test.js +++ b/test/jasmine/tests/cartesian_interact_test.js @@ -1397,6 +1397,383 @@ describe('axis zoom/pan and main plot zoom', function() { .then(done); }); }); + + describe('redrag behavior', function() { + function _assertZoombox(msg, exp) { + var gd3 = d3.select(gd); + var zb = gd3.select('g.zoomlayer').select('.zoombox-corners'); + + if(zb.size()) { + expect(zb.attr('d')).toBe(exp.zoombox, msg + '| zoombox path'); + } else { + expect(false).toBe(exp.zoombox, msg + '| no zoombox'); + } + } + + function _assertClipRect(msg, exp) { + var gd3 = d3.select(gd); + var uid = gd._fullLayout._uid; + var clipRect = gd3.select('#clip' + uid + 'xyplot > rect'); + var xy = Drawing.getTranslate(clipRect); + expect(xy.x).toBeCloseTo(exp.clipTranslate[0], 2, msg + '| clip rect translate.x'); + expect(xy.y).toBeCloseTo(exp.clipTranslate[1], 2, msg + '| clip rect translate.y'); + } + + it('should handle extendTraces redraws during drag interactions', function(done) { + var step = 500; + var interval; + var xrngPrev; + + function _assert(msg, exp) { + return function() { + var fullLayout = gd._fullLayout; + + expect(fullLayout.xaxis.range).toBeCloseToArray(exp.xrng === 'previous' ? + xrngPrev : + exp.xrng, 2, msg + '|xaxis range'); + expect(d3.select(gd).selectAll('.point').size()).toBe(exp.nodeCnt, msg + '|pt cnt'); + expect(Boolean(gd._dragdata)).toBe(exp.hasDragData, msg + '|has gd._dragdata?'); + _assertZoombox(msg, exp); + _assertClipRect(msg, exp); + + xrngPrev = fullLayout.xaxis.range.slice(); + }; + } + + Plotly.plot(gd, [{y: [1, 2, 1]}], {dragmode: 'zoom'}) + .then(_assert('base', { + nodeCnt: 3, + xrng: [-0.128, 2.128], + hasDragData: false, + zoombox: false, + clipTranslate: [0, 0] + })) + .then(function() { + interval = setInterval(function() { + Plotly.extendTraces(gd, { y: [[Math.random()]] }, [0]); + }, step); + }) + .then(delay(1.5 * step)) + .then(_assert('after 1st extendTraces trace call', { + nodeCnt: 4, + xrng: [-0.1927, 3.1927], + hasDragData: false, + zoombox: false, + clipTranslate: [0, 0] + })) + .then(function() { + var drag = makeDragFns('xy', 'nsew', 30, 0); + return drag.start() + .then(_assert('just after start of zoombox', { + nodeCnt: 4, + xrng: [-0.1927, 3.1927], + hasDragData: true, + zoombox: 'M269.5,114.5h-3v41h3ZM300.5,114.5h3v41h-3Z', + clipTranslate: [0, 0] + })) + .then(delay(step)) + .then(_assert('during zoombox drag', { + nodeCnt: 5, + xrng: [-0.257, 4.257], + hasDragData: true, + zoombox: 'M269.5,114.5h-3v41h3ZM300.5,114.5h3v41h-3Z', + clipTranslate: [0, 0] + })) + .then(drag.end); + }) + .then(_assert('just after zoombox drag', { + nodeCnt: 5, + xrng: [2, 2.2507], + hasDragData: false, + zoombox: false, + clipTranslate: [0, 0] + })) + .then(delay(step)) + .then(function() { + return Plotly.relayout(gd, { + dragmode: 'pan', + 'xaxis.autorange': true, + 'yaxis.autorange': true + }); + }) + .then(delay(step)) + .then(_assert('after extendTraces two more steps / back to autorange:true', { + nodeCnt: 7, + xrng: [-0.385, 6.385], + hasDragData: false, + zoombox: false, + clipTranslate: [0, 0] + })) + .then(function() { + var drag = makeDragFns('xy', 'nsew', 60, 0); + return drag.start() + .then(_assert('just after pan start', { + nodeCnt: 7, + xrng: [-1.137, 5.633], + hasDragData: true, + zoombox: false, + clipTranslate: [-60, 0] + })) + .then(delay(step)) + .then(_assert('during pan mousedown', { + nodeCnt: 8, + xrng: [-1.327, 6.572], + hasDragData: true, + zoombox: false, + clipTranslate: [-60, 0] + })) + .then(drag.end); + }) + .then(_assert('just after pan end', { + nodeCnt: 8, + // N.B same xrng as just before on dragend + xrng: 'previous', + hasDragData: false, + zoombox: false, + clipTranslate: [0, 0] + })) + .then(delay(step)) + .then(_assert('last extendTraces call', { + nodeCnt: 9, + // N.B. same range as previous assert + // as now that xaxis range is set + xrng: 'previous', + hasDragData: false, + zoombox: false, + clipTranslate: [0, 0] + })) + .catch(failTest) + .then(function() { clearInterval(interval); }) + .then(done); + }); + + it('should handle plotly_relayout callback during drag interactions', function(done) { + var step = 500; + var relayoutTracker = []; + var restyleTracker = []; + var zCnt = 0; + var xrngPrev; + var yrngPrev; + + function z() { + return [[1, 2, 3], [2, (zCnt++) * 5, 1], [3, 2, 1]]; + } + + function _assert(msg, exp) { + return function() { + var trace = gd._fullData[0]; + var fullLayout = gd._fullLayout; + + expect(fullLayout.xaxis.range).toBeCloseToArray(exp.xrng === 'previous' ? + xrngPrev : + exp.xrng, 2, msg + '|xaxis range'); + expect(fullLayout.yaxis.range).toBeCloseToArray(exp.yrng === 'previous' ? + yrngPrev : + exp.yrng, 2, msg + '|yaxis range'); + + expect(trace.zmax).toBe(exp.zmax, msg + '|zmax'); + expect(Boolean(gd._dragdata)).toBe(exp.hasDragData, msg + '|has gd._dragdata?'); + expect(relayoutTracker.length).toBe(exp.relayoutCnt, msg + '|relayout cnt'); + expect(restyleTracker.length).toBe(exp.restyleCnt, msg + '|restyle cnt'); + _assertZoombox(msg, exp); + _assertClipRect(msg, exp); + + xrngPrev = fullLayout.xaxis.range.slice(); + yrngPrev = fullLayout.yaxis.range.slice(); + }; + } + + Plotly.plot(gd, [{ type: 'heatmap', z: z() }], {dragmode: 'pan'}) + .then(function() { + // inspired by https://github.com/plotly/plotly.js/issues/2687 + gd.on('plotly_relayout', function(d) { + relayoutTracker.unshift(d); + setTimeout(function() { + Plotly.restyle(gd, 'z', [z()]); + }, step); + }); + gd.on('plotly_restyle', function(d) { + restyleTracker.unshift(d); + }); + }) + .then(_assert('base', { + zmax: 3, + xrng: [-0.5, 2.5], + yrng: [-0.5, 2.5], + relayoutCnt: 0, + restyleCnt: 0, + hasDragData: false, + zoombox: false, + clipTranslate: [0, 0] + })) + .then(doDrag('xy', 'nsew', 30, 30)) + .then(_assert('after drag / before update #1', { + zmax: 3, + xrng: [-0.6707, 2.329], + yrng: [-0.1666, 2.833], + relayoutCnt: 1, + restyleCnt: 0, + hasDragData: false, + zoombox: false, + clipTranslate: [0, 0] + })) + .then(delay(step + 10)) + .then(_assert('after update #1', { + zmax: 5, + xrng: [-0.6707, 2.329], + yrng: [-0.1666, 2.833], + relayoutCnt: 1, + restyleCnt: 1, + hasDragData: false, + zoombox: false, + clipTranslate: [0, 0] + })) + .then(doDrag('xy', 'nsew', 30, 30)) + .then(delay(step / 2)) + .then(function() { + var drag = makeDragFns('xy', 'nsew', 30, 30); + return drag.start() + .then(_assert('just after pan start', { + zmax: 5, + xrng: [-1.005, 1.994], + yrng: [0.5, 3.5], + relayoutCnt: 2, + restyleCnt: 1, + hasDragData: true, + zoombox: false, + clipTranslate: [-30, -30] + })) + .then(delay(step)) + .then(_assert('after update #2 / during pan mousedown', { + zmax: 10, + xrng: 'previous', + yrng: 'previous', + relayoutCnt: 2, + restyleCnt: 2, + hasDragData: true, + zoombox: false, + clipTranslate: [-30, -30] + })) + .then(drag.end); + }) + .then(_assert('after pan end', { + zmax: 10, + xrng: 'previous', + yrng: 'previous', + relayoutCnt: 3, + restyleCnt: 2, + hasDragData: false, + zoombox: false, + clipTranslate: [0, 0] + })) + .then(delay(step)) + .then(_assert('after update #3', { + zmax: 15, + xrng: 'previous', + yrng: 'previous', + relayoutCnt: 3, + restyleCnt: 3, + hasDragData: false, + zoombox: false, + clipTranslate: [0, 0] + })) + .catch(failTest) + .then(done); + }); + + it('should handle react calls in plotly_selecting callback', function(done) { + var selectingTracker = []; + var selectedTracker = []; + + function _assert(msg, exp) { + return function() { + var gd3 = d3.select(gd); + + expect(gd3.selectAll('.point').size()).toBe(exp.nodeCnt, msg + '|pt cnt'); + expect(Boolean(gd._dragdata)).toBe(exp.hasDragData, msg + '|has gd._dragdata?'); + expect(selectingTracker.length).toBe(exp.selectingCnt, msg + '| selecting cnt'); + expect(selectedTracker.length).toBe(exp.selectedCnt, msg + '| selected cnt'); + + var outline = d3.select('.zoomlayer > .select-outline'); + if(outline.size()) { + expect(outline.attr('d')).toBe(exp.selectOutline, msg + '| selection outline path'); + } else { + expect(false).toBe(exp.selectOutline, msg + '| no selection outline'); + } + }; + } + + var trace0 = {mode: 'markers', x: [1, 2, 3], y: [1, 2, 1], marker: {opacity: 0.5}}; + var trace1 = {mode: 'markers', x: [], y: [], marker: {size: 20}}; + + var layout = { + dragmode: 'select', + showlegend: false, + width: 400, + height: 400, + margin: {l: 0, r: 0, t: 0, b: 0} + }; + + Plotly.plot(gd, [trace0], layout) + .then(function() { + // inspired by https://github.com/plotly/plotly.js-crossfilter.js + gd.on('plotly_selecting', function(d) { + selectingTracker.unshift(d); + + if(d && d.points) { + trace1.x = d.points.map(function(p) { return trace0.x[p.pointNumber]; }); + trace1.y = d.points.map(function(p) { return trace0.y[p.pointNumber]; }); + } else { + trace1.x = []; + trace1.y = []; + } + + Plotly.react(gd, [trace0, trace1], layout); + }); + + gd.on('plotly_selected', function(d) { + selectedTracker.unshift(d); + Plotly.react(gd, [trace0], layout); + }); + }) + .then(_assert('base', { + nodeCnt: 3, + hasDragData: false, + selectingCnt: 0, + selectedCnt: 0, + selectOutline: false + })) + .then(function() { + var drag = makeDragFns('xy', 'nsew', 200, 200, 20, 20); + return drag.start() + .then(_assert('just after pan start', { + nodeCnt: 4, + hasDragData: true, + selectingCnt: 1, + selectedCnt: 0, + selectOutline: 'M20,20L20,220L220,220L220,20L20,20Z' + })) + .then(delay(100)) + .then(_assert('while holding on mouse', { + nodeCnt: 4, + hasDragData: true, + selectingCnt: 1, + selectedCnt: 0, + selectOutline: 'M20,20L20,220L220,220L220,20L20,20Z' + })) + .then(drag.end); + }) + .then(_assert('after drag', { + nodeCnt: 3, + hasDragData: false, + selectingCnt: 1, + selectedCnt: 1, + selectOutline: false + })) + .catch(failTest) + .then(done); + }); + }); }); describe('Event data:', function() { diff --git a/test/jasmine/tests/plot_promise_test.js b/test/jasmine/tests/plot_promise_test.js index e493a1049df..53856770ba3 100644 --- a/test/jasmine/tests/plot_promise_test.js +++ b/test/jasmine/tests/plot_promise_test.js @@ -62,32 +62,6 @@ describe('Plotly.___ methods', function() { }); }); - describe('Plotly.plot promise', function() { - var gd; - var promise; - var promiseRejected = false; - - beforeEach(function(done) { - var data = [{ x: [1, 2, 3], y: [4, 5, 6] }]; - - gd = createGraphDiv(); - - gd._dragging = true; - - promise = Plotly.plot(gd, data, {}); - - promise.then(null, function() { - promiseRejected = true; - done(); - }); - }); - - - it('should reject the promise when graph is being dragged', function() { - expect(promiseRejected).toBe(true); - }); - }); - describe('Plotly.redraw promise', function() { var promise; var promiseGd; diff --git a/test/jasmine/tests/plots_test.js b/test/jasmine/tests/plots_test.js index 0b4b37ca925..95c74b6c214 100644 --- a/test/jasmine/tests/plots_test.js +++ b/test/jasmine/tests/plots_test.js @@ -405,10 +405,10 @@ describe('Test Plots', function() { // because _dragging and _dragged were not cleared by purge. gd._dragging = true; gd._dragged = true; + gd._dragdata = true; gd._hoverdata = true; gd._snapshotInProgress = true; gd._editing = true; - gd._replotPending = true; gd._mouseDownTime = true; gd._legendMouseDownTime = true; }); @@ -427,8 +427,8 @@ describe('Test Plots', function() { 'empty', 'fid', 'undoqueue', 'undonum', 'autoplay', 'changed', '_promises', '_redrawTimer', 'firstscatter', '_transitionData', '_transitioning', '_hmpixcount', '_hmlumcount', - '_dragging', '_dragged', '_hoverdata', '_snapshotInProgress', '_editing', - '_replotPending', '_mouseDownTime', '_legendMouseDownTime' + '_dragging', '_dragged', '_dragdata', '_hoverdata', '_snapshotInProgress', '_editing', + '_mouseDownTime', '_legendMouseDownTime' ]; Plots.purge(gd);