diff --git a/package.json b/package.json index 303f6c4993e..f073cd1194a 100644 --- a/package.json +++ b/package.json @@ -56,6 +56,8 @@ "3d-view": "^2.0.0", "@plotly/d3-sankey": "^0.5.0", "alpha-shape": "^1.0.0", + "bubleify": "^1.0.0", + "canvas-fit": "^1.5.0", "color-rgba": "^1.1.1", "convex-hull": "^1.0.3", "country-regex": "^1.1.0", @@ -84,8 +86,10 @@ "gl-spikes2d": "^1.0.1", "gl-surface3d": "^1.3.1", "has-hover": "^1.0.1", + "kdgrass": "^1.0.1", "mapbox-gl": "^0.22.0", "matrix-camera-controller": "^2.1.3", + "minify-stream": "^1.1.0", "mouse-change": "^1.4.0", "mouse-event-offset": "^3.0.2", "mouse-wheel": "^1.0.2", @@ -95,11 +99,15 @@ "ndarray-ops": "^1.2.2", "polybooljs": "^1.2.0", "regl": "^1.3.0", + "regl-error2d": "^2.0.3", + "regl-line2d": "^2.1.0", + "regl-scatter2d": "^2.1.6", "right-now": "^1.0.0", "robust-orientation": "^1.1.3", "sane-topojson": "^2.0.0", "strongly-connected-components": "^1.0.1", "superscript-text": "^1.0.0", + "svg-path-sdf": "^1.1.1", "tinycolor2": "^1.3.0", "topojson-client": "^2.1.0", "webgl-context": "^2.2.0", @@ -109,6 +117,7 @@ "brfs": "^1.4.3", "browserify": "^14.1.0", "browserify-transform-tools": "^1.7.0", + "cross-spawn": "^5.1.0", "deep-equal": "^1.0.1", "ecstatic": "^2.1.0", "eslint": "^3.17.1", @@ -140,7 +149,6 @@ "read-last-lines": "^1.1.0", "requirejs": "^2.3.1", "through2": "^2.0.3", - "uglify-js": "^2.8.12", "watchify": "^3.9.0", "xml2js": "^0.4.16" } diff --git a/src/components/drawing/index.js b/src/components/drawing/index.js index 1c244ba7c69..b1aa90ad957 100644 --- a/src/components/drawing/index.js +++ b/src/components/drawing/index.js @@ -213,6 +213,7 @@ drawing.symbolNames = []; drawing.symbolFuncs = []; drawing.symbolNeedLines = {}; drawing.symbolNoDot = {}; +drawing.symbolNoFill = {}; drawing.symbolList = []; Object.keys(SYMBOLDEFS).forEach(function(k) { @@ -231,6 +232,9 @@ Object.keys(SYMBOLDEFS).forEach(function(k) { drawing.symbolList = drawing.symbolList.concat( [symDef.n + 200, k + '-dot', symDef.n + 300, k + '-open-dot']); } + if(symDef.noFill) { + drawing.symbolNoFill[symDef.n] = true; + } }); var MAXSYMBOL = drawing.symbolNames.length, // add a dot in the middle of the symbol diff --git a/src/components/drawing/symbol_defs.js b/src/components/drawing/symbol_defs.js index 548a8c5c307..1c337876c7f 100644 --- a/src/components/drawing/symbol_defs.js +++ b/src/components/drawing/symbol_defs.js @@ -355,7 +355,8 @@ module.exports = { return 'M0,' + rc + 'V-' + rc + 'M' + rc + ',0H-' + rc; }, needLine: true, - noDot: true + noDot: true, + noFill: true }, 'x-thin': { n: 34, @@ -365,7 +366,8 @@ module.exports = { 'M' + rx + ',-' + rx + 'L-' + rx + ',' + rx; }, needLine: true, - noDot: true + noDot: true, + noFill: true }, asterisk: { n: 35, @@ -377,7 +379,8 @@ module.exports = { 'M' + rs + ',-' + rs + 'L-' + rs + ',' + rs; }, needLine: true, - noDot: true + noDot: true, + noFill: true }, hash: { n: 36, @@ -389,7 +392,8 @@ module.exports = { 'M' + r2 + ',' + r1 + 'H-' + r2 + 'm0,-' + r2 + 'H' + r2; }, - needLine: true + needLine: true, + noFill: true }, 'y-up': { n: 37, @@ -400,7 +404,8 @@ module.exports = { return 'M-' + x + ',' + y1 + 'L0,0M' + x + ',' + y1 + 'L0,0M0,-' + y0 + 'L0,0'; }, needLine: true, - noDot: true + noDot: true, + noFill: true }, 'y-down': { n: 38, @@ -411,7 +416,8 @@ module.exports = { return 'M-' + x + ',-' + y1 + 'L0,0M' + x + ',-' + y1 + 'L0,0M0,' + y0 + 'L0,0'; }, needLine: true, - noDot: true + noDot: true, + noFill: true }, 'y-left': { n: 39, @@ -422,7 +428,8 @@ module.exports = { return 'M' + x1 + ',' + y + 'L0,0M' + x1 + ',-' + y + 'L0,0M-' + x0 + ',0L0,0'; }, needLine: true, - noDot: true + noDot: true, + noFill: true }, 'y-right': { n: 40, @@ -433,7 +440,8 @@ module.exports = { return 'M-' + x1 + ',' + y + 'L0,0M-' + x1 + ',-' + y + 'L0,0M' + x0 + ',0L0,0'; }, needLine: true, - noDot: true + noDot: true, + noFill: true }, 'line-ew': { n: 41, @@ -442,7 +450,8 @@ module.exports = { return 'M' + rc + ',0H-' + rc; }, needLine: true, - noDot: true + noDot: true, + noFill: true }, 'line-ns': { n: 42, @@ -451,7 +460,8 @@ module.exports = { return 'M0,' + rc + 'V-' + rc; }, needLine: true, - noDot: true + noDot: true, + noFill: true }, 'line-ne': { n: 43, @@ -460,7 +470,8 @@ module.exports = { return 'M' + rx + ',-' + rx + 'L-' + rx + ',' + rx; }, needLine: true, - noDot: true + noDot: true, + noFill: true }, 'line-nw': { n: 44, @@ -469,6 +480,7 @@ module.exports = { return 'M' + rx + ',' + rx + 'L-' + rx + ',-' + rx; }, needLine: true, - noDot: true + noDot: true, + noFill: true } }; diff --git a/src/components/fx/hover.js b/src/components/fx/hover.js index 3df5fd605ae..6f0cff967e2 100644 --- a/src/components/fx/hover.js +++ b/src/components/fx/hover.js @@ -519,7 +519,7 @@ function createHoverText(hoverData, opts, gd) { var i, traceHoverinfo; for(i = 0; i < hoverData.length; i++) { traceHoverinfo = hoverData[i].hoverinfo || hoverData[i].trace.hoverinfo; - var parts = traceHoverinfo.split('+'); + var parts = Array.isArray(traceHoverinfo) ? traceHoverinfo : traceHoverinfo.split('+'); if(parts.indexOf('all') === -1 && parts.indexOf(hovermode) === -1) { showCommonLabel = false; @@ -1077,8 +1077,9 @@ function cleanPoint(d, hovermode) { } var infomode = d.hoverinfo || d.trace.hoverinfo; + if(infomode !== 'all') { - infomode = infomode.split('+'); + infomode = Array.isArray(infomode) ? infomode : infomode.split('+'); if(infomode.indexOf('x') === -1) d.xLabel = undefined; if(infomode.indexOf('y') === -1) d.yLabel = undefined; if(infomode.indexOf('z') === -1) d.zLabel = undefined; diff --git a/src/fonts/ploticon/config.json b/src/fonts/ploticon/config.json index 851669be315..6bdb659f75d 100644 --- a/src/fonts/ploticon/config.json +++ b/src/fonts/ploticon/config.json @@ -87,7 +87,7 @@ "width": 1500 }, "search": [ - "tooltip_basic" + "tooltip_basic" ] }, { diff --git a/src/lib/gl_format_color.js b/src/lib/gl_format_color.js index 83052c63360..8d93b7e1f92 100644 --- a/src/lib/gl_format_color.js +++ b/src/lib/gl_format_color.js @@ -59,6 +59,7 @@ function formatColor(containerIn, opacityIn, len) { if(isArrayColorIn) { getColor = function(c, i) { + // FIXME: there is double work, considering that sclFunc does the opposite return c[i] === undefined ? colorDfltRgba : rgba(sclFunc(c[i])); }; } diff --git a/src/plot_api/plot_api.js b/src/plot_api/plot_api.js index 205994bf1a6..e40fb0263af 100644 --- a/src/plot_api/plot_api.js +++ b/src/plot_api/plot_api.js @@ -220,16 +220,11 @@ Plotly.plot = function(gd, data, layout, config) { 'left': 0, 'width': '100%', 'height': '100%', - 'overflow': 'visible' + 'overflow': 'visible', + 'pointer-events': 'none' }) .attr('width', fullLayout.width) .attr('height', fullLayout.height); - - fullLayout._glcanvas.filter(function(d) { - return !d.pick; - }).style({ - 'pointer-events': 'none' - }); } return Lib.syncOrAsync([ @@ -2804,6 +2799,7 @@ function makePlotFramework(gd) { // FIXME: parcoords reuses this object, not the best pattern fullLayout._glcontainer = fullLayout._paperdiv.selectAll('.gl-container') .data([{}]); + fullLayout._glcontainer.enter().append('div') .classed('gl-container', true); diff --git a/src/plot_api/subroutines.js b/src/plot_api/subroutines.js index f59434c9963..1d943300628 100644 --- a/src/plot_api/subroutines.js +++ b/src/plot_api/subroutines.js @@ -498,7 +498,6 @@ exports.doModeBar = function(gd) { if(updateFx) updateFx(fullLayout); } - return Plots.previousPromises(gd); }; diff --git a/src/plots/cartesian/dragbox.js b/src/plots/cartesian/dragbox.js index 1b7ca54a061..8029679da70 100644 --- a/src/plots/cartesian/dragbox.js +++ b/src/plots/cartesian/dragbox.js @@ -759,7 +759,9 @@ module.exports = function dragBox(gd, plotinfo, x, y, w, h, ns, ew) { } // don't scale at all if neither axis is scalable here - if(!xScaleFactor2 && !yScaleFactor2) continue; + if(!xScaleFactor2 && !yScaleFactor2) { + continue; + } // but if only one is, reset the other axis scaling if(!xScaleFactor2) xScaleFactor2 = 1; diff --git a/src/plots/cartesian/index.js b/src/plots/cartesian/index.js index a9db08dd172..118472c6eb5 100644 --- a/src/plots/cartesian/index.js +++ b/src/plots/cartesian/index.js @@ -137,7 +137,7 @@ function plotOne(gd, plotinfo, cdSubplot, transitionOpts, makeOnCompleteCallback } } - _module.plot(gd, plotinfo, cdModule, transitionOpts, makeOnCompleteCallback); + if(_module.plot) _module.plot(gd, plotinfo, cdModule, transitionOpts, makeOnCompleteCallback); } } @@ -145,13 +145,14 @@ exports.clean = function(newFullData, newFullLayout, oldFullData, oldFullLayout) var oldModules = oldFullLayout._modules || [], newModules = newFullLayout._modules || []; - var hadScatter, hasScatter, i; + var hadScatter, hasScatter, hadGl, hasGl, i, oldPlots, ids, subplotInfo; + for(i = 0; i < oldModules.length; i++) { if(oldModules[i].name === 'scatter') { hadScatter = true; - break; } + break; } for(i = 0; i < newModules.length; i++) { @@ -161,12 +162,26 @@ exports.clean = function(newFullData, newFullLayout, oldFullData, oldFullLayout) } } + for(i = 0; i < oldModules.length; i++) { + if(oldModules[i].name === 'scattergl') { + hadGl = true; + } + break; + } + + for(i = 0; i < newModules.length; i++) { + if(newModules[i].name === 'scattergl') { + hasGl = true; + break; + } + } + if(hadScatter && !hasScatter) { - var oldPlots = oldFullLayout._plots, - ids = Object.keys(oldPlots || {}); + oldPlots = oldFullLayout._plots; + ids = Object.keys(oldPlots || {}); for(i = 0; i < ids.length; i++) { - var subplotInfo = oldPlots[ids[i]]; + subplotInfo = oldPlots[ids[i]]; if(subplotInfo.plot) { subplotInfo.plot.select('g.scatterlayer') @@ -181,6 +196,19 @@ exports.clean = function(newFullData, newFullLayout, oldFullData, oldFullLayout) .remove(); } + if(hadGl && !hasGl) { + oldPlots = oldFullLayout._plots; + ids = Object.keys(oldPlots || {}); + + for(i = 0; i < ids.length; i++) { + subplotInfo = oldPlots[ids[i]]; + + if(subplotInfo._scene) { + subplotInfo._scene.destroy(); + } + } + } + var hadCartesian = (oldFullLayout._has && oldFullLayout._has('cartesian')); var hasCartesian = (newFullLayout._has && newFullLayout._has('cartesian')); @@ -222,7 +250,6 @@ exports.drawFramework = function(gd) { plotinfo.overlays = []; makeSubplotLayer(plotinfo); - // fill in list of overlay subplots if(plotinfo.mainplot) { var mainplot = fullLayout._plots[plotinfo.mainplot]; diff --git a/src/plots/cartesian/select.js b/src/plots/cartesian/select.js index e1429f6e911..9fdd82faa53 100644 --- a/src/plots/cartesian/select.js +++ b/src/plots/cartesian/select.js @@ -62,12 +62,11 @@ module.exports = function prepSelect(e, startX, startY, dragOptions, mode) { if(mode === 'lasso') { filterPoly = filteredPolygon([[x0, y0]], constants.BENDPX); } - - var outlines = zoomLayer.selectAll('path.select-outline').data([1, 2]); + var outlines = zoomLayer.selectAll('path.select-outline-' + plotinfo.id).data([1, 2]); outlines.enter() .append('path') - .attr('class', function(d) { return 'select-outline select-outline-' + d; }) + .attr('class', function(d) { return 'select-outline select-outline-' + d + ' select-outline-' + plotinfo.id; }) .attr('transform', 'translate(' + xs + ', ' + ys + ')') .attr('d', path0 + 'Z'); @@ -148,7 +147,7 @@ module.exports = function prepSelect(e, startX, startY, dragOptions, mode) { } }; } else { - fillRangeItems = function(eventData, currentPolygon, filterPoly) { + fillRangeItems = function(eventData, poly, filterPoly) { var dataPts = eventData.lassoPoints = {}; for(i = 0; i < allAxes.length; i++) { @@ -225,7 +224,8 @@ module.exports = function prepSelect(e, startX, startY, dragOptions, mode) { var ppts = mergedPolygons[i]; paths.push(ppts.join('L') + 'L' + ppts[0]); } - outlines.attr('d', 'M' + paths.join('M') + 'Z'); + outlines + .attr('d', 'M' + paths.join('M') + 'Z'); throttle.throttle( throttleID, @@ -233,14 +233,15 @@ module.exports = function prepSelect(e, startX, startY, dragOptions, mode) { function() { selection = []; - var traceSelections = [], traceSelection; + var thisSelection, traceSelections = [], traceSelection; for(i = 0; i < searchTraces.length; i++) { searchInfo = searchTraces[i]; traceSelection = searchInfo.selectPoints(searchInfo, testPoly); traceSelections.push(traceSelection); - var thisSelection = fillSelectionItem(traceSelection, searchInfo); + thisSelection = fillSelectionItem(traceSelection, searchInfo); + if(selection.length) { for(var j = 0; j < thisSelection.length; j++) { selection.push(thisSelection[j]); @@ -308,8 +309,8 @@ function updateSelectedState(gd, searchTraces, eventData) { var fullData = pt.fullData; if(pt.pointIndices) { - data.selectedpoints = data.selectedpoints.concat(pt.pointIndices); - fullData.selectedpoints = fullData.selectedpoints.concat(pt.pointIndices); + [].push.apply(data.selectedpoints, pt.pointIndices); + [].push.apply(fullData.selectedpoints, pt.pointIndices); } else { data.selectedpoints.push(pt.pointIndex); fullData.selectedpoints.push(pt.pointIndex); @@ -321,6 +322,11 @@ function updateSelectedState(gd, searchTraces, eventData) { trace = searchTraces[i].cd[0].trace; delete trace.selectedpoints; delete trace._input.selectedpoints; + + // delete scattergl selection + if(searchTraces[i].cd[0].t && searchTraces[i].cd[0].t.scene) { + searchTraces[i].cd[0].t.scene.clearSelect(); + } } } diff --git a/src/plots/plots.js b/src/plots/plots.js index 5d0515a0a42..aabf96f2609 100644 --- a/src/plots/plots.js +++ b/src/plots/plots.js @@ -161,6 +161,7 @@ plots.getSubplotData = function getSubplotData(data, type, subplotId) { return subplotData; }; + /** * Get calcdata traces(s) associated with a given subplot * @@ -572,7 +573,6 @@ plots.createTransitionData = function(gd) { // or trace has a category plots._hasPlotType = function(category) { // check plot - var basePlotModules = this._basePlotModules || []; var i; @@ -681,10 +681,6 @@ plots.linkSubplots = function(newFullData, newFullLayout, oldFullData, oldFullLa if(oldSubplot) { plotinfo = newSubplots[id] = oldSubplot; - if(plotinfo._scene2d) { - plotinfo._scene2d.updateRefs(newFullLayout); - } - if(plotinfo.xaxis.layer !== xaxis.layer) { plotinfo.xlines.attr('d', null); plotinfo.xaxislayer.selectAll('*').remove(); @@ -814,7 +810,6 @@ plots.supplyDataDefaults = function(dataIn, dataOut, layout, fullLayout) { var _module = fullTrace._module; if(!_module) return; - Lib.pushUnique(modules, _module); Lib.pushUnique(basePlotModules, fullTrace._module.basePlotModule); @@ -860,7 +855,6 @@ plots.supplyDataDefaults = function(dataIn, dataOut, layout, fullLayout) { } } else { - // add identify refs for consistency with transformed traces fullTrace._fullInput = fullTrace; fullTrace._expandedInput = fullTrace; @@ -1000,7 +994,7 @@ plots.supplyTraceDefaults = function(traceIn, traceOutIndex, layout, traceInInde var subplotType = subplotTypes[i]; // done below (only when visible is true) - // TODO unified this pattern + // TODO unify this pattern if(['cartesian', 'gl2d'].indexOf(subplotType) !== -1) continue; var attr = subplotsRegistry[subplotType].attr; @@ -1008,13 +1002,14 @@ plots.supplyTraceDefaults = function(traceIn, traceOutIndex, layout, traceInInde if(attr) coerceSubplotAttr(subplotType, attr); } + + var _module = plots.getModule(traceOut); + traceOut._module = _module; + if(visible) { coerce('customdata'); coerce('ids'); - var _module = plots.getModule(traceOut); - traceOut._module = _module; - if(plots.traceIs(traceOut, 'showLegend')) { coerce('showlegend'); coerce('legendgroup'); @@ -1046,7 +1041,7 @@ plots.supplyTraceDefaults = function(traceIn, traceOutIndex, layout, traceInInde traceOut.visible = !!traceOut.visible; } - if(_module && _module.selectPoints && traceOut.type !== 'scattergl') { + if(_module && _module.selectPoints) { coerce('selectedpoints'); } @@ -1326,7 +1321,6 @@ plots.supplyLayoutModuleDefaults = function(layoutIn, layoutOut, fullData, trans // Remove all plotly attributes from a div so it can be replotted fresh // TODO: these really need to be encapsulated into a much smaller set... plots.purge = function(gd) { - // note: we DO NOT remove _context because it doesn't change when we insert // a new plot, and may have been set outside of our scope. diff --git a/src/traces/parcoords/attributes.js b/src/traces/parcoords/attributes.js index e994a5d2c9a..d7b34b20d9d 100644 --- a/src/traces/parcoords/attributes.js +++ b/src/traces/parcoords/attributes.js @@ -19,7 +19,6 @@ var extendDeepAll = extend.extendDeepAll; var extendFlat = extend.extendFlat; module.exports = { - domain: { x: { valType: 'info_array', @@ -136,6 +135,7 @@ module.exports = { line: extendFlat( // the default autocolorscale isn't quite usable for parcoords due to context ambiguity around 0 (grey, off-white) + // autocolorscale therefore defaults to false too, to avoid being overridden by the blue-white-red autocolor palette extendDeepAll( colorAttributes('line', 'calc'), @@ -153,7 +153,6 @@ module.exports = { 'The default value is false, so that `parcoords` colorscale can default to `Viridis`.' ].join(' ') } - } ), diff --git a/src/traces/parcoords/base_plot.js b/src/traces/parcoords/base_plot.js index c562787b289..9a2a5074191 100644 --- a/src/traces/parcoords/base_plot.js +++ b/src/traces/parcoords/base_plot.js @@ -33,7 +33,6 @@ exports.clean = function(newFullData, newFullLayout, oldFullData, oldFullLayout) }; exports.toSVG = function(gd) { - var imageRoot = gd._fullLayout._glimages; var root = d3.select(gd).selectAll('.svg-container'); var canvases = root.filter(function(d, i) {return i === root.size() - 1;}) @@ -47,11 +46,11 @@ exports.toSVG = function(gd) { image.attr({ xmlns: xmlnsNamespaces.svg, 'xlink:href': imageData, + preserveAspectRatio: 'none', x: 0, y: 0, width: canvas.width, - height: canvas.height, - preserveAspectRatio: 'none' + height: canvas.height }); } diff --git a/src/traces/parcoords/lines.js b/src/traces/parcoords/lines.js index b63b14b13f8..31d861eb99e 100644 --- a/src/traces/parcoords/lines.js +++ b/src/traces/parcoords/lines.js @@ -55,7 +55,8 @@ function renderBlock(regl, glAes, renderState, blockLineCount, sampleCount, item item.offset = sectionVertexCount * blockNumber * blockLineCount; item.count = sectionVertexCount * count; if(blockNumber === 0) { - window.cancelAnimationFrame(renderState.currentRafs[rafKey]); // stop drawing possibly stale glyphs before clearing + // stop drawing possibly stale glyphs before clearing + window.cancelAnimationFrame(renderState.currentRafs[rafKey]); delete renderState.currentRafs[rafKey]; clear(regl, item.scissorX, item.scissorY, item.scissorWidth, item.viewBoxSize[1]); } @@ -353,6 +354,7 @@ module.exports = function(canvasGL, d, scatter) { colorClamp: colorClamp, scatter: scatter || 0, + scissorX: (I === leftmost ? 0 : x + overdrag) + (model.pad.l - overdrag) + model.layoutWidth * domain.x[0], scissorWidth: (I === rightmost ? canvasWidth - x + overdrag : panelSizeX + 0.5) + (I === leftmost ? x + overdrag : 0), scissorY: y + model.pad.b + model.layoutHeight * domain.y[0], @@ -431,6 +433,7 @@ module.exports = function(canvasGL, d, scatter) { } function destroy() { + canvasGL.style['pointer-events'] = 'none'; paletteTexture.destroy(); } diff --git a/src/traces/parcoords/parcoords.js b/src/traces/parcoords/parcoords.js index 96b83392650..3246b1a3bf8 100644 --- a/src/traces/parcoords/parcoords.js +++ b/src/traces/parcoords/parcoords.js @@ -305,6 +305,7 @@ module.exports = function(root, svg, parcoordsLineLayers, styledData, layout, ca .filter(function(d) { return d.pick; }) + .style('pointer-events', 'auto') .on('mousemove', function(d) { if(linePickActive && d.lineLayer && callbacks && callbacks.hover) { var event = d3.event; diff --git a/src/traces/parcoords/plot.js b/src/traces/parcoords/plot.js index 433938ec1b9..284a25f4766 100644 --- a/src/traces/parcoords/plot.js +++ b/src/traces/parcoords/plot.js @@ -12,7 +12,6 @@ var parcoords = require('./parcoords'); var createRegl = require('regl'); module.exports = function plot(gd, cdparcoords) { - var fullLayout = gd._fullLayout; var svg = fullLayout._toppaper; var root = fullLayout._paperdiv; diff --git a/src/traces/pointcloud/attributes.js b/src/traces/pointcloud/attributes.js index 17f4af0418a..32f5499c158 100644 --- a/src/traces/pointcloud/attributes.js +++ b/src/traces/pointcloud/attributes.js @@ -8,7 +8,7 @@ 'use strict'; -var scatterglAttrs = require('../scattergl/attributes'); +var scatterglAttrs = require('../scatter/attributes'); module.exports = { x: scatterglAttrs.x, diff --git a/src/traces/scattergl/attributes.js b/src/traces/scattergl/attributes.js index cf225193709..437c266c3ac 100644 --- a/src/traces/scattergl/attributes.js +++ b/src/traces/scattergl/attributes.js @@ -12,7 +12,6 @@ var scatterAttrs = require('../scatter/attributes'); var colorAttributes = require('../../components/colorscale/color_attributes'); var DASHES = require('../../constants/gl2d_dashes'); -var MARKERS = require('../../constants/gl2d_markers'); var extendFlat = require('../../lib/extend').extendFlat; var overrideAll = require('../../plot_api/edit_types').overrideAll; @@ -58,14 +57,7 @@ var attrs = module.exports = overrideAll({ } }, marker: extendFlat({}, colorAttributes('marker'), { - symbol: { - valType: 'enumerated', - values: Object.keys(MARKERS), - dflt: 'circle', - arrayOk: true, - role: 'style', - description: 'Sets the marker symbol type.' - }, + symbol: scatterMarkerAttrs.symbol, size: scatterMarkerAttrs.size, sizeref: scatterMarkerAttrs.sizeref, sizemin: scatterMarkerAttrs.sizemin, @@ -78,11 +70,18 @@ var attrs = module.exports = overrideAll({ }) }), connectgaps: scatterAttrs.connectgaps, - fill: extendFlat({}, scatterAttrs.fill, { - values: ['none', 'tozeroy', 'tozerox'] - }), + fill: scatterAttrs.fill, fillcolor: scatterAttrs.fillcolor, + hoveron: scatterAttrs.hoveron, + + selected: { + marker: scatterAttrs.selected.marker + }, + unselected: { + marker: scatterAttrs.unselected.marker + }, + error_y: scatterAttrs.error_y, error_x: scatterAttrs.error_x }, 'calc', 'nested'); diff --git a/src/traces/scattergl/calc.js b/src/traces/scattergl/calc.js deleted file mode 100644 index 1be524af55e..00000000000 --- a/src/traces/scattergl/calc.js +++ /dev/null @@ -1,43 +0,0 @@ -/** -* Copyright 2012-2017, Plotly, Inc. -* All rights reserved. -* -* This source code is licensed under the MIT license found in the -* LICENSE file in the root directory of this source tree. -*/ - - -'use strict'; - -var Axes = require('../../plots/cartesian/axes'); -var arraysToCalcdata = require('../scatter/arrays_to_calcdata'); -var calcColorscales = require('../scatter/colorscale_calc'); - -module.exports = function calc(gd, trace) { - var dragmode = gd._fullLayout.dragmode; - var cd; - - if(dragmode === 'lasso' || dragmode === 'select') { - var xa = Axes.getFromId(gd, trace.xaxis || 'x'); - var ya = Axes.getFromId(gd, trace.yaxis || 'y'); - - var x = xa.makeCalcdata(trace, 'x'); - var y = ya.makeCalcdata(trace, 'y'); - - var serieslen = Math.min(x.length, y.length), i; - - // create the "calculated data" to plot - cd = new Array(serieslen); - - for(i = 0; i < serieslen; i++) { - cd[i] = {x: x[i], y: y[i]}; - } - } else { - cd = [{x: false, y: false, trace: trace, t: {}}]; - arraysToCalcdata(cd, trace); - } - - calcColorscales(trace); - - return cd; -}; diff --git a/src/traces/scattergl/convert.js b/src/traces/scattergl/convert.js deleted file mode 100644 index 5b7be4466d9..00000000000 --- a/src/traces/scattergl/convert.js +++ /dev/null @@ -1,768 +0,0 @@ -/** -* Copyright 2012-2017, Plotly, Inc. -* All rights reserved. -* -* This source code is licensed under the MIT license found in the -* LICENSE file in the root directory of this source tree. -*/ - - -'use strict'; - -var createScatter = require('gl-scatter2d'); -var createFancyScatter = require('gl-scatter2d-sdf'); -var createLine = require('gl-line2d'); -var createError = require('gl-error2d'); -var isNumeric = require('fast-isnumeric'); - -var Lib = require('../../lib'); -var Axes = require('../../plots/cartesian/axes'); -var autoType = require('../../plots/cartesian/axis_autotype'); -var ErrorBars = require('../../components/errorbars'); -var str2RGBArray = require('../../lib/str2rgbarray'); -var truncate = require('../../lib/typed_array_truncate'); -var formatColor = require('../../lib/gl_format_color'); -var subTypes = require('../scatter/subtypes'); -var makeBubbleSizeFn = require('../scatter/make_bubble_size_func'); -var getTraceColor = require('../scatter/get_trace_color'); -var MARKER_SYMBOLS = require('../../constants/gl2d_markers'); -var DASHES = require('../../constants/gl2d_dashes'); -var DESELECTDIM = require('../../constants/interactions').DESELECTDIM; - -var AXES = ['xaxis', 'yaxis']; -var TRANSPARENT = [0, 0, 0, 0]; - -function LineWithMarkers(scene, uid) { - this.scene = scene; - this.uid = uid; - this.type = 'scattergl'; - - this.pickXData = []; - this.pickYData = []; - this.xData = []; - this.yData = []; - this.textLabels = []; - this.color = 'rgb(0, 0, 0)'; - this.name = ''; - this.hoverinfo = 'all'; - this.connectgaps = true; - - this.index = null; - this.idToIndex = []; - this.bounds = [0, 0, 0, 0]; - - this.isVisible = false; - this.hasLines = false; - this.hasErrorX = false; - this.hasErrorY = false; - this.hasMarkers = false; - - this.line = this.initObject(createLine, { - positions: new Float64Array(0), - color: [0, 0, 0, 1], - width: 1, - fill: [false, false, false, false], - fillColor: [ - [0, 0, 0, 1], - [0, 0, 0, 1], - [0, 0, 0, 1], - [0, 0, 0, 1]], - dashes: [1], - }, 0); - - this.errorX = this.initObject(createError, { - positions: new Float64Array(0), - errors: new Float64Array(0), - lineWidth: 1, - capSize: 0, - color: [0, 0, 0, 1] - }, 1); - - this.errorY = this.initObject(createError, { - positions: new Float64Array(0), - errors: new Float64Array(0), - lineWidth: 1, - capSize: 0, - color: [0, 0, 0, 1] - }, 2); - - var scatterOptions0 = { - positions: new Float64Array(0), - sizes: [], - colors: [], - glyphs: [], - borderWidths: [], - borderColors: [], - size: 12, - color: [0, 0, 0, 1], - borderSize: 1, - borderColor: [0, 0, 0, 1], - snapPoints: true - }; - var scatterOptions1 = Lib.extendFlat({}, scatterOptions0, {snapPoints: false}); - - this.scatter = this.initObject(createScatter, scatterOptions0, 3); - this.fancyScatter = this.initObject(createFancyScatter, scatterOptions0, 4); - this.selectScatter = this.initObject(createScatter, scatterOptions1, 5); -} - -var proto = LineWithMarkers.prototype; - -proto.initObject = function(createFn, options, objIndex) { - var _this = this; - var glplot = _this.scene.glplot; - var options0 = Lib.extendFlat({}, options); - var obj = null; - - function update() { - if(!obj) { - obj = createFn(glplot, options); - obj._trace = _this; - obj._index = objIndex; - } - obj.update(options); - } - - function clear() { - if(obj) obj.update(options0); - } - - function dispose() { - if(obj) obj.dispose(); - } - - return { - options: options, - update: update, - clear: clear, - dispose: dispose - }; -}; - -proto.handlePick = function(pickResult) { - var index = pickResult.pointId; - - if(pickResult.object !== this.line || this.connectgaps) { - index = this.idToIndex[pickResult.pointId]; - } - - var x = this.pickXData[index]; - - return { - trace: this, - dataCoord: pickResult.dataCoord, - traceCoord: [ - isNumeric(x) || !Lib.isDateTime(x) ? x : Lib.dateTime2ms(x), - this.pickYData[index] - ], - textLabel: Array.isArray(this.textLabels) ? - this.textLabels[index] : - this.textLabels, - color: Array.isArray(this.color) ? - this.color[index] : - this.color, - name: this.name, - pointIndex: index, - hoverinfo: this.hoverinfo - }; -}; - -// check if trace is fancy -proto.isFancy = function(options) { - if(this.scene.xaxis.type !== 'linear' && this.scene.xaxis.type !== 'date') return true; - if(this.scene.yaxis.type !== 'linear') return true; - - if(!options.x || !options.y) return true; - - if(this.hasMarkers) { - var marker = options.marker || {}; - - if(Array.isArray(marker.symbol) || - marker.symbol !== 'circle' || - Array.isArray(marker.size) || - Array.isArray(marker.color) || - Array.isArray(marker.line.width) || - Array.isArray(marker.line.color) || - Array.isArray(marker.opacity) - ) return true; - } - - if(this.hasLines && !this.connectgaps) return true; - - if(this.hasErrorX) return true; - if(this.hasErrorY) return true; - - return false; -}; - -// handle the situation where values can be array-like or not array like -function convertArray(convert, data, count) { - if(!Array.isArray(data)) data = [data]; - - return _convertArray(convert, data, count); -} - -function _convertArray(convert, data, count) { - var result = new Array(count), - data0 = data[0]; - - for(var i = 0; i < count; ++i) { - result[i] = (i >= data.length) ? - convert(data0) : - convert(data[i]); - } - - return result; -} - -var convertNumber = convertArray.bind(null, function(x) { return +x; }); -var convertColorBase = convertArray.bind(null, str2RGBArray); -var convertSymbol = convertArray.bind(null, function(x) { - return MARKER_SYMBOLS[x] ? x : 'circle'; -}); - -function convertColor(color, opacity, count) { - return _convertColor( - convertColorBase(color, count), - convertNumber(opacity, count), - count - ); -} - -function convertColorScale(containerIn, markerOpacity, traceOpacity, count) { - var colors = formatColor(containerIn, markerOpacity, count); - - colors = Array.isArray(colors[0]) ? - colors : - _convertArray(Lib.identity, [colors], count); - - return _convertColor( - colors, - convertNumber(traceOpacity, count), - count - ); -} - -function _convertColor(colors, opacities, count) { - var result = new Array(4 * count); - - for(var i = 0; i < count; ++i) { - for(var j = 0; j < 3; ++j) result[4 * i + j] = colors[i][j]; - - result[4 * i + 3] = colors[i][3] * opacities[i]; - } - - return result; -} - -function isSymbolOpen(symbol) { - return symbol.split('-open')[1] === ''; -} - -function fillColor(colorIn, colorOut, offsetIn, offsetOut, isDimmed) { - var dim = isDimmed ? DESELECTDIM : 1; - var j; - - for(j = 0; j < 3; j++) { - colorIn[4 * offsetIn + j] = colorOut[4 * offsetOut + j]; - } - colorIn[4 * offsetIn + j] = dim * colorOut[4 * offsetOut + j]; -} - -proto.update = function(options, cdscatter) { - if(options.visible !== true) { - this.isVisible = false; - this.hasLines = false; - this.hasErrorX = false; - this.hasErrorY = false; - this.hasMarkers = false; - } - else { - this.isVisible = true; - this.hasLines = subTypes.hasLines(options); - this.hasErrorX = options.error_x.visible === true; - this.hasErrorY = options.error_y.visible === true; - this.hasMarkers = subTypes.hasMarkers(options); - } - - this.textLabels = options.text; - this.name = options.name; - this.hoverinfo = options.hoverinfo; - this.bounds = [Infinity, Infinity, -Infinity, -Infinity]; - this.connectgaps = !!options.connectgaps; - - if(!this.isVisible) { - this.line.clear(); - this.errorX.clear(); - this.errorY.clear(); - this.scatter.clear(); - this.fancyScatter.clear(); - } - else if(this.isFancy(options)) { - this.updateFancy(options); - } - else { - this.updateFast(options); - } - - // sort objects so that order is preserve on updates: - // - lines - // - errorX - // - errorY - // - markers - this.scene.glplot.objects.sort(function(a, b) { - return a._index - b._index; - }); - - // set trace index so that scene2d can sort object per traces - this.index = options.index; - - // not quite on-par with 'scatter', but close enough for now - // does not handle the colorscale case - this.color = getTraceColor(options, {}); - - // provide reference for selecting points - if(cdscatter && cdscatter[0] && !cdscatter[0]._glTrace) { - cdscatter[0]._glTrace = this; - } -}; - -// We'd ideally know that all values are of fast types; sampling gives no certainty but faster -// (for the future, typed arrays can guarantee it, and Date values can be done with -// representing the epoch milliseconds in a typed array; -// also, perhaps the Python / R interfaces take care of String->Date conversions -// such that there's no need to check for string dates in plotly.js) -// Patterned from axis_autotype.js:moreDates -// Code DRYing is not done to preserve the most direct compilation possible for speed; -// also, there are quite a few differences -function allFastTypesLikely(a) { - var len = a.length, - inc = Math.max(1, (len - 1) / Math.min(Math.max(len, 1), 1000)), - ai; - - for(var i = 0; i < len; i += inc) { - ai = a[Math.floor(i)]; - if(!isNumeric(ai) && !(ai instanceof Date)) { - return false; - } - } - - return true; -} - -proto.updateFast = function(options) { - var x = this.xData = this.pickXData = options.x; - var y = this.yData = this.pickYData = options.y; - - var len = x.length, - idToIndex = new Array(len), - positions = new Float64Array(2 * len), - bounds = this.bounds, - pId = 0, - ptr = 0, - selection = options.selection, - i, selPositions, l; - - var xx, yy; - - var xcalendar = options.xcalendar; - - var fastType = allFastTypesLikely(x); - var isDateTime = !fastType && autoType(x, xcalendar) === 'date'; - - // TODO add 'very fast' mode that bypasses this loop - // TODO bypass this on modebar +/- zoom - if(fastType || isDateTime) { - - for(i = 0; i < len; ++i) { - xx = x[i]; - yy = y[i]; - - if(isNumeric(yy)) { - - if(!fastType) { - xx = Lib.dateTime2ms(xx, xcalendar); - } - - positions[ptr++] = xx; - positions[ptr++] = yy; - - idToIndex[pId++] = i; - - bounds[0] = Math.min(bounds[0], xx); - bounds[1] = Math.min(bounds[1], yy); - bounds[2] = Math.max(bounds[2], xx); - bounds[3] = Math.max(bounds[3], yy); - } - } - } - - positions = truncate(positions, ptr); - this.idToIndex = idToIndex; - - // form selected set - if(selection && selection.length) { - selPositions = new Float64Array(2 * selection.length); - - for(i = 0, l = selection.length; i < l; i++) { - selPositions[i * 2 + 0] = selection[i].x; - selPositions[i * 2 + 1] = selection[i].y; - } - } - - this.updateLines(options, positions); - this.updateError('X', options); - this.updateError('Y', options); - - var markerSize; - - if(this.hasMarkers) { - var markerColor, borderColor, opacity; - - // if we have selPositions array - means we have to render all points transparent, and selected points opaque - if(selPositions) { - this.scatter.options.positions = null; - - markerColor = str2RGBArray(options.marker.color); - borderColor = str2RGBArray(options.marker.line.color); - opacity = (options.opacity) * (options.marker.opacity) * DESELECTDIM; - - markerColor[3] *= opacity; - this.scatter.options.color = markerColor; - - borderColor[3] *= opacity; - this.scatter.options.borderColor = borderColor; - - markerSize = options.marker.size; - this.scatter.options.size = markerSize; - this.scatter.options.borderSize = options.marker.line.width; - - this.scatter.update(); - this.scatter.options.positions = positions; - - - this.selectScatter.options.positions = selPositions; - - markerColor = str2RGBArray(options.marker.color); - borderColor = str2RGBArray(options.marker.line.color); - opacity = (options.opacity) * (options.marker.opacity); - - markerColor[3] *= opacity; - this.selectScatter.options.color = markerColor; - - borderColor[3] *= opacity; - this.selectScatter.options.borderColor = borderColor; - - markerSize = options.marker.size; - this.selectScatter.options.size = markerSize; - this.selectScatter.options.borderSize = options.marker.line.width; - - this.selectScatter.update(); - } - - else { - this.scatter.options.positions = positions; - - markerColor = str2RGBArray(options.marker.color); - borderColor = str2RGBArray(options.marker.line.color); - opacity = (options.opacity) * (options.marker.opacity); - markerColor[3] *= opacity; - this.scatter.options.color = markerColor; - - borderColor[3] *= opacity; - this.scatter.options.borderColor = borderColor; - - markerSize = options.marker.size; - this.scatter.options.size = markerSize; - this.scatter.options.borderSize = options.marker.line.width; - - this.scatter.update(); - } - - } - else { - this.scatter.clear(); - } - - // turn off fancy scatter plot - this.fancyScatter.clear(); - - // add item for autorange routine - this.expandAxesFast(bounds, markerSize); -}; - -proto.updateFancy = function(options) { - var scene = this.scene, - xaxis = scene.xaxis, - yaxis = scene.yaxis, - bounds = this.bounds, - selection = options.selection; - - // makeCalcdata runs d2c (data-to-coordinate) on every point - var x = this.pickXData = xaxis.makeCalcdata(options, 'x').slice(); - var y = this.pickYData = yaxis.makeCalcdata(options, 'y').slice(); - - this.xData = x.slice(); - this.yData = y.slice(); - - // get error values - var errorVals = ErrorBars.calcFromTrace(options, scene.fullLayout); - - var len = x.length, - idToIndex = new Array(len), - positions = new Float64Array(2 * len), - errorsX = new Float64Array(4 * len), - errorsY = new Float64Array(4 * len), - pId = 0, - ptr = 0, - ptrX = 0, - ptrY = 0; - - var getX = (xaxis.type === 'log') ? xaxis.d2l : function(x) { return x; }; - var getY = (yaxis.type === 'log') ? yaxis.d2l : function(y) { return y; }; - - var i, xx, yy, ex0, ex1, ey0, ey1; - - for(i = 0; i < len; ++i) { - this.xData[i] = xx = getX(x[i]); - this.yData[i] = yy = getY(y[i]); - - if(isNaN(xx) || isNaN(yy)) continue; - - idToIndex[pId++] = i; - - positions[ptr++] = xx; - positions[ptr++] = yy; - - ex0 = errorsX[ptrX++] = xx - errorVals[i].xs || 0; - ex1 = errorsX[ptrX++] = errorVals[i].xh - xx || 0; - errorsX[ptrX++] = 0; - errorsX[ptrX++] = 0; - - errorsY[ptrY++] = 0; - errorsY[ptrY++] = 0; - ey0 = errorsY[ptrY++] = yy - errorVals[i].ys || 0; - ey1 = errorsY[ptrY++] = errorVals[i].yh - yy || 0; - - bounds[0] = Math.min(bounds[0], xx - ex0); - bounds[1] = Math.min(bounds[1], yy - ey0); - bounds[2] = Math.max(bounds[2], xx + ex1); - bounds[3] = Math.max(bounds[3], yy + ey1); - } - - positions = truncate(positions, ptr); - this.idToIndex = idToIndex; - - this.updateLines(options, positions); - this.updateError('X', options, positions, errorsX); - this.updateError('Y', options, positions, errorsY); - - var sizes, selIds; - - if(selection && selection.length) { - selIds = {}; - for(i = 0; i < selection.length; i++) { - selIds[selection[i].pointNumber] = true; - } - } - - if(this.hasMarkers) { - this.scatter.options.positions = positions; - - // TODO rewrite convert function so that - // we don't have to loop through the data another time - - this.scatter.options.sizes = new Array(pId); - this.scatter.options.glyphs = new Array(pId); - this.scatter.options.borderWidths = new Array(pId); - this.scatter.options.colors = new Array(pId * 4); - this.scatter.options.borderColors = new Array(pId * 4); - - var markerSizeFunc = makeBubbleSizeFn(options); - var markerOpts = options.marker; - var markerOpacity = markerOpts.opacity; - var traceOpacity = options.opacity; - var symbols = convertSymbol(markerOpts.symbol, len); - var colors = convertColorScale(markerOpts, markerOpacity, traceOpacity, len); - var borderWidths = convertNumber(markerOpts.line.width, len); - var borderColors = convertColorScale(markerOpts.line, markerOpacity, traceOpacity, len); - var index, size, symbol, symbolSpec, isOpen, isDimmed, _colors, _borderColors, bw, minBorderWidth; - - sizes = convertArray(markerSizeFunc, markerOpts.size, len); - - for(i = 0; i < pId; ++i) { - index = idToIndex[i]; - - symbol = symbols[index]; - symbolSpec = MARKER_SYMBOLS[symbol]; - isOpen = isSymbolOpen(symbol); - isDimmed = selIds && !selIds[index]; - - if(symbolSpec.noBorder && !isOpen) { - _colors = borderColors; - } else { - _colors = colors; - } - - if(isOpen) { - _borderColors = colors; - } else { - _borderColors = borderColors; - } - - // See https://github.com/plotly/plotly.js/pull/1781#discussion_r121820798 - // for more info on this logic - size = sizes[index]; - bw = borderWidths[index]; - minBorderWidth = (symbolSpec.noBorder || symbolSpec.noFill) ? 0.1 * size : 0; - - this.scatter.options.sizes[i] = 4.0 * size; - this.scatter.options.glyphs[i] = symbolSpec.unicode; - this.scatter.options.borderWidths[i] = 0.5 * ((bw > minBorderWidth) ? bw - minBorderWidth : 0); - - if(isOpen && !symbolSpec.noBorder && !symbolSpec.noFill) { - fillColor(this.scatter.options.colors, TRANSPARENT, i, 0); - } else { - fillColor(this.scatter.options.colors, _colors, i, index, isDimmed); - } - fillColor(this.scatter.options.borderColors, _borderColors, i, index, isDimmed); - } - - // prevent scatter from resnapping points - if(selIds) { - this.scatter.options.positions = null; - this.fancyScatter.update(); - this.scatter.options.positions = positions; - } - else { - this.fancyScatter.update(); - } - } - else { - this.fancyScatter.clear(); - } - - // turn off fast scatter plot - this.scatter.clear(); - - // add item for autorange routine - this.expandAxesFancy(x, y, sizes); -}; - -proto.updateLines = function(options, positions) { - var i; - - if(this.hasLines) { - var linePositions = positions; - - if(!options.connectgaps) { - var p = 0; - var x = this.xData; - var y = this.yData; - linePositions = new Float64Array(2 * x.length); - - for(i = 0; i < x.length; ++i) { - linePositions[p++] = x[i]; - linePositions[p++] = y[i]; - } - } - - this.line.options.positions = linePositions; - - var lineColor = convertColor(options.line.color, options.opacity, 1), - lineWidth = Math.round(0.5 * this.line.options.width), - dashes = (DASHES[options.line.dash] || [1]).slice(); - - for(i = 0; i < dashes.length; ++i) dashes[i] *= lineWidth; - - switch(options.fill) { - case 'tozeroy': - this.line.options.fill = [false, true, false, false]; - break; - case 'tozerox': - this.line.options.fill = [true, false, false, false]; - break; - default: - this.line.options.fill = [false, false, false, false]; - break; - } - - var fillColor = str2RGBArray(options.fillcolor); - - this.line.options.color = lineColor; - this.line.options.width = 2.0 * options.line.width; - this.line.options.dashes = dashes; - this.line.options.fillColor = [fillColor, fillColor, fillColor, fillColor]; - - this.line.update(); - } - else { - this.line.clear(); - } -}; - -proto.updateError = function(axLetter, options, positions, errors) { - var errorObj = this['error' + axLetter], - errorOptions = options['error_' + axLetter.toLowerCase()]; - - if(axLetter.toLowerCase() === 'x' && errorOptions.copy_ystyle) { - errorOptions = options.error_y; - } - - if(this['hasError' + axLetter]) { - errorObj.options.positions = positions; - errorObj.options.errors = errors; - errorObj.options.capSize = errorOptions.width; - errorObj.options.lineWidth = errorOptions.thickness / 2; // ballpark rescaling - errorObj.options.color = convertColor(errorOptions.color, 1, 1); - - errorObj.update(); - } - else { - errorObj.clear(); - } -}; - -proto.expandAxesFast = function(bounds, markerSize) { - var pad = markerSize || 10; - 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 }); - } -}; - -// not quite on-par with 'scatter' (scatter fill in several other expand options) -// but close enough for now -proto.expandAxesFancy = function(x, y, ppad) { - var scene = this.scene, - expandOpts = { padded: true, ppad: ppad }; - - Axes.expand(scene.xaxis, x, expandOpts); - Axes.expand(scene.yaxis, y, expandOpts); -}; - -proto.dispose = function() { - this.line.dispose(); - this.errorX.dispose(); - this.errorY.dispose(); - this.scatter.dispose(); - this.fancyScatter.dispose(); -}; - -function createLineWithMarkers(scene, data, cdscatter) { - var plot = new LineWithMarkers(scene, data.uid); - plot.update(data, cdscatter); - - return plot; -} - -module.exports = createLineWithMarkers; diff --git a/src/traces/scattergl/defaults.js b/src/traces/scattergl/defaults.js index b2bbdf5ca98..e3c2e80f730 100644 --- a/src/traces/scattergl/defaults.js +++ b/src/traces/scattergl/defaults.js @@ -41,8 +41,11 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout handleLineDefaults(traceIn, traceOut, defaultColor, layout, coerce); } + var dfltHoverOn = []; + if(subTypes.hasMarkers(traceOut)) { - handleMarkerDefaults(traceIn, traceOut, defaultColor, layout, coerce, {noSelect: true}); + handleMarkerDefaults(traceIn, traceOut, defaultColor, layout, coerce); + dfltHoverOn.push('points'); } coerce('fill'); @@ -50,6 +53,14 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout handleFillColorDefaults(traceIn, traceOut, defaultColor, coerce); } + if(traceOut.fill === 'tonext' || traceOut.fill === 'toself') { + dfltHoverOn.push('fills'); + } + + coerce('hoveron', dfltHoverOn.join('+') || 'points'); + errorBarsSupplyDefaults(traceIn, traceOut, defaultColor, {axis: 'y'}); errorBarsSupplyDefaults(traceIn, traceOut, defaultColor, {axis: 'x', inherit: 'y'}); + + Lib.coerceSelectionMarkerOpacity(traceOut, coerce); }; diff --git a/src/traces/scattergl/index.js b/src/traces/scattergl/index.js index 6ad0666d16a..5e09b2bfbcf 100644 --- a/src/traces/scattergl/index.js +++ b/src/traces/scattergl/index.js @@ -8,29 +8,1133 @@ 'use strict'; -var ScatterGl = {}; +var Lib = require('../../lib'); +var getTraceColor = require('../scatter/get_trace_color'); +var ErrorBars = require('../../components/errorbars'); +var extend = require('object-assign'); +var Axes = require('../../plots/cartesian/axes'); +var kdtree = require('kdgrass'); +var Fx = require('../../components/fx'); +var subTypes = require('../scatter/subtypes'); +var calcColorscales = require('../scatter/colorscale_calc'); +var Drawing = require('../../components/drawing'); +var makeBubbleSizeFn = require('../scatter/make_bubble_size_func'); +var DASHES = require('../../constants/gl2d_dashes'); +var formatColor = require('../../lib/gl_format_color'); +var linkTraces = require('../scatter/link_traces'); +var createScatter = require('regl-scatter2d'); +var createLine = require('regl-line2d'); +var createError = require('regl-error2d'); +var svgSdf = require('svg-path-sdf'); +var createRegl = require('regl'); +var fillHoverText = require('../scatter/fill_hover_text'); +var isNumeric = require('fast-isnumeric'); +var Scatter = require('../scatter'); +var MAXDIST = Fx.constants.MAXDIST; +var SYMBOL_SDF_SIZE = 200; +var SYMBOL_SIZE = 20; +var SYMBOL_STROKE = SYMBOL_SIZE / 20; +var SYMBOL_SDF = {}; +var SYMBOL_SVG_CIRCLE = Drawing.symbolFuncs[0](SYMBOL_SIZE * 0.05); +var TOO_MANY_POINTS = 1e5; +var DOT_RE = /-dot/; + + +var ScatterGl = module.exports = {}; + +ScatterGl.name = 'scattergl'; +ScatterGl.categories = ['gl', 'regl', 'cartesian', 'symbols', 'errorBarsOK', 'markerColorscale', 'showLegend', 'scatter-like']; ScatterGl.attributes = require('./attributes'); ScatterGl.supplyDefaults = require('./defaults'); -ScatterGl.colorbar = require('../scatter/colorbar'); -ScatterGl.hoverPoints = require('../scatter/hover'); +ScatterGl.cleanData = Scatter.cleanData; +ScatterGl.arraysToCalcdata = Scatter.arraysToCalcdata; +ScatterGl.colorbar = Scatter.colorbar; +ScatterGl.meta = Scatter.meta; +ScatterGl.animatable = true; +ScatterGl.hasLines = subTypes.hasLines; +ScatterGl.hasMarkers = subTypes.hasMarkers; +ScatterGl.hasText = subTypes.hasText; +ScatterGl.isBubble = subTypes.isBubble; +ScatterGl.moduleType = 'trace'; +ScatterGl.basePlotModule = require('../../plots/cartesian'); -// reuse the Scatter3D 'dummy' calc step so that legends know what to do -ScatterGl.calc = require('./calc'); -ScatterGl.plot = require('./convert'); -ScatterGl.selectPoints = require('./select'); -ScatterGl.moduleType = 'trace'; -ScatterGl.name = 'scattergl'; -ScatterGl.basePlotModule = require('../../plots/gl2d'); -ScatterGl.categories = ['gl', 'gl2d', 'symbols', 'errorBarsOK', 'markerColorscale', 'showLegend', 'scatter-like']; -ScatterGl.meta = { - description: [ - 'The data visualized as scatter point or lines is set in `x` and `y`', - 'using the WebGl plotting engine.', - 'Bubble charts are achieved by setting `marker.size` and/or `marker.color`', - 'to a numerical arrays.' - ].join(' ') +ScatterGl.calc = function calc(container, trace) { + var layout = container._fullLayout; + var positions; + var stash = {}; + var xaxis = Axes.getFromId(container, trace.xaxis); + var yaxis = Axes.getFromId(container, trace.yaxis); + var markerOpts = trace.marker; + + // FIXME: is it the best way to obtain subplot object from trace + var subplot = layout._plots[trace.xaxis + trace.yaxis]; + // makeCalcdata runs d2c (data-to-coordinate) on every point + var x = xaxis.type === 'linear' ? trace.x : xaxis.makeCalcdata(trace, 'x'); + var y = yaxis.type === 'linear' ? trace.y : yaxis.makeCalcdata(trace, 'y'); + var count = (x || y).length, i, l, xx, yy, ptrX = 0, ptrY = 0; + + if(!x) { + x = Array(count); + for(i = 0; i < count; i++) { + x[i] = i; + } + } + if(!y) { + y = Array(count); + for(i = 0; i < count; i++) { + y[i] = i; + } + } + + var lineOptions, markerOptions, errorXOptions, errorYOptions, fillOptions, selectedOptions, unselectedOptions; + var hasLines, hasErrorX, hasErrorY, hasError, hasMarkers, hasFill; + var linePositions; + + // get log converted positions + var rawx, rawy; + if(xaxis.type === 'log') { + rawx = Array(x.length); + for(i = 0, l = x.length; i < l; i++) { + rawx[i] = x[i]; + x[i] = xaxis.d2l(x[i]); + } + } + else { + rawx = x; + for(i = 0, l = x.length; i < l; i++) { + x[i] = parseFloat(x[i]); + } + } + if(yaxis.type === 'log') { + rawy = Array(y.length); + for(i = 0, l = y.length; i < l; i++) { + rawy[i] = y[i]; + y[i] = yaxis.d2l(y[i]); + } + } + else { + rawy = y; + for(i = 0, l = y.length; i < l; i++) { + y[i] = parseFloat(y[i]); + } + } + + // we need hi-precision for scatter2d + positions = new Array(count * 2); + + for(i = 0; i < count; i++) { + // if no x defined, we are creating simple int sequence (API) + // we use parseFloat because it gives NaN (we need that for empty values to avoid drawing lines) and it is incredibly fast + xx = isNumeric(x[i]) ? +x[i] : NaN; + yy = isNumeric(y[i]) ? +y[i] : NaN; + + positions[i * 2] = xx; + positions[i * 2 + 1] = yy; + } + + calcColorscales(trace); + + // we don't build a tree for log axes since it takes long to convert log2px + // and it is also + if(xaxis.type !== 'log' && yaxis.type !== 'log') { + // FIXME: delegate this to webworker + stash.tree = kdtree(positions, 512); + } + else { + var ids = stash.ids = Array(count); + for(i = 0; i < count; i++) { + ids[i] = i; + } + } + + if(trace.visible !== true) { + hasLines = false; + hasErrorX = false; + hasErrorY = false; + hasMarkers = false; + hasFill = false; + } + else { + hasLines = subTypes.hasLines(trace) && positions.length > 2; + hasErrorX = trace.error_x.visible === true; + hasErrorY = trace.error_y.visible === true; + hasError = hasErrorX || hasErrorY; + hasMarkers = subTypes.hasMarkers(trace); + hasFill = !!trace.fill && trace.fill !== 'none'; + } + + // get error values + var errorVals = hasError ? ErrorBars.calcFromTrace(trace, layout) : null; + + if(hasErrorX) { + errorXOptions = {}; + errorXOptions.positions = positions; + var errorsX = new Float64Array(4 * count); + + for(i = 0; i < count; ++i) { + errorsX[ptrX++] = rawx[i] - errorVals[i].xs || 0; + errorsX[ptrX++] = errorVals[i].xh - rawx[i] || 0; + errorsX[ptrX++] = 0; + errorsX[ptrX++] = 0; + } + + if(trace.error_x.copy_ystyle) { + trace.error_x = trace.error_y; + } + + errorXOptions.positions = positions; + errorXOptions.errors = errorsX; + errorXOptions.capSize = trace.error_x.width * 2; + errorXOptions.lineWidth = trace.error_x.thickness; + errorXOptions.color = trace.error_x.color; + } + + if(hasErrorY) { + errorYOptions = {}; + errorYOptions.positions = positions; + var errorsY = new Float64Array(4 * count); + + for(i = 0; i < count; ++i) { + errorsY[ptrY++] = 0; + errorsY[ptrY++] = 0; + errorsY[ptrY++] = rawy[i] - errorVals[i].ys || 0; + errorsY[ptrY++] = errorVals[i].yh - rawy[i] || 0; + } + + errorYOptions.positions = positions; + errorYOptions.errors = errorsY; + errorYOptions.capSize = trace.error_y.width * 2; + errorYOptions.lineWidth = trace.error_y.thickness; + errorYOptions.color = trace.error_y.color; + } + + if(hasLines) { + lineOptions = {}; + lineOptions.thickness = trace.line.width; + lineOptions.color = trace.line.color; + lineOptions.opacity = trace.opacity; + lineOptions.overlay = true; + + var dashes = (DASHES[trace.line.dash] || [1]).slice(); + for(i = 0; i < dashes.length; ++i) dashes[i] *= lineOptions.thickness; + lineOptions.dashes = dashes; + + if(trace.line.shape === 'hv') { + linePositions = []; + for(i = 0; i < Math.floor(positions.length / 2) - 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 < Math.floor(positions.length / 2) - 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; + } + } + lineOptions.join = (hasNaN || linePositions.length > TOO_MANY_POINTS) ? 'rect' : hasMarkers ? 'rect' : 'round'; + + // fill gaps + if(hasNaN && trace.connectgaps) { + var lastX = linePositions[0], 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]; + } + } + } + + lineOptions.positions = linePositions; + } + + if(hasFill) { + fillOptions = {}; + fillOptions.fill = trace.fillcolor; + fillOptions.thickness = 0; + fillOptions.closed = true; + } + + if(hasMarkers) { + markerOptions = makeMarkerOptions(markerOpts); + selectedOptions = trace.selected ? makeMarkerOptions(extend({}, markerOpts, trace.selected.marker)) : markerOptions; + unselectedOptions = trace.unselected ? makeMarkerOptions(extend({}, markerOpts, trace.unselected.marker)) : markerOptions; + + markerOptions.positions = positions; + } + // expand no-markers axes + else { + Axes.expand(xaxis, rawx, { padded: true }); + Axes.expand(yaxis, rawy, { padded: true }); + } + + function makeMarkerOptions(markerOpts) { + var markerOptions = {}; + + // get basic symbol info + var multiMarker = Array.isArray(markerOpts.symbol); + var isOpen, symbol; + if(!multiMarker) { + isOpen = /-open/.test(markerOpts.symbol); + } + // prepare colors + if(multiMarker || Array.isArray(markerOpts.color) || Array.isArray(markerOpts.line.color) || Array.isArray(markerOpts.line) || Array.isArray(markerOpts.opacity)) { + markerOptions.colors = new Array(count); + markerOptions.borderColors = new Array(count); + var colors = formatColor(markerOpts, markerOpts.opacity, count); + var borderColors = formatColor(markerOpts.line, markerOpts.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; + } + } + + markerOptions.colors = colors; + markerOptions.borderColors = borderColors; + + for(i = 0; i < count; i++) { + if(multiMarker) { + symbol = markerOpts.symbol[i]; + isOpen = /-open/.test(symbol); + } + if(isOpen) { + borderColors[i] = colors[i].slice(); + colors[i] = colors[i].slice(); + colors[i][3] = 0; + } + } + + markerOptions.opacity = trace.opacity; + } + else { + markerOptions.color = markerOpts.color; + markerOptions.borderColor = markerOpts.line.color; + markerOptions.opacity = trace.opacity * markerOpts.opacity; + + if(isOpen) { + markerOptions.borderColor = markerOptions.color.slice(); + markerOptions.color = markerOptions.color.slice(); + markerOptions.color[3] = 0; + } + } + + // prepare markers + if(Array.isArray(markerOpts.symbol)) { + markerOptions.markers = new Array(count); + for(i = 0; i < count; ++i) { + markerOptions.markers[i] = getSymbolSdf(markerOpts.symbol[i]); + } + } + else { + markerOptions.marker = getSymbolSdf(markerOpts.symbol); + } + + // prepare sizes and expand axes + var multiSize = markerOpts && (Array.isArray(markerOpts.size) || Array.isArray(markerOpts.line.width)); + var xbounds = [Infinity, -Infinity], ybounds = [Infinity, -Infinity]; + var markerSizeFunc = makeBubbleSizeFn(trace); + var size, sizes; + + if(multiSize) { + sizes = markerOptions.sizes = new Array(count); + var borderSizes = markerOptions.borderSizes = new Array(count); + + if(Array.isArray(markerOpts.size)) { + for(i = 0; i < count; ++i) { + sizes[i] = markerSizeFunc(markerOpts.size[i]); + } + } + else { + size = markerSizeFunc(markerOpts.size); + for(i = 0; i < count; ++i) { + sizes[i] = size; + } + } + + // See https://github.com/plotly/plotly.js/pull/1781#discussion_r121820798 + if(Array.isArray(markerOpts.line.width)) { + for(i = 0; i < count; ++i) { + borderSizes[i] = markerOpts.line.width[i] * 0.5; + } + } + else { + size = markerSizeFunc(markerOpts.line.width) * 0.5; + for(i = 0; i < count; ++i) { + borderSizes[i] = size; + } + } + + Axes.expand(xaxis, rawx, { padded: true, ppad: sizes }); + Axes.expand(yaxis, rawy, { padded: true, ppad: sizes }); + } + else { + size = markerOptions.size = markerSizeFunc(markerOpts && markerOpts.size || 10); + markerOptions.borderSizes = markerOpts.line.width * 0.5; + + // axes bounds + for(i = 0; i < count; i++) { + xx = x[i], yy = y[i]; + if(xbounds[0] > xx) xbounds[0] = xx; + if(xbounds[1] < xx) xbounds[1] = xx; + if(ybounds[0] > yy) ybounds[0] = yy; + if(ybounds[1] < yy) ybounds[1] = yy; + } + + // FIXME: is there a better way to separate expansion? + if(count < TOO_MANY_POINTS) { + Axes.expand(xaxis, rawx, { padded: true, ppad: size }); + Axes.expand(yaxis, rawy, { padded: true, ppad: size }); + } + // update axes fast for big number of points + else { + var pad = markerOptions.size; + if(xaxis._min) { + xaxis._min.push({ val: xbounds[0], pad: pad }); + } + if(xaxis._max) { + xaxis._max.push({ val: xbounds[1], pad: pad }); + } + + if(yaxis._min) { + yaxis._min.push({ val: ybounds[0], pad: pad }); + } + if(yaxis._max) { + yaxis._max.push({ val: ybounds[1], pad: pad }); + } + } + } + + return markerOptions; + } + + + // make sure scene exists + var scene = subplot._scene; + + if(!subplot._scene) { + scene = subplot._scene = { + // number of traces in subplot, since scene:subplot → 1:1 + count: 0, + + // whether scene requires init hook in plot call (dirty plot call) + dirty: true, + + // last used options + lineOptions: [], + fillOptions: [], + markerOptions: [], + selectedOptions: [], + unselectedOptions: [], + errorXOptions: [], + errorYOptions: [], + selectBatch: null, + unselectBatch: null, + + // regl- component stubs, initialized in dirty plot call + fill2d: hasFill, + scatter2d: hasMarkers, + error2d: hasError, + line2d: hasLines, + select2d: null + }; + + // apply new option to all regl components + scene.update = function update(opt) { + var opts = Array(scene.count); + for(var i = 0; i < scene.count; i++) { + opts[i] = opt; + } + if(scene.fill2d) scene.fill2d.update(opts); + if(scene.scatter2d) scene.scatter2d.update(opts); + if(scene.line2d) scene.line2d.update(opts); + if(scene.error2d) scene.error2d.update([].push.apply(opts, opts)); + if(scene.select2d) scene.select2d.update(opts); + + scene.draw(); + }; + + // draw traces in proper order + scene.draw = function draw() { + for(var i = 0; i < scene.count; i++) { + if(scene.fill2d) scene.fill2d.draw(i); + if(scene.line2d) { + scene.line2d.draw(i); + } + if(scene.error2d) { + scene.error2d.draw(i); + scene.error2d.draw(i + scene.count); + } + if(scene.scatter2d && !scene.selectBatch) { + scene.scatter2d.draw(i); + } + } + + // persistent selection draw + if(scene.select2d && scene.selectBatch) { + scene.select2d.draw(scene.selectBatch); + scene.scatter2d.draw(scene.unselectBatch); + } + + scene.dirty = false; + }; + + // make sure canvas is clear + scene.clear = function clear() { + var vpSize = layout._size, width = layout.width, height = layout.height; + var vp = [ + vpSize.l + xaxis.domain[0] * vpSize.w, + vpSize.b + yaxis.domain[0] * vpSize.h, + (width - vpSize.r) - (1 - xaxis.domain[1]) * vpSize.w, + (height - vpSize.t) - (1 - yaxis.domain[1]) * vpSize.h + ]; + + var gl, regl; + + regl = scene.select2d.regl; + gl = regl._gl; + gl.enable(gl.SCISSOR_TEST); + gl.scissor(vp[0], vp[1], vp[2] - vp[0], vp[3] - vp[1]); + gl.clearColor(0, 0, 0, 0); + gl.clear(gl.COLOR_BUFFER_BIT); + + regl = scene.scatter2d.regl; + gl = regl._gl; + gl.enable(gl.SCISSOR_TEST); + gl.scissor(vp[0], vp[1], vp[2] - vp[0], vp[3] - vp[1]); + gl.clearColor(0, 0, 0, 0); + gl.clear(gl.COLOR_BUFFER_BIT); + }; + + // remove selection + scene.clearSelect = function clearSelect() { + if(!scene.selectBatch) return; + scene.selectBatch = null; + scene.unselectBatch = null; + scene.scatter2d.update(scene.markerOptions); + scene.clear(); + scene.draw(); + }; + + // remove scene resources + scene.destroy = function destroy() { + if(scene.fill2d) scene.fill2d.destroy(); + if(scene.scatter2d) scene.scatter2d.destroy(); + if(scene.error2d) scene.error2d.destroy(); + if(scene.line2d) scene.line2d.destroy(); + if(scene.select2d) scene.select2d.destroy(); + + scene.lineOptions = null; + scene.fillOptions = null; + scene.markerOptions = null; + scene.selectedOptions = null; + scene.unselectedOptions = null; + scene.errorXOptions = null; + scene.errorYOptions = null; + scene.selectBatch = null; + scene.unselectBatch = null; + + delete subplot._scene; + }; + } + else { + if(hasFill && !scene.fill2d) scene.fill2d = true; + if(hasMarkers && !scene.scatter2d) scene.scatter2d = true; + if(hasLines && !scene.line2d) scene.line2d = true; + if(hasError && !scene.error2d) scene.error2d = true; + } + + // In case if we have scene from the last calc - reset data + if(!scene.dirty) { + scene.dirty = true; + scene.count = 0; + scene.lineOptions = []; + scene.fillOptions = []; + scene.markerOptions = []; + scene.selectedOptions = []; + scene.unselectedOptions = []; + scene.errorXOptions = []; + scene.errorYOptions = []; + } + + // save initial batch + scene.lineOptions.push(hasLines ? lineOptions : null); + scene.errorXOptions.push(hasErrorX ? errorXOptions : null); + scene.errorYOptions.push(hasErrorY ? errorYOptions : null); + scene.fillOptions.push(hasFill ? fillOptions : null); + scene.markerOptions.push(hasMarkers ? markerOptions : null); + scene.selectedOptions.push(hasMarkers ? selectedOptions : null); + scene.unselectedOptions.push(hasMarkers ? unselectedOptions : null); + scene.count++; + + // stash scene ref + stash.scene = scene; + stash.index = scene.count - 1; + stash.x = x; + stash.y = y; + stash.rawx = rawx; + stash.rawy = rawy; + stash.positions = positions; + stash.count = count; + + return [{x: false, y: false, t: stash, trace: trace}]; }; -module.exports = ScatterGl; + +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 = DOT_RE.test(symbol); + + // 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; +} + + +ScatterGl.plot = function plot(container, subplot, cdata) { + var layout = container._fullLayout; + var scene = subplot._scene; + + // we may have more subplots than initialized data due to Axes.getSubplots method + if(!scene) return; + + var vpSize = layout._size, width = layout.width, height = layout.height; + + // make sure proper regl instances are created + layout._glcanvas.each(function(d) { + if(d.regl || d.pick) return; + d.regl = createRegl({ + canvas: this, + attributes: { + antialias: !d.pick, + preserveDrawingBuffer: true + }, + extensions: ['ANGLE_instanced_arrays', 'OES_element_index_uint'], + pixelRatio: container._context.plotGlPixelRatio || global.devicePixelRatio + }); + }); + + var regl = layout._glcanvas.data()[0].regl; + + // that is needed for fills + linkTraces(container, subplot, cdata); + + if(scene.dirty) { + // make sure scenes are created + if(scene.error2d === true) { + scene.error2d = createError(regl); + } + if(scene.line2d === true) { + scene.line2d = createLine(regl); + } + if(scene.scatter2d === true) { + scene.scatter2d = createScatter(regl); + } + if(scene.fill2d === true) { + scene.fill2d = createLine(regl); + } + + if(scene.line2d) { + scene.line2d.update(scene.lineOptions); + } + if(scene.error2d) { + var errorBatch = (scene.errorXOptions || []).concat(scene.errorYOptions || []); + scene.error2d.update(errorBatch); + } + if(scene.scatter2d) { + if(!scene.selectBatch) { + scene.scatter2d.update(scene.markerOptions); + } + else { + scene.scatter2d.update(scene.unselectedOptions); + scene.select2d.update(scene.selectedOptions); + } + } + // fill requires linked traces, so we generate it's positions here + if(scene.fill2d) { + scene.fillOptions.forEach(function(fillOptions, i) { + var cdscatter = cdata[i]; + if(!cdscatter || !cdscatter[0] || !cdscatter[0].trace) return; + var cd = cdscatter[0]; + var trace = cd.trace; + var stash = cd.t; + var lineOptions = scene.lineOptions[i]; + var last, j; + + var pos = [], srcPos = (lineOptions && lineOptions.positions) || stash.positions; + + if(trace.fill === 'tozeroy') { + pos = [srcPos[0], 0]; + pos = pos.concat(srcPos); + pos.push(srcPos[srcPos.length - 2]); + pos.push(0); + } + else if(trace.fill === 'tozerox') { + pos = [0, srcPos[1]]; + pos = pos.concat(srcPos); + pos.push(0); + pos.push(srcPos[srcPos.length - 1]); + } + else if(trace.fill === 'toself' || trace.fill === 'tonext') { + pos = []; + last = 0; + for(j = 0; j < srcPos.length; j += 2) { + if(isNaN(srcPos[j]) || isNaN(srcPos[j + 1])) { + pos = pos.concat(srcPos.slice(last, j)); + pos.push(srcPos[last], srcPos[last + 1]); + last = j + 2; + } + } + pos = pos.concat(srcPos.slice(last)); + if(last) { + pos.push(srcPos[last], srcPos[last + 1]); + } + } + else { + var nextTrace = trace._nexttrace; + + if(nextTrace) { + var nextOptions = scene.lineOptions[i + 1]; + + if(nextOptions) { + var nextPos = nextOptions.positions; + if(trace.fill === 'tonexty') { + pos = srcPos.slice(); + + for(i = Math.floor(nextPos.length / 2); i--;) { + var xx = nextPos[i * 2], yy = nextPos[i * 2 + 1]; + if(isNaN(xx) || isNaN(yy)) continue; + pos.push(xx); + pos.push(yy); + } + fillOptions.fill = nextTrace.fillcolor; + } + } + } + } + + // detect prev trace positions to exclude from current fill + if(trace._prevtrace && trace._prevtrace.fill === 'tonext') { + var prevLinePos = scene.lineOptions[i - 1].positions; + + // FIXME: likely this logic should be tested better + var offset = pos.length / 2; + last = offset; + var hole = [last]; + for(j = 0; j < prevLinePos.length; j += 2) { + if(isNaN(prevLinePos[j]) || isNaN(prevLinePos[j + 1])) { + hole.push(j / 2 + offset + 1); + last = j + 2; + } + } + + pos = pos.concat(prevLinePos); + fillOptions.hole = hole; + } + + fillOptions.opacity = trace.opacity; + fillOptions.positions = pos; + }); + + scene.fill2d.update(scene.fillOptions); + } + } + + // make sure selection layer is initialized if we require selection + var dragmode = layout.dragmode; + + if(dragmode === 'lasso' || dragmode === 'select') { + if(scene.select2d && scene.selectBatch) { + scene.scatter2d.update(scene.unselectedOptions); + } + } + + // provide viewport and range + var vpRange = cdata.map(function(cdscatter) { + if(!cdscatter || !cdscatter[0] || !cdscatter[0].trace) return; + var cd = cdscatter[0]; + var trace = cd.trace; + var stash = cd.t; + var x = stash.rawx, + y = stash.rawy; + var xaxis = Axes.getFromId(container, trace.xaxis || 'x'); + var yaxis = Axes.getFromId(container, trace.yaxis || 'y'); + var i; + + var range = [ + xaxis._rl[0], + yaxis._rl[0], + xaxis._rl[1], + yaxis._rl[1] + ]; + + var viewport = [ + vpSize.l + xaxis.domain[0] * vpSize.w, + vpSize.b + yaxis.domain[0] * vpSize.h, + (width - vpSize.r) - (1 - xaxis.domain[1]) * vpSize.w, + (height - vpSize.t) - (1 - yaxis.domain[1]) * vpSize.h + ]; + + if(trace.selectedpoints || dragmode === 'lasso' || dragmode === 'select') { + // create select2d + if(!scene.select2d && scene.scatter2d) { + var selectRegl = layout._glcanvas.data()[1].regl; + + // smol hack to create scatter instance by cloning scatter2d + scene.select2d = createScatter(selectRegl, {clone: scene.scatter2d}); + scene.select2d.update(scene.selectedOptions); + + // create selection style once we have something selected + if(trace.selectedpoints && !scene.selectBatch) { + scene.selectBatch = Array(scene.count); + scene.unselectBatch = Array(scene.count); + scene.scatter2d.update(scene.unselectedOptions); + } + } + + // form unselected batch + if(trace.selectedpoints && !scene.unselectBatch[stash.index]) { + scene.selectBatch[stash.index] = trace.selectedpoints; + var selPts = trace.selectedpoints; + var selDict = {}; + for(i = 0; i < selPts.length; i++) { + selDict[selPts[i]] = true; + } + var unselPts = []; + for(i = 0; i < stash.count; i++) { + if(!selDict[i]) unselPts.push(i); + } + scene.unselectBatch[stash.index] = unselPts; + } + + // precalculate px coords since we are not going to pan during select + var xpx = Array(stash.count), ypx = Array(stash.count); + for(i = 0; i < stash.count; i++) { + xpx[i] = xaxis.c2p(x[i]); + ypx[i] = yaxis.c2p(y[i]); + } + stash.xpx = xpx; + stash.ypx = ypx; + } + else { + stash.xpx = stash.ypx = null; + } + + return trace.visible ? { + viewport: viewport, + range: range + } : null; + }); + + // uploat batch data to GPU + if(scene.fill2d) { + scene.fill2d.update(vpRange); + } + if(scene.line2d) { + scene.line2d.update(vpRange); + } + if(scene.error2d) { + scene.error2d.update(vpRange.concat(vpRange)); + } + if(scene.scatter2d) { + scene.scatter2d.update(vpRange); + } + if(scene.select2d) { + scene.select2d.update(vpRange); + } + + scene.draw(); + + return; +}; + + +ScatterGl.hoverPoints = function hover(pointData, xval, yval, hovermode) { + var cd = pointData.cd, + stash = cd[0].t, + trace = cd[0].trace, + xa = pointData.xa, + ya = pointData.ya, + x = stash.rawx, + y = stash.rawy, + xpx = xa.c2p(xval), + ypx = ya.c2p(yval), + ids; + + // FIXME: make sure this is a proper way to calc search radius + if(stash.tree) { + if(hovermode === 'x') { + ids = stash.tree.range( + xa.p2c(xpx - MAXDIST), ya._rl[0], + xa.p2c(xpx + MAXDIST), ya._rl[1] + ); + } + else { + ids = stash.tree.range( + xa.p2c(xpx - MAXDIST), ya.p2c(ypx + MAXDIST), + xa.p2c(xpx + MAXDIST), ya.p2c(ypx - MAXDIST) + ); + } + } + else if(stash.ids) { + ids = stash.ids; + } + else return [pointData]; + + // pick the id closest to the point + // note that point possibly may not be found + var min = MAXDIST, id, ptx, pty, i, dx, dy, dist; + + if(hovermode === 'x') { + for(i = 0; i < ids.length; i++) { + ptx = x[ids[i]]; + dx = Math.abs(xa.c2p(ptx) - xpx); + if(dx < min) { + min = dx; + id = ids[i]; + } + } + } + else { + for(i = 0; i < ids.length; i++) { + ptx = x[ids[i]]; + pty = y[ids[i]]; + dx = xa.c2p(ptx) - xpx, dy = ya.c2p(pty) - ypx; + + dist = Math.sqrt(dx * dx + dy * dy); + if(dist < min) { + min = dist; + id = ids[i]; + } + } + } + + pointData.index = id; + + if(id === undefined) return [pointData]; + + // the closest data point + var di = { + pointNumber: id, + x: x[id], + y: y[id] + }; + + // that is single-item arrays_to_calcdata excerpt, since we are doing it for a single point and we don't have to do it beforehead for 1e6 points + di.tx = Array.isArray(trace.text) ? trace.text[id] : trace.text; + di.htx = Array.isArray(trace.hovertext) ? trace.hovertext[id] : trace.hovertext; + di.data = Array.isArray(trace.customdata) ? trace.customdata[id] : trace.customdata; + di.tp = Array.isArray(trace.textposition) ? trace.textposition[id] : trace.textposition; + + var font = trace.textfont; + if(font) { + di.ts = Array.isArray(font.size) ? font.size[id] : font.size; + di.tc = Array.isArray(font.color) ? font.color[id] : font.color; + di.tf = Array.isArray(font.family) ? font.family[id] : font.family; + } + + var marker = trace.marker; + if(marker) { + di.ms = Array.isArray(marker.size) ? marker.size[id] : marker.size; + di.mo = Array.isArray(marker.opacity) ? marker.opacity[id] : marker.opacity; + di.mx = Array.isArray(marker.symbol) ? marker.symbol[id] : marker.symbol; + di.mc = Array.isArray(marker.color) ? marker.color[id] : marker.color; + } + + var line = marker && marker.line; + if(line) { + di.mlc = Array.isArray(line.color) ? line.color[id] : line.color; + di.mlw = Array.isArray(line.width) ? line.width[id] : line.width; + } + + var grad = marker && marker.gradient; + if(grad && grad.type !== 'none') { + di.mgt = Array.isArray(grad.type) ? grad.type[id] : grad.type; + di.mgc = Array.isArray(grad.color) ? grad.color[id] : grad.color; + } + + var xc = xa.c2p(di.x, true), + yc = ya.c2p(di.y, true), + rad = di.mrc || 1; + + var hoverlabel = trace.hoverlabel; + + if(hoverlabel) { + di.hbg = Array.isArray(hoverlabel.bgcolor) ? hoverlabel.bgcolor[id] : hoverlabel.bgcolor; + di.hbc = Array.isArray(hoverlabel.bordercolor) ? hoverlabel.bordercolor[id] : hoverlabel.bordercolor; + di.hts = Array.isArray(hoverlabel.font.size) ? hoverlabel.font.size[id] : hoverlabel.font.size; + di.htc = Array.isArray(hoverlabel.font.color) ? hoverlabel.font.color[id] : hoverlabel.font.color; + di.htf = Array.isArray(hoverlabel.font.family) ? hoverlabel.font.family[id] : hoverlabel.font.family; + di.hnl = Array.isArray(hoverlabel.namelength) ? hoverlabel.namelength[id] : hoverlabel.namelength; + } + var hoverinfo = trace.hoverinfo; + if(hoverinfo) { + di.hi = Array.isArray(hoverinfo) ? hoverinfo[id] : hoverinfo; + } + + var fakeCd = {}; + fakeCd[pointData.index] = di; + + Lib.extendFlat(pointData, { + color: getTraceColor(trace, di), + + x0: xc - rad, + x1: xc + rad, + xLabelVal: di.x, + + y0: yc - rad, + y1: yc + rad, + yLabelVal: di.y, + + cd: fakeCd + }); + + if(di.htx) pointData.text = di.htx; + else if(di.tx) pointData.text = di.tx; + else if(trace.text) pointData.text = trace.text; + + fillHoverText(di, trace, pointData); + ErrorBars.hoverInfo(di, trace, pointData); + + return [pointData]; +}; + + +ScatterGl.selectPoints = function select(searchInfo, polygon) { + var cd = searchInfo.cd, + selection = [], + trace = cd[0].trace, + stash = cd[0].t, + x = stash.x, + y = stash.y; + + var scene = stash.scene; + + if(!scene) return selection; + + var hasOnlyLines = (!subTypes.hasMarkers(trace) && !subTypes.hasText(trace)); + if(trace.visible !== true || hasOnlyLines) return selection; + + // degenerate polygon does not enable selection + // filter out points by visible scatter ones + var els = null, unels = null, i; + if(polygon !== false && !polygon.degenerate) { + els = [], unels = []; + for(i = 0; i < stash.count; i++) { + if(polygon.contains([stash.xpx[i], stash.ypx[i]])) { + els.push(i); + selection.push({ + pointNumber: i, + x: x[i], + y: y[i] + }); + } + else { + unels.push(i); + } + } + } + else { + unels = Array(stash.count); + for(i = 0; i < stash.count; i++) { + unels[i] = i; + } + } + + // create selection style once we have something selected + if(!scene.selectBatch) { + scene.selectBatch = Array(scene.count); + scene.unselectBatch = Array(scene.count); + scene.scatter2d.update(scene.unselectedOptions); + } + scene.selectBatch[stash.index] = els; + scene.unselectBatch[stash.index] = unels; + + return selection; +}; + + +ScatterGl.style = function style(gd, cd) { + if(cd) { + var stash = cd[0].t; + var scene = stash.scene; + scene.clear(); + scene.draw(); + } +}; diff --git a/src/traces/scattergl/select.js b/src/traces/scattergl/select.js deleted file mode 100644 index e35b7ac4325..00000000000 --- a/src/traces/scattergl/select.js +++ /dev/null @@ -1,60 +0,0 @@ -/** -* Copyright 2012-2017, Plotly, Inc. -* All rights reserved. -* -* This source code is licensed under the MIT license found in the -* LICENSE file in the root directory of this source tree. -*/ - - -'use strict'; - -var subtypes = require('../scatter/subtypes'); - -module.exports = function selectPoints(searchInfo, polygon) { - var cd = searchInfo.cd, - xa = searchInfo.xaxis, - ya = searchInfo.yaxis, - selection = [], - trace = cd[0].trace, - i, - di, - x, - y; - - var glTrace = cd[0]._glTrace; - var scene = glTrace.scene; - - var hasOnlyLines = (!subtypes.hasMarkers(trace) && !subtypes.hasText(trace)); - if(hasOnlyLines) return []; - - // filter out points by visible scatter ones - if(polygon === false) { - // clear selection - for(i = 0; i < cd.length; i++) cd[i].dim = 0; - } - else { - for(i = 0; i < cd.length; i++) { - di = cd[i]; - x = xa.c2p(di.x); - y = ya.c2p(di.y); - if(polygon.contains([x, y])) { - selection.push({ - pointNumber: i, - x: xa.c2d(di.x), - y: ya.c2d(di.y) - }); - di.dim = 0; - } - else di.dim = 1; - } - } - - // highlight selected points here - trace.selection = selection; - - glTrace.update(trace, cd); - scene.glplot.setDirty(); - - return selection; -}; diff --git a/tasks/bundle.js b/tasks/bundle.js index b1a175d4654..dd767f018ff 100644 --- a/tasks/bundle.js +++ b/tasks/bundle.js @@ -33,6 +33,7 @@ _bundle(constants.pathToPlotlyIndex, constants.pathToPlotlyDist, { pathToMinBundle: constants.pathToPlotlyDistMin }); + // Browserify the geo assets _bundle(constants.pathToPlotlyGeoAssetsSrc, constants.pathToPlotlyGeoAssetsDist, { standalone: 'PlotlyGeoAssets' diff --git a/tasks/stats.js b/tasks/stats.js index 6478db4a47a..63227f1f5b4 100644 --- a/tasks/stats.js +++ b/tasks/stats.js @@ -1,6 +1,6 @@ var path = require('path'); var fs = require('fs'); -var spawn = require('child_process').spawn; +var spawn = require('cross-spawn'); var falafel = require('falafel'); var gzipSize = require('gzip-size'); diff --git a/tasks/util/browserify_wrapper.js b/tasks/util/browserify_wrapper.js index fa57092764e..65a8de6c07b 100644 --- a/tasks/util/browserify_wrapper.js +++ b/tasks/util/browserify_wrapper.js @@ -2,11 +2,10 @@ var fs = require('fs'); var path = require('path'); var browserify = require('browserify'); -var UglifyJS = require('uglify-js'); +var minify = require('minify-stream'); var constants = require('./constants'); var compressAttributes = require('./compress_attributes'); -var patchMinified = require('./patch_minified'); var strictD3 = require('./strict_d3'); /** Convenience browserify wrapper @@ -46,30 +45,28 @@ module.exports = function _bundle(pathToIndex, pathToBundle, opts) { } var b = browserify(pathToIndex, browserifyOpts); - var bundleWriteStream = fs.createWriteStream(pathToBundle); - bundleWriteStream.on('finish', function() { - logger(pathToBundle); - if(opts.then) { - opts.then(); - } - }); - - b.bundle(function(err, buf) { + var bundleStream = b.bundle(function(err) { if(err) throw err; + }); - if(outputMinified) { - var minifiedCode = UglifyJS.minify(buf.toString(), constants.uglifyOptions).code; - minifiedCode = patchMinified(minifiedCode); - - fs.writeFile(pathToMinBundle, minifiedCode, function(err) { - if(err) throw err; - + if(outputMinified) { + bundleStream + .pipe(minify(constants.uglifyOptions)) + .pipe(fs.createWriteStream(pathToMinBundle)) + .on('finish', function() { logger(pathToMinBundle); }); - } - }) - .pipe(bundleWriteStream); + } + + bundleStream + .pipe(fs.createWriteStream(pathToBundle)) + .on('finish', function() { + logger(pathToBundle); + if(opts.then) { + opts.then(); + } + }); }; function logger(pathToOutput) { diff --git a/tasks/util/constants.js b/tasks/util/constants.js index 1815c740616..46f0ba357d4 100644 --- a/tasks/util/constants.js +++ b/tasks/util/constants.js @@ -84,16 +84,15 @@ module.exports = { testContainerHome: '/var/www/streambed/image_server/plotly.js', uglifyOptions: { - fromString: true, mangle: true, compress: { - warnings: false, - screw_ie8: true + warnings: false }, output: { beautify: false, ascii_only: true - } + }, + sourceMap: false }, licenseDist: [ diff --git a/tasks/util/patch_minified.js b/tasks/util/patch_minified.js deleted file mode 100644 index e1388c71fa4..00000000000 --- a/tasks/util/patch_minified.js +++ /dev/null @@ -1,22 +0,0 @@ -var PATTERN = /require\("\+(\w)\((\w)\)\+"\)/; -var NEW_SUBSTR = 'require("+ $1($2) +")'; - -/* Uber hacky in-house fix to - * - * https://github.com/substack/webworkify/issues/29 - * - * so that plotly.min.js loads in Jupyter NBs, more info here: - * - * - https://github.com/plotly/plotly.py/pull/545 - * - https://github.com/plotly/plotly.js/pull/914 - * - https://github.com/plotly/plotly.js/pull/1094 - * - * For example, this routine replaces - * 'require("+o(s)+")' -> 'require("+ o(s) +")' - * - * But works for any 1-letter variable that uglify-js may output. - * - */ -module.exports = function patchMinified(minifiedCode) { - return minifiedCode.replace(PATTERN, NEW_SUBSTR); -}; diff --git a/test/image/baselines/gl2d_parcoords_2.png b/test/image/baselines/gl2d_parcoords_2.png index a31ada48935..91d01c98f60 100644 Binary files a/test/image/baselines/gl2d_parcoords_2.png and b/test/image/baselines/gl2d_parcoords_2.png differ diff --git a/test/image/mocks/gl2d_12.json b/test/image/mocks/gl2d_12.json index 01d477ea386..1dd1a744dad 100644 --- a/test/image/mocks/gl2d_12.json +++ b/test/image/mocks/gl2d_12.json @@ -529,6 +529,7 @@ 15389.924680000002, 20509.64777, 10808.47561, + 9101.25, 9786.534714, 18678.31435, 25768.25759, @@ -561,6 +562,7 @@ 75.563, 78.098, 72.476, + 67.59, 74.002, 74.663, 77.926, @@ -595,6 +597,7 @@ "Country: Poland
Life Expectancy: 75.563
GDP per capita: 15389.92468
Population: 38518241.0
Year: 2007", "Country: Portugal
Life Expectancy: 78.098
GDP per capita: 20509.64777
Population: 10642836.0
Year: 2007", "Country: Romania
Life Expectancy: 72.476
GDP per capita: 10808.47561
Population: 22276056.0
Year: 2007", + "Country: Russia
Life Expectancy: 67.59
GDP per capita: 9101.25
Population: 142800000.0
Year: 2007", "Country: Serbia
Life Expectancy: 74.002
GDP per capita: 9786.534714
Population: 10150265.0
Year: 2007", "Country: Slovak Republic
Life Expectancy: 74.663
GDP per capita: 18678.31435
Population: 5447502.0
Year: 2007", "Country: Slovenia
Life Expectancy: 77.926
GDP per capita: 25768.25759
Population: 2009245.0
Year: 2007", diff --git a/test/image/mocks/gl2d_ultra_zoom.json b/test/image/mocks/gl2d_ultra_zoom.json new file mode 100644 index 00000000000..ecb7f0f02dd --- /dev/null +++ b/test/image/mocks/gl2d_ultra_zoom.json @@ -0,0 +1,53 @@ +{ + "data": [ + { + "x": [ + 1.0e-2, + 1.0000001e-2, + 1.0000002e-2, + 1.0000003e-2, + 1.0000004e-2, + 1.0000005e-2, + 1.0000006e-2, + 1.0000007e-2, + 1.0000008e-2, + 1.0000009e-2, + 1.0000010e-2 + ], + "y": [ + 1.0e-2, + 1.0000001e-2, + 1.0000002e-2, + 1.0000003e-2, + 1.0000004e-2, + 1.0000005e-2, + 1.0000006e-2, + 1.0000007e-2, + 1.0000008e-2, + 1.0000009e-2, + 1.0000010e-2 + ], + "mode": "markers", + "type": "scatter" + } + ], + "layout": { + "autosize": true, + "xaxis": { + "range": [ + 1.0e-2, + 1.0000010e-2 + ], + "type": "linear", + "autorange": false + }, + "yaxis": { + "range": [ + 1.0e-2, + 1.0000010e-2 + ], + "type": "linear", + "autorange": false + } + } +} diff --git a/test/jasmine/assets/read_pixel.js b/test/jasmine/assets/read_pixel.js new file mode 100644 index 00000000000..fadc705079c --- /dev/null +++ b/test/jasmine/assets/read_pixel.js @@ -0,0 +1,13 @@ +'use strict'; + +module.exports = function(canvas, x, y) { + if(!canvas) return null; + + var gl = canvas.getContext('webgl'); + + var pixels = new Uint8Array(4); + + gl.readPixels(x, y, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, pixels); + + return pixels; +}; diff --git a/test/jasmine/tests/axes_test.js b/test/jasmine/tests/axes_test.js index fb269708016..c7b8812294f 100644 --- a/test/jasmine/tests/axes_test.js +++ b/test/jasmine/tests/axes_test.js @@ -289,7 +289,7 @@ describe('Test axes', function() { }]; supplyLayoutDefaults(layoutIn, layoutOut, fullData); - expect(layoutOut._basePlotModules).toEqual([]); + expect(layoutOut._basePlotModules[0].name).toEqual('cartesian'); }); it('should detect orphan axes (gl2d + cartesian case)', function() { diff --git a/test/jasmine/tests/gl2d_click_test.js b/test/jasmine/tests/gl2d_click_test.js index 70609851a15..593d896d03a 100644 --- a/test/jasmine/tests/gl2d_click_test.js +++ b/test/jasmine/tests/gl2d_click_test.js @@ -194,6 +194,7 @@ describe('Test hover and click interactions', function() { } }; _mock.data[0].hoverinfo = _mock.data[0].x.map(function(_, i) { return i % 2 ? 'y' : 'x'; }); + _mock.data[0].hoverlabel = { bgcolor: 'blue', bordercolor: _mock.data[0].x.map(function(_, i) { return i % 2 ? 'red' : 'green'; }) @@ -357,11 +358,11 @@ describe('Test hover and click interactions', function() { y: 18, curveNumber: 2, pointNumber: 0, - bgcolor: 'rgb(255, 127, 14)', - bordercolor: 'rgb(68, 68, 68)', + bgcolor: 'rgb(44, 160, 44)', + bordercolor: 'rgb(255, 255, 255)', fontSize: 13, fontFamily: 'Arial', - fontColor: 'rgb(68, 68, 68)' + fontColor: 'rgb(255, 255, 255)' }, { msg: 'scattergl after visibility restyle' }); @@ -405,11 +406,11 @@ describe('Test hover and click interactions', function() { y: 18, curveNumber: 2, pointNumber: 0, - bgcolor: 'rgb(255, 127, 14)', - bordercolor: 'rgb(68, 68, 68)', + bgcolor: 'rgb(44, 160, 44)', + bordercolor: 'rgb(255, 255, 255)', fontSize: 13, fontFamily: 'Arial', - fontColor: 'rgb(68, 68, 68)' + fontColor: 'rgb(255, 255, 255)' }, { msg: 'scattergl fancy after visibility restyle' }); @@ -464,7 +465,7 @@ describe('@noCI Test gl2d lasso/select:', function() { }); var gd; - var selectPath = [[93, 193], [143, 193]]; + var selectPath = [[103, 193], [113, 193]]; var lassoPath = [[316, 171], [318, 239], [335, 243], [328, 169]]; var lassoPath2 = [[93, 193], [143, 193], [143, 500], [93, 500], [93, 193]]; @@ -508,9 +509,6 @@ describe('@noCI Test gl2d lasso/select:', function() { }); } - function countGlObjects() { - return gd._fullLayout._plots.xy._scene2d.glplot.objects.length; - } it('should work under fast mode with *select* dragmode', function(done) { var _mock = Lib.extendDeep({}, mockFast); @@ -520,19 +518,19 @@ describe('@noCI Test gl2d lasso/select:', function() { Plotly.plot(gd, _mock) .then(delay(100)) .then(function() { - expect(countGlObjects()).toBe(1, 'has on gl-scatter2d object'); + expect(gd._fullLayout._plots.xy._scene.select2d).not.toBe(undefined, 'scatter2d renderer'); return select(selectPath); }) .then(function(eventData) { assertEventData(eventData, { points: [ - {x: 3.911, y: 0.401}, - {x: 5.34, y: 0.403}, - {x: 6.915, y: 0.411} + {pointNumber: 25, x: 1.425, y: 0.538}, + {pointNumber: 26, x: 1.753, y: 0.5}, + {pointNumber: 27, x: 2.22, y: 0.45} ] }); - expect(countGlObjects()).toBe(2, 'adds a dimmed gl-scatter2d objects'); + }) .catch(fail) .then(done); @@ -546,19 +544,16 @@ describe('@noCI Test gl2d lasso/select:', function() { Plotly.plot(gd, _mock) .then(delay(100)) .then(function() { - expect(countGlObjects()).toBe(1); - return select(lassoPath2); }) .then(function(eventData) { assertEventData(eventData, { points: [ - {x: 3.911, y: 0.401}, - {x: 5.34, y: 0.403}, - {x: 6.915, y: 0.411} + {pointNumber: 25, x: 1.425, y: 0.538}, + {pointNumber: 26, x: 1.753, y: 0.5}, + {pointNumber: 27, x: 2.22, y: 0.45} ] }); - expect(countGlObjects()).toBe(2); }) .catch(fail) .then(done); @@ -572,15 +567,12 @@ describe('@noCI Test gl2d lasso/select:', function() { Plotly.plot(gd, _mock) .then(delay(100)) .then(function() { - expect(countGlObjects()).toBe(2, 'has a gl-line2d and a gl-scatter2d-sdf'); - return select(selectPath); }) .then(function(eventData) { assertEventData(eventData, { points: [{x: 0.004, y: 12.5}] }); - expect(countGlObjects()).toBe(2, 'only changes colors of gl-scatter2d-sdf object'); }) .catch(fail) .then(done); @@ -594,15 +586,12 @@ describe('@noCI Test gl2d lasso/select:', function() { Plotly.plot(gd, _mock) .then(delay(100)) .then(function() { - expect(countGlObjects()).toBe(2, 'has a gl-line2d and a gl-scatter2d-sdf'); - return select(lassoPath); }) .then(function(eventData) { assertEventData(eventData, { points: [{ x: 0.099, y: 2.75 }] }); - expect(countGlObjects()).toBe(2, 'only changes colors of gl-scatter2d-sdf object'); }) .catch(fail) .then(done); diff --git a/test/jasmine/tests/gl2d_date_axis_render_test.js b/test/jasmine/tests/gl2d_date_axis_render_test.js index 7a7f5d8a173..d3eff9200e3 100644 --- a/test/jasmine/tests/gl2d_date_axis_render_test.js +++ b/test/jasmine/tests/gl2d_date_axis_render_test.js @@ -31,12 +31,13 @@ describe('date axis', function() { expect(gd._fullLayout.xaxis.type).toBe('date'); expect(gd._fullLayout.yaxis.type).toBe('linear'); expect(gd._fullData[0].type).toBe('scattergl'); - expect(gd._fullData[0]._module.basePlotModule.name).toBe('gl2d'); + expect(gd._fullData[0]._module.basePlotModule.name).toBe('cartesian'); - // one way of check which renderer - fancy vs not - we're using - var objs = gd._fullLayout._plots.xy._scene2d.glplot.objects; - expect(objs.length).toEqual(2); - expect(objs[1].points.length).toEqual(4); + // one way of check which renderer - fancy vs not - we're + var scene = gd._fullLayout._plots.xy._scene; + expect(scene.scatter2d).toBeDefined(); + expect(scene.markerOptions[0].positions.length).toEqual(4); + expect(scene.line2d).not.toBeUndefined(); }); it('should use the fancy gl-vis/gl-scatter2d once again', function() { @@ -57,18 +58,18 @@ describe('date axis', function() { expect(gd._fullLayout.xaxis.type).toBe('date'); expect(gd._fullLayout.yaxis.type).toBe('linear'); expect(gd._fullData[0].type).toBe('scattergl'); - expect(gd._fullData[0]._module.basePlotModule.name).toBe('gl2d'); + expect(gd._fullData[0]._module.basePlotModule.name).toBe('cartesian'); - // one way of check which renderer - fancy vs not - we're using - var objs = gd._fullLayout._plots.xy._scene2d.glplot.objects; - expect(objs.length).toEqual(2); - expect(objs[1].points.length).toEqual(4); + var scene = gd._fullLayout._plots.xy._scene; + expect(scene.scatter2d).toBeDefined(); + expect(scene.markerOptions[0].positions.length).toEqual(4); + expect(scene.line2d).toBeDefined(); }); it('should now use the non-fancy gl-vis/gl-scatter2d', function() { Plotly.plot(gd, [{ type: 'scattergl', - mode: 'markers', // important, as otherwise lines are assumed (which needs fancy) + mode: 'markers', x: [new Date('2016-10-10'), new Date('2016-10-11')], y: [15, 16] }]); @@ -76,17 +77,18 @@ describe('date axis', function() { expect(gd._fullLayout.xaxis.type).toBe('date'); expect(gd._fullLayout.yaxis.type).toBe('linear'); expect(gd._fullData[0].type).toBe('scattergl'); - expect(gd._fullData[0]._module.basePlotModule.name).toBe('gl2d'); + expect(gd._fullData[0]._module.basePlotModule.name).toBe('cartesian'); - var objs = gd._fullLayout._plots.xy._scene2d.glplot.objects; - expect(objs.length).toEqual(1); - expect(objs[0].pointCount).toEqual(2); + var scene = gd._fullLayout._plots.xy._scene; + expect(scene.scatter2d).toBeDefined(); + expect(scene.markerOptions[0].positions.length).toEqual(4); + expect(scene.line2d).toBeDefined(); }); it('should use the non-fancy gl-vis/gl-scatter2d with string dates', function() { Plotly.plot(gd, [{ type: 'scattergl', - mode: 'markers', // important, as otherwise lines are assumed (which needs fancy) + mode: 'markers', x: ['2016-10-10', '2016-10-11'], y: [15, 16] }]); @@ -94,10 +96,11 @@ describe('date axis', function() { expect(gd._fullLayout.xaxis.type).toBe('date'); expect(gd._fullLayout.yaxis.type).toBe('linear'); expect(gd._fullData[0].type).toBe('scattergl'); - expect(gd._fullData[0]._module.basePlotModule.name).toBe('gl2d'); + expect(gd._fullData[0]._module.basePlotModule.name).toBe('cartesian'); - var objs = gd._fullLayout._plots.xy._scene2d.glplot.objects; - expect(objs.length).toEqual(1); - expect(objs[0].pointCount).toEqual(2); + var scene = gd._fullLayout._plots.xy._scene; + expect(scene.scatter2d).toBeDefined(); + expect(scene.markerOptions[0].positions.length).toEqual(4); + expect(scene.line2d).toBeDefined(); }); }); diff --git a/test/jasmine/tests/gl_plot_interact_test.js b/test/jasmine/tests/gl_plot_interact_test.js index f52f2d80485..1c76f573f9c 100644 --- a/test/jasmine/tests/gl_plot_interact_test.js +++ b/test/jasmine/tests/gl_plot_interact_test.js @@ -11,6 +11,7 @@ var fail = require('../assets/fail_test'); var mouseEvent = require('../assets/mouse_event'); var selectButton = require('../assets/modebar_button'); var delay = require('../assets/delay'); +var readPixel = require('../assets/read_pixel'); var customAssertions = require('../assets/custom_assertions'); var assertHoverLabelStyle = customAssertions.assertHoverLabelStyle; @@ -839,6 +840,7 @@ describe('Test gl2d plots', function() { beforeEach(function() { gd = createGraphDiv(); + jasmine.DEFAULT_TIMEOUT_INTERVAL = 3000; }); afterEach(function() { @@ -855,12 +857,13 @@ describe('Test gl2d plots', function() { it('should respond to drag interactions', function(done) { var _mock = Lib.extendDeep({}, mock); + var relayoutCallback = jasmine.createSpy('relayoutCallback'); var originalX = [-0.3037383177570093, 5.303738317757009]; var originalY = [-0.5532219548705213, 6.191112269783224]; var newX = [-0.5373831775700935, 5.070093457943925]; - var newY = [-1.7575673521301187, 4.986766872523626]; + var newY = [-1.7575673521301185, 4.986766872523626]; var precision = 5; Plotly.plot(gd, _mock) @@ -931,9 +934,10 @@ describe('Test gl2d plots', function() { // a callback value structure and contents check expect(relayoutCallback).toHaveBeenCalledWith(jasmine.objectContaining({ - lastInputTime: jasmine.any(Number), - xaxis: [jasmine.any(Number), jasmine.any(Number)], - yaxis: [jasmine.any(Number), jasmine.any(Number)] + 'xaxis.range[0]': jasmine.any(Number), + 'xaxis.range[1]': jasmine.any(Number), + 'yaxis.range[0]': jasmine.any(Number), + 'yaxis.range[1]': jasmine.any(Number) })); }) .then(done); @@ -942,60 +946,53 @@ describe('Test gl2d plots', function() { it('should be able to toggle visibility', function(done) { var _mock = Lib.extendDeep({}, mock); - // a line object + scatter fancy - var OBJECT_PER_TRACE = 2; - - var objects = function() { - return gd._fullLayout._plots.xy._scene2d.glplot.objects; - }; - Plotly.plot(gd, _mock) .then(delay(20)) .then(function() { - expect(objects().length).toEqual(OBJECT_PER_TRACE); - return Plotly.restyle(gd, 'visible', 'legendonly'); }) .then(function() { - expect(objects().length).toEqual(OBJECT_PER_TRACE); - expect(objects()[0].data.length).toEqual(0); + expect(readPixel(gd.querySelector('.gl-canvas-context'), 108, 100)[0]).toBe(0); return Plotly.restyle(gd, 'visible', true); }) .then(function() { - expect(objects().length).toEqual(OBJECT_PER_TRACE); - expect(objects()[0].data.length).not.toEqual(0); + expect(readPixel(gd.querySelector('.gl-canvas-context'), 108, 100)[0]).not.toBe(0); return Plotly.restyle(gd, 'visible', false); }) .then(function() { - expect(gd._fullLayout._plots.xy._scene2d).toBeUndefined(); + expect(gd.querySelector('.gl-canvas-context')).not.toBe(null); return Plotly.restyle(gd, 'visible', true); }) .then(function() { - expect(objects().length).toEqual(OBJECT_PER_TRACE); - expect(objects()[0].data.length).not.toEqual(0); + expect(readPixel(gd.querySelector('.gl-canvas-context'), 108, 100)[0]).not.toBe(0); }) .then(done); }); - it('should clear orphan cartesian subplots on addTraces', function(done) { - Plotly.newPlot(gd, [], { - xaxis: { title: 'X' }, - yaxis: { title: 'Y' } + it('should be able to toggle from svg to gl', function(done) { + Plotly.plot(gd, [{ + y: [1, 2, 1], + }]) + .then(function() { + expect(countCanvases()).toBe(0); + expect(d3.selectAll('.scatterlayer > .trace').size()).toBe(1); + + return Plotly.restyle(gd, 'type', 'scattergl'); }) .then(function() { - return Plotly.addTraces(gd, [{ - type: 'scattergl', - x: [1, 2, 3, 4, 5, 6, 7], - y: [0, 5, 8, 9, 8, 5, 0] - }]); + expect(countCanvases()).toBe(3); + expect(d3.selectAll('.scatterlayer > .trace').size()).toBe(0); + + return Plotly.restyle(gd, 'type', 'scatter'); }) .then(function() { - expect(d3.select('.xtitle').size()).toEqual(0); - expect(d3.select('.ytitle').size()).toEqual(0); + expect(countCanvases()).toBe(0); + expect(d3.selectAll('.scatterlayer > .trace').size()).toBe(1); }) + .catch(fail) .then(done); }); @@ -1033,8 +1030,8 @@ describe('Test gl2d plots', function() { // no change - too small mouseTo([centerX, centerY], [centerX - 5, centerY + 5]); - expect(gd.layout.xaxis.range).toBeCloseToArray([6, 8], 3); - expect(gd.layout.yaxis.range).toBeCloseToArray([5, 7], 3); + expect(gd.layout.xaxis.range).toBeCloseToArray([0, 16], 3); + expect(gd.layout.yaxis.range).toBeCloseToArray([0, 16], 3); }) .catch(fail) .then(done); @@ -1080,8 +1077,8 @@ describe('Test gl2d plots', function() { // no change - too small mouseTo([centerX, centerY], [centerX - 5, centerY + 5]); - expect(gd.layout.xaxis.range).toBeCloseToArray([4, 6], 3); - expect(gd.layout.yaxis.range).toBeCloseToArray([9, 10], 3); + expect(gd.layout.xaxis.range).toBeCloseToArray([-8, 24], 3); + expect(gd.layout.yaxis.range).toBeCloseToArray([0, 16], 3); return Plotly.relayout(gd, { 'xaxis.autorange': true, @@ -1089,8 +1086,8 @@ describe('Test gl2d plots', function() { }); }) .then(function() { - expect(gd.layout.xaxis.range).toBeCloseToArray([-8.09195, 24.09195], 3); - expect(gd.layout.yaxis.range).toBeCloseToArray([-0.04598, 16.04598], 3); + expect(gd.layout.xaxis.range).toBeCloseToArray([-8.091954022988505, 24.091954022988503], 3); + expect(gd.layout.yaxis.range).toBeCloseToArray([-0.04597701149425282, 16.04597701149425], 3); }) .catch(fail) .then(done); @@ -1098,7 +1095,6 @@ describe('Test gl2d plots', function() { it('should change plot type with incomplete data', function(done) { Plotly.plot(gd, [{}]); - expect(function() { Plotly.restyle(gd, {type: 'scattergl', x: [[1]]}, 0); }).not.toThrow(); @@ -1143,10 +1139,10 @@ describe('Test removal of gl contexts', function() { y: [2, 1, 3] }]) .then(function() { - expect(gd._fullLayout._plots.xy._scene2d.glplot).toBeDefined(); - + expect(gd._fullLayout._plots.xy._scene).toBeDefined(); Plots.cleanPlot([], {}, gd._fullData, gd._fullLayout); - expect(gd._fullLayout._plots).toEqual({}); + + expect(gd._fullLayout._plots.xy._scene).toBeUndefined(); }) .then(done); }); @@ -1204,8 +1200,8 @@ describe('Test removal of gl contexts', function() { y: [2, 1, 3] }]) .then(function() { - firstGlplotObject = gd._fullLayout._plots.xy._scene2d.glplot; - firstGlContext = firstGlplotObject.gl; + firstGlplotObject = gd._fullLayout._plots.xy._scene; + firstGlContext = firstGlplotObject.scatter2d.gl; firstCanvas = firstGlContext.canvas; expect(firstGlplotObject).toBeDefined(); @@ -1219,8 +1215,8 @@ describe('Test removal of gl contexts', function() { }], {}); }) .then(function() { - var secondGlplotObject = gd._fullLayout._plots.xy._scene2d.glplot; - var secondGlContext = secondGlplotObject.gl; + var secondGlplotObject = gd._fullLayout._plots.xy._scene; + var secondGlContext = secondGlplotObject.scatter2d.gl; var secondCanvas = secondGlContext.canvas; expect(Object.keys(gd._fullLayout._plots).length === 1); @@ -1285,27 +1281,33 @@ describe('Test gl plot side effects', function() { y: [2, 1, 2] }]; - Plotly.plot(gd, []).then(function() { + Plotly.plot(gd, []) + .then(function() { countCanvases(0); return Plotly.plot(gd, data); - }).then(function() { + }) + .then(function() { countCanvases(3); return Plotly.purge(gd); - }).then(function() { + }) + .then(function() { countCanvases(0); return Plotly.plot(gd, data); - }).then(function() { + }) + .then(function() { countCanvases(3); return Plotly.deleteTraces(gd, [0]); - }).then(function() { + }) + .then(function() { countCanvases(0); return Plotly.purge(gd); - }).then(done); + }) + .then(done); }); it('should be able to switch trace type', function(done) { @@ -1357,8 +1359,8 @@ describe('Test gl2d interactions', function() { var ann = d3.select('g.annotation-text-g').select('g'); var translate = Drawing.getTranslate(ann); - expect(translate.x).toBeWithin(xy[0], 1.5); - expect(translate.y).toBeWithin(xy[1], 1.5); + expect(translate.x).toBeWithin(xy[0], 3.5); + expect(translate.y).toBeWithin(xy[1], 3.5); } Plotly.plot(gd, [{ @@ -1374,10 +1376,10 @@ describe('Test gl2d interactions', function() { dragmode: 'pan' }) .then(function() { - assertAnnotation([327, 315]); + assertAnnotation([327, 312]); drag([250, 200], [200, 150]); - assertAnnotation([277, 265]); + assertAnnotation([277, 262]); return Plotly.relayout(gd, { 'xaxis.range': [1.5, 2.5],