Skip to content

Instantly share code, notes, and snippets.

@mbostock
Last active February 17, 2017 05:42
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 8 You must be signed in to fork a gist
  • Save mbostock/3289530 to your computer and use it in GitHub Desktop.
Save mbostock/3289530 to your computer and use it in GitHub Desktop.
Heightmap
license: gpl-3.0

This example demonstrates how to remap a monochrome image using a custom color scale. You might use this technique as an alternative to a CSV- or JSON-based heatmap; binary images can store lots of data efficiently.

The source image is a global heightmap from the Shuttle Radar Topography Mission, released as part of NASA’s Blue Marble collection at 8km resolution. The topography data is stored in a simple black-and-white, 8-bit PNG. Darker values represent lower elevations, and lighter values represent higher elevations. Colors are read out of the image using the Canvas API.

Since there are eight bits per channel, elevation values range from 0 to 255. Since some land areas are below sea level, the source image uses a value of 14 to indicate sea level. The 5th, 50th and 95th percentiles for land elevation are 15, 35 and 132 respectively; quantiles are an effective way to maximize contrast while remapping colors, similar to auto-tone features popular in image editors.

The percentiles are used as the domain of a diverging linear scale; red values are below the median elevation, and blue values are above. Interpolating in HCL colorspace improves perception.

var color = d3.scale.linear()
   .domain([15, 35, 132])
   .range(["#d7191c", "#ffffbf", "#2c7bb6"])
   .interpolate(d3.interpolateHcl);

Note that you could instead use a traditional rainbow color scale by interpolating hue from 0º to 360º. However, rainbow color scales are harmful, so please do your viewers a favor and use a perceptually-optimized scale instead.

<!DOCTYPE html>
<meta charset="utf-8">
<body>
<script src="//d3js.org/d3.v3.min.js"></script>
<script>
var width = 960,
height = 500;
var color = d3.scale.linear()
.domain([15, 35, 132])
.range(["#d7191c", "#ffffbf", "#2c7bb6"])
.interpolate(d3.interpolateHcl);
var canvas = d3.select("body").append("canvas")
.attr("width", width)
.attr("height", height);
var context = canvas.node().getContext("2d");
getImage("readme.png", function(image) {
context.drawImage(image, 0, 0, width, height);
image = context.getImageData(0, 0, width, height);
// Rescale the colors.
for (var c, i = 0, n = width * height * 4, d = image.data; i < n; i += 4) {
c = d3.rgb(color(d[i]));
d[i + 0] = c.r;
d[i + 1] = c.g;
d[i + 2] = c.b;
}
context.putImageData(image, 0, 0);
});
function getImage(path, callback) {
var image = new Image;
image.onload = function() { callback(image); };
image.src = path;
}
</script>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment