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,172 @@
|
|
|
1
|
+
import { createInvalidator } from "insomni";
|
|
2
|
+
import { linear } from "insomni";
|
|
3
|
+
import { describe, expect, test } from "vite-plus/test";
|
|
4
|
+
|
|
5
|
+
import type { GeomFrame } from "../geoms/types.ts";
|
|
6
|
+
import { createGrammarTransitions } from "./transitions.ts";
|
|
7
|
+
|
|
8
|
+
function makeFrame(count: number, xVal = 0, yVal = 0, ids?: string[]): GeomFrame {
|
|
9
|
+
const x = new Float32Array(count).fill(xVal);
|
|
10
|
+
const y = new Float32Array(count).fill(yVal);
|
|
11
|
+
const rgba = new Float32Array(count * 4).fill(0.5);
|
|
12
|
+
const a = new Float32Array(count).fill(1);
|
|
13
|
+
return { count, x, y, rgba, a, ids };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
describe("createGrammarTransitions", () => {
|
|
17
|
+
test("starts inactive with no stored frames", () => {
|
|
18
|
+
const inv = createInvalidator(false);
|
|
19
|
+
const t = createGrammarTransitions({ duration: 1, easing: linear, invalidator: inv });
|
|
20
|
+
expect(t.active).toBe(false);
|
|
21
|
+
expect(t.contextFor(0)).toBeUndefined();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test("storeFrame records a frame, notifyChange on empty store is a no-op", () => {
|
|
25
|
+
const inv = createInvalidator(false);
|
|
26
|
+
const t = createGrammarTransitions({ duration: 1, easing: linear, invalidator: inv });
|
|
27
|
+
// notifyChange before any frames = no-op (nothing to transition from)
|
|
28
|
+
t.notifyChange();
|
|
29
|
+
expect(t.active).toBe(false);
|
|
30
|
+
|
|
31
|
+
// storeFrame with no pending change: just stores, no transition
|
|
32
|
+
t.storeFrame(0, makeFrame(3, 10, 20));
|
|
33
|
+
expect(t.active).toBe(false);
|
|
34
|
+
expect(t.contextFor(0)).toBeUndefined();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("notifyChange after storeFrame starts a transition", () => {
|
|
38
|
+
const inv = createInvalidator(false);
|
|
39
|
+
const t = createGrammarTransitions({ duration: 1, easing: linear, invalidator: inv });
|
|
40
|
+
|
|
41
|
+
// First stable frame at x=10
|
|
42
|
+
t.storeFrame(0, makeFrame(2, 10, 0));
|
|
43
|
+
expect(t.active).toBe(false);
|
|
44
|
+
|
|
45
|
+
// Data changes → notify before pipeline runs
|
|
46
|
+
t.notifyChange();
|
|
47
|
+
expect(t.active).toBe(false); // transition hasn't started yet (waiting for first storeFrame)
|
|
48
|
+
|
|
49
|
+
// Pipeline compiles new data → store new frame at x=20
|
|
50
|
+
t.storeFrame(0, makeFrame(2, 20, 0));
|
|
51
|
+
|
|
52
|
+
// Now transition should be active
|
|
53
|
+
expect(t.active).toBe(true);
|
|
54
|
+
|
|
55
|
+
// contextFor returns the from-frame (old x=10) and t=0
|
|
56
|
+
const ctx = t.contextFor(0);
|
|
57
|
+
expect(ctx).toBeDefined();
|
|
58
|
+
expect(ctx!.t).toBe(0); // progress just started
|
|
59
|
+
expect(ctx!.from.x[0]).toBe(10); // from the OLD frame
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("step advances progress; transition ends at duration", () => {
|
|
63
|
+
const inv = createInvalidator(false);
|
|
64
|
+
const t = createGrammarTransitions({ duration: 1, easing: linear, invalidator: inv });
|
|
65
|
+
|
|
66
|
+
t.storeFrame(0, makeFrame(1, 0, 0));
|
|
67
|
+
t.notifyChange();
|
|
68
|
+
t.storeFrame(0, makeFrame(1, 100, 0));
|
|
69
|
+
|
|
70
|
+
expect(t.active).toBe(true);
|
|
71
|
+
|
|
72
|
+
t.step(0.5);
|
|
73
|
+
const ctx = t.contextFor(0);
|
|
74
|
+
expect(ctx!.t).toBeCloseTo(0.5, 5);
|
|
75
|
+
|
|
76
|
+
t.step(0.5);
|
|
77
|
+
expect(t.active).toBe(false);
|
|
78
|
+
expect(t.contextFor(0)).toBeUndefined();
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test("storeFrame during active transition does NOT clobber from-frame stable store", () => {
|
|
82
|
+
const inv = createInvalidator(false);
|
|
83
|
+
const t = createGrammarTransitions({ duration: 1, easing: linear, invalidator: inv });
|
|
84
|
+
|
|
85
|
+
// Stable frame: x=10
|
|
86
|
+
t.storeFrame(0, makeFrame(1, 10, 0));
|
|
87
|
+
t.notifyChange();
|
|
88
|
+
// New frame: x=50
|
|
89
|
+
t.storeFrame(0, makeFrame(1, 50, 0));
|
|
90
|
+
|
|
91
|
+
expect(t.active).toBe(true);
|
|
92
|
+
|
|
93
|
+
// Mid-animation, another storeFrame arrives (e.g. same frame ticked)
|
|
94
|
+
t.step(0.25);
|
|
95
|
+
t.storeFrame(0, makeFrame(1, 50, 0)); // same new values, just updating stable store
|
|
96
|
+
|
|
97
|
+
// from-frame should still be x=10 (not 50)
|
|
98
|
+
const ctx = t.contextFor(0);
|
|
99
|
+
expect(ctx!.from.x[0]).toBe(10);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test("requestTransition behaves same as notifyChange + storeFrame", () => {
|
|
103
|
+
const inv = createInvalidator(false);
|
|
104
|
+
const t = createGrammarTransitions({ duration: 1, easing: linear, invalidator: inv });
|
|
105
|
+
|
|
106
|
+
t.storeFrame(0, makeFrame(1, 5, 0));
|
|
107
|
+
t.requestTransition();
|
|
108
|
+
t.storeFrame(0, makeFrame(1, 15, 0));
|
|
109
|
+
|
|
110
|
+
expect(t.active).toBe(true);
|
|
111
|
+
const ctx = t.contextFor(0);
|
|
112
|
+
expect(ctx!.from.x[0]).toBe(5);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test("no context returned for layer with no stored from-frame", () => {
|
|
116
|
+
const inv = createInvalidator(false);
|
|
117
|
+
const t = createGrammarTransitions({ duration: 1, easing: linear, invalidator: inv });
|
|
118
|
+
|
|
119
|
+
// Store for layer 0 only
|
|
120
|
+
t.storeFrame(0, makeFrame(1, 10, 0));
|
|
121
|
+
t.notifyChange();
|
|
122
|
+
t.storeFrame(0, makeFrame(1, 20, 0));
|
|
123
|
+
|
|
124
|
+
expect(t.active).toBe(true);
|
|
125
|
+
// Layer 1 has no stored frame
|
|
126
|
+
expect(t.contextFor(1)).toBeUndefined();
|
|
127
|
+
// Layer 0 has one
|
|
128
|
+
expect(t.contextFor(0)).toBeDefined();
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test("scopes frames independently for faceted panels", () => {
|
|
132
|
+
const inv = createInvalidator(false);
|
|
133
|
+
const t = createGrammarTransitions({ duration: 1, easing: linear, invalidator: inv });
|
|
134
|
+
|
|
135
|
+
t.storeFrame(0, makeFrame(1, 10, 0), "a");
|
|
136
|
+
t.storeFrame(0, makeFrame(1, 30, 0), "b");
|
|
137
|
+
t.notifyChange();
|
|
138
|
+
t.storeFrame(0, makeFrame(1, 20, 0), "a");
|
|
139
|
+
t.storeFrame(0, makeFrame(1, 40, 0), "b");
|
|
140
|
+
|
|
141
|
+
expect(t.active).toBe(true);
|
|
142
|
+
expect(t.contextFor(0, "a")!.from.x[0]).toBe(10);
|
|
143
|
+
expect(t.contextFor(0, "b")!.from.x[0]).toBe(30);
|
|
144
|
+
expect(t.contextFor(0, "c")).toBeUndefined();
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test("contextFor resolves prior indices by stable ids when present", () => {
|
|
148
|
+
const inv = createInvalidator(false);
|
|
149
|
+
const t = createGrammarTransitions({ duration: 1, easing: linear, invalidator: inv });
|
|
150
|
+
|
|
151
|
+
t.storeFrame(0, makeFrame(2, 10, 0, ["a", "b"]));
|
|
152
|
+
t.notifyChange();
|
|
153
|
+
t.storeFrame(0, makeFrame(2, 20, 0, ["b", "a"]));
|
|
154
|
+
|
|
155
|
+
const ctx = t.contextFor(0)!;
|
|
156
|
+
expect(ctx.matchIndex("a")).toBe(0);
|
|
157
|
+
expect(ctx.matchIndex("b")).toBe(1);
|
|
158
|
+
expect(ctx.matchIndex("c")).toBeUndefined();
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test("step returns true while active, false when done", () => {
|
|
162
|
+
const inv = createInvalidator(false);
|
|
163
|
+
const t = createGrammarTransitions({ duration: 0.5, easing: linear, invalidator: inv });
|
|
164
|
+
|
|
165
|
+
t.storeFrame(0, makeFrame(1));
|
|
166
|
+
t.notifyChange();
|
|
167
|
+
t.storeFrame(0, makeFrame(1));
|
|
168
|
+
|
|
169
|
+
expect(t.step(0.25)).toBe(true);
|
|
170
|
+
expect(t.step(0.25)).toBe(false);
|
|
171
|
+
});
|
|
172
|
+
});
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// GrammarTransitions — data/scale animated transitions for the chart pipeline
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// Manages a single 0→1 progress tween that drives all channel interpolation
|
|
5
|
+
// in geom compile methods. Geoms call `captureFrame` to snapshot their current
|
|
6
|
+
// state; `notifyChange` snapshots those frames as "from" before the next data
|
|
7
|
+
// compile runs; then `contextFor` provides the ActiveTransition to each geom
|
|
8
|
+
// during the animated compile passes.
|
|
9
|
+
|
|
10
|
+
import { transition, type Transition, type Easing } from "insomni";
|
|
11
|
+
import type { Invalidator } from "insomni";
|
|
12
|
+
import type { ActiveTransition, GeomFrame } from "../geoms/types.ts";
|
|
13
|
+
|
|
14
|
+
export interface TransitionsOptions {
|
|
15
|
+
/** Duration in seconds. */
|
|
16
|
+
duration: number;
|
|
17
|
+
easing: Easing;
|
|
18
|
+
invalidator: Invalidator;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface GrammarTransitions {
|
|
22
|
+
/** Advance the progress tween. Returns true while a transition is active. */
|
|
23
|
+
step(dt: number): boolean;
|
|
24
|
+
/**
|
|
25
|
+
* Signal that data/config has changed. If stored frames exist, snapshots
|
|
26
|
+
* them as "from" frames before the new pipeline compiles.
|
|
27
|
+
*/
|
|
28
|
+
notifyChange(): void;
|
|
29
|
+
/**
|
|
30
|
+
* Returns the active transition context for geom at layer index `i`,
|
|
31
|
+
* or undefined when no transition is running or no stored frame exists.
|
|
32
|
+
*/
|
|
33
|
+
contextFor(layerIndex: number, scope?: unknown): ActiveTransition | undefined;
|
|
34
|
+
/**
|
|
35
|
+
* Store the current compiled frame for future "from" use.
|
|
36
|
+
* Always updates the stable store. If a transition was just signalled,
|
|
37
|
+
* kicks off the progress tween.
|
|
38
|
+
*/
|
|
39
|
+
storeFrame(layerIndex: number, frame: GeomFrame, scope?: unknown): void;
|
|
40
|
+
/** True while a transition is running. */
|
|
41
|
+
readonly active: boolean;
|
|
42
|
+
/** Imperatively request a transition on next draw, even without a data change. */
|
|
43
|
+
requestTransition(): void;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function createGrammarTransitions(opts: TransitionsOptions): GrammarTransitions {
|
|
47
|
+
const ROOT_SCOPE = Symbol("root");
|
|
48
|
+
// oxlint-disable-next-line no-redundant-type-constituents -- unknown subsumes typeof ROOT_SCOPE; kept for documentation intent (keys are either the root symbol or a caller-supplied scope value)
|
|
49
|
+
type ScopeKey = typeof ROOT_SCOPE | unknown;
|
|
50
|
+
// Stored frames from the last stable (non-animating) compile, keyed by scope + layer index.
|
|
51
|
+
const stored = new Map<ScopeKey, Map<number, GeomFrame>>();
|
|
52
|
+
// From-frames captured when a transition starts (snapshotted from `stored`).
|
|
53
|
+
const fromFrames = new Map<ScopeKey, Map<number, GeomFrame>>();
|
|
54
|
+
// Whether a change notification arrived and is waiting for the first storeFrame.
|
|
55
|
+
let transitionStarting = false;
|
|
56
|
+
|
|
57
|
+
// Duration must be > 0. If motion is disabled the caller passes a near-zero value.
|
|
58
|
+
const durationSec = Math.max(opts.duration, 0.001);
|
|
59
|
+
|
|
60
|
+
// Single progress transition: 0 → 1.
|
|
61
|
+
// Starts at 1 (not animating) so the first frame is instant.
|
|
62
|
+
const progress: Transition<number> = transition({
|
|
63
|
+
initial: 1,
|
|
64
|
+
duration: durationSec,
|
|
65
|
+
easing: opts.easing,
|
|
66
|
+
invalidator: opts.invalidator,
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
function normalizeScope(scope: unknown): ScopeKey {
|
|
70
|
+
return scope === undefined ? ROOT_SCOPE : scope;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function frameFor(
|
|
74
|
+
source: Map<ScopeKey, Map<number, GeomFrame>>,
|
|
75
|
+
scope: unknown,
|
|
76
|
+
layerIndex: number,
|
|
77
|
+
): GeomFrame | undefined {
|
|
78
|
+
return source.get(normalizeScope(scope))?.get(layerIndex);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function setFrame(
|
|
82
|
+
target: Map<ScopeKey, Map<number, GeomFrame>>,
|
|
83
|
+
scope: unknown,
|
|
84
|
+
layerIndex: number,
|
|
85
|
+
frame: GeomFrame,
|
|
86
|
+
): void {
|
|
87
|
+
const scopeKey = normalizeScope(scope);
|
|
88
|
+
let scoped = target.get(scopeKey);
|
|
89
|
+
if (!scoped) {
|
|
90
|
+
scoped = new Map<number, GeomFrame>();
|
|
91
|
+
target.set(scopeKey, scoped);
|
|
92
|
+
}
|
|
93
|
+
scoped.set(layerIndex, frame);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function copyFrames(
|
|
97
|
+
source: Map<ScopeKey, Map<number, GeomFrame>>,
|
|
98
|
+
target: Map<ScopeKey, Map<number, GeomFrame>>,
|
|
99
|
+
): void {
|
|
100
|
+
target.clear();
|
|
101
|
+
for (const [scope, scoped] of source) {
|
|
102
|
+
target.set(scope, new Map(scoped));
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
get active() {
|
|
108
|
+
return progress.active;
|
|
109
|
+
},
|
|
110
|
+
|
|
111
|
+
step(dt) {
|
|
112
|
+
if (dt <= 0) return progress.active;
|
|
113
|
+
progress.step(dt);
|
|
114
|
+
return progress.active;
|
|
115
|
+
},
|
|
116
|
+
|
|
117
|
+
notifyChange() {
|
|
118
|
+
if (stored.size === 0) return; // No stored frames = first draw, nothing to transition from.
|
|
119
|
+
// Snapshot stored → fromFrames BEFORE the new data compiles.
|
|
120
|
+
copyFrames(stored, fromFrames);
|
|
121
|
+
transitionStarting = true;
|
|
122
|
+
},
|
|
123
|
+
|
|
124
|
+
requestTransition() {
|
|
125
|
+
if (stored.size === 0) return;
|
|
126
|
+
copyFrames(stored, fromFrames);
|
|
127
|
+
transitionStarting = true;
|
|
128
|
+
},
|
|
129
|
+
|
|
130
|
+
contextFor(layerIndex, scope) {
|
|
131
|
+
if (!progress.active) return undefined;
|
|
132
|
+
const from = frameFor(fromFrames, scope, layerIndex);
|
|
133
|
+
if (!from) return undefined;
|
|
134
|
+
const idToIndex = from.ids ? new Map(from.ids.map((id, i) => [id, i] as const)) : null;
|
|
135
|
+
return {
|
|
136
|
+
t: progress.value,
|
|
137
|
+
from,
|
|
138
|
+
matchIndex(key, fallbackIndex) {
|
|
139
|
+
if (idToIndex) return idToIndex.get(key);
|
|
140
|
+
return fallbackIndex !== undefined && fallbackIndex < from.count
|
|
141
|
+
? fallbackIndex
|
|
142
|
+
: undefined;
|
|
143
|
+
},
|
|
144
|
+
};
|
|
145
|
+
},
|
|
146
|
+
|
|
147
|
+
storeFrame(layerIndex, frame, scope) {
|
|
148
|
+
// Always update the stable store with the latest compiled frame so future
|
|
149
|
+
// transitions have current data as their "from".
|
|
150
|
+
setFrame(stored, scope, layerIndex, frame);
|
|
151
|
+
// If a change was signalled, kick off the tween on the first storeFrame call.
|
|
152
|
+
if (transitionStarting) {
|
|
153
|
+
transitionStarting = false;
|
|
154
|
+
progress.setValue(0);
|
|
155
|
+
progress.setTarget(1);
|
|
156
|
+
opts.invalidator.invalidate();
|
|
157
|
+
}
|
|
158
|
+
},
|
|
159
|
+
};
|
|
160
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { type Frame, type GlyphAtlas, type Padding } from "insomni";
|
|
2
|
+
import { type AxisOptions } from "../axis.ts";
|
|
3
|
+
import type { Theme } from "./theme.ts";
|
|
4
|
+
/** Pixel size of a reserved side slot. */
|
|
5
|
+
export interface SlotSize {
|
|
6
|
+
width: number;
|
|
7
|
+
height: number;
|
|
8
|
+
}
|
|
9
|
+
export interface SlotMap {
|
|
10
|
+
top?: SlotSize;
|
|
11
|
+
right?: SlotSize;
|
|
12
|
+
bottom?: SlotSize;
|
|
13
|
+
left?: SlotSize;
|
|
14
|
+
}
|
|
15
|
+
export interface SlotAnchors {
|
|
16
|
+
/** Top-left of the top slot (full outer width above the plot). */
|
|
17
|
+
top?: {
|
|
18
|
+
x: number;
|
|
19
|
+
y: number;
|
|
20
|
+
width: number;
|
|
21
|
+
height: number;
|
|
22
|
+
};
|
|
23
|
+
/** Top-left of the right slot (full plot height to the right of the plot). */
|
|
24
|
+
right?: {
|
|
25
|
+
x: number;
|
|
26
|
+
y: number;
|
|
27
|
+
width: number;
|
|
28
|
+
height: number;
|
|
29
|
+
};
|
|
30
|
+
bottom?: {
|
|
31
|
+
x: number;
|
|
32
|
+
y: number;
|
|
33
|
+
width: number;
|
|
34
|
+
height: number;
|
|
35
|
+
};
|
|
36
|
+
left?: {
|
|
37
|
+
x: number;
|
|
38
|
+
y: number;
|
|
39
|
+
width: number;
|
|
40
|
+
height: number;
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
export interface PlotLayout {
|
|
44
|
+
/** Full canvas frame (outermost). */
|
|
45
|
+
readonly viewport: Frame;
|
|
46
|
+
/** Inset frame after the chart's outer padding. */
|
|
47
|
+
readonly outer: Frame;
|
|
48
|
+
/** Inner plot frame where marks render — axes + slots carved out. */
|
|
49
|
+
readonly plot: Frame;
|
|
50
|
+
/** Top-left + size of each reserved slot, when reserved. */
|
|
51
|
+
readonly slots: SlotAnchors;
|
|
52
|
+
}
|
|
53
|
+
export interface LayoutInput {
|
|
54
|
+
width: number;
|
|
55
|
+
height: number;
|
|
56
|
+
/** Outer-frame inset, in pixels. Default 16. */
|
|
57
|
+
padding?: Padding;
|
|
58
|
+
theme: Theme;
|
|
59
|
+
atlas: GlyphAtlas | undefined;
|
|
60
|
+
/** Sample tick values used to measure axis label width before we know domains. */
|
|
61
|
+
xSample: readonly (number | string | Date)[];
|
|
62
|
+
ySample: readonly (number | string | Date)[];
|
|
63
|
+
xAxisOptions?: AxisOptions<unknown>;
|
|
64
|
+
yAxisOptions?: AxisOptions<unknown>;
|
|
65
|
+
/** Reserved space on each side of the plot frame. */
|
|
66
|
+
slots?: SlotMap;
|
|
67
|
+
}
|
|
68
|
+
export declare function computeLayout(input: LayoutInput): PlotLayout;
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// Layout — turn outer frame + measured axes + slot reservations into a plot frame
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// Two-pass measure (matches what every demo does today):
|
|
5
|
+
// 1. Build axes against placeholder scales just to measure label widths.
|
|
6
|
+
// 2. Subtract those widths + slot reservations (top/right/bottom/left) from
|
|
7
|
+
// the outer frame to get the plot frame; rebuild axes against real scales.
|
|
8
|
+
//
|
|
9
|
+
// Slots replace the per-feature reservation API (title height, legend
|
|
10
|
+
// height/width). Whoever owns a slot — the pipeline composes title + legend
|
|
11
|
+
// as `Placeable`s and measures the outer bbox per side — passes the resulting
|
|
12
|
+
// width/height in here. Layout doesn't know what's in the slot, only how
|
|
13
|
+
// much room it needs.
|
|
14
|
+
|
|
15
|
+
import { viewportFrame, type Frame, type GlyphAtlas, type Padding } from "insomni";
|
|
16
|
+
import { bottomAxis, leftAxis, type AxisMeasurement, type AxisOptions } from "../axis.ts";
|
|
17
|
+
import { bandScale, linearScale } from "../scales.ts";
|
|
18
|
+
import { DEFAULT_PADDING, LAYOUT_GAP } from "./constants.ts";
|
|
19
|
+
import type { Theme } from "./theme.ts";
|
|
20
|
+
|
|
21
|
+
/** Pixel size of a reserved side slot. */
|
|
22
|
+
export interface SlotSize {
|
|
23
|
+
width: number;
|
|
24
|
+
height: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface SlotMap {
|
|
28
|
+
top?: SlotSize;
|
|
29
|
+
right?: SlotSize;
|
|
30
|
+
bottom?: SlotSize;
|
|
31
|
+
left?: SlotSize;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface SlotAnchors {
|
|
35
|
+
/** Top-left of the top slot (full outer width above the plot). */
|
|
36
|
+
top?: { x: number; y: number; width: number; height: number };
|
|
37
|
+
/** Top-left of the right slot (full plot height to the right of the plot). */
|
|
38
|
+
right?: { x: number; y: number; width: number; height: number };
|
|
39
|
+
bottom?: { x: number; y: number; width: number; height: number };
|
|
40
|
+
left?: { x: number; y: number; width: number; height: number };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface PlotLayout {
|
|
44
|
+
/** Full canvas frame (outermost). */
|
|
45
|
+
readonly viewport: Frame;
|
|
46
|
+
/** Inset frame after the chart's outer padding. */
|
|
47
|
+
readonly outer: Frame;
|
|
48
|
+
/** Inner plot frame where marks render — axes + slots carved out. */
|
|
49
|
+
readonly plot: Frame;
|
|
50
|
+
/** Top-left + size of each reserved slot, when reserved. */
|
|
51
|
+
readonly slots: SlotAnchors;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface LayoutInput {
|
|
55
|
+
width: number;
|
|
56
|
+
height: number;
|
|
57
|
+
/** Outer-frame inset, in pixels. Default 16. */
|
|
58
|
+
padding?: Padding;
|
|
59
|
+
theme: Theme;
|
|
60
|
+
atlas: GlyphAtlas | undefined;
|
|
61
|
+
/** Sample tick values used to measure axis label width before we know domains. */
|
|
62
|
+
xSample: readonly (number | string | Date)[];
|
|
63
|
+
ySample: readonly (number | string | Date)[];
|
|
64
|
+
xAxisOptions?: AxisOptions<unknown>;
|
|
65
|
+
yAxisOptions?: AxisOptions<unknown>;
|
|
66
|
+
/** Reserved space on each side of the plot frame. */
|
|
67
|
+
slots?: SlotMap;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function computeLayout(input: LayoutInput): PlotLayout {
|
|
71
|
+
const { width, height, padding = DEFAULT_PADDING } = input;
|
|
72
|
+
|
|
73
|
+
const viewport = viewportFrame(width, height);
|
|
74
|
+
const outer = viewport.padded(padding);
|
|
75
|
+
|
|
76
|
+
const xMeasure = measureAxis("x", input);
|
|
77
|
+
const yMeasure = measureAxis("y", input);
|
|
78
|
+
|
|
79
|
+
const slots = input.slots ?? {};
|
|
80
|
+
const topSlotH = slots.top?.height ?? 0;
|
|
81
|
+
const rightSlotW = slots.right?.width ?? 0;
|
|
82
|
+
const bottomSlotH = slots.bottom?.height ?? 0;
|
|
83
|
+
const leftSlotW = slots.left?.width ?? 0;
|
|
84
|
+
|
|
85
|
+
// Each populated slot is followed by its own gap before the plot edge.
|
|
86
|
+
// Empty slots contribute 0.
|
|
87
|
+
const topGap = topSlotH > 0 ? LAYOUT_GAP.legendToPlot : 0;
|
|
88
|
+
const rightGap = rightSlotW > 0 ? LAYOUT_GAP.legendToPlot : 0;
|
|
89
|
+
const bottomGap = bottomSlotH > 0 ? LAYOUT_GAP.legendToPlot : 0;
|
|
90
|
+
const leftGap = leftSlotW > 0 ? LAYOUT_GAP.legendToPlot : 0;
|
|
91
|
+
|
|
92
|
+
// Vertical y-axis title sits above the axis along the value range; that
|
|
93
|
+
// overhead must be reserved on top *in addition to* the top slot. This
|
|
94
|
+
// remains an axis-specific special case — see dev_docs entry on
|
|
95
|
+
// "axialTitleOverhead" for why.
|
|
96
|
+
const plot = outer.padded({
|
|
97
|
+
top: topSlotH + topGap + LAYOUT_GAP.topReservation + yMeasure.axialTitleOverhead,
|
|
98
|
+
right: rightSlotW + rightGap + LAYOUT_GAP.plotRight,
|
|
99
|
+
bottom: xMeasure.thickness + LAYOUT_GAP.axisToPlot + bottomSlotH + bottomGap,
|
|
100
|
+
left: yMeasure.thickness + LAYOUT_GAP.axisToPlot + leftSlotW + leftGap,
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
const slotAnchors: SlotAnchors = {};
|
|
104
|
+
if (topSlotH > 0) {
|
|
105
|
+
slotAnchors.top = {
|
|
106
|
+
x: outer.x,
|
|
107
|
+
y: outer.y,
|
|
108
|
+
width: outer.width,
|
|
109
|
+
height: topSlotH,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
if (rightSlotW > 0) {
|
|
113
|
+
slotAnchors.right = {
|
|
114
|
+
x: plot.x + plot.width + rightGap,
|
|
115
|
+
y: plot.y,
|
|
116
|
+
width: rightSlotW,
|
|
117
|
+
height: plot.height,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
if (bottomSlotH > 0) {
|
|
121
|
+
slotAnchors.bottom = {
|
|
122
|
+
x: outer.x,
|
|
123
|
+
y: plot.y + plot.height + xMeasure.thickness + LAYOUT_GAP.axisToPlot + bottomGap,
|
|
124
|
+
width: outer.width,
|
|
125
|
+
height: bottomSlotH,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
if (leftSlotW > 0) {
|
|
129
|
+
slotAnchors.left = {
|
|
130
|
+
x: outer.x,
|
|
131
|
+
y: plot.y,
|
|
132
|
+
width: leftSlotW,
|
|
133
|
+
height: plot.height,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return { viewport, outer, plot, slots: slotAnchors };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function measureAxis(channel: "x" | "y", input: LayoutInput): AxisMeasurement {
|
|
141
|
+
// Approximate. We don't know the real domain yet, so use an order-of-magnitude
|
|
142
|
+
// sample to size labels and pick a thickness that comfortably fits.
|
|
143
|
+
if (!input.atlas) {
|
|
144
|
+
const labelHeight = input.theme.axis.labelFontSize;
|
|
145
|
+
return {
|
|
146
|
+
thickness: channel === "x" ? 36 : 48,
|
|
147
|
+
maxLabelWidth: 0,
|
|
148
|
+
labelHeight,
|
|
149
|
+
hasTitle: false,
|
|
150
|
+
titleHeight: 0,
|
|
151
|
+
axialTitleOverhead: 0,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
const sample = channel === "x" ? input.xSample : input.ySample;
|
|
155
|
+
const numericSample = sample.filter((v) => typeof v === "number") as number[];
|
|
156
|
+
const stringSample = sample.filter((v) => typeof v === "string") as string[];
|
|
157
|
+
const lo = numericSample.length ? Math.min(...numericSample) : 0;
|
|
158
|
+
const hi = numericSample.length ? Math.max(...numericSample) : 1;
|
|
159
|
+
|
|
160
|
+
const opts = channel === "x" ? (input.xAxisOptions ?? {}) : (input.yAxisOptions ?? {});
|
|
161
|
+
const enriched: AxisOptions<unknown> = {
|
|
162
|
+
...opts,
|
|
163
|
+
atlas: input.atlas,
|
|
164
|
+
labelFontSize: opts.labelFontSize ?? input.theme.axis.labelFontSize,
|
|
165
|
+
labelColor: opts.labelColor ?? input.theme.axis.labelColor,
|
|
166
|
+
titleColor: opts.titleColor ?? input.theme.axis.titleColor,
|
|
167
|
+
titleFontSize: opts.titleFontSize ?? input.theme.axis.titleFontSize,
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
// For categorical samples we measure against the *actual* category strings
|
|
171
|
+
// via a band scale — using a numeric placeholder underestimates label width
|
|
172
|
+
// (numeric ticks are far shorter than e.g. "Almost Certainly") and labels
|
|
173
|
+
// overflow the canvas. Dedupe in case the sample has repeats.
|
|
174
|
+
if (stringSample.length > 0) {
|
|
175
|
+
const uniqueStrings = Array.from(new Set(stringSample));
|
|
176
|
+
const band = bandScale(uniqueStrings, [0, 100]);
|
|
177
|
+
return channel === "x"
|
|
178
|
+
? bottomAxis(band, enriched as AxisOptions<string>).measure(input.atlas)
|
|
179
|
+
: leftAxis(band, enriched as AxisOptions<string>).measure(input.atlas);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const placeholder = linearScale([lo, hi === lo ? hi + 1 : hi], [0, 100]);
|
|
183
|
+
return channel === "x"
|
|
184
|
+
? bottomAxis(placeholder, enriched).measure(input.atlas)
|
|
185
|
+
: leftAxis(placeholder, enriched).measure(input.atlas);
|
|
186
|
+
}
|