diff --git a/src/components/fx/layout_defaults.js b/src/components/fx/layout_defaults.js index 13d5d631919..a4800d094f0 100644 --- a/src/components/fx/layout_defaults.js +++ b/src/components/fx/layout_defaults.js @@ -28,6 +28,14 @@ module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, fullData) { else hovermodeDflt = 'closest'; coerce('hovermode', hovermodeDflt); + + // if only mapbox subplots is present on graph, + // reset 'zoom' dragmode to 'pan' until 'zoom' is implemented, + // so that the correct modebar button is active + if(layoutOut._has('mapbox') && layoutOut._basePlotModules.length === 1 && + layoutOut.dragmode === 'zoom') { + layoutOut.dragmode = 'pan'; + } }; function isHoriz(fullData) { diff --git a/src/components/modebar/manage.js b/src/components/modebar/manage.js index a97a86eb37d..5cb86a9eea2 100644 --- a/src/components/modebar/manage.js +++ b/src/components/modebar/manage.js @@ -11,6 +11,7 @@ var Axes = require('../../plots/cartesian/axes'); var scatterSubTypes = require('../../traces/scatter/subtypes'); +var Registry = require('../../registry'); var createModeBar = require('./modebar'); var modeBarButtons = require('./buttons'); @@ -78,7 +79,8 @@ function getButtonGroups(gd, buttonsToRemove, buttonsToAdd) { hasGeo = fullLayout._has('geo'), hasPie = fullLayout._has('pie'), hasGL2D = fullLayout._has('gl2d'), - hasTernary = fullLayout._has('ternary'); + hasTernary = fullLayout._has('ternary'), + hasMapbox = fullLayout._has('mapbox'); var groups = []; @@ -121,7 +123,10 @@ function getButtonGroups(gd, buttonsToRemove, buttonsToAdd) { if(((hasCartesian || hasGL2D) && !allAxesFixed) || hasTernary) { dragModeGroup = ['zoom2d', 'pan2d']; } - if((hasCartesian || hasTernary || hasGL2D) && isSelectable(fullData)) { + if(hasMapbox) { + dragModeGroup = ['pan2d']; + } + if(isSelectable(fullData)) { dragModeGroup.push('select2d'); dragModeGroup.push('lasso2d'); } @@ -173,7 +178,7 @@ function isSelectable(fullData) { if(!trace._module || !trace._module.selectPoints) continue; - if(trace.type === 'scatter' || trace.type === 'scatterternary' || trace.type === 'scattergl') { + if(Registry.traceIs(trace, 'scatter-like')) { if(scatterSubTypes.hasMarkers(trace) || scatterSubTypes.hasText(trace)) { selectable = true; } diff --git a/src/constants/interactions.js b/src/constants/interactions.js index 3e56a09f434..20044ea85f9 100644 --- a/src/constants/interactions.js +++ b/src/constants/interactions.js @@ -18,5 +18,8 @@ module.exports = { // ms between first mousedown and 2nd mouseup to constitute dblclick... // we don't seem to have access to the system setting - DBLCLICKDELAY: 300 + DBLCLICKDELAY: 300, + + // opacity dimming fraction for points that are not in selection + DESELECTDIM: 0.2 }; diff --git a/src/lib/index.js b/src/lib/index.js index 25eff611e5c..8518d176d1d 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' || k === 'glTrace') continue; + if(k.charAt(0) === '_' || typeof v === 'function') 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/subroutines.js b/src/plot_api/subroutines.js index 64d1663910d..a007e9ea940 100644 --- a/src/plot_api/subroutines.js +++ b/src/plot_api/subroutines.js @@ -378,21 +378,27 @@ exports.doTicksRelayout = function(gd) { exports.doModeBar = function(gd) { var fullLayout = gd._fullLayout; - var subplotIds, scene, i; + var subplotIds, subplotObj, i; ModeBar.manage(gd); initInteractions(gd); subplotIds = Plots.getSubplotIds(fullLayout, 'gl3d'); for(i = 0; i < subplotIds.length; i++) { - scene = fullLayout[subplotIds[i]]._scene; - scene.updateFx(fullLayout.dragmode, fullLayout.hovermode); + subplotObj = fullLayout[subplotIds[i]]._scene; + subplotObj.updateFx(fullLayout.dragmode, fullLayout.hovermode); } subplotIds = Plots.getSubplotIds(fullLayout, 'gl2d'); for(i = 0; i < subplotIds.length; i++) { - scene = fullLayout._plots[subplotIds[i]]._scene2d; - scene.updateFx(fullLayout.dragmode); + subplotObj = fullLayout._plots[subplotIds[i]]._scene2d; + subplotObj.updateFx(fullLayout.dragmode); + } + + subplotIds = Plots.getSubplotIds(fullLayout, 'mapbox'); + for(i = 0; i < subplotIds.length; i++) { + subplotObj = fullLayout[subplotIds[i]]._subplot; + subplotObj.updateFx(fullLayout); } return Plots.previousPromises(gd); diff --git a/src/plots/cartesian/index.js b/src/plots/cartesian/index.js index 883acb4d0d8..0649a155296 100644 --- a/src/plots/cartesian/index.js +++ b/src/plots/cartesian/index.js @@ -184,11 +184,6 @@ 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/cartesian/select.js b/src/plots/cartesian/select.js index 045585cdbac..d8542f5c090 100644 --- a/src/plots/cartesian/select.js +++ b/src/plots/cartesian/select.js @@ -25,8 +25,9 @@ function getAxId(ax) { return ax._id; } module.exports = function prepSelect(e, startX, startY, dragOptions, mode) { var zoomLayer = dragOptions.gd._fullLayout._zoomlayer, dragBBox = dragOptions.element.getBoundingClientRect(), - xs = dragOptions.plotinfo.xaxis._offset, - ys = dragOptions.plotinfo.yaxis._offset, + plotinfo = dragOptions.plotinfo, + xs = plotinfo.xaxis._offset, + ys = plotinfo.yaxis._offset, x0 = startX - dragBBox.left, y0 = startY - dragBBox.top, x1 = x0, @@ -71,6 +72,7 @@ module.exports = function prepSelect(e, startX, startY, dragOptions, mode) { searchInfo, selection = [], eventData; + for(i = 0; i < gd.calcdata.length; i++) { cd = gd.calcdata[i]; trace = cd[0].trace; @@ -106,9 +108,41 @@ module.exports = function prepSelect(e, startX, startY, dragOptions, mode) { function ascending(a, b) { return a - b; } + // allow subplots to override fillRangeItems routine + var fillRangeItems; + + if(plotinfo.fillRangeItems) { + fillRangeItems = plotinfo.fillRangeItems; + } else { + if(mode === 'select') { + fillRangeItems = function(eventData, poly) { + var ranges = eventData.range = {}; + + for(i = 0; i < allAxes.length; i++) { + var ax = allAxes[i]; + var axLetter = ax._id.charAt(0); + + ranges[ax._id] = [ + ax.p2d(poly[axLetter + 'min']), + ax.p2d(poly[axLetter + 'max']) + ].sort(ascending); + } + }; + } else { + fillRangeItems = function(eventData, poly, pts) { + var dataPts = eventData.lassoPoints = {}; + + for(i = 0; i < allAxes.length; i++) { + var ax = allAxes[i]; + dataPts[ax._id] = pts.filtered.map(axValue(ax)); + } + }; + } + } + dragOptions.moveFn = function(dx0, dy0) { - var poly, - ax; + var poly; + x1 = Math.max(0, Math.min(pw, dx0 + x0)); y1 = Math.max(0, Math.min(ph, dy0 + y0)); @@ -158,27 +192,7 @@ module.exports = function prepSelect(e, startX, startY, dragOptions, mode) { } eventData = {points: selection}; - - if(mode === 'select') { - var ranges = eventData.range = {}, - axLetter; - - for(i = 0; i < allAxes.length; i++) { - ax = allAxes[i]; - axLetter = ax._id.charAt(0); - ranges[ax._id] = [ - ax.p2d(poly[axLetter + 'min']), - ax.p2d(poly[axLetter + 'max'])].sort(ascending); - } - } - else { - var dataPts = eventData.lassoPoints = {}; - - for(i = 0; i < allAxes.length; i++) { - ax = allAxes[i]; - dataPts[ax._id] = pts.filtered.map(axValue(ax)); - } - } + fillRangeItems(eventData, poly, pts); dragOptions.gd.emit('plotly_selecting', eventData); }; diff --git a/src/plots/mapbox/mapbox.js b/src/plots/mapbox/mapbox.js index 30e28eace91..e2c923421b6 100644 --- a/src/plots/mapbox/mapbox.js +++ b/src/plots/mapbox/mapbox.js @@ -13,6 +13,8 @@ var mapboxgl = require('mapbox-gl'); var Fx = require('../../components/fx'); var Lib = require('../../lib'); +var dragElement = require('../../components/dragelement'); +var prepSelect = require('../cartesian/select'); var constants = require('./constants'); var layoutAttributes = require('./layout_attributes'); var createMapboxLayer = require('./layers'); @@ -86,9 +88,9 @@ proto.plot = function(calcData, fullLayout, promises) { }; proto.createMap = function(calcData, fullLayout, resolve, reject) { - var self = this, - gd = self.gd, - opts = self.opts; + var self = this; + var gd = self.gd; + var opts = self.opts; // store style id and URL or object var styleObj = self.styleObj = getStyleObj(opts.style); @@ -107,7 +109,9 @@ proto.createMap = function(calcData, fullLayout, resolve, reject) { pitch: opts.pitch, interactive: !self.isStatic, - preserveDrawingBuffer: self.isStatic + preserveDrawingBuffer: self.isStatic, + + boxZoom: false }); // clear navigation container @@ -128,6 +132,8 @@ proto.createMap = function(calcData, fullLayout, resolve, reject) { self.resolveOnRender(resolve); }); + if(self.isStatic) return; + // keep track of pan / zoom in user layout and emit relayout event map.on('moveend', function(eventData) { if(!self.map) return; @@ -261,6 +267,7 @@ proto.updateLayout = function(fullLayout) { this.updateLayers(); this.updateFramework(fullLayout); + this.updateFx(fullLayout); this.map.resize(); }; @@ -314,6 +321,69 @@ proto.createFramework = function(fullLayout) { self.updateFramework(fullLayout); }; +proto.updateFx = function(fullLayout) { + var self = this; + var map = self.map; + var gd = self.gd; + + if(self.isStatic) return; + + function invert(pxpy) { + var obj = self.map.unproject(pxpy); + return [obj.lng, obj.lat]; + } + + var dragMode = fullLayout.dragmode; + var fillRangeItems; + + if(dragMode === 'select') { + fillRangeItems = function(eventData, poly) { + var ranges = eventData.range = {}; + ranges[self.id] = [ + invert([poly.xmin, poly.ymin]), + invert([poly.xmax, poly.ymax]) + ]; + }; + } else { + fillRangeItems = function(eventData, poly, pts) { + var dataPts = eventData.lassoPoints = {}; + dataPts[self.id] = pts.filtered.map(invert); + }; + } + + if(dragMode === 'select' || dragMode === 'lasso') { + map.dragPan.disable(); + + var dragOptions = { + element: self.div, + gd: gd, + plotinfo: { + xaxis: self.xaxis, + yaxis: self.yaxis, + fillRangeItems: fillRangeItems + }, + xaxes: [self.xaxis], + yaxes: [self.yaxis], + subplot: self.id + }; + + dragOptions.prepFn = function(e, startX, startY) { + prepSelect(e, startX, startY, dragOptions, dragMode); + }; + + dragOptions.doneFn = function(dragged, numClicks) { + if(numClicks === 2) { + fullLayout._zoomlayer.selectAll('.select-outline').remove(); + } + }; + + dragElement.init(dragOptions); + } else { + map.dragPan.enable(); + self.div.onmousedown = null; + } +}; + proto.updateFramework = function(fullLayout) { var domain = fullLayout[this.id].domain, size = fullLayout._size; diff --git a/src/plots/plots.js b/src/plots/plots.js index 38416678886..de398f4353b 100644 --- a/src/plots/plots.js +++ b/src/plots/plots.js @@ -624,6 +624,10 @@ plots.cleanPlot = function(newFullData, newFullLayout, oldFullData, oldFullLayou .selectAll(query).remove(); } } + + if(oldFullLayout._zoomlayer) { + oldFullLayout._zoomlayer.selectAll('.select-outline').remove(); + } }; plots.linkSubplots = function(newFullData, newFullLayout, oldFullData, oldFullLayout) { diff --git a/src/plots/ternary/index.js b/src/plots/ternary/index.js index 2b24ed0938a..e1da80af7b1 100644 --- a/src/plots/ternary/index.js +++ b/src/plots/ternary/index.js @@ -69,8 +69,4 @@ exports.clean = function(newFullData, newFullLayout, oldFullData, oldFullLayout) oldTernary.clipDef.remove(); } } - - if(oldFullLayout._zoomlayer) { - oldFullLayout._zoomlayer.selectAll('.select-outline').remove(); - } }; diff --git a/src/traces/scatter/index.js b/src/traces/scatter/index.js index 197b540e8d2..6817e4c8162 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.categories = ['cartesian', 'symbols', 'markerColorscale', 'errorBarsOK', 'showLegend', 'scatter-like']; Scatter.meta = { description: [ 'The scatter trace type encompasses line charts, scatter charts, text charts, and bubble charts.', diff --git a/src/traces/scatter/select.js b/src/traces/scatter/select.js index 127179a790c..22aedd1685b 100644 --- a/src/traces/scatter/select.js +++ b/src/traces/scatter/select.js @@ -10,8 +10,7 @@ 'use strict'; var subtypes = require('./subtypes'); - -var DESELECTDIM = 0.2; +var DESELECTDIM = require('../../constants/interactions').DESELECTDIM; module.exports = function selectPoints(searchInfo, polygon) { var cd = searchInfo.cd, diff --git a/src/traces/scattergl/convert.js b/src/traces/scattergl/convert.js index f99a4a73a62..5b7be4466d9 100644 --- a/src/traces/scattergl/convert.js +++ b/src/traces/scattergl/convert.js @@ -27,9 +27,9 @@ var makeBubbleSizeFn = require('../scatter/make_bubble_size_func'); var getTraceColor = require('../scatter/get_trace_color'); var MARKER_SYMBOLS = require('../../constants/gl2d_markers'); var DASHES = require('../../constants/gl2d_dashes'); +var DESELECTDIM = require('../../constants/interactions').DESELECTDIM; var AXES = ['xaxis', 'yaxis']; -var DESELECTDIM = 0.2; var TRANSPARENT = [0, 0, 0, 0]; function LineWithMarkers(scene, uid) { @@ -322,8 +322,8 @@ proto.update = function(options, cdscatter) { this.color = getTraceColor(options, {}); // provide reference for selecting points - if(cdscatter && cdscatter[0] && !cdscatter[0].glTrace) { - cdscatter[0].glTrace = this; + if(cdscatter && cdscatter[0] && !cdscatter[0]._glTrace) { + cdscatter[0]._glTrace = this; } }; diff --git a/src/traces/scattergl/index.js b/src/traces/scattergl/index.js index 551e9adad7d..35d292f1c90 100644 --- a/src/traces/scattergl/index.js +++ b/src/traces/scattergl/index.js @@ -23,7 +23,7 @@ ScatterGl.selectPoints = require('./select'); ScatterGl.moduleType = 'trace'; ScatterGl.name = 'scattergl'; ScatterGl.basePlotModule = require('../../plots/gl2d'); -ScatterGl.categories = ['gl2d', 'symbols', 'errorBarsOK', 'markerColorscale', 'showLegend']; +ScatterGl.categories = ['gl2d', 'symbols', 'errorBarsOK', 'markerColorscale', 'showLegend', 'scatter-like']; ScatterGl.meta = { description: [ 'The data visualized as scatter point or lines is set in `x` and `y`', diff --git a/src/traces/scattergl/select.js b/src/traces/scattergl/select.js index 548824ad740..f594c69dd74 100644 --- a/src/traces/scattergl/select.js +++ b/src/traces/scattergl/select.js @@ -22,8 +22,8 @@ module.exports = function selectPoints(searchInfo, polygon) { x, y; - var scattergl = cd[0].glTrace; - var scene = cd[0].glTrace.scene; + var glTrace = cd[0]._glTrace; + var scene = glTrace.scene; var hasOnlyLines = (!subtypes.hasMarkers(trace) && !subtypes.hasText(trace)); if(trace.visible !== true || hasOnlyLines) return; @@ -53,7 +53,7 @@ module.exports = function selectPoints(searchInfo, polygon) { // highlight selected points here trace.selection = selection; - scattergl.update(trace, cd); + glTrace.update(trace, cd); scene.glplot.setDirty(); return selection; diff --git a/src/traces/scattermapbox/attributes.js b/src/traces/scattermapbox/attributes.js index c061f1141aa..5085eeb48fe 100644 --- a/src/traces/scattermapbox/attributes.js +++ b/src/traces/scattermapbox/attributes.js @@ -82,9 +82,7 @@ module.exports = { 'are only available for *circle* symbols.' ].join(' ') }, - opacity: extendFlat({}, markerAttrs.opacity, { - arrayOk: false - }), + opacity: markerAttrs.opacity, size: markerAttrs.size, sizeref: markerAttrs.sizeref, sizemin: markerAttrs.sizemin, diff --git a/src/traces/scattermapbox/convert.js b/src/traces/scattermapbox/convert.js index 01312110dae..7b88ee647d6 100644 --- a/src/traces/scattermapbox/convert.js +++ b/src/traces/scattermapbox/convert.js @@ -9,6 +9,8 @@ 'use strict'; +var isNumeric = require('fast-isnumeric'); + var Lib = require('../../lib'); var BADNUM = require('../../constants/numerical').BADNUM; var geoJsonUtils = require('../../lib/geojson_utils'); @@ -17,10 +19,11 @@ var Colorscale = require('../../components/colorscale'); var makeBubbleSizeFn = require('../scatter/make_bubble_size_func'); var subTypes = require('../scatter/subtypes'); var convertTextOpts = require('../../plots/mapbox/convert_text_opts'); +var DESELECTDIM = require('../../constants/interactions').DESELECTDIM; var COLOR_PROP = 'circle-color'; var SIZE_PROP = 'circle-radius'; - +var OPACITY_PROP = 'circle-opacity'; module.exports = function convert(calcTrace) { var trace = calcTrace[0].trace; @@ -80,12 +83,13 @@ module.exports = function convert(calcTrace) { var hash = {}; hash[COLOR_PROP] = {}; hash[SIZE_PROP] = {}; + hash[OPACITY_PROP] = {}; circle.geojson = makeCircleGeoJSON(calcTrace, hash); circle.layout.visibility = 'visible'; Lib.extendFlat(circle.paint, { - 'circle-opacity': trace.opacity * trace.marker.opacity, + 'circle-opacity': calcCircleOpacity(trace, hash), 'circle-color': calcCircleColor(trace, hash), 'circle-radius': calcCircleRadius(trace, hash) }); @@ -179,8 +183,22 @@ function makeCircleGeoJSON(calcTrace, hash) { var sizeFn; if(subTypes.isBubble(trace)) { sizeFn = makeBubbleSizeFn(trace); - } else if(Array.isArray(marker.size)) { - sizeFn = Lib.identity; + } + + function combineOpacities(d, mo) { + return trace.opacity * mo * (d.dim ? DESELECTDIM : 1); + } + + var opacityFn; + if(Array.isArray(marker.opacity)) { + opacityFn = function(d) { + var mo = isNumeric(d.mo) ? +Lib.constrain(d.mo, 0, 1) : 0; + return combineOpacities(d, mo); + }; + } else if(trace._hasDimmedPts) { + opacityFn = function(d) { + return combineOpacities(d, marker.opacity); + }; } // Translate vals in trace arrayOk containers @@ -204,7 +222,12 @@ function makeCircleGeoJSON(calcTrace, hash) { var mcc = calcPt.mcc = colorFn(calcPt.mc); translate(props, COLOR_PROP, mcc, i); } - if(sizeFn) translate(props, SIZE_PROP, sizeFn(calcPt.ms), i); + if(sizeFn) { + translate(props, SIZE_PROP, sizeFn(calcPt.ms), i); + } + if(opacityFn) { + translate(props, OPACITY_PROP, opacityFn(calcPt), i); + } features.push({ type: 'Feature', @@ -304,14 +327,9 @@ function calcCircleRadius(trace, hash) { stops.push([ hash[SIZE_PROP][val], +val ]); } - // stops indices must be sorted - stops.sort(function(a, b) { - return a[0] - b[0]; - }); - out = { property: SIZE_PROP, - stops: stops + stops: stops.sort(ascending) }; } else { @@ -321,6 +339,31 @@ function calcCircleRadius(trace, hash) { return out; } +function calcCircleOpacity(trace, hash) { + var marker = trace.marker; + var out; + + if(Array.isArray(marker.opacity) || trace._hasDimmedPts) { + var vals = Object.keys(hash[OPACITY_PROP]); + var stops = []; + + for(var i = 0; i < vals.length; i++) { + var val = vals[i]; + stops.push([hash[OPACITY_PROP][val], +val]); + } + + out = { + property: OPACITY_PROP, + stops: stops.sort(ascending) + }; + } + else { + out = trace.opacity * marker.opacity; + } + + return out; +} + function getFillFunc(attr) { if(Array.isArray(attr)) { return function(v) { return v; }; @@ -335,6 +378,8 @@ function getFillFunc(attr) { function blankFillFunc() { return ''; } +function ascending(a, b) { return a[0] - b[0]; } + // only need to check lon (OR lat) function isBADNUM(lonlat) { return lonlat[0] === BADNUM; diff --git a/src/traces/scattermapbox/index.js b/src/traces/scattermapbox/index.js index 6de4241ed82..251432a82ff 100644 --- a/src/traces/scattermapbox/index.js +++ b/src/traces/scattermapbox/index.js @@ -15,14 +15,15 @@ ScatterMapbox.attributes = require('./attributes'); ScatterMapbox.supplyDefaults = require('./defaults'); ScatterMapbox.colorbar = require('../scatter/colorbar'); ScatterMapbox.calc = require('../scattergeo/calc'); +ScatterMapbox.plot = require('./plot'); ScatterMapbox.hoverPoints = require('./hover'); ScatterMapbox.eventData = require('./event_data'); -ScatterMapbox.plot = require('./plot'); +ScatterMapbox.selectPoints = require('./select'); ScatterMapbox.moduleType = 'trace'; ScatterMapbox.name = 'scattermapbox'; ScatterMapbox.basePlotModule = require('../../plots/mapbox'); -ScatterMapbox.categories = ['mapbox', 'gl', 'symbols', 'markerColorscale', 'showLegend']; +ScatterMapbox.categories = ['mapbox', 'gl', 'symbols', 'markerColorscale', 'showLegend', 'scatterlike']; ScatterMapbox.meta = { hrName: 'scatter_mapbox', description: [ diff --git a/src/traces/scattermapbox/plot.js b/src/traces/scattermapbox/plot.js index f7cc4040a2f..0a4391371cf 100644 --- a/src/traces/scattermapbox/plot.js +++ b/src/traces/scattermapbox/plot.js @@ -92,6 +92,9 @@ proto.update = function update(calcTrace) { mapbox.setSourceData(this.idSourceSymbol, opts.symbol.geojson); mapbox.setOptions(this.idLayerSymbol, 'setPaintProperty', opts.symbol.paint); } + + // link ref for quick update during selections + calcTrace[0].trace._glTrace = this; }; proto.dispose = function dispose() { diff --git a/src/traces/scattermapbox/select.js b/src/traces/scattermapbox/select.js new file mode 100644 index 00000000000..5ceb57b51a4 --- /dev/null +++ b/src/traces/scattermapbox/select.js @@ -0,0 +1,57 @@ +/** +* 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; + var xa = searchInfo.xaxis; + var ya = searchInfo.yaxis; + var selection = []; + var trace = cd[0].trace; + + var di, lonlat, x, y, i; + + // flag used in ./convert.js + // to not insert data-driven 'circle-opacity' when we don't need to + trace._hasDimmedPts = false; + + if(trace.visible !== true || !subtypes.hasMarkers(trace)) return; + + if(polygon === false) { + for(i = 0; i < cd.length; i++) { + cd[i].dim = 0; + } + } else { + for(i = 0; i < cd.length; i++) { + di = cd[i]; + lonlat = di.lonlat; + x = xa.c2p(lonlat); + y = ya.c2p(lonlat); + + if(polygon.contains([x, y])) { + trace._hasDimmedPts = true; + selection.push({ + pointNumber: i, + lon: lonlat[0], + lat: lonlat[1] + }); + di.dim = 0; + } else { + di.dim = 1; + } + } + } + + trace._glTrace.update(cd); + + return selection; +}; diff --git a/src/traces/scatterternary/index.js b/src/traces/scatterternary/index.js index e7e75c4ddfc..abbdc7f06b1 100644 --- a/src/traces/scatterternary/index.js +++ b/src/traces/scatterternary/index.js @@ -22,7 +22,7 @@ ScatterTernary.selectPoints = require('./select'); ScatterTernary.moduleType = 'trace'; ScatterTernary.name = 'scatterternary'; ScatterTernary.basePlotModule = require('../../plots/ternary'); -ScatterTernary.categories = ['ternary', 'symbols', 'markerColorscale', 'showLegend']; +ScatterTernary.categories = ['ternary', 'symbols', 'markerColorscale', 'showLegend', 'scatter-like']; ScatterTernary.meta = { hrName: 'scatter_ternary', description: [ diff --git a/tasks/noci_test.sh b/tasks/noci_test.sh index b4a2283b673..17d7dfc4c50 100755 --- a/tasks/noci_test.sh +++ b/tasks/noci_test.sh @@ -8,7 +8,7 @@ EXIT_STATE=0 npm run test-jasmine -- --tags=noCI --nowatch || EXIT_STATE=$? # mapbox image tests take too much resources on CI -npm run test-image -- mapbox_* || EXIT_STATE=$? +npm run test-image -- mapbox_* --queue || EXIT_STATE=$? # run gl2d image test again (some mocks are skipped on CI) npm run test-image-gl2d || EXIT_STATE=$? diff --git a/test/image/baselines/mapbox_bubbles.png b/test/image/baselines/mapbox_bubbles.png index 11b5e21aa70..e0544baa69a 100644 Binary files a/test/image/baselines/mapbox_bubbles.png and b/test/image/baselines/mapbox_bubbles.png differ diff --git a/test/image/mocks/mapbox_bubbles.json b/test/image/mocks/mapbox_bubbles.json index 756ec774227..3f72cc2dbce 100644 --- a/test/image/mocks/mapbox_bubbles.json +++ b/test/image/mocks/mapbox_bubbles.json @@ -2,72 +2,30 @@ "data": [ { "type": "scattermapbox", - "lon": [ - 10, - 20, - 30 - ], - "lat": [ - 10, - 20, - 30 - ], + "lon": [10, 20, 30], + "lat": [10, 20, 30], "marker": { - "size": [ - 20, - 10, - 40 - ], - "color": [ - "red", - "blue", - "orange" - ] - } + "size": [20, 10, 40], + "color": ["red", "blue", "orange"], + "opacity": [0.3, 0.5, 1] + }, + "opacity": 0.7 }, { "type": "scattermapbox", - "lon": [ - -75, - -120, - 100 - ], - "lat": [ - 45, - 20, - -40 - ], + "lon": [-75, -120, 100], + "lat": [45, 20, -40], "marker": { - "size": [ - 60, - 20, - 40 - ], - "color": [ - 0, - 20, - 30 - ], + "size": [60, 20, 40], + "color": [0, 20, 30], "colorbar": {}, "cmin": 0, "cmax": 30, "colorscale": [ - [ - 0, - "rgb(220,220,220)" - ], - [ - 0.2, - "rgb(245,195,157)" - ], - [ - 0.4, - "rgb(245,160,105)" - ], - [ - 1, - "rgb(178,10,28)" - ] + [0, "rgb(220,220,220)"], + [0.2, "rgb(245,195,157)"], + [0.4, "rgb(245,160,105)"], + [1, "rgb(178,10,28)"] ] } } @@ -79,7 +37,7 @@ }, "showlegend": false, "height": 450, - "width": 1100, - "autosize": true + "width": 600, + "margin": {"l": 10} } } diff --git a/test/jasmine/tests/mapbox_test.js b/test/jasmine/tests/mapbox_test.js index 49b4583ff7f..050605b2494 100644 --- a/test/jasmine/tests/mapbox_test.js +++ b/test/jasmine/tests/mapbox_test.js @@ -1,4 +1,5 @@ var Plotly = require('@lib'); +var Plots = require('@src/plots/plots'); var Lib = require('@src/lib'); var constants = require('@src/plots/mapbox/constants'); @@ -31,7 +32,7 @@ describe('mapbox defaults', function() { beforeEach(function() { layoutOut = { font: { color: 'red' } }; - // needs a ternary-ref in a trace in order to be detected + // needs a mapbox-ref in a trace in order to be detected fullData = [{ type: 'scattermapbox', subplot: 'mapbox' }]; }); @@ -170,6 +171,16 @@ describe('mapbox defaults', function() { expect(layoutOut.mapbox.layers[3].fill).toBeUndefined(); expect(layoutOut.mapbox.layers[3].circle).toBeUndefined(); }); + + it('should set *layout.dragmode* to pan while zoom is not available', function() { + var gd = { + data: fullData, + layout: {} + }; + + Plots.supplyDefaults(gd); + expect(gd._fullLayout.dragmode).toBe('pan'); + }); }); describe('mapbox credentials', function() { diff --git a/test/jasmine/tests/scattermapbox_test.js b/test/jasmine/tests/scattermapbox_test.js index 5045fd8532d..938f96cd445 100644 --- a/test/jasmine/tests/scattermapbox_test.js +++ b/test/jasmine/tests/scattermapbox_test.js @@ -114,7 +114,7 @@ describe('scattermapbox convert', function() { jasmine.addMatchers(customMatchers); }); - function _convert(trace) { + function _convert(trace, selected) { var gd = { data: [trace] }; Plots.supplyDefaults(gd); @@ -122,6 +122,18 @@ describe('scattermapbox convert', function() { Plots.doCalcdata(gd, fullTrace); var calcTrace = gd.calcdata[0]; + + if(selected) { + var hasDimmedPts = false; + + selected.forEach(function(v, i) { + if(v) hasDimmedPts = true; + calcTrace[i].dim = v; + }); + + fullTrace._hasDimmedPts = hasDimmedPts; + } + return convert(calcTrace); } @@ -155,6 +167,8 @@ describe('scattermapbox convert', function() { stops: [ [0, 5], [1, 10], [2, 0] ] }, 'circle-radius stops'); + expect(opts.circle.paint['circle-opacity']).toBe(0.7, 'circle-opacity'); + var circleProps = opts.circle.geojson.features.map(function(f) { return f.properties; }); @@ -169,6 +183,95 @@ describe('scattermapbox convert', function() { ], 'geojson feature properties'); }); + it('should fill circle-opacity correctly', function() { + var opts = _convert(Lib.extendFlat({}, base, { + mode: 'markers', + marker: { + symbol: 'circle', + size: 10, + color: 'red', + opacity: [1, null, 0.5, '0.5', '1', 0, 0.8] + }, + opacity: 0.5 + })); + + assertVisibility(opts, ['none', 'none', 'visible', 'none']); + expect(opts.circle.paint['circle-color']).toBe('red', 'circle-color'); + expect(opts.circle.paint['circle-radius']).toBe(5, 'circle-radius'); + + expect(opts.circle.paint['circle-opacity']).toEqual({ + property: 'circle-opacity', + stops: [ [0, 0.5], [1, 0], [2, 0.25], [6, 0.4] ] + }, 'circle-opacity stops'); + + var circleProps = opts.circle.geojson.features.map(function(f) { + return f.properties; + }); + + + expect(circleProps).toEqual([ + { 'circle-opacity': 0 }, + { 'circle-opacity': 1 }, + { 'circle-opacity': 2 }, + // lat === null + // lon === null + { 'circle-opacity': 1 }, + { 'circle-opacity': 6 }, + ], 'geojson feature properties'); + }); + + it('should fill circle-opacity correctly during selections', function() { + var _base = { + type: 'scattermapbox', + mode: 'markers', + lon: [-10, 30, 20], + lat: [45, 90, 180], + marker: {symbol: 'circle'} + }; + + var specs = [{ + patch: {}, + selected: [0, 1, 1], + expected: {stops: [[0, 1], [1, 0.2]], props: [0, 1, 1]} + }, { + patch: {opacity: 0.5}, + selected: [0, 1, 1], + expected: {stops: [[0, 0.5], [1, 0.1]], props: [0, 1, 1]} + }, { + patch: { + marker: {opacity: 0.6} + }, + selected: [1, 0, 1], + expected: {stops: [[0, 0.12], [1, 0.6]], props: [0, 1, 0]} + }, { + patch: { + marker: {opacity: [0.5, 1, 0.6]} + }, + selected: [1, 0, 1], + expected: {stops: [[0, 0.1], [1, 1], [2, 0.12]], props: [0, 1, 2]} + }, { + patch: { + marker: {opacity: [2, null, -0.6]} + }, + selected: [1, 1, 1], + expected: {stops: [[0, 0.2], [1, 0]], props: [0, 1, 1]} + }]; + + specs.forEach(function(s, i) { + var msg0 = '- case ' + i + ' '; + var opts = _convert(Lib.extendDeep({}, _base, s.patch), s.selected); + + expect(opts.circle.paint['circle-opacity'].stops) + .toEqual(s.expected.stops, msg0 + 'stops'); + + var props = opts.circle.geojson.features.map(function(f) { + return f.properties['circle-opacity']; + }); + + expect(props).toEqual(s.expected.props, msg0 + 'props'); + }); + }); + it('should generate correct output for fill + markers + lines traces', function() { var opts = _convert(Lib.extendFlat({}, base, { mode: 'markers+lines', @@ -510,7 +613,6 @@ describe('@noCI scattermapbox hover', function() { }); }); - describe('@noCI Test plotly events on a scattermapbox plot:', function() { var mock = require('@mocks/mapbox_0.json'); diff --git a/test/jasmine/tests/select_test.js b/test/jasmine/tests/select_test.js index 86d0825700c..ac26eccb435 100644 --- a/test/jasmine/tests/select_test.js +++ b/test/jasmine/tests/select_test.js @@ -10,6 +10,8 @@ var fail = require('../assets/fail_test'); var mouseEvent = require('../assets/mouse_event'); var customMatchers = require('../assets/custom_matchers'); +var LONG_TIMEOUT_INTERVAL = 5 * jasmine.DEFAULT_TIMEOUT_INTERVAL; + describe('select box and lasso', function() { var mock = require('@mocks/14.json'); @@ -284,6 +286,11 @@ describe('select box and lasso', function() { y: 2.75, }], 'with the correct selected points (2)'); + expect(selectedData.lassoPoints.x).toBeCloseToArray( + [0.084, 0.087, 0.115, 0.103], 'lasso points x coords'); + expect(selectedData.lassoPoints.y).toBeCloseToArray( + [4.648, 1.342, 1.247, 4.821], 'lasso points y coords'); + doubleClick(250, 200).then(function() { expect(doubleClickData).toBe(null, 'with the correct deselect data'); done(); @@ -460,4 +467,134 @@ describe('select box and lasso', function() { .catch(fail) .then(done); }); + + it('@noCI should work on scattermapbox traces', function(done) { + var fig = Lib.extendDeep({}, require('@mocks/mapbox_bubbles-text')); + var gd = createGraphDiv(); + var eventData; + + fig.layout.dragmode = 'select'; + fig.config = { + mapboxAccessToken: require('@build/credentials.json').MAPBOX_ACCESS_TOKEN + }; + + function assertPoints(expected) { + var pts = eventData.points || []; + + expect(pts.length).toBe(expected.length, 'selected points length'); + + pts.forEach(function(p, i) { + var e = expected[i]; + expect(p.lon).toBe(e.lon, 'selected pt lon val'); + expect(p.lat).toBe(e.lat, 'selected pt lat val'); + }); + } + + function assertRanges(expected) { + var ranges = (eventData.range || {}).mapbox || []; + expect(ranges).toBeCloseTo2DArray(expected, 1, 'select box range (in lon/lat)'); + } + + function assertLassoPoints(expected) { + var lassoPoints = (eventData.lassoPoints || {}).mapbox || []; + expect(lassoPoints).toBeCloseTo2DArray(expected, 1, 'lasso points (in lon/lat)'); + } + + Plotly.plot(gd, fig).then(function() { + var selectingCnt = 0; + var selectedCnt = 0; + var deselectCnt = 0; + + gd.once('plotly_selecting', function() { + assertSelectionNodes(1, 2); + selectingCnt++; + }); + + gd.once('plotly_selected', function(d) { + assertSelectionNodes(0, 2); + selectedCnt++; + eventData = d; + }); + + gd.once('plotly_deselect', function() { + deselectCnt++; + assertSelectionNodes(0, 0); + }); + + drag([[370, 120], [500, 200]]); + assertPoints([{lon: 30, lat: 30}]); + assertRanges([[21.99, 34.55], [38.14, 25.98]]); + + return doubleClick(250, 200).then(function() { + expect(selectingCnt).toBe(1, 'plotly_selecting call count'); + expect(selectedCnt).toBe(1, 'plotly_selected call count'); + expect(deselectCnt).toBe(1, 'plotly_deselect call count'); + }); + }) + .then(function() { + return Plotly.relayout(gd, 'dragmode', 'lasso'); + }) + .then(function() { + var selectingCnt = 0; + var selectedCnt = 0; + var deselectCnt = 0; + + gd.once('plotly_selecting', function() { + assertSelectionNodes(1, 2); + selectingCnt++; + }); + + gd.once('plotly_selected', function(d) { + assertSelectionNodes(0, 2); + selectedCnt++; + eventData = d; + }); + + gd.once('plotly_deselect', function() { + deselectCnt++; + assertSelectionNodes(0, 0); + }); + + drag([[300, 200], [300, 300], [400, 300], [400, 200], [300, 200]]); + assertPoints([{lon: 20, lat: 20}]); + assertLassoPoints([ + [13.28, 25.97], [13.28, 14.33], [25.71, 14.33], [25.71, 25.97], [13.28, 25.97] + ]); + + return doubleClick(250, 200).then(function() { + expect(selectingCnt).toBe(1, 'plotly_selecting call count'); + expect(selectedCnt).toBe(1, 'plotly_selected call count'); + expect(deselectCnt).toBe(1, 'plotly_deselect call count'); + }); + }) + .then(function() { + // make selection handlers don't get called in 'pan' dragmode + return Plotly.relayout(gd, 'dragmode', 'pan'); + }) + .then(function() { + var selectingCnt = 0; + var selectedCnt = 0; + var deselectCnt = 0; + + gd.once('plotly_selecting', function() { + selectingCnt++; + }); + + gd.once('plotly_selected', function() { + selectedCnt++; + }); + + gd.once('plotly_deselect', function() { + deselectCnt++; + }); + + return doubleClick(250, 200).then(function() { + expect(selectingCnt).toBe(0, 'plotly_selecting call count'); + expect(selectedCnt).toBe(0, 'plotly_selected call count'); + expect(deselectCnt).toBe(0, 'plotly_deselect call count'); + }); + }) + .catch(fail) + .then(done); + }, LONG_TIMEOUT_INTERVAL); });