Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make bar charts animate #2

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions src/traces/bar/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
264 changes: 154 additions & 110 deletions src/traces/bar/plot.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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);
}
85 changes: 85 additions & 0 deletions test/image/mocks/animation_bars.json
Original file line number Diff line number Diff line change
@@ -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]
}
}
}]
}