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;
}
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.',
4 changes: 2 additions & 2 deletions src/traces/scattergl/convert.js
Original file line number Diff line number Diff line change
@@ -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;
Loading