insomni-plot 0.1.0-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +674 -0
- package/README.md +81 -0
- package/dist/core.d.mts +340 -0
- package/dist/core.mjs +1047 -0
- package/dist/index.d.mts +3426 -0
- package/dist/index.mjs +12762 -0
- package/dist/interactions-DEFL_F4E.mjs +5395 -0
- package/dist/range-presets-CzECsu3V.d.mts +1523 -0
- package/package.json +34 -0
- package/src/annotations.d.ts +121 -0
- package/src/annotations.ts +438 -0
- package/src/axis.d.ts +184 -0
- package/src/axis.test.ts +131 -0
- package/src/axis.ts +765 -0
- package/src/colorbar.d.ts +69 -0
- package/src/colorbar.ts +294 -0
- package/src/colors.d.ts +57 -0
- package/src/colors.test.ts +28 -0
- package/src/colors.ts +486 -0
- package/src/core.ts +299 -0
- package/src/format.d.ts +54 -0
- package/src/format.ts +138 -0
- package/src/grammar/accessibility.d.ts +147 -0
- package/src/grammar/accessibility.test.ts +199 -0
- package/src/grammar/accessibility.ts +443 -0
- package/src/grammar/aes.d.ts +35 -0
- package/src/grammar/aes.test.ts +75 -0
- package/src/grammar/aes.ts +120 -0
- package/src/grammar/annotations.d.ts +86 -0
- package/src/grammar/annotations.test.ts +68 -0
- package/src/grammar/annotations.ts +336 -0
- package/src/grammar/attach-brush.d.ts +44 -0
- package/src/grammar/attach-brush.test.ts +214 -0
- package/src/grammar/attach-brush.ts +111 -0
- package/src/grammar/attach-presets.d.ts +33 -0
- package/src/grammar/attach-presets.test.ts +106 -0
- package/src/grammar/attach-presets.ts +215 -0
- package/src/grammar/chart.d.ts +952 -0
- package/src/grammar/chart.test.ts +118 -0
- package/src/grammar/chart.ts +1172 -0
- package/src/grammar/color-utils.d.ts +29 -0
- package/src/grammar/color-utils.test.ts +53 -0
- package/src/grammar/color-utils.ts +66 -0
- package/src/grammar/constants.d.ts +45 -0
- package/src/grammar/constants.ts +61 -0
- package/src/grammar/coord.d.ts +183 -0
- package/src/grammar/coord.test.ts +355 -0
- package/src/grammar/coord.ts +619 -0
- package/src/grammar/data/pivot.d.ts +57 -0
- package/src/grammar/data/pivot.ts +107 -0
- package/src/grammar/emphasis-driver.d.ts +69 -0
- package/src/grammar/emphasis-driver.test.ts +199 -0
- package/src/grammar/emphasis-driver.ts +205 -0
- package/src/grammar/equality.d.ts +3 -0
- package/src/grammar/equality.ts +40 -0
- package/src/grammar/facet.d.ts +63 -0
- package/src/grammar/facet.test.ts +60 -0
- package/src/grammar/facet.ts +175 -0
- package/src/grammar/geoms/_categorical.d.ts +94 -0
- package/src/grammar/geoms/_categorical.ts +0 -0
- package/src/grammar/geoms/_distribution.d.ts +52 -0
- package/src/grammar/geoms/_distribution.ts +125 -0
- package/src/grammar/geoms/_mark.d.ts +69 -0
- package/src/grammar/geoms/_mark.ts +136 -0
- package/src/grammar/geoms/_shape.d.ts +41 -0
- package/src/grammar/geoms/_shape.ts +74 -0
- package/src/grammar/geoms/aggregate.d.ts +95 -0
- package/src/grammar/geoms/aggregate.test.ts +554 -0
- package/src/grammar/geoms/aggregate.ts +840 -0
- package/src/grammar/geoms/area.d.ts +32 -0
- package/src/grammar/geoms/area.test.ts +165 -0
- package/src/grammar/geoms/area.ts +578 -0
- package/src/grammar/geoms/band.d.ts +27 -0
- package/src/grammar/geoms/band.test.ts +57 -0
- package/src/grammar/geoms/band.ts +126 -0
- package/src/grammar/geoms/bar.d.ts +56 -0
- package/src/grammar/geoms/bar.test.ts +367 -0
- package/src/grammar/geoms/bar.ts +1054 -0
- package/src/grammar/geoms/boxplot.d.ts +129 -0
- package/src/grammar/geoms/boxplot.test.ts +299 -0
- package/src/grammar/geoms/boxplot.ts +834 -0
- package/src/grammar/geoms/connected-scatter.d.ts +27 -0
- package/src/grammar/geoms/connected-scatter.test.ts +157 -0
- package/src/grammar/geoms/connected-scatter.ts +63 -0
- package/src/grammar/geoms/emphasis.d.ts +76 -0
- package/src/grammar/geoms/emphasis.test.ts +135 -0
- package/src/grammar/geoms/emphasis.ts +162 -0
- package/src/grammar/geoms/histogram.d.ts +75 -0
- package/src/grammar/geoms/histogram.test.ts +262 -0
- package/src/grammar/geoms/histogram.ts +740 -0
- package/src/grammar/geoms/index.d.ts +20 -0
- package/src/grammar/geoms/index.ts +77 -0
- package/src/grammar/geoms/interval.d.ts +31 -0
- package/src/grammar/geoms/interval.test.ts +154 -0
- package/src/grammar/geoms/interval.ts +342 -0
- package/src/grammar/geoms/line.d.ts +38 -0
- package/src/grammar/geoms/line.test.ts +247 -0
- package/src/grammar/geoms/line.ts +659 -0
- package/src/grammar/geoms/point.d.ts +57 -0
- package/src/grammar/geoms/point.test.ts +163 -0
- package/src/grammar/geoms/point.ts +545 -0
- package/src/grammar/geoms/polar.test.ts +216 -0
- package/src/grammar/geoms/ribbon.d.ts +21 -0
- package/src/grammar/geoms/ribbon.test.ts +170 -0
- package/src/grammar/geoms/ribbon.ts +87 -0
- package/src/grammar/geoms/ridgeline.d.ts +89 -0
- package/src/grammar/geoms/ridgeline.test.ts +247 -0
- package/src/grammar/geoms/ridgeline.ts +1164 -0
- package/src/grammar/geoms/rolling.d.ts +43 -0
- package/src/grammar/geoms/rolling.test.ts +217 -0
- package/src/grammar/geoms/rolling.ts +387 -0
- package/src/grammar/geoms/rug.d.ts +28 -0
- package/src/grammar/geoms/rug.test.ts +126 -0
- package/src/grammar/geoms/rug.ts +214 -0
- package/src/grammar/geoms/rule.d.ts +23 -0
- package/src/grammar/geoms/rule.test.ts +69 -0
- package/src/grammar/geoms/rule.ts +212 -0
- package/src/grammar/geoms/smooth.d.ts +54 -0
- package/src/grammar/geoms/smooth.test.ts +78 -0
- package/src/grammar/geoms/smooth.ts +337 -0
- package/src/grammar/geoms/text.d.ts +29 -0
- package/src/grammar/geoms/text.test.ts +64 -0
- package/src/grammar/geoms/text.ts +234 -0
- package/src/grammar/geoms/tile.d.ts +61 -0
- package/src/grammar/geoms/tile.test.ts +157 -0
- package/src/grammar/geoms/tile.ts +621 -0
- package/src/grammar/geoms/types.d.ts +319 -0
- package/src/grammar/geoms/types.ts +362 -0
- package/src/grammar/geoms/violin.d.ts +85 -0
- package/src/grammar/geoms/violin.test.ts +187 -0
- package/src/grammar/geoms/violin.ts +672 -0
- package/src/grammar/index.d.ts +22 -0
- package/src/grammar/index.ts +269 -0
- package/src/grammar/interactions/_disposable.d.ts +5 -0
- package/src/grammar/interactions/_disposable.ts +23 -0
- package/src/grammar/interactions/_z.d.ts +4 -0
- package/src/grammar/interactions/_z.ts +16 -0
- package/src/grammar/interactions/brush-selection.test.ts +262 -0
- package/src/grammar/interactions/brush.d.ts +63 -0
- package/src/grammar/interactions/brush.test.ts +483 -0
- package/src/grammar/interactions/brush.ts +452 -0
- package/src/grammar/interactions/crosshair.d.ts +19 -0
- package/src/grammar/interactions/crosshair.test.ts +127 -0
- package/src/grammar/interactions/crosshair.ts +76 -0
- package/src/grammar/interactions/hit-layer.d.ts +64 -0
- package/src/grammar/interactions/hit-layer.ts +246 -0
- package/src/grammar/interactions/legend.d.ts +19 -0
- package/src/grammar/interactions/legend.ts +101 -0
- package/src/grammar/interactions/menu.d.ts +93 -0
- package/src/grammar/interactions/menu.test.ts +373 -0
- package/src/grammar/interactions/menu.ts +342 -0
- package/src/grammar/interactions/selection.d.ts +25 -0
- package/src/grammar/interactions/selection.test.ts +289 -0
- package/src/grammar/interactions/selection.ts +142 -0
- package/src/grammar/interactions/series-readout.d.ts +91 -0
- package/src/grammar/interactions/series-readout.test.ts +668 -0
- package/src/grammar/interactions/series-readout.ts +422 -0
- package/src/grammar/interactions/series-snap.d.ts +70 -0
- package/src/grammar/interactions/series-snap.test.ts +214 -0
- package/src/grammar/interactions/series-snap.ts +218 -0
- package/src/grammar/interactions/tooltip-axis.test.ts +176 -0
- package/src/grammar/interactions/tooltip-touch.browser.test.ts +49 -0
- package/src/grammar/interactions/tooltip-touch.test.ts +161 -0
- package/src/grammar/interactions/tooltip.d.ts +140 -0
- package/src/grammar/interactions/tooltip.test.ts +406 -0
- package/src/grammar/interactions/tooltip.ts +622 -0
- package/src/grammar/interactions/transitions.d.ts +34 -0
- package/src/grammar/interactions/transitions.test.ts +172 -0
- package/src/grammar/interactions/transitions.ts +160 -0
- package/src/grammar/layout.d.ts +68 -0
- package/src/grammar/layout.ts +186 -0
- package/src/grammar/legend-merge.test.ts +332 -0
- package/src/grammar/mount.d.ts +78 -0
- package/src/grammar/mount.test.ts +479 -0
- package/src/grammar/mount.ts +2112 -0
- package/src/grammar/palettes.d.ts +54 -0
- package/src/grammar/palettes.test.ts +80 -0
- package/src/grammar/palettes.ts +167 -0
- package/src/grammar/pan-zoom.test.ts +398 -0
- package/src/grammar/phylo.d.ts +65 -0
- package/src/grammar/phylo.test.ts +59 -0
- package/src/grammar/phylo.ts +112 -0
- package/src/grammar/pipeline.auto-ticks.test.ts +40 -0
- package/src/grammar/pipeline.d.ts +158 -0
- package/src/grammar/pipeline.test.ts +463 -0
- package/src/grammar/pipeline.ts +1233 -0
- package/src/grammar/profiling.d.ts +8 -0
- package/src/grammar/profiling.ts +24 -0
- package/src/grammar/scales.d.ts +188 -0
- package/src/grammar/scales.test.ts +181 -0
- package/src/grammar/scales.ts +800 -0
- package/src/grammar/svg.d.ts +3 -0
- package/src/grammar/svg.ts +39 -0
- package/src/grammar/theme.d.ts +261 -0
- package/src/grammar/theme.test.ts +105 -0
- package/src/grammar/theme.ts +490 -0
- package/src/heatmap/cpu.ts +109 -0
- package/src/heatmap/gpu.ts +565 -0
- package/src/heatmap/types.ts +177 -0
- package/src/heatmap.browser.test.ts +308 -0
- package/src/heatmap.test.ts +320 -0
- package/src/heatmap.ts +123 -0
- package/src/index.d.ts +1 -0
- package/src/index.ts +8 -0
- package/src/interactions.d.ts +48 -0
- package/src/interactions.test.ts +226 -0
- package/src/interactions.ts +394 -0
- package/src/layout/box.d.ts +48 -0
- package/src/layout/box.test.ts +107 -0
- package/src/layout/box.ts +143 -0
- package/src/legend.d.ts +115 -0
- package/src/legend.ts +422 -0
- package/src/marks/curve.d.ts +43 -0
- package/src/marks/curve.ts +244 -0
- package/src/marks/stack.d.ts +53 -0
- package/src/marks/stack.ts +184 -0
- package/src/marks.d.ts +273 -0
- package/src/marks.test.ts +541 -0
- package/src/marks.ts +1292 -0
- package/src/navigator.test.ts +174 -0
- package/src/navigator.ts +393 -0
- package/src/range-presets.d.ts +113 -0
- package/src/range-presets.test.ts +345 -0
- package/src/range-presets.ts +349 -0
- package/src/scales.d.ts +98 -0
- package/src/scales.test.ts +103 -0
- package/src/scales.ts +695 -0
- package/src/stats/index.d.ts +200 -0
- package/src/stats/index.test.ts +349 -0
- package/src/stats/index.ts +740 -0
- package/src/stats/regression.d.ts +38 -0
- package/src/stats/regression.test.ts +56 -0
- package/src/stats/regression.ts +396 -0
- package/src/stats/rolling-window.d.ts +55 -0
- package/src/stats/rolling-window.test.ts +237 -0
- package/src/stats/rolling-window.ts +256 -0
- package/src/test-setup.ts +19 -0
- package/src/viewport/axis-state.d.ts +72 -0
- package/src/viewport/axis-state.ts +476 -0
- package/src/viewport.d.ts +170 -0
- package/src/viewport.test.ts +363 -0
- package/src/viewport.ts +510 -0
|
@@ -0,0 +1,659 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// line geom
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
|
|
5
|
+
import { lerpColor, type Color, type Layer } from "insomni";
|
|
6
|
+
import { lineSwatch } from "../../legend.ts";
|
|
7
|
+
import {
|
|
8
|
+
DASHED_PATTERN,
|
|
9
|
+
DOTTED_PATTERN,
|
|
10
|
+
lineMark,
|
|
11
|
+
type LineCurve,
|
|
12
|
+
type LineDashStyle,
|
|
13
|
+
type MarkBuilder,
|
|
14
|
+
} from "../../marks.ts";
|
|
15
|
+
import { resamplePoints } from "../../marks/curve.ts";
|
|
16
|
+
import type { Aes } from "../aes.ts";
|
|
17
|
+
import { materialize, resolveAes } from "../aes.ts";
|
|
18
|
+
import { alphaize } from "../color-utils.ts";
|
|
19
|
+
import type { Coord, Point } from "../coord.ts";
|
|
20
|
+
import type {
|
|
21
|
+
CompileContext,
|
|
22
|
+
CompiledHitTest,
|
|
23
|
+
Geom,
|
|
24
|
+
GeomFrame,
|
|
25
|
+
GeomHoverDecorator,
|
|
26
|
+
HoveredHit,
|
|
27
|
+
ResolvedChannelMap,
|
|
28
|
+
} from "./types.ts";
|
|
29
|
+
import {
|
|
30
|
+
haloRing,
|
|
31
|
+
inlineMark,
|
|
32
|
+
resolveCoord,
|
|
33
|
+
SELECTION_DIM_ALPHA,
|
|
34
|
+
selectedIndicesFor,
|
|
35
|
+
selectionActive,
|
|
36
|
+
wrapMark,
|
|
37
|
+
} from "./_mark.ts";
|
|
38
|
+
import { emphasisContext } from "./emphasis.ts";
|
|
39
|
+
|
|
40
|
+
export interface LineChannels<T> {
|
|
41
|
+
x: Aes<T, number | Date>;
|
|
42
|
+
y: Aes<T, number | Date>;
|
|
43
|
+
/** Categorical color channel splits the line into one stroke per category. */
|
|
44
|
+
color?: Aes<T, unknown>;
|
|
45
|
+
/**
|
|
46
|
+
* Optional ordering aesthetic. When present, rows are connected in ascending
|
|
47
|
+
* order (globally, or within each color-grouped series).
|
|
48
|
+
*/
|
|
49
|
+
order?: Aes<T, number | Date>;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface LineOptions {
|
|
53
|
+
stroke?: Color;
|
|
54
|
+
strokeWidth?: number;
|
|
55
|
+
curve?: LineCurve;
|
|
56
|
+
curveSamples?: number;
|
|
57
|
+
dashPattern?: readonly number[];
|
|
58
|
+
/**
|
|
59
|
+
* Categorical dash treatment. `dashPattern` takes precedence when both
|
|
60
|
+
* are supplied. See {@link LineDashStyle}.
|
|
61
|
+
*/
|
|
62
|
+
dashStyle?: LineDashStyle;
|
|
63
|
+
label?: string;
|
|
64
|
+
/**
|
|
65
|
+
* When true, hover hit-tests resolve to the nearest vertex *by x* — the
|
|
66
|
+
* cursor's vertical position doesn't influence which datum is picked, so
|
|
67
|
+
* the user can hover anywhere along the line. Default `false` (per-vertex
|
|
68
|
+
* Euclidean pick within `pickRadius`). For multi-line charts this means
|
|
69
|
+
* the cursor's x picks one vertex per series; the topmost series wins
|
|
70
|
+
* via PointCloud zIndex order.
|
|
71
|
+
*/
|
|
72
|
+
nearestX?: boolean;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function line<T>(channels: LineChannels<T>, options: LineOptions = {}): Geom<T> {
|
|
76
|
+
return {
|
|
77
|
+
kind: "line",
|
|
78
|
+
channels,
|
|
79
|
+
label: options.label,
|
|
80
|
+
legendSwatch: (color, theme) => lineSwatch({ stroke: color, width: theme.marks.strokeWidth }),
|
|
81
|
+
compile(ctx: CompileContext<T>) {
|
|
82
|
+
const { data, scales, plot, theme } = ctx;
|
|
83
|
+
const coord = resolveCoord(ctx);
|
|
84
|
+
|
|
85
|
+
const xAes = resolveAes<T, unknown>(channels.x as Aes<T, unknown>);
|
|
86
|
+
const yAes = resolveAes<T, unknown>(channels.y as Aes<T, unknown>);
|
|
87
|
+
const colorAes = channels.color ? resolveAes<T, unknown>(channels.color) : undefined;
|
|
88
|
+
const orderAes = channels.order ? resolveAes<T, unknown>(channels.order) : undefined;
|
|
89
|
+
|
|
90
|
+
const xScale = scales.x.fn;
|
|
91
|
+
const yScale = scales.y.fn;
|
|
92
|
+
const colorScale = scales.color?.fn;
|
|
93
|
+
|
|
94
|
+
const baseStroke: Color = options.stroke ?? theme.palettes.categorical(0);
|
|
95
|
+
const strokeWidth = options.strokeWidth ?? theme.marks.strokeWidth;
|
|
96
|
+
const curve: LineCurve = options.curve ?? "linear";
|
|
97
|
+
|
|
98
|
+
const builders: MarkBuilder[] = [];
|
|
99
|
+
const anim = ctx.activeTransition;
|
|
100
|
+
// Dim non-selected line strokes when *any* selection is active anywhere
|
|
101
|
+
// in the chart. For single-line: dim if no vertex of this line is in the
|
|
102
|
+
// selected set. For multi-line: dim per-group if no row in the group is
|
|
103
|
+
// selected. Selection rings still mark the active vertices on top.
|
|
104
|
+
const selActive = selectionActive(ctx);
|
|
105
|
+
const selectedSetAll = selectedIndicesFor(ctx, "line");
|
|
106
|
+
// Hover dim now rides the core's GPU emphasis uniform (P5-T3): each color
|
|
107
|
+
// GROUP is tagged with a stable per-series key (ordinal = the group's
|
|
108
|
+
// position in `groupKeys`); the mount fades sibling groups with no marks
|
|
109
|
+
// recompile. Single-line charts have nothing to dim against, so they tag
|
|
110
|
+
// nothing. Resolved by `emphasisResolution` below.
|
|
111
|
+
const fromIndexFor = (d: T, i: number) =>
|
|
112
|
+
anim
|
|
113
|
+
? ctx.transitionKey
|
|
114
|
+
? anim.matchIndex(ctx.transitionKey(d, i), i)
|
|
115
|
+
: anim.matchIndex(String(i), i)
|
|
116
|
+
: undefined;
|
|
117
|
+
|
|
118
|
+
const sortedIndices = (indices: readonly number[]): number[] => {
|
|
119
|
+
const out = [...indices];
|
|
120
|
+
if (!orderAes) return out;
|
|
121
|
+
out.sort((a, b) => compareOrder(orderAes.fn(data[a]!, a), orderAes.fn(data[b]!, b), a, b));
|
|
122
|
+
return out;
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
// Single-line path (no color channel): one mark.
|
|
126
|
+
if (!colorAes) {
|
|
127
|
+
const indices = sortedIndices(data.map((_, i) => i));
|
|
128
|
+
// For animation, lerp the stroke color using datum 0's stored rgba as representative.
|
|
129
|
+
const animatedStroke: Color =
|
|
130
|
+
anim && anim.from.count > 0
|
|
131
|
+
? lerpColor(
|
|
132
|
+
{
|
|
133
|
+
r: anim.from.rgba[0]!,
|
|
134
|
+
g: anim.from.rgba[1]!,
|
|
135
|
+
b: anim.from.rgba[2]!,
|
|
136
|
+
a: anim.from.rgba[3]!,
|
|
137
|
+
},
|
|
138
|
+
baseStroke,
|
|
139
|
+
anim.t,
|
|
140
|
+
)
|
|
141
|
+
: baseStroke;
|
|
142
|
+
const dimSingle = selActive && selectedSetAll === null;
|
|
143
|
+
const finalStroke = dimSingle
|
|
144
|
+
? alphaize(animatedStroke, SELECTION_DIM_ALPHA)
|
|
145
|
+
: animatedStroke;
|
|
146
|
+
// Combined per-vertex projection through the active coord. Under
|
|
147
|
+
// `coordCartesian()` this is the identity. Under polar (Phase 3) the
|
|
148
|
+
// (x, y) pair projects together; we lerp the plot-frame values for
|
|
149
|
+
// animation and project the final pair.
|
|
150
|
+
const projectVertex = (globalIndex: number): { x: number; y: number } => {
|
|
151
|
+
const d = data[globalIndex]!;
|
|
152
|
+
const toX = xScale(xAes.fn(d, globalIndex));
|
|
153
|
+
const toY = yScale(yAes.fn(d, globalIndex));
|
|
154
|
+
const fromIndex = fromIndexFor(d, globalIndex);
|
|
155
|
+
let px = toX;
|
|
156
|
+
let py = toY;
|
|
157
|
+
if (anim && fromIndex !== undefined) {
|
|
158
|
+
if (Number.isFinite(anim.from.x[fromIndex]!)) {
|
|
159
|
+
px = anim.from.x[fromIndex]! + (toX - anim.from.x[fromIndex]!) * anim.t;
|
|
160
|
+
}
|
|
161
|
+
if (Number.isFinite(anim.from.y[fromIndex]!)) {
|
|
162
|
+
py = anim.from.y[fromIndex]! + (toY - anim.from.y[fromIndex]!) * anim.t;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return coord.project({ x: px, y: py });
|
|
166
|
+
};
|
|
167
|
+
if (coord.kind === "cartesian") {
|
|
168
|
+
const mark = lineMark(indices, {
|
|
169
|
+
x: (globalIndex) => projectVertex(globalIndex).x,
|
|
170
|
+
y: (globalIndex) => projectVertex(globalIndex).y,
|
|
171
|
+
stroke: finalStroke,
|
|
172
|
+
strokeWidth,
|
|
173
|
+
curve,
|
|
174
|
+
curveSamples: options.curveSamples,
|
|
175
|
+
dashPattern: options.dashPattern,
|
|
176
|
+
dashStyle: options.dashStyle,
|
|
177
|
+
});
|
|
178
|
+
builders.push(wrapMark(mark, plot.topLeft, indices.length));
|
|
179
|
+
} else {
|
|
180
|
+
// Non-cartesian coord: tessellate per-segment through `coord.segment`
|
|
181
|
+
// so adjacent vertices arc/curve correctly under polar instead of
|
|
182
|
+
// being connected by straight chords through projected endpoints.
|
|
183
|
+
builders.push(
|
|
184
|
+
tessellatedPolyline({
|
|
185
|
+
coord,
|
|
186
|
+
indices,
|
|
187
|
+
projectVertex: (globalIndex: number, _localIndex: number) =>
|
|
188
|
+
projectVertex(globalIndex),
|
|
189
|
+
ox: plot.topLeft.x,
|
|
190
|
+
oy: plot.topLeft.y,
|
|
191
|
+
stroke: finalStroke,
|
|
192
|
+
strokeWidth,
|
|
193
|
+
curve,
|
|
194
|
+
curveSamples: options.curveSamples,
|
|
195
|
+
dashPattern: options.dashPattern,
|
|
196
|
+
dashStyle: options.dashStyle,
|
|
197
|
+
emphasisKey: undefined,
|
|
198
|
+
}),
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
} else {
|
|
202
|
+
// Multi-line: bucket by color value, one mark per group. Track each
|
|
203
|
+
// row's original index in `data` alongside the row itself so the
|
|
204
|
+
// transition `from` lookup uses the same indexing as `captureFrame`
|
|
205
|
+
// (which iterates the full data array). Without this, group rows 0..n
|
|
206
|
+
// would all read positions from the FIRST series' captured frame.
|
|
207
|
+
const colorValues = materialize(colorAes, data);
|
|
208
|
+
type GroupBucket = { indices: number[] };
|
|
209
|
+
const groups = new Map<unknown, GroupBucket>();
|
|
210
|
+
// Stable first-seen group-key order — the ordinal source shared by
|
|
211
|
+
// tagging and `emphasisResolution` (both walk the same `colorValues`).
|
|
212
|
+
const groupKeys: string[] = [];
|
|
213
|
+
for (let i = 0; i < data.length; i++) {
|
|
214
|
+
const key = colorValues[i];
|
|
215
|
+
let bucket = groups.get(key);
|
|
216
|
+
if (!bucket) {
|
|
217
|
+
bucket = { indices: [] };
|
|
218
|
+
groups.set(key, bucket);
|
|
219
|
+
groupKeys.push(String(key));
|
|
220
|
+
}
|
|
221
|
+
bucket.indices.push(i);
|
|
222
|
+
}
|
|
223
|
+
// GPU emphasis context — only meaningful for multi-line (sibling groups
|
|
224
|
+
// to dim against). The resolver maps a hovered vertex's color group to
|
|
225
|
+
// its ordinal via the same `groupKeys`.
|
|
226
|
+
const emph = emphasisContext(ctx, "line", (hit) => {
|
|
227
|
+
const k = colorValues[hit.dataIndex];
|
|
228
|
+
if (k === undefined) return null;
|
|
229
|
+
// oxlint-disable-next-line no-base-to-string -- color key is a scalar (string/number); String() is safe here
|
|
230
|
+
const ord = groupKeys.indexOf(String(k));
|
|
231
|
+
return ord < 0 ? null : ord;
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
for (const [key, bucket] of groups) {
|
|
235
|
+
if (ctx.hidden?.has(String(key))) continue;
|
|
236
|
+
const indices = sortedIndices(bucket.indices);
|
|
237
|
+
const stroke = colorScale ? colorScale(key) : baseStroke;
|
|
238
|
+
// Group dims when selection is active and no selected datum's color
|
|
239
|
+
// matches this group's key. `selectedSetAll` indexes into the full
|
|
240
|
+
// `data` array, so we look up each selected row's color value.
|
|
241
|
+
let groupHasSel = false;
|
|
242
|
+
if (selActive && selectedSetAll) {
|
|
243
|
+
for (const i of selectedSetAll) {
|
|
244
|
+
if (colorValues[i] === key) {
|
|
245
|
+
groupHasSel = true;
|
|
246
|
+
break;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
// Selection dim stays compile-time (full recompile on select). Hover
|
|
251
|
+
// dim is now GPU-side via the per-group emphasis key below.
|
|
252
|
+
const groupDim = selActive && !groupHasSel;
|
|
253
|
+
const finalGroupStroke = groupDim ? alphaize(stroke, SELECTION_DIM_ALPHA) : stroke;
|
|
254
|
+
const groupEmphasisKey = emph?.keyFor(groupKeys.indexOf(String(key)));
|
|
255
|
+
const groupFromIndexFor = (d: T, localIndex: number) => {
|
|
256
|
+
const globalIndex = indices[localIndex]!;
|
|
257
|
+
return anim
|
|
258
|
+
? ctx.transitionKey
|
|
259
|
+
? anim.matchIndex(ctx.transitionKey(d, globalIndex), globalIndex)
|
|
260
|
+
: anim.matchIndex(String(globalIndex), globalIndex)
|
|
261
|
+
: undefined;
|
|
262
|
+
};
|
|
263
|
+
const projectGroupVertex = (
|
|
264
|
+
globalIndex: number,
|
|
265
|
+
localIndex: number,
|
|
266
|
+
): { x: number; y: number } => {
|
|
267
|
+
const d = data[globalIndex]!;
|
|
268
|
+
const toX = xScale(xAes.fn(d, globalIndex));
|
|
269
|
+
const toY = yScale(yAes.fn(d, globalIndex));
|
|
270
|
+
const fromIndex = groupFromIndexFor(d, localIndex);
|
|
271
|
+
let px = toX;
|
|
272
|
+
let py = toY;
|
|
273
|
+
if (anim && fromIndex !== undefined) {
|
|
274
|
+
if (Number.isFinite(anim.from.x[fromIndex]!)) {
|
|
275
|
+
px = anim.from.x[fromIndex]! + (toX - anim.from.x[fromIndex]!) * anim.t;
|
|
276
|
+
}
|
|
277
|
+
if (Number.isFinite(anim.from.y[fromIndex]!)) {
|
|
278
|
+
py = anim.from.y[fromIndex]! + (toY - anim.from.y[fromIndex]!) * anim.t;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
return coord.project({ x: px, y: py });
|
|
282
|
+
};
|
|
283
|
+
if (coord.kind === "cartesian") {
|
|
284
|
+
const mark = lineMark(indices, {
|
|
285
|
+
x: (globalIndex, localIndex) => projectGroupVertex(globalIndex, localIndex).x,
|
|
286
|
+
y: (globalIndex, localIndex) => projectGroupVertex(globalIndex, localIndex).y,
|
|
287
|
+
stroke: finalGroupStroke,
|
|
288
|
+
strokeWidth,
|
|
289
|
+
curve,
|
|
290
|
+
curveSamples: options.curveSamples,
|
|
291
|
+
dashPattern: options.dashPattern,
|
|
292
|
+
dashStyle: options.dashStyle,
|
|
293
|
+
emphasisKey: groupEmphasisKey,
|
|
294
|
+
});
|
|
295
|
+
builders.push(wrapMark(mark, plot.topLeft, indices.length));
|
|
296
|
+
} else {
|
|
297
|
+
builders.push(
|
|
298
|
+
tessellatedPolyline({
|
|
299
|
+
coord,
|
|
300
|
+
indices,
|
|
301
|
+
projectVertex: (globalIndex: number, localIndex: number) =>
|
|
302
|
+
projectGroupVertex(globalIndex, localIndex),
|
|
303
|
+
ox: plot.topLeft.x,
|
|
304
|
+
oy: plot.topLeft.y,
|
|
305
|
+
stroke: finalGroupStroke,
|
|
306
|
+
strokeWidth,
|
|
307
|
+
curve,
|
|
308
|
+
curveSamples: options.curveSamples,
|
|
309
|
+
dashPattern: options.dashPattern,
|
|
310
|
+
dashStyle: options.dashStyle,
|
|
311
|
+
emphasisKey: groupEmphasisKey,
|
|
312
|
+
}),
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Hover treatment for line is the GPU dim above (sibling series fade);
|
|
319
|
+
// the prior compile-time hover halo on the active vertex required a marks
|
|
320
|
+
// recompile per pointer move (gone under P5-T3), so it is dropped. The
|
|
321
|
+
// tooltip/crosshair still track the hovered vertex via the overlay.
|
|
322
|
+
|
|
323
|
+
// Selection rings — one per selected vertex. Stroke dimming for non-
|
|
324
|
+
// selected lines/groups happens above (during mark construction); the
|
|
325
|
+
// rings overlay selected vertices on top.
|
|
326
|
+
const selectedSet = selectedSetAll;
|
|
327
|
+
if (selectedSet) {
|
|
328
|
+
for (const i of selectedSet) {
|
|
329
|
+
const d = data[i];
|
|
330
|
+
if (d === undefined) continue;
|
|
331
|
+
const xv = xAes.fn(d, i);
|
|
332
|
+
const yv = yAes.fn(d, i);
|
|
333
|
+
if (xv == null || yv == null) continue;
|
|
334
|
+
const rawX = xScale(xv);
|
|
335
|
+
const rawY = yScale(yv);
|
|
336
|
+
if (!Number.isFinite(rawX) || !Number.isFinite(rawY)) continue;
|
|
337
|
+
const projected = coord.project({ x: rawX, y: rawY });
|
|
338
|
+
const ringColor: Color =
|
|
339
|
+
colorAes && colorScale ? colorScale(colorAes.fn(d, i)) : baseStroke;
|
|
340
|
+
const cx = plot.topLeft.x + projected.x;
|
|
341
|
+
const cy = plot.topLeft.y + projected.y;
|
|
342
|
+
const r = Math.max(4, strokeWidth * 1.5 + 2);
|
|
343
|
+
builders.push(inlineMark((layer) => haloRing(layer, cx, cy, r, ringColor, 2)));
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
return builders;
|
|
348
|
+
},
|
|
349
|
+
emphasisResolution(ctx) {
|
|
350
|
+
// Only multi-line (a color channel) dims sibling series; single-line has
|
|
351
|
+
// nothing to dim against, so it tags nothing and resolves to null.
|
|
352
|
+
const colorAes = channels.color ? resolveAes<T, unknown>(channels.color) : undefined;
|
|
353
|
+
if (!colorAes) return null;
|
|
354
|
+
const data = ctx.data;
|
|
355
|
+
const colorValues = materialize(colorAes, data);
|
|
356
|
+
// Recompute the SAME first-seen group-key order the compile path used.
|
|
357
|
+
const groupKeys: string[] = [];
|
|
358
|
+
const seen = new Set<string>();
|
|
359
|
+
for (let i = 0; i < data.length; i++) {
|
|
360
|
+
const k = String(colorValues[i]);
|
|
361
|
+
if (!seen.has(k)) {
|
|
362
|
+
seen.add(k);
|
|
363
|
+
groupKeys.push(k);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
return (
|
|
367
|
+
emphasisContext(ctx, "line", (hit) => {
|
|
368
|
+
const k = colorValues[hit.dataIndex];
|
|
369
|
+
if (k === undefined) return null;
|
|
370
|
+
// oxlint-disable-next-line no-base-to-string -- color key is a scalar (string/number); String() is safe here
|
|
371
|
+
const ord = groupKeys.indexOf(String(k));
|
|
372
|
+
return ord < 0 ? null : ord;
|
|
373
|
+
})?.resolver() ?? null
|
|
374
|
+
);
|
|
375
|
+
},
|
|
376
|
+
hoverDecoration(ctx: CompileContext<T>): GeomHoverDecorator | null {
|
|
377
|
+
// Focus halo on the hovered vertex — recovered from the old snap path's
|
|
378
|
+
// compile-time `haloRing` (deleted in e6d5643). Now an OVERLAY decorator
|
|
379
|
+
// replayed into the live overlay layer (NO marks recompile). The overlay
|
|
380
|
+
// ring leaves emphasisKey 0 ⇒ EXEMPT ⇒ it stays full-strength while sibling
|
|
381
|
+
// series dim via the GPU uniform. The hit's `dataIndex` is the vertex's
|
|
382
|
+
// datum index. Mirrors point.ts (re-derive geometry; theme hover fallback).
|
|
383
|
+
const { data, plot } = ctx;
|
|
384
|
+
if (data.length === 0) return null;
|
|
385
|
+
const coord = resolveCoord(ctx);
|
|
386
|
+
const hoverCfg = ctx.theme.interactions.hover;
|
|
387
|
+
const xAes = resolveAes<T, unknown>(channels.x as Aes<T, unknown>);
|
|
388
|
+
const yAes = resolveAes<T, unknown>(channels.y as Aes<T, unknown>);
|
|
389
|
+
const colorAes = channels.color ? resolveAes<T, unknown>(channels.color) : undefined;
|
|
390
|
+
const xScale = ctx.scales.x.fn;
|
|
391
|
+
const yScale = ctx.scales.y.fn;
|
|
392
|
+
const colorScale = ctx.scales.color?.fn;
|
|
393
|
+
const baseStroke: Color = options.stroke ?? ctx.theme.palettes.categorical(0);
|
|
394
|
+
const strokeWidth = options.strokeWidth ?? ctx.theme.marks.strokeWidth;
|
|
395
|
+
const r = Math.max(4, strokeWidth * 1.5 + 2);
|
|
396
|
+
const ox = plot.topLeft.x;
|
|
397
|
+
const oy = plot.topLeft.y;
|
|
398
|
+
|
|
399
|
+
return {
|
|
400
|
+
geomKind: "line",
|
|
401
|
+
data,
|
|
402
|
+
decorate(hit: HoveredHit, layer: Layer): void {
|
|
403
|
+
if (!hoverCfg.enabled || hit.data !== data) return;
|
|
404
|
+
const i = hit.dataIndex;
|
|
405
|
+
const d = data[i];
|
|
406
|
+
if (d === undefined) return;
|
|
407
|
+
const xv = xAes.fn(d, i);
|
|
408
|
+
const yv = yAes.fn(d, i);
|
|
409
|
+
if (xv == null || yv == null) return;
|
|
410
|
+
const rawX = xScale(xv);
|
|
411
|
+
const rawY = yScale(yv);
|
|
412
|
+
if (!Number.isFinite(rawX) || !Number.isFinite(rawY)) return;
|
|
413
|
+
const p = coord.project({ x: rawX, y: rawY });
|
|
414
|
+
const ringColor: Color =
|
|
415
|
+
hoverCfg.haloColor ??
|
|
416
|
+
(colorAes && colorScale ? colorScale(colorAes.fn(d, i)) : baseStroke);
|
|
417
|
+
haloRing(layer, ox + p.x, oy + p.y, r, ringColor, hoverCfg.haloStrokeWidth);
|
|
418
|
+
},
|
|
419
|
+
};
|
|
420
|
+
},
|
|
421
|
+
compileHitTest(ctx: CompileContext<T>): CompiledHitTest<T> | null {
|
|
422
|
+
const { data, scales, plot, theme, hidden } = ctx;
|
|
423
|
+
if (data.length === 0) return null;
|
|
424
|
+
const coord = resolveCoord(ctx);
|
|
425
|
+
|
|
426
|
+
const xAes = resolveAes<T, unknown>(channels.x as Aes<T, unknown>);
|
|
427
|
+
const yAes = resolveAes<T, unknown>(channels.y as Aes<T, unknown>);
|
|
428
|
+
const colorAes = channels.color ? resolveAes<T, unknown>(channels.color) : undefined;
|
|
429
|
+
|
|
430
|
+
const xScale = scales.x.fn;
|
|
431
|
+
const yScale = scales.y.fn;
|
|
432
|
+
|
|
433
|
+
const positions = new Float32Array(data.length * 2);
|
|
434
|
+
const dataIndex = new Int32Array(data.length);
|
|
435
|
+
const ox = plot.topLeft.x;
|
|
436
|
+
const oy = plot.topLeft.y;
|
|
437
|
+
let n = 0;
|
|
438
|
+
for (let i = 0; i < data.length; i++) {
|
|
439
|
+
const d = data[i]!;
|
|
440
|
+
|
|
441
|
+
if (hidden && colorAes && hidden.has(String(colorAes.fn(d, i)))) {
|
|
442
|
+
continue;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
const xv = xAes.fn(d, i);
|
|
446
|
+
const yv = yAes.fn(d, i);
|
|
447
|
+
if (xv == null || yv == null) continue;
|
|
448
|
+
const rawX = xScale(xv);
|
|
449
|
+
const rawY = yScale(yv);
|
|
450
|
+
if (!Number.isFinite(rawX) || !Number.isFinite(rawY)) continue;
|
|
451
|
+
const projected = coord.project({ x: rawX, y: rawY });
|
|
452
|
+
positions[n * 2] = ox + projected.x;
|
|
453
|
+
positions[n * 2 + 1] = oy + projected.y;
|
|
454
|
+
dataIndex[n] = i;
|
|
455
|
+
n++;
|
|
456
|
+
}
|
|
457
|
+
if (n === 0) return null;
|
|
458
|
+
|
|
459
|
+
const strokeWidth = options.strokeWidth ?? theme.marks.strokeWidth;
|
|
460
|
+
const channelsMap: ResolvedChannelMap<T> = {
|
|
461
|
+
x: xAes,
|
|
462
|
+
y: yAes,
|
|
463
|
+
color: colorAes,
|
|
464
|
+
};
|
|
465
|
+
|
|
466
|
+
// In nearestX mode the picker ignores the off-axis y, so only the
|
|
467
|
+
// along-axis (x) tolerance matters. Stretch the radius to a safely
|
|
468
|
+
// large value (cursor anywhere within plot width still picks).
|
|
469
|
+
const pickRadius = options.nearestX
|
|
470
|
+
? Math.max(plot.width, plot.height)
|
|
471
|
+
: Math.max(8, strokeWidth * 2 + 4);
|
|
472
|
+
return {
|
|
473
|
+
geomKind: "line",
|
|
474
|
+
label: options.label,
|
|
475
|
+
positions: positions.subarray(0, n * 2),
|
|
476
|
+
dataIndex: dataIndex.subarray(0, n),
|
|
477
|
+
pickRadius,
|
|
478
|
+
pickAxis: options.nearestX ? "x" : undefined,
|
|
479
|
+
channels: channelsMap,
|
|
480
|
+
data,
|
|
481
|
+
};
|
|
482
|
+
},
|
|
483
|
+
captureFrame(ctx: CompileContext<T>): GeomFrame | null {
|
|
484
|
+
const { data, scales, theme } = ctx;
|
|
485
|
+
if (data.length === 0) return null;
|
|
486
|
+
|
|
487
|
+
const xAes = resolveAes<T, unknown>(channels.x as Aes<T, unknown>);
|
|
488
|
+
const yAes = resolveAes<T, unknown>(channels.y as Aes<T, unknown>);
|
|
489
|
+
const colorAes = channels.color ? resolveAes<T, unknown>(channels.color) : undefined;
|
|
490
|
+
|
|
491
|
+
const xScale = scales.x.fn;
|
|
492
|
+
const yScale = scales.y.fn;
|
|
493
|
+
const colorScale = scales.color?.fn;
|
|
494
|
+
|
|
495
|
+
const baseStroke: Color = options.stroke ?? theme.palettes.categorical(0);
|
|
496
|
+
const transitionKey = ctx.transitionKey;
|
|
497
|
+
|
|
498
|
+
const count = data.length;
|
|
499
|
+
const x = new Float32Array(count);
|
|
500
|
+
const y = new Float32Array(count);
|
|
501
|
+
const rgba = new Float32Array(count * 4);
|
|
502
|
+
const a = new Float32Array(count);
|
|
503
|
+
const ids = transitionKey ? Array.from<string>({ length: count }) : undefined;
|
|
504
|
+
|
|
505
|
+
for (let i = 0; i < count; i++) {
|
|
506
|
+
const d = data[i]!;
|
|
507
|
+
const xv = xAes.fn(d, i);
|
|
508
|
+
const yv = yAes.fn(d, i);
|
|
509
|
+
const px = xScale(xv);
|
|
510
|
+
const py = yScale(yv);
|
|
511
|
+
x[i] = Number.isFinite(px) ? px : NaN;
|
|
512
|
+
y[i] = Number.isFinite(py) ? py : NaN;
|
|
513
|
+
|
|
514
|
+
// Per-datum stroke color (same as the series color for multi-line).
|
|
515
|
+
const c: Color = colorAes && colorScale ? colorScale(colorAes.fn(d, i)) : baseStroke;
|
|
516
|
+
rgba[i * 4] = c.r;
|
|
517
|
+
rgba[i * 4 + 1] = c.g;
|
|
518
|
+
rgba[i * 4 + 2] = c.b;
|
|
519
|
+
rgba[i * 4 + 3] = c.a;
|
|
520
|
+
a[i] = c.a;
|
|
521
|
+
if (ids && transitionKey) ids[i] = transitionKey(d, i);
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
return { count, x, y, rgba, a, ids };
|
|
525
|
+
},
|
|
526
|
+
};
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
function compareOrder(a: unknown, b: unknown, fallbackA: number, fallbackB: number): number {
|
|
530
|
+
const av = orderValue(a);
|
|
531
|
+
const bv = orderValue(b);
|
|
532
|
+
const aFinite = Number.isFinite(av);
|
|
533
|
+
const bFinite = Number.isFinite(bv);
|
|
534
|
+
if (aFinite && bFinite) {
|
|
535
|
+
if (av < bv) return -1;
|
|
536
|
+
if (av > bv) return 1;
|
|
537
|
+
return fallbackA - fallbackB;
|
|
538
|
+
}
|
|
539
|
+
if (aFinite) return -1;
|
|
540
|
+
if (bFinite) return 1;
|
|
541
|
+
return fallbackA - fallbackB;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
function orderValue(value: unknown): number {
|
|
545
|
+
if (value instanceof Date) return value.getTime();
|
|
546
|
+
return typeof value === "number" ? value : Number.NaN;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// ---------------------------------------------------------------------------
|
|
550
|
+
// Polar / non-cartesian tessellation
|
|
551
|
+
// ---------------------------------------------------------------------------
|
|
552
|
+
// Under `coordCartesian` the lineMark path emits a single polyline whose
|
|
553
|
+
// vertices are the projected per-datum (x, y). That works because projection
|
|
554
|
+
// is the identity — straight chords between adjacent vertices visit every
|
|
555
|
+
// pixel along the desired line.
|
|
556
|
+
//
|
|
557
|
+
// Under polar (or any non-identity coord) two adjacent vertices that share a
|
|
558
|
+
// radius but differ in θ should connect by an *arc*, not a chord. We resolve
|
|
559
|
+
// this by walking each consecutive pair through `coord.segment` (which
|
|
560
|
+
// tessellates at the coord's quality knob, ~1°) and stitching the results
|
|
561
|
+
// into a single polyline. The tessellated polyline is then run through the
|
|
562
|
+
// configured `curve` (still useful for non-linear smoothing) before push.
|
|
563
|
+
|
|
564
|
+
interface TessellatedPolylineArgs {
|
|
565
|
+
coord: Coord;
|
|
566
|
+
indices: readonly number[];
|
|
567
|
+
projectVertex: (globalIndex: number, localIndex: number) => Point;
|
|
568
|
+
ox: number;
|
|
569
|
+
oy: number;
|
|
570
|
+
stroke: Color;
|
|
571
|
+
strokeWidth: number;
|
|
572
|
+
curve: LineCurve;
|
|
573
|
+
curveSamples: number | undefined;
|
|
574
|
+
dashPattern: readonly number[] | undefined;
|
|
575
|
+
dashStyle: LineDashStyle | undefined;
|
|
576
|
+
emphasisKey: number | undefined;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
function tessellatedPolyline(args: TessellatedPolylineArgs): MarkBuilder {
|
|
580
|
+
const {
|
|
581
|
+
coord,
|
|
582
|
+
indices,
|
|
583
|
+
projectVertex,
|
|
584
|
+
ox,
|
|
585
|
+
oy,
|
|
586
|
+
stroke,
|
|
587
|
+
strokeWidth,
|
|
588
|
+
curve,
|
|
589
|
+
curveSamples,
|
|
590
|
+
dashPattern,
|
|
591
|
+
dashStyle,
|
|
592
|
+
emphasisKey,
|
|
593
|
+
} = args;
|
|
594
|
+
// Resolve dash style → pattern up front (mirrors lineMark's behavior).
|
|
595
|
+
const pattern =
|
|
596
|
+
dashPattern ??
|
|
597
|
+
(dashStyle === "dashed" ? DASHED_PATTERN : dashStyle === "dotted" ? DOTTED_PATTERN : undefined);
|
|
598
|
+
return {
|
|
599
|
+
length: indices.length,
|
|
600
|
+
addTo(layer) {
|
|
601
|
+
// Build the per-segment tessellated chain, breaking on non-finite
|
|
602
|
+
// projections (matches `collectSegments` semantics in `lineMark`).
|
|
603
|
+
const chains: { x: number; y: number }[][] = [];
|
|
604
|
+
let current: { x: number; y: number }[] = [];
|
|
605
|
+
const isFinitePoint = (p: Point) => Number.isFinite(p.x) && Number.isFinite(p.y);
|
|
606
|
+
for (let local = 0; local < indices.length; local++) {
|
|
607
|
+
const global = indices[local]!;
|
|
608
|
+
const cur = projectVertex(global, local);
|
|
609
|
+
if (!isFinitePoint(cur)) {
|
|
610
|
+
if (current.length > 0) {
|
|
611
|
+
chains.push(current);
|
|
612
|
+
current = [];
|
|
613
|
+
}
|
|
614
|
+
continue;
|
|
615
|
+
}
|
|
616
|
+
if (current.length === 0) {
|
|
617
|
+
current.push({ x: ox + cur.x, y: oy + cur.y });
|
|
618
|
+
continue;
|
|
619
|
+
}
|
|
620
|
+
// Tessellate from the previous to current vertex through `coord.segment`.
|
|
621
|
+
// `projectVertex` returns the post-projection plot-frame px; we recover
|
|
622
|
+
// the pre-projection input via `coord.unproject`, run `segment`, then
|
|
623
|
+
// re-offset back to layer-px for the polyline.
|
|
624
|
+
const prev = current[current.length - 1]!;
|
|
625
|
+
const prevProjected: Point = { x: prev.x - ox, y: prev.y - oy };
|
|
626
|
+
const prevUn = coord.unproject(prevProjected);
|
|
627
|
+
const curUn = coord.unproject(cur);
|
|
628
|
+
if (prevUn && curUn) {
|
|
629
|
+
const tess = coord.segment(prevUn, curUn);
|
|
630
|
+
// Skip first sample — same as `prev` (already pushed). The final
|
|
631
|
+
// sample equals `cur` (within FP error).
|
|
632
|
+
for (let k = 1; k < tess.length; k++) {
|
|
633
|
+
const p = tess[k]!;
|
|
634
|
+
current.push({ x: ox + p.x, y: oy + p.y });
|
|
635
|
+
}
|
|
636
|
+
} else {
|
|
637
|
+
// Outside the polar domain — fall back to a straight chord to the
|
|
638
|
+
// projected endpoint. Keeps render robust on edge cases.
|
|
639
|
+
current.push({ x: ox + cur.x, y: oy + cur.y });
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
if (current.length > 0) chains.push(current);
|
|
643
|
+
|
|
644
|
+
const samples = Math.max(2, curveSamples ?? 16);
|
|
645
|
+
for (const raw of chains) {
|
|
646
|
+
if (raw.length < 2) continue;
|
|
647
|
+
const points = curve === "linear" ? raw : resamplePoints(raw, curve, samples);
|
|
648
|
+
layer.pushPolyline({
|
|
649
|
+
points,
|
|
650
|
+
color: stroke,
|
|
651
|
+
width: strokeWidth,
|
|
652
|
+
dashPattern: pattern,
|
|
653
|
+
emphasisKey,
|
|
654
|
+
});
|
|
655
|
+
}
|
|
656
|
+
return layer;
|
|
657
|
+
},
|
|
658
|
+
};
|
|
659
|
+
}
|