Starting with D3 Transitions
Aim
Explore D3 transitions. Make an object (circle ball) follow a path.
Background
I have been on again off again with D3 for a while now. This year I have decided to be on again. This page contains some notes about paths and transitions. I decided to revisit transitions because of the terrible/incorrect execution in another set of notes.
Example
This is the remarkably boring transition.
index.html
<!DOCTYPE html>
<html>
<head>
<meta charset='utf-8'/>
<link rel="stylesheet" href="base.css">
<script src="d3.min.js"></script>
</head>
<body>
<div class='container'>
<div class='chart'>
</div>
</div>
<script src="activity.js"></script>
</body>
</html>
base.css
.container {
height: 300px;
width: 300px;
background-color: #fff;
}
.chart {
font: 10px sans-serif;
color: black;
}
path {
fill: none;
stroke: none;
stroke-width: 1px;
}
Activity.js
// Initial setup
var width = 150;
height = 150;
var svg = d3.select(".chart")
.append("svg")
.attr("width", width)
.attr("height", height)
// Add a primary circle
var centerCircle = svg.append("circle")
.attr("cx", width/2)
.attr("cy", height/2)
.attr("r", 40)
.attr("fill", "white")
.attr("stroke", "black")
// Add two reference lines
var block1 = svg.append("line")
.attr("x1", width-1)
.attr("y1", 0)
.attr("x2", width-1)
.attr("y2", height)
.attr("stroke", "black")
var block2 = svg.append("line")
.attr("x1", width)
.attr("y1", height)
.attr("x2", width/2)
.attr("y2", height)
.attr("stroke", "black")
// Setup the moving ball variables
var startx = 75;
starty = 75;
radius = 60;
maxDegree = 270;
speed = 2000;
// Create dataset
var points = [[startx, starty],
[startx + radius, starty]];
for (var ba=0; ba<maxDegree; ba++) {
var pos = circlePoint(startx,
starty,
radius,
ba)
points.push([pos.x, pos.y])
}
// Add additional points to break away from the
// circular movement.
points.push([startx + radius + 4,
starty - radius],
[startx + radius + 4,
starty + radius + 4])
// Convert points to a path
var path = svg.append("path")
.data([points])
.attr("d", d3.line())
// RUN
transition();
function circlePoint(x, y, radius, degree) {
/*
Calculate points around a circle.
This function is used to generate
points along an SVG "path".
Note that the position of 0 degrees is
at center-right (3 position on a clock)
rather than center-top (12 position on
a clock).
270
-----
/- | -\
180 - |__ - Start (0 degrees)
\- -/
-----
90
Args
----
x: Integer
- Specifies the center x position
of a circle in an SVG
y: Integer
- Specifies the center y position
of a circle in an SVG
radius: Integer
- Specifies the radius of the
circle
degree: Integer (0-359)
- Specifies the angle from the
center of the circle to the
edge. Degree is converted to
radians before calculation.
Example
-------
circlePont(150, 150, 50, 0)
>> Object { x: 200, y: 150 }
circlePoint(150, 150, 50, 45)
>> Object { x: 185.35533905932738,
y: 185.35533905932738 }
circlePoint(150, 150, 50, 180)
>> Object { x: 100, y: 150 }
circlePoint(150, 150, 50, 270)
>> Object { x: 150, y: 100 }
*/
var a = degree * Math.PI / 180;
var pointX = x + radius * Math.cos(a)
var pointY = y + radius * Math.sin(a)
var output = {x: pointX, y: pointY};
return output
}
function transition() {
/*
This function handles all transition events.
A green circle is created with a radius of 10.
The ball is then handled by another function
called repeat.
The repeat function is responsible for moving
the ball along a path. However, to make the
transition repeat you need to uncomment the line
containing the ".on" attribute.
This function requires several variables to
exist.
Variables
---------
points: Array of arrays
- Each array contains xy coordinates
speed: Integer
- E.g. 3000 (=3 seconds)
path: SVG element
- Any path created from "points" data
*/
var ball = svg.append("circle")
.attr("r", 10)
.attr("transform", "translate(" + points[0] + ")")
.attr("fill", "green")
.attr("stroke", "black");
repeat();
function repeat() {
ball.attr("fill", "green")
ball.transition()
.duration(speed)
.attr("fill", "blue")
.attrTween("transform", translatePosition(path.node()))
//.on("end", repeat); //UNCOMMENT TO REPEAT
}
}
function translatePosition(path) {
/*
This function is called for .attrTween.
It is used with the "transform" attribute.
Args
----
path: SVG path
Notes
-----
This function uses the relative
position (t) along the length of a path (l)
which seems like a proxy for time.
Note that the relative position (t) ranges
from 0 to 1 and does not change when increasing
or decreasing the duration of a transition.
Example
-------
Assuming object is at x: 75; y: 75.
>>> First point
path length (l): 530.7391357421875
time (t): 9.958835199999968e-8
position (p): SVGPoint {
x: 75.00005340576172,
y: 75 }
returns: translate(75.00005340576172,
75)
>>> Last point
path length (l): 530.7391357421875
time (t): 1
position (p): SVGPoint {
x: 139,
y: 139 }
returns: translate(139,139)
*/
var l = path.getTotalLength();
// The following function [apparently] does
// not use d, i or a - these are just placeholders?
return function(d, i, a) {
// t = relative position along a path
return function(t) {
var p = path.getPointAtLength(t * l);
// Print to console
// Uncomment for testing
/*
console.log("path length (l): ", l)
console.log("time (t): ", t)
console.log("position (p): ", p)
console.log("translate(" + p.x + "," + p.y + ")")
console.log("\n")
*/
return "translate(" + p.x + "," + p.y + ")";
};
};
}