diff --git a/src/legends.js b/src/legends.js index 77e1fb3f6e..c3dd5c76de 100644 --- a/src/legends.js +++ b/src/legends.js @@ -52,20 +52,21 @@ function legendColor(color, {legend = true, ...options}) { case "ramp": return legendRamp(color, options); default: - throw new Error(`unknown legend type: ${legend}`); + throw new Error(`unknown color legend type: ${legend}`); } } -function legendOpacity({type, interpolate, ...scale}, {legend = true, color = rgb(0, 0, 0), ...options}) { - if (!interpolate) throw new Error(`${type} opacity scales are not supported`); - if (legend === true) legend = "ramp"; - if (`${legend}`.toLowerCase() !== "ramp") throw new Error(`${legend} opacity legends are not supported`); - return legendColor({type, ...scale, interpolate: interpolateOpacity(color)}, {legend, ...options}); -} - -function interpolateOpacity(color) { +function legendOpacity(opacity, {legend = true, color = "black", ...options}) { + if (legend === true) legend = opacity.type === "ordinal" ? "swatches" : "ramp"; const {r, g, b} = rgb(color) || rgb(0, 0, 0); // treat invalid color as black - return (t) => `rgba(${r},${g},${b},${t})`; + switch (`${legend}`.toLowerCase()) { + case "swatches": + return legendSwatches({...opacity, scale: (x) => String(rgb(r, g, b, opacity.scale(x)))}, options); + case "ramp": + return legendRamp({...opacity, interpolate: (a) => String(rgb(r, g, b, a))}, options); + default: + throw new Error(`unknown opacity legend type: ${legend}`); + } } export function createLegends(scales, context, options) { diff --git a/src/scales/ordinal.js b/src/scales/ordinal.js index e43e5a4d05..e680364fbc 100644 --- a/src/scales/ordinal.js +++ b/src/scales/ordinal.js @@ -3,7 +3,7 @@ import {scaleBand, scaleOrdinal, scalePoint, scaleImplicit} from "d3"; import {ascendingDefined} from "../defined.js"; import {isNoneish, map, maybeRangeInterval} from "../options.js"; import {maybeSymbol} from "../symbol.js"; -import {registry, color, position, symbol} from "./index.js"; +import {registry, color, opacity, position, symbol} from "./index.js"; import {maybeBooleanRange, ordinalScheme, quantitativeScheme} from "./schemes.js"; // This denotes an implicitly ordinal color scale: the scale type was not set, @@ -51,6 +51,10 @@ export function createScaleOrdinal(key, channels, {type, interval, domain, range range = ordinalScheme(scheme); } } + } else if (registry.get(key) === opacity) { + if (range === undefined) { + range = ({length: n}) => quantize((t) => t, n); + } } if (unknown === scaleImplicit) { throw new Error(`implicit unknown on ${key} scale is not supported`); @@ -96,6 +100,7 @@ function inferDomain(channels, interval, key) { if (value === undefined) continue; for (const v of value) values.add(v); } + if (key === "opacity") values.add(0); // akin to inferZeroDomain if (interval !== undefined) { const [min, max] = extent(values).map(interval.floor, interval); return interval.range(min, interval.offset(max)); diff --git a/test/output/opacityLegendSwatches.html b/test/output/opacityLegendSwatches.html new file mode 100644 index 0000000000..5d11622d8e --- /dev/null +++ b/test/output/opacityLegendSwatches.html @@ -0,0 +1,47 @@ +
+ + + 0 + + 1 + + 2 + + 3 + + 4 + + 5 + + 6 + + 7 + + 8 + + 9 +
\ No newline at end of file diff --git a/test/output/opacityLegendSwatchesColor.html b/test/output/opacityLegendSwatchesColor.html new file mode 100644 index 0000000000..c5276b19fd --- /dev/null +++ b/test/output/opacityLegendSwatchesColor.html @@ -0,0 +1,47 @@ +
+ + + 0 + + 1 + + 2 + + 3 + + 4 + + 5 + + 6 + + 7 + + 8 + + 9 +
\ No newline at end of file diff --git a/test/output/ordinalOpacity.svg b/test/output/ordinalOpacity.svg new file mode 100644 index 0000000000..57f43b6c19 --- /dev/null +++ b/test/output/ordinalOpacity.svg @@ -0,0 +1,52 @@ + + + + + 0 + 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/output/ordinalOpacityImplicitZero.svg b/test/output/ordinalOpacityImplicitZero.svg new file mode 100644 index 0000000000..b25644a274 --- /dev/null +++ b/test/output/ordinalOpacityImplicitZero.svg @@ -0,0 +1,46 @@ + + + + + 0 + 1 + 2 + 3 + 4 + 5 + 6 + 7 + + + + + + + + + + + + \ No newline at end of file diff --git a/test/plots/index.ts b/test/plots/index.ts index f54398dca7..debde4073b 100644 --- a/test/plots/index.ts +++ b/test/plots/index.ts @@ -188,6 +188,7 @@ export * from "./nested-facets.js"; export * from "./npm-versions.js"; export * from "./opacity.js"; export * from "./ordinal-bar.js"; +export * from "./ordinal-opacity.js"; export * from "./pairs.js"; export * from "./penguin-annotated.js"; export * from "./penguin-culmen-array.js"; diff --git a/test/plots/legend-color.ts b/test/plots/legend-color.ts index 3609823998..d2d1e97849 100644 --- a/test/plots/legend-color.ts +++ b/test/plots/legend-color.ts @@ -1,5 +1,5 @@ -import * as d3 from "d3"; import * as Plot from "@observablehq/plot"; +import * as d3 from "d3"; export function colorLegendCategorical() { return Plot.legend({color: {domain: "ABCDEFGHIJ"}}); diff --git a/test/plots/legend-opacity.ts b/test/plots/legend-opacity.ts index 0bcc76a208..a69456759f 100644 --- a/test/plots/legend-opacity.ts +++ b/test/plots/legend-opacity.ts @@ -1,4 +1,5 @@ import * as Plot from "@observablehq/plot"; +import * as d3 from "d3"; export function opacityLegend() { return Plot.legend({opacity: {domain: [0, 10], label: "Quantitative"}}); @@ -23,3 +24,11 @@ export function opacityLegendLog() { export function opacityLegendSqrt() { return Plot.legend({opacity: {type: "sqrt", domain: [0, 1], label: "Sqrt"}}); } + +export function opacityLegendSwatches() { + return Plot.legend({opacity: {type: "ordinal", domain: d3.range(10)}}); +} + +export function opacityLegendSwatchesColor() { + return Plot.legend({opacity: {type: "ordinal", domain: d3.range(10)}, color: "red"}); +} diff --git a/test/plots/opacity.ts b/test/plots/opacity.ts index 6db86e1281..0c12692f12 100644 --- a/test/plots/opacity.ts +++ b/test/plots/opacity.ts @@ -1,5 +1,5 @@ -import * as d3 from "d3"; import * as Plot from "@observablehq/plot"; +import * as d3 from "d3"; export function opacityDotsFillUnscaled() { return Plot.dotX(d3.ticks(0.3, 0.7, 40), { diff --git a/test/plots/ordinal-opacity.ts b/test/plots/ordinal-opacity.ts new file mode 100644 index 0000000000..65438dfb5c --- /dev/null +++ b/test/plots/ordinal-opacity.ts @@ -0,0 +1,10 @@ +import * as Plot from "@observablehq/plot"; +import * as d3 from "d3"; + +export async function ordinalOpacity() { + return Plot.cellX(d3.range(10), {fill: "red", opacity: Plot.identity}).plot({opacity: {type: "ordinal"}}); +} + +export async function ordinalOpacityImplicitZero() { + return Plot.cellX(d3.range(2, 10), {fill: "red", opacity: Plot.identity}).plot({opacity: {type: "ordinal"}}); +}