Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions docs/marks/line.md
Original file line number Diff line number Diff line change
Expand Up @@ -361,6 +361,12 @@ Points along the line are connected in input order. Likewise, if there are multi

The line mark supports [curve options](../features/curves.md) to control interpolation between points, and [marker options](../features/markers.md) to add a marker (such as a dot or an arrowhead) on each of the control points. The default curve is *auto*, which is equivalent to *linear* if there is no [projection](../features/projections.md), and otherwise uses the associated projection. If any of the **x** or **y** values are invalid (undefined, null, or NaN), the line will be interrupted, resulting in a break that divides the line shape into multiple segments. (See [d3-shape’s *line*.defined](https://d3js.org/d3-shape/line#line_defined) for more.) If a line segment consists of only a single point, it may appear invisible unless rendered with rounded or square line caps. In addition, some curves such as *cardinal-open* only render a visible segment if it contains multiple points.

The line mark supports a **halo** option that draws an outline around each series, increasing legibility when lines overlap. The following halo options are supported:

* **halo** - if true, draws a halo; if a color, sets the halo color; if a number, sets the halo radius
* **haloColor** - the halo color; defaults to *var(--plot-background)*
* **haloRadius** - the halo radius in pixels; defaults to 2

## line(*data*, *options*) {#line}

```js
Expand Down
39 changes: 39 additions & 0 deletions src/marks/halo.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import {isColor} from "../options.js";

const defaultColor = "var(--plot-background)";
const defaultRadius = 2;

let nextHaloId = 0;

function getHaloId() {
return `plot-halo-${++nextHaloId}`;
}

export function applyHalo(selection, {halo}) {
if (!halo) return null;
const {color, radius} = halo;
const id = getHaloId();
const filter = selection.append("filter").attr("id", id);
filter
.append("feMorphology")
.attr("in", "SourceAlpha")
.attr("result", "dilated")
.attr("operator", "dilate")
.attr("radius", radius);
filter.append("feFlood").style("flood-color", color);
filter.append("feComposite").attr("in2", "dilated").attr("operator", "in");
const merge = filter.append("feMerge");
merge.append("feMergeNode");
merge.append("feMergeNode").attr("in", "SourceGraphic");
return `url(#${id})`;
}

export function maybeHalo(halo, color, radius) {
if (halo === undefined) halo = color !== undefined || radius !== undefined;
if (!halo) return false;
if (color === undefined) color = isColor(halo) ? halo : defaultColor;
else if (!isColor(color)) throw new Error(`Unsupported halo color: ${color}`);
if (radius === undefined) radius = typeof halo === "number" && !isNaN(halo) ? halo : defaultRadius;
else if (isNaN(+radius)) throw new Error(`Unsupported halo radius: ${radius}`);
return {color, radius};
}
17 changes: 17 additions & 0 deletions src/marks/line.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,23 @@ export interface LineOptions extends MarkOptions, MarkerOptions, CurveAutoOption
* **fill** if a channel, or **stroke** if a channel.
*/
z?: ChannelValue;

/**
* Draw a halo around the line to help separate overlapping lines. If true,
* draws a halo with the plot background color and a 2px radius. If a color,
* uses that color. If a number, uses that radius.
*/
halo?: boolean | string | number;

/**
* The halo’s color; defaults to background color.
*/
haloColor?: string;

/**
* The halo’s radius in pixels; defaults to 2.
*/
haloRadius?: number;
}

/** Options for the lineX mark. */
Expand Down
62 changes: 38 additions & 24 deletions src/marks/line.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {line as shapeLine} from "d3";
import {group, line as shapeLine} from "d3";
import {create} from "../context.js";
import {curveAuto, maybeCurveAuto} from "../curve.js";
import {Mark} from "../mark.js";
Expand All @@ -12,6 +12,7 @@ import {
groupIndex
} from "../style.js";
import {maybeDenseIntervalX, maybeDenseIntervalY} from "../transforms/bin.js";
import {applyHalo, maybeHalo} from "./halo.js";

const defaults = {
ariaLabel: "line",
Expand All @@ -25,7 +26,7 @@ const defaults = {

export class Line extends Mark {
constructor(data, options = {}) {
const {x, y, z, curve, tension} = options;
const {x, y, z, curve, tension, halo, haloColor, haloRadius} = options;
super(
data,
{
Expand All @@ -38,6 +39,7 @@ export class Line extends Mark {
);
this.z = z;
this.curve = maybeCurveAuto(curve, tension);
this.halo = maybeHalo(halo, haloColor, haloRadius);
markers(this, options);
}
filter(index) {
Expand All @@ -50,32 +52,44 @@ export class Line extends Mark {
}
}
render(index, scales, channels, dimensions, context) {
const {x: X, y: Y} = channels;
const {x: X, y: Y, z: Z} = channels;
const {curve} = this;
return create("svg:g", context)
const g = create("svg:g", context)
.call(applyIndirectStyles, this, dimensions, context)
.call(applyTransform, this, scales)
.call((g) =>
g
.call(applyTransform, this, scales);

// When adding a halo to multiple series, nest by series so each
// gets its own halo filter; otherwise render paths directly into g.
const filter = applyHalo(g, this);
const segments = groupIndex(index, [X, Y], this, channels);
(this.halo && Z
? g
.selectAll()
.data(groupIndex(index, [X, Y], this, channels))
.data(group(segments, (I) => Z[I.find((i) => i >= 0)]))
.enter()
.append("path")
.call(applyDirectStyles, this)
.call(applyGroupedChannelStyles, this, channels)
.call(applyGroupedMarkers, this, channels, context)
.attr(
"d",
curve === curveAuto && context.projection
? sphereLine(context.path(), X, Y)
: shapeLine()
.curve(curve)
.defined((i) => i >= 0)
.x((i) => X[i])
.y((i) => Y[i])
)
)
.node();
.append("g")
: g.datum([, segments])
)
.attr("filter", filter)
.selectAll()
.data(([, d]) => d)
.enter()
.append("path")
.call(applyDirectStyles, this)
.call(applyGroupedChannelStyles, this, channels)
.call(applyGroupedMarkers, this, channels, context)
.attr(
"d",
curve === curveAuto && context.projection
? sphereLine(context.path(), X, Y)
: shapeLine()
.curve(curve)
.defined((i) => i >= 0)
.x((i) => X[i])
.y((i) => Y[i])
);

return g.node();
}
}

Expand Down
Loading
Loading