Skip to content

Fix scattergl selectedpoints clearance under select/lasso drag modes #2492

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 3 commits into from
Mar 26, 2018
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 0 additions & 19 deletions src/constants/gl2d_dashes.js

This file was deleted.

8 changes: 4 additions & 4 deletions src/traces/scattergl/attributes.js
Original file line number Diff line number Diff line change
@@ -12,13 +12,13 @@ var plotAttrs = require('../../plots/attributes');
var scatterAttrs = require('../scatter/attributes');
var colorAttrs = require('../../components/colorscale/color_attributes');

var DASHES = require('../../constants/gl2d_dashes');
var extendFlat = require('../../lib/extend').extendFlat;
var overrideAll = require('../../plot_api/edit_types').overrideAll;
var DASHES = require('./constants').DASHES;

var scatterLineAttrs = scatterAttrs.line,
scatterMarkerAttrs = scatterAttrs.marker,
scatterMarkerLineAttrs = scatterMarkerAttrs.line;
var scatterLineAttrs = scatterAttrs.line;
var scatterMarkerAttrs = scatterAttrs.marker;
var scatterMarkerLineAttrs = scatterMarkerAttrs.line;

var attrs = module.exports = overrideAll({
x: scatterAttrs.x,
31 changes: 31 additions & 0 deletions src/traces/scattergl/constants.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/**
* Copyright 2012-2018, 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 SYMBOL_SIZE = 20;

module.exports = {
TOO_MANY_POINTS: 1e5,

SYMBOL_SDF_SIZE: 200,
SYMBOL_SIZE: SYMBOL_SIZE,
SYMBOL_STROKE: SYMBOL_SIZE / 20,

DOT_RE: /-dot/,
OPEN_RE: /-open/,

DASHES: {
solid: [1],
dot: [1, 1],
dash: [4, 1],
longdash: [8, 1],
dashdot: [4, 1, 1, 1],
longdashdot: [8, 1, 1, 1]
}
};
397 changes: 397 additions & 0 deletions src/traces/scattergl/convert.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,397 @@
/**
* Copyright 2012-2018, 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 svgSdf = require('svg-path-sdf');
var rgba = require('color-normalize');

var Registry = require('../../registry');
var Lib = require('../../lib');
var Drawing = require('../../components/drawing');
var AxisIDs = require('../../plots/cartesian/axis_ids');

var formatColor = require('../../lib/gl_format_color');
var subTypes = require('../scatter/subtypes');
var makeBubbleSizeFn = require('../scatter/make_bubble_size_func');

var constants = require('./constants');

function convertStyle(gd, trace) {
var i;

var opts = {
marker: null,
line: null,
fill: null,
errorX: null,
errorY: null,
selected: null,
unselected: null
};

if(trace.visible !== true) return opts;

if(subTypes.hasMarkers(trace)) {
opts.marker = convertMarkerStyle(trace);
opts.selected = convertMarkerSelection(trace, trace.selected);
Copy link
Contributor

Choose a reason for hiding this comment

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

In some reason trace.selected is empty here for multiple colors/multiple opacities #2500

Copy link
Contributor Author

@etpinard etpinard Mar 26, 2018

Choose a reason for hiding this comment

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

Ok - I'll take a look at this tomorrow

opts.unselected = convertMarkerSelection(trace, trace.unselected);
}

if(subTypes.hasLines(trace)) {
opts.line = {
overlay: true,
thickness: trace.line.width,
color: trace.line.color,
opacity: trace.opacity
};

var dashes = (constants.DASHES[trace.line.dash] || [1]).slice();
for(i = 0; i < dashes.length; ++i) {
dashes[i] *= trace.line.width;
}
opts.line.dashes = dashes;
}

if(trace.error_x && trace.error_x.visible) {
opts.errorX = convertErrorBarStyle(trace, trace.error_x);
}

if(trace.error_y && trace.error_y.visible) {
opts.errorY = convertErrorBarStyle(trace, trace.error_y);
}

if(!!trace.fill && trace.fill !== 'none') {
opts.fill = {
closed: true,
fill: trace.fillcolor,
thickness: 0
};
}

return opts;
}

function convertMarkerStyle(trace) {
var count = trace._length || (trace.dimensions || [])._length;
var optsIn = trace.marker;
var optsOut = {};
var i;

var multiSymbol = Array.isArray(optsIn.symbol);
var multiColor = Lib.isArrayOrTypedArray(optsIn.color);
var multiLineColor = Lib.isArrayOrTypedArray(optsIn.line.color);
var multiOpacity = Lib.isArrayOrTypedArray(optsIn.opacity);
var multiSize = Lib.isArrayOrTypedArray(optsIn.size);
var multiLineWidth = Lib.isArrayOrTypedArray(optsIn.line.width);

var isOpen;
if(!multiSymbol) isOpen = constants.OPEN_RE.test(optsIn.symbol);

// prepare colors
if(multiSymbol || multiColor || multiLineColor || multiOpacity) {
optsOut.colors = new Array(count);
optsOut.borderColors = new Array(count);

var colors = formatColor(optsIn, optsIn.opacity, count);
var borderColors = formatColor(optsIn.line, optsIn.opacity, count);

if(!Array.isArray(borderColors[0])) {
var borderColor = borderColors;
borderColors = Array(count);
for(i = 0; i < count; i++) {
borderColors[i] = borderColor;
}
}
if(!Array.isArray(colors[0])) {
var color = colors;
colors = Array(count);
for(i = 0; i < count; i++) {
colors[i] = color;
}
}

optsOut.colors = colors;
optsOut.borderColors = borderColors;

for(i = 0; i < count; i++) {
if(multiSymbol) {
var symbol = optsIn.symbol[i];
isOpen = constants.OPEN_RE.test(symbol);
}
if(isOpen) {
borderColors[i] = colors[i].slice();
colors[i] = colors[i].slice();
colors[i][3] = 0;
}
}

optsOut.opacity = trace.opacity;
} else {
if(isOpen) {
optsOut.color = rgba(optsIn.color, 'uint8');
optsOut.color[3] = 0;
optsOut.borderColor = rgba(optsIn.color, 'uint8');
} else {
optsOut.color = rgba(optsIn.color, 'uint8');
optsOut.borderColor = rgba(optsIn.line.color, 'uint8');
}

optsOut.opacity = trace.opacity * optsIn.opacity;
}

// prepare symbols
if(multiSymbol) {
optsOut.markers = new Array(count);
for(i = 0; i < count; i++) {
optsOut.markers[i] = getSymbolSdf(optsIn.symbol[i]);
}
} else {
optsOut.marker = getSymbolSdf(optsIn.symbol);
}

// prepare sizes
var markerSizeFunc = makeBubbleSizeFn(trace);
var s;

if(multiSize || multiLineWidth) {
var sizes = optsOut.sizes = new Array(count);
var borderSizes = optsOut.borderSizes = new Array(count);
var sizeTotal = 0;
var sizeAvg;

if(multiSize) {
for(i = 0; i < count; i++) {
sizes[i] = markerSizeFunc(optsIn.size[i]);
sizeTotal += sizes[i];
}
sizeAvg = sizeTotal / count;
} else {
s = markerSizeFunc(optsIn.size);
for(i = 0; i < count; i++) {
sizes[i] = s;
}
}

// See https://github.com/plotly/plotly.js/pull/1781#discussion_r121820798
if(multiLineWidth) {
for(i = 0; i < count; i++) {
borderSizes[i] = markerSizeFunc(optsIn.line.width[i]);
}
} else {
s = markerSizeFunc(optsIn.line.width);
for(i = 0; i < count; i++) {
borderSizes[i] = s;
}
}

optsOut.sizeAvg = sizeAvg;
} else {
optsOut.size = markerSizeFunc(optsIn && optsIn.size || 10);
optsOut.borderSizes = markerSizeFunc(optsIn.line.width);
}

return optsOut;
}

function convertMarkerSelection(trace, target) {
var optsIn = trace.marker;
var optsOut = {};

if(!target) return optsOut;

if(target.marker && target.marker.symbol) {
optsOut = convertMarkerStyle(Lib.extendFlat({}, optsIn, target.marker));
} else if(target.marker) {
if(target.marker.size) optsOut.sizes = target.marker.size;
if(target.marker.color) optsOut.colors = target.marker.color;
if(target.marker.opacity !== undefined) optsOut.opacity = target.marker.opacity;
}

return optsOut;
}

function convertErrorBarStyle(trace, target) {
var optsOut = {
capSize: target.width * 2,
lineWidth: target.thickness,
color: target.color
};

if(target.copy_ystyle) {
optsOut = trace.error_y;
}

return optsOut;
}

var SYMBOL_SDF_SIZE = constants.SYMBOL_SDF_SIZE;
var SYMBOL_SIZE = constants.SYMBOL_SIZE;
var SYMBOL_STROKE = constants.SYMBOL_STROKE;
var SYMBOL_SDF = {};
var SYMBOL_SVG_CIRCLE = Drawing.symbolFuncs[0](SYMBOL_SIZE * 0.05);

function getSymbolSdf(symbol) {
if(symbol === 'circle') return null;

var symbolPath, symbolSdf;
var symbolNumber = Drawing.symbolNumber(symbol);
var symbolFunc = Drawing.symbolFuncs[symbolNumber % 100];
var symbolNoDot = !!Drawing.symbolNoDot[symbolNumber % 100];
var symbolNoFill = !!Drawing.symbolNoFill[symbolNumber % 100];

var isDot = constants.DOT_RE.test(symbol);
Copy link
Collaborator

Choose a reason for hiding this comment

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

I know this isn't new here, just noticing it: this won't work if symbol is provided by number in the first place. Better to just test the number once we have it - ie isDot = symbolNumber >= 200 (and for isOpen isOpen = (symbolNumber % 200) >= 100 - perhaps we should make these into helper functions in the Drawing module?). Probably not a very well-known feature, but it is supported by SVG, and could be useful particularly for symbol arrays, as you could use a typed array.

Dunno if anyone would use both names and numbers in the same plot, but if they did, it would also help to key the SYMBOL_SDF cache off the number.

Copy link
Contributor Author

@etpinard etpinard Mar 26, 2018

Choose a reason for hiding this comment

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

Good eye. This probably deserves an issue of its own.


// get symbol sdf from cache or generate it
if(SYMBOL_SDF[symbol]) return SYMBOL_SDF[symbol];

if(isDot && !symbolNoDot) {
symbolPath = symbolFunc(SYMBOL_SIZE * 1.1) + SYMBOL_SVG_CIRCLE;
}
else {
symbolPath = symbolFunc(SYMBOL_SIZE);
}

symbolSdf = svgSdf(symbolPath, {
w: SYMBOL_SDF_SIZE,
h: SYMBOL_SDF_SIZE,
viewBox: [-SYMBOL_SIZE, -SYMBOL_SIZE, SYMBOL_SIZE, SYMBOL_SIZE],
stroke: symbolNoFill ? SYMBOL_STROKE : -SYMBOL_STROKE
});
SYMBOL_SDF[symbol] = symbolSdf;

return symbolSdf || null;
}

function convertLinePositions(gd, trace, positions) {
var count = positions.length / 2;
var linePositions;
var i;

if(subTypes.hasLines(trace) && count) {
if(trace.line.shape === 'hv') {
linePositions = [];
for(i = 0; i < count - 1; i++) {
if(isNaN(positions[i * 2]) || isNaN(positions[i * 2 + 1])) {
linePositions.push(NaN);
linePositions.push(NaN);
linePositions.push(NaN);
linePositions.push(NaN);
}
else {
linePositions.push(positions[i * 2]);
linePositions.push(positions[i * 2 + 1]);
linePositions.push(positions[i * 2 + 2]);
linePositions.push(positions[i * 2 + 1]);
}
}
linePositions.push(positions[positions.length - 2]);
linePositions.push(positions[positions.length - 1]);
} else if(trace.line.shape === 'vh') {
linePositions = [];
for(i = 0; i < count - 1; i++) {
if(isNaN(positions[i * 2]) || isNaN(positions[i * 2 + 1])) {
linePositions.push(NaN);
linePositions.push(NaN);
linePositions.push(NaN);
linePositions.push(NaN);
}
else {
linePositions.push(positions[i * 2]);
linePositions.push(positions[i * 2 + 1]);
linePositions.push(positions[i * 2]);
linePositions.push(positions[i * 2 + 3]);
}
}
linePositions.push(positions[positions.length - 2]);
linePositions.push(positions[positions.length - 1]);
} else {
linePositions = positions;
}
}

// If we have data with gaps, we ought to use rect joins
// FIXME: get rid of this
var hasNaN = false;
for(i = 0; i < linePositions.length; i++) {
if(isNaN(linePositions[i])) {
hasNaN = true;
break;
}
}

var join = (hasNaN || linePositions.length > constants.TOO_MANY_POINTS) ? 'rect' :
subTypes.hasMarkers(trace) ? 'rect' : 'round';

// fill gaps
if(hasNaN && trace.connectgaps) {
var lastX = linePositions[0];
var lastY = linePositions[1];

for(i = 0; i < linePositions.length; i += 2) {
if(isNaN(linePositions[i]) || isNaN(linePositions[i + 1])) {
linePositions[i] = lastX;
linePositions[i + 1] = lastY;
}
else {
lastX = linePositions[i];
lastY = linePositions[i + 1];
}
}
}

return {
join: join,
positions: linePositions
};
}

function convertErrorBarPositions(gd, trace, positions) {
var calcFromTrace = Registry.getComponentMethod('errorbars', 'calcFromTrace');
var vals = calcFromTrace(trace, gd._fullLayout);
var count = positions.length / 2;
var out = {};

function put(axLetter) {
var errors = new Float64Array(4 * count);
var ax = AxisIDs.getFromId(gd, trace[axLetter + 'axis']);
var pOffset = {x: 0, y: 1}[axLetter];
var eOffset = {x: [0, 1, 2, 3], y: [2, 3, 0, 1]}[axLetter];

for(var i = 0, p = 0; i < count; i++, p += 4) {
errors[p + eOffset[0]] = positions[i * 2 + pOffset] - ax.d2l(vals[i][axLetter + 's']) || 0;
errors[p + eOffset[1]] = ax.d2l(vals[i][axLetter + 'h']) - positions[i * 2 + pOffset] || 0;
errors[p + eOffset[2]] = 0;
errors[p + eOffset[3]] = 0;
}

return errors;
}


if(trace.error_x && trace.error_x.visible) {
out.x = {
positions: positions,
errors: put('x')
};
}
if(trace.error_y && trace.error_y.visible) {
out.y = {
positions: positions,
errors: put('y')
};
}

return out;
}

module.exports = {
convertStyle: convertStyle,
convertLinePositions: convertLinePositions,
convertErrorBarPositions: convertErrorBarPositions
};
443 changes: 55 additions & 388 deletions src/traces/scattergl/index.js

Large diffs are not rendered by default.

41 changes: 39 additions & 2 deletions test/jasmine/tests/gl2d_plot_interact_test.js
Original file line number Diff line number Diff line change
@@ -809,11 +809,48 @@ describe('@gl Test gl2d plots', function() {
var scene = gd._fullLayout._plots.xy._scene;

expect(scene.count).toBe(2);
expect(scene.selectBatch).toBeDefined();
expect(scene.unselectBatch).toBeDefined();
expect(scene.selectBatch).toEqual([[0]]);
expect(scene.unselectBatch).toEqual([[]]);
expect(scene.markerOptions.length).toBe(2);
expect(scene.markerOptions[1].color).toEqual(new Uint8Array([255, 0, 0, 255]));
expect(scene.scatter2d.draw).toHaveBeenCalled();

return Plotly.restyle(gd, 'selectedpoints', null);
})
.then(function() {
var scene = gd._fullLayout._plots.xy._scene;
var msg = 'clearing under dragmode select';

expect(scene.selectBatch).toEqual([], msg);
expect(scene.unselectBatch).toEqual([], msg);

// scattergl uses different pathways for select/lasso & zoom/pan
return Plotly.relayout(gd, 'dragmode', 'pan');
})
.then(function() {
var scene = gd._fullLayout._plots.xy._scene;
var msg = 'cleared under dragmode pan';

expect(scene.selectBatch).toEqual([], msg);
expect(scene.unselectBatch).toEqual([], msg);

return Plotly.restyle(gd, 'selectedpoints', [[1, 2], [0]]);
})
.then(function() {
var scene = gd._fullLayout._plots.xy._scene;
var msg = 'selecting via API under dragmode pan';

expect(scene.selectBatch).toEqual([[1, 2], [0]], msg);
expect(scene.unselectBatch).toEqual([[0], []], msg);

return Plotly.restyle(gd, 'selectedpoints', null);
})
.then(function() {
var scene = gd._fullLayout._plots.xy._scene;
var msg = 'clearing under dragmode pan';

expect(scene.selectBatch).toBe(null, msg);
expect(scene.unselectBatch).toBe(null, msg);
})
.catch(fail)
.then(done);