diff --git a/src/traces/bar/index.js b/src/traces/bar/index.js index fa579dbf3fa..615175c1cf0 100644 --- a/src/traces/bar/index.js +++ b/src/traces/bar/index.js @@ -24,6 +24,7 @@ Bar.style = require('./style').style; Bar.styleOnSelect = require('./style').styleOnSelect; Bar.hoverPoints = require('./hover').hoverPoints; Bar.selectPoints = require('./select'); +Bar.animatable = true; Bar.moduleType = 'trace'; Bar.name = 'bar'; diff --git a/src/traces/bar/plot.js b/src/traces/bar/plot.js index c76d8fe0373..7075399b337 100644 --- a/src/traces/bar/plot.js +++ b/src/traces/bar/plot.js @@ -28,118 +28,49 @@ var style = require('./style'); // padding in pixels around text var TEXTPAD = 3; -module.exports = function plot(gd, plotinfo, cdbar, barLayer) { - var xa = plotinfo.xaxis; - var ya = plotinfo.yaxis; - var fullLayout = gd._fullLayout; - - var bartraces = Lib.makeTraceGroups(barLayer, cdbar, 'trace bars').each(function(cd) { - var plotGroup = d3.select(this); - var cd0 = cd[0]; - var trace = cd0.trace; - - if(!plotinfo.isRangePlot) cd0.node3 = plotGroup; - - var pointGroup = Lib.ensureSingle(plotGroup, 'g', 'points'); - - var bars = pointGroup.selectAll('g.point').data(Lib.identity); - - bars.enter().append('g') - .classed('point', true); - - bars.exit().remove(); - - bars.each(function(di, i) { - var bar = d3.select(this); - - // now display the bar - // clipped xf/yf (2nd arg true): non-positive - // log values go off-screen by plotwidth - // so you see them continue if you drag the plot - var x0, x1, y0, y1; - if(trace.orientation === 'h') { - y0 = ya.c2p(di.p0, true); - y1 = ya.c2p(di.p1, true); - x0 = xa.c2p(di.s0, true); - x1 = xa.c2p(di.s1, true); - - // for selections - di.ct = [x1, (y0 + y1) / 2]; - } - else { - x0 = xa.c2p(di.p0, true); - x1 = xa.c2p(di.p1, true); - y0 = ya.c2p(di.s0, true); - y1 = ya.c2p(di.s1, true); - - // for selections - di.ct = [(x0 + x1) / 2, y1]; - } - - if(!isNumeric(x0) || !isNumeric(x1) || - !isNumeric(y0) || !isNumeric(y1) || - x0 === x1 || y0 === y1) { - bar.remove(); - return; - } - - var lw = (di.mlw + 1 || trace.marker.line.width + 1 || - (di.trace ? di.trace.marker.line.width : 0) + 1) - 1; - var offset = d3.round((lw / 2) % 1, 2); - - function roundWithLine(v) { - // if there are explicit gaps, don't round, - // it can make the gaps look crappy - return (fullLayout.bargap === 0 && fullLayout.bargroupgap === 0) ? - d3.round(Math.round(v) - offset, 2) : v; - } - - function expandToVisible(v, vc) { - // if it's not in danger of disappearing entirely, - // round more precisely - return Math.abs(v - vc) >= 2 ? roundWithLine(v) : - // but if it's very thin, expand it so it's - // necessarily visible, even if it might overlap - // its neighbor - (v > vc ? Math.ceil(v) : Math.floor(v)); - } - - if(!gd._context.staticPlot) { - // if bars are not fully opaque or they have a line - // around them, round to integer pixels, mainly for - // safari so we prevent overlaps from its expansive - // pixelation. if the bars ARE fully opaque and have - // no line, expand to a full pixel to make sure we - // can see them - var op = Color.opacity(di.mc || trace.marker.color); - var fixpx = (op < 1 || lw > 0.01) ? roundWithLine : expandToVisible; - x0 = fixpx(x0, x1); - x1 = fixpx(x1, x0); - y0 = fixpx(y0, y1); - y1 = fixpx(y1, y0); - } - - Lib.ensureSingle(bar, 'path') - .style('vector-effect', 'non-scaling-stroke') - .attr('d', - 'M' + x0 + ',' + y0 + 'V' + y1 + 'H' + x1 + 'V' + y0 + 'Z') - .call(Drawing.setClipUrl, plotinfo.layerClipId, gd); - - appendBarText(gd, bar, cd, i, x0, x1, y0, y1); - - if(plotinfo.layerClipId) { - Drawing.hideOutsideRangePoint(di, bar.select('text'), xa, ya, trace.xcalendar, trace.ycalendar); - } +module.exports = function plot(gd, plotinfo, cdbar, barLayer, transitionOpts, makeOnCompleteCallback) { + var onComplete; + var isFullReplot = !transitionOpts; + var hasTransition = !!transitionOpts && transitionOpts.duration > 0; + var join = barLayer.selectAll('g.trace') + .data(cdbar, function(d) { return d[0].trace.uid; }); + + // Append new traces: + join.enter().append('g') + .attr('class', 'trace bars'); + join.order(); + + if(hasTransition) { + if(makeOnCompleteCallback) { + onComplete = makeOnCompleteCallback(); + } + var transition = d3.transition() + .duration(transitionOpts.duration) + .ease(transitionOpts.easing) + .each('end', function() { + onComplete && onComplete(); + }) + .each('interrupt', function() { + onComplete && onComplete(); + }); + + transition.each(function() { + barLayer.selectAll('g.trace').each(function(d, i) { + plotOne(gd, i, plotinfo, d, cdbar, this, transitionOpts); + }); }); - - // lastly, clip points groups of `cliponaxis !== false` traces - // on `plotinfo._hasClipOnAxisFalse === true` subplots - var hasClipOnAxisFalse = cd0.trace.cliponaxis === false; - Drawing.setClipUrl(plotGroup, hasClipOnAxisFalse ? null : plotinfo.layerClipId, gd); - }); - + } else { + join.each(function(d, i) { + plotOne(gd, i, plotinfo, d, cdbar, this, transitionOpts); + }); + } + if(isFullReplot) { + join.exit().remove(); + } + // remove paths that didn't get used + barLayer.selectAll('path:not([d])').remove(); // error bars are on the top - Registry.getComponentMethod('errorbars', 'plot')(gd, bartraces, plotinfo); + Registry.getComponentMethod('errorbars', 'plot')(gd, join, plotinfo); }; function appendBarText(gd, bar, calcTrace, i, x0, x1, y0, y1) { @@ -434,3 +365,116 @@ function getTextPosition(trace, index) { var value = helpers.getValue(trace.textposition, index); return helpers.coerceEnumerated(attributeTextPosition, value); } + +function plotOne(gd, idx, plotinfo, cd, cdAll, element, transitionOpts) { + var xa = plotinfo.xaxis; + var ya = plotinfo.yaxis; + var fullLayout = gd._fullLayout; + var plotGroup = d3.select(element); + var cd0 = cd[0]; + var trace = cd0.trace; + var hasTransition = !!transitionOpts && transitionOpts.duration > 0; + function transition(selection) { + return hasTransition ? selection.transition() : selection; + } + + if(!plotinfo.isRangePlot) cd0.node3 = plotGroup; + + var pointGroup = Lib.ensureSingle(plotGroup, 'g', 'points'); + + var bars = pointGroup.selectAll('g.point') + .data(Lib.identity, function(d) { return d.p; }); + + bars.enter().append('g') + .classed('point', true); + + bars.exit().remove(); + + bars.each(function(di, i) { + var bar = d3.select(this); + + // now display the bar + // clipped xf/yf (2nd arg true): non-positive + // log values go off-screen by plotwidth + // so you see them continue if you drag the plot + var x0, x1, y0, y1; + if(trace.orientation === 'h') { + y0 = ya.c2p(di.p0, true); + y1 = ya.c2p(di.p1, true); + x0 = xa.c2p(di.s0, true); + x1 = xa.c2p(di.s1, true); + + // for selections + di.ct = [x1, (y0 + y1) / 2]; + } + else { + x0 = xa.c2p(di.p0, true); + x1 = xa.c2p(di.p1, true); + y0 = ya.c2p(di.s0, true); + y1 = ya.c2p(di.s1, true); + + // for selections + di.ct = [(x0 + x1) / 2, y1]; + } + + if(!isNumeric(x0) || !isNumeric(x1) || + !isNumeric(y0) || !isNumeric(y1)) { + bar.remove(); + return; + } + + var lw = (di.mlw + 1 || trace.marker.line.width + 1 || + (di.trace ? di.trace.marker.line.width : 0) + 1) - 1; + var offset = d3.round((lw / 2) % 1, 2); + + + function roundWithLine(v) { + // if there are explicit gaps, don't round, + // it can make the gaps look crappy + return (fullLayout.bargap === 0 && fullLayout.bargroupgap === 0) ? + d3.round(Math.round(v) - offset, 2) : v; + } + + function expandToVisible(v, vc) { + // if it's not in danger of disappearing entirely, + // round more precisely + return Math.abs(v - vc) >= 2 ? roundWithLine(v) : + // but if it's very thin, expand it so it's + // necessarily visible, even if it might overlap + // its neighbor + (v > vc ? Math.ceil(v) : Math.floor(v)); + } + + if(!gd._context.staticPlot) { + // if bars are not fully opaque or they have a line + // around them, round to integer pixels, mainly for + // safari so we prevent overlaps from its expansive + // pixelation. if the bars ARE fully opaque and have + // no line, expand to a full pixel to make sure we + // can see them + var op = Color.opacity(di.mc || trace.marker.color); + var fixpx = (op < 1 || lw > 0.01) ? roundWithLine : expandToVisible; + x0 = fixpx(x0, x1); + x1 = fixpx(x1, x0); + y0 = fixpx(y0, y1); + y1 = fixpx(y1, y0); + } + + transition(Lib.ensureSingle(bar, 'path')) + .style('vector-effect', 'non-scaling-stroke') + .attr('d', + 'M' + x0 + ',' + y0 + 'V' + y1 + 'H' + x1 + 'V' + y0 + 'Z') + .call(Drawing.setClipUrl, plotinfo.layerClipId, gd); + + appendBarText(gd, bar, cd, i, x0, x1, y0, y1); + + if(plotinfo.layerClipId) { + 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 = cd0.trace.cliponaxis === false; + Drawing.setClipUrl(plotGroup, hasClipOnAxisFalse ? null : plotinfo.layerClipId, gd); +} diff --git a/test/image/mocks/animation_bars.json b/test/image/mocks/animation_bars.json new file mode 100644 index 00000000000..e5f6a7c2363 --- /dev/null +++ b/test/image/mocks/animation_bars.json @@ -0,0 +1,85 @@ +{ + "data": [ + { + "x": [0, 1, 2], + "y": [0, 2, 8], + "type": "bar" + }, + { + "x": [0, 1, 2], + "y": [4, 2, 3], + "type": "bar" + } + ], + "layout": { + "title": "Animation test", + "showlegend": true, + "autosize": false, + "xaxis": { + "domain": [0, 1] + }, + "yaxis": { + "range": [0, 10], + "domain": [0, 1] + } + }, + "frames": [{ + "name": "base", + "data": [ + {"y": [0, 2, 8]}, + {"y": [4, 2, 3]} + ], + "layout": { + "yaxis": { + "range": [0, 10] + } + } + }, { + "name": "frame0", + "group": "even-frames", + "data": [ + {"y": [0.5, 1.5, 7.5]}, + {"y": [4.25, 2.25, 3.05]} + ], + "baseframe": "base", + "traces": [0, 1], + "layout": { } + }, { + "name": "frame1", + "group": "odd-frames", + "data": [ + {"y": [2.1, 1, 7]}, + {"y": [4.5, 2.5, 3.1]} + ], + "baseframe": "base", + "traces": [0, 1], + "layout": { } + }, { + "name": "frame2", + "group": "even-frames", + "data": [ + {"y": [3.5, 0.5, 6]}, + {"y": [5.7, 2.7, 3.9]} + ], + "baseframe": "base", + "traces": [0, 1], + "layout": { } + }, { + "name": "frame3", + "group": "odd-frames", + "data": [ + {"y": [5.1, 0.25, 5]}, + {"y": [7, 2.9, 6]} + ], + "baseframe": "base", + "traces": [0, 1], + "layout": { + "xaxis": { + "range": [-1, 4] + }, + "yaxis": { + "range": [-5, 15] + } + } + }] +} diff --git a/test/image/mocks/animation_bars_simple.json b/test/image/mocks/animation_bars_simple.json new file mode 100644 index 00000000000..8298b92140a --- /dev/null +++ b/test/image/mocks/animation_bars_simple.json @@ -0,0 +1,34 @@ +{ + "data": [ + { + "x": [1, 4], + "y": ["B", "C"], + "type": "bar", + "orientation": "h" + } + ], + "layout": { + }, + "frames": [{ + "name": "base", + "data": [{ + "x": [1, 4], + "y": ["B", "C"] + }], + "layout": {} + }, { + "name": "frame0", + "data": [{ + "x": [2, 5, 4], + "y": ["A", "B", "C"] + }], + "layout": {} + }, { + "name": "frame1", + "data": [{ + "x": [1, 4, 5, 2], + "y": ["A", "B", "C", "D"] + }], + "layout": {} + }] +}