Skip to content

Commit bfa7112

Browse files
committed
upgrade opacity legends: support for display-p3 colors, checkerboard, dark mode
1 parent c316a7d commit bfa7112

18 files changed

Lines changed: 1272 additions & 287 deletions

src/legends.js

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
import {rgb} from "d3";
1+
import {select} from "d3";
22
import {createContext} from "./context.js";
33
import {legendRamp} from "./legends/ramp.js";
44
import {isSymbolColorLegend, legendSwatches, legendSymbols} from "./legends/swatches.js";
55
import {inherit, isScaleOptions} from "./options.js";
6+
import {getFilterId} from "./style.js";
67
import {normalizeScale} from "./scales.js";
78

89
const legendRegistry = new Map([
@@ -56,16 +57,31 @@ function legendColor(color, {legend = true, ...options}) {
5657
}
5758
}
5859

59-
function legendOpacity({type, interpolate, ...scale}, {legend = true, color = rgb(0, 0, 0), ...options}) {
60+
function legendOpacity({type, interpolate, ...scale}, {legend = true, color = "currentColor", ...options}) {
6061
if (!interpolate) throw new Error(`${type} opacity scales are not supported`);
6162
if (legend === true) legend = "ramp";
6263
if (`${legend}`.toLowerCase() !== "ramp") throw new Error(`${legend} opacity legends are not supported`);
63-
return legendColor({type, ...scale, interpolate: interpolateOpacity(color)}, {legend, ...options});
64-
}
64+
const svg = legendColor({type, ...scale, interpolate: (t) => `rgba(0,0,0,${t})`}, {legend, ...options});
65+
if (!svg) return;
66+
const s = select(svg);
67+
const image = s.select("image");
68+
const x = +image.attr("x");
69+
const y = +image.attr("y");
70+
const w = +image.attr("width");
71+
const h = +image.attr("height");
72+
const pid = getFilterId();
73+
const fid = getFilterId();
74+
75+
// Checkerboard
76+
const pattern = s.append("pattern").attr("id", pid).attr("y", y).attr("width", h).attr("height", h).attr("patternUnits", "userSpaceOnUse"); // prettier-ignore
77+
pattern.append("rect").attr("width", h).attr("height", h).attr("fill", "var(--plot-background, white)");
78+
pattern.append("path").attr("d", `M0,0h${h / 2}v${h / 2}H0ZM${h / 2},${h / 2}h${h / 2}v${h / 2}H${h / 2}Z`).attr("fill", "color-mix(in srgb, var(--plot-background, white), currentColor 20%)"); // prettier-ignore
79+
s.insert("rect", "image").attr("x", x).attr("y", y).attr("width", w).attr("height", h).attr("fill", `url(#${pid})`); // prettier-ignore
6580

66-
function interpolateOpacity(color) {
67-
const {r, g, b} = rgb(color) || rgb(0, 0, 0); // treat invalid color as black
68-
return (t) => `rgba(${r},${g},${b},${t})`;
81+
// Color
82+
image.attr("filter", `url(#${fid})`);
83+
s.append("filter").attr("id", fid).call((f) => { f.append("feFlood").attr("flood-color", color); f.append("feComposite").attr("in2", "SourceGraphic").attr("operator", "in"); }); // prettier-ignore
84+
return svg;
6985
}
7086

7187
export function createLegends(scales, context, options) {

src/style.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,17 @@ export function setOffset(o) {
1313
}
1414

1515
let nextClipId = 0;
16+
let nextFilterId = 0;
1617
let nextPatternId = 0;
1718

1819
export function getClipId() {
1920
return `plot-clip-${++nextClipId}`;
2021
}
2122

23+
export function getFilterId() {
24+
return `plot-filter-${++nextFilterId}`;
25+
}
26+
2227
export function getPatternId() {
2328
return `plot-pattern-${++nextPatternId}`;
2429
}

test/output/opacityLegend.html

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
<div style="display: flex; gap: 20px;">
2+
<div style="background-color: white; color: black; color-scheme: light; padding: 20px;"><svg class="plot-ramp" font-family="system-ui, sans-serif" font-size="10" width="240" height="50" viewBox="0 0 240 50" color="black" style="--plot-background: white" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
3+
<style>
4+
:where(.plot-ramp) {
5+
display: block;
6+
height: auto;
7+
height: intrinsic;
8+
max-width: 100%;
9+
overflow: visible;
10+
}
11+
12+
:where(.plot-ramp text) {
13+
white-space: pre;
14+
}
15+
</style>
16+
<rect x="0" y="18" width="240" height="10" fill="url(#plot-filter-1)"></rect>
17+
<image x="0" y="18" width="240" height="10" preserveAspectRatio="none" href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAQAAAAABCAYAAAAxWXB3AAAABmJLR0QA/wD/AP+gvaeTAAAAWUlEQVQ4jd2SSwoAIAhEx+5/6DZCg/gZqFWBoCZPRzScZwCW+8tjozjmO6tqJxbnlX4K57Y2m3n6Z44h36eqMXK4X2SrO+rmV7hZbeVPepR7UrX9pOWllewNAjMBAXvLNnYAAAAASUVORK5CYII=" filter="url(#plot-filter-2)"></image>
18+
<g transform="translate(0,28)" fill="none" text-anchor="middle" font-variant="tabular-nums">
19+
<g class="tick" opacity="1" transform="translate(0.5,0)">
20+
<line stroke="currentColor" y2="6" y1="-10"></line>
21+
<text fill="currentColor" y="9" dy="0.71em">0</text>
22+
</g>
23+
<g class="tick" opacity="1" transform="translate(48.5,0)">
24+
<line stroke="currentColor" y2="6" y1="-10"></line>
25+
<text fill="currentColor" y="9" dy="0.71em">2</text>
26+
</g>
27+
<g class="tick" opacity="1" transform="translate(96.5,0)">
28+
<line stroke="currentColor" y2="6" y1="-10"></line>
29+
<text fill="currentColor" y="9" dy="0.71em">4</text>
30+
</g>
31+
<g class="tick" opacity="1" transform="translate(144.5,0)">
32+
<line stroke="currentColor" y2="6" y1="-10"></line>
33+
<text fill="currentColor" y="9" dy="0.71em">6</text>
34+
</g>
35+
<g class="tick" opacity="1" transform="translate(192.5,0)">
36+
<line stroke="currentColor" y2="6" y1="-10"></line>
37+
<text fill="currentColor" y="9" dy="0.71em">8</text>
38+
</g>
39+
<g class="tick" opacity="1" transform="translate(240.5,0)">
40+
<line stroke="currentColor" y2="6" y1="-10"></line>
41+
<text fill="currentColor" y="9" dy="0.71em">10</text>
42+
</g>
43+
</g>
44+
<text x="0" y="12" fill="currentColor" font-weight="bold">Quantitative</text>
45+
<pattern id="plot-filter-1" y="18" width="10" height="10" patternUnits="userSpaceOnUse">
46+
<rect width="10" height="10" fill="var(--plot-background, white)"></rect>
47+
<path d="M0,0h5v5H0ZM5,5h5v5H5Z" fill="color-mix(in srgb, var(--plot-background, white), currentColor 20%)"></path>
48+
</pattern>
49+
<filter id="plot-filter-2">
50+
<feFlood flood-color="currentColor"></feFlood>
51+
<feComposite in2="SourceGraphic" operator="in"></feComposite>
52+
</filter>
53+
</svg></div>
54+
<div style="background-color: black; color: white; color-scheme: dark; padding: 20px;"><svg class="plot-ramp" font-family="system-ui, sans-serif" font-size="10" width="240" height="50" viewBox="0 0 240 50" color="white" style="--plot-background: black" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
55+
<style>
56+
:where(.plot-ramp) {
57+
display: block;
58+
height: auto;
59+
height: intrinsic;
60+
max-width: 100%;
61+
overflow: visible;
62+
}
63+
64+
:where(.plot-ramp text) {
65+
white-space: pre;
66+
}
67+
</style>
68+
<rect x="0" y="18" width="240" height="10" fill="url(#plot-filter-3)"></rect>
69+
<image x="0" y="18" width="240" height="10" preserveAspectRatio="none" href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAQAAAAABCAYAAAAxWXB3AAAABmJLR0QA/wD/AP+gvaeTAAAAWUlEQVQ4jd2SSwoAIAhEx+5/6DZCg/gZqFWBoCZPRzScZwCW+8tjozjmO6tqJxbnlX4K57Y2m3n6Z44h36eqMXK4X2SrO+rmV7hZbeVPepR7UrX9pOWllewNAjMBAXvLNnYAAAAASUVORK5CYII=" filter="url(#plot-filter-4)"></image>
70+
<g transform="translate(0,28)" fill="none" text-anchor="middle" font-variant="tabular-nums">
71+
<g class="tick" opacity="1" transform="translate(0.5,0)">
72+
<line stroke="currentColor" y2="6" y1="-10"></line>
73+
<text fill="currentColor" y="9" dy="0.71em">0</text>
74+
</g>
75+
<g class="tick" opacity="1" transform="translate(48.5,0)">
76+
<line stroke="currentColor" y2="6" y1="-10"></line>
77+
<text fill="currentColor" y="9" dy="0.71em">2</text>
78+
</g>
79+
<g class="tick" opacity="1" transform="translate(96.5,0)">
80+
<line stroke="currentColor" y2="6" y1="-10"></line>
81+
<text fill="currentColor" y="9" dy="0.71em">4</text>
82+
</g>
83+
<g class="tick" opacity="1" transform="translate(144.5,0)">
84+
<line stroke="currentColor" y2="6" y1="-10"></line>
85+
<text fill="currentColor" y="9" dy="0.71em">6</text>
86+
</g>
87+
<g class="tick" opacity="1" transform="translate(192.5,0)">
88+
<line stroke="currentColor" y2="6" y1="-10"></line>
89+
<text fill="currentColor" y="9" dy="0.71em">8</text>
90+
</g>
91+
<g class="tick" opacity="1" transform="translate(240.5,0)">
92+
<line stroke="currentColor" y2="6" y1="-10"></line>
93+
<text fill="currentColor" y="9" dy="0.71em">10</text>
94+
</g>
95+
</g>
96+
<text x="0" y="12" fill="currentColor" font-weight="bold">Quantitative</text>
97+
<pattern id="plot-filter-3" y="18" width="10" height="10" patternUnits="userSpaceOnUse">
98+
<rect width="10" height="10" fill="var(--plot-background, white)"></rect>
99+
<path d="M0,0h5v5H0ZM5,5h5v5H5Z" fill="color-mix(in srgb, var(--plot-background, white), currentColor 20%)"></path>
100+
</pattern>
101+
<filter id="plot-filter-4">
102+
<feFlood flood-color="currentColor"></feFlood>
103+
<feComposite in2="SourceGraphic" operator="in"></feComposite>
104+
</filter>
105+
</svg></div>
106+
</div>

test/output/opacityLegend.svg

Lines changed: 0 additions & 43 deletions
This file was deleted.

test/output/opacityLegendCSS4.html

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
<div style="display: flex; gap: 20px;">
2+
<div style="background-color: white; color: black; color-scheme: light; padding: 20px;"><svg class="plot-ramp" font-family="system-ui, sans-serif" font-size="10" width="240" height="50" viewBox="0 0 240 50" color="black" style="--plot-background: white" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
3+
<style>
4+
:where(.plot-ramp) {
5+
display: block;
6+
height: auto;
7+
height: intrinsic;
8+
max-width: 100%;
9+
overflow: visible;
10+
}
11+
12+
:where(.plot-ramp text) {
13+
white-space: pre;
14+
}
15+
</style>
16+
<rect x="0" y="18" width="240" height="10" fill="url(#plot-filter-25)"></rect>
17+
<image x="0" y="18" width="240" height="10" preserveAspectRatio="none" href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAQAAAAABCAYAAAAxWXB3AAAABmJLR0QA/wD/AP+gvaeTAAAAWUlEQVQ4jd2SSwoAIAhEx+5/6DZCg/gZqFWBoCZPRzScZwCW+8tjozjmO6tqJxbnlX4K57Y2m3n6Z44h36eqMXK4X2SrO+rmV7hZbeVPepR7UrX9pOWllewNAjMBAXvLNnYAAAAASUVORK5CYII=" filter="url(#plot-filter-26)"></image>
18+
<g transform="translate(0,28)" fill="none" text-anchor="middle" font-variant="tabular-nums">
19+
<g class="tick" opacity="1" transform="translate(0.5,0)">
20+
<line stroke="currentColor" y2="6" y1="-10"></line>
21+
<text fill="currentColor" y="9" dy="0.71em">0.0</text>
22+
</g>
23+
<g class="tick" opacity="1" transform="translate(48.5,0)">
24+
<line stroke="currentColor" y2="6" y1="-10"></line>
25+
<text fill="currentColor" y="9" dy="0.71em">0.2</text>
26+
</g>
27+
<g class="tick" opacity="1" transform="translate(96.5,0)">
28+
<line stroke="currentColor" y2="6" y1="-10"></line>
29+
<text fill="currentColor" y="9" dy="0.71em">0.4</text>
30+
</g>
31+
<g class="tick" opacity="1" transform="translate(144.5,0)">
32+
<line stroke="currentColor" y2="6" y1="-10"></line>
33+
<text fill="currentColor" y="9" dy="0.71em">0.6</text>
34+
</g>
35+
<g class="tick" opacity="1" transform="translate(192.5,0)">
36+
<line stroke="currentColor" y2="6" y1="-10"></line>
37+
<text fill="currentColor" y="9" dy="0.71em">0.8</text>
38+
</g>
39+
<g class="tick" opacity="1" transform="translate(240.5,0)">
40+
<line stroke="currentColor" y2="6" y1="-10"></line>
41+
<text fill="currentColor" y="9" dy="0.71em">1.0</text>
42+
</g>
43+
</g>
44+
<text x="0" y="12" fill="currentColor" font-weight="bold">oklch</text>
45+
<pattern id="plot-filter-25" y="18" width="10" height="10" patternUnits="userSpaceOnUse">
46+
<rect width="10" height="10" fill="var(--plot-background, white)"></rect>
47+
<path d="M0,0h5v5H0ZM5,5h5v5H5Z" fill="color-mix(in srgb, var(--plot-background, white), currentColor 20%)"></path>
48+
</pattern>
49+
<filter id="plot-filter-26">
50+
<feFlood flood-color="oklch(70% 0.35 145)"></feFlood>
51+
<feComposite in2="SourceGraphic" operator="in"></feComposite>
52+
</filter>
53+
</svg></div>
54+
<div style="background-color: black; color: white; color-scheme: dark; padding: 20px;"><svg class="plot-ramp" font-family="system-ui, sans-serif" font-size="10" width="240" height="50" viewBox="0 0 240 50" color="white" style="--plot-background: black" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
55+
<style>
56+
:where(.plot-ramp) {
57+
display: block;
58+
height: auto;
59+
height: intrinsic;
60+
max-width: 100%;
61+
overflow: visible;
62+
}
63+
64+
:where(.plot-ramp text) {
65+
white-space: pre;
66+
}
67+
</style>
68+
<rect x="0" y="18" width="240" height="10" fill="url(#plot-filter-27)"></rect>
69+
<image x="0" y="18" width="240" height="10" preserveAspectRatio="none" href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAQAAAAABCAYAAAAxWXB3AAAABmJLR0QA/wD/AP+gvaeTAAAAWUlEQVQ4jd2SSwoAIAhEx+5/6DZCg/gZqFWBoCZPRzScZwCW+8tjozjmO6tqJxbnlX4K57Y2m3n6Z44h36eqMXK4X2SrO+rmV7hZbeVPepR7UrX9pOWllewNAjMBAXvLNnYAAAAASUVORK5CYII=" filter="url(#plot-filter-28)"></image>
70+
<g transform="translate(0,28)" fill="none" text-anchor="middle" font-variant="tabular-nums">
71+
<g class="tick" opacity="1" transform="translate(0.5,0)">
72+
<line stroke="currentColor" y2="6" y1="-10"></line>
73+
<text fill="currentColor" y="9" dy="0.71em">0.0</text>
74+
</g>
75+
<g class="tick" opacity="1" transform="translate(48.5,0)">
76+
<line stroke="currentColor" y2="6" y1="-10"></line>
77+
<text fill="currentColor" y="9" dy="0.71em">0.2</text>
78+
</g>
79+
<g class="tick" opacity="1" transform="translate(96.5,0)">
80+
<line stroke="currentColor" y2="6" y1="-10"></line>
81+
<text fill="currentColor" y="9" dy="0.71em">0.4</text>
82+
</g>
83+
<g class="tick" opacity="1" transform="translate(144.5,0)">
84+
<line stroke="currentColor" y2="6" y1="-10"></line>
85+
<text fill="currentColor" y="9" dy="0.71em">0.6</text>
86+
</g>
87+
<g class="tick" opacity="1" transform="translate(192.5,0)">
88+
<line stroke="currentColor" y2="6" y1="-10"></line>
89+
<text fill="currentColor" y="9" dy="0.71em">0.8</text>
90+
</g>
91+
<g class="tick" opacity="1" transform="translate(240.5,0)">
92+
<line stroke="currentColor" y2="6" y1="-10"></line>
93+
<text fill="currentColor" y="9" dy="0.71em">1.0</text>
94+
</g>
95+
</g>
96+
<text x="0" y="12" fill="currentColor" font-weight="bold">oklch</text>
97+
<pattern id="plot-filter-27" y="18" width="10" height="10" patternUnits="userSpaceOnUse">
98+
<rect width="10" height="10" fill="var(--plot-background, white)"></rect>
99+
<path d="M0,0h5v5H0ZM5,5h5v5H5Z" fill="color-mix(in srgb, var(--plot-background, white), currentColor 20%)"></path>
100+
</pattern>
101+
<filter id="plot-filter-28">
102+
<feFlood flood-color="oklch(70% 0.35 145)"></feFlood>
103+
<feComposite in2="SourceGraphic" operator="in"></feComposite>
104+
</filter>
105+
</svg></div>
106+
</div>

0 commit comments

Comments
 (0)