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 @@
+
+ 0123456789
+
\ 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 @@
+
+ 0123456789
+
\ 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 @@
+
\ 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 @@
+
\ 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"}});
+}