Skip to content

Instantly share code, notes, and snippets.

@mbostock
Last active November 22, 2022 23:32
Show Gist options
  • Save mbostock/1642874 to your computer and use it in GitHub Desktop.
Save mbostock/1642874 to your computer and use it in GitHub Desktop.
Line Transition
license: gpl-3.0

This example is the second of three in the Path Transitions tutorial; see the previous example for context.

The desired pairing of numbers for path interpolation is like this:

M x0, y0 L x1, y1 L x2, y2 L x3, y3 L xR, y4
   ↓   ↓    ↓   ↓    ↓   ↓    ↓   ↓
M xl, y0 L x0, y1 L x1, y2 L x2, y3 L x3, y4

Where xl is some negative value off the left side, and xr is some positive value off the right side. This way, the first point ⟨x0,y0⟩ is interpolated to ⟨xl,y0⟩; meaning, the x-coordinate is interpolated rather than the y-coordinate, and so the path appears to slide off to the left. Likewise, the incoming point ⟨xr,y4⟩ is interpolated to ⟨x3,y4⟩.

While you could write a custom interpolator and use transition.attrTween to achieve this, a much simpler solution is to interpolate the transform attribute rather than the path. This way, the shape of the path remains static while the it translates left during the transition.

Immediately prior to the transition, the path is redrawn as follows:

M x0, y0 L x1, y1 L x2, y2 L x3, y3 L xr, y4

Then, a transform transition is applied:

translate(0,0)
          ↓
translate(xl,0)

This causes the path to slide left. A clip path is used so the path is not visible outside of the chart body.

Note that for charts with spline interpolation, you’ll need to crop the visible part of the line by an extra point, so that the change in tangent is not visible; see the next example.

<!DOCTYPE html>
<meta charset="utf-8">
<style>
.line {
fill: none;
stroke: #000;
stroke-width: 1.5px;
}
</style>
<svg width="960" height="500"></svg>
<script src="//d3js.org/d3.v4.min.js"></script>
<script>
var n = 40,
random = d3.randomNormal(0, .2),
data = d3.range(n).map(random);
var svg = d3.select("svg"),
margin = {top: 20, right: 20, bottom: 20, left: 40},
width = +svg.attr("width") - margin.left - margin.right,
height = +svg.attr("height") - margin.top - margin.bottom,
g = svg.append("g").attr("transform", "translate(" + margin.left + "," + margin.top + ")");
var x = d3.scaleLinear()
.domain([0, n - 1])
.range([0, width]);
var y = d3.scaleLinear()
.domain([-1, 1])
.range([height, 0]);
var line = d3.line()
.x(function(d, i) { return x(i); })
.y(function(d, i) { return y(d); });
g.append("defs").append("clipPath")
.attr("id", "clip")
.append("rect")
.attr("width", width)
.attr("height", height);
g.append("g")
.attr("class", "axis axis--x")
.attr("transform", "translate(0," + y(0) + ")")
.call(d3.axisBottom(x));
g.append("g")
.attr("class", "axis axis--y")
.call(d3.axisLeft(y));
g.append("g")
.attr("clip-path", "url(#clip)")
.append("path")
.datum(data)
.attr("class", "line")
.transition()
.duration(500)
.ease(d3.easeLinear)
.on("start", tick);
function tick() {
// Push a new data point onto the back.
data.push(random());
// Redraw the line.
d3.select(this)
.attr("d", line)
.attr("transform", null);
// Slide it to the left.
d3.active(this)
.attr("transform", "translate(" + x(-1) + ",0)")
.transition()
.on("start", tick);
// Pop the old data point off the front.
data.shift();
}
</script>
@sampaioletti
Copy link

I forked the code pen from @Atticweb and updated to v4. Everything seems to be working properly, but I'm a little unhappy with the transition of the axis text, it doesn't respect the easing (jumpy), but the ticks transition smoothly. Any ideas?

https://codepen.io/sampaioletti/pen/eREyQZ

I'm pretty new to d3, but wanted to use this same idea in a similar project that requires v4.

(sorry if you get this twice I should have posted here in the first place probably not on bjorngi's gist)

@alexsmartens
Copy link

Has anyone implemented something similar in react-native? If so, would you mind sharing your experience?

@YaweiZhang-930
Copy link

@alexsmartens I am trying to build something similar in react-native as well. Did you get any chance to figure this out yet?

@NimChimpsky
Copy link

How can people use this knowing that the tick function is called by an exponentially number of growing of callees ? It will just grind user's cpu/browser into the ground.

@NimChimpsky
Copy link

.transition()
      .duration(500)
      .ease("linear")
      .attr("transform", "translate(" + x(-1) + ",0)")
      .each("end", tick);

.each(,) runs a new function "tick" for each point after each iteration, so it'll exponentially grow per cycle. I left this open on a second tab and it ran my CPU into the ground.

which therefore makes this example completely invalid without a significant refactoring, which everyone else seems to be happily ignoring ... I don't understand.

@nateblain
Copy link

@NimChimpsky I agree.

@thanhtschoepe
Copy link

thanhtschoepe commented Sep 3, 2021

I made a notebook using this example. Check it out. 😊. Please give me feedback if you have any.
https://observablehq.com/@ninjapupcodes/ping-chart

Thank you @mbostock for the amazing guide & @pepijnverburg for the duration 0 trick.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment