Reputation: 318
I am making a d3 chart based off of time series data. I would like to plot moving averages based off a time window specified by the user in real time. All lines will share the same dataset. I am implementing the general update pattern (I believe), however, when I change the dataset, the update doesn't occur unless I call my update function explicitly. This works, however, a node is created for each element in the array (creating a new DOM element over the old one) as opposed to just adding the updated element. I feel I am misunderstanding something fundamental about D3, or perhaps about javascript itself. Here is a minimal working example of my code:
In brief, my chart has a setInterval method that adds to the dataset and updates all the lines on the chart. Each line shares the chart data, but will transform the data in different ways.
window.onload = function() {
let mychart = chart( '#chart' );
let props1 = {
'className': 'line-1',
'Color': 'red',
'y': function( d ) { return d + 25; }
}
let line1 = line( mychart, props1 );
mychart.addPlot( line1 );
mychart.stream();
}
//need to find best way to share data across all indicators
var chart = function(svg_id) {
let data = [ 0, 1, 2, 3, 4, 5];
let plots = [];
let svg = ( function( ) {
return d3.select( svg_id + '-container' ).append( 'svg' )
.attr( 'height', 500 )
.attr( 'width', 1000 )
.attr( 'id', svg_id.replace('#', '' ))
})();
let xscale = d3.scaleLinear()
.domain( [0, 100] )
.range( [10, 990] )
let addPlot = function( plot ) {
plots.push( plot );
}
let stream = function() {
setInterval( function() {
data.push( Math.random(0, 1) * 100);
for( var i = 0; i < plots.length; i++ ) {
plots[i].update();
}
}, 1000);
}
return {
'svg': svg,
'xscale': xscale,
'data': data,
'addPlot': addPlot,
'stream': stream,
}
}
var line = function( chart, props ) {
let data = chart.data;
let xscale = chart.xscale;
let svg = chart.svg;
let buildLine = d3.line()
.x( function(d, i ) { return xscale( i ) + i*1; })
.y( function(d) { return props.y( d ); })
let update = function() {
let line = svg.selectAll( props.className ).data( data );
line.enter()
.append( 'path' )
.attr( 'stroke', props.Color )
.attr( 'stroke-width', 2 )
.attr( 'class', props.className )
.attr( 'fill', 'none' )
.merge( line )
.attr( 'd', buildLine( data ))
line.exit().remove();
}
return {
'update': update,
}
}
I found plenty online about nodes not being added, but nothing about d3 repainting on every iteration, and adding nodes every iteration. Thank you.
Upvotes: 0
Views: 64
Reputation: 318
I solved this using this tutorial on d3 paths https://www.dashingd3js.com/svg-paths-and-d3js . Key point is selectAll is not appropriate for paths since a line is only one 'path' element, therefore append is all that is necessary. I am removing the line using jquery, so still a bit hacky as I'm sure D3 has a way to do this. But here is an updated init and update functions.
let init = function() {
svg.append( 'path' )
.attr( 'd', buildLine( data ))
.attr( 'stroke', props.Color )
.attr( 'stroke-width', 2 )
.attr( 'class', props.className )
.attr( 'fill', 'none' )
}
let update = function( newdata ) {
data.push( newdata );
$( '.' + props.className ).remove();
init();
Upvotes: 0
Reputation: 256
I'm not sure, but perhaps you can take a look at this: Key functions
As second argument of .data( data )
you can provide a function which uniquely identifies your data points. Like .data( data, function(d) { return d.someUniqueId; })
D3 can use this key to update DOM elements instead of regenerating them, as I understand it correctly from Mike Bostock's post.
Upvotes: 1