Unlisted
Edited
Apr 7
Insert cell
Changed in parent
# Plot with color bar on axis I’ve found myself wanting to add a color strip to an axis on a plot a couple of times. This can be done in many ways in *Observable.plot*. The main constraints are:
-
- We want to control the width of the bar in *pixels*, not in axis units. The width and position in the plot should not change if the domain on the Y axis changes. (although this is hard to avoid if we don’t at least have a fixed minimum value)
+
- We want to control the width of the bar in *pixels*, not in axis units. The position may be specified in axis units, but it may be offset from that position by a fixed number of pixels.
- The mark we use must not force an axis to use a certain type of scale
-
- We want to avoid overlap with other elements. Ideally the bar shows up just below the X axis. - The colors for the color bar are part of, or derived from a set of data points, which may or may not be uniformly spaced.
+
- We want to avoid overlap with other elements
Insert cell
Insert cell
Changed in parent
Plot.plot({
-
insetBottom: 12, // make room for color bar
+
insetBottom: 22, // make room for color bar
height: 200, x: { label: "hue (degrees)", ticks: [0, 60, 120, 180, 240, 300, 360], grid: true }, y: { label: "luminance", }, marks: [ Plot.ruleY([0]), Plot.lineY(data, {x: "h", y: "lum"}), // color bar
-
Plot.line(data, { // position of data points x: "h", // reference position on Y axis y: 0, // required to avoid breaking up the line in separate series by color z: null, // line width and offset from reference position strokeWidth: 7, dy: 6, // style stroke: "css"})
+
Plot.line(data, {x: "h", y: 0, z: null, strokeWidth: 18, dy: 11, strokeLinecap: "square", stroke: "css"})
] })
Insert cell
Changed in parent
-
This is simple, and easily tolerates any spacing between data points. Due to the size of the line caps, this is suitable only for thin color bars. (Using `butt` line caps ideally should work, but it often results in poor rendering with visible gaps.)
+
This is simple, and easily tolerates any spacing between data points. However the colors are offset by half the line width, due to how the differently colored segments overlap. Using `butt` line caps often results in poor rendering with visible gaps.
Insert cell
Insert cell
Changed in parent
Plot.plot({ insetBottom: 22, // make room for color bar height: 200, x: { label: "hue (degrees)", ticks: [0, 60, 120, 180, 240, 300, 360], grid: true }, y: { label: "luminance", }, marks: [ Plot.ruleY([0]), Plot.lineY(data, {x: "h", y: "lum"}), // colored bar
-
Plot.ruleX(data, { // position of each data point x: "h", // reference position on Y axis y1: 0, y2: 0, // offset of both edges from reference position on Y axis insetTop: 2, insetBottom: -20, // style strokeWidth: dh * 2, stroke: "css"})
+
Plot.ruleX(data, {x: "h", y1: 0, y2: 0, insetTop: 2, insetBottom: -20, strokeWidth: dh * 2, stroke: "css"})
] })
Insert cell
Insert cell
Insert cell
Added in parent
 Plot.plot({
   insetBottom: 22, // make room for color bar
   height: 200,
   x: {
     label: "hue (degrees)",
     ticks: [0, 60, 120, 180, 240, 300, 360],
     grid: true
   },
   y: {
      label: "luminance",
    },
    marks: [
      Plot.ruleY([0]),
      Plot.lineY(data, {x: "h", y: "lum"}),
      Plot.rect(data, {
        // range covered by each rectangle
        x1: o => o.h - .5*dh,
        x2: o => o.h + .5*dh,
        // reference position on Y axis
        y1: 0, y2: 0,
        // offset of both edges from reference position on Y axis
        insetBottom: -20, insetTop: 2,
        // slight overlap between rectangles to avoid having browsers render gaps between rectangles
        insetRight: -0.5,
        // style
        fill: "css",
        strokeWidth: 0})
    ]
  })
Insert cell
Changed in parent
-
### Raster A raster mark works by first rasterizing your data to an image. `width` can be set to any number, or can be left out to infer it from your plot size. `height` can be set to 1. The data argument is used, together with the `fill` and `x` option, this ensures the colors are aligned correctly on the axis. The range is by default the entire domain of the axis, but it can be overridden with `x1` and `x2`. A raster can be made to look completely smooth in two ways: - If your data is evenly spaced, you can set the `width` to the number of data points, and rely on your browser to smoothly scale the resulting image to the desired on-screen size. - If you can calculate the color directly from a coordinate, you can directly rasterize the bar to a high enough resolution. There is however no easy way to control its exact size and position on the Y axis. If the domain of the Y axis is known you can pick an `y1`–`y2` interval on the axis just below this domain. _(H/T: Fil — https://observablehq.com/@fil for suggesting this mark, along with the child plot trick)_
+
### Raster1
Insert cell
Changed in parent
Plot.plot({ height: 200, x: { label: "hue (degrees)", ticks: [0, 60, 120, 180, 240, 300, 360], grid: true }, y: { label: "luminance" }, marks: [ Plot.ruleY([0]), Plot.lineY(data, { x: "h", y: "lum" }), // color bar
-
Plot.raster(data, { // height can be 1 pixel. width is inferred from on-screen plot size.
+
Plot.raster({ width: 360,
height: 1,
-
// position of data points x: "h", // specifying Y is required y: el => 0, // position of raster on Y axis. On the X axis we use the default, which // is to cover the entire axis. y1: -0.140, y2: -0.012, // fill space between data points. // Colors values are treated as _categorical_ values, so `barycentric` // interpolation will result in one color picked at random. interpolate: "nearest", // fill fill: "css"
+
y1: -0.1, y2: 0, fill: (h) => { const [r, g, b] = hsv2rgb(h, 1, 1); return d3.rgb(r * 255, g * 255, b * 255).formatHex(); }
}) ] })
Insert cell
Changed in parent
-
If we want a raster, but also want to control the width and position independently from the domain, we can create a second plot as a child element of our plot, and control the pixel position of that second plot. The raster will then completely fill this second plot.
+
### Raster2
Insert cell
Changed in parent
Plot.plot({
-
insetBottom: 22, // make room for color bar
height: 200, x: { label: "hue (degrees)", ticks: [0, 60, 120, 180, 240, 300, 360], grid: true }, y: { label: "luminance" },
+
insetBottom: 15,
marks: [ Plot.ruleY([0]), Plot.lineY(data, { x: "h", y: "lum" }), // color bar (_index, scales, _values, { width, height }) =>
-
Plot.raster(data, { // Same raster options as above, but x1 and x2 are now more or less required. width: data.length, x: "h", interpolate: "nearest",
+
Plot.raster({ width: 360,
height: 1,
-
fill: "css", // fill axis of on parent plot x1: scales.scales.x.domain[0], x2: scales.scales.x.domain[1]
+
fill: (h) => { const [r, g, b] = hsv2rgb(h, 1, 1); return d3.rgb(r * 255, g * 255, b * 255).formatHex(); }
}) .plot({ width,
-
// the x scale must be the same as the parent plot
+
height,
x: scales.scales.x,
-
// the y range controls where the bar ends up. y: { range: [scales.y(0) + 2, scales.y(0) + 20] }, // No axis on our child plot
+
y: { range: [scales.y(0), scales.y(0) + 15] },
axis: null }) .querySelector("g") // fixed in Plot 0.6.17, see https://github.com/observablehq/plot/pull/2219 ] })
Insert cell
Insert cell
Changed in parent
// Change interval between h values
-
dh = 3
+
dh = 2
Insert cell
Added in parent
// change the start of our data
h_begin = 0
Insert cell
Added in parent
viewof value_scale = Inputs.range([0.1, 1], {label: "Value", value: 1, step: .001})
Insert cell
Changed in parent
-
// a toy example of a data set that comes with a display color channel
data = { var d = [];
-
for (var h = h_begin; h <= 360; h += dh) { var [r, g, b] = hsv2rgb(h, 1, value_scale);
+
for (var h = 0; h <= 360; h += dh) { var [r, g, b] = hsv2rgb(h, 1, 1);
// quick & dirty estimate of rgb luminance var lum = 0.2126 * Math.pow(r, 2.2) + 0.7152 * Math.pow(g, 2.2) + 0.0722 * Math.pow(b, 2.2);
-
d.push({ h, lum, css: d3.rgb(r * 255, g * 255, b * 255).formatHex() });
+
d.push({ h, lum, css: `rgb(${r * 255} ${g * 255} ${b * 255})` });
} return d; }
Insert cell
function hsv2rgb(h,s,v)
{
let f= (n,k=(n+h/60)%6) => v - v*s*Math.max( Math.min(k,4-k,1), 0);
return [f(5),f(3),f(1)];
}
Insert cell