diff --git a/package.json b/package.json index 7fe52af4a17..845f792c8a7 100644 --- a/package.json +++ b/package.json @@ -56,7 +56,7 @@ "3d-view": "^2.0.0", "@plotly/d3-sankey": "^0.5.0", "alpha-shape": "^1.0.0", - "color-rgba": "^1.0.4", + "color-rgba": "^1.1.0", "convex-hull": "^1.0.3", "country-regex": "^1.1.0", "d3": "^3.5.12", @@ -76,8 +76,8 @@ "gl-plot2d": "^1.2.0", "gl-plot3d": "^1.5.4", "gl-pointcloud2d": "^1.0.0", - "gl-scatter2d": "^1.2.2", - "gl-scatter2d-sdf": "^1.3.10", + "gl-scatter2d": "^1.3.1", + "gl-scatter2d-sdf": "^1.3.11", "gl-scatter3d": "^1.0.4", "gl-select-box": "^1.0.1", "gl-shader": "4.2.0", @@ -86,6 +86,7 @@ "mapbox-gl": "^0.22.0", "matrix-camera-controller": "^2.1.3", "mouse-change": "^1.4.0", + "mouse-event-offset": "^3.0.2", "mouse-wheel": "^1.0.2", "ndarray": "^1.0.18", "ndarray-fill": "^1.0.2", diff --git a/src/components/modebar/manage.js b/src/components/modebar/manage.js index f3732850f8c..a97a86eb37d 100644 --- a/src/components/modebar/manage.js +++ b/src/components/modebar/manage.js @@ -121,7 +121,7 @@ function getButtonGroups(gd, buttonsToRemove, buttonsToAdd) { if(((hasCartesian || hasGL2D) && !allAxesFixed) || hasTernary) { dragModeGroup = ['zoom2d', 'pan2d']; } - if((hasCartesian || hasTernary) && isSelectable(fullData)) { + if((hasCartesian || hasTernary || hasGL2D) && isSelectable(fullData)) { dragModeGroup.push('select2d'); dragModeGroup.push('lasso2d'); } @@ -173,7 +173,7 @@ function isSelectable(fullData) { if(!trace._module || !trace._module.selectPoints) continue; - if(trace.type === 'scatter' || trace.type === 'scatterternary') { + if(trace.type === 'scatter' || trace.type === 'scatterternary' || trace.type === 'scattergl') { if(scatterSubTypes.hasMarkers(trace) || scatterSubTypes.hasText(trace)) { selectable = true; } diff --git a/src/lib/index.js b/src/lib/index.js index 8518d176d1d..25eff611e5c 100644 --- a/src/lib/index.js +++ b/src/lib/index.js @@ -451,7 +451,7 @@ lib.minExtend = function(obj1, obj2) { for(i = 0; i < keys.length; i++) { k = keys[i]; v = obj1[k]; - if(k.charAt(0) === '_' || typeof v === 'function') continue; + if(k.charAt(0) === '_' || typeof v === 'function' || k === 'glTrace') continue; else if(k === 'module') objOut[k] = v; else if(Array.isArray(v)) objOut[k] = v.slice(0, arrayLen); else if(v && (typeof v === 'object')) objOut[k] = lib.minExtend(obj1[k], obj2[k]); diff --git a/src/plot_api/plot_api.js b/src/plot_api/plot_api.js index f993e5aaba0..7c28d9a16f9 100644 --- a/src/plot_api/plot_api.js +++ b/src/plot_api/plot_api.js @@ -1948,7 +1948,8 @@ function _relayout(gd, aobj) { // trunk nodes (everything except the leaf) ptrunk = p.parts.slice(0, pend).join('.'), parentIn = Lib.nestedProperty(gd.layout, ptrunk).get(), - parentFull = Lib.nestedProperty(fullLayout, ptrunk).get(); + parentFull = Lib.nestedProperty(fullLayout, ptrunk).get(), + vOld = p.get(); if(vi === undefined) continue; @@ -1956,7 +1957,7 @@ function _relayout(gd, aobj) { // axis reverse is special - it is its own inverse // op and has no flag. - undoit[ai] = (pleaf === 'reverse') ? vi : p.get(); + undoit[ai] = (pleaf === 'reverse') ? vi : vOld; // Setting width or height to null must reset the graph's width / height // back to its initial value as computed during the first pass in Plots.plotAutoSize. @@ -2166,7 +2167,16 @@ function _relayout(gd, aobj) { } else if(fullLayout._has('gl2d') && (ai.indexOf('axis') !== -1 || ai === 'plot_bgcolor') - ) flags.doplot = true; + ) { + flags.doplot = true; + } + else if(fullLayout._has('gl2d') && + (ai === 'dragmode' && + (vi === 'lasso' || vi === 'select') && + !(vOld === 'lasso' || vOld === 'select')) + ) { + flags.docalc = true; + } else if(ai === 'hiddenlabels') flags.docalc = true; else if(proot.indexOf('legend') !== -1) flags.dolegend = true; else if(ai.indexOf('title') !== -1) flags.doticks = true; diff --git a/src/plot_api/subroutines.js b/src/plot_api/subroutines.js index 45238db4497..64d1663910d 100644 --- a/src/plot_api/subroutines.js +++ b/src/plot_api/subroutines.js @@ -126,15 +126,16 @@ exports.lsInner = function(gd) { var freefinished = []; subplotSelection.each(function(subplot) { - var plotinfo = fullLayout._plots[subplot], - xa = Plotly.Axes.getFromId(gd, subplot, 'x'), + var plotinfo = fullLayout._plots[subplot]; + + var xa = Plotly.Axes.getFromId(gd, subplot, 'x'), ya = Plotly.Axes.getFromId(gd, subplot, 'y'); // reset scale in case the margins have changed xa.setScale(); ya.setScale(); - if(plotinfo.bg) { + if(plotinfo.bg && fullLayout._has('cartesian')) { plotinfo.bg .call(Drawing.setRect, xa._offset - gs.p, ya._offset - gs.p, @@ -254,27 +255,29 @@ exports.lsInner = function(gd) { rightpos += xa._offset - gs.l; } - plotinfo.xlines - .attr('transform', originx) - .attr('d', ( - (showbottom ? (xpathPrefix + bottompos + xpathSuffix) : '') + - (showtop ? (xpathPrefix + toppos + xpathSuffix) : '') + - (showfreex ? (xpathPrefix + freeposx + xpathSuffix) : '')) || - // so it doesn't barf with no lines shown - 'M0,0') - .style('stroke-width', xlw + 'px') - .call(Color.stroke, xa.showline ? - xa.linecolor : 'rgba(0,0,0,0)'); - plotinfo.ylines - .attr('transform', originy) - .attr('d', ( - (showleft ? ('M' + leftpos + ypathSuffix) : '') + - (showright ? ('M' + rightpos + ypathSuffix) : '') + - (showfreey ? ('M' + freeposy + ypathSuffix) : '')) || - 'M0,0') - .attr('stroke-width', ylw + 'px') - .call(Color.stroke, ya.showline ? - ya.linecolor : 'rgba(0,0,0,0)'); + if(fullLayout._has('cartesian')) { + plotinfo.xlines + .attr('transform', originx) + .attr('d', ( + (showbottom ? (xpathPrefix + bottompos + xpathSuffix) : '') + + (showtop ? (xpathPrefix + toppos + xpathSuffix) : '') + + (showfreex ? (xpathPrefix + freeposx + xpathSuffix) : '')) || + // so it doesn't barf with no lines shown + 'M0,0') + .style('stroke-width', xlw + 'px') + .call(Color.stroke, xa.showline ? + xa.linecolor : 'rgba(0,0,0,0)'); + plotinfo.ylines + .attr('transform', originy) + .attr('d', ( + (showleft ? ('M' + leftpos + ypathSuffix) : '') + + (showright ? ('M' + rightpos + ypathSuffix) : '') + + (showfreey ? ('M' + freeposy + ypathSuffix) : '')) || + 'M0,0') + .attr('stroke-width', ylw + 'px') + .call(Color.stroke, ya.showline ? + ya.linecolor : 'rgba(0,0,0,0)'); + } plotinfo.xaxislayer.attr('transform', originx); plotinfo.yaxislayer.attr('transform', originy); @@ -375,19 +378,22 @@ exports.doTicksRelayout = function(gd) { exports.doModeBar = function(gd) { var fullLayout = gd._fullLayout; - var subplotIds, i; + var subplotIds, scene, i; ModeBar.manage(gd); initInteractions(gd); subplotIds = Plots.getSubplotIds(fullLayout, 'gl3d'); for(i = 0; i < subplotIds.length; i++) { - var scene = fullLayout[subplotIds[i]]._scene; + scene = fullLayout[subplotIds[i]]._scene; scene.updateFx(fullLayout.dragmode, fullLayout.hovermode); } - // no need to do this for gl2d subplots, - // Plots.linkSubplots takes care of it all. + subplotIds = Plots.getSubplotIds(fullLayout, 'gl2d'); + for(i = 0; i < subplotIds.length; i++) { + scene = fullLayout._plots[subplotIds[i]]._scene2d; + scene.updateFx(fullLayout.dragmode); + } return Plots.previousPromises(gd); }; diff --git a/src/plots/cartesian/axes.js b/src/plots/cartesian/axes.js index 18bdd7693ff..0aaf679a517 100644 --- a/src/plots/cartesian/axes.js +++ b/src/plots/cartesian/axes.js @@ -360,6 +360,9 @@ axes.doAutoRange = function(ax) { if(ax.autorange && hasDeps) { ax.range = axes.getAutoRange(ax); + ax._r = ax.range.slice(); + ax._rl = Lib.simpleMap(ax._r, ax.r2l); + // doAutoRange will get called on fullLayout, // but we want to report its results back to layout diff --git a/src/plots/cartesian/graph_interact.js b/src/plots/cartesian/graph_interact.js index 410e693fe09..283309865f5 100644 --- a/src/plots/cartesian/graph_interact.js +++ b/src/plots/cartesian/graph_interact.js @@ -20,7 +20,7 @@ var dragBox = require('./dragbox'); module.exports = function initInteractions(gd) { var fullLayout = gd._fullLayout; - if(!fullLayout._has('cartesian') || gd._context.staticPlot) return; + if((!fullLayout._has('cartesian') && !fullLayout._has('gl2d')) || gd._context.staticPlot) return; var subplots = Object.keys(fullLayout._plots || {}).sort(function(a, b) { // sort overlays last, then by x axis number, then y axis number @@ -38,8 +38,6 @@ module.exports = function initInteractions(gd) { subplots.forEach(function(subplot) { var plotinfo = fullLayout._plots[subplot]; - if(!fullLayout._has('cartesian')) return; - var xa = plotinfo.xaxis, ya = plotinfo.yaxis, diff --git a/src/plots/cartesian/index.js b/src/plots/cartesian/index.js index 0649a155296..883acb4d0d8 100644 --- a/src/plots/cartesian/index.js +++ b/src/plots/cartesian/index.js @@ -184,6 +184,11 @@ exports.clean = function(newFullData, newFullLayout, oldFullData, oldFullLayout) oldFullLayout._infolayer.select('.' + axIds[i] + 'title').remove(); } } + + // clean selection + if(oldFullLayout._zoomlayer) { + oldFullLayout._zoomlayer.selectAll('.select-outline').remove(); + } }; exports.drawFramework = function(gd) { diff --git a/src/plots/gl2d/index.js b/src/plots/gl2d/index.js index 2d4f2f7f099..482d55fa4b1 100644 --- a/src/plots/gl2d/index.js +++ b/src/plots/gl2d/index.js @@ -12,7 +12,8 @@ var Scene2D = require('./scene2d'); var Plots = require('../plots'); var xmlnsNamespaces = require('../../constants/xmlns_namespaces'); - +var constants = require('../cartesian/constants'); +var Cartesian = require('../cartesian'); exports.name = 'gl2d'; @@ -20,15 +21,9 @@ exports.attr = ['xaxis', 'yaxis']; exports.idRoot = ['x', 'y']; -exports.idRegex = { - x: /^x([2-9]|[1-9][0-9]+)?$/, - y: /^y([2-9]|[1-9][0-9]+)?$/ -}; +exports.idRegex = constants.idRegex; -exports.attrRegex = { - x: /^xaxis([2-9]|[1-9][0-9]+)?$/, - y: /^yaxis([2-9]|[1-9][0-9]+)?$/ -}; +exports.attrRegex = constants.attrRegex; exports.attributes = require('../cartesian/attributes'); @@ -82,6 +77,15 @@ exports.clean = function(newFullData, newFullLayout, oldFullData, oldFullLayout) delete oldFullLayout._plots[id]; } } + + // since we use cartesian interactions, do cartesian clean + Cartesian.clean.apply(this, arguments); +}; + +exports.drawFramework = function(gd) { + if(!gd._context.staticPlot) { + Cartesian.drawFramework(gd); + } }; exports.toSVG = function(gd) { diff --git a/src/plots/gl2d/scene2d.js b/src/plots/gl2d/scene2d.js index 763ebf6470f..6c4da94cbd8 100644 --- a/src/plots/gl2d/scene2d.js +++ b/src/plots/gl2d/scene2d.js @@ -154,6 +154,7 @@ proto.makeFramework = function() { // create div to catch the mouse event var mouseContainer = this.mouseContainer = document.createElement('div'); mouseContainer.style.position = 'absolute'; + mouseContainer.style['pointer-events'] = 'auto'; // append canvas, hover svg and mouse div to container var container = this.container; @@ -380,6 +381,7 @@ proto.plot = function(fullData, calcData, fullLayout) { this.updateRefs(fullLayout); this.updateTraces(fullData, calcData); + this.updateFx(fullLayout.dragmode); var width = fullLayout.width, height = fullLayout.height; @@ -520,7 +522,14 @@ proto.updateTraces = function(fullData, calcData) { this.glplot.objects.sort(function(a, b) { return a._trace.index - b._trace.index; }); +}; +proto.updateFx = function(dragmode) { + if(dragmode === 'lasso' || dragmode === 'select') { + this.mouseContainer.style['pointer-events'] = 'none'; + } else { + this.mouseContainer.style['pointer-events'] = 'auto'; + } }; proto.emitPointAction = function(nextSelection, eventType) { diff --git a/src/plots/gl3d/camera.js b/src/plots/gl3d/camera.js index e97fc8b213a..93d92e5ba08 100644 --- a/src/plots/gl3d/camera.js +++ b/src/plots/gl3d/camera.js @@ -14,6 +14,7 @@ var now = require('right-now'); var createView = require('3d-view'); var mouseChange = require('mouse-change'); var mouseWheel = require('mouse-wheel'); +var mouseOffset = require('mouse-event-offset'); function createCamera(element, options) { element = element || document.body; @@ -179,8 +180,24 @@ function createCamera(element, options) { return false; }); - var lastX = 0, lastY = 0; - mouseChange(element, function(buttons, x, y, mods) { + var lastX = 0, lastY = 0, lastMods = {shift: false, control: false, alt: false, meta: false}; + mouseChange(element, handleInteraction); + + // enable simple touch interactions + element.addEventListener('touchstart', function(ev) { + var xy = mouseOffset(ev.changedTouches[0], element); + handleInteraction(0, xy[0], xy[1], lastMods); + handleInteraction(1, xy[0], xy[1], lastMods); + }); + element.addEventListener('touchmove', function(ev) { + var xy = mouseOffset(ev.changedTouches[0], element); + handleInteraction(1, xy[0], xy[1], lastMods); + }); + element.addEventListener('touchend', function() { + handleInteraction(0, lastX, lastY, lastMods); + }); + + function handleInteraction(buttons, x, y, mods) { var keyBindingMode = camera.keyBindingMode; if(keyBindingMode === false) return; @@ -225,9 +242,10 @@ function createCamera(element, options) { lastX = x; lastY = y; + lastMods = mods; return true; - }); + } mouseWheel(element, function(dx, dy) { if(camera.keyBindingMode === false) return; diff --git a/src/traces/scatter/hover.js b/src/traces/scatter/hover.js index 7ceac0db070..ee968926c64 100644 --- a/src/traces/scatter/hover.js +++ b/src/traces/scatter/hover.js @@ -24,11 +24,12 @@ module.exports = function hoverPoints(pointData, xval, yval, hovermode) { ya = pointData.ya, xpx = xa.c2p(xval), ypx = ya.c2p(yval), - pt = [xpx, ypx]; + pt = [xpx, ypx], + hoveron = trace.hoveron || ''; // look for points to hover on first, then take fills only if we // didn't find a point - if(trace.hoveron.indexOf('points') !== -1) { + if(hoveron.indexOf('points') !== -1) { var dx = function(di) { // scatter points: d.mrc is the calculated marker radius // adjust the distance so if you're inside the marker it @@ -84,7 +85,7 @@ module.exports = function hoverPoints(pointData, xval, yval, hovermode) { } // even if hoveron is 'fills', only use it if we have polygons too - if(trace.hoveron.indexOf('fills') !== -1 && trace._polygons) { + if(hoveron.indexOf('fills') !== -1 && trace._polygons) { var polygons = trace._polygons, polygonsIn = [], inside = false, diff --git a/src/traces/scattergl/calc.js b/src/traces/scattergl/calc.js new file mode 100644 index 00000000000..1be524af55e --- /dev/null +++ b/src/traces/scattergl/calc.js @@ -0,0 +1,43 @@ +/** +* Copyright 2012-2017, Plotly, Inc. +* All rights reserved. +* +* This source code is licensed under the MIT license found in the +* LICENSE file in the root directory of this source tree. +*/ + + +'use strict'; + +var Axes = require('../../plots/cartesian/axes'); +var arraysToCalcdata = require('../scatter/arrays_to_calcdata'); +var calcColorscales = require('../scatter/colorscale_calc'); + +module.exports = function calc(gd, trace) { + var dragmode = gd._fullLayout.dragmode; + var cd; + + if(dragmode === 'lasso' || dragmode === 'select') { + var xa = Axes.getFromId(gd, trace.xaxis || 'x'); + var ya = Axes.getFromId(gd, trace.yaxis || 'y'); + + var x = xa.makeCalcdata(trace, 'x'); + var y = ya.makeCalcdata(trace, 'y'); + + var serieslen = Math.min(x.length, y.length), i; + + // create the "calculated data" to plot + cd = new Array(serieslen); + + for(i = 0; i < serieslen; i++) { + cd[i] = {x: x[i], y: y[i]}; + } + } else { + cd = [{x: false, y: false, trace: trace, t: {}}]; + arraysToCalcdata(cd, trace); + } + + calcColorscales(trace); + + return cd; +}; diff --git a/src/traces/scattergl/convert.js b/src/traces/scattergl/convert.js index d2956864376..74a2a1d9ea1 100644 --- a/src/traces/scattergl/convert.js +++ b/src/traces/scattergl/convert.js @@ -29,7 +29,8 @@ var MARKER_SYMBOLS = require('../../constants/gl2d_markers'); var DASHES = require('../../constants/gl2d_dashes'); var AXES = ['xaxis', 'yaxis']; -var transparent = [0, 0, 0, 0]; +var DESELECTDIM = 0.2; +var TRANSPARENT = [0, 0, 0, 0]; function LineWithMarkers(scene, uid) { this.scene = scene; @@ -95,11 +96,14 @@ function LineWithMarkers(scene, uid) { size: 12, color: [0, 0, 0, 1], borderSize: 1, - borderColor: [0, 0, 0, 1] + borderColor: [0, 0, 0, 1], + snapPoints: true }; + var scatterOptions1 = Lib.extendFlat({}, scatterOptions0, {snapPoints: false}); this.scatter = this.initObject(createScatter, scatterOptions0, 3); this.fancyScatter = this.initObject(createFancyScatter, scatterOptions0, 4); + this.selectScatter = this.initObject(createScatter, scatterOptions1, 5); } var proto = LineWithMarkers.prototype; @@ -255,13 +259,17 @@ function isSymbolOpen(symbol) { return symbol.split('-open')[1] === ''; } -function fillColor(colorIn, colorOut, offsetIn, offsetOut) { - for(var j = 0; j < 4; j++) { +function fillColor(colorIn, colorOut, offsetIn, offsetOut, isDimmed) { + var dim = isDimmed ? DESELECTDIM : 1; + var j; + + for(j = 0; j < 3; j++) { colorIn[4 * offsetIn + j] = colorOut[4 * offsetOut + j]; } + colorIn[4 * offsetIn + j] = dim * colorOut[4 * offsetOut + j]; } -proto.update = function(options) { +proto.update = function(options, cdscatter) { if(options.visible !== true) { this.isVisible = false; this.hasLines = false; @@ -312,6 +320,11 @@ proto.update = function(options) { // not quite on-par with 'scatter', but close enough for now // does not handle the colorscale case this.color = getTraceColor(options, {}); + + // provide reference for selecting points + if(cdscatter && cdscatter[0] && !cdscatter[0].glTrace) { + cdscatter[0].glTrace = this; + } }; // We'd ideally know that all values are of fast types; sampling gives no certainty but faster @@ -346,7 +359,9 @@ proto.updateFast = function(options) { positions = new Float64Array(2 * len), bounds = this.bounds, pId = 0, - ptr = 0; + ptr = 0, + selection = options.selection, + i, selPositions, l; var xx, yy; @@ -359,7 +374,7 @@ proto.updateFast = function(options) { // TODO bypass this on modebar +/- zoom if(fastType || isDateTime) { - for(var i = 0; i < len; ++i) { + for(i = 0; i < len; ++i) { xx = x[i]; yy = y[i]; @@ -369,11 +384,11 @@ proto.updateFast = function(options) { xx = Lib.dateTime2ms(xx, xcalendar); } - idToIndex[pId++] = i; - positions[ptr++] = xx; positions[ptr++] = yy; + idToIndex[pId++] = i; + bounds[0] = Math.min(bounds[0], xx); bounds[1] = Math.min(bounds[1], yy); bounds[2] = Math.max(bounds[2], xx); @@ -385,6 +400,16 @@ proto.updateFast = function(options) { positions = truncate(positions, ptr); this.idToIndex = idToIndex; + // form selected set + if(selection) { + selPositions = new Float64Array(2 * selection.length); + + for(i = 0, l = selection.length; i < l; i++) { + selPositions[i * 2 + 0] = selection[i].x; + selPositions[i * 2 + 1] = selection[i].y; + } + } + this.updateLines(options, positions); this.updateError('X', options); this.updateError('Y', options); @@ -392,23 +417,68 @@ proto.updateFast = function(options) { var markerSize; if(this.hasMarkers) { - this.scatter.options.positions = positions; + var markerColor, borderColor, opacity; + + // if we have selPositions array - means we have to render all points transparent, and selected points opaque + if(selPositions) { + this.scatter.options.positions = null; + + markerColor = str2RGBArray(options.marker.color); + borderColor = str2RGBArray(options.marker.line.color); + opacity = (options.opacity) * (options.marker.opacity) * DESELECTDIM; + + markerColor[3] *= opacity; + this.scatter.options.color = markerColor; + + borderColor[3] *= opacity; + this.scatter.options.borderColor = borderColor; + + markerSize = options.marker.size; + this.scatter.options.size = markerSize; + this.scatter.options.borderSize = options.marker.line.width; + + this.scatter.update(); + this.scatter.options.positions = positions; - var markerColor = str2RGBArray(options.marker.color), - borderColor = str2RGBArray(options.marker.line.color), + + this.selectScatter.options.positions = selPositions; + + markerColor = str2RGBArray(options.marker.color); + borderColor = str2RGBArray(options.marker.line.color); + opacity = (options.opacity) * (options.marker.opacity); + + markerColor[3] *= opacity; + this.selectScatter.options.color = markerColor; + + borderColor[3] *= opacity; + this.selectScatter.options.borderColor = borderColor; + + markerSize = options.marker.size; + this.selectScatter.options.size = markerSize; + this.selectScatter.options.borderSize = options.marker.line.width; + + this.selectScatter.update(); + } + + else { + this.scatter.options.positions = positions; + + markerColor = str2RGBArray(options.marker.color); + borderColor = str2RGBArray(options.marker.line.color); opacity = (options.opacity) * (options.marker.opacity); + markerColor[3] *= opacity; + this.scatter.options.color = markerColor; - markerColor[3] *= opacity; - this.scatter.options.color = markerColor; + borderColor[3] *= opacity; + this.scatter.options.borderColor = borderColor; - borderColor[3] *= opacity; - this.scatter.options.borderColor = borderColor; + markerSize = options.marker.size; + this.scatter.options.size = markerSize; + this.scatter.options.borderSize = options.marker.line.width; - markerSize = options.marker.size; - this.scatter.options.size = markerSize; - this.scatter.options.borderSize = options.marker.line.width; + this.scatter.update(); + } - this.scatter.update(); } else { this.scatter.clear(); @@ -425,7 +495,8 @@ proto.updateFancy = function(options) { var scene = this.scene, xaxis = scene.xaxis, yaxis = scene.yaxis, - bounds = this.bounds; + bounds = this.bounds, + selection = options.selection; // makeCalcdata runs d2c (data-to-coordinate) on every point var x = this.pickXData = xaxis.makeCalcdata(options, 'x').slice(); @@ -486,7 +557,14 @@ proto.updateFancy = function(options) { this.updateError('X', options, positions, errorsX); this.updateError('Y', options, positions, errorsY); - var sizes; + var sizes, selIds; + + if(selection) { + selIds = {}; + for(i = 0; i < selection.length; i++) { + selIds[selection[i].pointNumber] = true; + } + } if(this.hasMarkers) { this.scatter.options.positions = positions; @@ -508,7 +586,7 @@ proto.updateFancy = function(options) { var colors = convertColorScale(markerOpts, markerOpacity, traceOpacity, len); var borderWidths = convertNumber(markerOpts.line.width, len); var borderColors = convertColorScale(markerOpts.line, markerOpacity, traceOpacity, len); - var index, size, symbol, symbolSpec, isOpen, _colors, _borderColors, bw, minBorderWidth; + var index, size, symbol, symbolSpec, isOpen, isDimmed, _colors, _borderColors, bw, minBorderWidth; sizes = convertArray(markerSizeFunc, markerOpts.size, len); @@ -518,6 +596,7 @@ proto.updateFancy = function(options) { symbol = symbols[index]; symbolSpec = MARKER_SYMBOLS[symbol]; isOpen = isSymbolOpen(symbol); + isDimmed = selIds && !selIds[index]; if(symbolSpec.noBorder && !isOpen) { _colors = borderColors; @@ -542,14 +621,22 @@ proto.updateFancy = function(options) { this.scatter.options.borderWidths[i] = 0.5 * ((bw > minBorderWidth) ? bw - minBorderWidth : 0); if(isOpen && !symbolSpec.noBorder && !symbolSpec.noFill) { - fillColor(this.scatter.options.colors, transparent, i, 0); + fillColor(this.scatter.options.colors, TRANSPARENT, i, 0); } else { - fillColor(this.scatter.options.colors, _colors, i, index); + fillColor(this.scatter.options.colors, _colors, i, index, isDimmed); } - fillColor(this.scatter.options.borderColors, _borderColors, i, index); + fillColor(this.scatter.options.borderColors, _borderColors, i, index, isDimmed); } - this.fancyScatter.update(); + // prevent scatter from resnapping points + if(selIds) { + this.scatter.options.positions = null; + this.fancyScatter.update(); + this.scatter.options.positions = positions; + } + else { + this.fancyScatter.update(); + } } else { this.fancyScatter.clear(); @@ -671,9 +758,10 @@ proto.dispose = function() { this.fancyScatter.dispose(); }; -function createLineWithMarkers(scene, data) { +function createLineWithMarkers(scene, data, cdscatter) { var plot = new LineWithMarkers(scene, data.uid); - plot.update(data); + plot.update(data, cdscatter); + return plot; } diff --git a/src/traces/scattergl/index.js b/src/traces/scattergl/index.js index d5e71241a46..551e9adad7d 100644 --- a/src/traces/scattergl/index.js +++ b/src/traces/scattergl/index.js @@ -13,10 +13,12 @@ var ScatterGl = {}; ScatterGl.attributes = require('./attributes'); ScatterGl.supplyDefaults = require('./defaults'); ScatterGl.colorbar = require('../scatter/colorbar'); +ScatterGl.hoverPoints = require('../scatter/hover'); // reuse the Scatter3D 'dummy' calc step so that legends know what to do -ScatterGl.calc = require('../scatter3d/calc'); +ScatterGl.calc = require('./calc'); ScatterGl.plot = require('./convert'); +ScatterGl.selectPoints = require('./select'); ScatterGl.moduleType = 'trace'; ScatterGl.name = 'scattergl'; diff --git a/src/traces/scattergl/select.js b/src/traces/scattergl/select.js new file mode 100644 index 00000000000..548824ad740 --- /dev/null +++ b/src/traces/scattergl/select.js @@ -0,0 +1,60 @@ +/** +* Copyright 2012-2017, Plotly, Inc. +* All rights reserved. +* +* This source code is licensed under the MIT license found in the +* LICENSE file in the root directory of this source tree. +*/ + + +'use strict'; + +var subtypes = require('../scatter/subtypes'); + +module.exports = function selectPoints(searchInfo, polygon) { + var cd = searchInfo.cd, + xa = searchInfo.xaxis, + ya = searchInfo.yaxis, + selection = [], + trace = cd[0].trace, + i, + di, + x, + y; + + var scattergl = cd[0].glTrace; + var scene = cd[0].glTrace.scene; + + var hasOnlyLines = (!subtypes.hasMarkers(trace) && !subtypes.hasText(trace)); + if(trace.visible !== true || hasOnlyLines) return; + + // filter out points by visible scatter ones + if(polygon === false) { + // clear selection + for(i = 0; i < cd.length; i++) cd[i].dim = 0; + } + else { + for(i = 0; i < cd.length; i++) { + di = cd[i]; + x = xa.c2p(di.x); + y = ya.c2p(di.y); + if(polygon.contains([x, y])) { + selection.push({ + pointNumber: i, + x: di.x, + y: di.y + }); + di.dim = 0; + } + else di.dim = 1; + } + } + + // highlight selected points here + trace.selection = selection; + + scattergl.update(trace, cd); + scene.glplot.setDirty(); + + return selection; +}; diff --git a/test/jasmine/tests/gl2d_click_test.js b/test/jasmine/tests/gl2d_click_test.js index 134f3316d14..ddd576d5ab4 100644 --- a/test/jasmine/tests/gl2d_click_test.js +++ b/test/jasmine/tests/gl2d_click_test.js @@ -444,3 +444,157 @@ describe('Test hover and click interactions', function() { .then(done); }); }); + +describe('@noCI Test gl2d lasso/select:', function() { + var mockFancy = require('@mocks/gl2d_14.json'); + var mockFast = Lib.extendDeep({}, mockFancy, { + data: [{mode: 'markers'}], + layout: { + xaxis: {type: 'linear'}, + yaxis: {type: 'linear'} + } + }); + + var gd; + var selectPath = [[93, 193], [143, 193]]; + var lassoPath = [[316, 171], [318, 239], [335, 243], [328, 169]]; + var lassoPath2 = [[93, 193], [143, 193], [143, 500], [93, 500], [93, 193]]; + + afterEach(function() { + Plotly.purge(gd); + destroyGraphDiv(); + }); + + function drag(path) { + var len = path.length; + + mouseEvent('mousemove', path[0][0], path[0][1]); + mouseEvent('mousedown', path[0][0], path[0][1]); + + path.slice(1, len).forEach(function(pt) { + mouseEvent('mousemove', pt[0], pt[1]); + }); + + mouseEvent('mouseup', path[len - 1][0], path[len - 1][1]); + } + + function select(path) { + return new Promise(function(resolve, reject) { + gd.once('plotly_selected', resolve); + setTimeout(function() { reject('did not trigger *plotly_selected*');}, 100); + drag(path); + }); + } + + function assertEventData(actual, expected) { + expect(actual.points.length).toBe(expected.points.length); + + expected.points.forEach(function(e, i) { + var a = actual.points[i]; + if(a) { + expect(a.x).toBe(e.x, 'x'); + expect(a.y).toBe(e.y, 'y'); + } + }); + } + + function countGlObjects() { + return gd._fullLayout._plots.xy._scene2d.glplot.objects.length; + } + + it('should work under fast mode with *select* dragmode', function(done) { + var _mock = Lib.extendDeep({}, mockFast); + _mock.layout.dragmode = 'select'; + gd = createGraphDiv(); + + Plotly.plot(gd, _mock) + .then(delay(100)) + .then(function() { + expect(countGlObjects()).toBe(1, 'has on gl-scatter2d object'); + + return select(selectPath); + }) + .then(function(eventData) { + assertEventData(eventData, { + points: [ + {x: 3.911, y: 0.401}, + {x: 5.34, y: 0.403}, + {x: 6.915, y: 0.411} + ] + }); + expect(countGlObjects()).toBe(2, 'adds a dimmed gl-scatter2d objects'); + }) + .catch(fail) + .then(done); + }); + + it('should work under fast mode with *lasso* dragmode', function(done) { + var _mock = Lib.extendDeep({}, mockFast); + _mock.layout.dragmode = 'lasso'; + gd = createGraphDiv(); + + Plotly.plot(gd, _mock) + .then(delay(100)) + .then(function() { + expect(countGlObjects()).toBe(1); + + return select(lassoPath2); + }) + .then(function(eventData) { + assertEventData(eventData, { + points: [ + {x: 3.911, y: 0.401}, + {x: 5.34, y: 0.403}, + {x: 6.915, y: 0.411} + ] + }); + expect(countGlObjects()).toBe(2); + }) + .catch(fail) + .then(done); + }); + + it('should work under fancy mode with *select* dragmode', function(done) { + var _mock = Lib.extendDeep({}, mockFancy); + _mock.layout.dragmode = 'select'; + gd = createGraphDiv(); + + Plotly.plot(gd, _mock) + .then(delay(100)) + .then(function() { + expect(countGlObjects()).toBe(2, 'has a gl-line2d and a gl-scatter2d-sdf'); + + return select(selectPath); + }) + .then(function(eventData) { + assertEventData(eventData, { + points: [{x: 0.004, y: 12.5}] + }); + expect(countGlObjects()).toBe(2, 'only changes colors of gl-scatter2d-sdf object'); + }) + .catch(fail) + .then(done); + }); + + it('should work under fancy mode with *lasso* dragmode', function(done) { + var _mock = Lib.extendDeep({}, mockFancy); + _mock.layout.dragmode = 'lasso'; + gd = createGraphDiv(); + + Plotly.plot(gd, _mock) + .then(delay(100)) + .then(function() { + expect(countGlObjects()).toBe(2, 'has a gl-line2d and a gl-scatter2d-sdf'); + + return select(lassoPath); + }) + .then(function(eventData) { + assertEventData(eventData, { + points: [{ x: 0.099, y: 2.75 }] + }); + expect(countGlObjects()).toBe(2, 'only changes colors of gl-scatter2d-sdf object'); + }) + .catch(fail) + .then(done); + }); +}); diff --git a/test/jasmine/tests/gl_plot_interact_test.js b/test/jasmine/tests/gl_plot_interact_test.js index f112a779a5b..ae2b085cf16 100644 --- a/test/jasmine/tests/gl_plot_interact_test.js +++ b/test/jasmine/tests/gl_plot_interact_test.js @@ -963,7 +963,6 @@ describe('Test gl2d plots', function() { }); it('should clear orphan cartesian subplots on addTraces', function(done) { - Plotly.newPlot(gd, [], { xaxis: { title: 'X' }, yaxis: { title: 'Y' } @@ -976,12 +975,10 @@ describe('Test gl2d plots', function() { }]); }) .then(function() { - expect(d3.select('.subplot.xy').size()).toEqual(0); expect(d3.select('.xtitle').size()).toEqual(0); expect(d3.select('.ytitle').size()).toEqual(0); }) .then(done); - }); it('supports 1D and 2D Zoom', function(done) { diff --git a/test/jasmine/tests/plot_api_test.js b/test/jasmine/tests/plot_api_test.js index 95418c57349..94e5fe31a19 100644 --- a/test/jasmine/tests/plot_api_test.js +++ b/test/jasmine/tests/plot_api_test.js @@ -332,6 +332,76 @@ describe('Test plot api', function() { }); }); + describe('Plotly.relayout subroutines switchboard', function() { + var mockedMethods = [ + 'layoutReplot', + 'doLegend', + 'layoutStyles', + 'doTicksRelayout', + 'doModeBar', + 'doCamera' + ]; + + beforeAll(function() { + mockedMethods.forEach(function(m) { + spyOn(subroutines, m); + }); + }); + + function mock(gd) { + mockedMethods.forEach(function(m) { + subroutines[m].calls.reset(); + }); + + Plots.supplyDefaults(gd); + Plots.doCalcdata(gd); + return gd; + } + + it('should trigger recalc when switching into select or lasso dragmode', function() { + var gd = mock({ + data: [{ + type: 'scattergl', + x: [1, 2, 3], + y: [1, 2, 3] + }], + layout: { + dragmode: 'zoom' + } + }); + + function expectModeBarOnly() { + expect(gd.calcdata).toBeDefined(); + expect(subroutines.doModeBar).toHaveBeenCalled(); + expect(subroutines.layoutReplot).not.toHaveBeenCalled(); + } + + function expectRecalc() { + expect(gd.calcdata).toBeUndefined(); + expect(subroutines.doModeBar).not.toHaveBeenCalled(); + expect(subroutines.layoutReplot).toHaveBeenCalled(); + } + + Plotly.relayout(gd, 'dragmode', 'pan'); + expectModeBarOnly(); + + Plotly.relayout(mock(gd), 'dragmode', 'lasso'); + expectRecalc(); + + Plotly.relayout(mock(gd), 'dragmode', 'select'); + expectModeBarOnly(); + + Plotly.relayout(mock(gd), 'dragmode', 'lasso'); + expectModeBarOnly(); + + Plotly.relayout(mock(gd), 'dragmode', 'zoom'); + expectModeBarOnly(); + + Plotly.relayout(mock(gd), 'dragmode', 'select'); + expectRecalc(); + }); + }); + describe('Plotly.restyle subroutines switchboard', function() { beforeEach(function() { spyOn(PlotlyInternal, 'plot');