diff --git a/README.md b/README.md
index d5a58e8f35..f26e5fa1ed 100644
--- a/README.md
+++ b/README.md
@@ -258,7 +258,7 @@ Continuous color legends are rendered as a ramp, and can be configured with the
#### Plot.legend({[*name*]: *scale*, ...*options*})
-Returns a legend for the given *scale* definition, passing the options described in the previous section. The only supported name for now is *color*.
+Returns a legend for the given *scale* definition, passing the options described in the previous section. Currently supports only *color* and *opacity* scales. An opacity scale is treated as a color scale with varying transparency.
### Position options
diff --git a/src/legends.js b/src/legends.js
index d492ee5d58..5370475978 100644
--- a/src/legends.js
+++ b/src/legends.js
@@ -1,14 +1,17 @@
import {normalizeScale} from "./scales.js";
import {legendColor} from "./legends/color.js";
+import {legendOpacity} from "./legends/opacity.js";
+import {isObject} from "./mark.js";
const legendRegistry = new Map([
- ["color", legendColor]
+ ["color", legendColor],
+ ["opacity", legendOpacity]
]);
export function legend(options = {}) {
for (const [key, value] of legendRegistry) {
const scale = options[key];
- if (scale != null) {
+ if (isObject(scale)) { // e.g., ignore {color: "red"}
return value(normalizeScale(key, scale), legendOptions(scale, options));
}
}
diff --git a/src/legends/opacity.js b/src/legends/opacity.js
new file mode 100644
index 0000000000..dca39a523d
--- /dev/null
+++ b/src/legends/opacity.js
@@ -0,0 +1,19 @@
+import {rgb} from "d3";
+import {legendColor} from "./color.js";
+
+const black = rgb(0, 0, 0);
+
+export function legendOpacity({type, interpolate, ...scale}, {
+ legend = "ramp",
+ color = black,
+ ...options
+}) {
+ if (!interpolate) throw new Error(`${type} opacity scales are not supported`);
+ 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) {
+ const {r, g, b} = rgb(color) || black; // treat invalid color as black
+ return t => `rgba(${r},${g},${b},${t})`;
+}
diff --git a/src/mark.js b/src/mark.js
index 4336013c5a..86039ad5f6 100644
--- a/src/mark.js
+++ b/src/mark.js
@@ -173,12 +173,15 @@ export function arrayify(data, type) {
: (data instanceof type ? data : type.from(data)));
}
+// Disambiguates an options object (e.g., {y: "x2"}) from a primitive value.
+export function isObject(option) {
+ return option && option.toString === objectToString;
+}
+
// Disambiguates an options object (e.g., {y: "x2"}) from a channel value
// definition expressed as a channel transform (e.g., {transform: …}).
export function isOptions(option) {
- return option
- && option.toString === objectToString
- && typeof option.transform !== "function";
+ return isObject(option) && typeof option.transform !== "function";
}
// For marks specified either as [0, x] or [x1, x2], such as areas and bars.
diff --git a/test/output/opacityLegend.svg b/test/output/opacityLegend.svg
new file mode 100644
index 0000000000..3b26c8aae6
--- /dev/null
+++ b/test/output/opacityLegend.svg
@@ -0,0 +1,37 @@
+
\ No newline at end of file
diff --git a/test/output/opacityLegendColor.svg b/test/output/opacityLegendColor.svg
new file mode 100644
index 0000000000..e14b6d2c0a
--- /dev/null
+++ b/test/output/opacityLegendColor.svg
@@ -0,0 +1,37 @@
+
\ No newline at end of file
diff --git a/test/output/opacityLegendLinear.svg b/test/output/opacityLegendLinear.svg
new file mode 100644
index 0000000000..48096cc4a2
--- /dev/null
+++ b/test/output/opacityLegendLinear.svg
@@ -0,0 +1,37 @@
+
\ No newline at end of file
diff --git a/test/output/opacityLegendLog.svg b/test/output/opacityLegendLog.svg
new file mode 100644
index 0000000000..268e96fe41
--- /dev/null
+++ b/test/output/opacityLegendLog.svg
@@ -0,0 +1,49 @@
+
\ No newline at end of file
diff --git a/test/output/opacityLegendRange.svg b/test/output/opacityLegendRange.svg
new file mode 100644
index 0000000000..8be2684365
--- /dev/null
+++ b/test/output/opacityLegendRange.svg
@@ -0,0 +1,37 @@
+
\ No newline at end of file
diff --git a/test/output/opacityLegendSqrt.svg b/test/output/opacityLegendSqrt.svg
new file mode 100644
index 0000000000..0517968c76
--- /dev/null
+++ b/test/output/opacityLegendSqrt.svg
@@ -0,0 +1,37 @@
+
\ No newline at end of file
diff --git a/test/plots/index.js b/test/plots/index.js
index 510e9b7957..ca859a8424 100644
--- a/test/plots/index.js
+++ b/test/plots/index.js
@@ -52,7 +52,6 @@ export {default as industryUnemployment} from "./industry-unemployment.js";
export {default as industryUnemploymentShare} from "./industry-unemployment-share.js";
export {default as industryUnemploymentStream} from "./industry-unemployment-stream.js";
export {default as learningPoverty} from "./learning-poverty.js";
-export * from "./legend-color.js";
export {default as letterFrequencyBar} from "./letter-frequency-bar.js";
export {default as letterFrequencyCloud} from "./letter-frequency-cloud.js";
export {default as letterFrequencyColumn} from "./letter-frequency-column.js";
@@ -129,3 +128,6 @@ export {default as usRetailSales} from "./us-retail-sales.js";
export {default as usStatePopulationChange} from "./us-state-population-change.js";
export {default as wordCloud} from "./word-cloud.js";
export {default as wordLengthMobyDick} from "./word-length-moby-dick.js";
+
+export * from "./legend-color.js";
+export * from "./legend-opacity.js";
diff --git a/test/plots/legend-opacity.js b/test/plots/legend-opacity.js
new file mode 100644
index 0000000000..0bcc76a208
--- /dev/null
+++ b/test/plots/legend-opacity.js
@@ -0,0 +1,25 @@
+import * as Plot from "@observablehq/plot";
+
+export function opacityLegend() {
+ return Plot.legend({opacity: {domain: [0, 10], label: "Quantitative"}});
+}
+
+export function opacityLegendRange() {
+ return Plot.legend({opacity: {domain: [0, 1], range: [0.5, 1], label: "Range"}});
+}
+
+export function opacityLegendLinear() {
+ return Plot.legend({opacity: {type: "linear", domain: [0, 10], label: "Linear"}});
+}
+
+export function opacityLegendColor() {
+ return Plot.legend({opacity: {type: "linear", domain: [0, 10], label: "Linear"}, color: "steelblue"});
+}
+
+export function opacityLegendLog() {
+ return Plot.legend({opacity: {type: "log", domain: [1, 10], label: "Log"}});
+}
+
+export function opacityLegendSqrt() {
+ return Plot.legend({opacity: {type: "sqrt", domain: [0, 1], label: "Sqrt"}});
+}