Colors can serve different purposes in visualization: they may simply serve to make a graph more interesting or more pleasing, or they may help to reinforce and emphasize aspects of the data, or they may be primary carriers of information themselves. This chapter will explain how colors are represented in D3 and then discuss various color schemes and how they can be used to display information. The chapter ends with a description of false-color plots that strictly rely on color to represent data.
Specifying an individual color in D3 is easy: you provide a string
with either the name of a predefined color, or the color’s red,
green, blue (RGB) or hue, saturation, lightness (HSL) components using the CSS3 syntax (see Appendix B). But the string
format is not very convenient if you want to manipulate colors
programmatically. For such purposes, you can obtain a color
object
using one of the factory functions in Table 8-1. A
color
object can be used wherever a color specification is expected:
its toString()
function will be called automatically and return a
representation of the color in CSS3 format.
The returned color
objects provide only a minimal API; they mostly
serve as containers for their channel information. Every color
object exposes its three components as suitably named properties,
one for each channel. In addition, each object also has an opacity
property for the alpha channel.
The functions in Table 8-1 accept as arguments one of the
following and return a color
object in the requested color space:
A CSS3 color string
Another color
object (for conversion to another color space)
A set of three (or four, if an opacity value is specified) components
The exception is the generic d3.color()
factory, which takes a
CSS3 string or another color
object and returns an RGB or HSL
color
object, depending on the input.
Function | Componentsa | Comments |
---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Input must be a CSS3 string or another object; output is RGB, unless the input is HSL. |
a All color objects also have an |
The allowed parameter ranges are in general different for each color space.
The functions in Table 8-1 return null
if their inputs
are obviously illegal (such as negative RGB components), but even a nonnull
return value does not guarantee that the returned color is displayable.
You can use the function displayable()
on the returned color instance to
find out. If a color is not displayable, then its toString()
method will
substitute a “suitable” displayable color (such as white or black when the
required color is too bright or too dark) instead.
D3 comes with a fairly large number of color schemes for use with scale objects. It can be difficult to make sense of the plethora of built-in schemes, hence here is a survey of what is available.1
The following schemes are based on work originally intended for use in cartographic maps (the kind you find in an atlas) to depict political or other thematic information. They are best suited for coloring areas, but work less well for lines or shapes with minute detail. Because they are designed to be easy on the eyes, they are also often a good choice to enhance the perception of information that is already represented by other means.
Nine nonsemantic, categorical schemes, each with 8 to 12 entries. Adjacent
colors tend to have strong contrasts, except for the scheme d3.schemePaired
,
which consists of six pairs, each consisting of a light and a dark shade of comparable hue. (See Figures 5-7 and 9-3 for
examples.)
Nine diverging schemes, each having different dark and saturated colors of different hue at the ends, and passing through a light and pale version of grey, white, or yellow in the center. Use these to show deviations from a baseline in either direction. (See Figure 8-1.)
Six monotonic schemes, each running from a light and pale white or grey to a dark and saturated color, using a single hue. (See Figure 9-2 for an application.)
Twelve continuous schemes, each running from a light and pale white or grey color over one or two intermediate but “similar” colors (such as blue and green, or yellow, green, and blue) to a dark and saturated color.
These color schemes come in two variants:
A continuous variant (as an interpolator) for use with
d3.scaleSequential()
A discrete variant (as an array of discrete color values) for use with
d3.scaleOrdinal()
or one of the binning scales (such as
d3.scaleThreshold()
)2
Most of the discrete schemes come in several versions, each containing a different number of equally spaced, discrete elements (between 3 and about 10, check the D3 Reference Documentation regarding the exact number for each scheme).
The following schemes are intended for false-color plots or heatmaps. These schemes are only available as continuous interpolators (and not in discrete versions).
Five multihue schemes, all running from a very dark (nearly black) color to yellow or white. These schemes are careful to vary saturation and lightness simultaneously (which might be a better idea in principle than in practice).
Two rainbow schemes that are perceptually more uniform than the standard color wheel in HSL space. For one of these, its two halves are available separately, one running over the warm (red) side of the color wheel, and the other over the cool (blue, green) side.
The “sinebow” scheme d3.interpolateSinebow
is very interesting. In
the standard rainbow, the RGB components vary in a regular, piecewise
linear fashion as the hue ranges from 0 to 360. In the sinebow, this
piecewise linear behavior has been replaced by three sine functions,
which are phase-shifted by 120 degrees relative to each other. The
result is a bright but much more perceptually uniform rainbow (see
Figure 8-1). Highly recommended for its appearance and
conceptual simplicity.3
The sheer number and apparent variety of built-in color schemes may appear to exhaust all possibilities, but depending on context and purpose, other schemes may be more suitable (see “Palette Design”). It can be interesting to peruse collections of existing color schemes for ideas.5 There is no reason to use the built-in schemes if they don’t seem appropriate or are hard to adapt to a specific application.
In order to utilize color to represent information, it is often convenient to combine color with a scale object (see Chapter 7): specifically, to use colors to define the scale object’s output range. This section collects a few simple applications and techniques.
Discrete color scales can be realized using either binning scales or a discrete
scale object. Binning scales, such as those created by d3.scaleQuantize()
or d3.scaleThreshold()
, make sense when a continuous range of numbers
is to be represented through a discrete set of colors. The first divides
the input domain into equally sized bins, the latter expects the user to
explicitly define the threshold values that separate bins—see Examples 4-7 and 7-3.
Remember that the domain()
function is semantically overloaded for binning
scales (see Chapter 7 for details):
var
sc1
=
d3
.
scaleQuantize
().
domain
(
[
0
,
1
]
)
.
range
(
[
"black"
,
"red"
,
"yellow"
,
"white"
]
);
var
sc2
=
d3
.
scaleThreshold
().
domain
(
[
-
1
,
1
]
)
.
range
(
[
"blue"
,
"white"
,
"red"
]
);
Discrete scales, as created by d3.scaleOrdinal()
, are appropriate when a
discrete set of input values should be displayed using different colors.
A discrete scale object can establish an explicit relationship between
specific values from the input domain and the colors to represent them:
var
sc
=
d3
.
scaleOrdinal
(
[
"green"
,
"yellow"
,
"red"
]
)
.
domain
(
[
"good"
,
"medium"
,
"bad"
]
);
Alternatively, when no domain has been specified, a discrete scale object assigns the next unused color to each new symbol as it arises. Examples can be found in Examples 5-6 and 9-5.
Their implementation through scale objects aside, colors for discrete color schemes should be chosen according to their purpose. Sometimes the goal is merely to make it easier to distinguish different graphical elements without implying either semantics or ordering: the built-in categorical schemes are of this kind (see Figures 5-7 and 9-3 for examples). At other times, the colors should convey a semantic meaning, although not necessarily monotonic increments or a strict ordering: a red/yellow/green “traffic light” scheme as in Figure 7-5 is an example. And finally, sometimes distinct color schemes are intended to represent a monotonic sequence of steps (as in the right side of Figure 9-2). In the latter case, binning scales may be used together with the built-in monotonic or diverging scales to map a continuous range of values to a specific set of fixed colors with a clear sense of ordering.
The ability of D3 to interpolate colors makes it particularly easy to generate color gradients. The examples in this section are mostly intended to demonstrate the syntax for a few simple, yet typical cases (see Example 8-1 and Figure 8-1).
sc1
=
d3
.
scaleLinear
(
)
.
domain
(
[
0
,
3
,
10
]
)
.
range
(
[
"blue"
,
"white"
,
"red"
]
)
;
sc2
=
d3
.
scaleLinear
(
)
.
domain
(
[
0
,
5
,
5
,
10
]
)
.
range
(
[
"white"
,
"blue"
,
"red"
,
"white"
]
)
;
sc3
=
d3
.
scaleSequential
(
t
=>
""
+
d3
.
hsl
(
360
*
t
,
1
,
0.5
)
)
.
domain
(
[
0
,
10
]
)
;
sc4
=
d3
.
scaleSequential
(
t
=>
d3
.
interpolateSinebow
(
2
/
3
-
3
*
t
/
4
)
)
.
domain
(
[
0
,
10
]
)
;
sc5
=
d3
.
scaleDiverging
(
t
=>
d3
.
interpolateRdYlGn
(
1
-
t
)
)
.
domain
(
[
0
,
2
,
10
]
)
;
sc6
=
d3
.
scaleSequential
(
d3
.
interpolateRgbBasis
(
[
"#b2d899"
,
"#ffffbf"
,
"#bf9966"
,
"#ffffff"
]
)
)
.
domain
(
[
0
,
10
]
)
;
A simple blue/white/red gradient using default color space interpolation—but notice the asymmetrical position of the white band!
A gradient with a sharp transition in the middle.
The much-maligned “standard HSL rainbow”—mostly to show the syntax (not because it is a particularly good color scale).
A much better rainbow, using the built-in “sinebow” interpolator. Here, it is traversed backward (so that it runs from blue to red, conveying customary low-to-high semantics), and its range is restricted to prevent it from wrapping around and reusing colors.
An example of a diverging scale. It employs the built-in red/yellow/green interpolator, but changes its direction so that large values are represented as red. The domain of a diverging scale must specify three numeric values; the middle value is mapped to the interpolator position t = 0.5. Here, the yellow color, representing the center of the returned range of colors, is displaced to the value 2 in the input domain.
An example for the kind of color scheme conventionally used in
topographic maps. In contrast to other schemes, the lightness
does not increase monotonously, but exhibits additional, systematic
variation (the green and brown are relatively dark, the yellow and
white relatively light), which may provide a visual aide. This scheme
uses the d3.interpolateRgbBasis
interpolator, which constructs a
spline through all supplied colors (which are assumed to be equally
spaced). The advantage of the spline interpolator is not so much a
smoother color scheme, but that the user does not have to specify
the location of the intermediate colors using domain()
.
Any graph that relies on color to convey meaning (as opposed to using color merely to enhance appearance and perception) needs a key that clearly displays which values are mapped to which colors. Sometimes a textual description is sufficient, but a visual color box is usually much clearer. D3 does not have a built-in component for this purpose, but it is easy enough to create one. The one shown in Example 8-2 was used to generate the instances in Figure 8-1; it can serve as a model for general applications.
function
colorbox
(
sel
,
size
,
colors
,
ticks
)
{
var
[
x0
,
x1
]
=
d3
.
extent
(
colors
.
domain
(
)
)
;
var
bars
=
d3
.
range
(
x0
,
x1
,
(
x1
-
x0
)
/
size
[
0
]
)
;
var
sc
=
d3
.
scaleLinear
(
)
.
domain
(
[
x0
,
x1
]
)
.
range
(
[
0
,
size
[
0
]
]
)
;
sel
.
selectAll
(
"line"
)
.
data
(
bars
)
.
enter
(
)
.
append
(
"line"
)
.
attr
(
"x1"
,
sc
)
.
attr
(
"x2"
,
sc
)
.
attr
(
"y1"
,
0
)
.
attr
(
"y2"
,
size
[
1
]
)
.
attr
(
"stroke"
,
colors
)
;
sel
.
append
(
"rect"
)
.
attr
(
"width"
,
size
[
0
]
)
.
attr
(
"height"
,
size
[
1
]
)
.
attr
(
"fill"
,
"none"
)
.
attr
(
"stroke"
,
"black"
)
if
(
ticks
)
{
sel
.
append
(
"g"
)
.
call
(
d3
.
axisBottom
(
ticks
)
)
.
attr
(
"transform"
,
"translate( 0,"
+
size
[
1
]
+
")"
)
;
}
}
In addition to the target selection, the component takes the following arguments: the size (in pixels) of the color box as a two-element array, the actual color scale, and—optionally—a regular scale for creating tick marks.
This creates an array that contains, for each pixel in the desired width of the color bar, the corresponding value from the input domain.
This scale maps the values of the original domain to pixel coordinates in the colorbox.
Create a one-pixel wide, colored line for each entry in bars
.
Put a box around the colors…
… and add a set of tick marks if an appropriate scale object was passed in.
Whenever data can naturally be projected into a two-dimensional plane, then false-color plots or “heatmaps” are an attractive option, possibly together with (or as an alternative to) contour lines.
A form of visualization that relies particularly heavily on color coding is the false-color plot or heatmap. For data points on a two-dimensional grid, the observed value is represented as color: this is the way elevation is commonly shown in topographic maps.
The number of individual graph elements (data points or pixels) in a
false color plot can quickly become large, even for grids of quite
modest size. For performance reasons, it may therefore be advisable
(or even necessary) to use the HTML5 <canvas>
element when creating
such plots in D3 (see “The HTML5 <canvas> Element”).
Example 8-3 demonstrates a simple use of the <canvas>
element; Figure 8-2 shows the resulting figure.
function
makeMandelbrot
(
)
{
var
cnv
=
d3
.
select
(
"#canvas"
)
;
var
ctx
=
cnv
.
node
(
)
.
getContext
(
"2d"
)
;
var
pxX
=
465
,
pxY
=
250
,
maxIter
=
2000
;
var
x0
=
-
1.31
,
x1
=
-
0.845
,
y0
=
0.2
,
y1
=
0.45
;
var
scX
=
d3
.
scaleLinear
(
)
.
domain
(
[
0
,
pxX
]
)
.
range
(
[
x0
,
x1
]
)
;
var
scY
=
d3
.
scaleLinear
(
)
.
domain
(
[
0
,
pxY
]
)
.
range
(
[
y1
,
y0
]
)
;
var
scC
=
d3
.
scaleLinear
(
)
.
domain
(
[
0
,
10
,
23
,
35
,
55
,
1999
,
2000
]
)
.
range
(
[
"white"
,
"red"
,
"orange"
,
"yellow"
,
"lightyellow"
,
"white"
,
"darkgrey"
]
)
;
function
mandelbrot
(
x
,
y
)
{
var
u
=
0.0
,
v
=
0.0
,
k
=
0
;
for
(
k
=
0
;
k
<
maxIter
&&
(
u
*
u
+
v
*
v
)
<
4
;
k
++
)
{
var
t
=
u
*
u
-
v
*
v
+
x
;
v
=
2
*
u
*
v
+
y
;
u
=
t
;
}
return
k
;
}
for
(
var
j
=
0
;
j
<
pxY
;
j
++
)
{
for
(
var
i
=
0
;
i
<
pxX
;
i
++
)
{
var
d
=
mandelbrot
(
scX
(
i
)
,
scY
(
j
)
)
;
ctx
.
fillStyle
=
scC
(
d
)
;
ctx
.
fillRect
(
i
,
j
,
1
,
1
)
;
}
}
}
Select the <canvas>
element from the page and use it to obtain a
drawing context for simple, two-dimensional graphics. Notice that
getContext()
is not part of D3, but is part of the DOM API, hence you
must first obtain the underlying DOM Node
from the D3 Selection
using the node()
method.
Fix some configurational parameters: the size of the canvas in pixels, the maximum number of steps for the Mandelbrot iteration, and the region of interest in the complex plane.
Create two scales that map pixel coordinates to locations in the complex plane.
Create a scale to map the number of iteration steps to a color. The appearance of the graph depends strongly on the placement of the intermediate values.
This function implements the actual Mandelbrot iteration: for a given point in the complex plane, perform the iteration until either the squared distance from the origin exceeds 22 or until the maximum number of iterations is exceeded; return the number of steps taken. (See the Wikipedia page on Mandelbrot sets.)
The double loop runs over all pixels of the canvas. The pixel coordinate
is transformed to a location in the complex plane, which is passed to
the mandelbrot()
function. Its return value is converted to a color,
which is used to paint a filled, 1×1 rectangle (that is, a pixel) on the canvas.
An alternative (or possible addition) to false-color plots, suitable mostly for smoothly varying data, uses contour lines: curves representing levels of equal elevation (as in a topographic map). D3 provides a layout to compute the location of such lines (see Table 8-2).
Function | Description |
---|---|
|
Returns a new contours layout instance with default settings. |
|
Calculates contours for the supplied data set. The data must be formatted
as a one-dimensional array, such that the element at the position |
|
Sets the number of columns and rows as a two-element array. |
|
The argument should be an array of values for which contour lines
should be calculated. If the argument is a single integer |
|
Generates a single contour for the supplied data set at the specified threshold value. Returns a single GeoJSON object. |
The required representation of the data is peculiar. Data points are
expected to lie on a regular, rectangular cols
×rows
grid,
stored as a single, one-dimensional array of numbers, with the
rows forming a contiguous sequence. The item at array index i + j*cols
therefore represents the point at location [i, j]
. There are no provisions
to associate actual (“domain”) coordinates with each data point. One
consequence is that the resulting graph will be cols
×rows
pixels
in size; if you want a different size, you must apply a scaling
transformation.
When the contours layout mechanism is invoked on the data, it returns an
array of GeoJSON objects, one for each contour.6 You can then use the d3.geoPath()
generator to generate a
command string suitable for the d
attribute of a regular <path>
element. (This process is equivalent to the one discussed in
Chapter 5.) Each of the contour objects also exposes a value
property containing the threshold value represented by the contour.
If the contour lines are filled with color, the resulting graph is again a false-color plot or heatmap. In Example 8-4, both representations are used together: first, a large number of filled contours is drawn to create a smooth colored background. Then, a few contour lines are drawn on top of this background at specific threshold values (see Figure 8-3).
function
makeContours
(
)
{
d3
.
json
(
"haxby.json"
)
.
then
(
drawContours
)
;
}
function
drawContours
(
scheme
)
{
// Set up scales, including color
var
pxX
=
525
,
pxY
=
300
;
var
scX
=
d3
.
scaleLinear
(
)
.
domain
(
[
-
3
,
1
]
)
.
range
(
[
0
,
pxX
]
)
;
var
scY
=
d3
.
scaleLinear
(
)
.
domain
(
[
-
1.5
,
1.5
]
)
.
range
(
[
pxY
,
0
]
)
;
var
scC
=
d3
.
scaleSequential
(
d3
.
interpolateRgbBasis
(
scheme
[
"colors"
]
)
)
.
domain
(
[
-
1
,
1
]
)
var
scZ
=
d3
.
scaleLinear
(
)
.
domain
(
[
-
1
,
-
0.25
,
0.25
,
1
]
)
.
range
(
[
"white"
,
"grey"
,
"grey"
,
"black"
]
)
;
// Generate data
var
data
=
[
]
;
var
f
=
(
x
,
y
,
b
)
=>
(
y
*
*
4
+
x
*
y
*
*
2
+
b
*
y
)
*
Math
.
exp
(
-
(
y
*
*
2
)
)
for
(
var
j
=
0
;
j
<
pxY
;
j
++
)
{
for
(
var
i
=
0
;
i
<
pxX
;
i
++
)
{
data
.
push
(
f
(
scX
.
invert
(
i
)
,
scY
.
invert
(
j
)
,
0.3
)
)
;
}
}
var
svg
=
d3
.
select
(
"#contours"
)
,
g
=
svg
.
append
(
"g"
)
;
var
pathMkr
=
d3
.
geoPath
(
)
;
// Generate and draw filled contours (shading)
var
conMkr
=
d3
.
contours
(
)
.
size
(
[
pxX
,
pxY
]
)
.
thresholds
(
100
)
;
g
.
append
(
"g"
)
.
selectAll
(
"path"
)
.
data
(
conMkr
(
data
)
)
.
enter
(
)
.
append
(
"path"
)
.
attr
(
"d"
,
pathMkr
)
.
attr
(
"fill"
,
d
=>
scC
(
d
.
value
)
)
.
attr
(
"stroke"
,
"none"
)
// Generate and draw contour lines
conMkr
=
d3
.
contours
(
)
.
size
(
[
pxX
,
pxY
]
)
.
thresholds
(
10
)
;
g
.
append
(
"g"
)
.
selectAll
(
"path"
)
.
data
(
conMkr
(
data
)
)
.
enter
(
)
.
append
(
"path"
)
.
attr
(
"d"
,
pathMkr
)
.
attr
(
"fill"
,
"none"
)
.
attr
(
"stroke"
,
d
=>
scZ
(
d
.
value
)
)
;
// Generate a single contour
g
.
select
(
"g"
)
.
append
(
"path"
)
.
attr
(
"d"
,
pathMkr
(
conMkr
.
contour
(
data
,
0.025
)
)
)
.
attr
(
"fill"
,
"none"
)
.
attr
(
"stroke"
,
"red"
)
.
attr
(
"stroke-width"
,
2
)
;
// Generate axis
svg
.
append
(
"g"
)
.
call
(
d3
.
axisTop
(
scX
)
.
ticks
(
10
)
)
.
attr
(
"transform"
,
"translate(0,"
+
pxY
+
")"
)
;
svg
.
append
(
"g"
)
.
call
(
d3
.
axisRight
(
scY
)
.
ticks
(
5
)
)
;
// Generate colorbox
svg
.
append
(
"g"
)
.
call
(
colorbox
,
[
280
,
30
]
,
scC
)
.
attr
(
"transform"
,
"translate( 540,290 ) rotate(-90)"
)
.
selectAll
(
"text"
)
.
attr
(
"transform"
,
"rotate(90)"
)
;
svg
.
append
(
"g"
)
.
attr
(
"transform"
,
"translate( 570,10 )"
)
.
call
(
d3
.
axisRight
(
d3
.
scaleLinear
(
)
.
domain
(
[
-
1
,
1
]
)
.
range
(
[
280
,
0
]
)
)
)
;
}
Load the colors for the color scheme from a file, passing the result to
the drawContours()
function, which does the actual work.7
Set the size of the resulting figure in pixels. Remember that there needs to be one data point per pixel.
The colors loaded from the file are used together with the
d3.interpolateRgbSpline
interpolator to create a color scale
object (compare the last item in Example 8-1).
A separate scale object (scZ
) will determine the color of the
contour lines to make sure they are clearly visible against the
changing color background.
Generate the data by evaluating the function f(x,y)
for each pixel
coordinate.
The invert()
function of the scale object is used to obtain the
domain coordinates for each pixel location.
Obtain a handle on the DOM tree, and append a <g>
element as overall
container for the main plot.
Create a d3.geoPath
generator instance. This is a function object; when
invoked on a GeoJSON object, it will return a string suitable for
the d
attribute of a <path>
element.
Create a contours layout instance, and set the number of pixels in the graph and the data. The chosen number of contours is large: when each is filled with color, they will yield a smooth heatmap.
For each contour, append a <path>
element, and fill it with color
according to the value
property on the contour object. The
pathMkr
will be invoked on each contour and return a string suitable
for the d
attribute of the <path>
element.
Reconfigure the contours layout to create approximately 10 contours, then invoke it to produce unfilled contour lines.
Just to show how it is done: the contour()
function can be used to
generate a single contour at a specific threshold value.
Use the original scale object to add coordinate axes to the graph…
… and add a color box to show the numeric values corresponding to
the colors in the heatmap. This code reuses the colorbox
component
from Example 8-2, then uses an SVG transformation to
create vertical orientation.
1 See https://github.com/d3/d3-scale-chromatic for visual representations.
2 Having a separate discrete color scheme, as opposed to just evaluating the corresponding interpolator at a discrete set of values, allows mapping nonnumerical categorical values to colors as well.
3 Also see http://basecase.org/env/on-rainbows.
4 I have a bit more to say about this topic in Appendix D of my book Gnuplot in Action (Manning Publications, Second Edition).
5 A very large and diverse repository of palettes, not all of which are intended for scientific visualization, can be found at http://bit.ly/2Wamh09. Of particular interest in the present context is the work done by Kenneth Moreland and by the Generic Mapping Tools project, especially the GMT Haxby palette.
6 GeoJSON is a standard for representing geographical features; it is defined in RFC 7946.
7 This color scheme is based on the GMT Haxby palette from the Generic Mapping Tools project.