From 3dd18ea6e18f8887403ddda4de3cd5ee3ae3a772 Mon Sep 17 00:00:00 2001 From: Robert Monfera Date: Thu, 1 Sep 2016 15:06:40 +0200 Subject: [PATCH 01/11] Pointcloud - plotly,js bindings (squashed) --- lib/index-gl2d.js | 1 + lib/index.js | 1 + lib/pointcloud.js | 9 ++ package.json | 1 + src/.eslintrc | 1 + src/lib/float32_truncate.js | 21 +++ src/traces/pointcloud/attributes.js | 124 ++++++++++++++++ src/traces/pointcloud/convert.js | 215 ++++++++++++++++++++++++++++ src/traces/pointcloud/defaults.js | 41 ++++++ src/traces/pointcloud/index.js | 31 ++++ src/traces/scattergl/convert.js | 15 +- 11 files changed, 446 insertions(+), 14 deletions(-) create mode 100644 lib/pointcloud.js create mode 100644 src/lib/float32_truncate.js create mode 100644 src/traces/pointcloud/attributes.js create mode 100644 src/traces/pointcloud/convert.js create mode 100644 src/traces/pointcloud/defaults.js create mode 100644 src/traces/pointcloud/index.js diff --git a/lib/index-gl2d.js b/lib/index-gl2d.js index b5e0df006a4..665dc2db603 100644 --- a/lib/index-gl2d.js +++ b/lib/index-gl2d.js @@ -12,6 +12,7 @@ var Plotly = require('./core'); Plotly.register([ require('./scattergl'), + require('./pointcloud'), require('./heatmapgl'), require('./contourgl') ]); diff --git a/lib/index.js b/lib/index.js index 4f37449c9c3..1f91cda0c33 100644 --- a/lib/index.js +++ b/lib/index.js @@ -25,6 +25,7 @@ Plotly.register([ require('./scattergeo'), require('./choropleth'), require('./scattergl'), + require('./pointcloud'), require('./scatterternary'), require('./scattermapbox') ]); diff --git a/lib/pointcloud.js b/lib/pointcloud.js new file mode 100644 index 00000000000..f8ce0a7d5b2 --- /dev/null +++ b/lib/pointcloud.js @@ -0,0 +1,9 @@ +/** +* Copyright 2012-2016, 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. +*/ + +module.exports = require('../src/traces/pointcloud'); diff --git a/package.json b/package.json index 3baefba9324..2ee57d983aa 100644 --- a/package.json +++ b/package.json @@ -68,6 +68,7 @@ "gl-plot3d": "^1.5.0", "gl-scatter2d": "^1.0.5", "gl-scatter2d-fancy": "^1.1.1", + "gl-pointcloud2d": "gl-vis/gl-pointcloud2d", "gl-scatter3d": "^1.0.4", "gl-select-box": "^1.0.1", "gl-spikes2d": "^1.0.1", diff --git a/src/.eslintrc b/src/.eslintrc index 428c1ee3b53..c74775cdfea 100644 --- a/src/.eslintrc +++ b/src/.eslintrc @@ -7,6 +7,7 @@ "Promise": true, "Float32Array": true, "Uint8Array": true, + "Int32Array": true, "ArrayBuffer": true }, "rules": { diff --git a/src/lib/float32_truncate.js b/src/lib/float32_truncate.js new file mode 100644 index 00000000000..8d8f758f337 --- /dev/null +++ b/src/lib/float32_truncate.js @@ -0,0 +1,21 @@ +/** +* Copyright 2012-2016, 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'; + +/** + * Truncate a Float32Array to some length. A wrapper to support environments + * (e.g. node-webkit) that do not implement Float32Array.prototype.slice + */ +module.exports = function truncate(float32ArrayIn, len) { + // for some reason, ES2015 Float32Array.prototype.slice takes 2x as long... + // therefore we aren't checking for its existence + var float32ArrayOut = new Float32Array(len); + for(var i = 0; i < len; i++) float32ArrayOut[i] = float32ArrayIn[i]; + return float32ArrayOut; +}; diff --git a/src/traces/pointcloud/attributes.js b/src/traces/pointcloud/attributes.js new file mode 100644 index 00000000000..35057aed1d7 --- /dev/null +++ b/src/traces/pointcloud/attributes.js @@ -0,0 +1,124 @@ +/** +* Copyright 2012-2016, 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 scatterglAttrs = require('../scattergl/attributes'); + +module.exports = { + x: scatterglAttrs.x, + y: scatterglAttrs.y, + xy: { + valType: 'data_array', + description: [ + 'Faster alternative to specifying `x` and `y` separately.', + 'If supplied, it must be a typed `Float32Array` array that', + 'represents points such that `xy[i * 2] = x[i]` and `xy[i * 2 + 1] = y[i]`' + ].join(' ') + }, + indexid: { + valType: 'data_array', + description: [ + 'A sequential value, 0..n, supply it to avoid creating this array inside plotting.', + 'Its length must be equal to or greater than the number of points.', + 'For the best performance and memory use, create one large `indexid` typed array', + 'that is guaranteed to be at least as long as the largest number of points during', + 'use, and reuse it on each `Plotly.restyle()` call.' + ].join(' ') + }, + bounds: { + valType: 'data_array', + description: [ + 'Specify `bounds` in the shape of `[xMin, yMin, xMax, yMax] to avoid looping through', + 'the `xy` typed array.' + ].join(' ') + }, + text: scatterglAttrs.text, + marker: { + color: { + valType: 'color', + arrayOk: false, + role: 'style', + description: [ + 'Sets the marker fill color. It accepts a specific color.', + 'If the color is not fully opaque and there are hundreds of thousands', + 'of points, it may cause slower zooming and panning.' + ].join('') + }, + opacity: { + valType: 'number', + min: 0, + max: 1, + dflt: 1, + arrayOk: false, + role: 'style', + description: [ + 'Sets the marker opacity. The default value is `1` (fully opaque).', + 'If the markers are not fully opaque and there are hundreds of thousands', + 'of points, it may cause slower zooming and panning.', + 'Opacity fades the color even if `blend` is left on `false` even if there', + 'is no translucency effect in that case.' + ].join(' ') + }, + blend: { + valType: 'boolean', + dflt: false, + role: 'style', + description: [ + 'Determines if colors are blended together for a translucency effect', + 'in case `opacity` is specified as a value less then `1`.', + 'Setting `blend` to `true` reduces zoom/pan', + 'speed if used with large numbers of points.' + ].join(' ') + }, + sizemin: { + valType: 'number', + min: 0.1, + max: 2, + dflt: 0.5, + role: 'style', + description: [ + 'Sets the minimum size (in px) of the rendered marker points, effective when', + 'the `pointcloud` shows a million or more points.' + ].join(' ') + }, + sizemax: { + valType: 'number', + min: 0.1, + dflt: 20, + role: 'style', + description: [ + 'Sets the maximum size (in px) of the rendered marker points.', + 'Effective when the `pointcloud` shows only few points.' + ].join(' ') + }, + border: { + color: { + valType: 'color', + arrayOk: false, + role: 'style', + description: [ + 'Sets the stroke color. It accepts a specific color.', + 'If the color is not fully opaque and there are hundreds of thousands', + 'of points, it may cause slower zooming and panning.' + ].join(' ') + }, + arearatio: { + valType: 'number', + min: 0, + max: 1, + dflt: 0, + role: 'style', + description: [ + 'Specifies what fraction of the marker area is covered with the', + 'border.' + ].join(' ') + } + } + } +}; diff --git a/src/traces/pointcloud/convert.js b/src/traces/pointcloud/convert.js new file mode 100644 index 00000000000..8e09b188b3c --- /dev/null +++ b/src/traces/pointcloud/convert.js @@ -0,0 +1,215 @@ +/** +* Copyright 2012-2016, 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 createpointcloud = require('gl-pointcloud2d'); + +var str2RGBArray = require('../../lib/str2rgbarray'); +var getTraceColor = require('../scatter/get_trace_color'); + +var AXES = ['xaxis', 'yaxis']; + +function Pointcloud(scene, uid) { + this.scene = scene; + this.uid = uid; + + this.pickXData = []; + this.pickYData = []; + this.xData = []; + this.yData = []; + this.textLabels = []; + this.color = 'rgb(0, 0, 0)'; + this.name = ''; + this.hoverinfo = 'all'; + + this.idToIndex = new Int32Array(0); + this.bounds = [0, 0, 0, 0]; + + this.pointcloudOptions = { + positions: new Float32Array(0), + idToIndex: this.idToIndex, + sizemin: 0.5, + sizemax: 12, + color: [0, 0, 0, 1], + areaRatio: 1, + borderColor: [0, 0, 0, 1] + }; + this.pointcloud = createpointcloud(scene.glplot, this.pointcloudOptions); + this.pointcloud._trace = this; // scene2d requires this prop +} + +var proto = Pointcloud.prototype; + +proto.handlePick = function(pickResult) { + + var index = this.idToIndex[pickResult.pointId]; + + return { + trace: this, + dataCoord: pickResult.dataCoord, + traceCoord: [ + this.pickXData[index], + this.pickYData[index] + ], + textLabel: Array.isArray(this.textLabels) ? + this.textLabels[index] : + this.textLabels, + color: this.color, + name: this.name, + hoverinfo: this.hoverinfo + }; +}; + +proto.update = function(options) { + + this.textLabels = options.text; + this.name = options.name; + this.hoverinfo = options.hoverinfo; + this.bounds = [Infinity, Infinity, -Infinity, -Infinity]; + + this.updateFast(options); + + this.color = getTraceColor(options, {}); +}; + +proto.updateFast = function(options) { + var x = this.xData = this.pickXData = options.x; + var y = this.yData = this.pickYData = options.y; + + var xy = options.xy; + var userBounds = options.bounds; + var index = options.indexid; + + var len = x.length, + idToIndex, + positions, + bounds = this.bounds; + + var xx, yy, i; + + if(xy) { + + positions = xy; + + if(userBounds) { + + bounds[0] = userBounds[0]; + bounds[1] = userBounds[1]; + bounds[2] = userBounds[2]; + bounds[3] = userBounds[3]; + + } else { + + for(i = 0; i < len; i++) { + + xx = positions[i * 2]; + yy = positions[i * 2 + 1]; + + if(xx < bounds[0]) bounds[0] = xx; + if(xx > bounds[2]) bounds[2] = xx; + if(yy < bounds[1]) bounds[1] = yy; + if(yy > bounds[3]) bounds[3] = yy; + } + + } + + if(index) { + + idToIndex = index; + + } else { + + idToIndex = new Int32Array(len); + + for(i = 0; i < len; i++) { + + idToIndex[i] = i; + + } + + } + + } else { + + positions = new Float32Array(2 * len); + idToIndex = new Int32Array(len); + + for(i = 0; i < len; i++) { + xx = x[i]; + yy = y[i]; + + idToIndex[i] = i; + + positions[i * 2] = xx; + positions[i * 2 + 1] = yy; + + if(xx < bounds[0]) bounds[0] = xx; + if(xx > bounds[2]) bounds[2] = xx; + if(yy < bounds[1]) bounds[1] = yy; + if(yy > bounds[3]) bounds[3] = yy; + } + + } + + this.idToIndex = idToIndex; + this.pointcloudOptions.idToIndex = idToIndex; + + this.pointcloudOptions.positions = positions; + + var markerColor = str2RGBArray(options.marker.color), + borderColor = str2RGBArray(options.marker.border.color), + opacity = options.opacity * options.marker.opacity; + + markerColor[3] *= opacity; + this.pointcloudOptions.color = markerColor; + this.pointcloudOptions.blend = options.marker.blend; + + borderColor[3] *= opacity; + this.pointcloudOptions.borderColor = borderColor; + + var markerSizeMin = options.marker.sizemin; + var markerSizeMax = Math.max(options.marker.sizemax, options.marker.sizemin); + this.pointcloudOptions.sizeMin = markerSizeMin; + this.pointcloudOptions.sizeMax = markerSizeMax; + this.pointcloudOptions.areaRatio = options.marker.border.arearatio; + + this.pointcloud.update(this.pointcloudOptions); + + // add item for autorange routine + this.expandAxesFast(bounds, markerSizeMax / 2); // avoid axis reexpand just because of the adaptive point size +}; + +proto.expandAxesFast = function(bounds, markerSizeMin) { + var pad = markerSizeMin || 0.5; + var ax, min, max; + + for(var i = 0; i < 2; i++) { + ax = this.scene[AXES[i]]; + + min = ax._min; + if(!min) min = []; + min.push({ val: bounds[i], pad: pad }); + + max = ax._max; + if(!max) max = []; + max.push({ val: bounds[i + 2], pad: pad }); + } +}; + +proto.dispose = function() { + this.pointcloud.dispose(); +}; + +function createPointcloud(scene, data) { + var plot = new Pointcloud(scene, data.uid); + plot.update(data); + return plot; +} + +module.exports = createPointcloud; diff --git a/src/traces/pointcloud/defaults.js b/src/traces/pointcloud/defaults.js new file mode 100644 index 00000000000..9be8bd98765 --- /dev/null +++ b/src/traces/pointcloud/defaults.js @@ -0,0 +1,41 @@ +/** +* Copyright 2012-2016, 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 Lib = require('../../lib'); + +var attributes = require('./attributes'); + +module.exports = function supplyDefaults(traceIn, traceOut, defaultColor) { + function coerce(attr, dflt) { + return Lib.coerce(traceIn, traceOut, attributes, attr, dflt); + } + + coerce('x'); + coerce('y'); + coerce('bounds'); + + if(traceIn.xy && traceIn.xy instanceof Float32Array) { + traceOut.xy = traceIn.xy; + } + + if(traceIn.indexid && traceIn.indexid instanceof Int32Array) { + traceOut.indexid = traceIn.indexid; + } + + coerce('text'); + coerce('marker.color', defaultColor); + coerce('marker.opacity'); + coerce('marker.blend'); + coerce('marker.sizemin'); + coerce('marker.sizemax'); + coerce('marker.border.color', defaultColor); + coerce('marker.border.arearatio'); +}; diff --git a/src/traces/pointcloud/index.js b/src/traces/pointcloud/index.js new file mode 100644 index 00000000000..c26a108e949 --- /dev/null +++ b/src/traces/pointcloud/index.js @@ -0,0 +1,31 @@ +/** +* Copyright 2012-2016, 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 pointcloud = {}; + +pointcloud.attributes = require('./attributes'); +pointcloud.supplyDefaults = require('./defaults'); + +// reuse the Scatter3D 'dummy' calc step so that legends know what to do +pointcloud.calc = require('../scatter3d/calc'); +pointcloud.plot = require('./convert'); + +pointcloud.moduleType = 'trace'; +pointcloud.name = 'pointcloud'; +pointcloud.basePlotModule = require('../../plots/gl2d'); +pointcloud.categories = ['gl2d', 'showLegend']; +pointcloud.meta = { + description: [ + 'The data visualized as a point cloud set in `x` and `y`', + 'using the WebGl plotting engine.' + ].join(' ') +}; + +module.exports = pointcloud; diff --git a/src/traces/scattergl/convert.js b/src/traces/scattergl/convert.js index 5dbc421870e..95053e5ac5b 100644 --- a/src/traces/scattergl/convert.js +++ b/src/traces/scattergl/convert.js @@ -19,6 +19,7 @@ var Lib = require('../../lib'); var Axes = require('../../plots/cartesian/axes'); var ErrorBars = require('../../components/errorbars'); var str2RGBArray = require('../../lib/str2rgbarray'); +var truncate = require('../../lib/float32_truncate'); var formatColor = require('../../lib/gl_format_color'); var subTypes = require('../scatter/subtypes'); var makeBubbleSizeFn = require('../scatter/make_bubble_size_func'); @@ -219,20 +220,6 @@ function _convertColor(colors, opacities, count) { return result; } -/** - * Truncate a Float32Array to some length. A wrapper to support environments - * (e.g. node-webkit) that do not implement Float32Array.prototype.slice - */ -function truncate(float32ArrayIn, len) { - if(Float32Array.slice === undefined) { - var float32ArrayOut = new Float32Array(len); - for(var i = 0; i < len; i++) float32ArrayOut[i] = float32ArrayIn[i]; - return float32ArrayOut; - } - - return float32ArrayIn.slice(0, len); -} - /* Order is important here to get the correct laying: * - lines * - errorX From 7b6edbc1921928b82b1bbec1de8e484ef37c5860 Mon Sep 17 00:00:00 2001 From: Robert Monfera Date: Thu, 1 Sep 2016 15:07:16 +0200 Subject: [PATCH 02/11] Pointcloud - image test --- test/image/mocks/gl2d_pointcloud-basic.json | 72 +++++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 test/image/mocks/gl2d_pointcloud-basic.json diff --git a/test/image/mocks/gl2d_pointcloud-basic.json b/test/image/mocks/gl2d_pointcloud-basic.json new file mode 100644 index 00000000000..9160294e4c6 --- /dev/null +++ b/test/image/mocks/gl2d_pointcloud-basic.json @@ -0,0 +1,72 @@ +{ + "data": [ + { + "type": "pointcloud", + "mode": "markers", + "marker": { + "sizemin": 0.5, + "sizemax": 100, + "arearatio": 0, + "color": "rgba(255, 0, 0, 0.6)" + }, + "x": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + "y": [9, 8, 7, 6, 5, 4, 3, 2, 1, 0] + }, + { + "type": "pointcloud", + "mode": "markers", + "marker": { + "sizemin": 0.5, + "sizemax": 100, + "arearatio": 0, + "color": "rgba(0, 0, 255, 0.9)", + "opacity": 0.8, + "blend": true + }, + "opacity": 0.7, + "x": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + "y": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + }, + { + "type": "pointcloud", + "mode": "markers", + "marker": { + "sizemin": 0.5, + "sizemax": 100, + "border": { + "color": "rgb(0, 0, 0)", + "arearatio": 0.7071 + }, + "color": "green", + "opacity": 0.8, + "blend": true + }, + "opacity": 0.7, + "x": [3, 4.5, 6], + "y": [9, 9, 9] + } + ], + "layout": { + "title": "Point Cloud - basic", + "xaxis": { + "type": "linear", + "range": [ + -2.501411175139456, + 43.340777299865266 + ], + "autorange": true + }, + "yaxis": { + "type": "linear", + "range": [ + 4, + 6 + ], + "autorange": true + }, + "height": 598, + "width": 1080, + "autosize": true, + "showlegend": false + } +} From 95925a36bf7e630791eacc658317141ddb821719 Mon Sep 17 00:00:00 2001 From: Robert Monfera Date: Fri, 2 Sep 2016 11:56:16 +0200 Subject: [PATCH 03/11] PR feedback - rename indexid to indices --- src/traces/pointcloud/attributes.js | 4 ++-- src/traces/pointcloud/convert.js | 2 +- src/traces/pointcloud/defaults.js | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/traces/pointcloud/attributes.js b/src/traces/pointcloud/attributes.js index 35057aed1d7..d549742dd20 100644 --- a/src/traces/pointcloud/attributes.js +++ b/src/traces/pointcloud/attributes.js @@ -21,12 +21,12 @@ module.exports = { 'represents points such that `xy[i * 2] = x[i]` and `xy[i * 2 + 1] = y[i]`' ].join(' ') }, - indexid: { + indices: { valType: 'data_array', description: [ 'A sequential value, 0..n, supply it to avoid creating this array inside plotting.', 'Its length must be equal to or greater than the number of points.', - 'For the best performance and memory use, create one large `indexid` typed array', + 'For the best performance and memory use, create one large `indices` typed array', 'that is guaranteed to be at least as long as the largest number of points during', 'use, and reuse it on each `Plotly.restyle()` call.' ].join(' ') diff --git a/src/traces/pointcloud/convert.js b/src/traces/pointcloud/convert.js index 8e09b188b3c..413eea0aa18 100644 --- a/src/traces/pointcloud/convert.js +++ b/src/traces/pointcloud/convert.js @@ -84,7 +84,7 @@ proto.updateFast = function(options) { var xy = options.xy; var userBounds = options.bounds; - var index = options.indexid; + var index = options.indices; var len = x.length, idToIndex, diff --git a/src/traces/pointcloud/defaults.js b/src/traces/pointcloud/defaults.js index 9be8bd98765..452a3be3814 100644 --- a/src/traces/pointcloud/defaults.js +++ b/src/traces/pointcloud/defaults.js @@ -26,8 +26,8 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor) { traceOut.xy = traceIn.xy; } - if(traceIn.indexid && traceIn.indexid instanceof Int32Array) { - traceOut.indexid = traceIn.indexid; + if(traceIn.indices && traceIn.indices instanceof Int32Array) { + traceOut.indices = traceIn.indices; } coerce('text'); From 5f4847d878d50428e6cdcbca5b39901d670d34c0 Mon Sep 17 00:00:00 2001 From: Robert Monfera Date: Fri, 2 Sep 2016 11:58:41 +0200 Subject: [PATCH 04/11] PR feedback - camelcasing --- src/traces/pointcloud/convert.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/traces/pointcloud/convert.js b/src/traces/pointcloud/convert.js index 413eea0aa18..fe4d3ab6a25 100644 --- a/src/traces/pointcloud/convert.js +++ b/src/traces/pointcloud/convert.js @@ -8,7 +8,7 @@ 'use strict'; -var createpointcloud = require('gl-pointcloud2d'); +var createPointCloudRenderer = require('gl-pointcloud2d'); var str2RGBArray = require('../../lib/str2rgbarray'); var getTraceColor = require('../scatter/get_trace_color'); @@ -40,7 +40,7 @@ function Pointcloud(scene, uid) { areaRatio: 1, borderColor: [0, 0, 0, 1] }; - this.pointcloud = createpointcloud(scene.glplot, this.pointcloudOptions); + this.pointcloud = createPointCloudRenderer(scene.glplot, this.pointcloudOptions); this.pointcloud._trace = this; // scene2d requires this prop } From bfd0af8f4d3a72c2d5303240202d66ba4acf1044 Mon Sep 17 00:00:00 2001 From: Robert Monfera Date: Fri, 2 Sep 2016 13:16:56 +0200 Subject: [PATCH 05/11] PR feedback- split bounds to xbounds / ybounds --- src/traces/pointcloud/attributes.js | 14 +++++++++++--- src/traces/pointcloud/defaults.js | 4 +++- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/src/traces/pointcloud/attributes.js b/src/traces/pointcloud/attributes.js index d549742dd20..9571c3dff53 100644 --- a/src/traces/pointcloud/attributes.js +++ b/src/traces/pointcloud/attributes.js @@ -25,17 +25,25 @@ module.exports = { valType: 'data_array', description: [ 'A sequential value, 0..n, supply it to avoid creating this array inside plotting.', + 'If specified, it must be a typed `Int32Array` array.', 'Its length must be equal to or greater than the number of points.', 'For the best performance and memory use, create one large `indices` typed array', 'that is guaranteed to be at least as long as the largest number of points during', 'use, and reuse it on each `Plotly.restyle()` call.' ].join(' ') }, - bounds: { + xbounds: { valType: 'data_array', description: [ - 'Specify `bounds` in the shape of `[xMin, yMin, xMax, yMax] to avoid looping through', - 'the `xy` typed array.' + 'Specify `xbounds` in the shape of `[xMin, xMax] to avoid looping through', + 'the `xy` typed array. Use it in conjunction with `xy` and `ybounds` for the performance benefits.' + ].join(' ') + }, + ybounds: { + valType: 'data_array', + description: [ + 'Specify `ybounds` in the shape of `[yMin, yMax] to avoid looping through', + 'the `xy` typed array. Use it in conjunction with `xy` and `xbounds` for the performance benefits.' ].join(' ') }, text: scatterglAttrs.text, diff --git a/src/traces/pointcloud/defaults.js b/src/traces/pointcloud/defaults.js index 452a3be3814..f2da0311560 100644 --- a/src/traces/pointcloud/defaults.js +++ b/src/traces/pointcloud/defaults.js @@ -20,7 +20,9 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor) { coerce('x'); coerce('y'); - coerce('bounds'); + + coerce('xbounds'); + coerce('ybounds'); if(traceIn.xy && traceIn.xy instanceof Float32Array) { traceOut.xy = traceIn.xy; From 6871cb3d5b4d8bfc5a5c37d6e7c6320c744f890e Mon Sep 17 00:00:00 2001 From: Robert Monfera Date: Fri, 2 Sep 2016 15:31:51 +0200 Subject: [PATCH 06/11] Add support for permutations of direct array support --- src/traces/pointcloud/convert.js | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/traces/pointcloud/convert.js b/src/traces/pointcloud/convert.js index fe4d3ab6a25..8f53953cffd 100644 --- a/src/traces/pointcloud/convert.js +++ b/src/traces/pointcloud/convert.js @@ -83,10 +83,10 @@ proto.updateFast = function(options) { var y = this.yData = this.pickYData = options.y; var xy = options.xy; - var userBounds = options.bounds; + var userBounds = options.xbounds && options.ybounds var index = options.indices; - var len = x.length, + var len, idToIndex, positions, bounds = this.bounds; @@ -96,13 +96,14 @@ proto.updateFast = function(options) { if(xy) { positions = xy; + len = xy.length >>> 1; if(userBounds) { - bounds[0] = userBounds[0]; - bounds[1] = userBounds[1]; - bounds[2] = userBounds[2]; - bounds[3] = userBounds[3]; + bounds[0] = options.xbounds[0]; + bounds[2] = options.xbounds[1]; + bounds[1] = options.ybounds[0]; + bounds[3] = options.ybounds[1]; } else { @@ -137,6 +138,8 @@ proto.updateFast = function(options) { } else { + len = x.length; + positions = new Float32Array(2 * len); idToIndex = new Int32Array(len); From 16f7fd5bbb9d18f927b73a5a4e432cf1e91028da Mon Sep 17 00:00:00 2001 From: Robert Monfera Date: Fri, 2 Sep 2016 15:32:13 +0200 Subject: [PATCH 07/11] Add support for permutations of direct array support - test cases --- src/traces/pointcloud/convert.js | 16 +++--- test/image/mocks/gl2d_pointcloud-basic.json | 60 ++++++++++++++++++++- 2 files changed, 67 insertions(+), 9 deletions(-) diff --git a/src/traces/pointcloud/convert.js b/src/traces/pointcloud/convert.js index 8f53953cffd..0002d3ee696 100644 --- a/src/traces/pointcloud/convert.js +++ b/src/traces/pointcloud/convert.js @@ -50,13 +50,13 @@ proto.handlePick = function(pickResult) { var index = this.idToIndex[pickResult.pointId]; + // prefer the readout from XY, if present return { trace: this, dataCoord: pickResult.dataCoord, - traceCoord: [ - this.pickXData[index], - this.pickYData[index] - ], + traceCoord: this.pickXYData ? + [this.pickXYData[index * 2], this.pickXYData[index * 2 + 1]] : + [this.pickXData[index], this.pickYData[index]], textLabel: Array.isArray(this.textLabels) ? this.textLabels[index] : this.textLabels, @@ -81,9 +81,9 @@ proto.update = function(options) { proto.updateFast = function(options) { var x = this.xData = this.pickXData = options.x; var y = this.yData = this.pickYData = options.y; + var xy = this.pickXYData = options.xy; - var xy = options.xy; - var userBounds = options.xbounds && options.ybounds + var userBounds = options.xbounds && options.ybounds; var index = options.indices; var len, @@ -188,8 +188,8 @@ proto.updateFast = function(options) { this.expandAxesFast(bounds, markerSizeMax / 2); // avoid axis reexpand just because of the adaptive point size }; -proto.expandAxesFast = function(bounds, markerSizeMin) { - var pad = markerSizeMin || 0.5; +proto.expandAxesFast = function(bounds, markerSize) { + var pad = markerSize || 0.5; var ax, min, max; for(var i = 0; i < 2; i++) { diff --git a/test/image/mocks/gl2d_pointcloud-basic.json b/test/image/mocks/gl2d_pointcloud-basic.json index 9160294e4c6..b6fb43a851b 100644 --- a/test/image/mocks/gl2d_pointcloud-basic.json +++ b/test/image/mocks/gl2d_pointcloud-basic.json @@ -44,6 +44,64 @@ "opacity": 0.7, "x": [3, 4.5, 6], "y": [9, 9, 9] + }, + { + "type": "pointcloud", + "mode": "markers", + "marker": { + "sizemin": 0.5, + "sizemax": 100, + "color": "yellow", + "opacity": 0.8, + "blend": true + }, + "opacity": 0.7, + "xy": new Float32Array([1, 3, 9, 3]), + "indices": new Int32Array([0, 1]), + "xbounds": [1, 9], + "ybounds": [3, 3] + }, + { + "type": "pointcloud", + "mode": "markers", + "marker": { + "sizemin": 0.5, + "sizemax": 100, + "color": "orange", + "opacity": 0.8, + "blend": true + }, + "opacity": 0.7, + "xy": new Float32Array([1, 4, 9, 4]), + "indices": new Int32Array([0, 1]) + }, + { + "type": "pointcloud", + "mode": "markers", + "marker": { + "sizemin": 0.5, + "sizemax": 100, + "color": "darkorange", + "opacity": 0.8, + "blend": true + }, + "opacity": 0.7, + "xy": new Float32Array([1, 5, 9, 5]), + "xbounds": [1, 9], + "ybounds": [5, 5] + }, + { + "type": "pointcloud", + "mode": "markers", + "marker": { + "sizemin": 0.5, + "sizemax": 100, + "color": "red", + "opacity": 0.8, + "blend": true + }, + "opacity": 0.7, + "xy": new Float32Array([1, 6, 9, 6]) } ], "layout": { @@ -69,4 +127,4 @@ "autosize": true, "showlegend": false } -} +} \ No newline at end of file From a69af310fc3562a8a7dcf5025c070edf59b53f41 Mon Sep 17 00:00:00 2001 From: Robert Monfera Date: Wed, 7 Sep 2016 19:37:54 +0200 Subject: [PATCH 08/11] Use the npm publish - ed version of gl-pointcloud2d --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2ee57d983aa..ce9996e272e 100644 --- a/package.json +++ b/package.json @@ -68,7 +68,7 @@ "gl-plot3d": "^1.5.0", "gl-scatter2d": "^1.0.5", "gl-scatter2d-fancy": "^1.1.1", - "gl-pointcloud2d": "gl-vis/gl-pointcloud2d", + "gl-pointcloud2d": "^1.0.0", "gl-scatter3d": "^1.0.4", "gl-select-box": "^1.0.1", "gl-spikes2d": "^1.0.1", From 5d37b54369ae7851abe93cd62830f09b54464806 Mon Sep 17 00:00:00 2001 From: Robert Monfera Date: Wed, 7 Sep 2016 20:18:00 +0200 Subject: [PATCH 09/11] removing the typed array based test cases as they aren't JSON --- test/image/mocks/gl2d_pointcloud-basic.json | 58 --------------------- 1 file changed, 58 deletions(-) diff --git a/test/image/mocks/gl2d_pointcloud-basic.json b/test/image/mocks/gl2d_pointcloud-basic.json index b6fb43a851b..a8a0ed7445c 100644 --- a/test/image/mocks/gl2d_pointcloud-basic.json +++ b/test/image/mocks/gl2d_pointcloud-basic.json @@ -44,64 +44,6 @@ "opacity": 0.7, "x": [3, 4.5, 6], "y": [9, 9, 9] - }, - { - "type": "pointcloud", - "mode": "markers", - "marker": { - "sizemin": 0.5, - "sizemax": 100, - "color": "yellow", - "opacity": 0.8, - "blend": true - }, - "opacity": 0.7, - "xy": new Float32Array([1, 3, 9, 3]), - "indices": new Int32Array([0, 1]), - "xbounds": [1, 9], - "ybounds": [3, 3] - }, - { - "type": "pointcloud", - "mode": "markers", - "marker": { - "sizemin": 0.5, - "sizemax": 100, - "color": "orange", - "opacity": 0.8, - "blend": true - }, - "opacity": 0.7, - "xy": new Float32Array([1, 4, 9, 4]), - "indices": new Int32Array([0, 1]) - }, - { - "type": "pointcloud", - "mode": "markers", - "marker": { - "sizemin": 0.5, - "sizemax": 100, - "color": "darkorange", - "opacity": 0.8, - "blend": true - }, - "opacity": 0.7, - "xy": new Float32Array([1, 5, 9, 5]), - "xbounds": [1, 9], - "ybounds": [5, 5] - }, - { - "type": "pointcloud", - "mode": "markers", - "marker": { - "sizemin": 0.5, - "sizemax": 100, - "color": "red", - "opacity": 0.8, - "blend": true - }, - "opacity": 0.7, - "xy": new Float32Array([1, 6, 9, 6]) } ], "layout": { From 8a09ccd40dd6a11728d9eed6a2bcc6ead6fcbbc5 Mon Sep 17 00:00:00 2001 From: Robert Monfera Date: Wed, 7 Sep 2016 20:22:17 +0200 Subject: [PATCH 10/11] Added comment --- src/traces/pointcloud/convert.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/traces/pointcloud/convert.js b/src/traces/pointcloud/convert.js index 0002d3ee696..78e7617264b 100644 --- a/src/traces/pointcloud/convert.js +++ b/src/traces/pointcloud/convert.js @@ -96,6 +96,8 @@ proto.updateFast = function(options) { if(xy) { positions = xy; + + // dividing xy.length by 2 and truncating to integer if xy.length was not even len = xy.length >>> 1; if(userBounds) { From 8105955aac5eb27502544968d89b149e3438aa16 Mon Sep 17 00:00:00 2001 From: Robert Monfera Date: Wed, 7 Sep 2016 20:57:15 +0200 Subject: [PATCH 11/11] jasmine test case for pointcloud --- src/traces/pointcloud/convert.js | 1 + test/jasmine/karma.ciconf.js | 3 +- test/jasmine/tests/gl2d_pointcloud_test.js | 216 +++++++++++++++++++++ 3 files changed, 219 insertions(+), 1 deletion(-) create mode 100644 test/jasmine/tests/gl2d_pointcloud_test.js diff --git a/src/traces/pointcloud/convert.js b/src/traces/pointcloud/convert.js index 78e7617264b..2c7374c8a14 100644 --- a/src/traces/pointcloud/convert.js +++ b/src/traces/pointcloud/convert.js @@ -18,6 +18,7 @@ var AXES = ['xaxis', 'yaxis']; function Pointcloud(scene, uid) { this.scene = scene; this.uid = uid; + this.type = 'pointcloud'; this.pickXData = []; this.pickYData = []; diff --git a/test/jasmine/karma.ciconf.js b/test/jasmine/karma.ciconf.js index d05afab996a..6ba65949842 100644 --- a/test/jasmine/karma.ciconf.js +++ b/test/jasmine/karma.ciconf.js @@ -22,7 +22,8 @@ function func(config) { func.defaultConfig.exclude = [ 'tests/gl_plot_interact_test.js', 'tests/gl_plot_interact_basic_test.js', - 'tests/gl2d_scatterplot_contour_test.js' + 'tests/gl2d_scatterplot_contour_test.js', + 'tests/gl2d_pointcloud_test.js' ]; // if true, Karma captures browsers, runs the tests and exits diff --git a/test/jasmine/tests/gl2d_pointcloud_test.js b/test/jasmine/tests/gl2d_pointcloud_test.js new file mode 100644 index 00000000000..57773c86477 --- /dev/null +++ b/test/jasmine/tests/gl2d_pointcloud_test.js @@ -0,0 +1,216 @@ +'use strict'; + +var Plotly = require('@lib/index'); + +// pointcloud is not part of the dist plotly.js bundle initially +Plotly.register([ + require('@lib/pointcloud') +]); + +// Test utilities +var createGraphDiv = require('../assets/create_graph_div'); +var destroyGraphDiv = require('../assets/destroy_graph_div'); +var failTest = require('../assets/fail_test'); + +var plotData = { + 'data': [ + { + 'type': 'pointcloud', + 'mode': 'markers', + 'marker': { + 'sizemin': 0.5, + 'sizemax': 100, + 'arearatio': 0, + 'color': 'rgba(255, 0, 0, 0.6)' + }, + 'x': [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + 'y': [9, 8, 7, 6, 5, 4, 3, 2, 1, 0] + }, + { + 'type': 'pointcloud', + 'mode': 'markers', + 'marker': { + 'sizemin': 0.5, + 'sizemax': 100, + 'arearatio': 0, + 'color': 'rgba(0, 0, 255, 0.9)', + 'opacity': 0.8, + 'blend': true + }, + 'opacity': 0.7, + 'x': [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + 'y': [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + }, + { + 'type': 'pointcloud', + 'mode': 'markers', + 'marker': { + 'sizemin': 0.5, + 'sizemax': 100, + 'border': { + 'color': 'rgb(0, 0, 0)', + 'arearatio': 0.7071 + }, + 'color': 'green', + 'opacity': 0.8, + 'blend': true + }, + 'opacity': 0.7, + 'x': [3, 4.5, 6], + 'y': [9, 9, 9] + }, + { + 'type': 'pointcloud', + 'mode': 'markers', + 'marker': { + 'sizemin': 0.5, + 'sizemax': 100, + 'color': 'yellow', + 'opacity': 0.8, + 'blend': true + }, + 'opacity': 0.7, + 'xy': new Float32Array([1, 3, 9, 3]), + 'indices': new Int32Array([0, 1]), + 'xbounds': [1, 9], + 'ybounds': [3, 3] + }, + { + 'type': 'pointcloud', + 'mode': 'markers', + 'marker': { + 'sizemin': 0.5, + 'sizemax': 100, + 'color': 'orange', + 'opacity': 0.8, + 'blend': true + }, + 'opacity': 0.7, + 'xy': new Float32Array([1, 4, 9, 4]), + 'indices': new Int32Array([0, 1]) + }, + { + 'type': 'pointcloud', + 'mode': 'markers', + 'marker': { + 'sizemin': 0.5, + 'sizemax': 100, + 'color': 'darkorange', + 'opacity': 0.8, + 'blend': true + }, + 'opacity': 0.7, + 'xy': new Float32Array([1, 5, 9, 5]), + 'xbounds': [1, 9], + 'ybounds': [5, 5] + }, + { + 'type': 'pointcloud', + 'mode': 'markers', + 'marker': { + 'sizemin': 0.5, + 'sizemax': 100, + 'color': 'red', + 'opacity': 0.8, + 'blend': true + }, + 'opacity': 0.7, + 'xy': new Float32Array([1, 6, 9, 6]) + } + ], + 'layout': { + 'title': 'Point Cloud - basic', + 'xaxis': { + 'type': 'linear', + 'range': [ + -2.501411175139456, + 43.340777299865266 + ], + 'autorange': true + }, + 'yaxis': { + 'type': 'linear', + 'range': [ + 4, + 6 + ], + 'autorange': true + }, + 'height': 598, + 'width': 1080, + 'autosize': true, + 'showlegend': false + } +}; + +function makePlot(gd, mock, done) { + return Plotly.plot(gd, mock.data, mock.layout) + .then(null, failTest) + .then(done); +} + +describe('contourgl plots', function() { + + var gd; + + beforeEach(function() { + gd = createGraphDiv(); + }); + + afterEach(function() { + Plotly.purge(gd); + destroyGraphDiv(); + }); + + it('render without raising an error', function(done) { + makePlot(gd, plotData, done); + }); + + it('should update properly', function(done) { + var mock = plotData; + var scene2d; + + var xBaselineMins = [{'val': 0, 'pad': 50}, {'val': 0, 'pad': 50}, {'val': 3, 'pad': 50}, {'val': 1, 'pad': 50}, {'val': 1, 'pad': 50}, {'val': 1, 'pad': 50}, {'val': 1, 'pad': 50}]; + var xBaselineMaxes = [{'val': 9, 'pad': 50}, {'val': 9, 'pad': 50}, {'val': 6, 'pad': 50}, {'val': 9, 'pad': 50}, {'val': 9, 'pad': 50}, {'val': 9, 'pad': 50}, {'val': 9, 'pad': 50}]; + + var yBaselineMins = [{'val': 0, 'pad': 50}, {'val': 0, 'pad': 50}, {'val': 9, 'pad': 50}, {'val': 3, 'pad': 50}, {'val': 4, 'pad': 50}, {'val': 5, 'pad': 50}, {'val': 6, 'pad': 50}]; + var yBaselineMaxes = [{'val': 9, 'pad': 50}, {'val': 9, 'pad': 50}, {'val': 9, 'pad': 50}, {'val': 3, 'pad': 50}, {'val': 4, 'pad': 50}, {'val': 5, 'pad': 50}, {'val': 6, 'pad': 50}]; + + Plotly.plot(gd, mock.data, mock.layout).then(function() { + scene2d = gd._fullLayout._plots.xy._scene2d; + + expect(scene2d.traces[mock.data[0].uid].type).toEqual('pointcloud'); + + expect(scene2d.xaxis._min).toEqual(xBaselineMins); + expect(scene2d.xaxis._max).toEqual(xBaselineMaxes); + + expect(scene2d.yaxis._min).toEqual(yBaselineMins); + expect(scene2d.yaxis._max).toEqual(yBaselineMaxes); + + return Plotly.relayout(gd, 'xaxis.range', [3, 6]); + }).then(function() { + + expect(scene2d.xaxis._min).toEqual(xBaselineMins); + expect(scene2d.xaxis._max).toEqual(xBaselineMaxes); + + return Plotly.relayout(gd, 'xaxis.autorange', true); + }).then(function() { + + expect(scene2d.xaxis._min).toEqual(xBaselineMins); + expect(scene2d.xaxis._max).toEqual(xBaselineMaxes); + + return Plotly.relayout(gd, 'yaxis.range', [8, 20]); + }).then(function() { + + expect(scene2d.yaxis._min).toEqual(yBaselineMins); + expect(scene2d.yaxis._max).toEqual(yBaselineMaxes); + + return Plotly.relayout(gd, 'yaxis.autorange', true); + }).then(function() { + expect(scene2d.yaxis._min).toEqual(yBaselineMins); + expect(scene2d.yaxis._max).toEqual(yBaselineMaxes); + + done(); + }); + }); +});