hm711
hm711

Reputation: 168

transition contour lines from contourDensity()

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>

Update

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

Answers (3)

Paul Murray
Paul Murray

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.

Demo

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

euphrat
euphrat

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

Gerardo Furtado
Gerardo Furtado

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:

  1. How will you animate the enter selection? I mean, where will they come from? From the center of the SVG, from one of the corners of the SVG, or from somewhere else?
  2. How will you animate the exit selection? Where will they go to?
  3. And the most important: How will you animate the update selection? How does a given path around a data point relates to another path around another data point? Are you going to associate them by index (the default behaviour), or will you create a key function? If yes, what property are you going to use, 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

Related Questions