19 avr. 2015

Fullscreen D3 graphs

What are we going to tackle?

When creating D3 graphs, the more graphs you put in your page, the less readable it tends to be. A friend of mine advise me to set my graphs in fullscreen. I immediately loved the idea. That would definitely help users to focus.

What do we need to solve?

  1. Our graphs need to be responsive: when they will be toggled on fullscreen, they need to expand and take as much space as possible.
  2. When setting up fullscreen, the viewport is black despite all your already setup styles.
  3. Tooltips on your graphs needs to be included in the fullscreen's viewport.
  4. As your viewport is changing so are the references on where your tooltips needs to be displayed.

Piece by piece solution

First, let's setup our markup in Jade. figure is our SVG container, the div, .svg-content, is where D3 will place our SVG content and the div, .tip, is our tooltip for this graph:
figure
  .svg-content
    .tip
      span Point
      .arrow
Now, we need to setup our style so that our SVG content takes the available width of its container or the width of the viewport when it will be set on fullscreen. Note that we also need to set the background color of the container so that we don't end up with a black viewport. Here is the relevant extract of the Sylus file:
// Container for responsive SVG
// Mixin for creating SVG container with specific aspect ratio
svgContainer()
  display inline-block
  position relative
  width 100%
  vertical-align middle
  overflow hidden

figure
  display inline-block
  absolute top left
  height 100%
  margin 0
  // This container targets 1:1 SVG
  svgContainer()

  .svg-content
    // Here are the properties for the fullscreen content
    &:fullscreen
      size 100%
      background white
For the tooltip, we use a fixed position that will be modified using our logic. The style is pretty straightforward in Stylus:
.tip
  fixed top left
Now, for creating a responsive D3 graph, we need to use the preserveAspectRatio and the viewBox attributes on the graph instead of the regular width and height on the SVG tag. This is what is done by the following logic in Coffee on a 100x100 graph:
svgWidth = svgHeight = 100
svg = d3.select '.svg-content'
  .append 'svg:svg'
    .attr 'preserveAspectRatio', 'xMinYMin meet'
    .attr 'viewBox', "0 0 #{svgWidth} #{svgHeight}"
Setting the graph in fullscreen is eased thanks to screenfull.js. This library provide a cross browser API of the vanilla JavaScript Fullscreen API:
Template.home.rendered = ->
  @fullscreen = ->
    return screenfull.exit() if screenfull.isFullscreen
    target = (@$ '.svg-content')[0]
    screenfull.request target

Template.home.events
  'click button': (e, t) ->
    $button = t.$ e.target
    role = $button.attr 'data-role'
    t.fullscreen() if role is 'fullscreen'
Now, we need to adjust the tooltips so that they are positioned properly. In our example, we are using a Voronoï diagram. Hovering on the path elements of the Voronoï is triggering the tooltip of its points modeled by circle elements. Getting the position is achieved thank to the DOM API getBoundingClientRect:
  tip = @$ '.tip'
  @positionSetTip = (circle) ->
    rect = circle[0].getBoundingClientRect()
    tip.css 'transform', "translate3d(\
      #{rect.left + .5 * (rect.width - tip.width())}px,\
      #{rect.top - tip.height()}px, 0)"
  @showHideTip = -> tip.toggleClass 'show'
  @showTip = -> Meteor.setTimeout (-> tip.addClass 'show'), 300
  # A debouncing function is used for transitioning over path
  @lazyShowHideTip = _.debounce @showHideTip, 300
  path = svg.append 'g'
    .selectAll 'path'
  svg.append 'g'
    .selectAll 'circle'
    .data vertice
    .enter()
    .append 'circle'
      .attr 'transform', (d) -> "translate(#{d.toString()})"
      .attr 'r', 1
  data = path.data (voronoi vertice)
  data
    .enter()
    .append 'path'
      .attr 'd', polygon
      .order()
      .on 'mouseover', (d, i) =>
        circle = $ "circle:nth-child(#{i + 1})"
        @positionSetTip circle, i
        @showTip()
      .on 'mouseleave', =>
        @lazyShowHideTip()
  data.exit().remove()

Some links

Bonus

On the demo, I've also added some features like:
  • An animated tooltip: when entering or leaving a small animation is done.
  • The Voronoï graph is animated using a simple random function.
  • Key events are handle to let you set the graph in fullscreen or to animate it.
Happy coding!