Skip to content

Commit 92ea384

Browse files
mbostockFil
andauthored
ordinal opacity (#2285)
* ordinal opacity * threshold opacity scale * purdier * tidy imports * delete dead code * better ordinal & threshold opacity legend * delete more dead code * delete more dead code * minimize diff * simplify * svg filter for opacity ramp legend * haha transparent black not green * further simplify opacity legends --------- Co-authored-by: Philippe Rivière <fil@rezo.net>
1 parent dff4583 commit 92ea384

23 files changed

Lines changed: 519 additions & 30 deletions

src/legends.js

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1+
import {select} from "d3";
12
import {createContext} from "./context.js";
23
import {legendRamp} from "./legends/ramp.js";
34
import {isSymbolColorLegend, legendSwatches, legendSymbols} from "./legends/swatches.js";
45
import {inherit, isScaleOptions} from "./options.js";
56
import {normalizeScale} from "./scales.js";
7+
import {getFilterId} from "./style.js";
68

79
const legendRegistry = new Map([
810
["symbol", legendSymbols],
@@ -51,19 +53,30 @@ function legendColor(color, {legend = true, ...options}) {
5153
case "ramp":
5254
return legendRamp(color, options);
5355
default:
54-
throw new Error(`unknown legend type: ${legend}`);
56+
throw new Error(`unknown color legend type: ${legend}`);
5557
}
5658
}
5759

58-
function legendOpacity({type, interpolate, ...scale}, {legend = true, color = "currentColor", ...options}) {
59-
if (!interpolate) throw new Error(`${type} opacity scales are not supported`);
60-
if (legend === true) legend = "ramp";
61-
if (`${legend}`.toLowerCase() !== "ramp") throw new Error(`${legend} opacity legends are not supported`);
62-
return legendColor({type, ...scale, interpolate: interpolateOpacity(color)}, {legend, ...options});
63-
}
64-
65-
function interpolateOpacity(color) {
66-
return (t) => `color-mix(in srgb, transparent, ${color} ${(t * 100).toFixed(1)}%)`;
60+
function legendOpacity(opacity, {legend = true, color = "currentColor", ...options}) {
61+
if (legend === true) legend = opacity.type === "ordinal" ? "swatches" : "ramp";
62+
opacity = {...opacity, color, key: "opacity"};
63+
switch (`${legend}`.toLowerCase()) {
64+
case "swatches": {
65+
return legendSwatches(opacity, options);
66+
}
67+
case "ramp": {
68+
const legend = legendRamp(opacity, options);
69+
const fid = getFilterId();
70+
const svg = select(legend);
71+
svg.select("image").attr("filter", `url(#${fid})`);
72+
const filter = svg.append("filter").attr("id", fid);
73+
filter.append("feFlood").attr("flood-color", color);
74+
filter.append("feComposite").attr("in2", "SourceGraphic").attr("operator", "in");
75+
return legend;
76+
}
77+
default:
78+
throw new Error(`unknown opacity legend type: ${legend}`);
79+
}
6780
}
6881

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

src/legends/ramp.js

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -91,8 +91,9 @@ export function legendRamp(color, options) {
9191
canvas.width = n;
9292
canvas.height = 1;
9393
const context2 = canvas.getContext("2d", {colorSpace: "display-p3"}); // allow wide gamut
94+
const fillStyle = color.key === "opacity" ? "globalAlpha" : "fillStyle";
9495
for (let i = 0, j = n - 1; i < n; ++i) {
95-
context2.fillStyle = interpolator(i / j);
96+
context2[fillStyle] = interpolator(i / j);
9697
context2.fillRect(i, 0, 1, 1);
9798
}
9899

@@ -120,6 +121,7 @@ export function legendRamp(color, options) {
120121

121122
svg
122123
.append("g")
124+
.attr("fill", color.key === "opacity" ? color.color : null)
123125
.attr("fill-opacity", opacity)
124126
.selectAll()
125127
.data(range)
@@ -129,7 +131,7 @@ export function legendRamp(color, options) {
129131
.attr("y", marginTop)
130132
.attr("width", (d, i) => x(i) - x(i - 1))
131133
.attr("height", height - marginTop - marginBottom)
132-
.attr("fill", (d) => d);
134+
.attr(color.key === "opacity" ? "fill-opacity" : "fill", (d) => d);
133135

134136
ticks = map(thresholds, (_, i) => i);
135137
tickFormat = (i) => thresholdFormat(thresholds[i], i);
@@ -141,6 +143,7 @@ export function legendRamp(color, options) {
141143

142144
svg
143145
.append("g")
146+
.attr("fill", color.key === "opacity" ? color.color : null)
144147
.attr("fill-opacity", opacity)
145148
.selectAll()
146149
.data(domain)
@@ -150,7 +153,7 @@ export function legendRamp(color, options) {
150153
.attr("y", marginTop)
151154
.attr("width", Math.max(0, x.bandwidth() - 1))
152155
.attr("height", height - marginTop - marginBottom)
153-
.attr("fill", scale);
156+
.attr(color.key === "opacity" ? "fill-opacity" : "fill", scale);
154157

155158
tickAdjust = () => {};
156159
}

src/legends/swatches.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,9 @@ export function legendSwatches(color, {opacity, ...options} = {}) {
2121
.append("svg")
2222
.attr("width", width)
2323
.attr("height", height)
24-
.attr("fill", scale.scale)
24+
.attr("fill", color.key === "opacity" ? color.color : null)
2525
.attr("fill-opacity", maybeNumberChannel(opacity)[1])
26+
.attr(color.key === "opacity" ? "fill-opacity" : "fill", scale.scale)
2627
.append("rect")
2728
.attr("width", "100%")
2829
.attr("height", "100%")

src/scales/ordinal.js

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import {scaleBand, scaleOrdinal, scalePoint, scaleImplicit} from "d3";
33
import {ascendingDefined} from "../defined.js";
44
import {isNoneish, map, maybeRangeInterval} from "../options.js";
55
import {maybeSymbol} from "../symbol.js";
6-
import {registry, color, position, symbol} from "./index.js";
6+
import {registry, color, opacity, position, symbol} from "./index.js";
77
import {maybeBooleanRange, ordinalScheme, quantitativeScheme} from "./schemes.js";
88

99
// This denotes an implicitly ordinal color scale: the scale type was not set,
@@ -44,13 +44,17 @@ export function createScaleOrdinal(key, channels, {type, interval, domain, range
4444
if (scheme !== undefined) {
4545
if (range !== undefined) {
4646
const interpolate = quantitativeScheme(scheme);
47-
const t0 = range[0],
48-
d = range[1] - range[0];
49-
range = ({length: n}) => quantize((t) => interpolate(t0 + d * t), n);
47+
const t0 = range[0];
48+
const dt = range[1] - range[0];
49+
range = ({length: n}) => quantize((t) => interpolate(t0 + dt * t), n);
5050
} else {
5151
range = ordinalScheme(scheme);
5252
}
5353
}
54+
} else if (registry.get(key) === opacity) {
55+
if (range === undefined) {
56+
range = ({length: n}) => quantize((t) => t, n);
57+
}
5458
}
5559
if (unknown === scaleImplicit) {
5660
throw new Error(`implicit unknown on ${key} scale is not supported`);
@@ -96,6 +100,7 @@ function inferDomain(channels, interval, key) {
96100
if (value === undefined) continue;
97101
for (const v of value) values.add(v);
98102
}
103+
if (key === "opacity") values.add(0); // akin to inferZeroDomain
99104
if (interval !== undefined) {
100105
const [min, max] = extent(values).map(interval.floor, interval);
101106
return interval.range(min, interval.offset(max));

test/output/colorLegendOpacity.html

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,11 @@
2323
align-items: center;
2424
margin-right: 1em;
2525
}
26-
</style><span class="plot-swatch"><svg width="15" height="15" fill="#4269d0" fill-opacity="0.5" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
26+
</style><span class="plot-swatch"><svg width="15" height="15" fill-opacity="0.5" fill="#4269d0" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
2727
<rect width="100%" height="100%"></rect>
28-
</svg>Dream</span><span class="plot-swatch"><svg width="15" height="15" fill="#efb118" fill-opacity="0.5" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
28+
</svg>Dream</span><span class="plot-swatch"><svg width="15" height="15" fill-opacity="0.5" fill="#efb118" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
2929
<rect width="100%" height="100%"></rect>
30-
</svg>Torgersen</span><span class="plot-swatch"><svg width="15" height="15" fill="#ff725c" fill-opacity="0.5" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
30+
</svg>Torgersen</span><span class="plot-swatch"><svg width="15" height="15" fill-opacity="0.5" fill="#ff725c" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
3131
<rect width="100%" height="100%"></rect>
3232
</svg>Biscoe</span>
3333
</div>

test/output/opacityLegend.svg

Lines changed: 5 additions & 1 deletion
Loading

test/output/opacityLegendCSS4.svg

Lines changed: 5 additions & 1 deletion
Loading

test/output/opacityLegendColor.svg

Lines changed: 5 additions & 1 deletion
Loading

test/output/opacityLegendLinear.svg

Lines changed: 5 additions & 1 deletion
Loading

test/output/opacityLegendLog.svg

Lines changed: 5 additions & 1 deletion
Loading

0 commit comments

Comments
 (0)