Skip to content

Commit e891948

Browse files
committed
marker.ts; changing the API for marker
the second argument for markers has not been documented yet; changing it from *context* = {document} to *document*
1 parent 2c64654 commit e891948

File tree

3 files changed

+78
-29
lines changed

3 files changed

+78
-29
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2417,7 +2417,7 @@ The following named markers are supported:
24172417
* *circle*, equivalent to *circle-fill* - a filled circle with a white stroke and 3px radius
24182418
* *circle-stroke* - a hollow circle with a colored stroke and a white fill and 3px radius
24192419
2420-
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.
2420+
If *marker* is true, it defaults to *circle*. If *marker* is a function, it will be called with a given *color* and *document* and must return an SVG marker element.
24212421
24222422
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).
24232423

src/api.ts

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ type ScalesOptions = {
6868
/**
6969
* An instanciated mark
7070
*/
71-
type InstanciatedMark = {
71+
export type InstanciatedMark = {
7272
initialize: (data: Data) => void;
7373

7474
z?: ChannelOption; // copy the user option for error messages
@@ -242,14 +242,23 @@ export type LineOptions = MarkOptions & MarkerOptions;
242242
* A marker defines a graphic drawn on vertices of a delaunay, line or a link mark
243243
* @link https://github.com/observablehq/plot/blob/main/README.md#markers
244244
*/
245-
type MarkerOption = string | boolean | null | undefined;
246-
type MarkerOptions = {
245+
export type MarkerOption =
246+
| "none"
247+
| "arrow"
248+
| "dot"
249+
| "circle"
250+
| "circle-stroke"
251+
| MarkerFunction
252+
| boolean
253+
| null
254+
| undefined;
255+
export type MarkerOptions = {
247256
marker?: MarkerOption;
248257
markerStart?: MarkerOption;
249258
markerMid?: MarkerOption;
250259
markerEnd?: MarkerOption;
251260
};
252-
type MarkerFunction = (color: any, context: any) => SVGElement;
261+
export type MarkerFunction = (color: string, document: Context["document"]) => SVGElement;
253262
type MaybeMarkerFunction = MarkerFunction | null;
254263

255264
/**
@@ -428,3 +437,18 @@ export type ArrayType = ArrayConstructor | Float32ArrayConstructor | Float64Arra
428437
* Type a Datum as an object, when using the field accessor
429438
*/
430439
export type GenericObject = Record<string, ChannelValue>;
440+
441+
/**
442+
* A restrictive definition of D3 selections
443+
*/
444+
export type Selection = {
445+
append: (name: string) => Selection;
446+
attr: (name: string, value: any) => Selection;
447+
call: (callback: (selection: Selection, ...args: any[]) => void, ...args: any[]) => Selection;
448+
each: (callback: (d: any) => void) => Selection;
449+
filter: (filter: (d: any, i: number) => boolean) => Selection;
450+
property: (name: string, value: any) => Selection;
451+
style: (name: string, value: any) => Selection;
452+
text: (value: any) => Selection;
453+
[Symbol.iterator]: () => IterableIterator<SVGElement | HTMLElement>;
454+
};

src/marks/marker.ts

Lines changed: 49 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,26 @@
1+
import type {
2+
Context,
3+
index,
4+
InstanciatedMark,
5+
MarkerFunction,
6+
MarkerOption,
7+
MarkerOptions,
8+
Selection,
9+
Series
10+
} from "../api.js";
11+
112
import {create} from "../context.js";
213

3-
export function markers(mark, {marker, markerStart = marker, markerMid = marker, markerEnd = marker} = {}) {
14+
export function markers(
15+
mark: InstanciatedMark,
16+
{marker, markerStart = marker, markerMid = marker, markerEnd = marker}: MarkerOptions = {}
17+
) {
418
mark.markerStart = maybeMarker(markerStart);
519
mark.markerMid = maybeMarker(markerMid);
620
mark.markerEnd = maybeMarker(markerEnd);
721
}
822

9-
function maybeMarker(marker) {
23+
function maybeMarker(marker: MarkerOption) {
1024
if (marker == null || marker === false) return null;
1125
if (marker === true) return markerCircleFill;
1226
if (typeof marker === "function") return marker;
@@ -26,8 +40,8 @@ function maybeMarker(marker) {
2640
throw new Error(`invalid marker: ${marker}`);
2741
}
2842

29-
function markerArrow(color, context) {
30-
return create("svg:marker", context)
43+
function markerArrow(color: string, document: Context["document"]) {
44+
return create("svg:marker", {document})
3145
.attr("viewBox", "-5 -5 10 10")
3246
.attr("markerWidth", 6.67)
3347
.attr("markerHeight", 6.67)
@@ -38,66 +52,77 @@ function markerArrow(color, context) {
3852
.attr("stroke-linecap", "round")
3953
.attr("stroke-linejoin", "round")
4054
.call((marker) => marker.append("path").attr("d", "M-1.5,-3l3,3l-3,3"))
41-
.node();
55+
.node() as SVGElement;
4256
}
4357

44-
function markerDot(color, context) {
45-
return create("svg:marker", context)
58+
function markerDot(color: string, document: Context["document"]) {
59+
return create("svg:marker", {document})
4660
.attr("viewBox", "-5 -5 10 10")
4761
.attr("markerWidth", 6.67)
4862
.attr("markerHeight", 6.67)
4963
.attr("fill", color)
5064
.attr("stroke", "none")
5165
.call((marker) => marker.append("circle").attr("r", 2.5))
52-
.node();
66+
.node() as SVGElement;
5367
}
5468

55-
function markerCircleFill(color, context) {
56-
return create("svg:marker", context)
69+
function markerCircleFill(color: string, document: Context["document"]) {
70+
return create("svg:marker", {document})
5771
.attr("viewBox", "-5 -5 10 10")
5872
.attr("markerWidth", 6.67)
5973
.attr("markerHeight", 6.67)
6074
.attr("fill", color)
6175
.attr("stroke", "white")
6276
.attr("stroke-width", 1.5)
6377
.call((marker) => marker.append("circle").attr("r", 3))
64-
.node();
78+
.node() as SVGElement;
6579
}
6680

67-
function markerCircleStroke(color, context) {
68-
return create("svg:marker", context)
81+
function markerCircleStroke(color: string, document: Context["document"]) {
82+
return create("svg:marker", {document})
6983
.attr("viewBox", "-5 -5 10 10")
7084
.attr("markerWidth", 6.67)
7185
.attr("markerHeight", 6.67)
7286
.attr("fill", "white")
7387
.attr("stroke", color)
7488
.attr("stroke-width", 1.5)
7589
.call((marker) => marker.append("circle").attr("r", 3))
76-
.node();
90+
.node() as SVGElement;
7791
}
7892

7993
let nextMarkerId = 0;
8094

81-
export function applyMarkers(path, mark, {stroke: S} = {}) {
82-
return applyMarkersColor(path, mark, S && ((i) => S[i]));
95+
export function applyMarkers(path: Selection, mark: InstanciatedMark, {stroke: S}: {stroke?: string[]} = {}) {
96+
return applyMarkersColor(path, mark, S && ((i: index) => S[i]));
8397
}
8498

85-
export function applyGroupedMarkers(path, mark, {stroke: S} = {}) {
86-
return applyMarkersColor(path, mark, S && (([i]) => S[i]));
99+
export function applyGroupedMarkers(path: Selection, mark: InstanciatedMark, {stroke: S}: {stroke?: string[]} = {}) {
100+
return applyMarkersColor(path, mark, S && (([i]: Series) => S[i]));
87101
}
88102

89-
function applyMarkersColor(path, {markerStart, markerMid, markerEnd, stroke}, strokeof = () => stroke) {
90-
const iriByMarkerColor = new Map();
103+
// we're cheating typescript here by using index & Series:
104+
// the stroke function can be applied either on an individual or on a grouped mark;
105+
// the datum on the path will be either an index or a Series.
106+
// The stroke is either a color channel or a (non nullish) string constant.
107+
type StrokeAttr = (i: index & Series) => string;
108+
type IriColorMap = Map<string | null | undefined, string | undefined>;
109+
110+
function applyMarkersColor(
111+
path: Selection,
112+
{markerStart, markerMid, markerEnd, stroke}: InstanciatedMark,
113+
strokeof: StrokeAttr = () => stroke as string
114+
) {
115+
const iriByMarkerColor = new Map<MarkerFunction, IriColorMap>();
91116

92-
function applyMarker(marker) {
93-
return function (i) {
117+
function applyMarker(marker: MarkerFunction) {
118+
return function (this: SVGElement, i: index & Series) {
94119
const color = strokeof(i);
95120
let iriByColor = iriByMarkerColor.get(marker);
96121
if (!iriByColor) iriByMarkerColor.set(marker, (iriByColor = new Map()));
97122
let iri = iriByColor.get(color);
98123
if (!iri) {
99-
const context = {document: this.ownerDocument};
100-
const node = this.parentNode.insertBefore(marker(color, context), this);
124+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
125+
const node = this.parentNode!.insertBefore(marker(color, this.ownerDocument), this);
101126
const id = `plot-marker-${++nextMarkerId}`;
102127
node.setAttribute("id", id);
103128
iriByColor.set(color, (iri = `url(#${id})`));

0 commit comments

Comments
 (0)