# 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
Plot.plot({-insetBottom: 12, // make room for color bar+insetBottom: 22, // make room for color barheight: 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"})] })
-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.
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"})] })
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}) ] })
-### 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
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(); }}) ] })
-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
Plot.plot({-insetBottom: 22, // make room for color barheight: 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 ] })
// Change interval between h values-dh = 3+dh = 2
// change the start of our data h_begin = 0
viewof value_scale = Inputs.range([0.1, 1], {label: "Value", value: 1, step: .001})
-// a toy example of a data set that comes with a display color channeldata = { 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; }