In this chapter, we will explore some basic graphical building blocks provided by D3. In the previous chapters (Chapters 3 and 4), we learned how D3 manipulates the DOM tree in order to represent information visually, but we did not really concentrate on graphical objects. They will be the topic for the present chapter. This will also give us the opportunity to understand how different D3 facilities are structured and how they work together. We will also learn mechanisms to organize our own code for convenience and reuse.
SVG provides only a small set of built-in geometric shapes (like
circles, rectangles, and lines; see Appendix B). Anything else
has to be built up painstakingly (and rather painfully) either
from those basic shapes or by using the <path>
element and its turtle graphics command language. How does D3 assist with this?
It is worth remembering that D3 is a JavaScript library for manipulating the DOM tree, not a graphics package. It doesn’t paint pixels, it operates on DOM elements. It should therefore come as no surprise that the way D3 facilitates graphics operations is by organizing and streamlining the way DOM elements are handled.
To produce complex figures, D3 employs three different styles of helper functions. These can be distinguished by the scale they act on: generators generate individual attributes, components create entire DOM elements, and layouts determine the overall arrangement of the whole figure (see Figure 5-1). Pay close attention, because none of them does quite what one might expect.
Generators are wrappers around the <path>
element’s turtle graphics command language: they free the programmer from having to compose the cryptic command string. Generators typically consume a data set and return a string that is suitable for the <path>
element’s d
attribute.
But they don’t create the <path>
element: it’s up to the surrounding code to make sure such an element exists. We will see examples of generators in this chapter, and in Chapter 9.
Components are functions that inject newly created elements into the DOM tree. They always take a Selection
as argument, to indicate where in the DOM tree the new elements will be added. Often the selection will be a <g>
element as a container for the newly created DOM elements. Components return nothing;
they are usually invoked synthetically using the call()
function of the destination Selection
. Components do modify the DOM tree: they are the most “active” tools in the D3 toolbox. The axMkr
in Example 2-6 was an example of a component, as was the drag-and-drop behavior component in Example 4-4.
Layouts consume a data set and calculate the pixel coordinates and angles that graphical elements should have to represent the data set in some nontrivial way. For example, in Chapter 9 we will see a layout that consumes a hierarchical data set and calculates the pixel coordinates for each node in order to represent the data set as a tree diagram. Layouts return a data structure that can be bound to a `Selection`. But they don’t create or place any graphical elements themselves; they merely calculate the coordinates where the elements should go. It’s up to the calling code to make use of this information. We’ll see examples later in this chapter, and several more in Chapters 8 and 9.
All of these helpers are implemented as function objects. They perform their primary task when invoked as a function. But they also have state and expose an API of member functions to modify and configure their behavior. The general workflow is therefore as follows:
Create an instance of the desired helper.
Configure it as necessary (for example, by specifying accessor functions for the data set) using its member functions.
Invoke it in the appropriate evaluation context.
Frequently, all three steps are bundled into one, so that the instance
is created, configured, and evaluated as part of an attr()
, call()
,
or data()
invocation. (See Example 2-6 for some examples.)
Finally, keep in mind that the same patterns are available and convenient for your own code. The Component pattern, in particular, is frequently useful to reduce code duplication, even for tiny, single-page projects. (We’ll come back to this toward the end of this chapter.)
Symbols are predefined shapes that can be used to represent individual
data points in a graph (for example, in a scatter plot). This section
introduces two different mechanisms to create symbols for use in an SVG.
The first uses the D3 symbol generator and the SVG <path>
element. The
second employs the SVG <use>
tag, which allows you to reuse a
fragment of an SVG document elsewhere in the document.
D3 defines seven built-in symbol types for use with the d3.symbol()
generator; see Figure 5-2.
Example 5-1 demonstrates how you can create and use them; the
resulting graph is shown in Figure 5-3.
The symbol generator is a bit different than the other D3 shape generators because it does not consume a data set. All you can configure are the symbol type and size (see Table 5-1). In particular, there are no provisions to fix the position of the symbol on the graph! Instead, all symbols are rendered at the origin, and you use SVG transforms to move them into their final positions. This is a common idiom when working with D3 and SVG; we will see it often. Some useful information on SVG transformations is in “SVG Transformations”.
function
makeSymbols
(
)
{
var
data
=
[
{
"x"
:
40
,
"y"
:
0
,
"val"
:
"A"
}
,
{
"x"
:
80
,
"y"
:
30
,
"val"
:
"A"
}
,
{
"x"
:
120
,
"y"
:
-
10
,
"val"
:
"B"
}
,
{
"x"
:
160
,
"y"
:
15
,
"val"
:
"A"
}
,
{
"x"
:
200
,
"y"
:
0
,
"val"
:
"C"
}
,
{
"x"
:
240
,
"y"
:
10
,
"val"
:
"B"
}
]
;
var
symMkr
=
d3
.
symbol
(
)
.
size
(
81
)
.
type
(
d3
.
symbolStar
)
;
var
scY
=
d3
.
scaleLinear
(
)
.
domain
(
[
-
10
,
30
]
)
.
range
(
[
80
,
40
]
)
;
d3
.
select
(
"#symbols"
)
.
append
(
"g"
)
.
selectAll
(
"path"
)
.
data
(
data
)
.
enter
(
)
.
append
(
"path"
)
.
attr
(
"d"
,
symMkr
)
.
attr
(
"fill"
,
"red"
)
.
attr
(
"transform"
,
d
=>
"translate("
+
d
[
"x"
]
+
","
+
scY
(
d
[
"y"
]
)
+
")"
)
;
var
scT
=
d3
.
scaleOrdinal
(
d3
.
symbols
)
.
domain
(
[
"A"
,
"B"
,
"C"
]
)
;
d3
.
select
(
"#symbols"
)
.
append
(
"g"
)
.
attr
(
"transform"
,
"translate(300,0)"
)
.
selectAll
(
"path"
)
.
data
(
data
)
.
enter
(
)
.
append
(
"path"
)
.
attr
(
"d"
,
d
=>
symMkr
.
type
(
scT
(
d
[
"val"
]
)
)
(
)
)
.
attr
(
"fill"
,
"none"
)
.
attr
(
"stroke"
,
"blue"
)
.
attr
(
"stroke-width"
,
2
)
.
attr
(
"transform"
,
d
=>
"translate("
+
d
[
"x"
]
+
","
+
scY
(
d
[
"y"
]
)
+
")"
)
;
}
Define a data set, consisting of x and y coordinates and an additional “value.”
The d3.symbol()
factory function returns a symbol generator instance. Here, we immediately configure the size of the symbols and select the star shape as the type to use. (The size parameter is proportional to the symbol’s area, not its radius.)
Conveniently, the x values in the data set will work as pixel
coordinates, but we need a scale object to translate the y values
to vertical positions. The inverted range()
interval compensates
for the upside-down graphical coordinates used by SVG.
Create an initial selection and append a <g>
element. This <g>
will
hold the symbols on the left side of the graph and keep them separate
from the ones on the right.
Bind the data and create a new <path>
element for each data point.
Populate the d
attribute of the previously created <path>
element
using the symbol generator. Because the symbol generator is already
fully configured, we don’t need to do anything else at this point.
The symbol generator is not evaluated, but instead is supplied as a function;
it will automatically be invoked for each data point by the selection.
Move each newly created <path>
element to its final position through
an SVG transform, using the scale object to calculate the vertical
offset.
For the right side of the graph, we will change the shape of the
the symbol according to the third column in the data set. To do
so, we need to associate the values from the data set with symbol
shapes. An ordinal (or discrete) scale is essentially a hashmap that
associates each value in the input domain with a value in the
array d3.symbols
of available symbol shapes. (See Chapter 7
to learn more about the different scale objects.)
Append a new <g>
element for the second set of symbols, and shift
it to the right. This shift will also apply to all children of the
<g>
element.
We can reuse the symbol generator instance created earlier. Here, every time the symbol generator is invoked, its type is set explicitly, based on the value in the data set.
For a change, the symbols are not filled; only the outlines are shown.
The transform to move each symbol into position is exactly the
same as before. There is no need to change it, because the entire
containing <g>
element has been moved to avoid overprinting the
stars on the left side.
This example demonstrates the basic techniques; many further variations are possible. For example, not only the shape but also the size and color of each symbol can be varied dynamically. To choose a color, the ordinal scale will come in handy again, like so:
d3
.
scaleOrdinal
([
"red"
,
"green"
,
"blue"
]).
domain
([
"A"
,
"B"
,
"C"
]);
Or choose contrasting colors for interior and boundary (fill
and stroke
) to achieve a different effect.
Function | Description |
---|---|
|
Returns a new symbol generator. Unless configured otherwise, the generator will create a circle of 64 square pixels. |
|
Returns a string, suitable as value of the |
|
Sets the symbol shape. The argument should be one of the predefined symbol shapes shown in Figure 5-2. Returns the current value if called without argument. |
|
Sets the symbol size. The argument is interpreted as the approximate area of the symbol (in pixels, before any scale transformation is applied). Returns the current value if invoked without an argument. The default size is 64 square pixels. |
|
An array (not a function), containing the predefined symbol shapes. |
The way generators are used in Example 5-1 may appear a bit
opaque, but there is really no magic here. Ultimately, all that happens
is that the generator instance returns a string of commands using the
<path>
element’s command syntax. You can do that. For example, if you
define the following function:1
function
arrow
()
{
return
"M0 0 L16 0 M8 4 L16 0 L8 -4"
;
}
then you can replace step 6 in Example 5-1 with:
.
attr
(
"d"
,
arrow
)
Now try applying a data-dependent rotation, using an appropriate SVG
transform, to the generated <path>
element! There are many more options.
The <use>
tag lets you reuse arbitrary SVG fragments within a document
and hence provides an alternative for creating reusable symbols in
SVG.2 It is recommended (but not required) that components
intended for reuse are defined inside of the SVG document’s <defs>
section. (The <defs>
section must be part of the SVG document; do
not place it into the header of the HTML page, for example!)
Example 5-2 shows a document that defines a reusable
component inside its <defs>
section, and Example 5-3 shows
how one might use it. (See Figure 5-4 for the result.)
<svg
id=
"usedefs"
width=
"275"
height=
"100"
>
<defs
>
<g
id=
"crosshair"
>
<circle
cx=
"0"
cy=
"0"
r=
"2"
fill=
"none"
/>
<line
x1=
"-3"
y1=
"0"
x2=
"-1"
y2=
"0"
/>
<line
x1=
"1"
y1=
"0"
x2=
"3"
y2=
"0"
/>
<line
x1=
"0"
y1=
"-1"
x2=
"0"
y2=
"-3"
/>
<line
x1=
"0"
y1=
"1"
x2=
"0"
y2=
"3"
/>
</g>
</defs>
</svg>
Open a <defs>
section as a child of the <svg>
element.
Create a <g>
element that will be the container for all elements
making up the symbol, and assign an id
to it. The <use>
tag will
refer to this symbol via the identifier specified here.
Add the graphical elements for the symbol. Remember that the symbol can be rescaled when it is being used, hence its absolute size is not important here.
function
makeCrosshair
(
)
{
var
data
=
[
[
180
,
1
]
,
[
260
,
3
]
,
[
340
,
2
]
,
[
420
,
4
]
]
;
d3
.
select
(
"#usedefs"
)
.
selectAll
(
"use"
)
.
data
(
data
)
.
enter
(
)
.
append
(
"use"
)
.
attr
(
"href"
,
"#crosshair"
)
.
attr
(
"transform"
,
d
=>
"translate("
+
d
[
0
]
+
",50) scale("
+
2
*
d
[
1
]
+
")"
)
.
attr
(
"stroke"
,
"black"
)
.
attr
(
"stroke-width"
,
d
=>
0.5
/
Math
.
sqrt
(
d
[
1
]
)
)
;
}
A minimal data set. The first component of each record will be used for the horizontal position, the second one for the symbol size.
Select the SVG element and bind data as usual, creating a <use>
element for each record in the data set.
Set the href
attribute for every newly created <use>
element
to the symbol identifier. The final element in the page will look
something like: <use href="#crosshair" ...>
.
Use the transform
attribute to choose the symbol’s position and
size.
Set the stroke color. It is necessary to do so explicitly, because
the default value of the stroke
attribute is none
!
Set the stroke width. To obtain strokes of roughly equal width in the figure, the stroke width must be chosen so as to cancel the effect of the earlier scale transformation.
Of course, the contents of the <defs>
section is no different than
the rest of the document, which means that it, too, can be generated
dynamically using D3. For example, the following snippet creates a
diagonal cross (also called a “saltire”) that can be invoked through
<use>
tags:3
var
d
=
d3
.
select
(
"svg"
).
append
(
"defs"
)
.
append
(
"g"
).
attr
(
"id"
,
"saltire"
);
d
.
append
(
"line"
)
.
attr
(
"x1"
,
-
1
).
attr
(
"y1"
,
1
).
attr
(
"x2"
,
1
).
attr
(
"y2"
,
-
1
);
d
.
append
(
"line"
)
.
attr
(
"x1"
,
-
1
).
attr
(
"y1"
,
-
1
).
attr
(
"x2"
,
1
).
attr
(
"y2"
,
1
);
Yet another way is to load the appropriate SVG from a file and insert it into the current document (see Chapter 6 for an example).
Drawing lines and curves follows precisely the workflow outlined at the
beginning of this chapter: you instantiate a generator, feed it a data set,
and then invoke it to obtain a string suitable for the d
attribute of a <path>
element.
function
makeLines
(
)
{
// Prepare a data set and scale it properly for plotting
var
ds
=
[
[
1
,
1
]
,
[
2
,
2
]
,
[
3
,
4
]
,
[
4
,
4
]
,
[
5
,
2
]
,
[
6
,
2
]
,
[
7
,
3
]
,
[
8
,
1
]
,
[
9
,
2
]
]
;
var
xSc
=
d3
.
scaleLinear
(
)
.
domain
(
[
1
,
9
]
)
.
range
(
[
50
,
250
]
)
;
var
ySc
=
d3
.
scaleLinear
(
)
.
domain
(
[
0
,
5
]
)
.
range
(
[
175
,
25
]
)
;
ds
=
ds
.
map
(
d
=>
[
xSc
(
d
[
0
]
)
,
ySc
(
d
[
1
]
)
]
)
;
// Draw circles for the individual data points
d3
.
select
(
"#lines"
)
.
append
(
"g"
)
.
selectAll
(
"circle"
)
.
data
(
ds
)
.
enter
(
)
.
append
(
"circle"
)
.
attr
(
"r"
,
3
)
.
attr
(
"cx"
,
d
=>
d
[
0
]
)
.
attr
(
"cy"
,
d
=>
d
[
1
]
)
;
// Generate a line
var
lnMkr
=
d3
.
line
(
)
;
d3
.
select
(
"#lines"
)
.
append
(
"g"
)
.
append
(
"path"
)
.
attr
(
"d"
,
lnMkr
(
ds
)
)
.
attr
(
"fill"
,
"none"
)
.
attr
(
"stroke"
,
"red"
)
;
}
Here, we apply the scale operations to the entire data set ahead of time, rather than invoking them on an as-needed basis later, in order to keep the subsequent code simpler.
We use <circle>
elements to mark the individual data
points. (We could have used symbols instead, but the transform
syntax
required to move them into position is clumsier.)
Generate an instance of the line generator.
Remember that a line is ultimately realized as a <path>
element,
hence we must first create a <path>
element that will subsequently
be configured.
Invoke the line generator, with the data set as argument.
Fix the presentational attributes.
Three aspects of the line generator are configurable (see Table 5-2).
Using the x()
and y()
methods, you can set the accessor functions
that pick out the x and y coordinates from the data set. The accessors
are called with three arguments:
function
(
d
,
i
,
data
)
{
...
}
where d
is the current record in the data set, i
is its index, and
data
is the entire data set itself. If you don’t supply your accessors,
then the default is used, which selects the first and second column from
a multidimensional array. (This was done in Example 5-4.)
Another configuration option allows you to mark certain points as
“undefined,” by passing an accessor to the defined(accessor)
function. The accessor must return a Boolean value; if it is false
for some data points, then the generated line will exclude those points,
resulting in a break that separates the line into segments. An example
should make this clear. If you replace the line:
var
lnMkr
=
d3
.
line
();
in Example 5-4 with:
var
lnMkr
=
d3
.
line
().
defined
(
(
d
,
i
)
=>
i
==
3
?
false
:
true
);
then the point at index position 3 is treated as undefined, resulting in the graph on the right side of Figure 5-5. You can use this feature to exclude certain data points, for example, those that fall outside the desired plot range or where some quantity changes discontinuously, without having to remove them from the data set itself.
The shape and nature of the curve that is drawn to connect the individual
points can be changed through curve factories. (They will be discussed
in the next section.)
Finally, be aware that the line generator will connect points in the
sequence in which they appear in the data set. This implies that you
may need to sort points (for example, by their x coordinates) before
passing them to the line generator. The following snippet uses the
sort()
function of JavaScript’s Array
type to sort the data set
from Example 5-4 by the x coordinates of the data points:
ds
=
ds
.
sort
(
(
a
,
b
)
=>
a
[
0
]
>
b
[
0
]
)
As usual, the line generator’s member functions in Table 5-2 can act both as getters and setters. When called without arguments, they act as getters and return the current value. When called as setters, they return the current generator instance, in order to allow method chaining.
Function | Description |
---|---|
|
Returns a new line generator instance. |
|
When called with a data set, returns a string, suitable for the |
|
Set accessor functions for the x and y coordinates of each data point.
Each accessor will be called with three arguments: the current record |
|
Sets an accessor that allows to mark arbitrary points as undefined. The
accessor is called with same three arguments as the coordinate accessors,
but must return a Boolean value; points for which the accessor returns
|
|
In addition to straight lines, D3 offers a variety of other curve shapes to connect consecutive points—and you can also define your own. You choose a different curve shape by passing the appropriate curve factory to the line generator, like so:
var
lnMkr
=
d3
.
line
().
curve
(
d3
.
curveLinear
);
The built-in curve factories (see Figure 5-6) fall into two major groups:
Curves that are completely determined by the data set and that pass exactly through all data points (see Table 5-3).
Curves that depend on an additional curvature or stiffness parameter and that may not pass exactly through the data points (see Table 5-4).
The built-in curves are primarily intended for information visualization; they were not developed for scientific curve plotting. Curves are drawn in the order of the points in the data set; data points are not reordered based on the x coordinates! The built-in curves make no direct provisions for curve fitting, for filtering noisy data, or for weighting data points according to their local error. If you need such functionality, you will have to implement it yourself, for example, by implementing a custom curve generator (see the next section), or by first processing the input data separately. The D3 Reference Documentation contains additional details and references to the original literature.
In addition to straight lines and step functions, D3 also provides several
spline implementations to connect consecutive points.
Natural splines match direction and curvature where line segments meet,
and have zero curvature at the ends of the data set.
The curve created by d3.curveMonotoneX
does not
overshoot the data points in the vertical when passing through the
points in the horizontal direction (similarly for d3.curveMonontoneY
).
Straight lines | Splines | Step |
---|---|---|
|
|
|
|
|
|
|
|
Both cardinal and Catmull-Rom splines pass through the data points exactly, but relax the condition that the curvatures should match at all line segment joints. Instead, they impose additional constraints on the local tangents at the joints. Both depend on an adjustable parameter that controls their shape; this parameter should take values between 0 and 1. Cardinal and Catmull-Rom splines are identical to each other if this parameter is zero; they differ in their behavior if the parameter value increases. The curve factories provide methods to set the parameter, for example:
d3
.
curveCardinal
.
tension
(
0.5
);
d3
.
curveCatmullRomClosed
.
alpha
(
0.25
);
The curves created by d3.curveBundle
are not required to pass through
the data points exactly. When the adjustable parameter is zero, the
resulting curve coincides with the curve generated by d3.curveBasis
.
Except for d3.curveBundle
, the adjustable curve types provide open
and closed variants (such as d3.curveCardinalOpen
or
d3.curveBasisClosed
). The open curve factories use the end points of
the data set for the determination of the curve shape, but do not extend
the drawn curve to the actual end points (the curves stop at the
second-to-last point). For the closed curves, the ends of the data
set are connected, so that the first and last point are treated as
neighbors. The resulting line forms a geometrically closed curve.
Curve factory | Adjustable parameter | Default value |
---|---|---|
|
|
0 |
|
|
0.5 |
|
|
0.85 |
|
If you are dissatisfied with the built-in curves, you can provide your
own curve algorithms by supplying a custom curve factory to the
line
generator—for example, to implement a fitting or smoothing
method.4 A curve factory is a function that accepts a single argument
and returns an object that implements the curve API. When D3 invokes
the factory function, it supplies a d3.path
object as the argument. The
d3.path
facility exposes a turtle graphics interface similar to
the command language of the <path>
element. Your implementation of
the curve API must populate this object according to your algorithm.
The curve API, which your custom code must implement, consists of
three functions: lineStart()
, lineEnd()
, and point(x, y)
—their semantics will be explained in a moment. Eventually,
D3 will turn the path object into a string that can be used to
populate the d
attribute of a <path>
element.
Let’s consider a very simple example, just to explain the mechanism. Example 5-5 implements a curve factory that draws a horizontal line at the median of the vertical positions of all the data points for each line segment. It behaves exactly like the built-in factories; for example, you could use it in Example 5-4 like so:
var
lnMkr
=
d3
.
line
().
curve
(
curveVerticalMedian
);
function
curveVerticalMedian
(
context
)
{
return
{
lineStart
:
function
(
)
{
this
.
data
=
[
]
;
}
,
point
:
function
(
x
,
y
)
{
this
.
data
.
push
(
[
x
,
y
]
)
;
}
,
lineEnd
:
function
(
)
{
var
xrange
=
d3
.
extent
(
this
.
data
,
d
=>
d
[
0
]
)
;
var
median
=
d3
.
median
(
this
.
data
,
d
=>
d
[
1
]
)
;
context
.
moveTo
(
xrange
[
0
]
,
median
)
;
context
.
lineTo
(
xrange
[
1
]
,
median
)
;
}
}
;
}
The lineStart()
function is called at the beginning of every
line segment (remember that every point data point marked as “undefined”
ends the current and forces a new line segment). Here, an array is
initialized that will hold the data points.
The point(x, y)
function is called for every data point with the
coordinates of that point. In this case, the coordinates of each point
are simply appended to the end of the array.
The lineEnd()
function is called at the end of each line segment.
Now that all the data points for the current line segment have been
collected, the horizontal extent of the data set and the median of the
vertical positions can be found, and with this information, we are now
ready to draw the desired line.
The context
variable is a d3.path
instance, which is supplied to
the curve factory by the D3 infrastructure. This object exposes
functions similar to the command language of the <path>
element.
All drawing has to be done using this object.
Here, we invisibly place the pen at the beginning of the line and…
… draw a visible line to its end.
This is the basic workflow. With a little more work, you can change the
lineEnd()
function to calculate and draw, for example, a linear regression line, a locally weighted interpolation such as LOESS, or a kernel
density estimate.5
If the resulting curve is not a straight line, then it will generally be necessary to build it up from individual pieces in a loop. In the following snippet, the horizontal plot interval is broken into 100 steps:
for
(
var
t
=
xmin
;
t
<=
xmax
;
t
+=
0.01
*
(
xmax
-
xmin
)
)
{
context
.
lineTo
(
t
,
y
(
t
)
);
}
Here, xmin
and xmax
are the end points of the plot interval, and
y(t)
is the computed value of the curve at position t
.
To calculate the median or a linear regression, the entire data set
must be known before computation and drawing can begin. Other curves
can be calculated and drawn incrementally—straight lines connecting
consecutive points, for example. In this case, the main logic moves
from the lineEnd()
function to the point()
function: for each new
data point, the next part of the curve is calculated and the appropriate
d3.path
methods are called to draw it.
So far, we have not encountered an example for a D3 layout yet. Layouts consume a data set and calculate where graph elements should be placed to visualize the data. They return a data structure containing this information, but don’t actually create or place any DOM elements; instead, it is up to the surrounding code to do this. Some D3 generators are specifically designed to work with the data structure returned by a particular layout. Although both can be used separately, understanding the layout helps to understand the generator.
The D3 pie layout provides a particularly clear example to demonstrate how this combined process works, and I will describe it here in some detail. Pie charts are sometimes frowned upon, but the D3 pie layout is of interest nevertheless, not only to explain the D3 layout workflow, but also as an extremely versatile method for creating any sort of circular graph, not just classical pie charts. For example, it’s a fun exercise to use the pie layout to create color wheels in order to explore different color spaces! (See Chapter 8 for color spaces available in D3; Chapter 9 discusses D3 layouts for tree diagrams.)
Example 5-6 defines a data set describing, say, an election outcome: for each candidate, it gives the name and number of votes. Figure 5-7 shows a pie chart of this data set.
function
makePie
(
)
{
var
data
=
[
{
name
:
"Jim"
,
votes
:
12
}
,
{
name
:
"Sue"
,
votes
:
5
}
,
{
name
:
"Bob"
,
votes
:
21
}
,
{
name
:
"Ann"
,
votes
:
17
}
,
{
name
:
"Dan"
,
votes
:
3
}
]
;
var
pie
=
d3
.
pie
(
)
.
value
(
d
=>
d
.
votes
)
.
padAngle
(
0.025
)
(
data
)
;
var
arcMkr
=
d3
.
arc
(
)
.
innerRadius
(
50
)
.
outerRadius
(
150
)
.
cornerRadius
(
10
)
;
var
scC
=
d3
.
scaleOrdinal
(
d3
.
schemePastel2
)
.
domain
(
pie
.
map
(
d
=>
d
.
index
)
)
var
g
=
d3
.
select
(
"#pie"
)
.
append
(
"g"
)
.
attr
(
"transform"
,
"translate(300, 175)"
)
g
.
selectAll
(
"path"
)
.
data
(
pie
)
.
enter
(
)
.
append
(
"path"
)
.
attr
(
"d"
,
arcMkr
)
.
attr
(
"fill"
,
d
=>
scC
(
d
.
index
)
)
.
attr
(
"stroke"
,
"grey"
)
;
g
.
selectAll
(
"text"
)
.
data
(
pie
)
.
enter
(
)
.
append
(
"text"
)
.
text
(
d
=>
d
.
data
.
name
)
.
attr
(
"x"
,
d
=>
arcMkr
.
innerRadius
(
85
)
.
centroid
(
d
)
[
0
]
)
.
attr
(
"y"
,
d
=>
arcMkr
.
innerRadius
(
85
)
.
centroid
(
d
)
[
1
]
)
.
attr
(
"font-family"
,
"sans-serif"
)
.
attr
(
"font-size"
,
14
)
.
attr
(
"text-anchor"
,
"middle"
)
;
}
This line creates an instance of the pie layout, configures it,
and invokes it on the data set, all in one step. The returned
data structure pie
is an array of objects with one object for
each record in the original data set. Each object contains a
reference to the original data, start and end angles of the
associated pie slice, and so on.
Creates and configures an arc generator (but does not invoke it).
Creates a scale object that associates a color with each pie
element.
The colors are drawn from the built-in scheme d3.schemePastel2
,
which defines a set of gentle colors, suitable as background colors,
but that nevertheless provide good visual contrast to each other.
Ordinal scales look up objects by their string representation.
Unfortunately, JavaScript’s default toString()
method does not
return a unique identifier for an object, only a generic constant.
For this reason, we can’t map from the elements of the pie
array
directly and have to choose a uniquely identifying member of each
element.
Selects the destination element in the page, appends a <g>
element
as the common container of the chart components, and moves it into position.
(The pie chart generated by the arc generator will be centered at the
origin.)
Bind the pie
data structure…
… and invoke the arc generator on each element.
The remaining commands create the text labels in each pie slice.
Each object in the pie
array has a member data
that contains a
reference to the corresponding record in the original data set.
The centroid()
function on the arc generator returns the coordinates
of a location that is centered inside each pie slice as a suitable
position for a label. Here I change the inner radius of the arc
generator in order to push the labels further toward the rim.
This is all. If you compare the API for the pie and the arc generator (Tables 5-5 and 5-6), you will notice how closely the default settings for the arc generator match the data structure returned by the pie layout, leading to the rather compact code in Example 5-6.
Function | Description |
---|---|
|
Returns a new layout operator in default configuration. |
|
Takes an array of arbitrary records and produces an array of objects. Each output object represents one pie slice and has the following members:
The segments in the returned array are always sorted according to the sequence of values in the input data set; they are not necessarily sorted in order of ascending angular positions in the chart. |
|
Numeric value or accessor function to return the relevant numerical value that
the current slice represents. Defaults to: |
|
Sets the comparator that is used to sort elements of the original data set. The sorting affects the assignment of elements to positions around the chart; it does not affect the order of segments in the returned array. |
|
Sets the comparator that is used to sort elements of the original data set by their value. The sorting affects the assignment of elements to positions around the chart; it does not affect the order of segments in the returned array. By default, slices are sorted by value in descending order. |
|
Numeric value or accessor function to return the overall start and end angle of the pie chart. |
|
Numeric value or accessor function to return any padding to be inserted between adjacent slices. |
The arc generator can be used by itself to draw arbitrary circle segments even if they are not used as part of a pie chart, but in this case it is up to the user to provide the necessary information in a suitable form. The arc generator will fail unless start and end angles and inner and outer radius have been fixed; there are no default values. These parameters can be set either through API calls or be provided as an object when the generator is evaluated. In the latter case, the parameters are extracted from the object by the configured accessor functions. These techniques may also be mixed (see Example 5-7).
// API configuration
var
arcMkr
=
d3
.
arc
().
innerRadius
(
50
).
outerRadius
(
150
)
.
startAngle
(
1.5
*
Math
.
PI
).
endAngle
(
1.75
*
Math
.
PI
);
g
.
append
(
"path"
).
attr
(
"d"
,
arcMkr
()
);
// Object with default accessors
var
arcSpec
=
{
innerRadius
:
50
,
outerRadius
:
150
,
startAngle
:
0
,
endAngle
:
0.25
*
Math
.
PI
};
g
.
append
(
"path"
).
attr
(
"d"
,
d3
.
arc
()(
arcSpec
)
);
// Mixed, nondefault accessors
var
arcMkr
=
d3
.
arc
().
innerRadius
(
50
).
outerRadius
(
150
)
.
startAngle
(
d
=>
Math
.
PI
*
d
.
start
/
180
)
.
endAngle
(
d
=>
Math
.
PI
*
d
.
end
/
180
);
g
.
append
(
"path"
).
attr
(
"d"
,
arcMkr
(
{
start
:
60
,
end
:
90
}
)
);
D3 includes facilities for the generation and layout of other shapes:
The d3.area()
generator is related to the d3.line()
generator,
except that it creates areas that are bounded by two boundary lines.
The d3.lineRadial()
and d3.areaRadial()
generators work with
polar coordinates to make circular line and area plots.
The d3.stack()
layout is intended to be used with the d3.area()
generator for the creation of stacked graphs (where several components
are graphed together in such a way that the cumulative value of a set of
quantities is displayed).
The d3.chord()
layout shows the mutual flows between nodes in a network
as a circular arrangement. It is intended to be used together with the
d3.arc()
and the specialized d3.ribbon()
generator.
The d3.links()
generator is used to draw the branches in tree diagrams of hierarchical data sets. It is intended to be used together with the d3.tree()
and d3.cluster()
layouts.
The d3.pack()
, d3.treemap()
, and d3.partition()
layouts prepare hierarchical data to be displayed in containment hierarchies.
The last two items of this list will be discussed in Chapter 9. Check the D3 Reference Documentation for information about the other items.
Compared to the variety of generators and layouts, D3 does not contain many built-in components. (The axis facility from Example 2-6 is one example, and the drag-and-drop behavior from Example 4-4 is another.) But components are the primary means to organize and simplify your own work.
Components are functions that take a Selection
as argument and do
something to it.6 They return nothing; all their results are side effects.
Writing a component is useful whenever you want to repeatedly create several DOM elements together, in particular when their positioning relative to each other is important. But a component may also simply be a way of saving keystrokes on a recurring set of commands, or a method to encapsulate a bit of messy code. In many ways, components are to the DOM tree what functions are to code: a reusable set of actions that are always performed together.
Let’s say that we want to use, in multiple locations in a graph, a
rectangle with rounded corners and a text label centered inside of it
(see Figure 5-8).
In this kind of situation, it is convenient to keep the relative
positioning of the parts within the component (text and border)
separate from the positioning of the entire component itself. The
sticker()
function in Example 5-8 creates both the
rectangle and the text inside of it centered at the origin. It is then up to
the calling code to move both of them together to the final position.
function
sticker
(
sel
,
label
)
{
sel
.
append
(
"rect"
)
.
attr
(
"rx"
,
5
)
.
attr
(
"ry"
,
5
)
.
attr
(
"width"
,
70
)
.
attr
(
"height"
,
30
)
.
attr
(
"x"
,
-
35
)
.
attr
(
"y"
,
-
15
)
.
attr
(
"fill"
,
"none"
)
.
attr
(
"stroke"
,
"blue"
)
.
classed
(
"frame"
,
true
)
;
sel
.
append
(
"text"
)
.
attr
(
"x"
,
0
)
.
attr
(
"y"
,
5
)
.
attr
(
"text-anchor"
,
"middle"
)
.
attr
(
"font-family"
,
"sans-serif"
)
.
attr
(
"font-size"
,
14
)
.
classed
(
"label"
,
true
)
.
text
(
label
?
label
:
d
=>
d
)
;
}
function
makeSticker
(
)
{
var
labels
=
[
"Hello"
,
"World"
,
"How"
,
"Are"
,
"You?"
]
;
var
scX
=
d3
.
scaleLinear
(
)
.
domain
(
[
0
,
labels
.
length
-
1
]
)
.
range
(
[
100
,
500
]
)
;
var
scY
=
d3
.
scaleLinear
(
)
.
domain
(
[
0
,
labels
.
length
-
1
]
)
.
range
(
[
50
,
150
]
)
;
d3
.
select
(
"#sticker"
)
.
selectAll
(
"g"
)
.
data
(
labels
)
.
enter
(
)
.
append
(
"g"
)
.
attr
(
"transform"
,
(
d
,
i
)
=>
"translate("
+
scX
(
i
)
+
","
+
scY
(
i
)
+
")"
)
.
call
(
sticker
)
;
d3
.
select
(
"#sticker"
)
.
append
(
"g"
)
.
attr
(
"transform"
,
"translate(75,150)"
)
.
call
(
sticker
,
"I'm fine!"
)
.
selectAll
(
".label"
)
.
attr
(
"fill"
,
"red"
)
;
}
A component is simply a function that takes a Selection
instance
as its first argument. The remaining parameters are arbitrary.
Create and configure the rectangle as a child of the supplied selection. The rectangle is centered at the origin.
Assign a CSS class to the rectangle: this will make it easier to identify
it later for styling. The second parameter to classed()
is
important: true
indicates that the class name should be assigned to the
element, whereas false
indicates that the class name should be removed.
Create and configure the text element and assign it a class name as well.
If a second argument has been supplied when the sticker()
function
was called, use it as the label. Otherwise, use the data bound to the
current node. This makes it possible to use this component regardless of whether data is bound to the target selection or not.
To use this component with bound data, create a <g>
element for
each data point as usual, and then invoke the sticker()
function
using call()
. This will execute the sticker()
function, while
supplying the current selection as the first argument. In this case,
the “current selection” contains the newly created <g>
elements
(with their bound data).
To use this component without bound data, again append a <g>
element
as the container for the sticker, and invoke sticker()
via call()
, but
this time you must supply a label explicitly. The call()
facility
will simply forward any arguments past the first one when invoking
the supplied function.
The CSS class name makes it easy to select part of the component
to change its appearance. Keep in mind, however, that selectAll()
returns a new selection: any subsequent calls in the chain of
function calls will only apply to the elements that matched the
".labels"
selector!
A few general comments about working with components:
A component is usually created inside of a <g>
element as the common
parent to all parts of the component: for example, so that the entire
component can be moved to its final location through an SVG transformation
applied to the containing <g>
element. This <g>
element is usually
not created by the component, but by the calling code. If this seems
surprising, keep in mind that the component does not return the elements
it creates, but adds them directly to the DOM tree. To do so, it
needs to know where to add them, and the
calling code must supply this information—hence the surrounding code
creates the <g>
element as the destination for the component to add
its results.
Although a component can be invoked using regular function call
syntax, with the destination Selection
as the first argument, it is
more idiomatic to invoke it “synthetically” using
the call()
function. The call()
function will automatically
inject the destination Selection
as the first argument and return
the same Selection
instance, thus enabling method chaining.
If you need to gain access to the new DOM elements that were created
by the component, use selectAll()
with an appropriate selector
on the Selection
returned by call()
. The typical call sequence
therefore looks like this:
d3
.
select
(
"svg"
).
append
(
"g"
).
attr
(
"transform"
,
...
)
.
call
(
component
)
.
selectAll
(
"circle"
).
attr
(
"fill"
,
...
)...
Also see the very end of Example 5-8 for an example.
Although it is possible to pass in appearance options (like color and
so on) as additional parameters to the component, it is usually more
idiomatic to apply them later on using the usual mechanisms of the
Selection
API (as was done toward the end of Example 5-8).
This has the advantage that the appearance of the component is not fixed
by its author, but can be changed—as desired—by the user.
Whereas the sticker component in the previous section can reasonably claim
to be reusable, a component can also make sense as nothing more than an
ad hoc labor-saving device to save keystrokes in a local scope. For example,
imagine that you need to add <text>
elements in several spots, all
of which should use different text alignments and font sizes. Let’s
also assume a situation where it is not possible or practical to
collect these presentational attributes in a stylesheet or on a common
parent. An ad hoc component like the following can relieve you
from having to type the attribute names and function calls for
every <text>
element explicitly:
function
texter
(
sel
,
anchor
,
size
)
{
return
sel
.
attr
(
"text-anchor"
,
anchor
)
.
attr
(
"font-size"
,
size
)
.
attr
(
"font-family"
,
"sans-serif"
);
}
And you use it simply as shorthand where appropriate:
d3
.
select
(
...
)
.
append
(
"text"
).
call
(
texter
,
"middle"
,
14
).
text
(
...
);
Similar ad hoc components might be useful to set the fill and stroke colors of an object in one fell swoop, or to create a circle with its center coordinates and radius in a single call.
SVG transformations are extremely useful—but, unfortunately, they are also a bit verbose, in particular when you need to build up the argument string dynamically. A labor (and space) saving device would be welcome! (This section is probably best skipped on first reading.)
It’s easy enough to write a component for that purpose (in this section, I’ll restrict myself to translations for simplicity; extensions to more general SVG transformations are straightforward):
function
trsf
(
sel
,
dx
,
dy
)
{
return
sel
.
attr
(
"transform"
,
"translate("
+
dx
+
","
+
dy
+
")"
);
}
And you would invoke it like any other component:
d3
.
select
(
...
).
append
(
"g"
).
call
(
trsf
,
25
,
50
).
append
(
...
)...
This is fine as far as it goes, but what if the offsets should depend on data bound to a selection? What we want to do is this (compare, for instance, Example 5-8):
d3
.
select
(
...
).
data
(
data
).
enter
().
append
(
"g"
)
.
call
(
trsf
,
(
d
,
i
)
=>
...,
(
d
,
i
)
=>
...
).
append
(
"circle"
)...
But the simple trsf()
component just introduced will not work in that
situation. In order
to handle accessor functions as arguments, the component must distinguish
whether these arguments are functions or constants, and evaluate them in
the former case. One problem that arises if we want to stay within the
published Selection
API (instead of poking under the hood) is that
the API functions only allow for one accessor function to be
evaluated simultaneously, but our trsf()
takes two. Hence we need
to evaluate the first accessor for all elements of the selection, store
the results, then evaluate the second accessor, and finally combine the
intermediate results to create the actual transform
attribute value.
The following code makes use of the d3.local()
facility, intended for just this kind of application. It is essentially a
hashmap, but expects a DOM Node
as key. In this way, it is possible
to store an intermediate value per Node
:
function
trsf
(
sel
,
dx
,
dy
)
{
var
dxs
=
d3
.
local
(),
dys
=
d3
.
local
();
sel
.
each
(
function
(
d
,
i
)
{
dxs
.
set
(
this
,
typeof
dx
===
"function"
?
dx
(
d
,
i
)
:
dx
||
0
)
});
sel
.
each
(
function
(
d
,
i
)
{
dys
.
set
(
this
,
typeof
dy
===
"function"
?
dy
(
d
,
i
)
:
dy
||
0
)
});
return
sel
.
attr
(
"transform"
,
function
()
{
return
"translate("
+
dxs
.
get
(
this
)
+
","
+
dys
.
get
(
this
)
+
")"
}
);
}
A simpler but less clean method is to store the intermediate result in the node itself by adding additional “bogus” properties to it:
sel
.
each
(
function
(
d
,
i
)
{
this
.
bogus_dx
=
typeof
dx
===
"function"
?
dx
(
d
,
i
)
:
dx
||
0
}
);
Either way, the trsf()
component can now be called as intended.
For example, the following code could be added to Example 5-8:
var
vs
=
[
"This"
,
"That"
];
d3
.
select
(
"#sticker"
).
append
(
"g"
)
.
selectAll
(
"g"
).
data
(
vs
).
enter
().
append
(
"g"
)
.
call
(
trsf
,
(
d
,
i
)
=>
300
+
scX
(
i
),
(
d
,
i
)
=>
scY
(
i
)
)
.
call
(
sticker
);
1 Internally, the D3 symbol generator uses an HTML5 <canvas>
element for performance reasons, but there is no strict need to do so.
2 Although current browsers don’t seem to have a problem with the <use>
tag, I have found that not all SVG tools handle it properly. Be aware.
3 Another way to create such a symbol is to rotate d3.symbolCross
by 45 degrees using an SVG transformation!
4 This section is probably best skipped on first reading.
5 See, for example, Data Analysis with Open Source Tools by Philipp K. Janert (O’Reilly).
6 Despite the name, components are not things, they are actions, although often the action results in a thing being added to the DOM tree.