swoopyDrag.js

Artisanal label placement for d3 graphics

A fork of 1wheel's original swoopyDrag.js

“The annotation layer is the most important thing we do” —Amanda Cox

swoopyDrag helps you hand place annotations on d3 graphics. It takes an array of objects representing annotations and turns them into lines and labels. Drag the text and control circles below to update the annotations array:

New! Click and drag from any point to create a new annotation with bootstrap.

The x and y functions are called on each annotation to determine its position. Setting draggable to true adds control points to the path strings and enables label dragging - turn on while developing and off when you're ready to publish.

var swoopy = d3.swoopyDrag()
    .x(function(d){ return xScale(d.xVal) })
    .y(function(d){ return yScale(d.yVal) })
    .draggable(true)
    .annotations(annotations)

The shape of each annotation's line is determined by the path property, the text by the text property and the position of the text by the testOffset property. Currently only straight paths (paths of the form M 0,0 L 10,10) and béziers (paths of the form M 0,0 C 10,10 10,15, 20,15) are supported—see my interactive path string tutorial for more.

The annotations are added to the page just like d3.svg.axis - append a new group element and use call:

var swoopySel = svg.append('g').call(swoopy)

After posititioning the labels, open the dev tools, run copy(annotations) in the console and paste over the old annotations array in your text editor.

Responsive

Since each annotation's position is determined primarily by scales, lines and labels will still point to the correct position when the chart size changes. As the chart shrinks though, the annotations might overlap or cover up data points. To show fewer or differently positioned labels on mobile, you could create multiple annotation arrays for different screen sizes:

d3.swoopyDrag()
  .annotations(innerWidth < 800 ? mobileAnnotations : desktopAnnotations)

Alternatively if there's just one or two problematic annotations that only work above or below some sizes, you could add maxWidth and minWidth properties to the overlapping annotations and filter:

d3.swoopyDrag()
  .annotations(annotations.filter(function(d){
    return (typeof(d.minWidth) == 'undefined' || innerWidth > d.minWidth)
        && (typeof(d.maxWidth) == 'undefined' || innerWidth < d.maxWidth)
    }))

Arrowheads

SVG has native support for arrowheads, but they can be a little fiddly to get working. First, add a marker element to the page the describes the shape of the arrow:

svg.append('marker')
    .attr('id', 'arrow')
    .attr('viewBox', '-10 -10 20 20')
    .attr('markerWidth', 20)
    .attr('markerHeight', 20)
    .attr('orient', 'auto')
  .append('path')
    .attr('d', 'M-6.75,-6.75 L 0,0 L -6.75,6.75')

Next, select paths in each annotation and set their marker-end attribute:

swoopySel.selectAll('path').attr('marker-end', 'url(#arrow)')

Text Wrap

Multiline text can be added with d3-jetpack. Select all of the text elements, clear the existing text, then use d3.wordwrap and tspans to wrap the text:

swoopySel.selectAll('text')
    .each(function(d){
      d3.select(this)
          .text('')                        //clear existing text
          .tspans(d3.wordwrap(d.text, 20)) //wrap after 20 char
    })  

Since the annotations are made up of selectable path and group elements, they can be styled differently.

Bootstrap your annotations

Bootstrap lets you add annotations by clicking and dragging.

var swoopyBootstrap = swoopy.bootstrap()
  .sel(swoopySel)                        // your d3 selection for swoopy annotations
  .labelAccessor(ƒ('species'))           // this function sets label text
  .scale({x: yourXScale, y: yourYScale}) // your x and y scales

d3.selectAll('rect')
  .data(yourData).enter()
  ...
  .call(swoopyBootstrap)

Examples

Minute by Minute Point Differentials
NBA Win/Loss Records

Other Tools

swoopyarrows creates fancier swoops, including circular and loopy arcs.
labella.js uses a force directed layout to position timeline labels with no overlap.
svg-crowbar lets you export a svg file and add annotations manually.
ai2html illustrator script that creates responsive html.

Contribute

github.com/1wheel/swoopy-drag

ep = Math.max(1, Math.abs(600)) setWidth(curX + (isGrowing ? 1 : -1)*5) }) })() })