Skip to content

Commit 4f7ecbd

Browse files
committed
line halo
1 parent f132711 commit 4f7ecbd

11 files changed

Lines changed: 917 additions & 10 deletions

File tree

README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3257,6 +3257,15 @@ If *curve* is a function, it will be invoked with a given *context* in the same
32573257
32583258
The tension option only has an effect on cardinal and Catmull–Rom splines (*cardinal*, *cardinal-open*, *cardinal-closed*, *catmull-rom*, *catmull-rom-open*, and *catmull-rom-closed*). For cardinal splines, it corresponds to [tension](https://github.com/d3/d3-shape/blob/main/README.md#curveCardinal_tension); for Catmull–Rom splines, [alpha](https://github.com/d3/d3-shape/blob/main/README.md#curveCatmullRom_alpha).
32593259
3260+
3261+
## Halo
3262+
3263+
The line mark support a halo filter, allowing to better separate multiple lines by adding a thick white background underneath each line, [a technique described by Sara Soueidan](https://tympanus.net/codrops/2019/01/22/svg-filter-effects-outline-text-with-femorphology/). The halo options can be specified as:
3264+
3265+
* *haloColor* - the halo’s color, defaults to white
3266+
* *haloRadius* - the halo’s radius, which defaults to 2px
3267+
* *halo* - if true, activates the halo filter; if specified as a color, defines the halo’s color; if specified as a number, defines the halo’s radius
3268+
32603269
## Markers
32613270
32623271
A [marker](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/marker) defines a graphic drawn on vertices of a [line](#line) or a [link](#link) mark. The supported marker options are:

src/marks/halo.js

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import {isColor} from "../options.js";
2+
3+
let nextHaloId = 0;
4+
5+
export function applyHalo(g, {color, radius}) {
6+
const id = `plot-linehalo-${nextHaloId++}`;
7+
g.selectChildren().style("filter", `url(#${id})`);
8+
g.append("filter").attr("id", id).html(`
9+
<feMorphology in="SourceAlpha" result="DILATED" operator="dilate" radius="${radius}"></feMorphology>
10+
<feFlood flood-color="${color}" result="BG"></feFlood>
11+
<feComposite in="BG" in2="DILATED" operator="in" result="OUTLINE"></feComposite>
12+
<feMerge>
13+
<feMergeNode in="OUTLINE" />
14+
<feMergeNode in="SourceGraphic" />
15+
</feMerge>`);
16+
}
17+
18+
export function maybeHalo(halo, color, radius) {
19+
if (halo === undefined) halo = color !== undefined || radius !== undefined;
20+
if (!halo) return false;
21+
const defaults = {color: "white", radius: 2};
22+
if (color === undefined) {
23+
color = isColor(halo) ? halo : defaults.color;
24+
} else if (!isColor(color)) {
25+
throw new Error(`Unsupported halo color: ${color}`);
26+
}
27+
if (radius === undefined) {
28+
radius = !isNaN(+halo) ? +halo : defaults.radius;
29+
} else if (isNaN(+radius)) {
30+
throw new Error(`Unsupported halo radius: ${radius}`);
31+
}
32+
return {color, radius};
33+
}

src/marks/line.js

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {line as shapeLine} from "d3";
1+
import {group, line as shapeLine} from "d3";
22
import {create} from "../context.js";
33
import {Curve} from "../curve.js";
44
import {indexOf, identity, maybeTuple, maybeZ} from "../options.js";
@@ -12,6 +12,7 @@ import {
1212
} from "../style.js";
1313
import {maybeDenseIntervalX, maybeDenseIntervalY} from "../transforms/bin.js";
1414
import {applyGroupedMarkers, markers} from "./marker.js";
15+
import {applyHalo, maybeHalo} from "./halo.js";
1516

1617
const defaults = {
1718
ariaLabel: "line",
@@ -25,7 +26,7 @@ const defaults = {
2526

2627
export class Line extends Mark {
2728
constructor(data, options = {}) {
28-
const {x, y, z, curve, tension} = options;
29+
const {x, y, z, curve, tension, halo, haloColor, haloRadius} = options;
2930
super(
3031
data,
3132
{
@@ -38,14 +39,15 @@ export class Line extends Mark {
3839
);
3940
this.z = z;
4041
this.curve = Curve(curve, tension);
42+
this.halo = maybeHalo(halo, haloColor, haloRadius);
4143
markers(this, options);
4244
}
4345
filter(index) {
4446
return index;
4547
}
4648
render(index, scales, channels, dimensions, context) {
4749
const {x: X, y: Y} = channels;
48-
return create("svg:g", context)
50+
const g = create("svg:g", context)
4951
.call(applyIndirectStyles, this, scales, dimensions)
5052
.call(applyTransform, this, scales)
5153
.call((g) =>
@@ -65,8 +67,25 @@ export class Line extends Mark {
6567
.x((i) => X[i])
6668
.y((i) => Y[i])
6769
)
68-
)
69-
.node();
70+
);
71+
72+
if (this.halo) {
73+
// With variable aesthetics, we need to regroup segments by line
74+
let line = -1;
75+
let segmented = false;
76+
const groups = group(g.selectAll("path"), (d) =>
77+
d.__data__.segment === undefined ? ++line : ((segmented = true), line)
78+
);
79+
if (segmented) {
80+
for (const [, paths] of groups) {
81+
const l = g.append("g").node();
82+
for (const p of paths) l.appendChild(p);
83+
}
84+
}
85+
applyHalo(g, this.halo);
86+
}
87+
88+
return g.node();
7089
}
7190
}
7291

src/style.js

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,7 @@ export function* groupIndex(I, position, {z}, channels) {
248248
for (const G of Z ? groupZ(I, Z, z) : [I]) {
249249
let Ag; // the A-values (aesthetics) of the current group, if any
250250
let Gg; // the current group index (a subset of G, and I), if any
251+
let segment = 0; // counter of sub-groups for the current z
251252
out: for (const i of G) {
252253
// If any channel has an undefined value for this index, skip it.
253254
for (const c of C) {
@@ -260,8 +261,9 @@ export function* groupIndex(I, position, {z}, channels) {
260261
// Otherwise, if this is a new group, record the aesthetics for this
261262
// group. Yield the current group and start a new one.
262263
if (Ag === undefined) {
263-
if (Gg) yield Gg;
264-
(Ag = A.map((c) => keyof(c[i]))), (Gg = [i]);
264+
if (Gg) segment++ ? Object.assign(Gg, {segment}) : Gg;
265+
Ag = A.map((c) => keyof(c[i]));
266+
Gg = [i];
265267
continue;
266268
}
267269

@@ -272,15 +274,16 @@ export function* groupIndex(I, position, {z}, channels) {
272274
for (let j = 0; j < A.length; ++j) {
273275
const k = keyof(A[j][i]);
274276
if (k !== Ag[j]) {
275-
yield Gg;
276-
(Ag = A.map((c) => keyof(c[i]))), (Gg = [i]);
277+
yield segment++ ? Object.assign(Gg, {segment}) : Gg;
278+
Ag = A.map((c) => keyof(c[i]));
279+
Gg = [i];
277280
continue out;
278281
}
279282
}
280283
}
281284

282285
// Yield the current group, if any.
283-
if (Gg) yield Gg;
286+
if (Gg) yield segment++ ? Object.assign(Gg, {segment}) : Gg;
284287
}
285288
}
286289

test/data/README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,11 @@ https://observablehq.com/@moklick
118118
TSA
119119
https://www.tsa.gov/coronavirus/passenger-throughput
120120

121+
## us-gdp.csv
122+
U.S. Gross Domestic Product, chained dollars, Jan 1947-Jan 2022
123+
Federal Reserve Economic Data
124+
https://fred.stlouisfed.org/
125+
121126
## us-population-state-age.csv
122127
U.S. Census Bureau
123128
https://observablehq.com/@d3/barcode-plot

0 commit comments

Comments
 (0)