From 30ed257184dede431b1f70aab188e609495d64b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Fri, 29 Oct 2021 16:29:56 +0200 Subject: [PATCH 1/2] opacity legend --- README.md | 2 +- src/legends.js | 4 ++- src/legends/opacity.js | 11 +++++++ test/output/opacityLegend.svg | 37 ++++++++++++++++++++++ test/output/opacityLegendLinear.svg | 37 ++++++++++++++++++++++ test/output/opacityLegendLog.svg | 49 +++++++++++++++++++++++++++++ test/output/opacityLegendRange.svg | 37 ++++++++++++++++++++++ test/output/opacityLegendSqrt.svg | 37 ++++++++++++++++++++++ test/plots/index.js | 4 ++- test/plots/legend-opacity.js | 21 +++++++++++++ 10 files changed, 236 insertions(+), 3 deletions(-) create mode 100644 src/legends/opacity.js create mode 100644 test/output/opacityLegend.svg create mode 100644 test/output/opacityLegendLinear.svg create mode 100644 test/output/opacityLegendLog.svg create mode 100644 test/output/opacityLegendRange.svg create mode 100644 test/output/opacityLegendSqrt.svg create mode 100644 test/plots/legend-opacity.js 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..30fe979f7e 100644 --- a/src/legends.js +++ b/src/legends.js @@ -1,8 +1,10 @@ import {normalizeScale} from "./scales.js"; import {legendColor} from "./legends/color.js"; +import {legendOpacity} from "./legends/opacity.js"; const legendRegistry = new Map([ - ["color", legendColor] + ["color", legendColor], + ["opacity", legendOpacity] ]); export function legend(options = {}) { diff --git a/src/legends/opacity.js b/src/legends/opacity.js new file mode 100644 index 0000000000..39519cf131 --- /dev/null +++ b/src/legends/opacity.js @@ -0,0 +1,11 @@ +import {legendColor} from "./color.js"; + +export function legendOpacity({type, interpolate, ...scale}, {legend = "ramp", ...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}, {legend, ...options}); +} + +function interpolateOpacity(t) { + return `rgba(0,0,0,${t})`; +} 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 @@ + + + + + + 0 + + + 2 + + + 4 + + + 6 + + + 8 + + + 10 + Quantitative + + \ 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 @@ + + + + + + 0 + + + 2 + + + 4 + + + 6 + + + 8 + + + 10 + Linear + + \ 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 @@ + + + + + + 1 + + + 2 + + + 3 + + + + + + + + + + + + + + + + + + + + + 10 + Log + + \ 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 @@ + + + + + + 0.0 + + + 0.2 + + + 0.4 + + + 0.6 + + + 0.8 + + + 1.0 + Range + + \ 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 @@ + + + + + + 0.0 + + + 0.2 + + + 0.4 + + + 0.6 + + + 0.8 + + + 1.0 + Sqrt + + \ 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..953a1a78de --- /dev/null +++ b/test/plots/legend-opacity.js @@ -0,0 +1,21 @@ +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 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"}}); +} From 273df90593719462e4c7ecce567ca218690212af Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Thu, 25 Nov 2021 13:08:29 -0800 Subject: [PATCH 2/2] add color option --- src/legends.js | 3 ++- src/legends/opacity.js | 16 +++++++++---- src/mark.js | 9 +++++--- test/output/opacityLegendColor.svg | 37 ++++++++++++++++++++++++++++++ test/plots/legend-opacity.js | 4 ++++ 5 files changed, 61 insertions(+), 8 deletions(-) create mode 100644 test/output/opacityLegendColor.svg diff --git a/src/legends.js b/src/legends.js index 30fe979f7e..5370475978 100644 --- a/src/legends.js +++ b/src/legends.js @@ -1,6 +1,7 @@ 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], @@ -10,7 +11,7 @@ const legendRegistry = new Map([ 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 index 39519cf131..dca39a523d 100644 --- a/src/legends/opacity.js +++ b/src/legends/opacity.js @@ -1,11 +1,19 @@ +import {rgb} from "d3"; import {legendColor} from "./color.js"; -export function legendOpacity({type, interpolate, ...scale}, {legend = "ramp", ...options}) { +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}, {legend, ...options}); + return legendColor({type, ...scale, interpolate: interpolateOpacity(color)}, {legend, ...options}); } -function interpolateOpacity(t) { - return `rgba(0,0,0,${t})`; +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/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 @@ + + + + + + 0 + + + 2 + + + 4 + + + 6 + + + 8 + + + 10 + Linear + + \ No newline at end of file diff --git a/test/plots/legend-opacity.js b/test/plots/legend-opacity.js index 953a1a78de..0bcc76a208 100644 --- a/test/plots/legend-opacity.js +++ b/test/plots/legend-opacity.js @@ -12,6 +12,10 @@ 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"}}); }