diff --git a/src/components/errorbars/plot.js b/src/components/errorbars/plot.js index 3f44ed58df1..eb66769760e 100644 --- a/src/components/errorbars/plot.js +++ b/src/components/errorbars/plot.js @@ -34,15 +34,24 @@ module.exports = function plot(traces, plotinfo) { trace.marker.maxdisplayed > 0 ); + var keyFunc; + + if(trace.key) { + keyFunc = function(d) { return d.key; }; + } + if(!yObj.visible && !xObj.visible) return; - var errorbars = d3.select(this).selectAll('g.errorbar') - .data(Lib.identity); + var selection = d3.select(this).selectAll('g.errorbar'); - errorbars.enter().append('g') + var join = selection.data(Lib.identity, keyFunc); + + join.enter().append('g') .classed('errorbar', true); - errorbars.each(function(d) { + join.exit().remove(); + + join.each(function(d) { var errorbar = d3.select(this); var coords = errorCoords(d, xa, ya); diff --git a/src/plots/cartesian/index.js b/src/plots/cartesian/index.js index 56f827f0ec6..0f931fe5b7b 100644 --- a/src/plots/cartesian/index.js +++ b/src/plots/cartesian/index.js @@ -25,61 +25,74 @@ exports.attrRegex = constants.attrRegex; exports.attributes = require('./attributes'); -exports.plot = function(gd) { +exports.plot = function(gd, traces) { + var cdSubplot, cd, trace, i, j, k, isFullReplot; + var fullLayout = gd._fullLayout, subplots = Plots.getSubplotIds(fullLayout, 'cartesian'), calcdata = gd.calcdata, modules = fullLayout._modules; - function getCdSubplot(calcdata, subplot) { - var cdSubplot = []; - - for(var i = 0; i < calcdata.length; i++) { - var cd = calcdata[i]; - var trace = cd[0].trace; - - if(trace.xaxis + trace.yaxis === subplot) { - cdSubplot.push(cd); - } - } - - return cdSubplot; + if (!Array.isArray(traces)) { + // If traces is not provided, then it's a complete replot and missing + // traces are removed + isFullReplot = true; + traces = []; + for (i = 0; i < calcdata.length; i++) { + traces.push(i); + } + } else { + // If traces are explicitly specified, then it's a partial replot and + // traces are not removed. + isFullReplot = false; } - function getCdModule(cdSubplot, _module) { - var cdModule = []; + for(var i = 0; i < subplots.length; i++) { + var subplot = subplots[i], + subplotInfo = fullLayout._plots[subplot]; - for(var i = 0; i < cdSubplot.length; i++) { - var cd = cdSubplot[i]; + // Get all calcdata for this subplot: + cdSubplot = []; + for(j = 0; j < calcdata.length; j++) { + var cd = calcdata[j]; var trace = cd[0].trace; - if((trace._module === _module) && (trace.visible === true)) { - cdModule.push(cd); + // Skip trace if whitelist provided and it's not whitelisted: + // if (Array.isArray(traces) && traces.indexOf(i) === -1) continue; + + if(trace.xaxis + trace.yaxis === subplot && traces.indexOf(trace.index) !== -1) { + cdSubplot.push(cd); } } - return cdModule; - } - - for(var i = 0; i < subplots.length; i++) { - var subplot = subplots[i], - subplotInfo = fullLayout._plots[subplot], - cdSubplot = getCdSubplot(calcdata, subplot); - // remove old traces, then redraw everything - // TODO: use enter/exit appropriately in the plot functions - // so we don't need this - should sometimes be a big speedup - if(subplotInfo.plot) subplotInfo.plot.selectAll('g.trace').remove(); + // TODO: scatterlayer is manually excluded from this since it knows how + // to update instead of fully removing and redrawing every time. The + // remaining plot traces should also be able to do this. Once implemented, + // we won't need this - which should sometimes be a big speedup. + if(subplotInfo.plot) { + subplotInfo.plot.selectAll('g:not(.scatterlayer)').selectAll('g.trace').remove(); + } - for(var j = 0; j < modules.length; j++) { + // Plot all traces for each module at once: + for(j = 0; j < modules.length; j++) { var _module = modules[j]; // skip over non-cartesian trace modules if(_module.basePlotModule.name !== 'cartesian') continue; // plot all traces of this type on this subplot at once - var cdModule = getCdModule(cdSubplot, _module); - _module.plot(gd, subplotInfo, cdModule); + var cdModule = []; + for(k = 0; k < cdSubplot.length; k++) { + var cd = cdSubplot[k]; + var trace = cd[0].trace; + + if((trace._module === _module) && (trace.visible === true)) { + cdModule.push(cd); + } + } + + _module.plot(gd, subplotInfo, cdModule, isFullReplot); } } }; diff --git a/src/plots/plots.js b/src/plots/plots.js index 4b814bfd85c..ae7c1d68eb0 100644 --- a/src/plots/plots.js +++ b/src/plots/plots.js @@ -587,7 +587,8 @@ plots.cleanPlot = function(newFullData, newFullLayout, oldFullData, oldFullLayou oldFullLayout._paper.selectAll( '.hm' + oldUid + ',.contour' + oldUid + - ',#clip' + oldUid + ',#clip' + oldUid + + ',.trace' + oldUid ).remove(); } diff --git a/src/traces/scatter/link_traces.js b/src/traces/scatter/link_traces.js new file mode 100644 index 00000000000..7338f2a3f57 --- /dev/null +++ b/src/traces/scatter/link_traces.js @@ -0,0 +1,44 @@ +/** +* Copyright 2012-2016, Plotly, Inc. +* All rights reserved. +* +* This source code is licensed under the MIT license found in the +* LICENSE file in the root directory of this source tree. +*/ + +'use strict'; + +var Plots = require('../../plots/plots'); + +module.exports = function linkTraces(gd, plotinfo, cdscatter) { + var i, cd, trace; + var xa = plotinfo.x(); + var ya = plotinfo.y(); + + var prevtrace = null; + + for(i = 0; i < cdscatter.length; ++i) { + cd = cdscatter[i]; + trace = cd[0].trace; + + // console.log('visible:', trace.uid, trace.visible); + if(trace.visible === true && Plots.traceIs(trace, 'cartesian') && + trace.xaxis === xa._id && + trace.yaxis === ya._id) { + + trace._nexttrace = null; + + if(['tonextx', 'tonexty', 'tonext'].indexOf(trace.fill) !== -1) { + trace._prevtrace = prevtrace; + + if(prevtrace) { + prevtrace._nexttrace = trace; + } + } + + prevtrace = trace; + } else { + trace._prevtrace = trace._nexttrace = null; + } + } +}; diff --git a/src/traces/scatter/plot.js b/src/traces/scatter/plot.js index 2044c854fa1..5d656b7a831 100644 --- a/src/traces/scatter/plot.js +++ b/src/traces/scatter/plot.js @@ -18,74 +18,152 @@ var ErrorBars = require('../../components/errorbars'); var subTypes = require('./subtypes'); var arraysToCalcdata = require('./arrays_to_calcdata'); var linePoints = require('./line_points'); +var linkTraces = require('./link_traces'); +module.exports = function plot(gd, plotinfo, cdscatter, isFullReplot) { + var i, uids, selection, join; -module.exports = function plot(gd, plotinfo, cdscatter) { - selectMarkers(gd, plotinfo, cdscatter); + var scatterlayer = plotinfo.plot.select('g.scatterlayer'); - var xa = plotinfo.x(), - ya = plotinfo.y(); + selection = scatterlayer.selectAll('g.trace'); - // make the container for scatter plots - // (so error bars can find them along with bars) - var scattertraces = plotinfo.plot.select('.scatterlayer') - .selectAll('g.trace.scatter') - .data(cdscatter); + join = selection.data(cdscatter, function(d) {return d[0].trace.uid;}); - scattertraces.enter().append('g') - .attr('class', 'trace scatter') + // Append new traces: + var traceEnter = join.enter().append('g') + .attr('class', function (d) { + return 'trace scatter trace' + d[0].trace.uid; + }) .style('stroke-miterlimit', 2); - // error bars are at the bottom - scattertraces.call(ErrorBars.plot, plotinfo); + // After the elements are created but before they've been draw, we have to perform + // this extra step of linking the traces. This allows appending of fill layers so that + // the z-order of fill layers is correct. + linkTraces(gd, plotinfo, cdscatter); - // BUILD LINES AND FILLS - var prevpath = '', - ownFillEl3, ownFillDir, tonext, nexttonext; + createFills(gd, scatterlayer); - scattertraces.each(function(d) { - var trace = d[0].trace, - line = trace.line, - tr = d3.select(this); - if(trace.visible !== true) return; + traceEnter.each(function(d) { + plotOne(gd, plotinfo, d, this); + }); + + // Before performing a data join, style existing traces. This avoid .transition() with + // zero duration, which seems to still invoke a timing loop that's much slower than a + // plain style: + selection.each(function(d) { + plotOne(gd, plotinfo, d, this); + }); + + if (isFullReplot) { + join.exit().remove(); + } + + // Sort the traces, once created, so that the ordering is preserved even when traces + // are shown and hidden. This is needed since we're not just wiping everything out + // and recreating on every update. + for(i = 0, uids = []; i < cdscatter.length; i++) { + uids[i] = cdscatter[i][0].trace.uid; + } - ownFillDir = trace.fill.charAt(trace.fill.length - 1); - if(ownFillDir !== 'x' && ownFillDir !== 'y') ownFillDir = ''; + scatterlayer.selectAll('g.trace').sort(function(a, b) { + var idx1 = uids.indexOf(a[0].trace.uid); + var idx2 = uids.indexOf(b[0].trace.uid); + return idx1 > idx2 ? 1 : -1; + }); - d[0].node3 = tr; // store node for tweaking by selectPoints + // remove paths that didn't get used + scatterlayer.selectAll('path:not([d])').remove(); +}; - arraysToCalcdata(d); +function createFills(gd, scatterlayer) { + var trace; - if(!subTypes.hasLines(trace) && trace.fill === 'none') return; + scatterlayer.selectAll('g.trace').each(function(d) { + var tr = d3.select(this); - var thispath, - thisrevpath, - // fullpath is all paths for this curve, joined together straight - // across gaps, for filling - fullpath = '', - // revpath is fullpath reversed, for fill-to-next - revpath = '', - // functions for converting a point array to a path - pathfn, revpathbase, revpathfn; + // Loop only over the traces being redrawn: + trace = d[0].trace; - // make the fill-to-zero path now, so it shows behind the line - // fill to next puts the fill associated with one trace - // grouped with the previous if(trace.fill.substr(0, 6) === 'tozero' || trace.fill === 'toself' || - (trace.fill.substr(0, 2) === 'to' && !prevpath)) { - ownFillEl3 = tr.append('path') - .classed('js-fill', true); + (trace.fill.substr(0, 2) === 'to' && !trace._prevtrace)) { + trace._ownFill = tr.select('.js-fill.js-tozero'); + if(!trace._ownFill.size()) { + trace._ownFill = tr.insert('path', ':first-child').attr('class', 'js-fill js-tozero'); + } + } else { + tr.selectAll('.js-fill.js-tozero').remove(); + trace._ownFill = null; } - else ownFillEl3 = null; // make the fill-to-next path now for the NEXT trace, so it shows // behind both lines. - // nexttonext was created last time, but give it - // this curve's data for fill color - if(nexttonext) tonext = nexttonext.datum(d); + if(trace._nexttrace) { + trace._nextFill = tr.select('.js-fill.js-tonext'); + if(!trace._nextFill.size()) { + trace._nextFill = tr.insert('path', ':first-child').attr('class', 'js-fill js-tonext'); + } + } else { + tr.selectAll('.js-fill.js-tonext').remove(); + trace._nextFill = null; + } + }); +} + + +function plotOne(gd, plotinfo, cdscatter, element) { + var join; + + selectMarkers(gd, plotinfo, cdscatter); + + var xa = plotinfo.x(), + ya = plotinfo.y(); + + var trace = cdscatter[0].trace, + line = trace.line, + tr = d3.select(element); + + // (so error bars can find them along with bars) + // error bars are at the bottom + tr.call(ErrorBars.plot, plotinfo); + + if(trace.visible !== true) return; + + // BUILD LINES AND FILLS + var ownFillEl3, tonext; + var ownFillDir = trace.fill.charAt(trace.fill.length - 1); + if(ownFillDir !== 'x' && ownFillDir !== 'y') ownFillDir = ''; + + // store node for tweaking by selectPoints + cdscatter[0].node3 = tr; + + arraysToCalcdata(cdscatter); + + var prevpath = ''; + var prevtrace = trace._prevtrace; + + if(prevtrace) { + prevpath = prevtrace._revpath || ''; + tonext = prevtrace._nextFill; + } + + var thispath, + thisrevpath, + // fullpath is all paths for this curve, joined together straight + // across gaps, for filling + fullpath = '', + // revpath is fullpath reversed, for fill-to-next + revpath = '', + // functions for converting a point array to a path + pathfn, revpathbase, revpathfn; + + ownFillEl3 = trace._ownFill; - // now make a new nexttonext for next time - nexttonext = tr.append('path').classed('js-fill', true); + if(subTypes.hasLines(trace) || trace.fill !== 'none') { + + if(tonext) { + // This tells .style which trace to use for fill information: + tonext.datum(cdscatter); + } if(['hv', 'vh', 'hvh', 'vhv'].indexOf(line.shape) !== -1) { pathfn = Drawing.steps(line.shape); @@ -117,7 +195,7 @@ module.exports = function plot(gd, plotinfo, cdscatter) { return revpathbase(pts.reverse()); }; - var segments = linePoints(d, { + var segments = linePoints(cdscatter, { xaxis: xa, yaxis: ya, connectGaps: trace.connectgaps, @@ -147,7 +225,12 @@ module.exports = function plot(gd, plotinfo, cdscatter) { revpath = thisrevpath + 'Z' + revpath; } if(subTypes.hasLines(trace) && pts.length > 1) { - tr.append('path').classed('js-line', true).attr('d', thispath); + var lineJoin = tr.selectAll('.js-line').data([cdscatter]); + + lineJoin.enter() + .append('path').classed('js-line', true).attr('d', thispath); + + lineJoin.attr('d', thispath); } } if(ownFillEl3) { @@ -163,9 +246,10 @@ module.exports = function plot(gd, plotinfo, cdscatter) { // fill to zero: full trace path, plus extension of // the endpoints to the appropriate axis ownFillEl3.attr('d', fullpath + 'L' + pt1 + 'L' + pt0 + 'Z'); + } else { + // fill to self: just join the path to itself + ownFillEl3.attr('d', fullpath + 'Z'); } - // fill to self: just join the path to itself - else ownFillEl3.attr('d', fullpath + 'Z'); } } else if(trace.fill.substr(0, 6) === 'tonext' && fullpath && prevpath) { @@ -186,46 +270,89 @@ module.exports = function plot(gd, plotinfo, cdscatter) { tonext.attr('d', fullpath + 'L' + prevpath.substr(1) + 'Z'); } } - prevpath = revpath; } - }); - // remove paths that didn't get used - scattertraces.selectAll('path:not([d])').remove(); + trace._revpath = revpath; + } + function visFilter(d) { return d.filter(function(v) { return v.vis; }); } - scattertraces.append('g') - .attr('class', 'points') - .each(function(d) { - var trace = d[0].trace, - s = d3.select(this), - showMarkers = subTypes.hasMarkers(trace), - showText = subTypes.hasText(trace); - - if((!showMarkers && !showText) || trace.visible !== true) s.remove(); - else { - if(showMarkers) { - s.selectAll('path.point') - .data(trace.marker.maxdisplayed ? visFilter : Lib.identity) - .enter().append('path') - .classed('point', true) - .call(Drawing.translatePoints, xa, ya); - } - if(showText) { - s.selectAll('g') - .data(trace.marker.maxdisplayed ? visFilter : Lib.identity) - // each text needs to go in its own 'g' in case - // it gets converted to mathjax - .enter().append('g') - .append('text') - .call(Drawing.translatePoints, xa, ya); - } + function keyFunc(d) { + return d.key; + } + + // Returns a function if the trace is keyed, otherwise returns + function getKeyFunc(trace) { + if(trace.key) { + return keyFunc; + } + } + + function makePoints(d) { + var join, selection; + var trace = d[0].trace, + s = d3.select(this), + showMarkers = subTypes.hasMarkers(trace), + showText = subTypes.hasText(trace); + + if((!showMarkers && !showText) || trace.visible !== true) s.remove(); + else { + if(showMarkers) { + selection = s.selectAll('path.point'); + + join = selection + .data(trace.marker.maxdisplayed ? visFilter : Lib.identity, getKeyFunc(trace)); + + join.enter().append('path') + .classed('point', true) + .call(Drawing.pointStyle, trace) + .call(Drawing.translatePoints, xa, ya); + + selection + .call(Drawing.translatePoints, xa, ya) + .call(Drawing.pointStyle, trace); + + join.exit().remove(); } - }); -}; + if(showText) { + selection = s.selectAll('g'); + + join = selection + .data(trace.marker.maxdisplayed ? visFilter : Lib.identity); + + // each text needs to go in its own 'g' in case + // it gets converted to mathjax + join.enter().append('g') + .append('text') + .call(Drawing.translatePoints, xa, ya); + + selection + .call(Drawing.translatePoints, xa, ya); + + join.exit().remove(); + } + } + } + + // NB: selectAll is evaluated on instantiation: + var pointSelection = tr.selectAll('.points'); + + // Join with new data + join = pointSelection.data([cdscatter]); + + // Transition existing, but don't defer this to an async .transition since + // there's no timing involved: + pointSelection.each(makePoints); + + join.enter().append('g') + .classed('points', true) + .each(makePoints); + + join.exit().remove(); +} function selectMarkers(gd, plotinfo, cdscatter) { var xa = plotinfo.x(), @@ -233,40 +360,41 @@ function selectMarkers(gd, plotinfo, cdscatter) { xr = d3.extent(xa.range.map(xa.l2c)), yr = d3.extent(ya.range.map(ya.l2c)); - cdscatter.forEach(function(d, i) { - var trace = d[0].trace; - if(!subTypes.hasMarkers(trace)) return; - // if marker.maxdisplayed is used, select a maximum of - // mnum markers to show, from the set that are in the viewport - var mnum = trace.marker.maxdisplayed; - - // TODO: remove some as we get away from the viewport? - if(mnum === 0) return; - - var cd = d.filter(function(v) { - return v.x >= xr[0] && v.x <= xr[1] && v.y >= yr[0] && v.y <= yr[1]; - }), - inc = Math.ceil(cd.length / mnum), - tnum = 0; - cdscatter.forEach(function(cdj, j) { - var tracei = cdj[0].trace; - if(subTypes.hasMarkers(tracei) && - tracei.marker.maxdisplayed > 0 && j < i) { - tnum++; - } - }); + // XXX: Not okay. Just makes it work for now. + var i = 0; + + var trace = cdscatter[0].trace; + if(!subTypes.hasMarkers(trace)) return; + // if marker.maxdisplayed is used, select a maximum of + // mnum markers to show, from the set that are in the viewport + var mnum = trace.marker.maxdisplayed; + + // TODO: remove some as we get away from the viewport? + if(mnum === 0) return; + + var cd = cdscatter.filter(function(v) { + return v.x >= xr[0] && v.x <= xr[1] && v.y >= yr[0] && v.y <= yr[1]; + }), + inc = Math.ceil(cd.length / mnum), + tnum = 0; + cdscatter.forEach(function(cdj, j) { + var tracei = cdj[0].trace; + if(subTypes.hasMarkers(tracei) && + tracei.marker.maxdisplayed > 0 && j < i) { + tnum++; + } + }); - // if multiple traces use maxdisplayed, stagger which markers we - // display this formula offsets successive traces by 1/3 of the - // increment, adding an extra small amount after each triplet so - // it's not quite periodic - var i0 = Math.round(tnum * inc / 3 + Math.floor(tnum / 3) * inc / 7.1); - - // for error bars: save in cd which markers to show - // so we don't have to repeat this - d.forEach(function(v) { delete v.vis; }); - cd.forEach(function(v, i) { - if(Math.round((i + i0) % inc) === 0) v.vis = true; - }); + // if multiple traces use maxdisplayed, stagger which markers we + // display this formula offsets successive traces by 1/3 of the + // increment, adding an extra small amount after each triplet so + // it's not quite periodic + var i0 = Math.round(tnum * inc / 3 + Math.floor(tnum / 3) * inc / 7.1); + + // for error bars: save in cd which markers to show + // so we don't have to repeat this + cdscatter.forEach(function(v) { delete v.vis; }); + cd.forEach(function(v, i) { + if(Math.round((i + i0) % inc) === 0) v.vis = true; }); }