The goal is to create a smooth scrolling real time plot with multiple traces.
I was able to do this for a single trace, but when I add more lines to transition, the animation seems to get messed up. I have a feeling that transitions are being looped through and colliding, but I can't figure out how to prevent this.
If you set N_CH = 1 in the snippet, things run smoothly. When it's set to N_CH = 4 then the animation becomes jerky (seems like the transitions aren't fully completing) and also (interestingly) the x-axis scrolling appears to become 4 times faster than when N_CH = 1.
You can recover the smoothness by changing the transform in the tick() function to match the number of channels (i.e. iScale(-4) for N_CH = 4) but this isn't "correct" as the translation speed is artificially fast. In the end, I need accurate time measurement in real-time.
I've tried various different approaches including:
- adding traces to a group and trying to translate the group
- refactoring the
dataobject and allowingd3to iterate through the data structure with aselectAll()call
... the results always seem to be the same.
// set up some variables
const N_CH = 4;
const N_PTS = 40;
const margin = {top: 20, right: 30, bottom: 30, left: 40};
const width = 800;
const height = 300;
const colors = ['steelblue', 'red', 'orange', 'magenta']
// instantiate data array (timestamps)
var data = [];
var channelData = [];
for (let ch = 0; ch < N_CH; ch++) {
channelData = [];
for (let i = 0; i < N_PTS; i++) {
channelData.push({
x: Date.now() + i * 1000,
y: ch + Math.random()
})
}
data.push({
name: "CH" + ch,
values: channelData
});
}
// initialize //////////////////////////////
// instantiate svg and attach to DOM element
var svg = d3
.select("#chart")
.append("svg")
.attr("viewBox", `0 0 ${width} ${height}`)
// add clip path for smooth entry/exit
svg.append("defs").append("clipPath")
.attr("id", "clip")
.append("rect")
.attr("x", margin.left)
.attr("y", margin.bottom)
.attr("width", width - margin.left - margin.right)
.attr("height", height - margin.top - margin.bottom);
// set index scale for data buffer position/transition
var iScale = d3.scaleLinear()
.range([0, width - margin.right])
.domain([0, data[0].values.length - 1]);
// set up x-axis scale for data x units (time)
var xScale = d3.scaleUtc()
.range([margin.left, width - margin.right])
// add x-axis to svg
var xAxis = svg.append("g")
.attr("class", "x-axis")
.attr("transform", `translate(0, ${height - margin.top})`)
.call(d3.axisBottom(xScale));
// set up y-axis
var yScale = d3.scaleLinear()
.range([height - margin.top, margin.bottom]);
// add y-axis to svg
var yAxis = svg.append("g")
.attr("class", "y-axis")
.attr("transform", `translate(${margin.left}, 0)`)
.call(d3.axisLeft(yScale));
// set the domains
xScale.domain(d3.extent(this.data[0].values, d => d.x));
// get global y domain
var flatten = [].concat.apply([], data.map(o => o.values))
yScale.domain(d3.extent(flatten, d => d.y));
// define the line
var line = d3.line()
.x((d, i) => iScale(i))
.y(d => yScale(d.y));
// make a group where we will append our paths
traces = svg.append("g")
.attr("clip-path", "url(#clip)")
for (let ch=0; ch<N_CH; ch++) {
traces.append("path")
.datum(data[ch].values)
.attr("id", `trace-${ch}`)
.attr("class", "trace")
.attr("d", line)
.attr("stroke", colors[ch])
.attr("fill", "none")
.attr("stroke-width", 1.5)
.attr("transform", "translate(0)")
}
// end initialize ////////////////////
// animate
tick();
function tick() {
// add data to buffer
let lastData;
for (let ch = 0; ch < N_CH; ch++) {
lastData = data[ch].values[data[ch].values.length - 1];
data[ch].values.push({
x: lastData.x + 1000,
y: ch + Math.random()
});
}
// update individual trace path data
for (let ch = 0; ch < N_CH; ch++) {
traces.select(`#trace-${ch}`)
.attr("d", line)
}
// animate transition
traces
.selectAll('.trace')
.attr("transform", "translate(0)")
.transition().duration(1000).ease(d3.easeLinear)
.attr("transform", `translate(${iScale(-1)}, 0)`)
.on("end", tick)
// update the domain
xScale.domain(d3.extent(data[0].values, d => d.x));
// animate/redraw axis
xAxis
.transition().duration(1000).ease(d3.easeLinear)
.call(d3.axisBottom(xScale));
for (let ch=0; ch<N_CH; ch++) {
data[ch].values.shift();
}
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<div id="chart"></div>
