Skip to content
33 changes: 23 additions & 10 deletions src/legends.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import {select} from "d3";
import {createContext} from "./context.js";
import {legendRamp} from "./legends/ramp.js";
import {isSymbolColorLegend, legendSwatches, legendSymbols} from "./legends/swatches.js";
import {inherit, isScaleOptions} from "./options.js";
import {normalizeScale} from "./scales.js";
import {getFilterId} from "./style.js";

const legendRegistry = new Map([
["symbol", legendSymbols],
Expand Down Expand Up @@ -51,19 +53,30 @@ 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 = "currentColor", ...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) {
return (t) => `color-mix(in srgb, transparent, ${color} ${(t * 100).toFixed(1)}%)`;
function legendOpacity(opacity, {legend = true, color = "currentColor", ...options}) {
if (legend === true) legend = opacity.type === "ordinal" ? "swatches" : "ramp";
opacity = {...opacity, color, key: "opacity"};
switch (`${legend}`.toLowerCase()) {
case "swatches": {
return legendSwatches(opacity, options);
}
case "ramp": {
const legend = legendRamp(opacity, options);
const fid = getFilterId();
const svg = select(legend);
svg.select("image").attr("filter", `url(#${fid})`);
const filter = svg.append("filter").attr("id", fid);
filter.append("feFlood").attr("flood-color", color);
filter.append("feComposite").attr("in2", "SourceGraphic").attr("operator", "in");
return legend;
}
default:
throw new Error(`unknown opacity legend type: ${legend}`);
}
}

export function createLegends(scales, context, options) {
Expand Down
9 changes: 6 additions & 3 deletions src/legends/ramp.js
Original file line number Diff line number Diff line change
Expand Up @@ -91,8 +91,9 @@ export function legendRamp(color, options) {
canvas.width = n;
canvas.height = 1;
const context2 = canvas.getContext("2d", {colorSpace: "display-p3"}); // allow wide gamut
const fillStyle = color.key === "opacity" ? "globalAlpha" : "fillStyle";
for (let i = 0, j = n - 1; i < n; ++i) {
context2.fillStyle = interpolator(i / j);
context2[fillStyle] = interpolator(i / j);
context2.fillRect(i, 0, 1, 1);
}

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

svg
.append("g")
.attr("fill", color.key === "opacity" ? color.color : null)
.attr("fill-opacity", opacity)
.selectAll()
.data(range)
Expand All @@ -129,7 +131,7 @@ export function legendRamp(color, options) {
.attr("y", marginTop)
.attr("width", (d, i) => x(i) - x(i - 1))
.attr("height", height - marginTop - marginBottom)
.attr("fill", (d) => d);
.attr(color.key === "opacity" ? "fill-opacity" : "fill", (d) => d);

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

svg
.append("g")
.attr("fill", color.key === "opacity" ? color.color : null)
.attr("fill-opacity", opacity)
.selectAll()
.data(domain)
Expand All @@ -150,7 +153,7 @@ export function legendRamp(color, options) {
.attr("y", marginTop)
.attr("width", Math.max(0, x.bandwidth() - 1))
.attr("height", height - marginTop - marginBottom)
.attr("fill", scale);
.attr(color.key === "opacity" ? "fill-opacity" : "fill", scale);

tickAdjust = () => {};
}
Expand Down
3 changes: 2 additions & 1 deletion src/legends/swatches.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,9 @@ export function legendSwatches(color, {opacity, ...options} = {}) {
.append("svg")
.attr("width", width)
.attr("height", height)
.attr("fill", scale.scale)
.attr("fill", color.key === "opacity" ? color.color : null)
.attr("fill-opacity", maybeNumberChannel(opacity)[1])
.attr(color.key === "opacity" ? "fill-opacity" : "fill", scale.scale)
.append("rect")
.attr("width", "100%")
.attr("height", "100%")
Expand Down
13 changes: 9 additions & 4 deletions src/scales/ordinal.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -44,13 +44,17 @@ export function createScaleOrdinal(key, channels, {type, interval, domain, range
if (scheme !== undefined) {
if (range !== undefined) {
const interpolate = quantitativeScheme(scheme);
const t0 = range[0],
d = range[1] - range[0];
range = ({length: n}) => quantize((t) => interpolate(t0 + d * t), n);
const t0 = range[0];
const dt = range[1] - range[0];
range = ({length: n}) => quantize((t) => interpolate(t0 + dt * t), n);
} else {
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`);
Expand Down Expand Up @@ -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));
Expand Down
6 changes: 3 additions & 3 deletions test/output/colorLegendOpacity.html
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,11 @@
align-items: center;
margin-right: 1em;
}
</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">
</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">
<rect width="100%" height="100%"></rect>
</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">
</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">
<rect width="100%" height="100%"></rect>
</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">
</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">
<rect width="100%" height="100%"></rect>
</svg>Biscoe</span>
</div>
6 changes: 5 additions & 1 deletion test/output/opacityLegend.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 5 additions & 1 deletion test/output/opacityLegendCSS4.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 5 additions & 1 deletion test/output/opacityLegendColor.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 5 additions & 1 deletion test/output/opacityLegendLinear.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 5 additions & 1 deletion test/output/opacityLegendLog.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 5 additions & 1 deletion test/output/opacityLegendRange.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 5 additions & 1 deletion test/output/opacityLegendSqrt.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
47 changes: 47 additions & 0 deletions test/output/opacityLegendSwatches.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<div class="plot-swatches plot-swatches-wrap">
<style>
:where(.plot-swatches) {
font-family: system-ui, sans-serif;
font-size: 10px;
margin-bottom: 0.5em;
}

:where(.plot-swatch > svg) {
margin-right: 0.5em;
overflow: visible;
}

:where(.plot-swatches-wrap) {
display: flex;
align-items: center;
min-height: 33px;
flex-wrap: wrap;
}

:where(.plot-swatches-wrap .plot-swatch) {
display: inline-flex;
align-items: center;
margin-right: 1em;
}
</style><span class="plot-swatch"><svg width="15" height="15" fill="currentColor" fill-opacity="0" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<rect width="100%" height="100%"></rect>
</svg>0</span><span class="plot-swatch"><svg width="15" height="15" fill="currentColor" fill-opacity="0.111" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<rect width="100%" height="100%"></rect>
</svg>1</span><span class="plot-swatch"><svg width="15" height="15" fill="currentColor" fill-opacity="0.222" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<rect width="100%" height="100%"></rect>
</svg>2</span><span class="plot-swatch"><svg width="15" height="15" fill="currentColor" fill-opacity="0.333" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<rect width="100%" height="100%"></rect>
</svg>3</span><span class="plot-swatch"><svg width="15" height="15" fill="currentColor" fill-opacity="0.444" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<rect width="100%" height="100%"></rect>
</svg>4</span><span class="plot-swatch"><svg width="15" height="15" fill="currentColor" fill-opacity="0.556" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<rect width="100%" height="100%"></rect>
</svg>5</span><span class="plot-swatch"><svg width="15" height="15" fill="currentColor" fill-opacity="0.667" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<rect width="100%" height="100%"></rect>
</svg>6</span><span class="plot-swatch"><svg width="15" height="15" fill="currentColor" fill-opacity="0.778" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<rect width="100%" height="100%"></rect>
</svg>7</span><span class="plot-swatch"><svg width="15" height="15" fill="currentColor" fill-opacity="0.889" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<rect width="100%" height="100%"></rect>
</svg>8</span><span class="plot-swatch"><svg width="15" height="15" fill="currentColor" fill-opacity="1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<rect width="100%" height="100%"></rect>
</svg>9</span>
</div>
Loading