Creating Interactive Graphs With SVG, Part 2

- - posted in animation, graphs, javascript, jquery, svg, visualization | Comments

In the previous post in this series we have looked at creating SVG graphs. We constructed the graph using parts as the visual grid, data points, surfaces and text labels. In this post we will look at the ability to interact with the graph and add some nice animations.

This article originally appeared on the Inspire.nl Blog. Inspire is a Dutch software studio that develops innovative Ruby on Rails web applications for computer, tablet and mobile.

What to add

The previous post ended with this graph as its result. It is a nice visualization, but it is at some points a bit vague. For instance, it is not clear what the precise value of data point is. Also, it would be nice to make the graph alive by adding some animations to it. For this we have the following in mind:

  • Nice shadows to appear behind data points when the mouse is over it.
  • A vertical bar which helps the user focus on a data point and it’s location on the domain
  • A tooltip which precise values for each data point

Each of these three elements will be discussed in this post. The final result will be this.

Shadows on mouse hover

Our fist addition is to add a nice shadow around a data point when the mouse pointer hovers over it. This is to empathize that data point has the focus of the user. On the right you can see the desired result; the shadow is a round gradient around the point. This is our first difficulty; it is very hard to add (drop) shadows to SVG elements. In a normal situation we could use the border property of an element, but for the discs we have already used the fill and stroke properties. The solution we chose for was to place copies of the data points under each one, and give that copy gradient properties.

So lets first create that gradient. In a SVG document you can define some recurring elements and properties, using <defs> at the top of the document (or inside the <svg> element). Here you can see the code to construct a radial gradient, which we give an id gradientShadow.

Create a radial gradient to refer to later
1
2
3
4
5
6
<defs>
  <radialGradient cx="50%" cy="50%" fx="50%" fy="50%" id="gradientShadow" r="55%">
    <stop offset="0%" style="stop-color:rgb(0,0,0); stop-opacity:1"></stop>
    <stop offset="100%" style="stop-color: #AAA; stop-opacity:0"></stop>
  </radialGradient>
</defs>

For the precise workings of the <radialGradient> element, see the gradients and patterns specifications.

With this gradient defined we can create copied elements to mimic a shadow. For this we use the same coordinates as the plain circles and in the style we set the fill to the gradient we have just created:

Replacement for the points group with shadow-mimicing circles
1
2
3
4
5
6
7
8
9
10
11
12
<g class="first_set points" data-setname="Submission to first decision">
  <circle class="shadow" cx="113" cy="192" r="5" stroke="#AAA" stroke-width="4" stroke-opacity="1" style="fill: url(#gradientShadow);"></circle>
  <circle class="plain" cx="113" cy="192" data-time="7.2" r="5"></circle>
  <circle class="shadow" cx="259" cy="171" r="5" stroke="#AAA" stroke-width="4" stroke-opacity="1" style="fill: url(#gradientShadow);"></circle>
  <circle class="plain" cx="259" cy="171" data-time="8.1" r="5"></circle>
  <circle class="shadow" cx="405" cy="179" r="5" stroke="#AAA" stroke-width="4" stroke-opacity="1" style="fill: url(#gradientShadow);"></circle>
  <circle class="plain" cx="405" cy="179" data-time="7.7" r="5"></circle>
  <circle class="shadow" cx="551" cy="200" r="5" stroke="#AAA" stroke-width="4" stroke-opacity="1" style="fill: url(#gradientShadow);"></circle>
  <circle class="plain" cx="551" cy="200" data-time="6.8" r="5"></circle>
  <circle class="shadow" cx="697" cy="204" r="5" stroke="#AAA" stroke-width="4" stroke-opacity="1" style="fill: url(#gradientShadow);"></circle>
  <circle class="plain" cx="697" cy="204" data-time="6.7" r="5"></circle>
</g>

You will notice that we have placed styling properties as attributes, instead of the more common way in a stylesheet. That is because the SVG animation library can’t animate styles from CSS correctly, but it does with attributes.

So what do we need to animate this shadow? First, we need the jQuery-SVG plugin, the SVG-DOM extension (to traverse the DOM) and the SVG-Animate extension (to animate properties of elements). So, the following is added to our document head after loading the jQuery javascript:

Add the jQuery-SVG plugings
1
2
3
<script type="text/javascript" src="jquery.svg.js"></script>
<script type="text/javascript" src="jquery.svgdom.js"></script>
<script type="text/javascript" src="jquery.svganim.js"></script>

Now we can create our Javascript to add mouseenter and mouseleave events to each (plain) data point. When the mouse enters one, we first stop any ongoing animation for the shadow point and then grow the circle. When the mouse leaves the point we shrink the shadow point back to its original size. The SVG properties can be animated by prepending svg to their attribute name. We use the knowledge that (in order of elements) before each normal data point a shadow point is present to select it.

Add mouseenter and mouseleave events to grow and shrink shadow
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$('svg.graph .points circle.plain').each( function(index) {
  // For each plain circle...
  $(this).on('mouseenter', function(event) {
    // Grow shadow point when mouse enters the point
    $(this).prev().stop(true, false ).animate( {
      svgR: 12,
      svgStrokeOpacity: 0,
      svgStrokeWidth: 0,
    }, 200 );
  }).on('mouseleave', function(event) {
    // Shrink shadow point when mouse leaves the point
    $(this).prev().stop(true, false ).animate( {
      svgR: 5,
      svgStrokeOpacity: 1,
      svgStrokeWidth: 4
    }, 200 );
  });
});

This extension to the original SVG is now complete, and you can see the result here. We now have a nice visual effect, but it is not enough for out taste. We would also like to create a “vertical coherence”, to draw even more attention to the point. For this we create an “indicator line”.

Indicator line

The indicator we have in mind is a vertical bar which scrolls horizontally over the graph and goes from the lower part of the graph to the height of the (highest) data point. We use the <rect> element to construct it. We initialize it on the left of the graph, and thus it goes up to the first data point:

Use a rect element for the indicator line
1
<rect class="indicator" height="337" width="3" x="111" y="192"></rect>

We give it a simple style to be white with a gray border but be hidden on first display:

Style the indicator
1
2
3
4
5
6
svg.graph rect.indicator {
  stroke-width: 1px;
  stroke: #bfcdcb;
  fill: white;
  dispay: none;
}

This indicator line should move over the graph, sticking at the data points, when the user moves the mouse. We already have the data points at which we could add more functionality, but we would like the have the whole column of data lines triggering the indicator line to that vertical line. We can do this by adding, for each vertical line in our line grid, a <line> element with a specified stroke-width, which will act as a sensitivity area. We chose lines over rects because the lines will always be centered on the year-points; there is no need for extra calculations, independent to the width of the sensitivity. The image on the right shows the construction of the trigger lines when they would be visible.

Add mouse trigger areas for each domain point.
1
2
3
4
5
6
7
<g class="triggerLines">
  <line x1="113" x2="113" y1="360" y2="192"></line>
  <line x1="259" x2="259" y1="360" y2="171"></line>
  <line x1="405" x2="405" y1="360" y2="179"></line>
  <line x1="551" x2="551" y1="360" y2="200"></line>
  <line x1="697" x2="697" y1="360" y2="204"></line>
</g>
Make the trigger lines invisible
1
2
3
4
svg.graph .triggerLines {
  stroke-width: 60;
  stroke: transparent;
}

We place the trigger lines just after the indicator line but before the data points; otherwise the mouse events we are to apply will be “blocked” by other elements.

For the real animation we first create a Javascript function which moves the indicator line to a specified column. A tricky part is getting the height of the indicator; in our examples we have only used one data set, but the function can handle multiple data sets; it will always scale the indicator to the height of the first defined data set (which should be of largest value). The index of the triggering line is used to get the corresponding data point.

Function to move indicator line to a trigger line
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// Move the indicator line to a trigger line
function moveIndicator( index )
{

  triggerLine = $($('svg.graph .triggerLines line').get(index));
  indicator = $('svg .indicator');

  // If the indicator is already visible, then animate movement. Otherwise just appear
  animateMoveTime = 0;
  if( indicator.css('display') != 'none' )
    animateMoveTime = 250;

  // Get the y-coordinate of the first (and thus highest) data point
  y = $($('g.points:first').children('.plain').get( index )).attr('cy');

  indicator.stop(true , false).show().animate({
    svgX: parseInt( triggerLine.attr('x1') ) - (parseInt(indicator.attr('width') ) / 2),
    svgY: y,
    svgHeight: 385 - y
  }, animateMoveTime );
}

To make the animation complete, we have to add mouse event handlers to the tigger lines to move the indicator to that column:

Add mouse event handler to move indicator line
1
2
3
4
5
6
$('g.triggerLines line').each( function(index)
{
  $(this).on( 'mouseenter', function(){
      moveIndicator( index );
  });
});

We also modify the mouse event handlers for the data points to call the moveIndicator method when the mouse enters it:

Modified data point mouse event handlers
1
2
3
4
5
6
$('svg.graph .points circle.plain').each( function(index) {
  // For each plain circle...
  $(this).on('mouseenter', function(event) {
    // Move the indicator line
    moveIndicator(index);
    ...

And with that last mouse event we have completed the functionality for the moving line indicator. You can see the result of this stage here.

All the elements needed to give better indication of the users focus are now present. But still the data points are sometimes hard to interpret. For that, we want to create a tooltip giving precise information.

Tooltip

As a final showcase of interactivity with SVG elements we will create a tooltip which shows when we hover over a data point. In that tooltip we would like to display the precise value of a data point. The tooltip will have a nice litte triangle to point to the data point, which will be constructed with the <polygon> element. The #tooltip-title element will contain the data set name of the selected data point and span.value-part will hold the corresponding value of this data point.

Tooltip element
1
2
3
4
5
6
7
8
9
10
<div id="tooltip">
  <svg version="1.1" xmlns="http://www.w3.org/2000/svg">
    <polygon id="tooltip-triangle" points="0,0 11,13 0,13"></polygon>
  </svg>
  <p id="tooltip-title"></p>
  <div class="value">
    <span class="value-part"></span>
    <span class="value-total">Weeks</span>
  </div>
</div>

The styling for this tooltip element and its child elements is quite extensive and of little intereset, so we skip it here for brevity. In the final jsFiddle you can inspect it.

The first Javascript function we write is to show (of fade in) the tooltip, given a data point element which is of interest.

Show the tooltip at a data point
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
function showTooltip( dataPoint )
{
  tooltip = $('#tooltip');
  dataPoint = $(dataPoint);

  // Set the data-set title, using the group element
  tooltip.find('#tooltip-title').text( dataPoint.parent().data('setname') );

  // Set the value for this data point
  tooltip.find('.value-part').text( dataPoint.data('value') );

  // Determine whether to switch tooltip to the left, because of small screen
  tooltipX = dataPoint.offset().left + 10;
  tooltipY = dataPoint.offset().top + 40;

  // Check if tooltip fits, with a extra border of 10 px
  if( tooltipX + tooltip.outerWidth(true) + 10 > $(window).width() )
  {
    tooltipX = dataPoint.offset().left - tooltip.outerWidth(true);

    tooltip.addClass('right');

    // Adjust SVG pointer
    $('#tooltip-triangle').attr('transform', 'scale(-1,1) translate(-11,0)' );
  }
  else
  {
    tooltip.removeClass('right');
    $('#tooltip-triangle').attr('transform', '' );
  }

  tooltip.stop( true, true ).delay(100).animate( {
    top: tooltipY,
    left: tooltipX
  }, 0 ).fadeIn('fast');
}

A nice litte feature is that in normal situations the tooltip shows right of the data point. In case of a small screen, the tooltip flips to the left side, to prevent unnecessary scrollbars appearing. The data- attributes of the <g> and <circle> elements of the first tutorial are used to display information.

For everything we can show, we would like a function to hide:

Hide the tooltip
1
2
3
4
5
function hideTooltip()
{
  // Hide popover and stop animating the bar
  $('#tooltip').stop(true, true).delay(200).fadeOut('fast' );
}

Finally we again modify the mouse events for the data points, to show and hide the tooltip on enter and leave, respectively:

Modified mouse events for data points to show and hide tooltip
1
2
3
4
5
6
7
8
9
10
11
12
13
14
$('svg.graph .points circle.plain').each( function(index) {
  // For each plain circle...
  $(this).on('mouseenter', function(event) {
    // Move the indicator line
    moveIndicator(index);
    // Show the tooltip for this data point
    showTooltip(this);
    ...
  }).on('mouseleave', function(event) {
    // Hide the tooltip
    hideTooltip();
    ...
  });
});

Wrapup

We have created three extensions to our base SVG graph; shadows on the data points, an indicator line for more visual focus and a tooltip for every data point which gives the precise values. The final interactive graph can be inspected in this jsFiddle or on this stand alone page. You can see the real-world project for one of the graphs with multiple data sets at the Elsevier Journal Insights website.

We hope you have learned a bit about how you can use SVG, how to access it with jQuery and add interactivity by animating properties using mouse events.

This blog post appeared earlier on the Inspire.nl blog, where I was lead developer on the Elsevier Journal Insights project.

Comments