Skip to content

Add scattermapbox select/lasso drag modes #1836

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Jul 10, 2017
8 changes: 8 additions & 0 deletions src/components/fx/layout_defaults.js
Original file line number Diff line number Diff line change
@@ -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) {
11 changes: 8 additions & 3 deletions src/components/modebar/manage.js
Original file line number Diff line number Diff line change
@@ -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;
}
5 changes: 4 additions & 1 deletion src/constants/interactions.js
Original file line number Diff line number Diff line change
@@ -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
};
2 changes: 1 addition & 1 deletion src/lib/index.js
Original file line number Diff line number Diff line change
@@ -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]);
16 changes: 11 additions & 5 deletions src/plot_api/subroutines.js
Original file line number Diff line number Diff line change
@@ -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);
5 changes: 0 additions & 5 deletions src/plots/cartesian/index.js
Original file line number Diff line number Diff line change
@@ -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) {
64 changes: 39 additions & 25 deletions src/plots/cartesian/select.js
Original file line number Diff line number Diff line change
@@ -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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, probably we have to merge this before I continue multiselect, because I depend on this part of code a lot.

dragOptions.gd.emit('plotly_selecting', eventData);
};

78 changes: 74 additions & 4 deletions src/plots/mapbox/mapbox.js
Original file line number Diff line number Diff line change
@@ -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;
4 changes: 4 additions & 0 deletions src/plots/plots.js
Original file line number Diff line number Diff line change
@@ -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) {
4 changes: 0 additions & 4 deletions src/plots/ternary/index.js
Original file line number Diff line number Diff line change
@@ -69,8 +69,4 @@ exports.clean = function(newFullData, newFullLayout, oldFullData, oldFullLayout)
oldTernary.clipDef.remove();
}
}

if(oldFullLayout._zoomlayer) {
oldFullLayout._zoomlayer.selectAll('.select-outline').remove();
}
};
2 changes: 1 addition & 1 deletion src/traces/scatter/index.js
Original file line number Diff line number Diff line change
@@ -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.',
3 changes: 1 addition & 2 deletions src/traces/scatter/select.js
Original file line number Diff line number Diff line change
@@ -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,
6 changes: 3 additions & 3 deletions src/traces/scattergl/convert.js
Original file line number Diff line number Diff line change
@@ -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;
}
};

2 changes: 1 addition & 1 deletion src/traces/scattergl/index.js
Original file line number Diff line number Diff line change
@@ -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`',
6 changes: 3 additions & 3 deletions src/traces/scattergl/select.js
Original file line number Diff line number Diff line change
@@ -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;
4 changes: 1 addition & 3 deletions src/traces/scattermapbox/attributes.js
Original file line number Diff line number Diff line change
@@ -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,
67 changes: 56 additions & 11 deletions src/traces/scattermapbox/convert.js
Original file line number Diff line number Diff line change
@@ -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;
5 changes: 3 additions & 2 deletions src/traces/scattermapbox/index.js
Original file line number Diff line number Diff line change
@@ -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: [
3 changes: 3 additions & 0 deletions src/traces/scattermapbox/plot.js
Original file line number Diff line number Diff line change
@@ -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() {
57 changes: 57 additions & 0 deletions src/traces/scattermapbox/select.js
Original file line number Diff line number Diff line change
@@ -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;
};
2 changes: 1 addition & 1 deletion src/traces/scatterternary/index.js
Original file line number Diff line number Diff line change
@@ -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: [
2 changes: 1 addition & 1 deletion tasks/noci_test.sh
Original file line number Diff line number Diff line change
@@ -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=$?
Binary file modified test/image/baselines/mapbox_bubbles.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
76 changes: 17 additions & 59 deletions test/image/mocks/mapbox_bubbles.json
Original file line number Diff line number Diff line change
@@ -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}
}
}
13 changes: 12 additions & 1 deletion test/jasmine/tests/mapbox_test.js
Original file line number Diff line number Diff line change
@@ -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() {
106 changes: 104 additions & 2 deletions test/jasmine/tests/scattermapbox_test.js
Original file line number Diff line number Diff line change
@@ -114,14 +114,26 @@ describe('scattermapbox convert', function() {
jasmine.addMatchers(customMatchers);
});

function _convert(trace) {
function _convert(trace, selected) {
var gd = { data: [trace] };
Plots.supplyDefaults(gd);

var fullTrace = gd._fullData[0];
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');

137 changes: 137 additions & 0 deletions test/jasmine/tests/select_test.js
Original file line number Diff line number Diff line change
@@ -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);
});