diff --git a/src/components/fx/hover.js b/src/components/fx/hover.js index 73ec3f3abca..fd798c3674c 100644 --- a/src/components/fx/hover.js +++ b/src/components/fx/hover.js @@ -396,6 +396,10 @@ function _hover(gd, evt, subplot, noHoverEvent) { if(fullLayout[subplotId]) { pointData.subplot = fullLayout[subplotId]._subplot; } + // add ref to splom scene + if(fullLayout._splomScenes && fullLayout._splomScenes[trace.uid]) { + pointData.scene = fullLayout._splomScenes[trace.uid]; + } closedataPreviousLength = hoverData.length; diff --git a/src/plot_api/edit_types.js b/src/plot_api/edit_types.js index 83cc9c8655b..b31724cd874 100644 --- a/src/plot_api/edit_types.js +++ b/src/plot_api/edit_types.js @@ -15,7 +15,7 @@ var isPlainObject = Lib.isPlainObject; var traceOpts = { valType: 'flaglist', extras: ['none'], - flags: ['calc', 'clearAxisTypes', 'plot', 'style', 'colorbars'], + flags: ['calc', 'clearAxisTypes', 'plot', 'style', 'markerSize', 'colorbars'], description: [ 'trace attributes should include an `editType` string matching this flaglist.', '*calc* is the most extensive: a full `Plotly.plot` starting by clearing `gd.calcdata`', @@ -24,7 +24,8 @@ var traceOpts = { 'cause the automatic axis type detection to change. Log type will not be cleared, as that', 'is never automatically chosen so must have been user-specified.', '*plot* calls `Plotly.plot` but without first clearing `gd.calcdata`.', - '*style* only calls `module.style` for all trace modules and redraws the legend.', + '*style* only calls `module.style` (or module.editStyle) for all trace modules and redraws the legend.', + '*markerSize* is like *style*, but propagate axis-range changes due to scatter `marker.size`', '*colorbars* only redraws colorbars.' ].join(' ') }; diff --git a/src/plot_api/plot_api.js b/src/plot_api/plot_api.js index 4fe0a28da59..b887aef9d07 100644 --- a/src/plot_api/plot_api.js +++ b/src/plot_api/plot_api.js @@ -269,7 +269,7 @@ exports.plot = function(gd, data, layout, config) { Lib.error(msg); } else { Lib.log(msg + ' Clearing graph and plotting again.'); - Plots.cleanPlot([], {}, gd._fullData, fullLayout, gd.calcdata); + Plots.cleanPlot([], {}, gd._fullData, fullLayout); Plots.supplyDefaults(gd); fullLayout = gd._fullLayout; Plots.doCalcdata(gd); @@ -614,7 +614,7 @@ exports.newPlot = function(gd, data, layout, config) { gd = Lib.getGraphDiv(gd); // remove gl contexts - Plots.cleanPlot([], {}, gd._fullData || [], gd._fullLayout || {}, gd.calcdata || []); + Plots.cleanPlot([], {}, gd._fullData || [], gd._fullLayout || {}); Plots.purge(gd); return exports.plot(gd, data, layout, config); @@ -1344,8 +1344,21 @@ exports.restyle = function restyle(gd, astr, val, _traces) { } else { seq.push(Plots.previousPromises); + // maybe only call Plots.supplyDataDefaults in the splom case, + // to skip over long and slow axes defaults Plots.supplyDefaults(gd); + if(flags.markerSize) { + Plots.doCalcdata(gd); + addAxRangeSequence(seq); + + // TODO + // if all axes have autorange:false, then + // proceed to subroutines.doTraceStyle(), + // otherwise we must go through addAxRangeSequence, + // which in general must redraws 'all' axes + } + if(flags.style) seq.push(subroutines.doTraceStyle); if(flags.colorbars) seq.push(subroutines.doColorBars); @@ -1572,8 +1585,10 @@ function _restyle(gd, aobj, traces) { else { if(valObject) { // must redo calcdata when restyling array values of arrayOk attributes - if(valObject.arrayOk && ( - Lib.isArrayOrTypedArray(newVal) || Lib.isArrayOrTypedArray(oldVal)) + // ... but no need to this for regl-based traces + if(valObject.arrayOk && + !Registry.traceIs(contFull, 'regl') && + (Lib.isArrayOrTypedArray(newVal) || Lib.isArrayOrTypedArray(oldVal)) ) { flags.calc = true; } @@ -1701,15 +1716,11 @@ exports.relayout = function relayout(gd, astr, val) { seq.push(subroutines.layoutReplot); } else if(Object.keys(aobj).length) { - Plots.supplyDefaults(gd); + axRangeSupplyDefaultsByPass(gd, flags, specs) || Plots.supplyDefaults(gd); if(flags.legend) seq.push(subroutines.doLegend); if(flags.layoutstyle) seq.push(subroutines.layoutStyles); - - if(flags.axrange) { - addAxRangeSequence(seq, specs.rangesAltered); - } - + if(flags.axrange) addAxRangeSequence(seq, specs.rangesAltered); if(flags.ticks) seq.push(subroutines.doTicksRelayout); if(flags.modebar) seq.push(subroutines.doModeBar); if(flags.camera) seq.push(subroutines.doCamera); @@ -1733,13 +1744,35 @@ exports.relayout = function relayout(gd, astr, val) { }); }; +// Optimization mostly for large splom traces where +// Plots.supplyDefaults can take > 100ms +function axRangeSupplyDefaultsByPass(gd, flags, specs) { + var k; + + if(!flags.axrange) return false; + + for(k in flags) { + if(k !== 'axrange' && flags[k]) return false; + } + + for(k in specs.rangesAltered) { + var axName = Axes.id2name(k); + var axIn = gd.layout[axName]; + var axOut = gd._fullLayout[axName]; + axOut.autorange = axIn.autorange; + axOut.range = axIn.range.slice(); + axOut.cleanRange(); + } + return true; +} + function addAxRangeSequence(seq, rangesAltered) { // N.B. leave as sequence of subroutines (for now) instead of // subroutine of its own so that finalDraw always gets // executed after drawData var doTicks = rangesAltered ? - function(gd) { return subroutines.doTicksRelayout(gd, rangesAltered); } : - subroutines.doTicksRelayout; + function(gd) { return Axes.doTicks(gd, Object.keys(rangesAltered), true); } : + function(gd) { return Axes.doTicks(gd, 'redraw'); }; seq.push( subroutines.doAutoRangeAndConstraints, @@ -2156,15 +2189,13 @@ exports.update = function update(gd, traceUpdate, layoutUpdate, _traces) { } else { seq.push(Plots.previousPromises); - Plots.supplyDefaults(gd); + axRangeSupplyDefaultsByPass(gd, relayoutFlags, relayoutSpecs) || Plots.supplyDefaults(gd); if(restyleFlags.style) seq.push(subroutines.doTraceStyle); if(restyleFlags.colorbars) seq.push(subroutines.doColorBars); if(relayoutFlags.legend) seq.push(subroutines.doLegend); if(relayoutFlags.layoutstyle) seq.push(subroutines.layoutStyles); - if(relayoutFlags.axrange) { - addAxRangeSequence(seq, relayoutSpecs.rangesAltered); - } + if(relayoutFlags.axrange) addAxRangeSequence(seq, relayoutSpecs.rangesAltered); if(relayoutFlags.ticks) seq.push(subroutines.doTicksRelayout); if(relayoutFlags.modebar) seq.push(subroutines.doModeBar); if(relayoutFlags.camera) seq.push(subroutines.doCamera); @@ -3168,10 +3199,9 @@ exports.purge = function purge(gd) { var fullLayout = gd._fullLayout || {}; var fullData = gd._fullData || []; - var calcdata = gd.calcdata || []; // remove gl contexts - Plots.cleanPlot([], {}, fullData, fullLayout, calcdata); + Plots.cleanPlot([], {}, fullData, fullLayout); // purge properties Plots.purge(gd); diff --git a/src/plot_api/subroutines.js b/src/plot_api/subroutines.js index aa185cbf328..8827d4846fb 100644 --- a/src/plot_api/subroutines.js +++ b/src/plot_api/subroutines.js @@ -51,12 +51,25 @@ function lsInner(gd) { var gs = fullLayout._size; var pad = gs.p; var axList = Axes.list(gd, '', true); + var i, subplot, plotinfo, xa, ya; + + fullLayout._paperdiv.style({ + width: fullLayout.width + 'px', + height: fullLayout.height + 'px' + }) + .selectAll('.main-svg') + .call(Drawing.setSize, fullLayout.width, fullLayout.height); + gd._context.setBackground(gd, fullLayout.paper_bgcolor); + + exports.drawMainTitle(gd); + ModeBar.manage(gd); // _has('cartesian') means SVG specifically, not GL2D - but GL2D // can still get here because it makes some of the SVG structure // for shared features like selections. - var hasSVGCartesian = fullLayout._has('cartesian'); - var i; + if(!fullLayout._has('cartesian')) { + return gd._promises.length && Promise.all(gd._promises); + } function getLinePosition(ax, counterAx, side) { var lwHalf = ax._lw / 2; @@ -103,25 +116,22 @@ function lsInner(gd) { ax._mainSubplot = findMainSubplot(ax, fullLayout); } - fullLayout._paperdiv - .style({ - width: fullLayout.width + 'px', - height: fullLayout.height + 'px' - }) - .selectAll('.main-svg') - .call(Drawing.setSize, fullLayout.width, fullLayout.height); - - gd._context.setBackground(gd, fullLayout.paper_bgcolor); - - var subplotSelection = fullLayout._paper.selectAll('g.subplot'); - - // figure out which backgrounds we need to draw, and in which layers - // to put them + // figure out which backgrounds we need to draw, + // and in which layers to put them var lowerBackgroundIDs = []; + var backgroundIds = []; var lowerDomains = []; - subplotSelection.each(function(d) { - var subplot = d[0]; - var plotinfo = fullLayout._plots[subplot]; + // no need to draw background when paper and plot color are the same color, + // activate mode just for large splom (which benefit the most from this + // optimization), but this could apply to all cartesian subplots. + var noNeedForBg = ( + Color.opacity(fullLayout.paper_bgcolor) === 1 && + Color.opacity(fullLayout.plot_bgcolor) === 1 && + fullLayout.paper_bgcolor === fullLayout.plot_bgcolor + ); + + for(subplot in fullLayout._plots) { + plotinfo = fullLayout._plots[subplot]; if(plotinfo.mainplot) { // mainplot is a reference to the main plot this one is overlaid on @@ -131,23 +141,26 @@ function lsInner(gd) { plotinfo.bg.remove(); } plotinfo.bg = undefined; - return; - } - - var xDomain = plotinfo.xaxis.domain; - var yDomain = plotinfo.yaxis.domain; - var plotgroup = plotinfo.plotgroup; - - if(overlappingDomain(xDomain, yDomain, lowerDomains)) { - var pgNode = plotgroup.node(); - var plotgroupBg = plotinfo.bg = Lib.ensureSingle(plotgroup, 'rect', 'bg'); - pgNode.insertBefore(plotgroupBg.node(), pgNode.childNodes[0]); } else { - plotgroup.select('rect.bg').remove(); - lowerBackgroundIDs.push(subplot); - lowerDomains.push([xDomain, yDomain]); + var xDomain = plotinfo.xaxis.domain; + var yDomain = plotinfo.yaxis.domain; + var plotgroup = plotinfo.plotgroup; + + if(overlappingDomain(xDomain, yDomain, lowerDomains)) { + var pgNode = plotgroup.node(); + var plotgroupBg = plotinfo.bg = Lib.ensureSingle(plotgroup, 'rect', 'bg'); + pgNode.insertBefore(plotgroupBg.node(), pgNode.childNodes[0]); + backgroundIds.push(subplot); + } else { + plotgroup.select('rect.bg').remove(); + lowerDomains.push([xDomain, yDomain]); + if(!noNeedForBg) { + lowerBackgroundIDs.push(subplot); + backgroundIds.push(subplot); + } + } } - }); + } // now create all the lower-layer backgrounds at once now that // we have the list of subplots that need them @@ -163,13 +176,13 @@ function lsInner(gd) { fullLayout._plots[subplot].bg = d3.select(this); }); - subplotSelection.each(function(d) { - var subplot = d[0]; - var plotinfo = fullLayout._plots[subplot]; - var xa = plotinfo.xaxis; - var ya = plotinfo.yaxis; + // style all backgrounds + for(i = 0; i < backgroundIds.length; i++) { + plotinfo = fullLayout._plots[backgroundIds[i]]; + xa = plotinfo.xaxis; + ya = plotinfo.yaxis; - if(plotinfo.bg && hasSVGCartesian) { + if(plotinfo.bg) { plotinfo.bg .call(Drawing.setRect, xa._offset - pad, ya._offset - pad, @@ -177,72 +190,83 @@ function lsInner(gd) { .call(Color.fill, fullLayout.plot_bgcolor) .style('stroke-width', 0); } + } - // Clip so that data only shows up on the plot area. - var clipId = plotinfo.clipId = 'clip' + fullLayout._uid + subplot + 'plot'; + if(!fullLayout._hasOnlyLargeSploms) { + for(subplot in fullLayout._plots) { + plotinfo = fullLayout._plots[subplot]; + xa = plotinfo.xaxis; + ya = plotinfo.yaxis; - var plotClip = Lib.ensureSingleById(fullLayout._clips, 'clipPath', clipId, function(s) { - s.classed('plotclip', true) - .append('rect'); - }); + // Clip so that data only shows up on the plot area. + var clipId = plotinfo.clipId = 'clip' + fullLayout._uid + subplot + 'plot'; - plotinfo.clipRect = plotClip.select('rect').attr({ - width: xa._length, - height: ya._length - }); + var plotClip = Lib.ensureSingleById(fullLayout._clips, 'clipPath', clipId, function(s) { + s.classed('plotclip', true) + .append('rect'); + }); - Drawing.setTranslate(plotinfo.plot, xa._offset, ya._offset); + plotinfo.clipRect = plotClip.select('rect').attr({ + width: xa._length, + height: ya._length + }); - var plotClipId; - var layerClipId; + Drawing.setTranslate(plotinfo.plot, xa._offset, ya._offset); - if(plotinfo._hasClipOnAxisFalse) { - plotClipId = null; - layerClipId = clipId; - } else { - plotClipId = clipId; - layerClipId = null; - } + var plotClipId; + var layerClipId; - Drawing.setClipUrl(plotinfo.plot, plotClipId); + if(plotinfo._hasClipOnAxisFalse) { + plotClipId = null; + layerClipId = clipId; + } else { + plotClipId = clipId; + layerClipId = null; + } - // stash layer clipId value (null or same as clipId) - // to DRY up Drawing.setClipUrl calls on trace-module and trace layers - // downstream - plotinfo.layerClipId = layerClipId; + Drawing.setClipUrl(plotinfo.plot, plotClipId); - // figure out extra axis line and tick positions as needed - if(!hasSVGCartesian) return; + // stash layer clipId value (null or same as clipId) + // to DRY up Drawing.setClipUrl calls on trace-module and trace layers + // downstream + plotinfo.layerClipId = layerClipId; + } + } - var xLinesXLeft, xLinesXRight, xLinesYBottom, xLinesYTop, - leftYLineWidth, rightYLineWidth; - var yLinesYBottom, yLinesYTop, yLinesXLeft, yLinesXRight, - connectYBottom, connectYTop; - var extraSubplot; + var xLinesXLeft, xLinesXRight, xLinesYBottom, xLinesYTop, + leftYLineWidth, rightYLineWidth; + var yLinesYBottom, yLinesYTop, yLinesXLeft, yLinesXRight, + connectYBottom, connectYTop; + var extraSubplot; - function xLinePath(y) { - return 'M' + xLinesXLeft + ',' + y + 'H' + xLinesXRight; - } + function xLinePath(y) { + return 'M' + xLinesXLeft + ',' + y + 'H' + xLinesXRight; + } - function xLinePathFree(y) { - return 'M' + xa._offset + ',' + y + 'h' + xa._length; - } + function xLinePathFree(y) { + return 'M' + xa._offset + ',' + y + 'h' + xa._length; + } - function yLinePath(x) { - return 'M' + x + ',' + yLinesYTop + 'V' + yLinesYBottom; - } + function yLinePath(x) { + return 'M' + x + ',' + yLinesYTop + 'V' + yLinesYBottom; + } - function yLinePathFree(x) { - return 'M' + x + ',' + ya._offset + 'v' + ya._length; - } + function yLinePathFree(x) { + return 'M' + x + ',' + ya._offset + 'v' + ya._length; + } - function mainPath(ax, pathFn, pathFnFree) { - if(!ax.showline || subplot !== ax._mainSubplot) return ''; - if(!ax._anchorAxis) return pathFnFree(ax._mainLinePosition); - var out = pathFn(ax._mainLinePosition); - if(ax.mirror) out += pathFn(ax._mainMirrorPosition); - return out; - } + function mainPath(ax, pathFn, pathFnFree) { + if(!ax.showline || subplot !== ax._mainSubplot) return ''; + if(!ax._anchorAxis) return pathFnFree(ax._mainLinePosition); + var out = pathFn(ax._mainLinePosition); + if(ax.mirror) out += pathFn(ax._mainMirrorPosition); + return out; + } + + for(subplot in fullLayout._plots) { + plotinfo = fullLayout._plots[subplot]; + xa = plotinfo.xaxis; + ya = plotinfo.yaxis; /* * x lines get longer where they meet y lines, to make a crisp corner. @@ -323,11 +347,9 @@ function lsInner(gd) { ya.linecolor : 'rgba(0,0,0,0)'); } plotinfo.ylines.attr('d', yPath); - }); + } Axes.makeClipPaths(gd); - exports.drawMainTitle(gd); - ModeBar.manage(gd); return gd._promises.length && Promise.all(gd._promises); } @@ -440,17 +462,35 @@ exports.drawMainTitle = function(gd) { }); }; -// First, see if we need to do arraysToCalcdata -// call it regardless of what change we made, in case -// supplyDefaults brought in an array that was already -// in gd.data but not in gd._fullData previously exports.doTraceStyle = function(gd) { - for(var i = 0; i < gd.calcdata.length; i++) { - var cdi = gd.calcdata[i], - _module = ((cdi[0] || {}).trace || {})._module || {}, - arraysToCalcdata = _module.arraysToCalcdata; + var calcdata = gd.calcdata; + var editStyleCalls = []; + var i; + + for(i = 0; i < calcdata.length; i++) { + var cd = calcdata[i]; + var cd0 = cd[0] || {}; + var trace = cd0.trace || {}; + var _module = trace._module || {}; + + // See if we need to do arraysToCalcdata + // call it regardless of what change we made, in case + // supplyDefaults brought in an array that was already + // in gd.data but not in gd._fullData previously + var arraysToCalcdata = _module.arraysToCalcdata; + if(arraysToCalcdata) arraysToCalcdata(cd, trace); + + var editStyle = _module.editStyle; + if(editStyle) editStyleCalls.push({fn: editStyle, cd0: cd0}); + } - if(arraysToCalcdata) arraysToCalcdata(cdi, cdi[0].trace); + if(editStyleCalls.length) { + for(i = 0; i < editStyleCalls.length; i++) { + var edit = editStyleCalls[i]; + edit.fn(gd, edit.cd0); + } + clearGlCanvases(gd); + exports.redrawReglTraces(gd); } Plots.style(gd); @@ -498,16 +538,13 @@ exports.doLegend = function(gd) { return Plots.previousPromises(gd); }; -exports.doTicksRelayout = function(gd, rangesAltered) { - if(rangesAltered) { - Axes.doTicks(gd, Object.keys(rangesAltered), true); - } else { - Axes.doTicks(gd, 'redraw'); - } +exports.doTicksRelayout = function(gd) { + Axes.doTicks(gd, 'redraw'); if(gd._fullLayout._hasOnlyLargeSploms) { + Registry.subplotsRegistry.splom.updateGrid(gd); clearGlCanvases(gd); - Registry.subplotsRegistry.splom.plot(gd); + exports.redrawReglTraces(gd); } exports.drawMainTitle(gd); @@ -560,6 +597,8 @@ exports.drawData = function(gd) { basePlotModules[i].plot(gd); } + exports.redrawReglTraces(gd); + // styling separate from drawing Plots.style(gd); @@ -573,6 +612,62 @@ exports.drawData = function(gd) { return Plots.previousPromises(gd); }; +// Draw (or redraw) all regl-based traces in one go, +// useful during drag and selection where buffers of targeted traces are updated, +// but all traces need to be redrawn following clearGlCanvases. +// +// Note that _module.plot for regl trace does NOT draw things +// on the canvas, they only update the buffers. +// Drawing is perform here. +// +// TODO try adding per-subplot option using gl.SCISSOR_TEST for +// non-overlaying, disjoint subplots. +// +// TODO try to include parcoords in here. +// https://github.com/plotly/plotly.js/issues/3069 +exports.redrawReglTraces = function(gd) { + var fullLayout = gd._fullLayout; + + if(fullLayout._has('regl')) { + var fullData = gd._fullData; + var cartesianIds = []; + var polarIds = []; + var i, sp; + + if(fullLayout._hasOnlyLargeSploms) { + fullLayout._splomGrid.draw(); + } + + // N.B. + // - Loop over fullData (not _splomScenes) to preserve splom trace-to-trace ordering + // - Fill list if subplot ids (instead of fullLayout._subplots) to handle cases where all traces + // of a given module are `visible !== true` + for(i = 0; i < fullData.length; i++) { + var trace = fullData[i]; + + if(trace.visible === true) { + if(trace.type === 'splom') { + fullLayout._splomScenes[trace.uid].draw(); + } else if(trace.type === 'scattergl') { + Lib.pushUnique(cartesianIds, trace.xaxis + trace.yaxis); + } else if(trace.type === 'scatterpolargl') { + Lib.pushUnique(polarIds, trace.subplot); + } + } + } + + for(i = 0; i < cartesianIds.length; i++) { + sp = fullLayout._plots[cartesianIds[i]]; + if(sp._scene) sp._scene.draw(); + } + + for(i = 0; i < polarIds.length; i++) { + sp = fullLayout[polarIds[i]]._subplot; + if(sp._scene) sp._scene.draw(); + } + } +}; + exports.doAutoRangeAndConstraints = function(gd) { var axList = Axes.list(gd, '', true); diff --git a/src/plots/cartesian/dragbox.js b/src/plots/cartesian/dragbox.js index f9abcb1da6a..6f8d03c8cc2 100644 --- a/src/plots/cartesian/dragbox.js +++ b/src/plots/cartesian/dragbox.js @@ -16,13 +16,14 @@ var supportsPassive = require('has-passive-events'); var Registry = require('../../registry'); var Lib = require('../../lib'); var svgTextUtils = require('../../lib/svg_text_utils'); -var clearGlCanvases = require('../../lib/clear_gl_canvases'); var Color = require('../../components/color'); var Drawing = require('../../components/drawing'); var Fx = require('../../components/fx'); var setCursor = require('../../lib/setcursor'); var dragElement = require('../../components/dragelement'); var FROM_TL = require('../../constants/alignment').FROM_TL; +var clearGlCanvases = require('../../lib/clear_gl_canvases'); +var redrawReglTraces = require('../../plot_api/subroutines').redrawReglTraces; var Plots = require('../plots'); @@ -84,7 +85,7 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) { // do we need to edit x/y ranges? var editX, editY; // graph-wide optimization flags - var hasScatterGl, hasOnlyLargeSploms, hasSplom, hasSVG; + var hasScatterGl, hasSplom, hasSVG; // collected changes to be made to the plot by relayout at the end var updates; @@ -125,8 +126,7 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) { var fullLayout = gd._fullLayout; hasScatterGl = fullLayout._has('scattergl'); - hasOnlyLargeSploms = fullLayout._hasOnlyLargeSploms; - hasSplom = hasOnlyLargeSploms || fullLayout._has('splom'); + hasSplom = fullLayout._has('splom'); hasSVG = fullLayout._has('svg'); } @@ -744,33 +744,29 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) { var subplots = fullLayout._subplots.cartesian; var i, sp, xa, ya; - if(hasSplom || hasScatterGl) { - clearGlCanvases(gd); - } - if(hasSplom) { Registry.subplotsRegistry.splom.drag(gd); - if(hasOnlyLargeSploms) return; } if(hasScatterGl) { - // loop over all subplots (w/o exceptions) here, - // as we cleared the gl canvases above for(i = 0; i < subplots.length; i++) { sp = plotinfos[subplots[i]]; xa = sp.xaxis; ya = sp.yaxis; - var scene = sp._scene; - if(scene) { - // FIXME: possibly we could update axis internal _r and _rl here + if(sp._scene) { var xrng = Lib.simpleMap(xa.range, xa.r2l); var yrng = Lib.simpleMap(ya.range, ya.r2l); - scene.update({range: [xrng[0], yrng[0], xrng[1], yrng[1]]}); + sp._scene.update({range: [xrng[0], yrng[0], xrng[1], yrng[1]]}); } } } + if(hasSplom || hasScatterGl) { + clearGlCanvases(gd); + redrawReglTraces(gd); + } + if(hasSVG) { var xScaleFactor = viewBox[2] / xa0._length; var yScaleFactor = viewBox[3] / ya0._length; diff --git a/src/plots/cartesian/index.js b/src/plots/cartesian/index.js index a71485c244b..255b2a5d837 100644 --- a/src/plots/cartesian/index.js +++ b/src/plots/cartesian/index.js @@ -464,7 +464,6 @@ function makeSubplotLayer(gd, plotinfo) { // and other places // - we don't (x|y)lines and (x|y)axislayer for most subplots // usually just the bottom x and left y axes. - plotinfo.plot = ensureSingle(plotgroup, 'g', 'plot'); plotinfo.xlines = ensureSingle(plotgroup, 'path', 'xlines-above'); plotinfo.ylines = ensureSingle(plotgroup, 'path', 'ylines-above'); plotinfo.xaxislayer = ensureSingle(plotgroup, 'g', 'xaxislayer-above'); diff --git a/src/plots/cartesian/select.js b/src/plots/cartesian/select.js index fdc869f8690..35d39d285a6 100644 --- a/src/plots/cartesian/select.js +++ b/src/plots/cartesian/select.js @@ -19,7 +19,8 @@ var polygon = require('../../lib/polygon'); var throttle = require('../../lib/throttle'); var makeEventData = require('../../components/fx/helpers').makeEventData; var getFromId = require('./axis_ids').getFromId; -var sortModules = require('../sort_modules').sortModules; +var clearGlCanvases = require('../../lib/clear_gl_canvases'); +var redrawReglTraces = require('../../plot_api/subroutines').redrawReglTraces; var constants = require('./constants'); var MINSELECT = constants.MINSELECT; @@ -514,7 +515,9 @@ function determineSearchTraces(gd, xAxes, yAxes, subplot) { // FIXME: make sure we don't have more than single axis for splom trace._xaxes[xAxisIds[0]] && trace._yaxes[yAxisIds[0]] ) { - searchTraces.push(createSearchInfo(trace._module, cd, xAxes[0], yAxes[0])); + var info = createSearchInfo(trace._module, cd, xAxes[0], yAxes[0]); + info.scene = gd._fullLayout._splomScenes[trace.uid]; + searchTraces.push(info); } else { if(xAxisIds.indexOf(trace.xaxis) === -1) continue; if(yAxisIds.indexOf(trace.yaxis) === -1) continue; @@ -661,7 +664,7 @@ function isOnlyOnePointSelected(searchTraces) { } function updateSelectedState(gd, searchTraces, eventData) { - var i, j, searchInfo, trace; + var i, searchInfo, cd, trace; if(eventData) { var pts = eventData.points || []; @@ -694,43 +697,25 @@ function updateSelectedState(gd, searchTraces, eventData) { } } - // group searchInfo traces by trace modules - var lookup = {}; + var hasRegl = false; for(i = 0; i < searchTraces.length; i++) { searchInfo = searchTraces[i]; + cd = searchInfo.cd; + trace = cd[0].trace; - var name = searchInfo._module.name; - if(lookup[name]) { - lookup[name].push(searchInfo); - } else { - lookup[name] = [searchInfo]; + if(Registry.traceIs(trace, 'regl')) { + hasRegl = true; } + + var _module = searchInfo._module; + var fn = _module.styleOnSelect || _module.style; + if(fn) fn(gd, cd); } - var keys = Object.keys(lookup).sort(sortModules); - - for(i = 0; i < keys.length; i++) { - var items = lookup[keys[i]]; - var len = items.length; - var item0 = items[0]; - var trace0 = item0.cd[0].trace; - var _module = item0._module; - var styleSelection = _module.styleOnSelect || _module.style; - - if(Registry.traceIs(trace0, 'regl')) { - // plot regl traces per module - var cds = new Array(len); - for(j = 0; j < len; j++) { - cds[j] = items[j].cd; - } - styleSelection(gd, cds); - } else { - // plot svg trace per trace - for(j = 0; j < len; j++) { - styleSelection(gd, items[j].cd); - } - } + if(hasRegl) { + clearGlCanvases(gd); + redrawReglTraces(gd); } } diff --git a/src/plots/plots.js b/src/plots/plots.js index 8f172d14ba0..bb6b1573539 100644 --- a/src/plots/plots.js +++ b/src/plots/plots.js @@ -20,7 +20,6 @@ var Color = require('../components/color'); var BADNUM = require('../constants/numerical').BADNUM; var axisIDs = require('../plots/cartesian/axis_ids'); -var sortBasePlotModules = require('./sort_modules').sortBasePlotModules; var animationAttrs = require('./animation_attributes'); var frameAttrs = require('./frame_attributes'); @@ -468,7 +467,7 @@ plots.supplyDefaults = function(gd, opts) { plots.linkSubplots(newFullData, newFullLayout, oldFullData, oldFullLayout); // clean subplots and other artifacts from previous plot calls - plots.cleanPlot(newFullData, newFullLayout, oldFullData, oldFullLayout, oldCalcdata); + plots.cleanPlot(newFullData, newFullLayout, oldFullData, oldFullLayout); // relink functions and _ attributes to promote consistency between plots relinkPrivateKeys(newFullLayout, oldFullLayout); @@ -487,9 +486,6 @@ plots.supplyDefaults = function(gd, opts) { if(!skipUpdateCalc && oldCalcdata.length === newFullData.length) { plots.supplyDefaultsUpdateCalc(oldCalcdata, newFullData); } - - // sort base plot modules for consistent ordering - newFullLayout._basePlotModules.sort(sortBasePlotModules); }; plots.supplyDefaultsUpdateCalc = function(oldCalcdata, newFullData) { @@ -718,7 +714,7 @@ plots._hasPlotType = function(category) { return false; }; -plots.cleanPlot = function(newFullData, newFullLayout, oldFullData, oldFullLayout, oldCalcdata) { +plots.cleanPlot = function(newFullData, newFullLayout, oldFullData, oldFullLayout) { var i, j; var basePlotModules = oldFullLayout._basePlotModules || []; @@ -726,7 +722,7 @@ plots.cleanPlot = function(newFullData, newFullLayout, oldFullData, oldFullLayou var _module = basePlotModules[i]; if(_module.clean) { - _module.clean(newFullData, newFullLayout, oldFullData, oldFullLayout, oldCalcdata); + _module.clean(newFullData, newFullLayout, oldFullData, oldFullLayout); } } diff --git a/src/plots/polar/layout_attributes.js b/src/plots/polar/layout_attributes.js index d7fa1d6c31f..8334b0c3d96 100644 --- a/src/plots/polar/layout_attributes.js +++ b/src/plots/polar/layout_attributes.js @@ -59,7 +59,7 @@ var radialAxisAttrs = { visible: extendFlat({}, axesAttrs.visible, {dflt: true}), type: axesAttrs.type, - autorange: axesAttrs.autorange, + autorange: extendFlat({}, axesAttrs.autorange, {editType: 'plot'}), rangemode: { valType: 'enumerated', values: ['tozero', 'nonnegative', 'normal'], @@ -75,7 +75,13 @@ var radialAxisAttrs = { 'of the input data (same behavior as for cartesian axes).' ].join(' ') }, - range: axesAttrs.range, + range: extendFlat({}, axesAttrs.range, { + items: [ + {valType: 'any', editType: 'plot', impliedEdits: {'^autorange': false}}, + {valType: 'any', editType: 'plot', impliedEdits: {'^autorange': false}} + ], + editType: 'plot' + }), categoryorder: axesAttrs.categoryorder, categoryarray: axesAttrs.categoryarray, diff --git a/src/plots/polar/polar.js b/src/plots/polar/polar.js index 0fc1b6e3a77..97142b3a31d 100644 --- a/src/plots/polar/polar.js +++ b/src/plots/polar/polar.js @@ -28,6 +28,8 @@ var prepSelect = require('../cartesian/select').prepSelect; var selectOnClick = require('../cartesian/select').selectOnClick; var clearSelect = require('../cartesian/select').clearSelect; var setCursor = require('../../lib/setcursor'); +var clearGlCanvases = require('../../lib/clear_gl_canvases'); +var redrawReglTraces = require('../../plot_api/subroutines').redrawReglTraces; var MID_SHIFT = require('../../constants/alignment').MID_SHIFT; var constants = require('./constants'); @@ -1058,7 +1060,7 @@ proto.updateRadialDrag = function(fullLayout, polarLayout, rngIndex) { .attr('transform', strTranslate(cx, cy)) .selectAll('path').attr('transform', null); - if(_this._scene) _this._scene.clear(); + var hasRegl = false; for(var traceType in _this.traceHash) { var moduleCalcData = _this.traceHash[traceType]; @@ -1066,6 +1068,12 @@ proto.updateRadialDrag = function(fullLayout, polarLayout, rngIndex) { var _module = moduleCalcData[0][0].trace._module; var polarLayoutNow = gd._fullLayout[_this.id]; _module.plot(gd, _this, moduleCalcDataVisible, polarLayoutNow); + if(Registry.traceIs(traceType, 'gl') && moduleCalcDataVisible.length) hasRegl = true; + } + + if(hasRegl) { + clearGlCanvases(gd); + redrawReglTraces(gd); } } @@ -1185,7 +1193,7 @@ proto.updateAngularDrag = function(fullLayout) { scatterTraces.call(Drawing.hideOutsideRangePoints, _this); } - if(_this._scene) _this._scene.clear(); + var hasRegl = false; for(var traceType in _this.traceHash) { if(Registry.traceIs(traceType, 'gl')) { @@ -1193,8 +1201,14 @@ proto.updateAngularDrag = function(fullLayout) { var moduleCalcDataVisible = Lib.filterVisible(moduleCalcData); var _module = moduleCalcData[0][0].trace._module; _module.plot(gd, _this, moduleCalcDataVisible, polarLayoutNow); + if(moduleCalcDataVisible.length) hasRegl = true; } } + + if(hasRegl) { + clearGlCanvases(gd); + redrawReglTraces(gd); + } } function doneFn() { diff --git a/src/plots/sort_modules.js b/src/plots/sort_modules.js deleted file mode 100644 index 38090667d40..00000000000 --- a/src/plots/sort_modules.js +++ /dev/null @@ -1,25 +0,0 @@ -/** -* Copyright 2012-2018, Plotly, Inc. -* All rights reserved. -* -* This source code is licensed under the MIT license found in the -* LICENSE file in the root directory of this source tree. -*/ - -'use strict'; - -// always plot splom before cartesian (i.e. scattergl traces) -function sortModules(a, b) { - if(a === 'splom') return -1; - if(b === 'splom') return 1; - return 0; -} - -function sortBasePlotModules(a, b) { - return sortModules(a.name, b.name); -} - -module.exports = { - sortBasePlotModules: sortBasePlotModules, - sortModules: sortModules -}; diff --git a/src/traces/scattergl/attributes.js b/src/traces/scattergl/attributes.js index 385fb765742..94731763ffc 100644 --- a/src/traces/scattergl/attributes.js +++ b/src/traces/scattergl/attributes.js @@ -28,15 +28,7 @@ var attrs = module.exports = overrideAll({ y0: scatterAttrs.y0, dy: scatterAttrs.dy, - text: extendFlat({}, scatterAttrs.text, { - description: [ - 'Sets text elements associated with each (x,y) pair to appear on hover.', - 'If a single string, the same string appears over', - 'all the data points.', - 'If an array of string, the items are mapped in order to the', - 'this trace\'s (x,y) coordinates.' - ].join(' ') - }), + text: scatterAttrs.text, hovertext: scatterAttrs.hovertext, textposition: scatterAttrs.textposition, diff --git a/src/traces/scattergl/index.js b/src/traces/scattergl/index.js index 14f8b27d021..8b4d7dc4f7f 100644 --- a/src/traces/scattergl/index.js +++ b/src/traces/scattergl/index.js @@ -252,8 +252,6 @@ function sceneUpdate(gd, subplot) { scene.glText[i].update(opt); } } - - scene.draw(); }; // draw traces in proper order @@ -296,18 +294,6 @@ function sceneUpdate(gd, subplot) { scene.dirty = false; }; - scene.clear = function clear() { - var vp = getViewport(gd._fullLayout, subplot.xaxis, subplot.yaxis); - - if(scene.select2d) { - clearViewport(scene.select2d, vp); - } - - var anyComponent = scene.scatter2d || scene.line2d || - (scene.glText || [])[0] || scene.fill2d || scene.error2d; - if(anyComponent) clearViewport(anyComponent, vp); - }; - // remove scene resources scene.destroy = function destroy() { if(scene.fill2d) scene.fill2d.destroy(); @@ -359,14 +345,6 @@ function getViewport(fullLayout, xaxis, yaxis) { ]; } -function clearViewport(comp, vp) { - var gl = comp.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); -} - function plot(gd, subplot, cdata) { if(!cdata.length) return; @@ -640,8 +618,6 @@ function plot(gd, subplot, cdata) { if(scene.glText) { scene.glText.forEach(function(text) { text.update(vpRange0); }); } - - scene.draw(); } @@ -893,21 +869,6 @@ function selectPoints(searchInfo, selectionTester) { return selection; } -function style(gd, cds) { - if(!cds) return; - - var stash = cds[0][0].t; - var scene = stash._scene; - - // don't clear the subplot if there are splom traces - // on the graph - if(!gd._fullLayout._has('splom')) { - scene.clear(); - } - - scene.draw(); -} - function styleTextSelection(cd) { var cd0 = cd[0]; var stash = cd0.t; @@ -957,7 +918,6 @@ module.exports = { calc: calc, plot: plot, hoverPoints: hoverPoints, - style: style, selectPoints: selectPoints, sceneOptions: sceneOptions, diff --git a/src/traces/scatterpolargl/index.js b/src/traces/scatterpolargl/index.js index b68229c4aff..f317682eb90 100644 --- a/src/traces/scatterpolargl/index.js +++ b/src/traces/scatterpolargl/index.js @@ -183,7 +183,6 @@ module.exports = { calc: calc, plot: plot, hoverPoints: hoverPoints, - style: ScatterGl.style, selectPoints: ScatterGl.selectPoints, meta: { diff --git a/src/traces/splom/attributes.js b/src/traces/splom/attributes.js index dbc9169bbe4..bceb9e68f83 100644 --- a/src/traces/splom/attributes.js +++ b/src/traces/splom/attributes.js @@ -8,9 +8,15 @@ 'use strict'; +var scatterAttrs = require('../scatter/attributes'); +var colorAttrs = require('../../components/colorscale/attributes'); var scatterGlAttrs = require('../scattergl/attributes'); var cartesianIdRegex = require('../../plots/cartesian/constants').idRegex; var templatedArray = require('../../plot_api/plot_template').templatedArray; +var extendFlat = require('../../lib/extend').extendFlat; + +var scatterMarkerAttrs = scatterAttrs.marker; +var scatterMarkerLineAttrs = scatterMarkerAttrs.line; function makeAxesValObject(axLetter) { return { @@ -25,9 +31,12 @@ function makeAxesValObject(axLetter) { }, description: [ 'Sets the list of ' + axLetter + ' axes', - 'corresponding to this splom trace.', + 'corresponding to dimensions of this splom trace.', 'By default, a splom will match the first N ' + axLetter + 'axes', - 'where N is the number of input dimensions.' + 'where N is the number of input dimensions.', + 'Note that, in case where `diagonal.visible` is false and `showupperhalf`', + 'or `showlowerhalf` is false, this splom trace will generate', + 'one less x-axis and one less y-axis.', ].join(' ') }; } @@ -86,8 +95,30 @@ module.exports = { // mode: {}, (only 'markers' for now) - text: scatterGlAttrs.text, - marker: scatterGlAttrs.marker, + text: extendFlat({}, scatterGlAttrs.text, { + description: [ + 'Sets text elements associated with each (x,y) pair to appear on hover.', + 'If a single string, the same string appears over', + 'all the data points.', + 'If an array of string, the items are mapped in order to the', + 'this trace\'s (x,y) coordinates.' + ].join(' ') + }), + + marker: extendFlat({}, colorAttrs('marker'), { + symbol: scatterMarkerAttrs.symbol, + size: extendFlat({}, scatterMarkerAttrs.size, {editType: 'markerSize'}), + sizeref: scatterMarkerAttrs.sizeref, + sizemin: scatterMarkerAttrs.sizemin, + sizemode: scatterMarkerAttrs.sizemode, + opacity: scatterMarkerAttrs.opacity, + colorbar: scatterMarkerAttrs.colorbar, + line: extendFlat({}, colorAttrs('marker.line'), { + width: scatterMarkerLineAttrs.width, + editType: 'calc' + }), + editType: 'calc' + }), xaxes: makeAxesValObject('x'), yaxes: makeAxesValObject('y'), diff --git a/src/traces/splom/base_plot.js b/src/traces/splom/base_plot.js index c1489ef2fe2..6e3b211fa05 100644 --- a/src/traces/splom/base_plot.js +++ b/src/traces/splom/base_plot.js @@ -28,7 +28,7 @@ function plot(gd) { if(!success) return; if(fullLayout._hasOnlyLargeSploms) { - drawGrid(gd); + updateGrid(gd); } _module.plot(gd, {}, splomCalcData); @@ -39,25 +39,24 @@ function drag(gd) { var fullLayout = gd._fullLayout; if(fullLayout._hasOnlyLargeSploms) { - drawGrid(gd); + updateGrid(gd); } for(var i = 0; i < cd.length; i++) { var cd0 = cd[i][0]; var trace = cd0.trace; - var stash = cd0.t; - var scene = stash._scene; + var scene = fullLayout._splomScenes[trace.uid]; if(trace.type === 'splom' && scene && scene.matrix) { - dragOne(gd, trace, stash, scene); + dragOne(gd, trace, scene); } } } -function dragOne(gd, trace, stash, scene) { +function dragOne(gd, trace, scene) { var visibleLength = scene.matrixOptions.data.length; - var visibleDims = stash.visibleDims; - var ranges = new Array(visibleLength); + var visibleDims = trace._visibleDims; + var ranges = scene.viewOpts.ranges = new Array(visibleLength); for(var k = 0; k < visibleDims.length; k++) { var i = visibleDims[k]; @@ -78,14 +77,12 @@ function dragOne(gd, trace, stash, scene) { if(scene.selectBatch) { scene.matrix.update({ranges: ranges}, {ranges: ranges}); - scene.matrix.draw(scene.unselectBatch, scene.selectBatch); } else { scene.matrix.update({ranges: ranges}); - scene.matrix.draw(); } } -function drawGrid(gd) { +function updateGrid(gd) { var fullLayout = gd._fullLayout; var regl = fullLayout._glcanvas.data()[0].regl; var splomGrid = fullLayout._splomGrid; @@ -93,9 +90,7 @@ function drawGrid(gd) { if(!splomGrid) { splomGrid = fullLayout._splomGrid = createLine(regl); } - splomGrid.update(makeGridData(gd)); - splomGrid.draw(); } function makeGridData(gd) { @@ -168,46 +163,39 @@ function makeGridData(gd) { return gridBatches; } -function clean(newFullData, newFullLayout, oldFullData, oldFullLayout, oldCalcdata) { - var oldModules = oldFullLayout._modules || []; - var newModules = newFullLayout._modules || []; - - var hadSplom, hasSplom; +function clean(newFullData, newFullLayout, oldFullData, oldFullLayout) { + var lookup = {}; var i; - for(i = 0; i < oldModules.length; i++) { - if(oldModules[i].name === 'splom') { - hadSplom = true; - break; + if(oldFullLayout._splomScenes) { + for(i = 0; i < newFullData.length; i++) { + var newTrace = newFullData[i]; + if(newTrace.type === 'splom') { + lookup[newTrace.uid] = 1; + } } - } - for(i = 0; i < newModules.length; i++) { - if(newModules[i].name === 'splom') { - hasSplom = true; - break; + for(i = 0; i < oldFullData.length; i++) { + var oldTrace = oldFullData[i]; + if(!lookup[oldTrace.uid]) { + var scene = oldFullLayout._splomScenes[oldTrace.uid]; + if(scene && scene.destroy) scene.destroy(); + // must first set scene to null in order to get garbage collected + oldFullLayout._splomScenes[oldTrace.uid] = null; + delete oldFullLayout._splomScenes[oldTrace.uid]; + } } } - if(hadSplom && !hasSplom) { - for(i = 0; i < oldCalcdata.length; i++) { - var cd0 = oldCalcdata[i][0]; - var trace = cd0.trace; - var scene = cd0.t._scene; - - if( - trace.type === 'splom' && - scene && scene.matrix && scene.matrix.destroy - ) { - scene.matrix.destroy(); - cd0.t._scene = null; - } - } + if(Object.keys(oldFullLayout._splomScenes || {}).length === 0) { + delete oldFullLayout._splomScenes; } if(oldFullLayout._splomGrid && (!newFullLayout._hasOnlyLargeSploms && oldFullLayout._hasOnlyLargeSploms)) { + // must first set scene to null in order to get garbage collected oldFullLayout._splomGrid.destroy(); oldFullLayout._splomGrid = null; + delete oldFullLayout._splomGrid; } Cartesian.clean(newFullData, newFullLayout, oldFullData, oldFullLayout); @@ -228,7 +216,7 @@ function updateFx(gd) { var trace = cd0.trace; if(trace.type === 'splom') { - var scene = cd0.t._scene; + var scene = fullLayout._splomScenes[trace.uid]; if(scene.selectBatch === null) { scene.matrix.update(scene.matrixOptions, null); } @@ -246,6 +234,7 @@ module.exports = { drawFramework: Cartesian.drawFramework, plot: plot, drag: drag, + updateGrid: updateGrid, clean: clean, updateFx: updateFx, toSVG: Cartesian.toSVG diff --git a/src/traces/splom/defaults.js b/src/traces/splom/defaults.js index 37f6d1ca066..57d43cfea26 100644 --- a/src/traces/splom/defaults.js +++ b/src/traces/splom/defaults.js @@ -73,97 +73,96 @@ function handleAxisDefaults(traceIn, traceOut, layout, coerce) { var showDiag = traceOut.diagonal.visible; var i, j; - // N.B. one less x axis AND one less y axis when hiding one half and the diagonal - var axDfltLength = !showDiag && (!showUpper || !showLower) ? dimLength - 1 : dimLength; + var xAxesDflt = new Array(dimLength); + var yAxesDflt = new Array(dimLength); - var xaxes = coerce('xaxes', fillAxisIdArray('x', axDfltLength)); - var yaxes = coerce('yaxes', fillAxisIdArray('y', axDfltLength)); + for(i = 0; i < dimLength; i++) { + var suffix = i ? i + 1 : ''; + xAxesDflt[i] = 'x' + suffix; + yAxesDflt[i] = 'y' + suffix; + } - // to avoid costly indexOf - traceOut._xaxes = arrayToHashObject(xaxes); - traceOut._yaxes = arrayToHashObject(yaxes); + var xaxes = coerce('xaxes', xAxesDflt); + var yaxes = coerce('yaxes', yAxesDflt); - // allow users to under-specify number of axes - var axLength = Math.min(axDfltLength, xaxes.length, yaxes.length); + // build list of [x,y] axis corresponding to each dimensions[i], + // very useful for passing options to regl-splom + var diag = traceOut._diag = new Array(dimLength); - // fill in splom subplot keys - for(i = 0; i < axLength; i++) { - for(j = 0; j < axLength; j++) { - var id = xaxes[i] + yaxes[j]; + // lookup for 'drawn' x|y axes, to avoid costly indexOf downstream + traceOut._xaxes = {}; + traceOut._yaxes = {}; - if(i > j && showUpper) { - layout._splomSubplots[id] = 1; - } else if(i < j && showLower) { - layout._splomSubplots[id] = 1; - } else if(i === j && (showDiag || !showLower || !showUpper)) { - // need to include diagonal subplots when - // hiding one half and the diagonal - layout._splomSubplots[id] = 1; + // list of 'drawn' x|y axes, use to generate list of subplots + var xList = []; + var yList = []; + + function fillAxisStashes(axId, dim, list) { + if(!axId) return; + + var axLetter = axId.charAt(0); + var stash = layout._splomAxes[axLetter]; + + traceOut['_' + axLetter + 'axes'][axId] = 1; + list.push(axId); + + if(!(axId in stash)) { + var s = stash[axId] = {}; + if(dim) { + s.label = dim.label || ''; + if(dim.visible && dim.axis) { + s.type = dim.axis.type; + } } } } - // build list of [x,y] axis corresponding to each dimensions[i], - // very useful for passing options to regl-splom - var diag = traceOut._diag = new Array(dimLength); - // cases where showDiag and showLower or showUpper are false - // no special treatment as the xaxes and yaxes items no longer match - // the dimensions items 1-to-1 - var xShift = !showDiag && !showLower ? -1 : 0; - var yShift = !showDiag && !showUpper ? -1 : 0; + // no special treatment as the 'drawn' x-axes and y-axes no longer match + // the dimensions items and xaxes|yaxes 1-to-1 + var mustShiftX = !showDiag && !showLower; + var mustShiftY = !showDiag && !showUpper; for(i = 0; i < dimLength; i++) { var dim = dimensions[i]; - var xaId = xaxes[i + xShift]; - var yaId = yaxes[i + yShift]; - - fillAxisStash(layout, xaId, dim); - fillAxisStash(layout, yaId, dim); + var i0 = i === 0; + var iN = i === dimLength - 1; - // note that some the entries here may be undefined - diag[i] = [xaId, yaId]; - } - - // when lower half is omitted, override grid default - // to make sure axes remain on the left/bottom of the plot area - if(!showLower) { - layout._splomGridDflt.xside = 'bottom'; - layout._splomGridDflt.yside = 'left'; - } -} + var xaId = (i0 && mustShiftX) || (iN && mustShiftY) ? + undefined : + xaxes[i]; -function fillAxisIdArray(axLetter, len) { - var out = new Array(len); + var yaId = (i0 && mustShiftY) || (iN && mustShiftX) ? + undefined : + yaxes[i]; - for(var i = 0; i < len; i++) { - out[i] = axLetter + (i ? i + 1 : ''); + fillAxisStashes(xaId, dim, xList); + fillAxisStashes(yaId, dim, yList); + diag[i] = [xaId, yaId]; } - return out; -} - -function fillAxisStash(layout, axId, dim) { - if(!axId) return; - - var axLetter = axId.charAt(0); - var stash = layout._splomAxes[axLetter]; + // fill in splom subplot keys + for(i = 0; i < xList.length; i++) { + for(j = 0; j < yList.length; j++) { + var id = xList[i] + yList[j]; - if(!(axId in stash)) { - var s = stash[axId] = {}; - if(dim) { - s.label = dim.label || ''; - if(dim.visible && dim.axis) { - s.type = dim.axis.type; + if(i > j && showUpper) { + layout._splomSubplots[id] = 1; + } else if(i < j && showLower) { + layout._splomSubplots[id] = 1; + } else if(i === j && (showDiag || !showLower || !showUpper)) { + // need to include diagonal subplots when + // hiding one half and the diagonal + layout._splomSubplots[id] = 1; } } } -} -function arrayToHashObject(arr) { - var obj = {}; - for(var i = 0; i < arr.length; i++) { - obj[arr[i]] = 1; + // when lower half is omitted, or when just the diagonal is gone, + // override grid default to make sure axes remain on + // the left/bottom of the plot area + if(!showLower || (!showDiag && showUpper && showLower)) { + layout._splomGridDflt.xside = 'bottom'; + layout._splomGridDflt.yside = 'left'; } - return obj; } diff --git a/src/traces/splom/index.js b/src/traces/splom/index.js index cbbade4c81b..8e6e03e9091 100644 --- a/src/traces/splom/index.js +++ b/src/traces/splom/index.js @@ -30,14 +30,13 @@ var TOO_MANY_POINTS = require('../scattergl/constants').TOO_MANY_POINTS; function calc(gd, trace) { var dimensions = trace.dimensions; var commonLength = trace._length; - var stash = {}; var opts = {}; // 'c' for calculated, 'l' for linear, // only differ here for log axes, pass ldata to createMatrix as 'data' var cdata = opts.cdata = []; var ldata = opts.data = []; // keep track of visible dimensions - var visibleDims = stash.visibleDims = []; + var visibleDims = trace._visibleDims = []; var i, k, dim, xa, ya; function makeCalcdata(ax, dim) { @@ -87,42 +86,45 @@ function calc(gd, trace) { var visibleLength = cdata.length; var hasTooManyPoints = (visibleLength * commonLength) > TOO_MANY_POINTS; + // Reuse SVG scatter axis expansion routine. + // For graphs with very large number of points and array marker.size, + // use average marker size instead to speed things up. + var ppad; + if(hasTooManyPoints) { + ppad = 2 * (opts.sizeAvg || Math.max(opts.size, 3)); + } else { + ppad = calcMarkerSize(trace, commonLength); + } + for(k = 0; k < visibleDims.length; k++) { i = visibleDims[k]; dim = dimensions[i]; - xa = AxisIDs.getFromId(gd, trace._diag[i][0]) || {}; ya = AxisIDs.getFromId(gd, trace._diag[i][1]) || {}; - - // Reuse SVG scatter axis expansion routine. - // For graphs with very large number of points and array marker.size, - // use average marker size instead to speed things up. - var ppad; - if(hasTooManyPoints) { - ppad = 2 * (opts.sizeAvg || Math.max(opts.size, 3)); - } else { - ppad = calcMarkerSize(trace, commonLength); - } - calcAxisExpansion(gd, trace, xa, ya, cdata[k], cdata[k], ppad); } - var scene = stash._scene = sceneUpdate(gd, stash); + var scene = sceneUpdate(gd, trace); if(!scene.matrix) scene.matrix = true; scene.matrixOptions = opts; scene.selectedOptions = convertMarkerSelection(trace, trace.selected); scene.unselectedOptions = convertMarkerSelection(trace, trace.unselected); - return [{x: false, y: false, t: stash, trace: trace}]; + return [{x: false, y: false, t: {}, trace: trace}]; } -function sceneUpdate(gd, stash) { - var scene = stash._scene; +function sceneUpdate(gd, trace) { + var fullLayout = gd._fullLayout; + var uid = trace.uid; - var reset = { - dirty: true - }; + // must place ref to 'scene' in fullLayout, so that: + // - it can be relinked properly on updates + // - it can be destroyed properly when needed + var splomScenes = fullLayout._splomScenes; + if(!splomScenes) splomScenes = fullLayout._splomScenes = {}; + + var reset = {dirty: true}; var first = { selectBatch: null, @@ -131,15 +133,18 @@ function sceneUpdate(gd, stash) { select: null }; + var scene = splomScenes[trace.uid]; + if(!scene) { - scene = stash._scene = Lib.extendFlat({}, reset, first); + scene = splomScenes[uid] = Lib.extendFlat({}, reset, first); scene.draw = function draw() { - // draw traces in selection mode - if(scene.matrix && scene.selectBatch) { - scene.matrix.draw(scene.unselectBatch, scene.selectBatch); - } else if(scene.matrix) { - scene.matrix.draw(); + if(scene.matrix && scene.matrix.draw) { + if(scene.selectBatch) { + scene.matrix.draw(scene.unselectBatch, scene.selectBatch); + } else { + scene.matrix.draw(); + } } scene.dirty = false; @@ -147,13 +152,13 @@ function sceneUpdate(gd, stash) { // remove scene resources scene.destroy = function destroy() { - if(scene.matrix) scene.matrix.destroy(); - + if(scene.matrix && scene.matrix.destroy) { + scene.matrix.destroy(); + } scene.matrixOptions = null; scene.selectBatch = null; scene.unselectBatch = null; - - stash._scene = null; + scene = null; }; } @@ -178,7 +183,7 @@ function plotOne(gd, cd0) { var gs = fullLayout._size; var trace = cd0.trace; var stash = cd0.t; - var scene = stash._scene; + var scene = fullLayout._splomScenes[trace.uid]; var matrixOpts = scene.matrixOptions; var cdata = matrixOpts.cdata; var regl = fullLayout._glcanvas.data()[0].regl; @@ -194,9 +199,9 @@ function plotOne(gd, cd0) { matrixOpts.upper = trace.showlowerhalf; matrixOpts.diagonal = trace.diagonal.visible; - var visibleDims = stash.visibleDims; + var visibleDims = trace._visibleDims; var visibleLength = cdata.length; - var viewOpts = {}; + var viewOpts = scene.viewOpts = {}; viewOpts.ranges = new Array(visibleLength); viewOpts.domains = new Array(visibleLength); @@ -294,19 +299,31 @@ function plotOne(gd, cd0) { } } else { - scene.matrix.update(matrixOpts, null); - scene.matrix.update(viewOpts, null); + var opts = Lib.extendFlat({}, matrixOpts, viewOpts); + scene.matrix.update(opts, null); stash.xpx = stash.ypx = null; } +} + +function editStyle(gd, cd0) { + var trace = cd0.trace; + var scene = gd._fullLayout._splomScenes[trace.uid]; + + calcColorscales(trace); + + Lib.extendFlat(scene.matrixOptions, convertMarkerStyle(trace)); + // TODO [un]selected styles? + + var opts = Lib.extendFlat({}, scene.matrixOptions, scene.viewOpts); - scene.draw(); + // TODO this is too long for arrayOk attributes! + scene.matrix.update(opts, null); } function hoverPoints(pointData, xval, yval) { var cd = pointData.cd; var trace = cd[0].trace; - var stash = cd[0].t; - var scene = stash._scene; + var scene = pointData.scene; var cdata = scene.matrixOptions.cdata; var xa = pointData.xa; var ya = pointData.ya; @@ -314,8 +331,8 @@ function hoverPoints(pointData, xval, yval) { var ypx = ya.c2p(yval); var maxDistance = pointData.distance; - var xi = getDimIndex(trace, stash, xa); - var yi = getDimIndex(trace, stash, ya); + var xi = getDimIndex(trace, xa); + var yi = getDimIndex(trace, ya); if(xi === false || yi === false) return [pointData]; var x = cdata[xi]; @@ -352,7 +369,7 @@ function selectPoints(searchInfo, selectionTester) { var cd = searchInfo.cd; var trace = cd[0].trace; var stash = cd[0].t; - var scene = stash._scene; + var scene = searchInfo.scene; var cdata = scene.matrixOptions.cdata; var xa = searchInfo.xaxis; var ya = searchInfo.yaxis; @@ -364,8 +381,8 @@ function selectPoints(searchInfo, selectionTester) { var hasOnlyLines = (!subTypes.hasMarkers(trace) && !subTypes.hasText(trace)); if(trace.visible !== true || hasOnlyLines) return selection; - var xi = getDimIndex(trace, stash, xa); - var yi = getDimIndex(trace, stash, ya); + var xi = getDimIndex(trace, xa); + var yi = getDimIndex(trace, ya); if(xi === false || yi === false) return selection; var xpx = stash.xpx[xi]; @@ -419,38 +436,11 @@ function selectPoints(searchInfo, selectionTester) { return selection; } -function style(gd, cds) { - if(!cds) return; - - var fullLayout = gd._fullLayout; - var cd0 = cds[0]; - var scene0 = cd0[0].t._scene; - scene0.matrix.regl.clear({color: true, depth: true}); - - if(fullLayout._splomGrid) { - fullLayout._splomGrid.draw(); - } - - for(var i = 0; i < cds.length; i++) { - var scene = cds[i][0].t._scene; - scene.draw(); - } - - // redraw all subplot with scattergl traces, - // as we cleared the whole canvas above - if(fullLayout._has('cartesian')) { - for(var k in fullLayout._plots) { - var sp = fullLayout._plots[k]; - if(sp._scene) sp._scene.draw(); - } - } -} - -function getDimIndex(trace, stash, ax) { +function getDimIndex(trace, ax) { var axId = ax._id; var axLetter = axId.charAt(0); var ind = {x: 0, y: 1}[axLetter]; - var visibleDims = stash.visibleDims; + var visibleDims = trace._visibleDims; for(var k = 0; k < visibleDims.length; k++) { var i = visibleDims[k]; @@ -474,7 +464,7 @@ module.exports = { plot: plot, hoverPoints: hoverPoints, selectPoints: selectPoints, - style: style, + editStyle: editStyle, meta: { description: [ diff --git a/test/image/baselines/splom_nodiag.png b/test/image/baselines/splom_nodiag.png new file mode 100644 index 00000000000..2d6a7e14579 Binary files /dev/null and b/test/image/baselines/splom_nodiag.png differ diff --git a/test/image/mocks/splom_nodiag.json b/test/image/mocks/splom_nodiag.json new file mode 100644 index 00000000000..65d98b216ee --- /dev/null +++ b/test/image/mocks/splom_nodiag.json @@ -0,0 +1,705 @@ +{ + "data": [ + { + "type": "splom", + "name": "Setosa", + "diagonal": {"visible": false}, + "dimensions": [ + { + "label": "SepalLength", + "values": [ + "5.1", + "4.9", + "4.7", + "4.6", + "5.0", + "5.4", + "4.6", + "5.0", + "4.4", + "4.9", + "5.4", + "4.8", + "4.8", + "4.3", + "5.8", + "5.7", + "5.4", + "5.1", + "5.7", + "5.1", + "5.4", + "5.1", + "4.6", + "5.1", + "4.8", + "5.0", + "5.0", + "5.2", + "5.2", + "4.7", + "4.8", + "5.4", + "5.2", + "5.5", + "4.9", + "5.0", + "5.5", + "4.9", + "4.4", + "5.1", + "5.0", + "4.5", + "4.4", + "5.0", + "5.1", + "4.8", + "5.1", + "4.6", + "5.3", + "5.0" + ] + }, + { + "label": "SepalWidth", + "values": [ + "3.5", + "3.0", + "3.2", + "3.1", + "3.6", + "3.9", + "3.4", + "3.4", + "2.9", + "3.1", + "3.7", + "3.4", + "3.0", + "3.0", + "4.0", + "4.4", + "3.9", + "3.5", + "3.8", + "3.8", + "3.4", + "3.7", + "3.6", + "3.3", + "3.4", + "3.0", + "3.4", + "3.5", + "3.4", + "3.2", + "3.1", + "3.4", + "4.1", + "4.2", + "3.1", + "3.2", + "3.5", + "3.1", + "3.0", + "3.4", + "3.5", + "2.3", + "3.2", + "3.5", + "3.8", + "3.0", + "3.8", + "3.2", + "3.7", + "3.3" + ] + }, + { + "label": "PetalLength", + "values": [ + "1.4", + "1.4", + "1.3", + "1.5", + "1.4", + "1.7", + "1.4", + "1.5", + "1.4", + "1.5", + "1.5", + "1.6", + "1.4", + "1.1", + "1.2", + "1.5", + "1.3", + "1.4", + "1.7", + "1.5", + "1.7", + "1.5", + "1.0", + "1.7", + "1.9", + "1.6", + "1.6", + "1.5", + "1.4", + "1.6", + "1.6", + "1.5", + "1.5", + "1.4", + "1.5", + "1.2", + "1.3", + "1.5", + "1.3", + "1.5", + "1.3", + "1.3", + "1.3", + "1.6", + "1.9", + "1.4", + "1.6", + "1.4", + "1.5", + "1.4" + ] + }, + { + "label": "PetalWidth", + "values": [ + "0.2", + "0.2", + "0.2", + "0.2", + "0.2", + "0.4", + "0.3", + "0.2", + "0.2", + "0.1", + "0.2", + "0.2", + "0.1", + "0.1", + "0.2", + "0.4", + "0.4", + "0.3", + "0.3", + "0.3", + "0.2", + "0.4", + "0.2", + "0.5", + "0.2", + "0.2", + "0.4", + "0.2", + "0.2", + "0.2", + "0.2", + "0.4", + "0.1", + "0.2", + "0.1", + "0.2", + "0.2", + "0.1", + "0.2", + "0.2", + "0.3", + "0.3", + "0.2", + "0.6", + "0.4", + "0.3", + "0.2", + "0.2", + "0.2", + "0.2" + ] + } + ], + "marker": { + "color": "red" + } + }, + { + "type": "splom", + "name": "Versicolor", + "diagonal": {"visible": false}, + "dimensions": [ + { + "label": "SepalLength", + "values": [ + "7.0", + "6.4", + "6.9", + "5.5", + "6.5", + "5.7", + "6.3", + "4.9", + "6.6", + "5.2", + "5.0", + "5.9", + "6.0", + "6.1", + "5.6", + "6.7", + "5.6", + "5.8", + "6.2", + "5.6", + "5.9", + "6.1", + "6.3", + "6.1", + "6.4", + "6.6", + "6.8", + "6.7", + "6.0", + "5.7", + "5.5", + "5.5", + "5.8", + "6.0", + "5.4", + "6.0", + "6.7", + "6.3", + "5.6", + "5.5", + "5.5", + "6.1", + "5.8", + "5.0", + "5.6", + "5.7", + "5.7", + "6.2", + "5.1", + "5.7" + ] + }, + { + "label": "SepalWidth", + "values": [ + "3.2", + "3.2", + "3.1", + "2.3", + "2.8", + "2.8", + "3.3", + "2.4", + "2.9", + "2.7", + "2.0", + "3.0", + "2.2", + "2.9", + "2.9", + "3.1", + "3.0", + "2.7", + "2.2", + "2.5", + "3.2", + "2.8", + "2.5", + "2.8", + "2.9", + "3.0", + "2.8", + "3.0", + "2.9", + "2.6", + "2.4", + "2.4", + "2.7", + "2.7", + "3.0", + "3.4", + "3.1", + "2.3", + "3.0", + "2.5", + "2.6", + "3.0", + "2.6", + "2.3", + "2.7", + "3.0", + "2.9", + "2.9", + "2.5", + "2.8" + ] + }, + { + "label": "PetalLength", + "values": [ + "4.7", + "4.5", + "4.9", + "4.0", + "4.6", + "4.5", + "4.7", + "3.3", + "4.6", + "3.9", + "3.5", + "4.2", + "4.0", + "4.7", + "3.6", + "4.4", + "4.5", + "4.1", + "4.5", + "3.9", + "4.8", + "4.0", + "4.9", + "4.7", + "4.3", + "4.4", + "4.8", + "5.0", + "4.5", + "3.5", + "3.8", + "3.7", + "3.9", + "5.1", + "4.5", + "4.5", + "4.7", + "4.4", + "4.1", + "4.0", + "4.4", + "4.6", + "4.0", + "3.3", + "4.2", + "4.2", + "4.2", + "4.3", + "3.0", + "4.1" + ] + }, + { + "label": "PetalWidth", + "values": [ + "1.4", + "1.5", + "1.5", + "1.3", + "1.5", + "1.3", + "1.6", + "1.0", + "1.3", + "1.4", + "1.0", + "1.5", + "1.0", + "1.4", + "1.3", + "1.4", + "1.5", + "1.0", + "1.5", + "1.1", + "1.8", + "1.3", + "1.5", + "1.2", + "1.3", + "1.4", + "1.4", + "1.7", + "1.5", + "1.0", + "1.1", + "1.0", + "1.2", + "1.6", + "1.5", + "1.6", + "1.5", + "1.3", + "1.3", + "1.3", + "1.2", + "1.4", + "1.2", + "1.0", + "1.3", + "1.2", + "1.3", + "1.3", + "1.1", + "1.3" + ] + } + ], + "marker": { + "color": "green" + } + }, + { + "type": "splom", + "name": "Virginica", + "diagonal": {"visible": false}, + "dimensions": [ + { + "label": "SepalLength", + "values": [ + "6.3", + "5.8", + "7.1", + "6.3", + "6.5", + "7.6", + "4.9", + "7.3", + "6.7", + "7.2", + "6.5", + "6.4", + "6.8", + "5.7", + "5.8", + "6.4", + "6.5", + "7.7", + "7.7", + "6.0", + "6.9", + "5.6", + "7.7", + "6.3", + "6.7", + "7.2", + "6.2", + "6.1", + "6.4", + "7.2", + "7.4", + "7.9", + "6.4", + "6.3", + "6.1", + "7.7", + "6.3", + "6.4", + "6.0", + "6.9", + "6.7", + "6.9", + "5.8", + "6.8", + "6.7", + "6.7", + "6.3", + "6.5", + "6.2", + "5.9" + ] + }, + { + "label": "SepalWidth", + "values": [ + "3.3", + "2.7", + "3.0", + "2.9", + "3.0", + "3.0", + "2.5", + "2.9", + "2.5", + "3.6", + "3.2", + "2.7", + "3.0", + "2.5", + "2.8", + "3.2", + "3.0", + "3.8", + "2.6", + "2.2", + "3.2", + "2.8", + "2.8", + "2.7", + "3.3", + "3.2", + "2.8", + "3.0", + "2.8", + "3.0", + "2.8", + "3.8", + "2.8", + "2.8", + "2.6", + "3.0", + "3.4", + "3.1", + "3.0", + "3.1", + "3.1", + "3.1", + "2.7", + "3.2", + "3.3", + "3.0", + "2.5", + "3.0", + "3.4", + "3.0" + ] + }, + { + "label": "PetalLength", + "values": [ + "6.0", + "5.1", + "5.9", + "5.6", + "5.8", + "6.6", + "4.5", + "6.3", + "5.8", + "6.1", + "5.1", + "5.3", + "5.5", + "5.0", + "5.1", + "5.3", + "5.5", + "6.7", + "6.9", + "5.0", + "5.7", + "4.9", + "6.7", + "4.9", + "5.7", + "6.0", + "4.8", + "4.9", + "5.6", + "5.8", + "6.1", + "6.4", + "5.6", + "5.1", + "5.6", + "6.1", + "5.6", + "5.5", + "4.8", + "5.4", + "5.6", + "5.1", + "5.1", + "5.9", + "5.7", + "5.2", + "5.0", + "5.2", + "5.4", + "5.1" + ] + }, + { + "label": "PetalWidth", + "values": [ + "2.5", + "1.9", + "2.1", + "1.8", + "2.2", + "2.1", + "1.7", + "1.8", + "1.8", + "2.5", + "2.0", + "1.9", + "2.1", + "2.0", + "2.4", + "2.3", + "1.8", + "2.2", + "2.3", + "1.5", + "2.3", + "2.0", + "2.0", + "1.8", + "2.1", + "1.8", + "1.8", + "1.8", + "2.1", + "1.6", + "1.9", + "2.0", + "2.2", + "1.5", + "1.4", + "2.3", + "2.4", + "1.8", + "1.8", + "2.1", + "2.4", + "2.3", + "1.9", + "2.3", + "2.5", + "2.3", + "1.9", + "2.0", + "2.3", + "1.8" + ] + } + ], + "marker": { + "color": "blue" + } + } + ], + "layout": { + "title": "Iris dataset splom", + "width": 600, + "height": 500, + "legend": { + "x": 0, + "xanchor": "left", + "y": 1, + "yanchor": "top" + } + } +} diff --git a/test/jasmine/assets/custom_assertions.js b/test/jasmine/assets/custom_assertions.js index 9923b23add9..86496bb2033 100644 --- a/test/jasmine/assets/custom_assertions.js +++ b/test/jasmine/assets/custom_assertions.js @@ -246,9 +246,6 @@ exports.assertElemInside = function(elem, container, msg) { * quick plot area dimension check: test width and/or height of the inner * plot area (single subplot) to verify that the margins are as expected * - * Note: if you use margin.pad on the plot, width and height will be larger - * than you expected by twice that padding. - * * opts can have keys (all optional): * width (exact width match) * height (exact height match) @@ -261,7 +258,7 @@ exports.assertPlotSize = function(opts, msg) { var widthLessThan = opts.widthLessThan; var heightLessThan = opts.heightLessThan; - var plotBB = d3.select('.bglayer .bg').node().getBoundingClientRect(); + var plotBB = d3.select('.plotclip > rect').node().getBoundingClientRect(); var actualWidth = plotBB.width; var actualHeight = plotBB.height; diff --git a/test/jasmine/tests/cartesian_test.js b/test/jasmine/tests/cartesian_test.js index 6f5c26b203b..e4d9888e2cf 100644 --- a/test/jasmine/tests/cartesian_test.js +++ b/test/jasmine/tests/cartesian_test.js @@ -520,7 +520,8 @@ describe('subplot creation / deletion:', function() { yaxis2: {domain: [0.5, 1], anchor: 'x2'}, yaxis3: {overlaying: 'y'}, // legend makes its own .bg rect - delete so we can ignore that here - showlegend: false + showlegend: false, + plot_bgcolor: '#d3d3d3' }) .then(function() { // touching but not overlapping: all backgrounds are in back @@ -553,6 +554,74 @@ describe('subplot creation / deletion:', function() { .then(done); }); + it('puts not have backgrounds nodes when plot and paper color match', function(done) { + Plotly.plot(gd, [ + {y: [1, 2, 3]}, + {y: [2, 3, 1], xaxis: 'x2', yaxis: 'y2'}, + {y: [3, 1, 2], yaxis: 'y3'} + ], { + xaxis: {domain: [0, 0.5]}, + xaxis2: {domain: [0.5, 1], anchor: 'y2'}, + yaxis: {domain: [0, 1]}, + yaxis2: {domain: [0.5, 1], anchor: 'x2'}, + yaxis3: {overlaying: 'y'}, + // legend makes its own .bg rect - delete so we can ignore that here + showlegend: false, + plot_bgcolor: 'white', + paper_bgcolor: 'white' + }) + .then(function() { + // touching but not overlapping, matching colors -> no + checkBGLayers(0, 0, ['xy', 'x2y2', 'xy3']); + + // now add a slight overlap: that's enough to put x2y2 in front + return Plotly.relayout(gd, {'xaxis2.domain': [0.49, 1]}); + }) + .then(function() { + // need to draw one backgroud + checkBGLayers(0, 1, ['xy', 'x2y2', 'xy3']); + + // x ranges overlap, but now y ranges are disjoint + return Plotly.relayout(gd, {'xaxis2.domain': [0, 1], 'yaxis.domain': [0, 0.5]}); + }) + .then(function() { + // disjoint, matching colors -> no + checkBGLayers(0, 0, ['xy', 'x2y2', 'xy3']); + + // regular inset + return Plotly.relayout(gd, { + 'xaxis.domain': [0, 1], + 'yaxis.domain': [0, 1], + 'xaxis2.domain': [0.6, 0.9], + 'yaxis2.domain': [0.6, 0.9] + }); + }) + .then(function() { + // need to draw one backgroud + checkBGLayers(0, 1, ['xy', 'x2y2', 'xy3']); + + // change paper color + return Plotly.relayout(gd, 'paper_bgcolor', 'black'); + }) + .then(function() { + // need a backgroud on main subplot to distinguish plot from + // paper color + checkBGLayers(1, 1, ['xy', 'x2y2', 'xy3']); + + // change bg colors to same semi-transparent color + return Plotly.relayout(gd, { + 'paper_bgcolor': 'rgba(255,0,0,0.2)', + 'plot_bgcolor': 'rgba(255,0,0,0.2)' + }); + }) + .then(function() { + // still need a to get correct semi-transparent look + checkBGLayers(1, 1, ['xy', 'x2y2', 'xy3']); + }) + .catch(failTest) + .then(done); + }); + it('should clear overlaid subplot trace layers on restyle', function(done) { var fig = Lib.extendDeep({}, require('@mocks/overlaying-axis-lines.json')); diff --git a/test/jasmine/tests/gl2d_click_test.js b/test/jasmine/tests/gl2d_click_test.js index c2b2121e386..94d0d97d84a 100644 --- a/test/jasmine/tests/gl2d_click_test.js +++ b/test/jasmine/tests/gl2d_click_test.js @@ -1135,4 +1135,46 @@ describe('@noCI Test gl2d lasso/select:', function() { .catch(failTest) .then(done); }); + + it('@gl should work on overlaid subplots', function(done) { + gd = createGraphDiv(); + + var scene, scene2; + + Plotly.plot(gd, [{ + x: [1, 2, 3], + y: [40, 50, 60], + type: 'scattergl', + mode: 'markers' + }, { + x: [2, 3, 4], + y: [4, 5, 6], + yaxis: 'y2', + type: 'scattergl', + mode: 'markers' + }], { + xaxis: {domain: [0.2, 1]}, + yaxis2: {overlaying: 'y', side: 'left', position: 0}, + showlegend: false, + margin: {l: 0, t: 0, b: 0, r: 0}, + width: 400, + height: 400, + dragmode: 'select' + }) + .then(delay(100)) + .then(function() { + scene = gd._fullLayout._plots.xy._scene; + scene2 = gd._fullLayout._plots.xy2._scene; + + spyOn(scene.scatter2d, 'draw'); + spyOn(scene2.scatter2d, 'draw'); + }) + .then(function() { return select([[20, 20], [380, 250]]); }) + .then(function() { + expect(scene.scatter2d.draw).toHaveBeenCalledTimes(1); + expect(scene2.scatter2d.draw).toHaveBeenCalledTimes(1); + }) + .catch(failTest) + .then(done); + }); }); diff --git a/test/jasmine/tests/gl2d_plot_interact_test.js b/test/jasmine/tests/gl2d_plot_interact_test.js index bb4e3a1dda5..aa01c2a671a 100644 --- a/test/jasmine/tests/gl2d_plot_interact_test.js +++ b/test/jasmine/tests/gl2d_plot_interact_test.js @@ -1182,7 +1182,10 @@ describe('Test scattergl autorange:', function() { describe('should return the approximative values for ~big~ data', function() { beforeEach(function() { - spyOn(ScatterGl, 'plot'); + // to avoid expansive draw calls (which could be problematic on CI) + spyOn(ScatterGl, 'plot').and.callFake(function(gd) { + gd._fullLayout._plots.xy._scene.scatter2d = {draw: function() {}}; + }); }); // threshold for 'fast' axis expansion routine diff --git a/test/jasmine/tests/plot_api_test.js b/test/jasmine/tests/plot_api_test.js index 4e38fc1e982..6b974857fdd 100644 --- a/test/jasmine/tests/plot_api_test.js +++ b/test/jasmine/tests/plot_api_test.js @@ -6,6 +6,7 @@ var Queue = require('@src/lib/queue'); var Scatter = require('@src/traces/scatter'); var Bar = require('@src/traces/bar'); var Legend = require('@src/components/legend'); +var Axes = require('@src/plots/cartesian/axes'); var pkg = require('../../../package.json'); var subroutines = require('@src/plot_api/subroutines'); var helpers = require('@src/plot_api/helpers'); @@ -531,14 +532,18 @@ describe('Test plot api', function() { mockedMethods.forEach(function(m) { spyOn(subroutines, m); }); + spyOn(Axes, 'doTicks'); + spyOn(Plots, 'supplyDefaults').and.callThrough(); }); function mock(gd) { mockedMethods.forEach(function(m) { subroutines[m].calls.reset(); }); + Axes.doTicks.calls.reset(); supplyAllDefaults(gd); + Plots.supplyDefaults.calls.reset(); Plots.doCalcdata(gd); gd.emit = function() {}; return gd; @@ -634,7 +639,7 @@ describe('Test plot api', function() { }); it('should trigger minimal sequence for cartesian axis range updates', function() { - var seq = ['doAutoRangeAndConstraints', 'doTicksRelayout', 'drawData', 'finalDraw']; + var seq = ['doAutoRangeAndConstraints', 'drawData', 'finalDraw']; function _assert(msg) { expect(gd.calcdata).toBeDefined(); @@ -644,6 +649,10 @@ describe('Test plot api', function() { '# of ' + m + ' calls - ' + msg ); }); + expect(Axes.doTicks).toHaveBeenCalledTimes(1); + expect(Axes.doTicks.calls.allArgs()[0][1]).toEqual(['x']); + expect(Axes.doTicks.calls.allArgs()[0][2]).toBe(true, 'skip-axis-title argument'); + expect(Plots.supplyDefaults).not.toHaveBeenCalled(); } var specs = [ @@ -2588,6 +2597,7 @@ describe('Test plot api', function() { spyOn(annotations, 'drawOne').and.callThrough(); spyOn(annotations, 'draw').and.callThrough(); spyOn(images, 'draw').and.callThrough(); + spyOn(Axes, 'doTicks').and.callThrough(); }); afterEach(destroyGraphDiv); @@ -2823,10 +2833,11 @@ describe('Test plot api', function() { Plotly.newPlot(gd, data, layout) .then(countPlots) .then(function() { + expect(Axes.doTicks).toHaveBeenCalledWith(gd, ''); return Plotly.react(gd, data, layout2); }) .then(function() { - expect(subroutines.doTicksRelayout).toHaveBeenCalledTimes(1); + expect(Axes.doTicks).toHaveBeenCalledWith(gd, 'redraw'); expect(subroutines.layoutStyles).not.toHaveBeenCalled(); }) .catch(failTest) diff --git a/test/jasmine/tests/plots_test.js b/test/jasmine/tests/plots_test.js index ede60e8b130..6e00acd23d7 100644 --- a/test/jasmine/tests/plots_test.js +++ b/test/jasmine/tests/plots_test.js @@ -153,26 +153,6 @@ describe('Test Plots', function() { testSanitizeMarginsHasBeenCalledOnlyOnce(gd); }); - - it('should sort base plot modules on fullLayout object', function() { - var gd = Lib.extendDeep({}, require('@mocks/plot_types.json')); - gd.data.unshift({type: 'scattergl'}); - gd.data.push({type: 'splom'}); - - supplyAllDefaults(gd); - var names = gd._fullLayout._basePlotModules.map(function(m) { - return m.name; - }); - - expect(names).toEqual([ - 'splom', - 'cartesian', - 'gl3d', - 'geo', - 'pie', - 'ternary' - ]); - }); }); describe('Plots.supplyLayoutGlobalDefaults should', function() { diff --git a/test/jasmine/tests/select_test.js b/test/jasmine/tests/select_test.js index 116a26b52b0..2a903e20c1c 100644 --- a/test/jasmine/tests/select_test.js +++ b/test/jasmine/tests/select_test.js @@ -649,25 +649,16 @@ describe('Click-to-select', function() { }); }); - // The gl traces: use @gl CI annotation + // The gl and mapbox traces: use @gl and @noCI tag [ testCase('scatterpolargl', require('@mocks/glpolar_scatter.json'), 130, 290, [[], [], [], [19], [], []], { dragmode: 'zoom' }), - testCase('splom', require('@mocks/splom_lower.json'), 427, 400, [[], [7], []]) - ] - .forEach(function(testCase) { - it('@gl trace type ' + testCase.label, function(done) { - _run(testCase, done); - }); - }); - - // The mapbox traces: use @noCI annotation cause they are usually too resource-intensive - [ + testCase('splom', require('@mocks/splom_lower.json'), 427, 400, [[], [7], []]), testCase('scattermapbox', require('@mocks/mapbox_0.json'), 650, 195, [[2], []], {}, { mapboxAccessToken: require('@build/credentials.json').MAPBOX_ACCESS_TOKEN }) ] .forEach(function(testCase) { - it('@noCI trace type ' + testCase.label, function(done) { + it('@noCI @gl trace type ' + testCase.label, function(done) { _run(testCase, done); }); }); diff --git a/test/jasmine/tests/splom_test.js b/test/jasmine/tests/splom_test.js index b44bf5d5917..ab8c21c7a92 100644 --- a/test/jasmine/tests/splom_test.js +++ b/test/jasmine/tests/splom_test.js @@ -1,6 +1,7 @@ var Plotly = require('@lib'); var Lib = require('@src/lib'); var Plots = require('@src/plots/plots'); +var Axes = require('@src/plots/cartesian/axes'); var SUBPLOT_PATTERN = require('@src/plots/cartesian/constants').SUBPLOT_PATTERN; var d3 = require('d3'); @@ -74,7 +75,7 @@ describe('Test splom trace defaults:', function() { expect(fullLayout.yaxis.domain).toBeCloseToArray([0, 1]); }); - it('should set `grid.xaxes` and `grid.yaxes` default using the new of dimensions', function() { + it('should set `grid.xaxes` and `grid.yaxes` default using the number of dimensions', function() { _supply({ dimensions: [ {values: [1, 2, 3]}, @@ -101,6 +102,105 @@ describe('Test splom trace defaults:', function() { expect(subplots.cartesian).toEqual(['xy', 'xy2', 'x2y', 'x2y2']); }); + it('should set `grid.xaxes` and `grid.yaxes` default using the number of dimensions (no upper half, no diagonal case)', function() { + _supply({ + dimensions: [ + {values: [1, 2, 3]}, + {values: [2, 1, 2]}, + {values: [3, 1, 5]} + ], + showupperhalf: false, + diagonal: {visible: false} + }); + + var fullTrace = gd._fullData[0]; + expect(fullTrace.xaxes).toEqual(['x', 'x2', 'x3']); + expect(fullTrace.yaxes).toEqual(['y', 'y2', 'y3']); + + var fullLayout = gd._fullLayout; + expect(fullLayout.xaxis.domain).toBeCloseToArray([0, 0.47]); + expect(fullLayout.yaxis2.domain).toBeCloseToArray([0.53, 1]); + expect(fullLayout.xaxis2.domain).toBeCloseToArray([0.53, 1]); + expect(fullLayout.yaxis3.domain).toBeCloseToArray([0, 0.47]); + + var subplots = fullLayout._subplots; + expect(subplots.xaxis).toEqual(['x', 'x2']); + expect(subplots.yaxis).toEqual(['y2', 'y3']); + expect(subplots.cartesian).toEqual(['xy2', 'xy3', 'x2y3']); + }); + + it('should set `grid.xaxes` and `grid.yaxes` default using the number of dimensions (no lower half, no diagonal case)', function() { + _supply({ + dimensions: [ + {values: [1, 2, 3]}, + {values: [2, 1, 2]}, + {values: [3, 1, 5]} + ], + showlowerhalf: false, + diagonal: {visible: false} + }); + + var fullTrace = gd._fullData[0]; + expect(fullTrace.xaxes).toEqual(['x', 'x2', 'x3']); + expect(fullTrace.yaxes).toEqual(['y', 'y2', 'y3']); + + var fullLayout = gd._fullLayout; + expect(fullLayout.xaxis2.domain).toBeCloseToArray([0, 0.47]); + expect(fullLayout.yaxis.domain).toBeCloseToArray([0.53, 1]); + expect(fullLayout.xaxis3.domain).toBeCloseToArray([0.53, 1]); + expect(fullLayout.yaxis2.domain).toBeCloseToArray([0, 0.47]); + + var subplots = fullLayout._subplots; + expect(subplots.xaxis).toEqual(['x2', 'x3']); + expect(subplots.yaxis).toEqual(['y', 'y2']); + expect(subplots.cartesian).toEqual(['x2y', 'x3y', 'x3y2']); + }); + + it('should set `grid.xaxes` and `grid.yaxes` default using the number of dimensions (no upper half, no diagonal, set x|y axes case)', function() { + _supply({ + dimensions: [ + {values: [1, 2, 3]}, + {values: [2, 1, 2]}, + {values: [3, 1, 5]} + ], + showupperhalf: false, + diagonal: {visible: false}, + xaxes: ['x5', 'x6', 'x7'], + yaxes: ['y6', 'y7', 'y8'] + }); + + var fullTrace = gd._fullData[0]; + expect(fullTrace.xaxes).toEqual(['x5', 'x6', 'x7']); + expect(fullTrace.yaxes).toEqual(['y6', 'y7', 'y8']); + + var subplots = gd._fullLayout._subplots; + expect(subplots.xaxis).toEqual(['x5', 'x6']); + expect(subplots.yaxis).toEqual(['y7', 'y8']); + expect(subplots.cartesian).toEqual(['x5y7', 'x5y8', 'x6y8']); + }); + + it('should set `grid.xaxes` and `grid.yaxes` default using the number of dimensions (no lower half, no diagonal, set x|y axes case)', function() { + _supply({ + dimensions: [ + {values: [1, 2, 3]}, + {values: [2, 1, 2]}, + {values: [3, 1, 5]} + ], + showlowerhalf: false, + diagonal: {visible: false}, + xaxes: ['x5', 'x6', 'x7'], + yaxes: ['y6', 'y7', 'y8'] + }); + + var fullTrace = gd._fullData[0]; + expect(fullTrace.xaxes).toEqual(['x5', 'x6', 'x7']); + expect(fullTrace.yaxes).toEqual(['y6', 'y7', 'y8']); + + var subplots = gd._fullLayout._subplots; + expect(subplots.xaxis).toEqual(['x6', 'x7']); + expect(subplots.yaxis).toEqual(['y6', 'y7']); + expect(subplots.cartesian).toEqual(['x6y6', 'x7y6', 'x7y7']); + }); it('should use special `grid.xside` and `grid.yside` defaults on splom w/o lower half generated grids', function() { var gridOut; @@ -422,10 +522,11 @@ describe('Test splom trace calc step:', function() { yaxis: {type: 'linear'} }); - var cd = gd.calcdata[0][0]; + var trace = gd._fullData[0]; + var scene = gd._fullLayout._splomScenes[trace.uid]; - expect(cd.t._scene.matrixOptions.data).toBeCloseTo2DArray([[2, 1, 2]]); - expect(cd.t.visibleDims).toEqual([1]); + expect(scene.matrixOptions.data).toBeCloseTo2DArray([[2, 1, 2]]); + expect(trace._visibleDims).toEqual([1]); expect(Lib.log).toHaveBeenCalledTimes(1); expect(Lib.log).toHaveBeenCalledWith('Skipping splom dimension 0 with conflicting axis types'); }); @@ -448,13 +549,14 @@ describe('Test splom interactions:', function() { Plotly.plot(gd, fig).then(function() { expect(gd._fullLayout._splomGrid).toBeDefined(); - expect(gd.calcdata[0][0].t._scene).toBeDefined(); + expect(gd._fullLayout._splomScenes).toBeDefined(); + expect(Object.keys(gd._fullLayout._splomScenes).length).toBe(1); - return Plots.cleanPlot([], {}, gd._fullData, gd._fullLayout, gd.calcdata); + return Plots.cleanPlot([], {}, gd._fullData, gd._fullLayout); }) .then(function() { - expect(gd._fullLayout._splomGrid).toBe(null); - expect(gd.calcdata[0][0].t._scene).toBe(null); + expect(gd._fullLayout._splomGrid).toBeUndefined(); + expect(gd._fullLayout._splomScenes).toBeUndefined(); }) .catch(failTest) .then(done); @@ -486,14 +588,14 @@ describe('Test splom interactions:', function() { return Plotly.restyle(gd, 'diagonal.visible', false); }) .then(function() { - _assert([1138, 7636, 1594, 4]); + _assert([1138, 7654, 1600]); return Plotly.restyle(gd, { showupperhalf: true, showlowerhalf: false }); }) .then(function() { - _assert([64, 8176, 1582, 112]); + _assert([8188, 112, 1588]); return Plotly.restyle(gd, 'diagonal.visible', true); }) .then(function() { @@ -522,7 +624,9 @@ describe('Test splom interactions:', function() { function _assert(exp) { var msg = ' - call #' + cnt; - var subplots = d3.selectAll('g.cartesianlayer > g.subplot'); + var gd3 = d3.select(gd); + var subplots = gd3.selectAll('g.cartesianlayer > g.subplot'); + var bgs = gd3.selectAll('.bglayer > rect.bg'); expect(subplots.size()) .toBe(exp.subplotCnt, '# of ' + msg); @@ -543,22 +647,47 @@ describe('Test splom interactions:', function() { expect(!!gd._fullLayout._splomGrid) .toBe(exp.hasSplomGrid, 'has regl-line2d splom grid' + msg); + expect(bgs.size()).toBe(exp.bgCnt, '# of ' + msg); + cnt++; } Plotly.plot(gd, figLarge).then(function() { _assert({ subplotCnt: 400, - innerSubplotNodeCnt: 5, - hasSplomGrid: true + innerSubplotNodeCnt: 4, + hasSplomGrid: true, + bgCnt: 0 }); + + return Plotly.relayout(gd, 'paper_bgcolor', 'red'); + }) + .then(function() { + _assert({ + subplotCnt: 400, + innerSubplotNodeCnt: 4, + hasSplomGrid: true, + bgCnt: 400 + }); + + return Plotly.relayout(gd, 'plot_bgcolor', 'red'); + }) + .then(function() { + _assert({ + subplotCnt: 400, + innerSubplotNodeCnt: 4, + hasSplomGrid: true, + bgCnt: 0 + }); + return Plotly.restyle(gd, 'dimensions', [dimsSmall]); }) .then(function() { _assert({ subplotCnt: 25, innerSubplotNodeCnt: 17, - hasSplomGrid: false + hasSplomGrid: false, + bgCnt: 0 }); // make sure 'new' subplot layers are in order @@ -588,9 +717,10 @@ describe('Test splom interactions:', function() { // new subplots though have reduced number of children. innerSubplotNodeCnt: function(d) { var p = d.match(SUBPLOT_PATTERN); - return (p[1] > 5 || p[2] > 5) ? 5 : 17; + return (p[1] > 5 || p[2] > 5) ? 4 : 17; }, - hasSplomGrid: true + hasSplomGrid: true, + bgCnt: 0 }); }) .catch(failTest) @@ -617,21 +747,21 @@ describe('Test splom interactions:', function() { expect(gd._fullLayout.grid.yside).toBe('left', 'sanity check dflt grid.yside'); _assert({ - x: 433, x2: 433, x3: 433, + x2: 433, x3: 433, x4: 433, y: 80, y2: 80, y3: 80 }); return Plotly.relayout(gd, 'grid.yside', 'left plot'); }) .then(function() { _assert({ - x: 433, x2: 433, x3: 433, + x2: 433, x3: 433, x4: 433, y: 79, y2: 230, y3: 382 }); return Plotly.relayout(gd, 'grid.xside', 'bottom plot'); }) .then(function() { _assert({ - x: 212, x2: 323, x3: 433, + x2: 212, x3: 323, x4: 433, y: 79, y2: 230, y3: 382 }); }) @@ -658,14 +788,22 @@ describe('Test splom interactions:', function() { var fig = Lib.extendDeep({}, require('@mocks/splom_iris.json')); function _assert(msg, exp) { + var splomScenes = gd._fullLayout._splomScenes; + var ids = Object.keys(splomScenes); + for(var i = 0; i < 3; i++) { - expect(Boolean(gd.calcdata[i][0].t._scene)) - .toBe(Boolean(exp[i]), msg + ' - trace ' + i); + var drawFn = splomScenes[ids[i]].draw; + expect(drawFn).toHaveBeenCalledTimes(exp[i], msg + ' - trace ' + i); + drawFn.calls.reset(); } } Plotly.plot(gd, fig).then(function() { - _assert('base', [1, 1, 1]); + var splomScenes = gd._fullLayout._splomScenes; + for(var k in splomScenes) { + spyOn(splomScenes[k], 'draw').and.callThrough(); + } + return Plotly.restyle(gd, 'visible', 'legendonly', [0, 2]); }) .then(function() { @@ -683,7 +821,7 @@ describe('Test splom interactions:', function() { .then(done); }); - it('@gl should clear graph and replot when canvas and WebGL context dimensions do not match', function(done) { + it('@noCI @gl should clear graph and replot when canvas and WebGL context dimensions do not match', function(done) { var fig = Lib.extendDeep({}, require('@mocks/splom_iris.json')); function assertDims(msg, w, h) { @@ -735,6 +873,270 @@ describe('Test splom interactions:', function() { .catch(failTest) .then(done); }); + + it('@gl should update axis arrangement on show(upper|lower)half + diagonal.visible restyles', function(done) { + var seq = ['', '2', '3', '4']; + + function getAxesTypes(cont, letter) { + return seq.map(function(s) { + var ax = cont[letter + 'axis' + s]; + return ax ? ax.type : null; + }); + } + + // undefined means there's an axis object, but no 'type' key in it + // null means there's no axis object + function _assertAxisTypes(msg, exp) { + var xaxes = getAxesTypes(gd.layout, 'x'); + var yaxes = getAxesTypes(gd.layout, 'y'); + var fullXaxes = getAxesTypes(gd._fullLayout, 'x'); + var fullYaxes = getAxesTypes(gd._fullLayout, 'y'); + + expect(xaxes).toEqual(exp.xaxes, msg); + expect(fullXaxes).toEqual(exp.fullXaxes, msg); + expect(yaxes).toEqual(exp.yaxes, msg); + expect(fullYaxes).toEqual(exp.fullYaxes, msg); + } + + var data = [{ + type: 'splom', + showupperhalf: false, + diagonal: {visible: false}, + dimensions: [{ + values: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] + }, { + values: ['lyndon', 'richard', 'gerald', 'jimmy', 'ronald', 'george', 'bill', 'georgeW', 'barack', 'donald'] + }, { + values: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + axis: {type: 'category'} + }, { + values: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + axis: {type: 'log'} + }] + }]; + + Plotly.plot(gd, data).then(function() { + _assertAxisTypes('no upper half / no diagonal', { + xaxes: ['linear', 'category', undefined, null], + fullXaxes: ['linear', 'category', 'category', null], + yaxes: [null, 'category', undefined, undefined], + fullYaxes: [null, 'category', 'category', 'log'] + }); + + return Plotly.restyle(gd, { + 'showupperhalf': true, + 'diagonal.visible': true + }); + }) + .then(function() { + _assertAxisTypes('full grid', { + xaxes: ['linear', 'category', undefined, undefined], + fullXaxes: ['linear', 'category', 'category', 'log'], + yaxes: ['linear', 'category', undefined, undefined], + fullYaxes: ['linear', 'category', 'category', 'log'] + }); + + return Plotly.restyle(gd, { + 'showlowerhalf': false, + 'diagonal.visible': false + }); + }) + .then(function() { + _assertAxisTypes('no lower half / no diagonal', { + xaxes: ['linear', 'category', undefined, undefined], + fullXaxes: [null, 'category', 'category', 'log'], + yaxes: ['linear', 'category', undefined, undefined], + fullYaxes: ['linear', 'category', 'category', null] + }); + }) + .catch(failTest) + .then(done); + }); +}); + +describe('Test splom update switchboard:', function() { + var gd; + + beforeEach(function() { + gd = createGraphDiv(); + }); + + afterEach(function() { + Plotly.purge(gd); + destroyGraphDiv(); + }); + + var methods; + + function addSpies() { + methods.forEach(function(m) { + var obj = m[0]; + var k = m[1]; + spyOn(obj, k).and.callThrough(); + }); + } + + function assertSpies(msg, exp) { + methods.forEach(function(m, i) { + var obj = m[0]; + var k = m[1]; + var expi = exp[i]; + expect(obj[k]).toHaveBeenCalledTimes(expi[1], expi[0]); + obj[k].calls.reset(); + }); + } + + function toPlainArray(typedArray) { + return Array.prototype.slice.call(typedArray); + } + + it('@gl should trigger minimal sequence for axis range updates (large splom case)', function(done) { + var fig = Lib.extendDeep({}, require('@mocks/splom_large.json')); + var matrix, regl, splomGrid; + + Plotly.plot(gd, fig).then(function() { + var fullLayout = gd._fullLayout; + var trace = gd._fullData[0]; + var scene = fullLayout._splomScenes[trace.uid]; + matrix = scene.matrix; + regl = matrix.regl; + splomGrid = fullLayout._splomGrid; + + methods = [ + [Plots, 'supplyDefaults'], + [Axes, 'doTicks'], + [regl, 'clear'], + [splomGrid, 'update'] + ]; + addSpies(); + + expect(fullLayout.xaxis.range).toBeCloseToArray([-0.0921, 0.9574], 1, 'xrng (base)'); + + return Plotly.relayout(gd, 'xaxis.range', [0, 1]); + }) + .then(function() { + var msg = 'after update'; + + assertSpies(msg, [ + ['supplyDefaults', 0], + ['doTicks', 1], + ['regl clear', 1], + ['splom grid update', 1], + ['splom grid draw', 1], + ['splom matrix update', 1], + ['splom matrix draw', 1] + ]); + + expect(gd.layout.xaxis.range).toBeCloseToArray([0, 1], 1, 'xrng ' + msg); + expect(gd._fullLayout.xaxis.range).toBeCloseToArray([0, 1], 1, 'xrng ' + msg); + }) + .catch(failTest) + .then(done); + }); + + it('@gl should trigger minimal sequence for marker style updates', function(done) { + var fig = Lib.extendDeep({}, require('@mocks/splom_0.json')); + var scene, matrix, regl; + + Plotly.plot(gd, fig).then(function() { + var fullLayout = gd._fullLayout; + var trace = gd._fullData[0]; + scene = fullLayout._splomScenes[trace.uid]; + matrix = scene.matrix; + regl = matrix.regl; + + methods = [ + [Plots, 'supplyDefaults'], + [Plots, 'doCalcdata'], + [Axes, 'doTicks'], + [regl, 'clear'], + [matrix, 'update'], + [matrix, 'draw'] + ]; + addSpies(); + + expect(toPlainArray(scene.matrixOptions.color)) + .toBeCloseToArray([31, 119, 180, 255], 1, 'base color'); + expect(scene.matrixOptions.size).toBe(3, 'base size'); + expect(fullLayout.xaxis.range).toBeCloseToArray([0.851, 3.148], 1, 'base xrng'); + + return Plotly.restyle(gd, 'marker.color', 'black'); + }) + .then(function() { + var msg = 'after scaler marker.color restyle'; + + assertSpies(msg, [ + ['supplyDefaults', 1], + ['doCalcdata', 0], + ['doTicks', 0], + ['regl clear', 1], + ['update', 1], + ['draw', 1] + ]); + + expect(toPlainArray(scene.matrixOptions.color)) + .toBeCloseToArray([0, 0, 0, 255], 1, msg); + + return Plotly.restyle(gd, 'marker.color', [['red', 'green', 'blue']]); + }) + .then(function() { + var msg = 'after arrayOk marker.color restyle'; + + assertSpies(msg, [ + ['supplyDefaults', 1], + ['doCalcdata', 0], + ['doTicks', 0], + ['clear', 1], + ['update', 1], + ['draw', 1] + ]); + + expect(toPlainArray(scene.matrixOptions.colors[0])) + .toBeCloseToArray([1, 0, 0, 1], 1, msg + '- 0'); + expect(toPlainArray(scene.matrixOptions.colors[1])) + .toBeCloseToArray([0, 0.501, 0, 1], 1, msg + '- 1'); + expect(toPlainArray(scene.matrixOptions.colors[2])) + .toBeCloseToArray([0, 0, 1, 1], 1, msg + '- 2'); + + return Plotly.restyle(gd, 'marker.size', 20); + }) + .then(function() { + var msg = 'after scalar marker.size restyle'; + + assertSpies(msg, [ + ['supplyDefaults', 1], + ['doCalcdata', 1], + ['doTicks', 1], + ['regl clear', 1], + ['update', 1], + ['draw', 1] + ]); + + expect(scene.matrixOptions.size).toBe(10, msg); + expect(gd._fullLayout.xaxis.range) + .toBeCloseToArray([0.753, 3.246], 1, 'xrng ' + msg); + + return Plotly.restyle(gd, 'marker.size', [[4, 10, 20]]); + }) + .then(function() { + var msg = 'after scalar marker.size restyle'; + + assertSpies(msg, [ + ['supplyDefaults', 1], + ['doCalcdata', 1], + ['doTicks', 1], + ['regl clear', 1], + ['update', 1], + ['draw', 1] + ]); + + expect(scene.matrixOptions.sizes).toBeCloseToArray([2, 5, 10], 1, msg); + expect(gd._fullLayout.xaxis.range) + .toBeCloseToArray([0.853, 3.235], 1, 'xrng ' + msg); + }) + .catch(failTest) + .then(done); + }); }); describe('Test splom hover:', function() { @@ -890,7 +1292,8 @@ describe('Test splom drag:', function() { Plotly.plot(gd, fig) .then(function() { - var scene = gd.calcdata[0][0].t._scene; + var uid = gd._fullData[0].uid; + var scene = gd._fullLayout._splomScenes[uid]; spyOn(scene.matrix, 'update'); spyOn(scene.matrix, 'draw'); @@ -906,13 +1309,14 @@ describe('Test splom drag:', function() { }) .then(function() { return _drag([130, 130], [150, 150]); }) .then(function() { - var scene = gd.calcdata[0][0].t._scene; + var uid = gd._fullData[0].uid; + var scene = gd._fullLayout._splomScenes[uid]; // N.B. _drag triggers two updateSubplots call // - 1 update and 1 draw call per updateSubplot - // - 2 update calls (1 for data, 1 for view opts) + // - 1 update calls for data+view opts // during splom plot on mouseup // - 1 draw call during splom plot on mouseup - expect(scene.matrix.update).toHaveBeenCalledTimes(4); + expect(scene.matrix.update).toHaveBeenCalledTimes(3); expect(scene.matrix.draw).toHaveBeenCalledTimes(3); _assertRanges('after drag', [ @@ -1072,23 +1476,26 @@ describe('Test splom select:', function() { var cnt = 0; var scatterGlCnt = 0; var splomCnt = 0; + var scatterglScene, splomScene; Plotly.newPlot(gd, fig).then(function() { - // 'scattergl' trace module - spyOn(gd._fullLayout._modules[0], 'style').and.callFake(function() { + var fullLayout = gd._fullLayout; + scatterglScene = fullLayout._plots.xy._scene; + splomScene = fullLayout._splomScenes[gd._fullData[1].uid]; + + spyOn(scatterglScene, 'draw').and.callFake(function() { cnt++; scatterGlCnt = cnt; }); - // 'splom' trace module - spyOn(gd._fullLayout._modules[1], 'style').and.callFake(function() { + spyOn(splomScene, 'draw').and.callFake(function() { cnt++; splomCnt = cnt; }); }) .then(function() { return _select([[20, 395], [195, 205]]); }) .then(function() { - expect(gd._fullLayout._modules[0].style).toHaveBeenCalledTimes(1); - expect(gd._fullLayout._modules[1].style).toHaveBeenCalledTimes(1); + expect(scatterglScene.draw).toHaveBeenCalledTimes(1); + expect(splomScene.draw).toHaveBeenCalledTimes(1); expect(cnt).toBe(2); expect(splomCnt).toBe(1, 'splom redraw before scattergl'); @@ -1098,7 +1505,7 @@ describe('Test splom select:', function() { .then(done); }); - it('@gl should behave correctly during select->dblclick->pan scenarios', function(done) { + it('@noCI @gl should behave correctly during select->dblclick->pan scenarios', function(done) { var fig = Lib.extendDeep({}, require('@mocks/splom_0.json')); fig.layout = { width: 400, @@ -1107,7 +1514,7 @@ describe('Test splom select:', function() { grid: {xgap: 0, ygap: 0} }; - var scene; + var uid, scene; function _assert(msg, exp) { expect(scene.matrix.update).toHaveBeenCalledTimes(exp.updateCnt, 'update cnt'); @@ -1122,7 +1529,8 @@ describe('Test splom select:', function() { } Plotly.plot(gd, fig).then(function() { - scene = gd.calcdata[0][0].t._scene; + uid = gd._fullData[0].uid; + scene = gd._fullLayout._splomScenes[uid]; spyOn(scene.matrix, 'update').and.callThrough(); spyOn(scene.matrix, 'draw').and.callThrough(); }) diff --git a/test/jasmine/tests/streamtube_test.js b/test/jasmine/tests/streamtube_test.js index e15eceb4bc4..647fb49f5a2 100644 --- a/test/jasmine/tests/streamtube_test.js +++ b/test/jasmine/tests/streamtube_test.js @@ -295,7 +295,7 @@ describe('Test streamtube interactions', function() { }); }); -describe('Test streamtube hover', function() { +describe('@noCI Test streamtube hover', function() { var gd; beforeEach(function() {