Skip to content

Commit 551f53a

Browse files
mbostockFil
andauthored
marker (#731)
* line marker * link, rule marker * rules lack orientation * fix for constant stroke * minimize diff * document markers * document the interaction with curves * Update README.md * Update README.md * Update README.md Co-authored-by: Philippe Rivière <[email protected]>
1 parent 88ead30 commit 551f53a

File tree

10 files changed

+403
-13
lines changed

10 files changed

+403
-13
lines changed

README.md

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -949,7 +949,7 @@ The **fill** defaults to none. The **stroke** defaults to currentColor if the fi
949949

950950
Points along the line are connected in input order. Likewise, if there are multiple series via the *z*, *fill*, or *stroke* channel, the series are drawn in input order such that the last series is drawn on top. Typically, the data is already in sorted order, such as chronological for time series; if sorting is needed, consider a [sort transform](#transforms).
951951

952-
The line mark supports [curve options](#curves) to control interpolation between points. 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://github.com/d3/d3-shape/blob/master/README.md#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.
952+
The line mark supports [curve options](#curves) to control interpolation between points, and [marker options](#markers) to add a marker (such as a dot or an arrow head) on each of the control points. 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://github.com/d3/d3-shape/blob/master/README.md#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.
953953

954954
#### Plot.line(*data*, *options*)
955955

@@ -992,7 +992,7 @@ For vertical or horizontal links, the **x** option can be specified as shorthand
992992

993993
The link mark supports the [standard mark options](#marks). The **stroke** defaults to currentColor. The **fill** defaults to none. The **strokeWidth** and **strokeMiterlimit** default to one.
994994

995-
The link mark supports [curve options](#curves) to control interpolation between points. Since a link always has two points by definition, only the following curves (or a custom curve) are recommended: *linear*, *step*, *step-after*, *step-before*, *bump-x*, or *bump-y*. Note that the *linear* curve is incapable of showing a fill since a straight line has zero area. For a curved link, you can use a bent [arrow](#arrow) (with no arrowhead, if desired).
995+
The link mark supports [curve options](#curves) to control interpolation between points, and [marker options](#markers) to add a marker (such as a dot or an arrow head) on each of the control points. Since a link always has two points by definition, only the following curves (or a custom curve) are recommended: *linear*, *step*, *step-after*, *step-before*, *bump-x*, or *bump-y*. Note that the *linear* curve is incapable of showing a fill since a straight line has zero area. For a curved link, you can use a bent [arrow](#arrow) (with no arrowhead, if desired).
996996

997997
#### Plot.link(*data*, *options*)
998998

@@ -1896,6 +1896,26 @@ If *curve* is a function, it will be invoked with a given *context* in the same
18961896

18971897
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/master/README.md#curveCardinal_tension); for Catmull–Rom splines, [alpha](https://github.com/d3/d3-shape/blob/master/README.md#curveCatmullRom_alpha).
18981898

1899+
## Markers
1900+
1901+
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:
1902+
1903+
* **markerStart** - the marker for the starting point of a line segment
1904+
* **markerMid** - the marker for any intermediate point of a line segment
1905+
* **markerEnd** - the marker for the end point of a line segment
1906+
* **marker** - shorthand for setting the marker on all points
1907+
1908+
The following named markers are supported:
1909+
1910+
* *none* - no marker (default)
1911+
* *arrow* - an arrowhead
1912+
* *circle*, equivalent to *circle-fill* - a filled circle with a white stroke and 3px radius
1913+
* *circle-stroke* - a hollow circle with a colored stroke and a white fill and 3px radius
1914+
1915+
If *marker* is true, it defaults to *circle*. If *marker* is a function, it will be called with a given *color* and must return an SVG marker element.
1916+
1917+
The primary color of a marker is inherited from the *stroke* of the associated mark. The *arrow* marker is [automatically oriented](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/orient) such that it points in the tangential direction of the path at the position the marker is placed. The *circle* markers are centered around the given vertex. Note that lines whose curve is not *linear* (the default), markers are not necessarily drawn at the data positions given by *x* and *y*; marker placement is determined by the (possibly Bézier) path segments generated by the curve. To ensure that symbols are drawn at a given *x* and *y* position, consider using a [dot](#dot).
1918+
18991919
## Formats
19001920

19011921
These helper functions are provided for use as a *scale*.tickFormat [axis option](#position-options), as the text option for [Plot.text](#plottextdata-options), or for general use. See also [d3-format](https://github.com/d3/d3-format), [d3-time-format](https://github.com/d3/d3-time-format), and JavaScript’s built-in [date formatting](https://observablehq.com/@mbostock/date-formatting) and [number formatting](https://observablehq.com/@mbostock/number-formatting).

src/marks/line.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {defined} from "../defined.js";
44
import {Mark} from "../plot.js";
55
import {indexOf, identity, maybeTuple, maybeZ} from "../options.js";
66
import {applyDirectStyles, applyIndirectStyles, applyTransform, applyGroupedChannelStyles, offset} from "../style.js";
7+
import {applyGroupedMarkers, markers} from "./marker.js";
78

89
const defaults = {
910
ariaLabel: "line",
@@ -27,6 +28,7 @@ export class Line extends Mark {
2728
defaults
2829
);
2930
this.curve = Curve(curve, tension);
31+
markers(this, options);
3032
}
3133
render(I, {x, y}, channels) {
3234
const {x: X, y: Y, z: Z} = channels;
@@ -39,6 +41,7 @@ export class Line extends Mark {
3941
.join("path")
4042
.call(applyDirectStyles, this)
4143
.call(applyGroupedChannelStyles, this, channels)
44+
.call(applyGroupedMarkers, this, channels)
4245
.attr("d", shapeLine()
4346
.curve(this.curve)
4447
.defined(i => defined(X[i]) && defined(Y[i]))

src/marks/link.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import {create, path} from "d3";
22
import {Curve} from "../curve.js";
33
import {Mark} from "../plot.js";
44
import {applyChannelStyles, applyDirectStyles, applyIndirectStyles, applyTransform, offset} from "../style.js";
5+
import {markers, applyMarkers} from "./marker.js";
56

67
const defaults = {
78
ariaLabel: "link",
@@ -25,6 +26,7 @@ export class Link extends Mark {
2526
defaults
2627
);
2728
this.curve = Curve(curve, tension);
29+
markers(this, options);
2830
}
2931
render(index, {x, y}, channels) {
3032
const {x1: X1, y1: Y1, x2: X2 = X1, y2: Y2 = Y1} = channels;
@@ -45,7 +47,8 @@ export class Link extends Mark {
4547
c.lineEnd();
4648
return p;
4749
})
48-
.call(applyChannelStyles, this, channels))
50+
.call(applyChannelStyles, this, channels)
51+
.call(applyMarkers, this, channels))
4952
.node();
5053
}
5154
}

src/marks/marker.js

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import {create} from "d3";
2+
3+
export function markers(mark, {
4+
marker,
5+
markerStart = marker,
6+
markerMid = marker,
7+
markerEnd = marker
8+
} = {}) {
9+
mark.markerStart = maybeMarker(markerStart);
10+
mark.markerMid = maybeMarker(markerMid);
11+
mark.markerEnd = maybeMarker(markerEnd);
12+
}
13+
14+
function maybeMarker(marker) {
15+
if (marker == null || marker === false) return null;
16+
if (marker === true) return markerCircleFill;
17+
if (typeof marker === "function") return marker;
18+
switch (`${marker}`.toLowerCase()) {
19+
case "none": return null;
20+
case "arrow": return markerArrow;
21+
case "circle": case "circle-fill": return markerCircleFill;
22+
case "circle-stroke": return markerCircleStroke;
23+
}
24+
throw new Error(`invalid marker: ${marker}`);
25+
}
26+
27+
function markerArrow(color) {
28+
return create("svg:marker")
29+
.attr("viewBox", "-5 -5 10 10")
30+
.attr("markerWidth", 6.67)
31+
.attr("markerHeight", 6.67)
32+
.attr("orient", "auto")
33+
.attr("fill", "none")
34+
.attr("stroke", color)
35+
.attr("stroke-width", 1.5)
36+
.attr("stroke-linecap", "round")
37+
.attr("stroke-linejoin", "round")
38+
.call(marker => marker.append("path").attr("d", "M-1.5,-3l3,3l-3,3"))
39+
.node();
40+
}
41+
42+
function markerCircleFill(color) {
43+
return create("svg:marker")
44+
.attr("viewBox", "-5 -5 10 10")
45+
.attr("markerWidth", 6.67)
46+
.attr("markerHeight", 6.67)
47+
.attr("fill", color)
48+
.attr("stroke", "white")
49+
.attr("stroke-width", 1.5)
50+
.call(marker => marker.append("circle").attr("r", 3))
51+
.node();
52+
}
53+
54+
function markerCircleStroke(color) {
55+
return create("svg:marker")
56+
.attr("viewBox", "-5 -5 10 10")
57+
.attr("markerWidth", 6.67)
58+
.attr("markerHeight", 6.67)
59+
.attr("fill", "white")
60+
.attr("stroke", color)
61+
.attr("stroke-width", 1.5)
62+
.call(marker => marker.append("circle").attr("r", 3))
63+
.node();
64+
}
65+
66+
let nextMarkerId = 0;
67+
68+
export function applyMarkers(path, mark, {stroke: S}) {
69+
return applyMarkersColor(path, mark, S && (i => S[i]));
70+
}
71+
72+
export function applyGroupedMarkers(path, mark, {stroke: S}) {
73+
return applyMarkersColor(path, mark, S && (([i]) => S[i]));
74+
}
75+
76+
function applyMarkersColor(path, {markerStart, markerMid, markerEnd, stroke}, strokeof = () => stroke) {
77+
const iriByMarkerColor = new Map();
78+
79+
function applyMarker(marker) {
80+
return function(i) {
81+
const color = strokeof(i);
82+
let iriByColor = iriByMarkerColor.get(marker);
83+
if (!iriByColor) iriByMarkerColor.set(marker, iriByColor = new Map());
84+
let iri = iriByColor.get(color);
85+
if (!iri) {
86+
const node = this.parentNode.insertBefore(marker(color), this);
87+
const id = `plot-marker-${++nextMarkerId}`;
88+
node.setAttribute("id", id);
89+
iriByColor.set(color, iri = `url(#${id})`);
90+
}
91+
return iri;
92+
};
93+
}
94+
95+
if (markerStart) path.attr("marker-start", applyMarker(markerStart));
96+
if (markerMid) path.attr("marker-mid", applyMarker(markerMid));
97+
if (markerEnd) path.attr("marker-end", applyMarker(markerEnd));
98+
}

test/output/crimeanWarArrow.svg

Lines changed: 103 additions & 0 deletions
Loading

0 commit comments

Comments
 (0)