# Raster mark ^0.6.2

TIP

To produce contours instead of a heatmap, see the contour mark.

The **raster mark** renders a raster image — that is, an image formed by discrete pixels in a grid, not a vector graphic like other marks. And whereas the image mark shows an *existing* image, the raster mark *creates* one from abstract data, either by interpolating spatial samples (arbitrary points in **x** and **y**) or by sampling a function *f*(*x*,*y*) along the grid.

For example, the heatmap below shows the topography of the Maungawhau volcano, produced from a 87×61 grid of elevation samples.

Fork```
Plot.plot({
color: {label: "Elevation (m)", legend: true},
marks: [
Plot.raster(volcano.values, {width: volcano.width, height: volcano.height})
]
})
```

The grid (`volcano.values`

above) is a list of numbers `[103, 104, 104, …]`

. The first number `103`

is the elevation of the bottom-left corner. This grid is in row-major order, meaning that the elevations of the first row are followed by the second row, then the third, and so on. Here’s a smaller grid to demonstrate the concept.

```
grid = ({
"width": 10,
"height": 10,
"values": [
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 1, 2, 3, 4, 5, 6, 7, 8, 9,
0, 2, 4, 6, 8, 10, 12, 14, 16, 18,
0, 3, 6, 9, 12, 15, 18, 21, 24, 27,
0, 4, 8, 12, 16, 20, 24, 28, 32, 36,
0, 5, 10, 15, 20, 25, 30, 35, 40, 45,
0, 6, 12, 18, 24, 30, 36, 42, 48, 54,
0, 7, 14, 21, 28, 35, 42, 49, 56, 63,
0, 8, 16, 24, 32, 40, 48, 56, 64, 72,
0, 9, 18, 27, 36, 45, 54, 63, 72, 81
]
})
```

We can visualize this small grid directly with a text mark using the same color encoding. Notice that the image below is flipped vertically relative to the data: the first row of the data is the *bottom* of the image because below *y* points up↑.

```
Plot.plot({
grid: true,
x: {domain: [0, grid.width], label: "column"},
y: {domain: [0, grid.height], label: "row"},
marks: [
Plot.text(grid.values, {
text: Plot.identity,
fill: Plot.identity,
x: (d, i) => i % grid.width + 0.5,
y: (d, i) => Math.floor(i / grid.width) + 0.5
})
]
})
```

Also notice that the grid points are offset by 0.5: they represent the *middle* of each pixel rather than the corner. Below, the raster mark is laid under the text mark to show the raster image.

```
Plot.plot({
marks: [
Plot.raster(grid.values, {
width: grid.width,
height: grid.height,
imageRendering: "pixelated" // to better show the grid
}),
Plot.text(grid.values, {
text: Plot.identity,
fill: "white",
x: (d, i) => i % grid.width + 0.5,
y: (d, i) => Math.floor(i / grid.width) + 0.5
})
]
})
```

CAUTION

Safari does not currently support the **imageRendering** option.

While the raster mark provides convenient shorthand for strictly gridded data, as above, it *also* works with samples in arbitrary positions and arbitrary order. For example, in 1955 the Great Britain aeromagnetic survey measured the Earth’s magnetic field by plane. Each sample recorded the longitude and latitude alongside the strength of the IGRF in nanoteslas.

```
LONGITUDE,LATITUDE,MAG_IGRF90
-2.36216,51.70945,7
-2.36195,51.71727,6
-2.36089,51.72404,9
-2.35893,51.73758,12
-2.35715,51.7532,18
-2.35737,51.76636,24
```

Using a dot mark, we can make a quick scatterplot to see the irregular grid. We’ll use a *diverging* color scale to distinguish positive and negative values.

`Plot.dot(ca55, {x: "LONGITUDE", y: "LATITUDE", fill: "MAG_IGRF90"}).plot({color: {type: "diverging"}})`

And using a line mark, we can connect the line segments to reveal the flight paths.

Fork`Plot.line(ca55, {x: "LONGITUDE", y: "LATITUDE", stroke: "MAG_IGRF90", z: "LINE_NUMB-SEG"}).plot({color: {type: "diverging"}})`

The image above starts to be readable, but it would be frustrating to not do more with this data given all the effort that went into collecting it! Fortunately the raster mark’s **interpolate** option can quickly produce a continuous image.

The *nearest* interpolator assigns the value of each pixel in the grid using the nearest sample in the data. In effect, this produces a Voronoi diagram.

`Plot.raster(ca55, {x: "LONGITUDE", y: "LATITUDE", fill: "MAG_IGRF90", interpolate: "nearest"}).plot({color: {type: "diverging"}})`

TIP

You can also make this Voronoi diagram with the voronoi mark.

If the observed phenomenon is continuous, we can use the *barycentric* interpolator. This constructs a Delaunay triangulation of the samples, and then paints each triangle by interpolating the values of the triangle’s vertices in barycentric coordinates. (Points outside the convex hull are extrapolated.)

`Plot.raster(ca55, {x: "LONGITUDE", y: "LATITUDE", fill: "MAG_IGRF90", interpolate: "barycentric"}).plot({color: {type: "diverging"}})`

Finally, the *random-walk* interpolator assigns the value at each grid location simply by taking a random walk that stops after reaching a minimum distance from any sample! The interpolator uses the walk on spheres algorithm, limited to 2 consecutive jumps.

`Plot.raster(ca55, {x: "LONGITUDE", y: "LATITUDE", fill: "MAG_IGRF90", interpolate: "random-walk"}).plot({color: {type: "diverging"}})`

With the *random-walk* method, the image is grainy, reflecting the uncertainty of the random walk. Use the **blur** option to make it smoother.

`Plot.raster(ca55, {x: "LONGITUDE", y: "LATITUDE", fill: "MAG_IGRF90", interpolate: "random-walk", blur: 5}).plot({color: {type: "diverging"}})`

TIP

If none of the built-in spatial interpolators suffice, you can write your own as a custom function!

The raster mark can interpolate categorical values, too! Below, this creates an interesting “map” of penguin species in the space of culmen length *vs.* depth.

```
Plot.plot({
color: {legend: true},
marks: [
Plot.raster(penguins, {x: "culmen_length_mm", y: "culmen_depth_mm", fill: "species", interpolate: "random-walk"}),
Plot.dot(penguins, {x: "culmen_length_mm", y: "culmen_depth_mm"})
]
})
```

As an alternative to interpolating discrete samples, you can supply values as a continuous function *f*(*x*,*y*); the raster mark will invoke this function for the midpoint of each pixel in the raster grid, similar to a WebGL fragment shader. For example, below we visualize the Mandelbrot set by counting the number of iterations needed until the point “escapes”.

`Plot.raster({fill: mandelbrot, x1: -2, x2: 1, y1: -1.164, y2: 1.164}).plot({aspectRatio: 1})`

```
function mandelbrot(x, y) {
for (let n = 0, zr = 0, zi = 0; n < 80; ++n) {
[zr, zi] = [zr * zr - zi * zi + x, 2 * zr * zi + y];
if (zr * zr + zi * zi > 4) return n;
}
}
```

Or to visualize the arctangent function:

Fork`Plot.raster({x1: -1, x2: 1, y1: -1, y2: 1, fill: (x, y) => Math.atan2(y, x)}).plot()`

TIP

When faceting, the sample function *f*(*x*,*y*) is passed a third argument of the facet values {*fx*, *fy*}.

The raster mark supports Plot’s projection system. The chart below shows global atmospheric water vapor measurements from NASA Earth Observations.

Fork```
Plot.plot({
projection: "equal-earth",
color: {
scheme: "BuPu",
domain: [0, 6],
legend: true,
label: "Water vapor (cm)"
},
marks: [
Plot.raster(vapor, {
fill: Plot.identity,
width: 360,
height: 180,
x1: -180,
y1: 90,
x2: 180,
y2: -90,
interpolate: "barycentric",
clip: "sphere"
}),
Plot.sphere({stroke: "black"})
]
})
```

## Raster options

If *data* is provided, it represents discrete samples in abstract coordinates **x** and **y**; the **fill** and **fillOpacity** channels specify further abstract values (*e.g.*, height in a topographic map) to be spatially interpolated to produce an image.

`Plot.raster(volcano.values, {width: volcano.width, height: volcano.height})`

The **fill** and **fillOpacity** channels may alternatively be specified as continuous functions *f*(*x*,*y*) to be evaluated at each pixel centroid of the raster grid (without interpolation).

`Plot.raster({x1: -1, x2: 1, y1: -1, y2: 1, fill: (x, y) => Math.atan2(y, x)})`

The resolution of the rectangular raster image may be specified with the following options:

**width**- the number of pixels on each horizontal line**height**- the number of lines; a positive integer

The raster dimensions may also be imputed from the extent of *x* and *y* and a pixel size:

**x1**- the starting horizontal position; bound to the*x*scale**x2**- the ending horizontal position; bound to the*x*scale**y1**- the starting vertical position; bound to the*y*scale**y2**- the ending vertical position; bound to the*y*scale**pixelSize**- the screen size of a raster pixel; defaults to 1

If **width** is specified, **x1** defaults to 0 and **x2** defaults to **width**; likewise, if **height** is specified, **y1** defaults to 0 and **y2** defaults to **height**. Otherwise, if **data** is specified, **x1**, **y1**, **x2**, and **y2** respectively default to the frame’s left, top, right, and bottom coordinates. Lastly, if **data** is not specified (as when **fill** or **fillOpacity** is a function of *x* and *y*), you must specify all of **x1**, **x2**, **y1**, and **y2** to define the raster domain (see below). The **pixelSize** may be set to the inverse of the devicePixelRatio for a sharper image.

The following raster-specific constant options are supported:

**interpolate**- the spatial interpolation method**imageRendering**- the image-rendering attribute; defaults to*auto*(bilinear)**blur**- a non-negative pixel radius for smoothing; defaults to 0

The **imageRendering** option may be set to *pixelated* for a sharper image. The **interpolate** option is ignored when **fill** or **fillOpacity** is a function of *x* and *y*.

## raster(*data*, *options*)

`Plot.raster(volcano.values, {width: volcano.width, height: volcano.height})`

Returns a new raster mark with the given (optional) *data* and *options*.

## Spatial interpolators

The raster and contour marks use **spatial interpolators** to populate a raster grid from a discrete set of (often ungridded) spatial samples. The **interpolate** option controls how these marks compute the raster grid. The following built-in methods are provided:

*none*(or null) - assign each sample to the containing pixel*nearest*- assign each pixel to the closest sample’s value (Voronoi diagram)*barycentric*- apply barycentric interpolation over the Delaunay triangulation*random-walk*- apply a random walk from each pixel, stopping when near a sample

The **interpolate** option can also be specified as a function with the following arguments:

*index*- an array of numeric indexes into the channels*x*,*y*,*value**width*- the width of the raster grid; a positive integer*height*- the height of the raster grid; a positive integer*x*- an array of values representing the*x*-position of samples*y*- an array of values representing the*y*-position of samples*value*- an array of values representing the sample’s observed value

So, *x*[*index*[0]] represents the *x*-position of the first sample, *y*[*index*[0]] its *y*-position, and *value*[*index*[0]] its value (*e.g.*, the observed height for a topographic map).

## interpolateNone(*index*, *width*, *height*, *x*, *y*, *value*)

`Plot.raster(ca55, {x: "LONGITUDE", y: "LATITUDE", fill: "MAG_IGRF90", interpolate: Plot.interpolateNone})`

Applies a simple forward mapping of samples, binning them into pixels in the raster grid without any blending or interpolation. If multiple samples map to the same pixel, the last one wins; this can introduce bias if the points are not in random order, so use Plot.shuffle to randomize the input if needed.

## interpolateNearest(*index*, *width*, *height*, *x*, *y*, *value*)

`Plot.raster(ca55, {x: "LONGITUDE", y: "LATITUDE", fill: "MAG_IGRF90", interpolate: Plot.interpolateNearest})`

Assigns each pixel in the raster grid the value of the closest sample; effectively a Voronoi diagram.

## interpolatorBarycentric(*options*)

`Plot.raster(ca55, {x: "LONGITUDE", y: "LATITUDE", fill: "MAG_IGRF90", interpolate: Plot.interpolatorBarycentric()})`

Constructs a Delaunay triangulation of the samples, and then for each pixel in the raster grid, determines the triangle that covers the pixel’s centroid and interpolates the values associated with the triangle’s vertices using barycentric coordinates. If the interpolated values are ordinal or categorical (*i.e.*, anything other than numbers or dates), then one of the three values will be picked randomly weighted by the barycentric coordinates; the given **random** number generator will be used, which defaults to a linear congruential generator with a fixed seed (for deterministic results).

## interpolatorRandomWalk(*options*)

`Plot.raster(ca55, {x: "LONGITUDE", y: "LATITUDE", fill: "MAG_IGRF90", interpolate: Plot.interpolatorRandomWalk()})`

For each pixel in the raster grid, initiates a random walk, stopping when either the walk is within a given distance (**minDistance**) of a sample or the maximum allowable number of steps (**maxSteps**) have been taken, and then assigning the current pixel the closest sample’s value. The random walk uses the “walk on spheres” algorithm in two dimensions described by Sawhney and Crane, SIGGRAPH 2020; the given **random** number generator will be used, which defaults to a linear congruential generator with a fixed seed (for deterministic results).