d3-annotation

Made with by Susie Lu

#Introduction

Annotations establish context, and direct our users to insights and anomalies. So why are annotations so few and far between in visualizations on the web? Because implementing them is difficult.

But it shouldn't be.

Use d3-annotation with built-in annotation types, or extend it to make custom annotations. It is made for d3-v4 in SVG.

Contact me through the github repo or twitter.

#Setup

Include the file directly

You must include the d3 library before including the annotation file. Then you can add the compiled js file to your website

Using CDN

You can add the latest version of d3-annotation hosted on cdnjs.

Using NPM

You can add d3-annotation as a node module by running

npm i d3-svg-annotation -S

#Annotation Types

  • Presets
  • Options

    Note

    Line Type

    Orientation


    Align


    Connector

    Type

    End

d3.annotationLabel A centered label annotation Code below is ready to use with these setttings Longer textto show textwrappingAnnotations:)

Use d3.annotationLabel:

              
const type = d3.annotationLabel

const annotations = [{
  note: {
    label: "Longer text to show text wrapping",
    bgPadding: 20,
    title: "Annotations :)"
  },
  //can use x, y directly instead of data
  data: { date: "18-Sep-09", close: 185.02 },
  className: "show-bg",
  dy: 137,
  dx: 162
}]

const parseTime = d3.timeParse("%d-%b-%y")
const timeFormat = d3.timeFormat("%d-%b-%y")

//Skipping setting domains for sake of example
const x = d3.scaleTime().range([0, 800])
const y = d3.scaleLinear().range([300, 0])

const makeAnnotations = d3.annotation()
  .editMode(true)
  //also can set and override in the note.padding property
  //of the annotation object
  .notePadding(15)
  .type(type)
  //accessors & accessorsInverse not needed
  //if using x, y in annotations JSON
  .accessors({
    x: d => x(parseTime(d.date)),
    y: d => y(d.close)
  })
  .accessorsInverse({
     date: d => timeFormat(x.invert(d.x)),
     close: d => y.invert(d.y)
  })
  .annotations(annotations)

d3.select("svg")
  .append("g")
  .attr("class", "annotation-group")
  .call(makeAnnotations)

            

#In Practice

All annotations are made of just three parts, a note, a connector, and a subject.

Anatomy of an annotation

They are the foundational blocks of this library.

Customize the Subject by picking a base annotation

Settings for subject types are in the annotation object's .subject:

const annotations = [
  {
    note: { label: "Hi" },
    x: 100,
    y: 100,
    dy: 137,
    dx: 162,
    subject: { radius: 50, radiusPadding: 10 },
  },
];

d3.annotation().annotations(annotations);

d3.annotationCalloutCircle

  • radius or outerRadius and innerRadius: Number, pixels
  • radiusPadding: Number, pixels

d3.annotationCalloutRect

  • width: Number, pixels
  • height: Number, pixels

d3.annotationXYThreshold

  • x1, x2 or y1, y2: Number, pixels

d3.annotationBadge: this is the only base annotation that doesn't have a connector or note

  • text: String
  • radius: Number, pixels
  • x: "left" or "right"
  • y: "top" or "bottom"

No subject

  • d3.annotationLabel
  • d3.annotationCallout
  • d3.annotationCalloutElbow
  • d3.annotationCalloutCurve

Customize the Connector and Note

The Options panel in the Annotation Types UI exposes all of the options for connectors and notes. So the "Line Type" in the UI maps to { connector: { lineType : "horizontal" } }

There are two ways to customize the connectors and notes. You can either change these properties per annotation:

const annotations = [
  {
    note: { label: "Hi" },
    x: 100,
    y: 100,
    dy: 137,
    dx: 162,
    type: d3.annotationCalloutElbow,
    connector: { end: "arrow" },
  },
];

d3.annotation().annotations(annotations);

Or if you want all of the annotations to have these settings create a custom type with d3.annotationCustomType(annotationType, typeSettings):

const calloutWithArrow = d3.annotationCustomType(d3.annotationCalloutElbow, {
  connector: { end: "arrow" },
});

d3.annotation()
  .type(calloutWithArrow)
  .annotations([
    {
      text: "Plant paradise",
      data: { date: "18-Sep-09", close: 185.02 },
      dy: 37,
      dx: 42,
    },
  ])
  .editMode(true);

Both examples above produce the same results.

#Selecting Elements

  • All of the visible shapes (aside from the edit handles) in the default annotations are paths
  • There is an invisible rect (rect.annotation-note-bg) behind the text in the notes as a helper for more click area etc.
  • Hierarchy of classes: Annotation classes
  • Within the g.annotation-note-content there could be three additional elements: text.annotation-note-label, text.annotation-note-title, rect.annotation-note-bg

# Basic Styles

Now the library comes with default styles, read more about it in the 2.0 release post.

Before v2, there were style sheets you needed to use:

Available on github.

#Tips

  • In addition to the alignment settings for the note, you can also use the css text-anchor attribute to align the text within the note
  • When you update the d3.annotation().type() you will need to use the call functionality again to set up the annotations with the new type. See the Responsive with Types and Hover example
  • You do not need to call d3.annotation().update() if you are only changing the position(x,y) or the offset(dx, dy). See the Overlapping example
  • If you are importing custom fonts, you may notice the annotations don't load perfectly with text wrapping and alignment. To fix that you can use, document.fonts.ready to make sure the fonts are loaded first to reflect the custom font's spacing for all of the calculations. Here's an example:
document.fonts.ready.then(function () {
  d3.select("svg")
    .append("g")
    .attr("class", "annotation-group")
    .style("font-size", fontSize(ratio))
    .call(makeAnnotations);
});

#Examples

#Basic Without Scales

#Tooltips

#Responsive with Types and Hover

Example showing how to dynamically change anntation types

#Reimagining the Circle Pack

Remake of the circle pack with annotations as the exterior circles

#Thresholds and Annotation Color Styles

Using annotation design for emphasis

#Points of Interest with Badges

Expanding on points of interest outside of the chart itself

#Overlapping

Moving annotations algorithmically to prevent overlap using rect collision

#Encircling

Annotations following a set of points using d3.packEnclose

#Map with Tooltips and Edit Mode

A map with tooltips, double-click to enable/disable editMode

#d3 v2.0 Features

Easy annotation coloring, new badge options, and new note positioning options

#Essays

#API

d3.annotation()

annotation.annotations([ objects ])

Pass an array of objects with annotation properties:

  • id: This can be anything that will help you filter and parse your annotations

Annotation JSON

  • x,y (number:pixels): Position of the subject and one end of the connector
  • data (object): If you also set accessor functions, you can give data instead of x,y coordinates for placing your annotations
  • dx, dy (number:pixels): Position of the note and one end of the connector, as an offset from x,y
  • nx, ny (number:pixels): Position of the note and one end of the connector, as the raw x,y position not an offset
  • type (d3annotationType): Type for this annotation. Recommended to set the base type at the d3.annotation().type() property and use this to override the base
  • disable ([string]): takes the values 'connector', 'subject', and 'note' pass them in this array if you want to disable those parts from rendering
  • color([string]): only in version 2.0, you can pass a color string that will be applied to the annotation. This color can be overridden via css or inline-styles
  • note (object): You can specify a title and label property here. All of the annotation types that come with d3-annotation have built in functionality to take the title and the label and add them to the note, however the underlying system is composable in a way that you could customize the note to contain any type of content. You can also use this to overwrite the default note properties (align, orientation, lineType, wrap, padding) in the type. For example if on one of the notes you wanted to align it differently. In v2.1.0 and higher you can pass a regex or string to customize the wrapping { wrapSplitter: /\n/ }. In v2.3.0 and higher, you can pass a bgPadding that accepts a number or an object with one or more top, bottom, left, and right properties, to increase the size of the rectangle behind the text.
  • connector (object): Some connectors such as the curve connector require additional parameters to set up the annotation. You can also use this to overwrite the default connector properties (type, end) in the type. For example if you wanted to add an arrow to the end of some of the annotations in the array you could add { end: "arrow" } to this connector property on the relevant annotations. In v2.1.0 and higher, there is also a { endScale: 2 } that allows you to scale the size of the dot or arrow end types
  • subject (object): Some subjects such as the circle require additional parameters to set up the annotation.

If you don't pass anything to this function, it returns the current array of annotations.

annotation.accessors({ x: function, y: function })

Functions that would map the .data attribute of your annotation to x and y positions:

//Sample .data for an annotation
//{date: "2-Jan-08", close: 194.84}
const parseTime = d3.timeParse("%d-%b-%y")

d3.annotation().accessors({
  x: d => x(parseTime(d.date)),
  y: d => y(d.close)
})

annotation.accessorsInverse({ <x property mapping>: function, <y property mapping>: function })

The inverse of the accessor function. If you are given x, y coordinates, how to get back to the original data properties. Only for the x and y accessors:

//Sample .data for an annotation
//{date: "2-Jan-08", close: 194.84}
const timeFormat = d3.timeFormat("%d-%b-%y")

d3.annotation().accessorsInverse({
  date: d => timeFormat(x.invert(d.x)),
  close: d => y.invert(d.y)
})

annotation.editMode(boolean)

If this is true, then the annotation will create handles for parts of the annotation that are draggable. You can style these handles with the circle.handle selector. If you are hooking this up to a button, you will need to run the update function below, after changing the editMode. Example in Map with Tooltips and Edit Mode

annotation.update()

Redraws all of the annotations. Typically used to reflect updated settings. If you are only updating the position (x, y) or the offset (dx, dy) you do not need to run call on makeAnnotations afterwards. Example in Layout - Encircling Annotation.

annotation.updateText()

If you only want to update the text then use this function. It will re-evaluate with the new text and text wrapping settings. This is separated from the update() function for flexibility with performance. If you call the entire set again it will run both functions.

annotation.updatedAccessors()

Redraws all the annotations with updated accessor scales. Example in Responsive with Types and Hover

annotation.type( d3annotationType ) You can pass different types into the annotation objects themselves, but you can also set a default type here. If you want to change the type, you will need to re-call the d3.annotation function on your element to recreate the annotations with the new type. Example in Responsive with Types and Hover

annotation.json()

You can run this in the developer console and it will print out the current annotation settings and copy them to your clipboard. Please note that in the console each annotation will also include the type that you've associated with it.

annotation.collection()

Access to the collection of annotations with the instantiated types.

annotation.textWrap() Change the overall textWrap, otherwise in the annotation object array you can change each individual one with the {note: {wrap: 30}} property. This function calls updateText() internally so you do not need to call both functions when updating textWrap.

annotation.notePadding() Change the overall notePadding, otherwise in the annotation object array you can change each individual one with the {note: {padding: 30}} property

annotation.disable() Takes the values 'connector', 'subject', and 'note' pass them in this array if you want to disable those parts from rendering

annotation.on() Takes the values 'subjectover', 'subjectout', 'subjectclick', 'connectorover', 'connectorout', 'connectorclick', 'noteover', 'noteout', 'noteclick', 'dragend', 'dragstart' as custom dispatch events you can hook into.

#Extending Annotation Types

The underlying code for d3-annotation has a base annotation type that all of the annotation types extend. The settings and components that make up the different types are customizable.

The goal was to make a system that was easy to add new types and implement layout algorithms with. A longer post with details about how you can make your own type will be coming out soon.

If you're interested in looking at the architecture before the post you can find the source code here.

#Notes

Extremely grateful to my team at Netflix for mentoring me, giving me feedback, and helping out on this project. Cheers Elijah, James, Jason, and Nathan.

Invaluable help from Fil, thanks for jumping in early to help with testing and discussion.

Sam keeps me sane and gets all the perf wins \o/

This is a data visualization project that wouldn't be possible without Mike Bostock's work on d3, and all of the inspiring prior art in annotations, particularly Adam Pearce's Swoopy Drag, and Andrew Mollica's Ring Note.

Thumbs up to Nunito and Bungee via Google Fonts and Materialize for making the docs site building a breeze.