d3.js Simple Responsive Line Graph with Animation: Part 2

I’m back for another addition of d3.js for noobs. I’m a professional developer but I’m no d3.js pro, but I believe I’ve learned a few things that could be useful.

marginContainer.append( 'path' )
    .datum( startData )
    .attr('class', 'line')
    .attr( 'd', line )
    .transition()
    .duration( 500 )
    .ease('quad')
    .attrTween( 'd', function() {
      var interpolator = d3.interpolateArray( startData, data );

      return function( t ) {
        return line( interpolator( t ) );
      }
    } )
    .each('end', function() {
      drawCircles( data, marginContainer );
    });

So last time we got to the point where we’re calling the drawCircles() function. When you use .each('end', function(){ the function gets called at the end of each selection. In this case we only have one selection.

function drawCircles( data, container ) {
  circleContainer = container.append( 'g').attr('class', 'circles');
  data.forEach( function( datum, index ) {
    drawCircle( datum, index );
  } );
}

The drawCircles() function is simple. It appends the circleContainer and it takes the passed in data and calls the drawCircle() function for each data point.

function drawCircle( datum, index ) {
  circleContainer.datum( datum )
    .append( 'circle' )
    .attr( 'class', 'circle' )
    .attr( 'r', 0 )
    .attr(
      'cx',
      function( d ) {
        return x( d.date );
      }
    )
    .attr(
      'cy',
      function( d ) {
        return y( d.value );
      }
    )
    .on( 'mouseenter', function( d ) {
      d3.select( this )
        .attr(
          'class',
          'circle active'
        )
        .attr( 'r', 7 );

      d.active = true;

      showCircleDetail( d );
    } )
    .on( 'mouseout', function( d ) {
      d3.select( this )
        .attr(
          'class',
          'circle'
        )
        .attr( 'r', 6 );

      if ( d.active ) {
        hideCircleDetails();

        d.active = false;
      }
    } )
    .on( 'click touch', function( d ) {
      if ( d.active ) {
        showCircleDetail( d )
      } else {
        hideCircleDetails();
      }
    } )
    .transition()
    .delay( 100 * index )
    .duration(750)
    .ease('elastic', 1.5, .75)
    .attr( 'r', 6 )
  ;
}

The drawCircle() function its pretty long but most of it is just handling the mouse events. First create a circle using the datum. The r attribute is the circle radius. The cx and cy attributes handle the x and y coordinates of the circle, date and value respectively.

Now to handle our mouse events. On mouseenter we chance the class of the circe to include active. We increase the radius a smidge. Finally we call the function to show the detail popover. On mouseout we reverse the mouseenter changes. We also handle touch events with click touch.

At the end we handle the initial animation for the circles. The .delay() function creates the delay between each created point. We add some fun easing to make the circles look like they are plopping in. The 1.5 sets the percentage of the the max size of the elastic animation and the .75 is the min percentage.

function showCircleDetail( data ) {
  var details = circleContainer.append( 'g' )
    .attr( 'class', 'bubble' )
    .attr(
      'transform',
      function() {
        var result = 'translate(';

        var xVal = x( data.date ) - detailWidth/2;
        if(xVal + detailWidth > width ){
          xVal = width - detailWidth;
        }
        else if(xVal < 0){
          xVal = 0;
        }

        result += xVal;
        result += ', ';
        result += y( data.value ) - detailHeight - detailMargin;
        result += ')';

        return result;
      }
    );

  details.append( 'rect' )
    .attr( 'width', detailWidth )
    .attr( 'height', detailHeight )
    .attr('rx', 5)
    .attr('ry', 5);

  var text = details.append( 'text' )
    .attr( 'class', 'text' );

  var dateFormat = d3.time.format("%m/%d/%Y");

  text.append( 'tspan' )
    .attr( 'class', 'price' )
    .attr( 'x', detailWidth / 2 )
    .attr( 'y', detailHeight / 3 )
    .attr( 'text-anchor', 'middle' )
    .text( 'Price: ' + data.value );

  text.append( 'tspan' )
    .attr( 'class', 'date' )
    .attr( 'x', detailWidth / 2 )
    .attr( 'y', detailHeight / 4 * 3 )
    .attr( 'text-anchor', 'middle' )
    .text( 'Date: ' + dateFormat(data.date) );
}

Finally the showCircleDetail() function is what creates the popover when you hover over a data point on the line. First we append a new element called details. We’re going to use css transform translate() to position the popover in the right place.

if(xVal + detailWidth > width ){
  xVal = width - detailWidth;
}
else if(xVal < 0){
  xVal = 0;
}

This piece of code may seem unclear but the reason we’re doing this is to ensure our popover is not being placed outside of the width of graph. So at most it’ll be touching the left or right edge of our graph.

Then we create our actual SVG parts, the rectangle as the background/container and the text. The text is broken up using tspan to position them.

That’s all for now. Don’t forget to check out Part 1 if you missed it. Cheers!

Codepen

Github