Reputation: 168
I have a plot of contour lines based on scattered point data and using contourDensity()
from d3.js. When updating the dataset I want to smoothly transition the contour lines between the states. I know this is possible with single lines, but how to apply it to the paths created by contourDensity()
?
Example of the plot:
var data = [];
var svg = d3.select( 'svg' );
var width = parseInt( svg.attr( 'width' ) );
var height = parseInt( svg.attr( 'height' ) );
function updateData() {
for( var i = 0; i < 50; i++ ) {
data[i] = {
x: Math.random() * 800,
y: Math.random() * 600
}
}
}
function updateGraph() {
svg
.selectAll( 'path' )
.remove();
svg
.selectAll( 'circle' )
.remove();
svg
.selectAll( 'circle' )
.data( data )
.enter()
.append( 'circle' )
.attr( 'cx', function( d ) { return d.x; })
.attr( 'cy', function( d ) { return d.y; })
.attr( 'r', 2 )
.attr( 'stroke', 'none' )
.attr( 'fill', 'red' );
var contours = svg
.selectAll( 'path' )
.data( d3.contourDensity()
.x( function( d ) { return d.x; } )
.y( function( d ) { return d.y; } )
.size( [width, height] )
.bandwidth( 70 )
( data )
);
contours
.enter()
.append( "path" )
.attr( "d", d3.geoPath() );
}
updateData();
updateGraph();
setInterval( function() {
updateData();
updateGraph();
}, 2000 );
<script src="https://d3js.org/d3-contour.v1.min.js"></script>
<script src="https://d3js.org/d3.v4.min.js"></script>
<svg width="800" height="600" fill="none" stroke="blue" stroke-linejoin="round"></svg>
What I try to achieve is the visual effect of a slowly changing contour map (like a weather map), so the underlying data doesn't matter.
My first attempt was to fill a dot array with noise and use d3.contours()
to draw the contours. By changing the seed of the noise on each animation tick the contour map could be animated but it is very resource-heavy. Also the contours weren't smooth and I didn't manage to control scale and detail of the pattern.
That's why I switched to d3.contourDensity()
which builds smooth contours based on little data.
Am I missing the right approach to create a continuous animation of a contour map?
The closest I got with d3.contourDensity()
is the following. But as @gerardo-furtado pointed out the transition of paths would mean to link the paths between updates which seems impossible:
var data = [];
var svg = d3.select( 'svg' );
var width = parseInt( svg.attr( 'width' ) );
var height = parseInt( svg.attr( 'height' ) );
var speed = 4;
function updateData() {
for( var i = 0; i < 50; i++ ) {
if( !data[i] ) {
data[i] = {
x: Math.random() * 800,
y: Math.random() * 600
}
} else {
data[i].x = data[i].x + ( ( 0.5 - Math.random() ) * 2 * speed );
data[i].y = data[i].y + ( ( 0.5 - Math.random() ) * 2 * speed );
}
}
}
function updateGraph() {
svg
.selectAll( 'path' )
.remove();
svg
.selectAll( 'circle' )
.remove();
svg
.selectAll( 'circle' )
.data( data )
.enter()
.append( 'circle' )
.attr( 'cx', function( d ) { return d.x; })
.attr( 'cy', function( d ) { return d.y; })
.attr( 'r', 2 )
.attr( 'stroke', 'none' )
.attr( 'fill', 'red' );
var contours = svg
.selectAll( 'path' )
.data( d3.contourDensity()
.x( function( d ) { return d.x; } )
.y( function( d ) { return d.y; } )
.size( [width, height] )
.bandwidth( 70 )
( data )
);
contours
.enter()
.append( "path" )
.attr( "d", d3.geoPath() );
}
updateData();
updateGraph();
setInterval( function() {
updateData();
updateGraph();
}, 500 );
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="https://d3js.org/d3-contour.v1.min.js"></script>
<svg width="800" height="600" fill="none" stroke="blue" stroke-linejoin="round"></svg>
Upvotes: 1
Views: 1467
Reputation: 1012
You can transition contours by keeping track of which points are entering and exiting, and then tweening the weight
applied to each point.
Entering points will gradually increase their effect on the contour generator, while exiting points gradually decrease their effect, resulting in a smooth transition.
To keep contours consistent, it helps to specify an explicit array of threshold
values.
Upvotes: 0
Reputation: 254
I would not try to animate the contours themselves, this is most probably cumbersome. Instead, try to interpolate the underlying point data, e.g., using ICP, or as a cheap first attempt, compute a 1:1 association between every point in the set at time t with their closest point in the set at time step t+1 (each point must have exactly one correspondent though, greedy approach should suffice). If the temporal sampling of your data is too coarse to allow for a smooth animation using static density contours, then interpolating individual point positions is typically a very good approximation for a continuous change of the density field. Statically extracting density contours from each interpolated state of the point set and plotting them in a continuous sequence should give you the result you are expecting.
Upvotes: 0
Reputation: 102198
You can transition the path elements with a pretty basic set of enter, update and exit selections, that's not the problem.
The real problem is this: the contour generator creates a different number of path elements every time you run your updateGraph
function. So, the questions are:
d.value
?As you can see, the issue is more complex than it seems. For instance, this is your code with a basic enter, exit and update selection. I'm animating the update selection, and using 0 opacity to fade-in/fade-out the enter and exit selections. The result is not pleasant:
var data = [];
var svg = d3.select('svg');
var width = parseInt(svg.attr('width'));
var height = parseInt(svg.attr('height'));
function updateData() {
for (var i = 0; i < 50; i++) {
data[i] = {
x: Math.random() * 800,
y: Math.random() * 600
}
}
}
function updateGraph() {
var circles = svg
.selectAll('circle')
.data(data);
circles.enter()
.append('circle')
.attr('cx', function(d) {
return d.x;
})
.attr('cy', function(d) {
return d.y;
})
.attr('r', 2)
.attr('stroke', 'none')
.attr('fill', 'red');
circles.transition()
.duration(1000)
.attr('cx', function(d) {
return d.x;
})
.attr('cy', function(d) {
return d.y;
});
var contours = svg
.selectAll('path')
.data(d3.contourDensity()
.x(function(d) {
return d.x;
})
.y(function(d) {
return d.y;
})
.size([width, height])
.bandwidth(70)
(data)
);
contours.exit().transition()
.duration(1000)
.style("opacity", 0)
.remove();
contours.enter()
.append("path")
.style("opacity", 0)
.attr("d", d3.geoPath())
.transition()
.duration(1000)
.style("opacity", 1);
contours.transition()
.duration(1500)
.attr("d", d3.geoPath());
}
updateData();
updateGraph();
setInterval(function() {
updateData();
updateGraph();
}, 2000);
<script src="https://d3js.org/d3-contour.v1.min.js"></script>
<script src="https://d3js.org/d3.v4.min.js"></script>
<svg width="800" height="600" fill="none" stroke="blue" stroke-linejoin="round"></svg>
Upvotes: 2