diff --git a/src/lib/index.js b/src/lib/index.js index abe8e1a8fa8..cd9f7e2ad6b 100644 --- a/src/lib/index.js +++ b/src/lib/index.js @@ -118,6 +118,8 @@ lib.clearThrottle = throttleModule.clear; lib.getGraphDiv = require('./get_graph_div'); +lib.makeTraceGroups = require('./make_trace_groups'); + lib._ = require('./localize'); lib.notifier = require('./notifier'); diff --git a/src/lib/make_trace_groups.js b/src/lib/make_trace_groups.js new file mode 100644 index 00000000000..b4a571f8e8d --- /dev/null +++ b/src/lib/make_trace_groups.js @@ -0,0 +1,35 @@ +/** +* 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'; + +/** + * General helper to manage trace groups based on calcdata + * + * @param {d3.selection} traceLayer: a selection containing a single group + * to draw these traces into + * @param {array} cdModule: array of calcdata items for this + * module and subplot combination. Assumes the calcdata item for each + * trace is an array with the fullData trace attached to the first item. + * @param {string} cls: the class attribute to give each trace group + * so you can give multiple classes separated by spaces + */ +module.exports = function makeTraceGroups(traceLayer, cdModule, cls) { + var traces = traceLayer.selectAll('g.' + cls.replace(/\s/g, '.')) + .data(cdModule, function(cd) { return cd[0].trace.uid; }); + + traces.exit().remove(); + + traces.enter().append('g') + .attr('class', cls); + + traces.order(); + + return traces; +}; diff --git a/src/plots/get_data.js b/src/plots/get_data.js index de6710a5159..3455d50fa7c 100644 --- a/src/plots/get_data.js +++ b/src/plots/get_data.js @@ -124,20 +124,3 @@ exports.getSubplotData = function getSubplotData(data, type, subplotId) { return subplotData; }; - -/** - * Get a lookup object of trace uids corresponding in a given calcdata array. - * - * @param {array} calcdata: as in gd.calcdata (or a subset) - * @return {object} lookup object of uids (`uid: 1`) - */ -exports.getUidsFromCalcData = function(calcdata) { - var out = {}; - - for(var i = 0; i < calcdata.length; i++) { - var trace = calcdata[i][0].trace; - out[trace.uid] = 1; - } - - return out; -}; diff --git a/src/traces/bar/plot.js b/src/traces/bar/plot.js index 309f6eca7c4..433b30d628d 100644 --- a/src/traces/bar/plot.js +++ b/src/traces/bar/plot.js @@ -35,30 +35,20 @@ module.exports = function plot(gd, plotinfo, cdbar, barLayer) { var ya = plotinfo.yaxis; var fullLayout = gd._fullLayout; - var bartraces = barLayer.selectAll('g.trace.bars') - .data(cdbar, function(d) { return d[0].trace.uid; }); - - bartraces.enter().append('g') - .attr('class', 'trace bars') - .append('g') - .attr('class', 'points'); - - bartraces.exit().remove(); - - bartraces.order(); - - bartraces.each(function(d) { - var cd0 = d[0]; + var bartraces = Lib.makeTraceGroups(barLayer, cdbar, 'trace bars').each(function(cd) { + var plotGroup = d3.select(this); + var cd0 = cd[0]; var t = cd0.t; var trace = cd0.trace; - var sel = d3.select(this); - if(!plotinfo.isRangePlot) cd0.node3 = sel; + if(!plotinfo.isRangePlot) cd0.node3 = plotGroup; var poffset = t.poffset; var poffsetIsArray = Array.isArray(poffset); - var bars = sel.select('g.points').selectAll('g.point').data(Lib.identity); + var pointGroup = Lib.ensureSingle(plotGroup, 'g', 'points'); + + var bars = pointGroup.selectAll('g.point').data(Lib.identity); bars.enter().append('g') .classed('point', true); @@ -147,17 +137,17 @@ module.exports = function plot(gd, plotinfo, cdbar, barLayer) { 'M' + x0 + ',' + y0 + 'V' + y1 + 'H' + x1 + 'V' + y0 + 'Z') .call(Drawing.setClipUrl, plotinfo.layerClipId); - appendBarText(gd, bar, d, i, x0, x1, y0, y1); + appendBarText(gd, bar, cd, i, x0, x1, y0, y1); if(plotinfo.layerClipId) { - Drawing.hideOutsideRangePoint(d[i], bar.select('text'), xa, ya, trace.xcalendar, trace.ycalendar); + Drawing.hideOutsideRangePoint(di, bar.select('text'), xa, ya, trace.xcalendar, trace.ycalendar); } }); // lastly, clip points groups of `cliponaxis !== false` traces // on `plotinfo._hasClipOnAxisFalse === true` subplots - var hasClipOnAxisFalse = d[0].trace.cliponaxis === false; - Drawing.setClipUrl(sel, hasClipOnAxisFalse ? null : plotinfo.layerClipId); + var hasClipOnAxisFalse = cd0.trace.cliponaxis === false; + Drawing.setClipUrl(plotGroup, hasClipOnAxisFalse ? null : plotinfo.layerClipId); }); // error bars are on the top diff --git a/src/traces/box/plot.js b/src/traces/box/plot.js index afc98857029..e8538b22957 100644 --- a/src/traces/box/plot.js +++ b/src/traces/box/plot.js @@ -21,28 +21,16 @@ function plot(gd, plotinfo, cdbox, boxLayer) { var fullLayout = gd._fullLayout; var xa = plotinfo.xaxis; var ya = plotinfo.yaxis; + var numBoxes = fullLayout._numBoxes; + var groupFraction = (1 - fullLayout.boxgap); + var group = (fullLayout.boxmode === 'group' && numBoxes > 1); - var boxtraces = boxLayer.selectAll('g.trace.boxes') - .data(cdbox, function(d) { return d[0].trace.uid; }); - - boxtraces.enter().append('g') - .attr('class', 'trace boxes'); - - boxtraces.exit().remove(); - - boxtraces.order(); - - boxtraces.each(function(d) { - var cd0 = d[0]; + Lib.makeTraceGroups(boxLayer, cdbox, 'trace boxes').each(function(cd) { + var plotGroup = d3.select(this); + var cd0 = cd[0]; var t = cd0.t; var trace = cd0.trace; - var sel = d3.select(this); - if(!plotinfo.isRangePlot) cd0.node3 = sel; - var numBoxes = fullLayout._numBoxes; - - var groupFraction = (1 - fullLayout.boxgap); - - var group = (fullLayout.boxmode === 'group' && numBoxes > 1); + if(!plotinfo.isRangePlot) cd0.node3 = plotGroup; // box half width var bdPos = t.dPos * groupFraction * (1 - fullLayout.boxgroupgap) / (group ? numBoxes : 1); // box center offset @@ -51,7 +39,7 @@ function plot(gd, plotinfo, cdbox, boxLayer) { var wdPos = bdPos * trace.whiskerwidth; if(trace.visible !== true || t.empty) { - sel.remove(); + plotGroup.remove(); return; } @@ -73,9 +61,9 @@ function plot(gd, plotinfo, cdbox, boxLayer) { // always split the distance to the closest box t.wHover = t.dPos * (group ? groupFraction / numBoxes : 1); - plotBoxAndWhiskers(sel, {pos: posAxis, val: valAxis}, trace, t); - plotPoints(sel, {x: xa, y: ya}, trace, t); - plotBoxMean(sel, {pos: posAxis, val: valAxis}, trace, t); + plotBoxAndWhiskers(plotGroup, {pos: posAxis, val: valAxis}, trace, t); + plotPoints(plotGroup, {x: xa, y: ya}, trace, t); + plotBoxMean(plotGroup, {pos: posAxis, val: valAxis}, trace, t); }); } diff --git a/src/traces/carpet/plot.js b/src/traces/carpet/plot.js index c1a04ba4087..c7ad270a55c 100644 --- a/src/traces/carpet/plot.js +++ b/src/traces/carpet/plot.js @@ -17,61 +17,45 @@ var orientText = require('./orient_text'); var svgTextUtils = require('../../lib/svg_text_utils'); var Lib = require('../../lib'); var alignmentConstants = require('../../constants/alignment'); -var getUidsFromCalcData = require('../../plots/get_data').getUidsFromCalcData; module.exports = function plot(gd, plotinfo, cdcarpet, carpetLayer) { - var uidLookup = getUidsFromCalcData(cdcarpet); - - carpetLayer.selectAll('g.trace').each(function() { - var classString = d3.select(this).attr('class'); - var oldUid = classString.split('carpet')[1].split(/\s/)[0]; - - if(!uidLookup[oldUid]) { - d3.select(this).remove(); - } - }); - - for(var i = 0; i < cdcarpet.length; i++) { - plotOne(gd, plotinfo, cdcarpet[i], carpetLayer); - } -}; - -function plotOne(gd, plotinfo, cd, carpetLayer) { - var t = cd[0]; - var trace = cd[0].trace, - xa = plotinfo.xaxis, - ya = plotinfo.yaxis, - aax = trace.aaxis, - bax = trace.baxis, - fullLayout = gd._fullLayout; - + var xa = plotinfo.xaxis; + var ya = plotinfo.yaxis; + var fullLayout = gd._fullLayout; var clipLayer = fullLayout._clips; - var axisLayer = Lib.ensureSingle(carpetLayer, 'g', 'carpet' + trace.uid).classed('trace', true); - var minorLayer = Lib.ensureSingle(axisLayer, 'g', 'minorlayer'); - var majorLayer = Lib.ensureSingle(axisLayer, 'g', 'majorlayer'); - var boundaryLayer = Lib.ensureSingle(axisLayer, 'g', 'boundarylayer'); - var labelLayer = Lib.ensureSingle(axisLayer, 'g', 'labellayer'); + Lib.makeTraceGroups(carpetLayer, cdcarpet, 'trace').each(function(cd) { + var axisLayer = d3.select(this); + var cd0 = cd[0]; + var trace = cd0.trace; + var aax = trace.aaxis; + var bax = trace.baxis; - axisLayer.style('opacity', trace.opacity); + var minorLayer = Lib.ensureSingle(axisLayer, 'g', 'minorlayer'); + var majorLayer = Lib.ensureSingle(axisLayer, 'g', 'majorlayer'); + var boundaryLayer = Lib.ensureSingle(axisLayer, 'g', 'boundarylayer'); + var labelLayer = Lib.ensureSingle(axisLayer, 'g', 'labellayer'); - drawGridLines(xa, ya, majorLayer, aax, 'a', aax._gridlines, true); - drawGridLines(xa, ya, majorLayer, bax, 'b', bax._gridlines, true); - drawGridLines(xa, ya, minorLayer, aax, 'a', aax._minorgridlines, true); - drawGridLines(xa, ya, minorLayer, bax, 'b', bax._minorgridlines, true); + axisLayer.style('opacity', trace.opacity); - // NB: These are not ommitted if the lines are not active. The joins must be executed - // in order for them to get cleaned up without a full redraw - drawGridLines(xa, ya, boundaryLayer, aax, 'a-boundary', aax._boundarylines); - drawGridLines(xa, ya, boundaryLayer, bax, 'b-boundary', bax._boundarylines); + drawGridLines(xa, ya, majorLayer, aax, 'a', aax._gridlines, true); + drawGridLines(xa, ya, majorLayer, bax, 'b', bax._gridlines, true); + drawGridLines(xa, ya, minorLayer, aax, 'a', aax._minorgridlines, true); + drawGridLines(xa, ya, minorLayer, bax, 'b', bax._minorgridlines, true); - var labelOrientationA = drawAxisLabels(gd, xa, ya, trace, t, labelLayer, aax._labels, 'a-label'); - var labelOrientationB = drawAxisLabels(gd, xa, ya, trace, t, labelLayer, bax._labels, 'b-label'); + // NB: These are not ommitted if the lines are not active. The joins must be executed + // in order for them to get cleaned up without a full redraw + drawGridLines(xa, ya, boundaryLayer, aax, 'a-boundary', aax._boundarylines); + drawGridLines(xa, ya, boundaryLayer, bax, 'b-boundary', bax._boundarylines); - drawAxisTitles(gd, labelLayer, trace, t, xa, ya, labelOrientationA, labelOrientationB); + var labelOrientationA = drawAxisLabels(gd, xa, ya, trace, cd0, labelLayer, aax._labels, 'a-label'); + var labelOrientationB = drawAxisLabels(gd, xa, ya, trace, cd0, labelLayer, bax._labels, 'b-label'); - drawClipPath(trace, t, clipLayer, xa, ya); -} + drawAxisTitles(gd, labelLayer, trace, cd0, xa, ya, labelOrientationA, labelOrientationB); + + drawClipPath(trace, cd0, clipLayer, xa, ya); + }); +}; function drawClipPath(trace, t, layer, xaxis, yaxis) { var seg, xp, yp, i; diff --git a/src/traces/choropleth/plot.js b/src/traces/choropleth/plot.js index fc0f0d176ac..2e29c3e467d 100644 --- a/src/traces/choropleth/plot.js +++ b/src/traces/choropleth/plot.js @@ -22,18 +22,8 @@ module.exports = function plot(gd, geo, calcData) { calcGeoJSON(calcData[i], geo.topojson); } - function keyFunc(d) { return d[0].trace.uid; } - - var gTraces = geo.layers.backplot.select('.choroplethlayer') - .selectAll('g.trace.choropleth') - .data(calcData, keyFunc); - - gTraces.enter().append('g') - .attr('class', 'trace choropleth'); - - gTraces.exit().remove(); - - gTraces.each(function(calcTrace) { + var choroplethLayer = geo.layers.backplot.select('.choroplethlayer'); + Lib.makeTraceGroups(choroplethLayer, calcData, 'trace choropleth').each(function(calcTrace) { var sel = calcTrace[0].node3 = d3.select(this); var paths = sel.selectAll('path.choroplethlocation') diff --git a/src/traces/contour/plot.js b/src/traces/contour/plot.js index e0e11f7fb34..7a7879d733d 100644 --- a/src/traces/contour/plot.js +++ b/src/traces/contour/plot.js @@ -27,78 +27,60 @@ var constants = require('./constants'); var costConstants = constants.LABELOPTIMIZER; exports.plot = function plot(gd, plotinfo, cdcontours, contourLayer) { - plotWrapper(gd, plotinfo, cdcontours, contourLayer, plotOne); -}; - -function plotWrapper(gd, plotinfo, cdcontours, contourLayer, plotOneFn) { - var contours = contourLayer.selectAll('g.contour') - .data( - cdcontours.map(function(d) { return d[0]; }), - function(cd) { return cd.trace.uid; } - ); - - contours.exit().remove(); - - contours.enter().append('g') - .classed('contour', true); - - contours.each(function(cd) { - plotOneFn(gd, plotinfo, cd, d3.select(this)); - }) - .order(); -} -exports.plotWrapper = plotWrapper; - -function plotOne(gd, plotinfo, cd, plotGroup) { - var trace = cd.trace; - var x = cd.x; - var y = cd.y; - var contours = trace.contours; var xa = plotinfo.xaxis; var ya = plotinfo.yaxis; var fullLayout = gd._fullLayout; - var pathinfo = emptyPathinfo(contours, plotinfo, cd); - - // use a heatmap to fill - draw it behind the lines - var heatmapColoringLayer = Lib.ensureSingle(plotGroup, 'g', 'heatmapcoloring'); - var cdheatmaps = []; - if(contours.coloring === 'heatmap') { - if(trace.zauto && (trace.autocontour === false)) { - trace._input.zmin = trace.zmin = - contours.start - contours.size / 2; - trace._input.zmax = trace.zmax = - trace.zmin + pathinfo.length * contours.size; + + Lib.makeTraceGroups(contourLayer, cdcontours, 'contour').each(function(cd) { + var plotGroup = d3.select(this); + var cd0 = cd[0]; + var trace = cd0.trace; + var x = cd0.x; + var y = cd0.y; + var contours = trace.contours; + var pathinfo = emptyPathinfo(contours, plotinfo, cd0); + + // use a heatmap to fill - draw it behind the lines + var heatmapColoringLayer = Lib.ensureSingle(plotGroup, 'g', 'heatmapcoloring'); + var cdheatmaps = []; + if(contours.coloring === 'heatmap') { + if(trace.zauto && (trace.autocontour === false)) { + trace._input.zmin = trace.zmin = + contours.start - contours.size / 2; + trace._input.zmax = trace.zmax = + trace.zmin + pathinfo.length * contours.size; + } + cdheatmaps = [cd]; + } + heatmapPlot(gd, plotinfo, cdheatmaps, heatmapColoringLayer); + + makeCrossings(pathinfo); + findAllPaths(pathinfo); + + var leftedge = xa.c2p(x[0], true), + rightedge = xa.c2p(x[x.length - 1], true), + bottomedge = ya.c2p(y[0], true), + topedge = ya.c2p(y[y.length - 1], true), + perimeter = [ + [leftedge, topedge], + [rightedge, topedge], + [rightedge, bottomedge], + [leftedge, bottomedge] + ]; + + var fillPathinfo = pathinfo; + if(contours.type === 'constraint') { + fillPathinfo = convertToConstraints(pathinfo, contours._operation); + closeBoundaries(fillPathinfo, contours._operation, perimeter, trace); } - cdheatmaps = [[cd]]; - } - heatmapPlot(gd, plotinfo, cdheatmaps, heatmapColoringLayer); - - makeCrossings(pathinfo); - findAllPaths(pathinfo); - - var leftedge = xa.c2p(x[0], true), - rightedge = xa.c2p(x[x.length - 1], true), - bottomedge = ya.c2p(y[0], true), - topedge = ya.c2p(y[y.length - 1], true), - perimeter = [ - [leftedge, topedge], - [rightedge, topedge], - [rightedge, bottomedge], - [leftedge, bottomedge] - ]; - - var fillPathinfo = pathinfo; - if(contours.type === 'constraint') { - fillPathinfo = convertToConstraints(pathinfo, contours._operation); - closeBoundaries(fillPathinfo, contours._operation, perimeter, trace); - } - // draw everything - makeBackground(plotGroup, perimeter, contours); - makeFills(plotGroup, fillPathinfo, perimeter, contours); - makeLinesAndLabels(plotGroup, pathinfo, gd, cd, contours, perimeter); - clipGaps(plotGroup, plotinfo, fullLayout._clips, cd, perimeter); -} + // draw everything + makeBackground(plotGroup, perimeter, contours); + makeFills(plotGroup, fillPathinfo, perimeter, contours); + makeLinesAndLabels(plotGroup, pathinfo, gd, cd0, contours, perimeter); + clipGaps(plotGroup, plotinfo, fullLayout._clips, cd0, perimeter); + }); +}; function makeBackground(plotgroup, perimeter, contours) { var bggroup = Lib.ensureSingle(plotgroup, 'g', 'contourbg'); @@ -653,8 +635,6 @@ function clipGaps(plotGroup, plotinfo, clips, cd0, perimeter) { else clipId = null; plotGroup.call(Drawing.setClipUrl, clipId); - plotinfo.plot.selectAll('.hm' + cd0.trace.uid) - .call(Drawing.setClipUrl, clipId); } function makeClipMask(cd0) { diff --git a/src/traces/contour/style.js b/src/traces/contour/style.js index dfbd81b88f4..f7005a03d14 100644 --- a/src/traces/contour/style.js +++ b/src/traces/contour/style.js @@ -21,12 +21,12 @@ module.exports = function style(gd) { var contours = d3.select(gd).selectAll('g.contour'); contours.style('opacity', function(d) { - return d.trace.opacity; + return d[0].trace.opacity; }); contours.each(function(d) { var c = d3.select(this); - var trace = d.trace; + var trace = d[0].trace; var contours = trace.contours; var line = trace.line; var cs = contours.size || 1; diff --git a/src/traces/contourcarpet/plot.js b/src/traces/contourcarpet/plot.js index 6b1a9eb2784..4507e7e87d6 100644 --- a/src/traces/contourcarpet/plot.js +++ b/src/traces/contourcarpet/plot.js @@ -26,95 +26,96 @@ var lookupCarpet = require('../carpet/lookup_carpetid'); var closeBoundaries = require('../contour/close_boundaries'); module.exports = function plot(gd, plotinfo, cdcontours, contourcarpetLayer) { - contourPlot.plotWrapper(gd, plotinfo, cdcontours, contourcarpetLayer, plotOne); -}; + var xa = plotinfo.xaxis; + var ya = plotinfo.yaxis; -function plotOne(gd, plotinfo, cd, plotGroup) { - var trace = cd.trace; + Lib.makeTraceGroups(contourcarpetLayer, cdcontours, 'contour').each(function(cd) { + var plotGroup = d3.select(this); + var cd0 = cd[0]; + var trace = cd0.trace; - var carpet = trace._carpetTrace = lookupCarpet(gd, trace); - var carpetcd = gd.calcdata[carpet.index][0]; + var carpet = trace._carpetTrace = lookupCarpet(gd, trace); + var carpetcd = gd.calcdata[carpet.index][0]; - if(!carpet.visible || carpet.visible === 'legendonly') return; + if(!carpet.visible || carpet.visible === 'legendonly') return; - var a = cd.a; - var b = cd.b; - var contours = trace.contours; - var xa = plotinfo.xaxis; - var ya = plotinfo.yaxis; - var pathinfo = emptyPathinfo(contours, plotinfo, cd); - var isConstraint = contours.type === 'constraint'; - var operation = contours._operation; - var coloring = isConstraint ? (operation === '=' ? 'lines' : 'fill') : contours.coloring; - - // Map [a, b] (data) --> [i, j] (pixels) - function ab2p(ab) { - var pt = carpet.ab2xy(ab[0], ab[1], true); - return [xa.c2p(pt[0]), ya.c2p(pt[1])]; - } - - // Define the perimeter in a/b coordinates: - var perimeter = [ - [a[0], b[b.length - 1]], - [a[a.length - 1], b[b.length - 1]], - [a[a.length - 1], b[0]], - [a[0], b[0]] - ]; - - // Extract the contour levels: - makeCrossings(pathinfo); - var atol = (a[a.length - 1] - a[0]) * 1e-8; - var btol = (b[b.length - 1] - b[0]) * 1e-8; - findAllPaths(pathinfo, atol, btol); - - // Constraints might need to be draw inverted, which is not something contours - // handle by default since they're assumed fully opaque so that they can be - // drawn overlapping. This function flips the paths as necessary so that they're - // drawn correctly. - // - // TODO: Perhaps this should be generalized and *all* paths should be drawn as - // closed regions so that translucent contour levels would be valid. - // See: https://github.com/plotly/plotly.js/issues/1356 - var fillPathinfo = pathinfo; - if(contours.type === 'constraint') { - fillPathinfo = convertToConstraints(pathinfo, operation); - closeBoundaries(fillPathinfo, operation, perimeter, trace); - } + var a = cd0.a; + var b = cd0.b; + var contours = trace.contours; + var pathinfo = emptyPathinfo(contours, plotinfo, cd0); + var isConstraint = contours.type === 'constraint'; + var operation = contours._operation; + var coloring = isConstraint ? (operation === '=' ? 'lines' : 'fill') : contours.coloring; - // Map the paths in a/b coordinates to pixel coordinates: - mapPathinfo(pathinfo, ab2p); + // Map [a, b] (data) --> [i, j] (pixels) + function ab2p(ab) { + var pt = carpet.ab2xy(ab[0], ab[1], true); + return [xa.c2p(pt[0]), ya.c2p(pt[1])]; + } - // draw everything + // Define the perimeter in a/b coordinates: + var perimeter = [ + [a[0], b[b.length - 1]], + [a[a.length - 1], b[b.length - 1]], + [a[a.length - 1], b[0]], + [a[0], b[0]] + ]; + + // Extract the contour levels: + makeCrossings(pathinfo); + var atol = (a[a.length - 1] - a[0]) * 1e-8; + var btol = (b[b.length - 1] - b[0]) * 1e-8; + findAllPaths(pathinfo, atol, btol); + + // Constraints might need to be draw inverted, which is not something contours + // handle by default since they're assumed fully opaque so that they can be + // drawn overlapping. This function flips the paths as necessary so that they're + // drawn correctly. + // + // TODO: Perhaps this should be generalized and *all* paths should be drawn as + // closed regions so that translucent contour levels would be valid. + // See: https://github.com/plotly/plotly.js/issues/1356 + var fillPathinfo = pathinfo; + if(contours.type === 'constraint') { + fillPathinfo = convertToConstraints(pathinfo, operation); + closeBoundaries(fillPathinfo, operation, perimeter, trace); + } - // Compute the boundary path - var seg, xp, yp, i; - var segs = []; - for(i = carpetcd.clipsegments.length - 1; i >= 0; i--) { - seg = carpetcd.clipsegments[i]; - xp = map1dArray([], seg.x, xa.c2p); - yp = map1dArray([], seg.y, ya.c2p); - xp.reverse(); - yp.reverse(); - segs.push(makepath(xp, yp, seg.bicubic)); - } + // Map the paths in a/b coordinates to pixel coordinates: + mapPathinfo(pathinfo, ab2p); + + // draw everything + + // Compute the boundary path + var seg, xp, yp, i; + var segs = []; + for(i = carpetcd.clipsegments.length - 1; i >= 0; i--) { + seg = carpetcd.clipsegments[i]; + xp = map1dArray([], seg.x, xa.c2p); + yp = map1dArray([], seg.y, ya.c2p); + xp.reverse(); + yp.reverse(); + segs.push(makepath(xp, yp, seg.bicubic)); + } - var boundaryPath = 'M' + segs.join('L') + 'Z'; + var boundaryPath = 'M' + segs.join('L') + 'Z'; - // Draw the baseline background fill that fills in the space behind any other - // contour levels: - makeBackground(plotGroup, carpetcd.clipsegments, xa, ya, isConstraint, coloring); + // Draw the baseline background fill that fills in the space behind any other + // contour levels: + makeBackground(plotGroup, carpetcd.clipsegments, xa, ya, isConstraint, coloring); - // Draw the specific contour fills. As a simplification, they're assumed to be - // fully opaque so that it's easy to draw them simply overlapping. The alternative - // would be to flip adjacent paths and draw closed paths for each level instead. - makeFills(trace, plotGroup, xa, ya, fillPathinfo, perimeter, ab2p, carpet, carpetcd, coloring, boundaryPath); + // Draw the specific contour fills. As a simplification, they're assumed to be + // fully opaque so that it's easy to draw them simply overlapping. The alternative + // would be to flip adjacent paths and draw closed paths for each level instead. + makeFills(trace, plotGroup, xa, ya, fillPathinfo, perimeter, ab2p, carpet, carpetcd, coloring, boundaryPath); - // Draw contour lines: - makeLinesAndLabels(plotGroup, pathinfo, gd, cd, contours, plotinfo, carpet); + // Draw contour lines: + makeLinesAndLabels(plotGroup, pathinfo, gd, cd0, contours, plotinfo, carpet); - // Clip the boundary of the plot - Drawing.setClipUrl(plotGroup, carpet._clipPathId); -} + // Clip the boundary of the plot + Drawing.setClipUrl(plotGroup, carpet._clipPathId); + }); +}; function makeLinesAndLabels(plotgroup, pathinfo, gd, cd0, contours, plotinfo, carpet) { var lineContainer = Lib.ensureSingle(plotgroup, 'g', 'contourlines'); diff --git a/src/traces/heatmap/plot.js b/src/traces/heatmap/plot.js index 3d5f73828f4..48130d77238 100644 --- a/src/traces/heatmap/plot.js +++ b/src/traces/heatmap/plot.js @@ -16,370 +16,349 @@ var Registry = require('../../registry'); var Lib = require('../../lib'); var Colorscale = require('../../components/colorscale'); var xmlnsNamespaces = require('../../constants/xmlns_namespaces'); -var getUidsFromCalcData = require('../../plots/get_data').getUidsFromCalcData; var maxRowLength = require('./max_row_length'); module.exports = function(gd, plotinfo, cdheatmaps, heatmapLayer) { - var uidLookup = getUidsFromCalcData(cdheatmaps); - - heatmapLayer.selectAll('.hm > image').each(function(d) { - var oldTrace = d.trace || {}; - - if(!uidLookup[oldTrace.uid]) { - d3.select(this.parentNode).remove(); - } - }); - - for(var i = 0; i < cdheatmaps.length; i++) { - plotOne(gd, plotinfo, cdheatmaps[i], heatmapLayer); - } -}; - -function plotOne(gd, plotinfo, cd, heatmapLayer) { - var cd0 = cd[0]; - var trace = cd0.trace; var xa = plotinfo.xaxis; var ya = plotinfo.yaxis; - var id = 'hm' + trace.uid; - - var z = cd0.z; - var x = cd0.x; - var y = cd0.y; - var xc = cd0.xCenter; - var yc = cd0.yCenter; - var isContour = Registry.traceIs(trace, 'contour'); - var zsmooth = isContour ? 'best' : trace.zsmooth; - - // get z dims - var m = z.length; - var n = maxRowLength(z); - var xrev = false; - var yrev = false; - - var left, right, temp, top, bottom, i; - - // TODO: if there are multiple overlapping categorical heatmaps, - // or if we allow category sorting, then the categories may not be - // sequential... may need to reorder and/or expand z - - // Get edges of png in pixels (xa.c2p() maps axes coordinates to pixel coordinates) - // figure out if either axis is reversed (y is usually reversed, in pixel coords) - // also clip the image to maximum 50% outside the visible plot area - // bigger image lets you pan more naturally, but slows performance. - // TODO: use low-resolution images outside the visible plot for panning - // these while loops find the first and last brick bounds that are defined - // (in case of log of a negative) - i = 0; - while(left === undefined && i < x.length - 1) { - left = xa.c2p(x[i]); - i++; - } - i = x.length - 1; - while(right === undefined && i > 0) { - right = xa.c2p(x[i]); - i--; - } - - if(right < left) { - temp = right; - right = left; - left = temp; - xrev = true; - } - i = 0; - while(top === undefined && i < y.length - 1) { - top = ya.c2p(y[i]); - i++; - } - i = y.length - 1; - while(bottom === undefined && i > 0) { - bottom = ya.c2p(y[i]); - i--; - } - - if(bottom < top) { - temp = top; - top = bottom; - bottom = temp; - yrev = true; - } - - // for contours with heatmap fill, we generate the boundaries based on - // brick centers but then use the brick edges for drawing the bricks - if(isContour) { - xc = x; - yc = y; - x = cd0.xfill; - y = cd0.yfill; - } - - // make an image that goes at most half a screen off either side, to keep - // time reasonable when you zoom in. if zsmooth is true/fast, don't worry - // about this, because zooming doesn't increase number of pixels - // if zsmooth is best, don't include anything off screen because it takes too long - if(zsmooth !== 'fast') { - var extra = zsmooth === 'best' ? 0 : 0.5; - left = Math.max(-extra * xa._length, left); - right = Math.min((1 + extra) * xa._length, right); - top = Math.max(-extra * ya._length, top); - bottom = Math.min((1 + extra) * ya._length, bottom); - } + Lib.makeTraceGroups(heatmapLayer, cdheatmaps, 'hm').each(function(cd) { + var plotGroup = d3.select(this); + var cd0 = cd[0]; + var trace = cd0.trace; + + var z = cd0.z; + var x = cd0.x; + var y = cd0.y; + var xc = cd0.xCenter; + var yc = cd0.yCenter; + var isContour = Registry.traceIs(trace, 'contour'); + var zsmooth = isContour ? 'best' : trace.zsmooth; + + // get z dims + var m = z.length; + var n = maxRowLength(z); + var xrev = false; + var yrev = false; + + var left, right, temp, top, bottom, i; + + // TODO: if there are multiple overlapping categorical heatmaps, + // or if we allow category sorting, then the categories may not be + // sequential... may need to reorder and/or expand z + + // Get edges of png in pixels (xa.c2p() maps axes coordinates to pixel coordinates) + // figure out if either axis is reversed (y is usually reversed, in pixel coords) + // also clip the image to maximum 50% outside the visible plot area + // bigger image lets you pan more naturally, but slows performance. + // TODO: use low-resolution images outside the visible plot for panning + // these while loops find the first and last brick bounds that are defined + // (in case of log of a negative) + i = 0; + while(left === undefined && i < x.length - 1) { + left = xa.c2p(x[i]); + i++; + } + i = x.length - 1; + while(right === undefined && i > 0) { + right = xa.c2p(x[i]); + i--; + } - var imageWidth = Math.round(right - left), - imageHeight = Math.round(bottom - top); + if(right < left) { + temp = right; + right = left; + left = temp; + xrev = true; + } - // setup image nodes + i = 0; + while(top === undefined && i < y.length - 1) { + top = ya.c2p(y[i]); + i++; + } + i = y.length - 1; + while(bottom === undefined && i > 0) { + bottom = ya.c2p(y[i]); + i--; + } - // if image is entirely off-screen, don't even draw it - var isOffScreen = (imageWidth <= 0 || imageHeight <= 0); + if(bottom < top) { + temp = top; + top = bottom; + bottom = temp; + yrev = true; + } - var plotgroup = heatmapLayer.selectAll('g.hm.' + id) - .data(isOffScreen ? [] : [0]); + // for contours with heatmap fill, we generate the boundaries based on + // brick centers but then use the brick edges for drawing the bricks + if(isContour) { + xc = x; + yc = y; + x = cd0.xfill; + y = cd0.yfill; + } - plotgroup.enter().append('g') - .classed('hm', true) - .classed(id, true); + // make an image that goes at most half a screen off either side, to keep + // time reasonable when you zoom in. if zsmooth is true/fast, don't worry + // about this, because zooming doesn't increase number of pixels + // if zsmooth is best, don't include anything off screen because it takes too long + if(zsmooth !== 'fast') { + var extra = zsmooth === 'best' ? 0 : 0.5; + left = Math.max(-extra * xa._length, left); + right = Math.min((1 + extra) * xa._length, right); + top = Math.max(-extra * ya._length, top); + bottom = Math.min((1 + extra) * ya._length, bottom); + } - plotgroup.exit().remove(); + var imageWidth = Math.round(right - left), + imageHeight = Math.round(bottom - top); - if(isOffScreen) return; + // setup image nodes - // generate image data + // if image is entirely off-screen, don't even draw it + var isOffScreen = (imageWidth <= 0 || imageHeight <= 0); - var canvasW, canvasH; - if(zsmooth === 'fast') { - canvasW = n; - canvasH = m; - } else { - canvasW = imageWidth; - canvasH = imageHeight; - } + if(isOffScreen) { + var noImage = plotGroup.selectAll('image').data([]); + noImage.exit().remove(); + return; + } - var canvas = document.createElement('canvas'); - canvas.width = canvasW; - canvas.height = canvasH; - var context = canvas.getContext('2d'); - - var sclFunc = Colorscale.makeColorScaleFunc( - Colorscale.extractScale( - trace.colorscale, - trace.zmin, - trace.zmax - ), - { noNumericCheck: true, returnArray: true } - ); - - // map brick boundaries to image pixels - var xpx, - ypx; - if(zsmooth === 'fast') { - xpx = xrev ? - function(index) { return n - 1 - index; } : - Lib.identity; - ypx = yrev ? - function(index) { return m - 1 - index; } : - Lib.identity; - } - else { - xpx = function(index) { - return Lib.constrain(Math.round(xa.c2p(x[index]) - left), - 0, imageWidth); - }; - ypx = function(index) { - return Lib.constrain(Math.round(ya.c2p(y[index]) - top), - 0, imageHeight); - }; - } + // generate image data - // build the pixel map brick-by-brick - // cruise through z-matrix row-by-row - // build a brick at each z-matrix value - var yi = ypx(0); - var yb = [yi, yi]; - var xbi = xrev ? 0 : 1; - var ybi = yrev ? 0 : 1; - // for collecting an average luminosity of the heatmap - var pixcount = 0; - var rcount = 0; - var gcount = 0; - var bcount = 0; - - var xb, j, xi, v, row, c; - - function setColor(v, pixsize) { - if(v !== undefined) { - var c = sclFunc(v); - c[0] = Math.round(c[0]); - c[1] = Math.round(c[1]); - c[2] = Math.round(c[2]); - - pixcount += pixsize; - rcount += c[0] * pixsize; - gcount += c[1] * pixsize; - bcount += c[2] * pixsize; - return c; + var canvasW, canvasH; + if(zsmooth === 'fast') { + canvasW = n; + canvasH = m; + } else { + canvasW = imageWidth; + canvasH = imageHeight; } - return [0, 0, 0, 0]; - } - function interpColor(r0, r1, xinterp, yinterp) { - var z00 = r0[xinterp.bin0]; - if(z00 === undefined) return setColor(undefined, 1); - - var z01 = r0[xinterp.bin1], - z10 = r1[xinterp.bin0], - z11 = r1[xinterp.bin1], - dx = (z01 - z00) || 0, - dy = (z10 - z00) || 0, - dxy; - - // the bilinear interpolation term needs different calculations - // for all the different permutations of missing data - // among the neighbors of the main point, to ensure - // continuity across brick boundaries. - if(z01 === undefined) { - if(z11 === undefined) dxy = 0; - else if(z10 === undefined) dxy = 2 * (z11 - z00); - else dxy = (2 * z11 - z10 - z00) * 2 / 3; + var canvas = document.createElement('canvas'); + canvas.width = canvasW; + canvas.height = canvasH; + var context = canvas.getContext('2d'); + + var sclFunc = Colorscale.makeColorScaleFunc( + Colorscale.extractScale( + trace.colorscale, + trace.zmin, + trace.zmax + ), + { noNumericCheck: true, returnArray: true } + ); + + // map brick boundaries to image pixels + var xpx, + ypx; + if(zsmooth === 'fast') { + xpx = xrev ? + function(index) { return n - 1 - index; } : + Lib.identity; + ypx = yrev ? + function(index) { return m - 1 - index; } : + Lib.identity; } - else if(z11 === undefined) { - if(z10 === undefined) dxy = 0; - else dxy = (2 * z00 - z01 - z10) * 2 / 3; + else { + xpx = function(index) { + return Lib.constrain(Math.round(xa.c2p(x[index]) - left), + 0, imageWidth); + }; + ypx = function(index) { + return Lib.constrain(Math.round(ya.c2p(y[index]) - top), + 0, imageHeight); + }; } - else if(z10 === undefined) dxy = (2 * z11 - z01 - z00) * 2 / 3; - else dxy = (z11 + z00 - z01 - z10); - return setColor(z00 + xinterp.frac * dx + yinterp.frac * (dy + xinterp.frac * dxy)); - } - - if(zsmooth) { // best or fast, works fastest with imageData - var pxIndex = 0, - pixels; - - try { - pixels = new Uint8Array(imageWidth * imageHeight * 4); - } - catch(e) { - pixels = new Array(imageWidth * imageHeight * 4); + // build the pixel map brick-by-brick + // cruise through z-matrix row-by-row + // build a brick at each z-matrix value + var yi = ypx(0); + var yb = [yi, yi]; + var xbi = xrev ? 0 : 1; + var ybi = yrev ? 0 : 1; + // for collecting an average luminosity of the heatmap + var pixcount = 0; + var rcount = 0; + var gcount = 0; + var bcount = 0; + + var xb, j, xi, v, row, c; + + function setColor(v, pixsize) { + if(v !== undefined) { + var c = sclFunc(v); + c[0] = Math.round(c[0]); + c[1] = Math.round(c[1]); + c[2] = Math.round(c[2]); + + pixcount += pixsize; + rcount += c[0] * pixsize; + gcount += c[1] * pixsize; + bcount += c[2] * pixsize; + return c; + } + return [0, 0, 0, 0]; } - if(zsmooth === 'best') { - var xForPx = xc || x; - var yForPx = yc || y; - var xPixArray = new Array(xForPx.length); - var yPixArray = new Array(yForPx.length); - var xinterpArray = new Array(imageWidth); - var findInterpX = xc ? findInterpFromCenters : findInterp; - var findInterpY = yc ? findInterpFromCenters : findInterp; - var yinterp, r0, r1; - - // first make arrays of x and y pixel locations of brick boundaries - for(i = 0; i < xForPx.length; i++) xPixArray[i] = Math.round(xa.c2p(xForPx[i]) - left); - for(i = 0; i < yForPx.length; i++) yPixArray[i] = Math.round(ya.c2p(yForPx[i]) - top); - - // then make arrays of interpolations - // (bin0=closest, bin1=next, frac=fractional dist.) - for(i = 0; i < imageWidth; i++) xinterpArray[i] = findInterpX(i, xPixArray); - - // now do the interpolations and fill the png - for(j = 0; j < imageHeight; j++) { - yinterp = findInterpY(j, yPixArray); - r0 = z[yinterp.bin0]; - r1 = z[yinterp.bin1]; - for(i = 0; i < imageWidth; i++, pxIndex += 4) { - c = interpColor(r0, r1, xinterpArray[i], yinterp); - putColor(pixels, pxIndex, c); - } + function interpColor(r0, r1, xinterp, yinterp) { + var z00 = r0[xinterp.bin0]; + if(z00 === undefined) return setColor(undefined, 1); + + var z01 = r0[xinterp.bin1], + z10 = r1[xinterp.bin0], + z11 = r1[xinterp.bin1], + dx = (z01 - z00) || 0, + dy = (z10 - z00) || 0, + dxy; + + // the bilinear interpolation term needs different calculations + // for all the different permutations of missing data + // among the neighbors of the main point, to ensure + // continuity across brick boundaries. + if(z01 === undefined) { + if(z11 === undefined) dxy = 0; + else if(z10 === undefined) dxy = 2 * (z11 - z00); + else dxy = (2 * z11 - z10 - z00) * 2 / 3; } - } - else { // zsmooth = fast - for(j = 0; j < m; j++) { - row = z[j]; - yb = ypx(j); - for(i = 0; i < imageWidth; i++) { - c = setColor(row[i], 1); - pxIndex = (yb * imageWidth + xpx(i)) * 4; - putColor(pixels, pxIndex, c); - } + else if(z11 === undefined) { + if(z10 === undefined) dxy = 0; + else dxy = (2 * z00 - z01 - z10) * 2 / 3; } - } + else if(z10 === undefined) dxy = (2 * z11 - z01 - z00) * 2 / 3; + else dxy = (z11 + z00 - z01 - z10); - var imageData = context.createImageData(imageWidth, imageHeight); - try { - imageData.data.set(pixels); + return setColor(z00 + xinterp.frac * dx + yinterp.frac * (dy + xinterp.frac * dxy)); } - catch(e) { - var pxArray = imageData.data, - dlen = pxArray.length; - for(j = 0; j < dlen; j ++) { - pxArray[j] = pixels[j]; + + if(zsmooth) { // best or fast, works fastest with imageData + var pxIndex = 0, + pixels; + + try { + pixels = new Uint8Array(imageWidth * imageHeight * 4); + } + catch(e) { + pixels = new Array(imageWidth * imageHeight * 4); } - } - context.putImageData(imageData, 0, 0); - } else { // zsmooth = false -> filling potentially large bricks works fastest with fillRect - - // gaps do not need to be exact integers, but if they *are* we will get - // cleaner edges by rounding at least one edge - var xGap = trace.xgap; - var yGap = trace.ygap; - var xGapLeft = Math.floor(xGap / 2); - var yGapTop = Math.floor(yGap / 2); - - for(j = 0; j < m; j++) { - row = z[j]; - yb.reverse(); - yb[ybi] = ypx(j + 1); - if(yb[0] === yb[1] || yb[0] === undefined || yb[1] === undefined) { - continue; + if(zsmooth === 'best') { + var xForPx = xc || x; + var yForPx = yc || y; + var xPixArray = new Array(xForPx.length); + var yPixArray = new Array(yForPx.length); + var xinterpArray = new Array(imageWidth); + var findInterpX = xc ? findInterpFromCenters : findInterp; + var findInterpY = yc ? findInterpFromCenters : findInterp; + var yinterp, r0, r1; + + // first make arrays of x and y pixel locations of brick boundaries + for(i = 0; i < xForPx.length; i++) xPixArray[i] = Math.round(xa.c2p(xForPx[i]) - left); + for(i = 0; i < yForPx.length; i++) yPixArray[i] = Math.round(ya.c2p(yForPx[i]) - top); + + // then make arrays of interpolations + // (bin0=closest, bin1=next, frac=fractional dist.) + for(i = 0; i < imageWidth; i++) xinterpArray[i] = findInterpX(i, xPixArray); + + // now do the interpolations and fill the png + for(j = 0; j < imageHeight; j++) { + yinterp = findInterpY(j, yPixArray); + r0 = z[yinterp.bin0]; + r1 = z[yinterp.bin1]; + for(i = 0; i < imageWidth; i++, pxIndex += 4) { + c = interpColor(r0, r1, xinterpArray[i], yinterp); + putColor(pixels, pxIndex, c); + } + } } - xi = xpx(0); - xb = [xi, xi]; - for(i = 0; i < n; i++) { - // build one color brick! - xb.reverse(); - xb[xbi] = xpx(i + 1); - if(xb[0] === xb[1] || xb[0] === undefined || xb[1] === undefined) { - continue; + else { // zsmooth = fast + for(j = 0; j < m; j++) { + row = z[j]; + yb = ypx(j); + for(i = 0; i < imageWidth; i++) { + c = setColor(row[i], 1); + pxIndex = (yb * imageWidth + xpx(i)) * 4; + putColor(pixels, pxIndex, c); + } } - v = row[i]; - c = setColor(v, (xb[1] - xb[0]) * (yb[1] - yb[0])); - context.fillStyle = 'rgba(' + c.join(',') + ')'; - - context.fillRect(xb[0] + xGapLeft, yb[0] + yGapTop, - xb[1] - xb[0] - xGap, yb[1] - yb[0] - yGap); } - } - } - rcount = Math.round(rcount / pixcount); - gcount = Math.round(gcount / pixcount); - bcount = Math.round(bcount / pixcount); - var avgColor = tinycolor('rgb(' + rcount + ',' + gcount + ',' + bcount + ')'); + var imageData = context.createImageData(imageWidth, imageHeight); + try { + imageData.data.set(pixels); + } + catch(e) { + var pxArray = imageData.data, + dlen = pxArray.length; + for(j = 0; j < dlen; j ++) { + pxArray[j] = pixels[j]; + } + } - gd._hmpixcount = (gd._hmpixcount||0) + pixcount; - gd._hmlumcount = (gd._hmlumcount||0) + pixcount * avgColor.getLuminance(); + context.putImageData(imageData, 0, 0); + } else { // zsmooth = false -> filling potentially large bricks works fastest with fillRect - var image3 = plotgroup.selectAll('image') - .data(cd); + // gaps do not need to be exact integers, but if they *are* we will get + // cleaner edges by rounding at least one edge + var xGap = trace.xgap; + var yGap = trace.ygap; + var xGapLeft = Math.floor(xGap / 2); + var yGapTop = Math.floor(yGap / 2); - image3.enter().append('svg:image').attr({ - xmlns: xmlnsNamespaces.svg, - preserveAspectRatio: 'none' - }); + for(j = 0; j < m; j++) { + row = z[j]; + yb.reverse(); + yb[ybi] = ypx(j + 1); + if(yb[0] === yb[1] || yb[0] === undefined || yb[1] === undefined) { + continue; + } + xi = xpx(0); + xb = [xi, xi]; + for(i = 0; i < n; i++) { + // build one color brick! + xb.reverse(); + xb[xbi] = xpx(i + 1); + if(xb[0] === xb[1] || xb[0] === undefined || xb[1] === undefined) { + continue; + } + v = row[i]; + c = setColor(v, (xb[1] - xb[0]) * (yb[1] - yb[0])); + context.fillStyle = 'rgba(' + c.join(',') + ')'; + + context.fillRect(xb[0] + xGapLeft, yb[0] + yGapTop, + xb[1] - xb[0] - xGap, yb[1] - yb[0] - yGap); + } + } + } - image3.attr({ - height: imageHeight, - width: imageWidth, - x: left, - y: top, - 'xlink:href': canvas.toDataURL('image/png') + rcount = Math.round(rcount / pixcount); + gcount = Math.round(gcount / pixcount); + bcount = Math.round(bcount / pixcount); + var avgColor = tinycolor('rgb(' + rcount + ',' + gcount + ',' + bcount + ')'); + + gd._hmpixcount = (gd._hmpixcount||0) + pixcount; + gd._hmlumcount = (gd._hmlumcount||0) + pixcount * avgColor.getLuminance(); + + var image3 = plotGroup.selectAll('image') + .data(cd); + + image3.enter().append('svg:image').attr({ + xmlns: xmlnsNamespaces.svg, + preserveAspectRatio: 'none' + }); + + image3.attr({ + height: imageHeight, + width: imageWidth, + x: left, + y: top, + 'xlink:href': canvas.toDataURL('image/png') + }); }); - - image3.exit().remove(); -} +}; // get interpolated bin value. Returns {bin0:closest bin, frac:fractional dist to next, bin1:next bin} function findInterp(pixel, pixArray) { diff --git a/src/traces/ohlc/plot.js b/src/traces/ohlc/plot.js index f915612c957..85adb7eaada 100644 --- a/src/traces/ohlc/plot.js +++ b/src/traces/ohlc/plot.js @@ -16,31 +16,21 @@ module.exports = function plot(gd, plotinfo, cdOHLC, ohlcLayer) { var xa = plotinfo.xaxis; var ya = plotinfo.yaxis; - var traces = ohlcLayer.selectAll('g.trace') - .data(cdOHLC, function(d) { return d[0].trace.uid; }); - - traces.enter().append('g') - .attr('class', 'trace ohlc'); - - traces.exit().remove(); - - traces.order(); - - traces.each(function(d) { - var cd0 = d[0]; + Lib.makeTraceGroups(ohlcLayer, cdOHLC, 'trace ohlc').each(function(cd) { + var plotGroup = d3.select(this); + var cd0 = cd[0]; var t = cd0.t; var trace = cd0.trace; - var sel = d3.select(this); - if(!plotinfo.isRangePlot) cd0.node3 = sel; + if(!plotinfo.isRangePlot) cd0.node3 = plotGroup; if(trace.visible !== true || t.empty) { - sel.remove(); + plotGroup.remove(); return; } var tickLen = t.tickLen; - var paths = sel.selectAll('path').data(Lib.identity); + var paths = plotGroup.selectAll('path').data(Lib.identity); paths.enter().append('path'); diff --git a/src/traces/pie/plot.js b/src/traces/pie/plot.js index a1efb11946d..2e71d08674b 100644 --- a/src/traces/pie/plot.js +++ b/src/traces/pie/plot.js @@ -24,24 +24,17 @@ module.exports = function plot(gd, cdpie) { scalePies(cdpie, fullLayout._size); - var pieGroups = fullLayout._pielayer.selectAll('g.trace').data(cdpie); - - pieGroups.enter().append('g') - .attr({ - 'stroke-linejoin': 'round', // TODO: miter might look better but can sometimes cause problems - // maybe miter with a small-ish stroke-miterlimit? - 'class': 'trace' - }); - pieGroups.exit().remove(); - pieGroups.order(); - - pieGroups.each(function(cd) { + var pieGroups = Lib.makeTraceGroups(fullLayout._pielayer, cdpie, 'trace').each(function(cd) { var pieGroup = d3.select(this); var cd0 = cd[0]; var trace = cd0.trace; setCoords(cd); + // TODO: miter might look better but can sometimes cause problems + // maybe miter with a small-ish stroke-miterlimit? + pieGroup.attr('stroke-linejoin', 'round'); + pieGroup.each(function() { var slices = d3.select(this).selectAll('g.slice').data(cd); diff --git a/src/traces/scattergeo/plot.js b/src/traces/scattergeo/plot.js index 9bba530371e..e3b46277461 100644 --- a/src/traces/scattergeo/plot.js +++ b/src/traces/scattergeo/plot.js @@ -24,22 +24,14 @@ module.exports = function plot(gd, geo, calcData) { calcGeoJSON(calcData[i], geo.topojson); } - function keyFunc(d) { return d[0].trace.uid; } - function removeBADNUM(d, node) { if(d.lonlat[0] === BADNUM) { d3.select(node).remove(); } } - var gTraces = geo.layers.frontplot.select('.scatterlayer') - .selectAll('g.trace.scattergeo') - .data(calcData, keyFunc); - - gTraces.enter().append('g') - .attr('class', 'trace scattergeo'); - - gTraces.exit().remove(); + var scatterLayer = geo.layers.frontplot.select('.scatterlayer'); + var gTraces = Lib.makeTraceGroups(scatterLayer, calcData, 'trace scattergeo'); // TODO find a way to order the inner nodes on update gTraces.selectAll('*').remove(); diff --git a/src/traces/violin/plot.js b/src/traces/violin/plot.js index 6af773346f0..6912fd6f0f3 100644 --- a/src/traces/violin/plot.js +++ b/src/traces/violin/plot.js @@ -11,11 +11,12 @@ var d3 = require('d3'); var Lib = require('../../lib'); var Drawing = require('../../components/drawing'); + var boxPlot = require('../box/plot'); var linePoints = require('../scatter/line_points'); var helpers = require('./helpers'); -module.exports = function plot(gd, plotinfo, cd, violinLayer) { +module.exports = function plot(gd, plotinfo, cdViolins, violinLayer) { var fullLayout = gd._fullLayout; var xa = plotinfo.xaxis; var ya = plotinfo.yaxis; @@ -32,22 +33,12 @@ module.exports = function plot(gd, plotinfo, cd, violinLayer) { return Drawing.smoothopen(segments[0], 1); } - var traces = violinLayer.selectAll('g.trace.violins') - .data(cd, function(d) { return d[0].trace.uid; }); - - traces.enter().append('g') - .attr('class', 'trace violins'); - - traces.exit().remove(); - - traces.order(); - - traces.each(function(d) { - var cd0 = d[0]; + Lib.makeTraceGroups(violinLayer, cdViolins, 'trace violins').each(function(cd) { + var plotGroup = d3.select(this); + var cd0 = cd[0]; var t = cd0.t; var trace = cd0.trace; - var sel = d3.select(this); - if(!plotinfo.isRangePlot) cd0.node3 = sel; + if(!plotinfo.isRangePlot) cd0.node3 = plotGroup; var numViolins = fullLayout._numViolins; var group = (fullLayout.violinmode === 'group' && numViolins > 1); var groupFraction = 1 - fullLayout.violingap; @@ -60,7 +51,7 @@ module.exports = function plot(gd, plotinfo, cd, violinLayer) { t.wHover = t.dPos * (group ? groupFraction / numViolins : 1); if(trace.visible !== true || t.empty) { - d3.select(this).remove(); + plotGroup.remove(); return; } @@ -71,7 +62,7 @@ module.exports = function plot(gd, plotinfo, cd, violinLayer) { var hasNegativeSide = hasBothSides || trace.side === 'negative'; var groupStats = fullLayout._violinScaleGroupStats[trace.scalegroup]; - var violins = sel.selectAll('path.violin').data(Lib.identity); + var violins = plotGroup.selectAll('path.violin').data(Lib.identity); violins.enter().append('path') .style('vector-effect', 'non-scaling-stroke') @@ -165,14 +156,14 @@ module.exports = function plot(gd, plotinfo, cd, violinLayer) { } // inner box - boxPlot.plotBoxAndWhiskers(sel, {pos: posAxis, val: valAxis}, trace, { + boxPlot.plotBoxAndWhiskers(plotGroup, {pos: posAxis, val: valAxis}, trace, { bPos: bPos, bdPos: bdPosScaled, bPosPxOffset: bPosPxOffset }); // meanline insider box - boxPlot.plotBoxMean(sel, {pos: posAxis, val: valAxis}, trace, { + boxPlot.plotBoxMean(plotGroup, {pos: posAxis, val: valAxis}, trace, { bPos: bPos, bdPos: bdPosScaled, bPosPxOffset: bPosPxOffset @@ -185,7 +176,7 @@ module.exports = function plot(gd, plotinfo, cd, violinLayer) { // N.B. use different class name than boxPlot.plotBoxMean, // to avoid selectAll conflict - var meanPaths = sel.selectAll('path.meanline').data(fn || []); + var meanPaths = plotGroup.selectAll('path.meanline').data(fn || []); meanPaths.enter().append('path') .attr('class', 'meanline') .style('fill', 'none') @@ -202,6 +193,6 @@ module.exports = function plot(gd, plotinfo, cd, violinLayer) { ); }); - boxPlot.plotPoints(sel, {x: xa, y: ya}, trace, t); + boxPlot.plotPoints(plotGroup, {x: xa, y: ya}, trace, t); }); }; diff --git a/test/image/baselines/connectgaps_2d.png b/test/image/baselines/connectgaps_2d.png index fb5e18862be..99438b3d0ff 100644 Binary files a/test/image/baselines/connectgaps_2d.png and b/test/image/baselines/connectgaps_2d.png differ diff --git a/test/image/baselines/contour_heatmap_coloring.png b/test/image/baselines/contour_heatmap_coloring.png index 7bbafb31c21..a259c8fb887 100644 Binary files a/test/image/baselines/contour_heatmap_coloring.png and b/test/image/baselines/contour_heatmap_coloring.png differ diff --git a/test/jasmine/tests/carpet_test.js b/test/jasmine/tests/carpet_test.js index 3e784c9772b..ffadb31317b 100644 --- a/test/jasmine/tests/carpet_test.js +++ b/test/jasmine/tests/carpet_test.js @@ -537,6 +537,39 @@ describe('Test carpet interactions:', function() { .catch(failTest) .then(done); }); + + it('preserves order of carpets on the same subplot after hide/show', function(done) { + function getIndices() { + var out = []; + d3.selectAll('.carpetlayer .trace').each(function(d) { out.push(d[0].trace.index); }); + return out; + } + + Plotly.newPlot(gd, [{ + type: 'carpet', + a: [1, 2, 3], + b: [1, 2, 3], + y: [[0, 0.8, 2], [1.2, 2, 3.2], [2, 2.8, 4]] + }, { + type: 'carpet', + a: [1, 2, 3], + b: [1, 2, 3], + y: [[10, 10.8, 12], [11.2, 12, 13.2], [12, 12.8, 14]] + }]) + .then(function() { + expect(getIndices()).toEqual([0, 1]); + return Plotly.restyle(gd, 'visible', false, [0]); + }) + .then(function() { + expect(getIndices()).toEqual([1]); + return Plotly.restyle(gd, 'visible', true, [0]); + }) + .then(function() { + expect(getIndices()).toEqual([0, 1]); + }) + .catch(failTest) + .then(done); + }); }); describe('scattercarpet array attributes', function() { @@ -658,7 +691,7 @@ describe('contourcarpet plotting & editing', function() { it('keeps the correct ordering after hide and show', function(done) { function getIndices() { var out = []; - d3.selectAll('.contour').each(function(d) { out.push(d.trace.index); }); + d3.selectAll('.contour').each(function(d) { out.push(d[0].trace.index); }); return out; } diff --git a/test/jasmine/tests/choropleth_test.js b/test/jasmine/tests/choropleth_test.js index 15a807543fa..f994387a459 100644 --- a/test/jasmine/tests/choropleth_test.js +++ b/test/jasmine/tests/choropleth_test.js @@ -8,6 +8,7 @@ var d3 = require('d3'); var createGraphDiv = require('../assets/create_graph_div'); var destroyGraphDiv = require('../assets/destroy_graph_div'); var mouseEvent = require('../assets/mouse_event'); +var failTest = require('../assets/fail_test'); var customAssertions = require('../assets/custom_assertions'); var assertHoverLabelStyle = customAssertions.assertHoverLabelStyle; @@ -175,7 +176,7 @@ describe('Test choropleth hover:', function() { }); }); -describe('choropleth bad data', function() { +describe('choropleth drawing', function() { var gd; beforeEach(function() { @@ -196,7 +197,38 @@ describe('choropleth bad data', function() { // only utopia logs - others are silently ignored expect(Lib.log).toHaveBeenCalledTimes(1); }) - .catch(fail) + .catch(failTest) + .then(done); + }); + + it('preserves order after hide/show', function(done) { + function getIndices() { + var out = []; + d3.selectAll('.choropleth').each(function(d) { out.push(d[0].trace.index); }); + return out; + } + + Plotly.newPlot(gd, [{ + type: 'choropleth', + locations: ['CAN', 'USA'], + z: [1, 2] + }, { + type: 'choropleth', + locations: ['CAN', 'USA'], + z: [2, 1] + }]) + .then(function() { + expect(getIndices()).toEqual([0, 1]); + return Plotly.restyle(gd, 'visible', false, [0]); + }) + .then(function() { + expect(getIndices()).toEqual([1]); + return Plotly.restyle(gd, 'visible', true, [0]); + }) + .then(function() { + expect(getIndices()).toEqual([0, 1]); + }) + .catch(failTest) .then(done); }); }); diff --git a/test/jasmine/tests/contour_test.js b/test/jasmine/tests/contour_test.js index 7f19e18c7d4..af831c2cb83 100644 --- a/test/jasmine/tests/contour_test.js +++ b/test/jasmine/tests/contour_test.js @@ -487,7 +487,7 @@ describe('contour plotting and editing', function() { it('keeps the correct ordering after hide and show', function(done) { function getIndices() { var out = []; - d3.selectAll('.contour').each(function(d) { out.push(d.trace.index); }); + d3.selectAll('.contour').each(function(d) { out.push(d[0].trace.index); }); return out; } diff --git a/test/jasmine/tests/heatmap_test.js b/test/jasmine/tests/heatmap_test.js index 692a6513d88..28e18931514 100644 --- a/test/jasmine/tests/heatmap_test.js +++ b/test/jasmine/tests/heatmap_test.js @@ -479,15 +479,20 @@ describe('heatmap calc', function() { describe('heatmap plot', function() { 'use strict'; + var gd; + + beforeEach(function() { + gd = createGraphDiv(); + }); + afterEach(destroyGraphDiv); it('should not draw traces that are off-screen', function(done) { - var mock = require('@mocks/heatmap_multi-trace.json'), - mockCopy = Lib.extendDeep({}, mock), - gd = createGraphDiv(); + var mock = require('@mocks/heatmap_multi-trace.json'); + var mockCopy = Lib.extendDeep({}, mock); function assertImageCnt(cnt) { - var images = d3.selectAll('.hm').select('image'); + var images = d3.selectAll('.hm image'); expect(images.size()).toEqual(cnt); } @@ -502,15 +507,44 @@ describe('heatmap plot', function() { return Plotly.relayout(gd, 'xaxis.autorange', true); }).then(function() { assertImageCnt(5); + }) + .catch(failTest) + .then(done); + }); - done(); - }); + it('keeps the correct ordering after hide and show', function(done) { + function getIndices() { + var out = []; + d3.selectAll('.hm image').each(function(d) { out.push(d.trace.index); }); + return out; + } + + Plotly.newPlot(gd, [{ + type: 'heatmap', + z: [[1, 2], [3, 4]] + }, { + type: 'heatmap', + z: [[2, 1], [4, 3]], + contours: {coloring: 'lines'} + }]) + .then(function() { + expect(getIndices()).toEqual([0, 1]); + return Plotly.restyle(gd, 'visible', false, [0]); + }) + .then(function() { + expect(getIndices()).toEqual([1]); + return Plotly.restyle(gd, 'visible', true, [0]); + }) + .then(function() { + expect(getIndices()).toEqual([0, 1]); + }) + .catch(failTest) + .then(done); }); it('should be able to restyle', function(done) { - var mock = require('@mocks/13.json'), - mockCopy = Lib.extendDeep({}, mock), - gd = createGraphDiv(); + var mock = require('@mocks/13.json'); + var mockCopy = Lib.extendDeep({}, mock); function getImageURL() { return d3.select('.hm > image').attr('href'); @@ -538,19 +572,18 @@ describe('heatmap plot', function() { imageURLs.push(getImageURL()); expect(imageURLs[1]).toEqual(imageURLs[3]); - - done(); - }); + }) + .catch(failTest) + .then(done); }); it('draws canvas with correct margins', function(done) { - var mockWithPadding = require('@mocks/heatmap_brick_padding.json'), - mockWithoutPadding = Lib.extendDeep({}, mockWithPadding), - gd = createGraphDiv(), - getContextStub = { - fillRect: jasmine.createSpy() - }, - originalCreateElement = document.createElement; + var mockWithPadding = require('@mocks/heatmap_brick_padding.json'); + var mockWithoutPadding = Lib.extendDeep({}, mockWithPadding); + var getContextStub = { + fillRect: jasmine.createSpy() + }; + var originalCreateElement = document.createElement; mockWithoutPadding.data[0].xgap = 0; mockWithoutPadding.data[0].ygap = 0; @@ -591,7 +624,6 @@ describe('heatmap plot', function() { }); it('can change z values with connected gaps', function(done) { - var gd = createGraphDiv(); Plotly.newPlot(gd, [{ type: 'heatmap', connectgaps: true, z: [[1, 2], [null, 4], [1, 2]] @@ -615,7 +647,7 @@ describe('heatmap plot', function() { .then(function() { expect(gd.calcdata[0][0].z).toEqual([[1, 2], [2, 4], [1, 2]]); }) - .catch(fail) + .catch(failTest) .then(done); }); }); diff --git a/test/jasmine/tests/scattergeo_test.js b/test/jasmine/tests/scattergeo_test.js index b7368d1aa8c..50c70e48704 100644 --- a/test/jasmine/tests/scattergeo_test.js +++ b/test/jasmine/tests/scattergeo_test.js @@ -12,7 +12,7 @@ var mouseEvent = require('../assets/mouse_event'); var customAssertions = require('../assets/custom_assertions'); var assertHoverLabelStyle = customAssertions.assertHoverLabelStyle; var assertHoverLabelContent = customAssertions.assertHoverLabelContent; -var fail = require('../assets/fail_test'); +var failTest = require('../assets/fail_test'); var supplyAllDefaults = require('../assets/supply_defaults'); describe('Test scattergeo defaults', function() { @@ -252,7 +252,7 @@ describe('Test scattergeo hover', function() { lat: [10, 20, 30], text: ['A', 'B', 'C'] }]) - .catch(fail) + .catch(failTest) .then(done); }); @@ -287,7 +287,7 @@ describe('Test scattergeo hover', function() { Plotly.restyle(gd, 'hoverinfo', 'lon+lat+text+name').then(function() { check([381, 221], ['(10°, 10°)\nA', 'trace 0']); }) - .catch(fail) + .catch(failTest) .then(done); }); @@ -295,7 +295,7 @@ describe('Test scattergeo hover', function() { Plotly.restyle(gd, 'text', 'text').then(function() { check([381, 221], ['(10°, 10°)\ntext', null]); }) - .catch(fail) + .catch(failTest) .then(done); }); @@ -303,7 +303,7 @@ describe('Test scattergeo hover', function() { Plotly.restyle(gd, 'hovertext', 'hovertext').then(function() { check([381, 221], ['(10°, 10°)\nhovertext', null]); }) - .catch(fail) + .catch(failTest) .then(done); }); @@ -311,7 +311,7 @@ describe('Test scattergeo hover', function() { Plotly.restyle(gd, 'hovertext', ['Apple', 'Banana', 'Orange']).then(function() { check([381, 221], ['(10°, 10°)\nApple', null]); }) - .catch(fail) + .catch(failTest) .then(done); }); @@ -329,7 +329,7 @@ describe('Test scattergeo hover', function() { fontFamily: 'Arial' }); }) - .catch(fail) + .catch(failTest) .then(done); }); @@ -337,12 +337,12 @@ describe('Test scattergeo hover', function() { Plotly.restyle(gd, 'hoverinfo', [['lon', null, 'lat+name']]).then(function() { check([381, 221], ['lon: 10°', null]); }) - .catch(fail) + .catch(failTest) .then(done); }); }); -describe('scattergeo bad data', function() { +describe('scattergeo drawing', function() { var gd; beforeEach(function() { @@ -362,7 +362,33 @@ describe('scattergeo bad data', function() { // only utopia logs - others are silently ignored expect(Lib.log).toHaveBeenCalledTimes(1); }) - .catch(fail) + .catch(failTest) + .then(done); + }); + + it('preserves order after hide/show', function(done) { + function getIndices() { + var out = []; + d3.selectAll('.scattergeo').each(function(d) { out.push(d[0].trace.index); }); + return out; + } + + Plotly.newPlot(gd, [ + {type: 'scattergeo', lon: [10, 20], lat: [10, 20]}, + {type: 'scattergeo', lon: [10, 20], lat: [10, 20]} + ]) + .then(function() { + expect(getIndices()).toEqual([0, 1]); + return Plotly.restyle(gd, 'visible', false, [0]); + }) + .then(function() { + expect(getIndices()).toEqual([1]); + return Plotly.restyle(gd, 'visible', true, [0]); + }) + .then(function() { + expect(getIndices()).toEqual([0, 1]); + }) + .catch(failTest) .then(done); }); });