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,27 @@
|
|
|
1
|
+
import type { Aes } from "../aes.ts";
|
|
2
|
+
import { type LineOptions } from "./line.ts";
|
|
3
|
+
import { type PointOptions } from "./point.ts";
|
|
4
|
+
import type { Geom } from "./types.ts";
|
|
5
|
+
export interface ConnectedScatterChannels<T> {
|
|
6
|
+
x: Aes<T, number | Date>;
|
|
7
|
+
y: Aes<T, number | Date>;
|
|
8
|
+
color?: Aes<T, unknown>;
|
|
9
|
+
order: Aes<T, number | Date>;
|
|
10
|
+
size?: Aes<T, number>;
|
|
11
|
+
shape?: Aes<T, unknown>;
|
|
12
|
+
alpha?: Aes<T, number>;
|
|
13
|
+
}
|
|
14
|
+
export interface ConnectedScatterOptions {
|
|
15
|
+
/** Line-layer options. */
|
|
16
|
+
line?: LineOptions;
|
|
17
|
+
/** Point-layer options. Pass `false` to render only the connecting path. */
|
|
18
|
+
point?: PointOptions | false;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Grammar helper for the common "connected scatterplot" recipe:
|
|
22
|
+
* a path ordered by a third variable, optionally topped with points.
|
|
23
|
+
*
|
|
24
|
+
* Returns plain geoms so callers still compose with `.layer(text(...))`,
|
|
25
|
+
* `.annotate(...)`, facets, transitions, and the normal interaction stack.
|
|
26
|
+
*/
|
|
27
|
+
export declare function connectedScatter<T>(channels: ConnectedScatterChannels<T>, options?: ConnectedScatterOptions): readonly Geom<T>[];
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { createFrame } from "insomni";
|
|
2
|
+
import { describe, expect, test } from "vite-plus/test";
|
|
3
|
+
|
|
4
|
+
import { resolveAes } from "../aes.ts";
|
|
5
|
+
import { buildPositionScale, type ScaleBundle } from "../scales.ts";
|
|
6
|
+
import { themeDefault } from "../theme.ts";
|
|
7
|
+
import { connectedScatter } from "./connected-scatter.ts";
|
|
8
|
+
import type { CompileContext } from "./types.ts";
|
|
9
|
+
|
|
10
|
+
interface Row {
|
|
11
|
+
x: number;
|
|
12
|
+
y: number;
|
|
13
|
+
series: string;
|
|
14
|
+
order: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const data: Row[] = [
|
|
18
|
+
{ x: 0, y: 0, series: "a", order: 1 },
|
|
19
|
+
{ x: 5, y: 10, series: "a", order: 2 },
|
|
20
|
+
{ x: 10, y: 20, series: "a", order: 3 },
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
function makeCtx(rows: readonly Row[]): CompileContext<Row> {
|
|
24
|
+
const xAes = resolveAes<Row, unknown>("x");
|
|
25
|
+
const yAes = resolveAes<Row, unknown>("y");
|
|
26
|
+
const xScale = buildPositionScale(xAes, rows, [0, 100]);
|
|
27
|
+
const yScale = buildPositionScale(yAes, rows, [200, 0]);
|
|
28
|
+
const scales: ScaleBundle = { x: xScale, y: yScale };
|
|
29
|
+
return {
|
|
30
|
+
data: rows,
|
|
31
|
+
scales,
|
|
32
|
+
plot: createFrame({ x: 50, y: 30, width: 100, height: 200 }),
|
|
33
|
+
theme: themeDefault,
|
|
34
|
+
atlas: undefined,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Collect the polylines and circles emitted across a geom's compiled builders. */
|
|
39
|
+
function captureShapes(
|
|
40
|
+
geom: { compile: (ctx: CompileContext<Row>) => readonly { addTo: (l: never) => void }[] },
|
|
41
|
+
ctx: CompileContext<Row>,
|
|
42
|
+
): { polylines: Array<{ x: number; y: number }[]>; circles: Array<{ cx: number; cy: number }> } {
|
|
43
|
+
const polylines: Array<{ x: number; y: number }[]> = [];
|
|
44
|
+
const circles: Array<{ cx: number; cy: number }> = [];
|
|
45
|
+
const layer = {
|
|
46
|
+
pushPolyline({ points }: { points: Array<{ x: number; y: number }> }) {
|
|
47
|
+
polylines.push(points);
|
|
48
|
+
},
|
|
49
|
+
pushCircle(shape: { cx: number; cy: number }) {
|
|
50
|
+
circles.push(shape);
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
for (const builder of geom.compile(ctx)) builder.addTo(layer as never);
|
|
54
|
+
return { polylines, circles };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
describe("connectedScatter geom — factory composition", () => {
|
|
58
|
+
test("default returns a line geom followed by a point geom", () => {
|
|
59
|
+
const geoms = connectedScatter<Row>({ x: "x", y: "y", order: "order" });
|
|
60
|
+
expect(geoms).toHaveLength(2);
|
|
61
|
+
expect(geoms[0]!.kind).toBe("line");
|
|
62
|
+
expect(geoms[1]!.kind).toBe("point");
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("point: false renders only the connecting path", () => {
|
|
66
|
+
const geoms = connectedScatter<Row>({ x: "x", y: "y", order: "order" }, { point: false });
|
|
67
|
+
expect(geoms).toHaveLength(1);
|
|
68
|
+
expect(geoms[0]!.kind).toBe("line");
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("threads x/y channels into both layers", () => {
|
|
72
|
+
const geoms = connectedScatter<Row>({ x: "x", y: "y", order: "order" });
|
|
73
|
+
for (const g of geoms) {
|
|
74
|
+
expect(g.channels.x).toBe("x");
|
|
75
|
+
expect(g.channels.y).toBe("y");
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("color channel propagates to both line and point", () => {
|
|
80
|
+
const geoms = connectedScatter<Row>({ x: "x", y: "y", order: "order", color: "series" });
|
|
81
|
+
expect(geoms[0]!.channels.color).toBe("series");
|
|
82
|
+
expect(geoms[1]!.channels.color).toBe("series");
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("size/shape/alpha channels go only to the point layer", () => {
|
|
86
|
+
const geoms = connectedScatter<Row>({
|
|
87
|
+
x: "x",
|
|
88
|
+
y: "y",
|
|
89
|
+
order: "order",
|
|
90
|
+
size: "y",
|
|
91
|
+
shape: "series",
|
|
92
|
+
alpha: "y",
|
|
93
|
+
});
|
|
94
|
+
const pointGeom = geoms[1]!;
|
|
95
|
+
expect(pointGeom.channels.size).toBe("y");
|
|
96
|
+
expect(pointGeom.channels.shape).toBe("series");
|
|
97
|
+
expect(pointGeom.channels.alpha).toBe("y");
|
|
98
|
+
// The line layer carries none of these point-only channels.
|
|
99
|
+
expect(geoms[0]!.channels.size).toBeUndefined();
|
|
100
|
+
expect(geoms[0]!.channels.shape).toBeUndefined();
|
|
101
|
+
expect(geoms[0]!.channels.alpha).toBeUndefined();
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
describe("connectedScatter geom — compile output", () => {
|
|
106
|
+
test("line layer draws one ordered polyline through the scaled vertices", () => {
|
|
107
|
+
const geoms = connectedScatter<Row>({ x: "x", y: "y", order: "order" });
|
|
108
|
+
const ctx = makeCtx(data);
|
|
109
|
+
const { polylines } = captureShapes(geoms[0]!, ctx);
|
|
110
|
+
expect(polylines).toHaveLength(1);
|
|
111
|
+
// x: 0,5,10 over domain [0,10] → range [0,100] → 0,50,100; +plot.x(50).
|
|
112
|
+
expect(polylines[0]!.map((p) => p.x)).toEqual([50, 100, 150]);
|
|
113
|
+
// y: 0,10,20 over domain [0,20] → range [200,0] → 200,100,0; +plot.y(30).
|
|
114
|
+
expect(polylines[0]!.map((p) => p.y)).toEqual([230, 130, 30]);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test("ordering aesthetic sorts the path even when rows arrive unordered", () => {
|
|
118
|
+
const shuffled: Row[] = [
|
|
119
|
+
{ x: 10, y: 20, series: "a", order: 3 },
|
|
120
|
+
{ x: 0, y: 0, series: "a", order: 1 },
|
|
121
|
+
{ x: 5, y: 10, series: "a", order: 2 },
|
|
122
|
+
];
|
|
123
|
+
const geoms = connectedScatter<Row>({ x: "x", y: "y", order: "order" });
|
|
124
|
+
const { polylines } = captureShapes(geoms[0]!, makeCtx(shuffled));
|
|
125
|
+
expect(polylines).toHaveLength(1);
|
|
126
|
+
expect(polylines[0]!.map((p) => p.x)).toEqual([50, 100, 150]);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test("point layer emits one circle per datum at the scaled anchor", () => {
|
|
130
|
+
const geoms = connectedScatter<Row>({ x: "x", y: "y", order: "order" });
|
|
131
|
+
const { circles } = captureShapes(geoms[1]!, makeCtx(data));
|
|
132
|
+
expect(circles).toHaveLength(3);
|
|
133
|
+
expect(circles.map((c) => c.cx)).toEqual([50, 100, 150]);
|
|
134
|
+
expect(circles.map((c) => c.cy)).toEqual([230, 130, 30]);
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
describe("connectedScatter geom — edge cases", () => {
|
|
139
|
+
test("empty data: both layers compile to no drawn shapes", () => {
|
|
140
|
+
const geoms = connectedScatter<Row>({ x: "x", y: "y", order: "order" });
|
|
141
|
+
const ctx = makeCtx([]);
|
|
142
|
+
const line = captureShapes(geoms[0]!, ctx);
|
|
143
|
+
const points = captureShapes(geoms[1]!, ctx);
|
|
144
|
+
expect(line.polylines).toHaveLength(0);
|
|
145
|
+
expect(points.circles).toHaveLength(0);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test("single point: no polyline (one vertex), one circle", () => {
|
|
149
|
+
const single: Row[] = [{ x: 5, y: 10, series: "a", order: 1 }];
|
|
150
|
+
const geoms = connectedScatter<Row>({ x: "x", y: "y", order: "order" });
|
|
151
|
+
const ctx = makeCtx(single);
|
|
152
|
+
const line = captureShapes(geoms[0]!, ctx);
|
|
153
|
+
const points = captureShapes(geoms[1]!, ctx);
|
|
154
|
+
expect(line.polylines).toHaveLength(0);
|
|
155
|
+
expect(points.circles).toHaveLength(1);
|
|
156
|
+
});
|
|
157
|
+
});
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { Aes } from "../aes.ts";
|
|
2
|
+
import { line, type LineOptions } from "./line.ts";
|
|
3
|
+
import { point, type PointOptions } from "./point.ts";
|
|
4
|
+
import type { Geom } from "./types.ts";
|
|
5
|
+
|
|
6
|
+
export interface ConnectedScatterChannels<T> {
|
|
7
|
+
x: Aes<T, number | Date>;
|
|
8
|
+
y: Aes<T, number | Date>;
|
|
9
|
+
color?: Aes<T, unknown>;
|
|
10
|
+
order: Aes<T, number | Date>;
|
|
11
|
+
size?: Aes<T, number>;
|
|
12
|
+
shape?: Aes<T, unknown>;
|
|
13
|
+
alpha?: Aes<T, number>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface ConnectedScatterOptions {
|
|
17
|
+
/** Line-layer options. */
|
|
18
|
+
line?: LineOptions;
|
|
19
|
+
/** Point-layer options. Pass `false` to render only the connecting path. */
|
|
20
|
+
point?: PointOptions | false;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Grammar helper for the common "connected scatterplot" recipe:
|
|
25
|
+
* a path ordered by a third variable, optionally topped with points.
|
|
26
|
+
*
|
|
27
|
+
* Returns plain geoms so callers still compose with `.layer(text(...))`,
|
|
28
|
+
* `.annotate(...)`, facets, transitions, and the normal interaction stack.
|
|
29
|
+
*/
|
|
30
|
+
export function connectedScatter<T>(
|
|
31
|
+
channels: ConnectedScatterChannels<T>,
|
|
32
|
+
options: ConnectedScatterOptions = {},
|
|
33
|
+
): readonly Geom<T>[] {
|
|
34
|
+
const out: Geom<T>[] = [
|
|
35
|
+
line(
|
|
36
|
+
{
|
|
37
|
+
x: channels.x,
|
|
38
|
+
y: channels.y,
|
|
39
|
+
color: channels.color,
|
|
40
|
+
order: channels.order,
|
|
41
|
+
},
|
|
42
|
+
options.line,
|
|
43
|
+
),
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
if (options.point !== false) {
|
|
47
|
+
out.push(
|
|
48
|
+
point(
|
|
49
|
+
{
|
|
50
|
+
x: channels.x,
|
|
51
|
+
y: channels.y,
|
|
52
|
+
color: channels.color,
|
|
53
|
+
size: channels.size,
|
|
54
|
+
shape: channels.shape,
|
|
55
|
+
alpha: channels.alpha,
|
|
56
|
+
},
|
|
57
|
+
options.point,
|
|
58
|
+
),
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return out;
|
|
63
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/** Per-geom ordinal capacity / band width. See module header. */
|
|
2
|
+
export declare const EMPHASIS_GEOM_STRIDE: number;
|
|
3
|
+
/**
|
|
4
|
+
* Geom kinds whose hover dim-others treatment rides the core's ANIMATED GPU
|
|
5
|
+
* emphasis uniform (P5-T3): they tag each mark instance with a stable emphasis
|
|
6
|
+
* key at compile time (see {@link emphasisContext}), and the mount drives
|
|
7
|
+
* `renderer.setEmphasis({ focusedKey, dimAlpha, t })` from the hover signal —
|
|
8
|
+
* zero marks recompile per hover frame. A chart containing ANY of these (with
|
|
9
|
+
* hover enabled) pins its marks layer to `cache:"never"` (a bake would freeze
|
|
10
|
+
* the no-op emphasis; live marks let the uniform dim them — see the mount's
|
|
11
|
+
* `marksCacheHint`).
|
|
12
|
+
*
|
|
13
|
+
* `boxplot` / `violin` / `ridgeline` / `rug` joined this set once the core's
|
|
14
|
+
* emphasis dim reached `pushPolygon` (polygon-fill keys) — they tag a whole
|
|
15
|
+
* logical entity (box / violin / ridge row / tick) with ONE key so it dims as a
|
|
16
|
+
* unit. The remaining hover-INERT geoms are the `nearestX` curve geoms
|
|
17
|
+
* (`rolling`, `area`, whose compile-time halo updates only on a full frame) and
|
|
18
|
+
* `point` (its focus halo rides the overlay decorator). bar/histogram/tile/line
|
|
19
|
+
* ALSO carry overlay focus-halo decorators — dim + halo coexist: the dim is the
|
|
20
|
+
* global uniform, the halo is a `cache:"never"` overlay shape left at key 0
|
|
21
|
+
* (exempt) so it stays full-strength.
|
|
22
|
+
*/
|
|
23
|
+
export declare const GPU_DIM_GEOM_KINDS: ReadonlySet<string>;
|
|
24
|
+
/**
|
|
25
|
+
* Largest geom index whose key band stays inside u32. `geomEmphasisBase(gi)` is
|
|
26
|
+
* `(gi+1) * 2^20`; `emphasisKeyFor` adds up to `(2^20 - 1) + 1 = 2^20`. So the
|
|
27
|
+
* max key for geom `gi` is `(gi+2) * 2^20`. The u32 ceiling is `2^32 - 1`, i.e.
|
|
28
|
+
* `4096 * 2^20`, so we need `(gi+2) * 2^20 <= 4096 * 2^20`, i.e. `gi <= 4094`.
|
|
29
|
+
* At `gi = 4094` the max key is exactly `4096 * 2^20 = 2^32`, which overflows
|
|
30
|
+
* u32 → wraps to 0 (the silent EXEMPT sentinel — a non-dimming hole). So the
|
|
31
|
+
* last SAFE geom index is `4093` and `gi >= 4094` is rejected by
|
|
32
|
+
* {@link geomEmphasisBase}.
|
|
33
|
+
*/
|
|
34
|
+
export declare const EMPHASIS_MAX_GEOM_INDEX = 4093;
|
|
35
|
+
/**
|
|
36
|
+
* Disjoint emphasis-key band base for the geom at `geomIndex` in the chart's
|
|
37
|
+
* layer list (the pipeline's `gi`). `geomIndex + 1` so geom 0 starts at one
|
|
38
|
+
* full stride (key 0 stays the opt-out sentinel for axis / grid / overlay).
|
|
39
|
+
*/
|
|
40
|
+
export declare function geomEmphasisBase(geomIndex: number): number;
|
|
41
|
+
/**
|
|
42
|
+
* Emphasis key for the `ordinal`-th participating instance of a geom whose band
|
|
43
|
+
* starts at `base`. `ordinal + 1` keeps the band's first instance off key 0.
|
|
44
|
+
* `ordinal` is taken mod the stride so a runaway count can never spill into the
|
|
45
|
+
* next geom's band (soundness floor — see module header).
|
|
46
|
+
*/
|
|
47
|
+
export declare function emphasisKeyFor(base: number, ordinal: number): number;
|
|
48
|
+
/**
|
|
49
|
+
* A geom's hover-dim emphasis-key resolver, captured at compile time so the
|
|
50
|
+
* mount can map an active {@link HoveredHit} to the namespaced key it tagged
|
|
51
|
+
* its focused instance(s) with — WITHOUT recompiling. `geomKind` + `data`
|
|
52
|
+
* identity match the resolver to the hit (the same keys that route a hit to its
|
|
53
|
+
* geom, mirroring {@link GeomHoverDecorator}). `resolve` returns the focused
|
|
54
|
+
* key, or `null` when this hit focuses nothing in the geom (the mount then
|
|
55
|
+
* leaves emphasis settled). The geom computes the SAME ordinal it used to tag.
|
|
56
|
+
*/
|
|
57
|
+
export interface EmphasisResolver {
|
|
58
|
+
readonly geomKind: import("./types.ts").GeomKind;
|
|
59
|
+
readonly data: readonly unknown[];
|
|
60
|
+
resolve(hit: import("./types.ts").HoveredHit): number | null;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Build the per-frame emphasis context a dim-participating geom uses to tag and
|
|
64
|
+
* resolve. Returns `null` when emphasis is off (`emphasisBase` absent, e.g.
|
|
65
|
+
* SSR/SVG, or `theme.interactions.hover.enabled === false`) so callers cheaply
|
|
66
|
+
* skip all tagging. `ordinalOf` maps a hit (or an instance's identity) to the
|
|
67
|
+
* geom's ordinal — by default the hit's `dataIndex`, which every single-series
|
|
68
|
+
* dim geom (bar-single/histogram/boxplot/violin/tile/ridgeline/rug) already
|
|
69
|
+
* uses as its compile index. Multi-series geoms pass a custom `ordinalOf`.
|
|
70
|
+
*/
|
|
71
|
+
export declare function emphasisContext<T>(ctx: import("./types.ts").CompileContext<T>, kind: import("./types.ts").GeomKind, ordinalOf?: (hit: import("./types.ts").HoveredHit) => number | null): {
|
|
72
|
+
/** Namespaced key for the instance at `ordinal`. Tag marks with this. */
|
|
73
|
+
keyFor(ordinal: number): number;
|
|
74
|
+
/** Resolver to register so the mount can map a hit → focused key. */
|
|
75
|
+
resolver(): EmphasisResolver;
|
|
76
|
+
} | null;
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { describe, expect, test } from "vite-plus/test";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
EMPHASIS_GEOM_STRIDE,
|
|
5
|
+
EMPHASIS_MAX_GEOM_INDEX,
|
|
6
|
+
GPU_DIM_GEOM_KINDS,
|
|
7
|
+
emphasisContext,
|
|
8
|
+
emphasisKeyFor,
|
|
9
|
+
geomEmphasisBase,
|
|
10
|
+
} from "./emphasis.ts";
|
|
11
|
+
import { themeDefault } from "../theme.ts";
|
|
12
|
+
import type { CompileContext, HoveredHit } from "./types.ts";
|
|
13
|
+
|
|
14
|
+
describe("emphasis-key namespacing (P5-T3)", () => {
|
|
15
|
+
test("geom bands are disjoint full strides; geom 0 starts at one stride", () => {
|
|
16
|
+
expect(geomEmphasisBase(0)).toBe(EMPHASIS_GEOM_STRIDE);
|
|
17
|
+
expect(geomEmphasisBase(1)).toBe(2 * EMPHASIS_GEOM_STRIDE);
|
|
18
|
+
expect(geomEmphasisBase(2)).toBe(3 * EMPHASIS_GEOM_STRIDE);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test("keyFor offsets ordinal+1 so ordinal 0 never collides with the sentinel 0", () => {
|
|
22
|
+
const base = geomEmphasisBase(0);
|
|
23
|
+
expect(emphasisKeyFor(base, 0)).toBe(base + 1);
|
|
24
|
+
expect(emphasisKeyFor(base, 5)).toBe(base + 6);
|
|
25
|
+
// No geom's key is ever 0 (the EXEMPT/opt-out sentinel).
|
|
26
|
+
expect(emphasisKeyFor(geomEmphasisBase(0), 0)).toBeGreaterThan(0);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test("two distinct geoms never share a key for the same ordinal", () => {
|
|
30
|
+
const a = emphasisKeyFor(geomEmphasisBase(0), 3);
|
|
31
|
+
const b = emphasisKeyFor(geomEmphasisBase(1), 3);
|
|
32
|
+
expect(a).not.toBe(b);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("ordinal at/over the stride wraps (stays inside the geom's band)", () => {
|
|
36
|
+
const base = geomEmphasisBase(0);
|
|
37
|
+
// ordinal === stride wraps to ordinal 0's key (mod), never spilling into geom 1.
|
|
38
|
+
expect(emphasisKeyFor(base, EMPHASIS_GEOM_STRIDE)).toBe(emphasisKeyFor(base, 0));
|
|
39
|
+
expect(emphasisKeyFor(base, EMPHASIS_GEOM_STRIDE)).toBeLessThan(geomEmphasisBase(1));
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// Fix E — the key-band ceiling. At gi=4094 the MAX key reaches exactly 2^32 and
|
|
43
|
+
// wraps to 0 (the silent EXEMPT sentinel). So the last SAFE index is 4093.
|
|
44
|
+
test("the last safe geom index keeps its max key inside u32", () => {
|
|
45
|
+
expect(EMPHASIS_MAX_GEOM_INDEX).toBe(4093);
|
|
46
|
+
const base = geomEmphasisBase(EMPHASIS_MAX_GEOM_INDEX);
|
|
47
|
+
const maxKey = emphasisKeyFor(base, EMPHASIS_GEOM_STRIDE - 1);
|
|
48
|
+
// The largest key for the last safe geom is < 2^32 (no wrap).
|
|
49
|
+
expect(maxKey).toBeLessThan(2 ** 32);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("geomEmphasisBase throws past the ceiling (would overflow u32 → exempt-0 hole)", () => {
|
|
53
|
+
expect(() => geomEmphasisBase(EMPHASIS_MAX_GEOM_INDEX)).not.toThrow();
|
|
54
|
+
expect(() => geomEmphasisBase(EMPHASIS_MAX_GEOM_INDEX + 1)).toThrow(RangeError);
|
|
55
|
+
expect(() => geomEmphasisBase(10000)).toThrow(/ceiling/);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe("GPU_DIM_GEOM_KINDS (P5-T3 — single source of truth)", () => {
|
|
60
|
+
test("includes the four polygon-fill geoms plus the original four", () => {
|
|
61
|
+
expect([...GPU_DIM_GEOM_KINDS].sort()).toEqual(
|
|
62
|
+
["bar", "boxplot", "histogram", "line", "ridgeline", "rug", "tile", "violin"].sort(),
|
|
63
|
+
);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test("excludes the deliberately hover-inert geoms (point / area / rolling)", () => {
|
|
67
|
+
expect(GPU_DIM_GEOM_KINDS.has("point")).toBe(false);
|
|
68
|
+
expect(GPU_DIM_GEOM_KINDS.has("area")).toBe(false);
|
|
69
|
+
expect(GPU_DIM_GEOM_KINDS.has("rolling")).toBe(false);
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
describe("emphasisContext", () => {
|
|
74
|
+
function ctx(over: Partial<CompileContext<unknown>> = {}): CompileContext<unknown> {
|
|
75
|
+
return {
|
|
76
|
+
data: [{}, {}, {}],
|
|
77
|
+
scales: {} as never,
|
|
78
|
+
plot: {} as never,
|
|
79
|
+
theme: themeDefault,
|
|
80
|
+
atlas: undefined,
|
|
81
|
+
emphasisBase: geomEmphasisBase(0),
|
|
82
|
+
...over,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
const hit = (dataIndex: number): HoveredHit => ({
|
|
86
|
+
geomKind: "bar",
|
|
87
|
+
dataIndex,
|
|
88
|
+
data: ctx().data,
|
|
89
|
+
x: 0,
|
|
90
|
+
y: 0,
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test("returns null when emphasisBase is absent (SSR/SVG)", () => {
|
|
94
|
+
expect(emphasisContext(ctx({ emphasisBase: undefined }), "bar")).toBeNull();
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test("returns null when hover emphasis is disabled in the theme", () => {
|
|
98
|
+
const theme = {
|
|
99
|
+
...themeDefault,
|
|
100
|
+
interactions: {
|
|
101
|
+
...themeDefault.interactions,
|
|
102
|
+
hover: { ...themeDefault.interactions.hover, enabled: false },
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
expect(emphasisContext(ctx({ theme }), "bar")).toBeNull();
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test("keyFor uses the geom's band; default ordinalOf = hit.dataIndex", () => {
|
|
109
|
+
const data = [{}, {}, {}];
|
|
110
|
+
const c = ctx({ data });
|
|
111
|
+
const emph = emphasisContext(c, "bar")!;
|
|
112
|
+
const base = geomEmphasisBase(0);
|
|
113
|
+
expect(emph.keyFor(2)).toBe(emphasisKeyFor(base, 2));
|
|
114
|
+
const res = emph.resolver();
|
|
115
|
+
expect(res.geomKind).toBe("bar");
|
|
116
|
+
expect(res.data).toBe(data);
|
|
117
|
+
expect(res.resolve({ geomKind: "bar", dataIndex: 1, data, x: 0, y: 0 })).toBe(
|
|
118
|
+
emphasisKeyFor(base, 1),
|
|
119
|
+
);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test("resolver rejects hits from a different geom kind or data array", () => {
|
|
123
|
+
const data = [{}, {}];
|
|
124
|
+
const emph = emphasisContext(ctx({ data }), "bar")!;
|
|
125
|
+
const res = emph.resolver();
|
|
126
|
+
expect(res.resolve({ geomKind: "tile", dataIndex: 0, data, x: 0, y: 0 })).toBeNull();
|
|
127
|
+
expect(res.resolve({ geomKind: "bar", dataIndex: 0, data: [{}], x: 0, y: 0 })).toBeNull();
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test("custom ordinalOf returning null → resolver returns null (focuses nothing)", () => {
|
|
131
|
+
const data = [{}, {}];
|
|
132
|
+
const emph = emphasisContext(ctx({ data }), "bar", () => null)!;
|
|
133
|
+
expect(emph.resolver().resolve(hit(0))).toBeNull();
|
|
134
|
+
});
|
|
135
|
+
});
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// Emphasis-key namespacing (P5-T3 — animated GPU hover dim)
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// Dim-participating geoms (bar / histogram / boxplot / violin / tile /
|
|
5
|
+
// ridgeline / rug / line) tag each mark instance with a STABLE per-instance
|
|
6
|
+
// emphasis key at compile time (hover-independent). The core renderer writes
|
|
7
|
+
// that key into the instance's `lane4`; `renderer.setEmphasis({ focusedKey,
|
|
8
|
+
// dimAlpha, t })` then dims — on the transparent/OIT path only — every TAGGED
|
|
9
|
+
// instance whose key differs from `focusedKey`, animated by `t` 0→1. Zero marks
|
|
10
|
+
// recompile per hover frame (the old "snap" rebake is gone).
|
|
11
|
+
//
|
|
12
|
+
// KEY NAMESPACING (the whole-frame budget). Keys are GLOBAL per frame across
|
|
13
|
+
// every layer (one emphasis uniform), so two geoms must never collide. Each
|
|
14
|
+
// geom gets a disjoint BASE band; the geom adds `ordinal + 1` within its band.
|
|
15
|
+
//
|
|
16
|
+
// base(geomIndex) = (geomIndex + 1) * EMPHASIS_GEOM_STRIDE
|
|
17
|
+
// key(base, ordinal) = base + (ordinal mod EMPHASIS_GEOM_STRIDE) + 1
|
|
18
|
+
//
|
|
19
|
+
// The `+ 1` keeps key 0 reserved as the EXEMPT/opt-out sentinel (core contract:
|
|
20
|
+
// a key-0 instance never dims). `geomIndex + 1` keeps geom 0 off the sentinel
|
|
21
|
+
// band too. With a u32 emphasis key:
|
|
22
|
+
//
|
|
23
|
+
// • EMPHASIS_GEOM_STRIDE = 2^20 → up to 1,048,575 distinct ordinals per geom
|
|
24
|
+
// (more than enough for per-row / per-segment marks; a chart with a million
|
|
25
|
+
// visible marks is already past the SDF instance budget).
|
|
26
|
+
// • geom band index up to 4093 (inclusive) per chart — the max key for geom
|
|
27
|
+
// `gi` is `(gi+2) * 2^20`, which must stay <= u32 max `2^32 - 1 < 4096*2^20`,
|
|
28
|
+
// so `gi <= 4094` keeps the BASE in range but `gi = 4094` makes the max KEY
|
|
29
|
+
// exactly `2^32` (wraps to 0, the silent EXEMPT sentinel). Hence the last
|
|
30
|
+
// SAFE index is 4093; `geomEmphasisBase` throws on `gi >= 4094` (which no
|
|
31
|
+
// real chart reaches). See `EMPHASIS_MAX_GEOM_INDEX`.
|
|
32
|
+
//
|
|
33
|
+
// An ordinal at or beyond the stride wraps (mod), trading a (cosmetic) dim
|
|
34
|
+
// collision for never escaping the geom's band — soundness over fidelity at a
|
|
35
|
+
// scale no real chart reaches. `ordinal` is whatever index the geom reports as
|
|
36
|
+
// the hit's `dataIndex` (per row for bars/tiles/box/violin/ridgeline, the flat
|
|
37
|
+
// cell counter for histogram) — so tagging and hit-resolution share one source.
|
|
38
|
+
|
|
39
|
+
/** Per-geom ordinal capacity / band width. See module header. */
|
|
40
|
+
export const EMPHASIS_GEOM_STRIDE = 2 ** 20;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Geom kinds whose hover dim-others treatment rides the core's ANIMATED GPU
|
|
44
|
+
* emphasis uniform (P5-T3): they tag each mark instance with a stable emphasis
|
|
45
|
+
* key at compile time (see {@link emphasisContext}), and the mount drives
|
|
46
|
+
* `renderer.setEmphasis({ focusedKey, dimAlpha, t })` from the hover signal —
|
|
47
|
+
* zero marks recompile per hover frame. A chart containing ANY of these (with
|
|
48
|
+
* hover enabled) pins its marks layer to `cache:"never"` (a bake would freeze
|
|
49
|
+
* the no-op emphasis; live marks let the uniform dim them — see the mount's
|
|
50
|
+
* `marksCacheHint`).
|
|
51
|
+
*
|
|
52
|
+
* `boxplot` / `violin` / `ridgeline` / `rug` joined this set once the core's
|
|
53
|
+
* emphasis dim reached `pushPolygon` (polygon-fill keys) — they tag a whole
|
|
54
|
+
* logical entity (box / violin / ridge row / tick) with ONE key so it dims as a
|
|
55
|
+
* unit. The remaining hover-INERT geoms are the `nearestX` curve geoms
|
|
56
|
+
* (`rolling`, `area`, whose compile-time halo updates only on a full frame) and
|
|
57
|
+
* `point` (its focus halo rides the overlay decorator). bar/histogram/tile/line
|
|
58
|
+
* ALSO carry overlay focus-halo decorators — dim + halo coexist: the dim is the
|
|
59
|
+
* global uniform, the halo is a `cache:"never"` overlay shape left at key 0
|
|
60
|
+
* (exempt) so it stays full-strength.
|
|
61
|
+
*/
|
|
62
|
+
export const GPU_DIM_GEOM_KINDS: ReadonlySet<string> = new Set([
|
|
63
|
+
"bar",
|
|
64
|
+
"histogram",
|
|
65
|
+
"tile",
|
|
66
|
+
"line",
|
|
67
|
+
"boxplot",
|
|
68
|
+
"violin",
|
|
69
|
+
"ridgeline",
|
|
70
|
+
"rug",
|
|
71
|
+
]);
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Largest geom index whose key band stays inside u32. `geomEmphasisBase(gi)` is
|
|
75
|
+
* `(gi+1) * 2^20`; `emphasisKeyFor` adds up to `(2^20 - 1) + 1 = 2^20`. So the
|
|
76
|
+
* max key for geom `gi` is `(gi+2) * 2^20`. The u32 ceiling is `2^32 - 1`, i.e.
|
|
77
|
+
* `4096 * 2^20`, so we need `(gi+2) * 2^20 <= 4096 * 2^20`, i.e. `gi <= 4094`.
|
|
78
|
+
* At `gi = 4094` the max key is exactly `4096 * 2^20 = 2^32`, which overflows
|
|
79
|
+
* u32 → wraps to 0 (the silent EXEMPT sentinel — a non-dimming hole). So the
|
|
80
|
+
* last SAFE geom index is `4093` and `gi >= 4094` is rejected by
|
|
81
|
+
* {@link geomEmphasisBase}.
|
|
82
|
+
*/
|
|
83
|
+
export const EMPHASIS_MAX_GEOM_INDEX = 4093;
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Disjoint emphasis-key band base for the geom at `geomIndex` in the chart's
|
|
87
|
+
* layer list (the pipeline's `gi`). `geomIndex + 1` so geom 0 starts at one
|
|
88
|
+
* full stride (key 0 stays the opt-out sentinel for axis / grid / overlay).
|
|
89
|
+
*/
|
|
90
|
+
export function geomEmphasisBase(geomIndex: number): number {
|
|
91
|
+
// Defensive guard: at `geomIndex >= 4094` the max emphasis KEY reaches/exceeds
|
|
92
|
+
// 2^32 and wraps to 0 (the EXEMPT sentinel) — a silent non-dimming hole. No
|
|
93
|
+
// real chart has 4094 geoms; reject it loudly rather than emit corrupt keys.
|
|
94
|
+
if (geomIndex >= EMPHASIS_MAX_GEOM_INDEX + 1) {
|
|
95
|
+
throw new RangeError(
|
|
96
|
+
`geomEmphasisBase: geomIndex ${geomIndex} exceeds the emphasis key-band ceiling ` +
|
|
97
|
+
`(${EMPHASIS_MAX_GEOM_INDEX}); keys would overflow u32 and wrap to the exempt sentinel.`,
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
return (geomIndex + 1) * EMPHASIS_GEOM_STRIDE;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Emphasis key for the `ordinal`-th participating instance of a geom whose band
|
|
105
|
+
* starts at `base`. `ordinal + 1` keeps the band's first instance off key 0.
|
|
106
|
+
* `ordinal` is taken mod the stride so a runaway count can never spill into the
|
|
107
|
+
* next geom's band (soundness floor — see module header).
|
|
108
|
+
*/
|
|
109
|
+
export function emphasisKeyFor(base: number, ordinal: number): number {
|
|
110
|
+
return base + (ordinal % EMPHASIS_GEOM_STRIDE) + 1;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* A geom's hover-dim emphasis-key resolver, captured at compile time so the
|
|
115
|
+
* mount can map an active {@link HoveredHit} to the namespaced key it tagged
|
|
116
|
+
* its focused instance(s) with — WITHOUT recompiling. `geomKind` + `data`
|
|
117
|
+
* identity match the resolver to the hit (the same keys that route a hit to its
|
|
118
|
+
* geom, mirroring {@link GeomHoverDecorator}). `resolve` returns the focused
|
|
119
|
+
* key, or `null` when this hit focuses nothing in the geom (the mount then
|
|
120
|
+
* leaves emphasis settled). The geom computes the SAME ordinal it used to tag.
|
|
121
|
+
*/
|
|
122
|
+
export interface EmphasisResolver {
|
|
123
|
+
readonly geomKind: import("./types.ts").GeomKind;
|
|
124
|
+
readonly data: readonly unknown[];
|
|
125
|
+
resolve(hit: import("./types.ts").HoveredHit): number | null;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Build the per-frame emphasis context a dim-participating geom uses to tag and
|
|
130
|
+
* resolve. Returns `null` when emphasis is off (`emphasisBase` absent, e.g.
|
|
131
|
+
* SSR/SVG, or `theme.interactions.hover.enabled === false`) so callers cheaply
|
|
132
|
+
* skip all tagging. `ordinalOf` maps a hit (or an instance's identity) to the
|
|
133
|
+
* geom's ordinal — by default the hit's `dataIndex`, which every single-series
|
|
134
|
+
* dim geom (bar-single/histogram/boxplot/violin/tile/ridgeline/rug) already
|
|
135
|
+
* uses as its compile index. Multi-series geoms pass a custom `ordinalOf`.
|
|
136
|
+
*/
|
|
137
|
+
export function emphasisContext<T>(
|
|
138
|
+
ctx: import("./types.ts").CompileContext<T>,
|
|
139
|
+
kind: import("./types.ts").GeomKind,
|
|
140
|
+
ordinalOf: (hit: import("./types.ts").HoveredHit) => number | null = (hit) => hit.dataIndex,
|
|
141
|
+
): {
|
|
142
|
+
/** Namespaced key for the instance at `ordinal`. Tag marks with this. */
|
|
143
|
+
keyFor(ordinal: number): number;
|
|
144
|
+
/** Resolver to register so the mount can map a hit → focused key. */
|
|
145
|
+
resolver(): EmphasisResolver;
|
|
146
|
+
} | null {
|
|
147
|
+
const base = ctx.emphasisBase;
|
|
148
|
+
if (base === undefined || !ctx.theme.interactions.hover.enabled) return null;
|
|
149
|
+
const data = ctx.data as readonly unknown[];
|
|
150
|
+
return {
|
|
151
|
+
keyFor: (ordinal) => emphasisKeyFor(base, ordinal),
|
|
152
|
+
resolver: () => ({
|
|
153
|
+
geomKind: kind,
|
|
154
|
+
data,
|
|
155
|
+
resolve(hit) {
|
|
156
|
+
if (hit.geomKind !== kind || hit.data !== data) return null;
|
|
157
|
+
const ordinal = ordinalOf(hit);
|
|
158
|
+
return ordinal === null ? null : emphasisKeyFor(base, ordinal);
|
|
159
|
+
},
|
|
160
|
+
}),
|
|
161
|
+
};
|
|
162
|
+
}
|