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.
You must include the d3 library before including the annotation file. Then you can add the compiled js file to your website
You can add the latest version of d3-annotation hosted on cdnjs.
You can add d3-annotation as a node module by running
npm i d3-svg-annotation -S
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)
All annotations are made of just three parts, a note, a connector, and a subject.
They are the foundational blocks of this library.
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
d3.annotationCalloutRect
d3.annotationXYThreshold
d3.annotationBadge: this is the only base annotation that doesn't have a connector or note
No subject
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.
rect.annotation-note-bg
) behind the text in the notes as a helper for more click area etc.text.annotation-note-label
, text.annotation-note-title
, rect.annotation-note-bg
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.
text-anchor
attribute to align the text within the notedocument.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);
});
d3.annotation()
annotation.annotations([ objects ])
Pass an array of objects with annotation properties:
{ 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.{ 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 typesIf 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.
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.
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.