Reputation: 43
I am new to D3 and JS and trying to figure out where I'm going wrong. I have time series x/y data that I'm looking to animate with D3. The goal is a plot that shows all 4 points and transitions them through each of the 5 timestamps in the sample data, with a speed that is calculated with the timestamp.
I've structed the data in what I think is the proper format for D3 to be able to animate it efficiently. The nested json I'm using follows this format:
[{"time": 0, "info":[{"id": "A", "x": 10, "y": 20},
{"id": "B", "x": 40, "y": 90},
{"id": "C", "x": 10, "y": 90},
{"id": "D", "x": 80, "y": 70}]},
{"time": 0.5, "info":[{"id": "A", "x": 20, "y": 30},
{"id": "B", "x": 60, "y": 70},
{"id": "C", "x": 100, "y": 10},
{"id": "D", "x": 10, "y": 32}]},
{"time": 1, "info":[{"id": "A", "x": 50, "y": 60},
{"id": "B", "x": 0, "y": 0},
{"id": "C", "x": 80, "y": 10},
{"id": "D", "x": 50, "y": 50}]},
{"time": 1.5, "info":[{"id": "A", "x": 40, "y": 50},
{"id": "B", "x": 100, "y": 30},
{"id": "C", "x": 80, "y": 90},
{"id": "D", "x": 80, "y": 40}]},
{"time": 2, "info":[{"id": "A", "x": 60, "y": 10},
{"id": "B", "x": 0, "y": 50},
{"id": "C", "x": 10, "y": 50},
{"id": "D", "x": 30, "y": 60}]}]
I can plot individual times of this data but there are a few issues when trying to animate this: Here's a Fiddle with my code in it. The transition duration is set by the difference between the current time and the previous time.
There looks to be many ways of chaining transitions iterating through data, but they seem to be inefficient. Here's the way that I've gotten it to (partially) work, it can also be seen in the Fiddle.
var data = [{"time": 0, "info":[{"id": "A", "x": 10, "y": 20},
{"id": "B", "x": 40, "y": 90},
{"id": "C", "x": 10, "y": 90},
{"id": "D", "x": 80, "y": 70}]},
{"time": 0.5, "info":[{"id": "A", "x": 20, "y": 30},
{"id": "B", "x": 60, "y": 70},
{"id": "C", "x": 100, "y": 10},
{"id": "D", "x": 10, "y": 32}]},
{"time": 1, "info":[{"id": "A", "x": 50, "y": 60},
{"id": "B", "x": 80, "y": 50},
{"id": "C", "x": 80, "y": 10},
{"id": "D", "x": 50, "y": 50}]},
{"time": 1.5, "info":[{"id": "A", "x": 40, "y": 50},
{"id": "B", "x": 100, "y": 30},
{"id": "C", "x": 80, "y": 90},
{"id": "D", "x": 80, "y": 40}]},
{"time": 2, "info":[{"id": "A", "x": 60, "y": 10},
{"id": "B", "x": 0, "y": 50},
{"id": "C", "x": 10, "y": 50},
{"id": "D", "x": 30, "y": 60}]}]
var margin = {top: 30, right: 30, bottom: 30, left: 30},
width = 300 - margin.left - margin.right,
height = 300 - margin.top - margin.bottom;
// append the svg object to the body of the page
var svg = d3.select("#container")
.append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform",
"translate(" + margin.left + "," + margin.top + ")");
// Add X axis
var x = d3.scaleLinear()
.domain([0, 100])
.range([ 0, width ]);
svg.append("g")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x));
// Add Y axis
var y = d3.scaleLinear()
.domain([0, 100])
.range([ height, 0]);
svg.append("g")
.call(d3.axisLeft(y));
function runTheSimulation() {
var id = 0
if (id == 0) {
var points = svg.selectAll('circle')
.data(data[0].info, function(d, i) {return d.id})
points.enter().append('circle')
.attr('cx', function(d) {return x(d.x)})
.attr('cy', function(d) {return y(d.y)})
.attr('r', 8)
.attr('fill', 'white')
.attr('stroke', 'black')
id++;
next_frame()
} else {
next_frame()
}
function next_frame() {
var delta_time = (data[id].time - data[id-1].time)*1000
var players = svg.selectAll('circle')
.data(data[id].info, function(d) {return d.id})
.transition()
.duration(delta_time)
.ease(d3.easeLinear)
.attr('cx', function(d) {return x(d.x)})
.attr('cy', function(d) {return y(d.y)})
.on('end', function() {
id++;
//console.log(id)
if (id >= data.length) {
return;
}
next_frame();
})
}
}
body {
background: #20262E;
padding: 20px;
font-family: Helvetica;
}
#app {
background: #fff;
border-radius: 4px;
padding: 20px;
transition: all 0.2s;
}
li {
margin: 8px 0;
}
h2 {
font-weight: bold;
margin-bottom: 15px;
}
.done {
color: rgba(0, 0, 0, 0.3);
text-decoration: line-through;
}
input {
margin-right: 5px;
}
<!-- Bootstrap CSS -->
<link rel="stylesheet" href="http://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css">
<!-- jQuery -->
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js"></script>
<!-- Bootstrap JavaScript -->
<script src="http://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"></script>
<!-- Load d3.js -->
<script src="https://d3js.org/d3.v4.js"></script>
</head>
<body>
<button onclick="runTheSimulation()">RUN THE SIMULATION</button>
<div id="container"></div>
The issues I have:
Thank you for any thoughts and help!
Upvotes: 4
Views: 350
Reputation: 38211
I believe you'll find this a lot easier if you restructure your data. Instead of having a data array with frames containing each node's position, try a data array containing each node over time, eg:
var data = [ {"id":"A", frames: [{time:0, x: 1},{time:1,x: 2}]}, ...
This allows you to bind the data once, transition each element separately (triggering an end event without having to count how many transitions have occured as you aren't waiting for a batch to finish), and you could even store the current frame on the datum, rather than tracking it separately.
I used the following structure:
Which I got by running your code through a basic transpose, located in the snippet below.
Each item in the data array represents one element in the DOM - consistent with the D3 idiom. The index of current frame being drawn is stored in d.currentFrame
, while all frames are stored in d.frames
.
Basically your transition functionality would be fairly simple:
function transition(d) {
// Is there another frame?
if(d.currentFrame == d.frames.length-1 ) return; // don't keep going if there is no more data.
// Change in time
var dt = -d.frames[d.currentFrame++].time + d.frames[d.currentFrame].time;
// Do the transition:
d3.select(this)
.transition()
.duration(dt*1000)
.attr('cx', function(d) {return x(d.frames[d.currentFrame].x)})
.attr('cy', function(d) {return y(d.frames[d.currentFrame].y)})
.on("end", transition) // and repeat for this element
}
We can trigger the run simulation by resetting the current frame and triggering the transition:
function runTheSimulation() {
svg.selectAll("circle")
.each(function(d) { d.currentFrame = 0; })
.attr('cx', function(d) {return x(d.frames[d.currentFrame].x)})
.attr('cy', function(d) {return y(d.frames[d.currentFrame].y)})
.each(transition);
}
Each data point in this case doesn't actually need to have the same steps, either in number or values, which I believe is in keeping with D3's idioim: each datum is separate and independent from the processing of the others: there is no need to wait for a batch of transitions to complete, or use the time periods of the others.
Putting it together:
var data = [{"time": 0, "info":[{"id": "A", "x": 10, "y": 20},
{"id": "B", "x": 40, "y": 90},
{"id": "C", "x": 10, "y": 90},
{"id": "D", "x": 80, "y": 70}]},
{"time": 0.5, "info":[{"id": "A", "x": 20, "y": 30},
{"id": "B", "x": 60, "y": 70},
{"id": "C", "x": 100, "y": 10},
{"id": "D", "x": 10, "y": 32}]},
{"time": 1, "info":[{"id": "A", "x": 50, "y": 60},
{"id": "B", "x": 80, "y": 50},
{"id": "C", "x": 80, "y": 10},
{"id": "D", "x": 50, "y": 50}]},
{"time": 1.5, "info":[{"id": "A", "x": 40, "y": 50},
{"id": "B", "x": 100, "y": 30},
{"id": "C", "x": 80, "y": 90},
{"id": "D", "x": 80, "y": 40}]},
{"time": 2, "info":[{"id": "A", "x": 60, "y": 10},
{"id": "B", "x": 0, "y": 50},
{"id": "C", "x": 10, "y": 50},
{"id": "D", "x": 30, "y": 60}]}];
// Manipulate array:
var newData = data[0].info.map((_, i) => { return {currentFrame: 0, id: data[0].info[i].id ,frames:data.map((row,t) => { return { x: row.info[i].x, y: row.info[i].y, time: data[t].time }}) } });
var margin = {top: 30, right: 30, bottom: 30, left: 30},
width = 300 - margin.left - margin.right,
height = 300 - margin.top - margin.bottom;
// append the svg object to the body of the page
var svg = d3.select("#container")
.append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform",
"translate(" + margin.left + "," + margin.top + ")");
// Add X axis
var x = d3.scaleLinear()
.domain([0, 100])
.range([ 0, width ]);
svg.append("g")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x));
// Add Y axis
var y = d3.scaleLinear()
.domain([0, 100])
.range([ height, 0]);
svg.append("g")
.call(d3.axisLeft(y));
var points = svg.selectAll('circle')
.data(newData)
.enter()
.append('circle')
.attr('cx', function(d) {return x(d.frames[d.currentFrame].x)})
.attr('cy', function(d) {return y(d.frames[d.currentFrame].y)})
.attr('r', 8)
.attr('fill', 'white')
.attr('stroke', 'black')
function runTheSimulation() {
svg.selectAll("circle")
.each(function(d) { d.currentFrame = 0; })
.attr('cx', function(d) {return x(d.frames[d.currentFrame].x)})
.attr('cy', function(d) {return y(d.frames[d.currentFrame].y)})
.each(transition);
}
function transition(d) {
// Is there another frame?
if(d.currentFrame == d.frames.length-1 ) return; // don't keep going if there is no more data.
// Change in time
var dt = -d.frames[d.currentFrame++].time + d.frames[d.currentFrame].time;
// Do the transition:
d3.select(this)
.transition()
.duration(dt*1000)
.attr('cx', function(d) {return x(d.frames[d.currentFrame].x)})
.attr('cy', function(d) {return y(d.frames[d.currentFrame].y)})
.on("end", transition)
}
body {
background: #20262E;
padding: 20px;
font-family: Helvetica;
}
#app {
background: #fff;
border-radius: 4px;
padding: 20px;
transition: all 0.2s;
}
li {
margin: 8px 0;
}
h2 {
font-weight: bold;
margin-bottom: 15px;
}
.done {
color: rgba(0, 0, 0, 0.3);
text-decoration: line-through;
}
input {
margin-right: 5px;
}
<head>
<!-- Bootstrap CSS -->
<link rel="stylesheet" href="http://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css">
<!-- jQuery -->
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js"></script>
<!-- Bootstrap JavaScript -->
<script src="http://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"></script>
<!-- Load d3.js -->
<script src="https://d3js.org/d3.v4.js"></script>
</head>
<body>
<button onclick="runTheSimulation()">RUN THE SIMULATION</button>
<div id="container"></div>
</body>
Upvotes: 2
Reputation: 108567
So I took some liberties with your code to make it more d3-ish. My attempt here is to make your data drive the visualization by chaining all the transitions instead of hooking the end event and moving to the next index.
<head>
<!-- Load d3.js -->
<script src="https://d3js.org/d3.v4.js"></script>
<style>
</style>
</head>
<body>
<button onclick="runTheSimulation()">RUN THE SIMULATION</button>
<div id="container"></div>
<script>
var data = [
{
time: 0,
info: [
{ id: 'A', x: 10, y: 20 },
{ id: 'B', x: 40, y: 90 },
{ id: 'C', x: 10, y: 90 },
{ id: 'D', x: 80, y: 70 },
],
},
{
time: 0.5,
info: [
{ id: 'A', x: 20, y: 30 },
{ id: 'B', x: 60, y: 70 },
{ id: 'C', x: 100, y: 10 },
{ id: 'D', x: 10, y: 32 },
],
},
{
time: 1,
info: [
{ id: 'A', x: 50, y: 60 },
{ id: 'B', x: 80, y: 50 },
{ id: 'C', x: 80, y: 10 },
{ id: 'D', x: 50, y: 50 },
],
},
{
time: 1.5,
info: [
{ id: 'A', x: 40, y: 50 },
{ id: 'B', x: 100, y: 30 },
{ id: 'C', x: 80, y: 90 },
{ id: 'D', x: 80, y: 40 },
],
},
{
time: 2,
info: [
{ id: 'A', x: 60, y: 10 },
{ id: 'B', x: 0, y: 50 },
{ id: 'C', x: 10, y: 50 },
{ id: 'D', x: 30, y: 60 },
],
},
];
var margin = { top: 30, right: 30, bottom: 30, left: 30 },
width = 300 - margin.left - margin.right,
height = 300 - margin.top - margin.bottom;
// append the svg object to the body of the page
var svg = d3
.select('#container')
.append('svg')
.attr('width', width + margin.left + margin.right)
.attr('height', height + margin.top + margin.bottom)
.append('g')
.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
// Add X axis
var x = d3.scaleLinear().domain([0, 100]).range([0, width]);
svg
.append('g')
.attr('transform', 'translate(0,' + height + ')')
.call(d3.axisBottom(x));
// Add Y axis
var y = d3.scaleLinear().domain([0, 100]).range([height, 0]);
svg.append('g').call(d3.axisLeft(y));
function runTheSimulation() {
var chainedTransition,
prevTime = 0,
elapsedTime = 0;
// create a null selection which reprensents each timepiont
svg
.selectAll(null)
.data(data)
.enter()
.each(function(d,i){
// for each timepoint, update the data
points = svg.selectAll('circle')
.data(d.info, (d) => d.id);
// handle enter to create circles first time through
points
.enter()
.append('circle')
.attr('r', 8)
.attr('fill', 'white')
.attr('stroke', 'black')
.attr('cx', d => x(d.x))
.attr('cy', d => y(d.y));
// calculate how long we'll transition
// between this timepoint and last
let transTime = (d.time - prevTime) * 1000;
// chain the transition for all data updates
chainedTransition = points
.transition()
.duration(transTime)
.delay(elapsedTime)
.ease(d3.easeLinear)
.attr('cx', d => x(d.x))
.attr('cy', d => y(d.y));
// rememeber this time
prevTime = d.time;
// and elpased time for chaining
elapsedTime += transTime;
});
}
</script>
</body>
Upvotes: 2