Chapter 8. Colors, Color Scales, and Heatmaps

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.

Colors and Color Space Conversions

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.

Table 8-1. Factory functions to create color objects in different color spaces, the color components of the returned object, and their legal ranges
Function Componentsa Comments

d3.rgb()

r, g, b

  • 0 ≤ r, g, b ≤ 255 strictly

d3.hsl()

h, s, l

  • h: any value (positive or negative), the hue will be interpreted modulo 360

  • 0 ≤ s, l ≤ 1 strictly

d3.lab()

l, a, b

  • 0 ≤ l ≤ 100 strictly

  • -100 ≤ a, b ≤ 100 approximately

d3.hcl()

h, c, l

  • h: any value (positive or negative), the hue will be interpreted modulo 360

  • -125 ≤ c ≤ 125 approximately

  • 0 ≤ l ≤ 100 approximately

d3.color()

r, g, b or h, s, l

Input must be a CSS3 string or another object; output is RGB, unless the input is HSL.

a All color objects also have an opacity component, with values from 0 (transparent) to 1 (opaque).

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.

Color Schemes

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

Cartographic Schemes

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.

Categorical schemes

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.)

Diverging schemes

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.)

Single-hue monotonic schemes

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.)

Two- or three-hue monotonic schemes

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).

False-Color Schemes

The following schemes are intended for false-color plots or heatmaps. These schemes are only available as continuous interpolators (and not in discrete versions).

Multihue schemes

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).

Rainbow schemes

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

Other Color Schemes

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.

Color Scales

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 Colors

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.

Color Gradients

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).

Example 8-1. Creating color scales (see Figure 8-1)
sc1 = d3.scaleLinear().domain( [0, 3, 10] )                       1
    .range( ["blue", "white", "red"] );

sc2 = d3.scaleLinear().domain( [0, 5, 5, 10] )                    2
    .range( ["white", "blue", "red", "white"] );

sc3 = d3.scaleSequential( t => "" + d3.hsl( 360*t, 1, 0.5 ) )     3
        .domain( [0, 10] );

sc4 = d3.scaleSequential( t => d3.interpolateSinebow(2/3-3*t/4) ) 4
        .domain( [0, 10] );

sc5 = d3.scaleDiverging( t => d3.interpolateRdYlGn(1-t) )         5
        .domain( [0, 2, 10] );

sc6 = d3.scaleSequential( d3.interpolateRgbBasis(                 6
    ["#b2d899","#ffffbf","#bf9966","#ffffff"] ) ).domain( [0,10] );
1

A simple blue/white/red gradient using default color space interpolation—but notice the asymmetrical position of the white band!

2

A gradient with a sharp transition in the middle.

3

The much-maligned “standard HSL rainbow”—mostly to show the syntax (not because it is a particularly good color scale).

4

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.

5

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.

6

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().

dfti 0801
Figure 8-1. Color scales (see Example 8-1)

Making a Color Box

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.

Example 8-2. Commands to create a color box component (compare Figure 8-1)
function colorbox( sel, size, colors, ticks ) {                   1
    var [x0, x1] = d3.extent( colors.domain() );                  2
    var bars = d3.range( x0, x1, (x1-x0)/size[0] );

    var sc = d3.scaleLinear()                                     3
        .domain( [x0, x1] ).range( [0, size[0] ] );
    sel.selectAll( "line" ).data( bars ).enter().append( "line" ) 4
        .attr( "x1", sc ).attr( "x2", sc )
        .attr( "y1", 0 ).attr( "y2", size[1] )
        .attr( "stroke", colors );

    sel.append( "rect" )                                          5
        .attr( "width", size[0] ).attr( "height", size[1] )
        .attr( "fill", "none" ).attr( "stroke", "black" )

    if( ticks ) {                                                 6
        sel.append( "g" ).call( d3.axisBottom( ticks ) )
            .attr( "transform", "translate( 0," + size[1] + ")" );
    }
}
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.

2

This creates an array that contains, for each pixel in the desired width of the color bar, the corresponding value from the input domain.

3

This scale maps the values of the original domain to pixel coordinates in the colorbox.

4

Create a one-pixel wide, colored line for each entry in bars.

5

Put a box around the colors…

6

… and add a set of tick marks if an appropriate scale object was passed in.

False-Color Graphs and Related Techniques

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.

Heatmaps

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.

Example 8-3. A false-color graph of parts of the Mandelbrot set. This graph was created using an HTML5 canvas element (see Figure 8-2).
function makeMandelbrot() {
    var cnv = d3.select( "#canvas" );                             1
    var ctx = cnv.node().getContext( "2d" );

    var pxX = 465, pxY = 250, maxIter = 2000;                     2
    var x0 = -1.31, x1 = -0.845, y0 = 0.2, y1 = 0.45;

    var scX = d3.scaleLinear().domain([0, pxX]).range([x0, x1]);  3
    var scY = d3.scaleLinear().domain([0, pxY]).range([y1, y0]);

    var scC = d3.scaleLinear().domain([0,10,23,35,55,1999,2000])  4
        .range( ["white","red","orange","yellow","lightyellow",
		 "white","darkgrey"] );

    function mandelbrot( x, y ) {                                 5
        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++ ) {                                  6
        for( var i=0; i<pxX; i++ ) {
            var d = mandelbrot( scX(i), scY(j) );
            ctx.fillStyle = scC( d );
            ctx.fillRect( i, j, 1, 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.

2

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.

3

Create two scales that map pixel coordinates to locations in the complex plane.

4

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.

5

This function implements the actual Mandelbrot iteration: for a given point x+iy 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.)

6

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.

dfti 0802
Figure 8-2. Using the HTML5 <canvas> element (see Example 8-3)

Contour Lines

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).

Table 8-2. Functions for calculating contour lines (conMkr is a contours layout instance)
Function Description

d3.contours()

Returns a new contours layout instance with default settings.

conMkr( [data] )

Calculates contours for the supplied data set. The data must be formatted as a one-dimensional array, such that the element at the position [i, j] in the grid occupies the array element with index [i, j*cols]. Returns an array of GeoJSON objects, sorted by the threshold value they represent.

conMkr.size( [cols, rows] )

Sets the number of columns and rows as a two-element array.

conMkr.thresholds( args )

The argument should be an array of values for which contour lines should be calculated. If the argument is a single integer n, then approximately n contour lines will be calculated with suitable separation.

conMkr.contour( [data], threshold )

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).

dfti 0803
Figure 8-3. A false-color plot of a smooth function. This graph was created using the D3 contours layout mechanism (see Example 8-4).
Example 8-4. Creating a false-color plot using the contours layout mechanism (see Figure 8-3)
function makeContours() {
    d3.json( "haxby.json" ).then( drawContours );                 1
}

function drawContours( scheme ) {
    // Set up scales, including color
    var pxX = 525, pxY = 300;                                     2
    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]) 3
    var scZ = d3.scaleLinear().domain( [-1, -0.25, 0.25, 1] )
        .range( [ "white", "grey", "grey", "black" ] );

    // Generate data
    var data = [];                                                4
    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" );    5
    var pathMkr = d3.geoPath();                                   6

    // Generate and draw filled contours (shading)
    var conMkr = d3.contours().size([pxX, pxY]).thresholds(100);  7
    g.append("g").selectAll( "path" ).data( conMkr(data) ).enter()
        .append( "path" ).attr( "d", pathMkr )                    8
        .attr( "fill", d=>scC(d.value) ).attr( "stroke", "none" )

    // Generate and draw contour lines
    conMkr = d3.contours().size( [pxX,pxY] ).thresholds( 10 );    9
    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" )                              10
        .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) )           11
        .attr( "transform", "translate(0," + pxY + ")" );
    svg.append( "g" ).call( d3.axisRight(scY).ticks(5) );

    // Generate colorbox
    svg.append( "g" ).call( colorbox, [280,30], scC )             12
        .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] ) ) );
}
1

Load the colors for the color scheme from a file, passing the result to the drawContours() function, which does the actual work.7

2

Set the size of the resulting figure in pixels. Remember that there needs to be one data point per pixel.

3

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.

4

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.

5

Obtain a handle on the DOM tree, and append a <g> element as overall container for the main plot.

6

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.

7

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.

8

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.

9

Reconfigure the contours layout to create approximately 10 contours, then invoke it to produce unfilled contour lines.

10

Just to show how it is done: the contour() function can be used to generate a single contour at a specific threshold value.

11

Use the original scale object to add coordinate axes to the graph…

12

… 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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset