diff --git a/lib/index-gl2d.js b/lib/index-gl2d.js index ad78095ed80..5d8e572c0d1 100644 --- a/lib/index-gl2d.js +++ b/lib/index-gl2d.js @@ -13,6 +13,7 @@ var Plotly = require('./core'); Plotly.register([ require('./scattergl'), require('./splom'), + require('./pointcloud'), require('./heatmapgl'), require('./parcoords') ]); diff --git a/lib/index.js b/lib/index.js index 72930ceb362..30dbb1ac4bf 100644 --- a/lib/index.js +++ b/lib/index.js @@ -44,6 +44,7 @@ Plotly.register([ require('./scattergl'), require('./splom'), + require('./pointcloud'), require('./heatmapgl'), require('./parcoords'), diff --git a/lib/pointcloud.js b/lib/pointcloud.js new file mode 100644 index 00000000000..15d851007e8 --- /dev/null +++ b/lib/pointcloud.js @@ -0,0 +1,11 @@ +/** +* Copyright 2012-2021, 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'; + +module.exports = require('../src/traces/pointcloud'); diff --git a/package-lock.json b/package-lock.json index cd2a3337465..5fbd8bd9d2c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5586,6 +5586,17 @@ } } }, + "gl-pointcloud2d": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/gl-pointcloud2d/-/gl-pointcloud2d-1.0.3.tgz", + "integrity": "sha512-OS2e1irvJXVRpg/GziXj10xrFJm9kkRfFoB6BLUvkjCQV7ZRNNcs2CD+YSK1r0gvMwTg2T3lfLM3UPwNtz+4Xw==", + "requires": { + "gl-buffer": "^2.1.2", + "gl-shader": "^4.2.1", + "glslify": "^7.0.0", + "typedarray-pool": "^1.1.0" + } + }, "gl-quat": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/gl-quat/-/gl-quat-1.0.0.tgz", diff --git a/package.json b/package.json index 63bbe4b860d..9d70870d3bd 100644 --- a/package.json +++ b/package.json @@ -87,6 +87,7 @@ "gl-mesh3d": "^2.3.1", "gl-plot2d": "^1.4.5", "gl-plot3d": "^2.4.7", + "gl-pointcloud2d": "^1.0.3", "gl-scatter3d": "^1.2.3", "gl-select-box": "^1.0.4", "gl-spikes2d": "^1.0.2", diff --git a/src/components/images/draw.js b/src/components/images/draw.js index 61dcfe5757e..17c17382e99 100644 --- a/src/components/images/draw.js +++ b/src/components/images/draw.js @@ -237,7 +237,7 @@ module.exports = function draw(gd) { var subplotObj = fullLayout._plots[subplot]; // filter out overlaid plots (which have their images on the main plot) - // and heatmapgl plots (which don't support below images, at least not yet) + // and gl2d plots (which don't support below images, at least not yet) if(!subplotObj.imagelayer) continue; var imagesOnSubplot = subplotObj.imagelayer.selectAll('image') diff --git a/src/components/modebar/manage.js b/src/components/modebar/manage.js index 264e8f0062b..6743ba5aae5 100644 --- a/src/components/modebar/manage.js +++ b/src/components/modebar/manage.js @@ -89,7 +89,7 @@ function getButtonGroups(gd) { var hasGeo = fullLayout._has('geo'); var hasPie = fullLayout._has('pie'); var hasFunnelarea = fullLayout._has('funnelarea'); - var hasHeatmapgl = fullLayout._has('gl2d'); + var hasGL2D = fullLayout._has('gl2d'); var hasTernary = fullLayout._has('ternary'); var hasMapbox = fullLayout._has('mapbox'); var hasPolar = fullLayout._has('polar'); @@ -124,7 +124,7 @@ function getButtonGroups(gd) { var resetGroup = []; var dragModeGroup = []; - if((hasCartesian || hasHeatmapgl || hasPie || hasFunnelarea || hasTernary) + hasGeo + hasGL3D + hasMapbox + hasPolar > 1) { + if((hasCartesian || hasGL2D || hasPie || hasFunnelarea || hasTernary) + hasGeo + hasGL3D + hasMapbox + hasPolar > 1) { // graphs with more than one plot types get 'union buttons' // which reset the view or toggle hover labels across all subplots. hoverGroup = ['toggleHover']; @@ -140,7 +140,7 @@ function getButtonGroups(gd) { zoomGroup = ['zoomInMapbox', 'zoomOutMapbox']; hoverGroup = ['toggleHover']; resetGroup = ['resetViewMapbox']; - } else if(hasHeatmapgl) { + } else if(hasGL2D) { hoverGroup = ['hoverClosestGl2d']; } else if(hasPie) { hoverGroup = ['hoverClosestPie']; @@ -161,14 +161,14 @@ function getButtonGroups(gd) { hoverGroup = []; } - if((hasCartesian || hasHeatmapgl) && !allAxesFixed) { + if((hasCartesian || hasGL2D) && !allAxesFixed) { zoomGroup = ['zoomIn2d', 'zoomOut2d', 'autoScale2d']; if(resetGroup[0] !== 'resetViews') resetGroup = ['resetScale2d']; } if(hasGL3D) { dragModeGroup = ['zoom3d', 'pan3d', 'orbitRotation', 'tableRotation']; - } else if(((hasCartesian || hasHeatmapgl) && !allAxesFixed) || hasTernary) { + } else if(((hasCartesian || hasGL2D) && !allAxesFixed) || hasTernary) { dragModeGroup = ['zoom2d', 'pan2d']; } else if(hasMapbox || hasGeo) { dragModeGroup = ['pan2d']; diff --git a/src/plot_api/plot_schema.js b/src/plot_api/plot_schema.js index 73aca869bc8..cfdaa67c823 100644 --- a/src/plot_api/plot_schema.js +++ b/src/plot_api/plot_schema.js @@ -332,7 +332,7 @@ function layoutHeadAttr(fullLayout, head) { _module = basePlotModules[i]; if(_module.attrRegex && _module.attrRegex.test(head)) { // if a module defines overrides, these take precedence - // initially this was to allow heatmapgl different editTypes from svg cartesian + // initially this is to allow gl2d different editTypes from svg cartesian if(_module.layoutAttrOverrides) return _module.layoutAttrOverrides; // otherwise take the first attributes we find @@ -340,7 +340,7 @@ function layoutHeadAttr(fullLayout, head) { } // a module can also override the behavior of base (and component) module layout attrs - // again see heatmapgl for initial use case + // again see gl2d for initial use case var baseOverrides = _module.baseLayoutAttrOverrides; if(baseOverrides && head in baseOverrides) return baseOverrides[head]; } diff --git a/src/plot_api/subroutines.js b/src/plot_api/subroutines.js index ab9b0a59ade..7c78b2026cc 100644 --- a/src/plot_api/subroutines.js +++ b/src/plot_api/subroutines.js @@ -68,7 +68,7 @@ function lsInner(gd) { exports.drawMainTitle(gd); ModeBar.manage(gd); - // _has('cartesian') means SVG specifically, not heatmapgl - but heatmapgl + // _has('cartesian') means SVG specifically, not GL2D - but GL2D // can still get here because it makes some of the SVG structure // for shared features like selections. if(!fullLayout._has('cartesian')) { diff --git a/src/plots/cartesian/axes.js b/src/plots/cartesian/axes.js index 0f7da3b3716..bd53e4364ff 100644 --- a/src/plots/cartesian/axes.js +++ b/src/plots/cartesian/axes.js @@ -102,7 +102,7 @@ axes.coerceRef = function(containerIn, containerOut, gd, attr, dflt, extraOption if(!extraOption) extraOption = dflt; axlist = axlist.concat(axlist.map(function(x) { return x + ' domain'; })); - // data-ref annotations are not supported in heatmapgl yet + // data-ref annotations are not supported in gl2d yet attrDef[refAttr] = { valType: 'enumerated', diff --git a/src/plots/cartesian/include_components.js b/src/plots/cartesian/include_components.js index 2ca206406ac..c699716b5ff 100644 --- a/src/plots/cartesian/include_components.js +++ b/src/plots/cartesian/include_components.js @@ -35,7 +35,7 @@ module.exports = function makeIncludeComponents(containerArrayName) { var xaList = subplots.xaxis; var yaList = subplots.yaxis; var cartesianList = subplots.cartesian; - var hasCartesianOrHeatmapgl = layoutOut._has('cartesian') || layoutOut._has('gl2d'); + var hasCartesianOrGL2D = layoutOut._has('cartesian') || layoutOut._has('gl2d'); for(var i = 0; i < array.length; i++) { var itemi = array[i]; @@ -49,7 +49,7 @@ module.exports = function makeIncludeComponents(containerArrayName) { var hasXref = idRegex.x.test(xref); var hasYref = idRegex.y.test(yref); if(hasXref || hasYref) { - if(!hasCartesianOrHeatmapgl) Lib.pushUnique(layoutOut._basePlotModules, Cartesian); + if(!hasCartesianOrGL2D) Lib.pushUnique(layoutOut._basePlotModules, Cartesian); var newAxis = false; if(hasXref && xaList.indexOf(xref) === -1) { diff --git a/src/plots/plots.js b/src/plots/plots.js index e8f1f1af56e..2174bbc756c 100644 --- a/src/plots/plots.js +++ b/src/plots/plots.js @@ -467,12 +467,12 @@ plots.supplyDefaults = function(gd, opts) { // clean subplots and other artifacts from previous plot calls plots.cleanPlot(newFullData, newFullLayout, oldFullData, oldFullLayout); - var hadHeatmapgl = !!(oldFullLayout._has && oldFullLayout._has('gl2d')); - var hasHeatmapgl = !!(newFullLayout._has && newFullLayout._has('gl2d')); + var hadGL2D = !!(oldFullLayout._has && oldFullLayout._has('gl2d')); + var hasGL2D = !!(newFullLayout._has && newFullLayout._has('gl2d')); var hadCartesian = !!(oldFullLayout._has && oldFullLayout._has('cartesian')); var hasCartesian = !!(newFullLayout._has && newFullLayout._has('cartesian')); - var hadBgLayer = hadCartesian || hadHeatmapgl; - var hasBgLayer = hasCartesian || hasHeatmapgl; + var hadBgLayer = hadCartesian || hadGL2D; + var hasBgLayer = hasCartesian || hasGL2D; if(hadBgLayer && !hasBgLayer) { // remove bgLayer oldFullLayout._bgLayer.remove(); diff --git a/src/traces/heatmap/make_bound_array.js b/src/traces/heatmap/make_bound_array.js index 4356e8c9d80..aad299e565d 100644 --- a/src/traces/heatmap/make_bound_array.js +++ b/src/traces/heatmap/make_bound_array.js @@ -15,7 +15,7 @@ module.exports = function makeBoundArray(trace, arrayIn, v0In, dvIn, numbricks, var arrayOut = []; var isContour = Registry.traceIs(trace, 'contour'); var isHist = Registry.traceIs(trace, 'histogram'); - var isHeatmapgl = Registry.traceIs(trace, 'gl2d'); + var isGL2D = Registry.traceIs(trace, 'gl2d'); var v0; var dv; var i; @@ -30,7 +30,7 @@ module.exports = function makeBoundArray(trace, arrayIn, v0In, dvIn, numbricks, // and extend it linearly based on the last two points if(len <= numbricks) { // contour plots only want the centers - if(isContour || isHeatmapgl) arrayOut = arrayIn.slice(0, numbricks); + if(isContour || isGL2D) arrayOut = arrayIn.slice(0, numbricks); else if(numbricks === 1) { arrayOut = [arrayIn[0] - 0.5, arrayIn[0] + 0.5]; } else { @@ -77,7 +77,7 @@ module.exports = function makeBoundArray(trace, arrayIn, v0In, dvIn, numbricks, dv = dvIn || 1; - for(i = (isContour || isHeatmapgl) ? 0 : -0.5; i < numbricks; i++) { + for(i = (isContour || isGL2D) ? 0 : -0.5; i < numbricks; i++) { arrayOut.push(v0 + dv * i); } } diff --git a/src/traces/pointcloud/attributes.js b/src/traces/pointcloud/attributes.js new file mode 100644 index 00000000000..15ac4d51d98 --- /dev/null +++ b/src/traces/pointcloud/attributes.js @@ -0,0 +1,146 @@ +/** +* Copyright 2012-2021, 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('../scatter/attributes'); + +module.exports = { + x: scatterglAttrs.x, + y: scatterglAttrs.y, + xy: { + valType: 'data_array', + editType: 'calc', + 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(' ') + }, + indices: { + valType: 'data_array', + editType: 'calc', + 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(' ') + }, + xbounds: { + valType: 'data_array', + editType: 'calc', + description: [ + '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', + editType: 'calc', + 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, + marker: { + color: { + valType: 'color', + arrayOk: false, + + editType: 'calc', + 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, + + editType: 'calc', + 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: null, + + editType: 'calc', + 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, + + editType: 'calc', + 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, + + editType: 'calc', + 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, + + editType: 'calc', + 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, + + editType: 'calc', + description: [ + 'Specifies what fraction of the marker area is covered with the', + 'border.' + ].join(' ') + }, + editType: 'calc' + }, + editType: 'calc' + }, + transforms: undefined +}; diff --git a/src/traces/pointcloud/convert.js b/src/traces/pointcloud/convert.js new file mode 100644 index 00000000000..9735071ba54 --- /dev/null +++ b/src/traces/pointcloud/convert.js @@ -0,0 +1,200 @@ +/** +* Copyright 2012-2021, 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 createPointCloudRenderer = require('gl-pointcloud2d'); + +var str2RGBArray = require('../../lib/str2rgbarray'); +var findExtremes = require('../../plots/cartesian/autorange').findExtremes; +var getTraceColor = require('../scatter/get_trace_color'); + +function Pointcloud(scene, uid) { + this.scene = scene; + this.uid = uid; + this.type = 'pointcloud'; + + 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 = createPointCloudRenderer(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]; + + // prefer the readout from XY, if present + return { + trace: this, + dataCoord: pickResult.dataCoord, + 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, + color: this.color, + name: this.name, + pointIndex: index, + hoverinfo: this.hoverinfo + }; +}; + +proto.update = function(options) { + this.index = options.index; + 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 = this.pickXYData = options.xy; + + var userBounds = options.xbounds && options.ybounds; + var index = options.indices; + + var len; + var idToIndex; + var positions; + var bounds = this.bounds; + + var xx, yy, i; + + 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) { + bounds[0] = options.xbounds[0]; + bounds[2] = options.xbounds[1]; + bounds[1] = options.ybounds[0]; + bounds[3] = options.ybounds[1]; + } 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 { + len = x.length; + + 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); + var borderColor = str2RGBArray(options.marker.border.color); + var opacity = options.opacity * options.marker.opacity; + + markerColor[3] *= opacity; + this.pointcloudOptions.color = markerColor; + + // detect blending from the number of points, if undefined + // because large data with blending hits performance + var blend = options.marker.blend; + if(blend === null) { + var maxPoints = 100; + blend = x.length < maxPoints || y.length < maxPoints; + } + this.pointcloudOptions.blend = 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 + var xa = this.scene.xaxis; + var ya = this.scene.yaxis; + var pad = markerSizeMax / 2 || 0.5; + options._extremes[xa._id] = findExtremes(xa, [bounds[0], bounds[2]], {ppad: pad}); + options._extremes[ya._id] = findExtremes(ya, [bounds[1], bounds[3]], {ppad: 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..0aedf577504 --- /dev/null +++ b/src/traces/pointcloud/defaults.js @@ -0,0 +1,46 @@ +/** +* Copyright 2012-2021, 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('xbounds'); + coerce('ybounds'); + + if(traceIn.xy && traceIn.xy instanceof Float32Array) { + traceOut.xy = traceIn.xy; + } + + if(traceIn.indices && traceIn.indices instanceof Int32Array) { + traceOut.indices = traceIn.indices; + } + + 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'); + + // disable 1D transforms - that would defeat the purpose of this trace type, performance! + traceOut._length = null; +}; diff --git a/src/traces/pointcloud/index.js b/src/traces/pointcloud/index.js new file mode 100644 index 00000000000..868e1cce512 --- /dev/null +++ b/src/traces/pointcloud/index.js @@ -0,0 +1,29 @@ +/** +* Copyright 2012-2021, 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'; + +module.exports = { + attributes: require('./attributes'), + supplyDefaults: require('./defaults'), + + // reuse the Scatter3D 'dummy' calc step so that legends know what to do + calc: require('../scatter3d/calc'), + plot: require('./convert'), + + moduleType: 'trace', + name: 'pointcloud', + basePlotModule: require('../../plots/gl2d'), + categories: ['gl', 'gl2d', 'showLegend'], + meta: { + description: [ + 'The data visualized as a point cloud set in `x` and `y`', + 'using the WebGl plotting engine.' + ].join(' ') + } +}; diff --git a/test/image/baselines/gl2d_pointcloud-basic.png b/test/image/baselines/gl2d_pointcloud-basic.png new file mode 100644 index 00000000000..13e73650e51 Binary files /dev/null and b/test/image/baselines/gl2d_pointcloud-basic.png differ diff --git a/test/image/compare_pixels_test.js b/test/image/compare_pixels_test.js index 46361b59818..9ec29d774ae 100644 --- a/test/image/compare_pixels_test.js +++ b/test/image/compare_pixels_test.js @@ -121,7 +121,7 @@ if(argv['skip-flaky']) { }); } -/* non-regl mock(s) i.e. heatmapgl +/* gl2d pointcloud and other non-regl gl2d mock(s) * must be tested first on in order to work; * sort them here. * @@ -136,8 +136,8 @@ if(argv['skip-flaky']) { * More info here: * https://github.com/plotly/plotly.js/pull/1037 */ -function sortHeatmapglMockList(mockList) { - var mockNames = ['gl2d_heatmapgl']; +function sortGl2dMockList(mockList) { + var mockNames = ['gl2d_pointcloud-basic', 'gl2d_heatmapgl']; var pos = 0; mockNames.forEach(function(m) { @@ -283,7 +283,7 @@ function comparePixels(mockName, cb) { .on('close', checkImage); } -sortHeatmapglMockList(allMockList); +sortGl2dMockList(allMockList); console.log(''); // main diff --git a/test/image/mocks/gl2d_pointcloud-basic.json b/test/image/mocks/gl2d_pointcloud-basic.json new file mode 100644 index 00000000000..bdab3ae5510 --- /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": {"text": "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 + } +} diff --git a/test/jasmine/assets/mock_lists.js b/test/jasmine/assets/mock_lists.js index 221ae703166..59af1a73109 100644 --- a/test/jasmine/assets/mock_lists.js +++ b/test/jasmine/assets/mock_lists.js @@ -58,6 +58,7 @@ var glMockList = [ ['gl2d_heatmapgl', require('@mocks/gl2d_heatmapgl.json')], ['gl2d_line_dash', require('@mocks/gl2d_line_dash.json')], ['gl2d_parcoords_2', require('@mocks/gl2d_parcoords_2.json')], + ['gl2d_pointcloud-basic', require('@mocks/gl2d_pointcloud-basic.json')], ['gl3d_annotations', require('@mocks/gl3d_annotations.json')], ['gl3d_set-ranges', require('@mocks/gl3d_set-ranges.json')], ['gl3d_world-cals', require('@mocks/gl3d_world-cals.json')], diff --git a/test/jasmine/bundle_tests/no_webgl_test.js b/test/jasmine/bundle_tests/no_webgl_test.js index 7d33d980c43..10a22e858c5 100644 --- a/test/jasmine/bundle_tests/no_webgl_test.js +++ b/test/jasmine/bundle_tests/no_webgl_test.js @@ -37,8 +37,8 @@ describe('Plotly w/o WebGL support:', function() { .then(done, done.fail); }); - it('heatmapgl subplots', function(done) { - Plotly.react(gd, require('@mocks/gl2d_heatmapgl.json')) + it('gl2d subplots', function(done) { + Plotly.react(gd, require('@mocks/gl2d_pointcloud-basic.json')) .then(function() { checkNoWebGLMsg(true); return Plotly.react(gd, require('@mocks/10.json')); diff --git a/test/jasmine/tests/gl2d_click_test.js b/test/jasmine/tests/gl2d_click_test.js index 69fb784fec7..c615e191e41 100644 --- a/test/jasmine/tests/gl2d_click_test.js +++ b/test/jasmine/tests/gl2d_click_test.js @@ -387,6 +387,33 @@ describe('Test hover and click interactions', function() { .then(done, done.fail); }); + it('@gl should output correct event data for pointcloud', function(done) { + var _mock = Lib.extendDeep({}, require('@mocks/gl2d_pointcloud-basic.json')); + + _mock.layout.hoverlabel = { font: {size: 8} }; + _mock.data[2].hoverlabel = { + bgcolor: ['red', 'green', 'blue'] + }; + + var run = makeRunner([540, 150], { + x: 4.5, + y: 9, + curveNumber: 2, + pointNumber: 1, + bgcolor: 'rgb(0, 128, 0)', + bordercolor: 'rgb(255, 255, 255)', + fontSize: 8, + fontFamily: 'Arial', + fontColor: 'rgb(255, 255, 255)' + }, { + msg: 'pointcloud' + }); + + Plotly.newPlot(gd, _mock) + .then(run) + .then(done, done.fail); + }); + it('@gl scattergl should propagate marker colors to hover labels', function(done) { var _mock = Lib.extendDeep({}, mock0); _mock.layout.width = 800; diff --git a/test/jasmine/tests/mock_test.js b/test/jasmine/tests/mock_test.js index cf9b0ac63ee..44811150551 100644 --- a/test/jasmine/tests/mock_test.js +++ b/test/jasmine/tests/mock_test.js @@ -426,6 +426,7 @@ var list = [ 'gl2d_parcoords_tick_format', 'gl2d_period_positioning', 'gl2d_point-selection', + 'gl2d_pointcloud-basic', 'gl2d_rgb_dont_accept_alpha_scattergl', 'gl2d_scatter_fill_self_next', 'gl2d_scatter_fill_self_next_vs_nogl', @@ -1513,6 +1514,7 @@ figs['gl2d_parcoords_style_labels'] = require('@mocks/gl2d_parcoords_style_label figs['gl2d_parcoords_tick_format'] = require('@mocks/gl2d_parcoords_tick_format'); figs['gl2d_period_positioning'] = require('@mocks/gl2d_period_positioning'); figs['gl2d_point-selection'] = require('@mocks/gl2d_point-selection'); +// figs['gl2d_pointcloud-basic'] = require('@mocks/gl2d_pointcloud-basic'); // figs['gl2d_rgb_dont_accept_alpha_scattergl'] = require('@mocks/gl2d_rgb_dont_accept_alpha_scattergl'); figs['gl2d_scatter_fill_self_next'] = require('@mocks/gl2d_scatter_fill_self_next'); figs['gl2d_scatter_fill_self_next_vs_nogl'] = require('@mocks/gl2d_scatter_fill_self_next_vs_nogl'); diff --git a/test/jasmine/tests/modebar_test.js b/test/jasmine/tests/modebar_test.js index 83b89734e08..a6b52bd45b1 100644 --- a/test/jasmine/tests/modebar_test.js +++ b/test/jasmine/tests/modebar_test.js @@ -521,7 +521,7 @@ describe('ModeBar', function() { checkButtons(modeBar, buttons, 1); }); - it('creates mode bar (heatmapgl version)', function() { + it('creates mode bar (gl2d version)', function() { var buttons = getButtons([ ['toImage'], ['zoom2d', 'pan2d'], diff --git a/test/jasmine/tests/pointcloud_test.js b/test/jasmine/tests/pointcloud_test.js new file mode 100644 index 00000000000..3fba5e52381 --- /dev/null +++ b/test/jasmine/tests/pointcloud_test.js @@ -0,0 +1,255 @@ +'use strict'; + +var Plotly = require('@lib/index'); +var Lib = require('@src/lib'); +var d3Select = require('../../strict-d3').select; + +// Test utilities +var createGraphDiv = require('../assets/create_graph_div'); +var destroyGraphDiv = require('../assets/destroy_graph_div'); + +var delay = require('../assets/delay'); +var mouseEvent = require('../assets/mouse_event'); +var readPixel = require('../assets/read_pixel'); + +var multipleScatter2dMock = require('@mocks/gl2d_scatter2d-multiple-colors.json'); + +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 + } +}; + +describe('pointcloud traces', function() { + var gd; + + beforeEach(function() { + gd = createGraphDiv(); + }); + + afterEach(function() { + Plotly.purge(gd); + destroyGraphDiv(); + }); + + it('@gl renders without raising an error', function(done) { + Plotly.newPlot(gd, Lib.extendDeep({}, plotData)) + .then(done, done.fail); + }); + + it('@gl should update properly', function(done) { + var scene2d; + + Plotly.newPlot(gd, Lib.extendDeep({}, plotData)) + .then(function() { + scene2d = gd._fullLayout._plots.xy._scene2d; + expect(scene2d.traces[gd._fullData[0].uid].type).toBe('pointcloud'); + + return Plotly.relayout(gd, 'xaxis.range', [3, 6]); + }).then(function() { + expect(scene2d.xaxis.range).toEqual([3, 6]); + expect(scene2d.yaxis.range).toBeCloseToArray([-1.415, 10.415], 2); + return Plotly.relayout(gd, 'xaxis.autorange', true); + }).then(function() { + expect(scene2d.xaxis.range).toBeCloseToArray([-0.548, 9.548], 2); + expect(scene2d.yaxis.range).toBeCloseToArray([-1.415, 10.415], 2); + return Plotly.relayout(gd, 'yaxis.range', [8, 20]); + }).then(function() { + expect(scene2d.xaxis.range).toBeCloseToArray([-0.548, 9.548], 2); + expect(scene2d.yaxis.range).toEqual([8, 20]); + return Plotly.relayout(gd, 'yaxis.autorange', true); + }).then(function() { + expect(scene2d.xaxis.range).toBeCloseToArray([-0.548, 9.548], 2); + expect(scene2d.yaxis.range).toBeCloseToArray([-1.415, 10.415], 2); + }) + .then(done, done.fail); + }); + + it('@gl should not change other traces colors', function(done) { + var _mock = Lib.extendDeep({}, multipleScatter2dMock); + Plotly.newPlot(gd, _mock) + .then(delay(20)) + .then(function() { + var canvas = d3Select('.gl-canvas-context').node(); + + var RGBA = readPixel(canvas, canvas.width / 2 - 1, canvas.height / 2 - 1, 1, 1); + + expect(RGBA[0] === 255).toBe(true, 'be red'); + expect(RGBA[1] === 0).toBe(true, 'no green'); + expect(RGBA[2] === 0).toBe(true, 'no blue'); + expect(RGBA[3] === 255).toBe(true, 'no transparent'); + }) + .then(done, done.fail); + }); + + it('@gl should respond to drag', function(done) { + function _drag(p0, p1) { + mouseEvent('mousemove', p0[0], p0[1], {buttons: 1}); + mouseEvent('mousedown', p0[0], p0[1], {buttons: 1}); + mouseEvent('mousemove', (p0[0] + p1[0]) / 2, (p0[1] + p1[1]) / 2, {buttons: 1}); + mouseEvent('mousemove', p1[0], p1[1], {buttons: 0}); + mouseEvent('mouseup', p1[0], p1[1], {buttons: 0}); + } + + function _assertRange(msg, xrng, yrng) { + expect(gd._fullLayout.xaxis.range).toBeCloseToArray(xrng, 2, msg); + expect(gd._fullLayout.yaxis.range).toBeCloseToArray(yrng, 2, msg); + } + + Plotly.newPlot(gd, Lib.extendDeep({}, plotData)) + .then(delay(20)) + .then(function() { + _assertRange('base', [-0.548, 9.548], [-1.415, 10.415]); + }) + .then(delay(20)) + .then(function() { _drag([200, 200], [350, 350]); }) + .then(delay(20)) + .then(function() { + _assertRange('after zoombox drag', [0.768, 1.591], [5.462, 7.584]); + }) + .then(function() { + return Plotly.relayout(gd, { + 'xaxis.autorange': true, + 'yaxis.autorange': true + }); + }) + .then(function() { + _assertRange('back to base', [-0.548, 9.548], [-1.415, 10.415]); + }) + .then(function() { + return Plotly.relayout(gd, 'dragmode', 'pan'); + }) + .then(delay(20)) + .then(function() { _drag([200, 200], [350, 350]); }) + .then(delay(20)) + .then(function() { + _assertRange('after pan drag', [0.2743, 10.3719], [-3.537, 8.292]); + }) + .then(done, done.fail); + }); +});