Skip to content

marker #731

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Feb 1, 2022
Merged
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
24 changes: 22 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -949,7 +949,7 @@ The **fill** defaults to none. The **stroke** defaults to currentColor if the fi

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).

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.
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.

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

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

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.

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).
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).

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

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

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).

## Markers

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:

* **markerStart** - the marker for the starting point of a line segment
* **markerMid** - the marker for any intermediate point of a line segment
* **markerEnd** - the marker for the end point of a line segment
* **marker** - shorthand for setting the marker on all points

The following named markers are supported:

* *none* - no marker (default)
* *arrow* - an arrowhead
* *circle*, equivalent to *circle-fill* - a filled circle with a white stroke and 3px radius
* *circle-stroke* - a hollow circle with a colored stroke and a white fill and 3px radius

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.

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).

## Formats

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).
Expand Down
3 changes: 3 additions & 0 deletions src/marks/line.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {defined} from "../defined.js";
import {Mark} from "../plot.js";
import {indexOf, identity, maybeTuple, maybeZ} from "../options.js";
import {applyDirectStyles, applyIndirectStyles, applyTransform, applyGroupedChannelStyles, offset} from "../style.js";
import {applyGroupedMarkers, markers} from "./marker.js";

const defaults = {
ariaLabel: "line",
Expand All @@ -27,6 +28,7 @@ export class Line extends Mark {
defaults
);
this.curve = Curve(curve, tension);
markers(this, options);
}
render(I, {x, y}, channels) {
const {x: X, y: Y, z: Z} = channels;
Expand All @@ -39,6 +41,7 @@ export class Line extends Mark {
.join("path")
.call(applyDirectStyles, this)
.call(applyGroupedChannelStyles, this, channels)
.call(applyGroupedMarkers, this, channels)
.attr("d", shapeLine()
.curve(this.curve)
.defined(i => defined(X[i]) && defined(Y[i]))
Expand Down
5 changes: 4 additions & 1 deletion src/marks/link.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {create, path} from "d3";
import {Curve} from "../curve.js";
import {Mark} from "../plot.js";
import {applyChannelStyles, applyDirectStyles, applyIndirectStyles, applyTransform, offset} from "../style.js";
import {markers, applyMarkers} from "./marker.js";

const defaults = {
ariaLabel: "link",
Expand All @@ -25,6 +26,7 @@ export class Link extends Mark {
defaults
);
this.curve = Curve(curve, tension);
markers(this, options);
}
render(index, {x, y}, channels) {
const {x1: X1, y1: Y1, x2: X2 = X1, y2: Y2 = Y1} = channels;
Expand All @@ -45,7 +47,8 @@ export class Link extends Mark {
c.lineEnd();
return p;
})
.call(applyChannelStyles, this, channels))
.call(applyChannelStyles, this, channels)
.call(applyMarkers, this, channels))
.node();
}
}
Expand Down
98 changes: 98 additions & 0 deletions src/marks/marker.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import {create} from "d3";

export function markers(mark, {
marker,
markerStart = marker,
markerMid = marker,
markerEnd = marker
} = {}) {
mark.markerStart = maybeMarker(markerStart);
mark.markerMid = maybeMarker(markerMid);
mark.markerEnd = maybeMarker(markerEnd);
}

function maybeMarker(marker) {
if (marker == null || marker === false) return null;
if (marker === true) return markerCircleFill;
if (typeof marker === "function") return marker;
switch (`${marker}`.toLowerCase()) {
case "none": return null;
case "arrow": return markerArrow;
case "circle": case "circle-fill": return markerCircleFill;
case "circle-stroke": return markerCircleStroke;
}
throw new Error(`invalid marker: ${marker}`);
}

function markerArrow(color) {
return create("svg:marker")
.attr("viewBox", "-5 -5 10 10")
.attr("markerWidth", 6.67)
.attr("markerHeight", 6.67)
.attr("orient", "auto")
.attr("fill", "none")
.attr("stroke", color)
.attr("stroke-width", 1.5)
.attr("stroke-linecap", "round")
.attr("stroke-linejoin", "round")
.call(marker => marker.append("path").attr("d", "M-1.5,-3l3,3l-3,3"))
.node();
}

function markerCircleFill(color) {
return create("svg:marker")
.attr("viewBox", "-5 -5 10 10")
.attr("markerWidth", 6.67)
.attr("markerHeight", 6.67)
.attr("fill", color)
.attr("stroke", "white")
.attr("stroke-width", 1.5)
.call(marker => marker.append("circle").attr("r", 3))
.node();
}

function markerCircleStroke(color) {
return create("svg:marker")
.attr("viewBox", "-5 -5 10 10")
.attr("markerWidth", 6.67)
.attr("markerHeight", 6.67)
.attr("fill", "white")
.attr("stroke", color)
.attr("stroke-width", 1.5)
.call(marker => marker.append("circle").attr("r", 3))
.node();
}

let nextMarkerId = 0;

export function applyMarkers(path, mark, {stroke: S}) {
return applyMarkersColor(path, mark, S && (i => S[i]));
}

export function applyGroupedMarkers(path, mark, {stroke: S}) {
return applyMarkersColor(path, mark, S && (([i]) => S[i]));
}

function applyMarkersColor(path, {markerStart, markerMid, markerEnd, stroke}, strokeof = () => stroke) {
const iriByMarkerColor = new Map();

function applyMarker(marker) {
return function(i) {
const color = strokeof(i);
let iriByColor = iriByMarkerColor.get(marker);
if (!iriByColor) iriByMarkerColor.set(marker, iriByColor = new Map());
let iri = iriByColor.get(color);
if (!iri) {
const node = this.parentNode.insertBefore(marker(color), this);
const id = `plot-marker-${++nextMarkerId}`;
node.setAttribute("id", id);
iriByColor.set(color, iri = `url(#${id})`);
}
return iri;
};
}

if (markerStart) path.attr("marker-start", applyMarker(markerStart));
if (markerMid) path.attr("marker-mid", applyMarker(markerMid));
if (markerEnd) path.attr("marker-end", applyMarker(markerEnd));
}
103 changes: 103 additions & 0 deletions test/output/crimeanWarArrow.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading