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,479 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
/**
|
|
3
|
+
* Unit tests for the mount.ts render-orchestration invariants (P5 migration).
|
|
4
|
+
*
|
|
5
|
+
* mount.ts cannot be directly imported in node-mode tests because it
|
|
6
|
+
* transitively imports "insomni/reactivity" (a browser-only export). So these
|
|
7
|
+
* tests exercise the REAL building blocks the mount composes:
|
|
8
|
+
* - `createLayer` (real insomni) with the explicit zIndex bands + cache hints,
|
|
9
|
+
* proving the band layout + cache-pin contract mount.ts mints layers under;
|
|
10
|
+
* - the REAL `createEmphasisDriver` state machine (`./emphasis-driver.ts`),
|
|
11
|
+
* which mount.ts wires to renderer.setEmphasis / inv / requestOverlay / RAF;
|
|
12
|
+
* - the per-tick frame-kind dispatch RULE (recompile-only-when-dirty), proving
|
|
13
|
+
* Fix D: an emphasis ramp produces FULL renders with ZERO pipeline recompiles.
|
|
14
|
+
*
|
|
15
|
+
* The emphasis STATE MACHINE itself is tested directly in emphasis-driver.test.ts
|
|
16
|
+
* (deterministic clock); the key-band + GPU_DIM_GEOM_KINDS contracts in
|
|
17
|
+
* geoms/emphasis.test.ts. No hand-copied driver mirror lives here anymore.
|
|
18
|
+
*
|
|
19
|
+
* Plot NEVER calls cacheLayer/uncacheLayer and NEVER passes fullFrame:true — the
|
|
20
|
+
* core's per-frame cache policy + view fingerprint own the bake/full decision.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { createLayer } from "insomni";
|
|
24
|
+
import { describe, expect, test } from "vite-plus/test";
|
|
25
|
+
|
|
26
|
+
import { createEmphasisDriver, type EmphasisState } from "./emphasis-driver.ts";
|
|
27
|
+
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
// Layer band layout + cache hints (real `createLayer`)
|
|
30
|
+
//
|
|
31
|
+
// Mount mints the four owned layers as:
|
|
32
|
+
// const staticCacheHint = partial ? "auto" : "never";
|
|
33
|
+
// axisLayer = createLayer({ space:"ui", zIndex:0, cache: staticCacheHint });
|
|
34
|
+
// marksLayer = createLayer({ space:"ui", zIndex:100, cache: staticCacheHint });
|
|
35
|
+
// hudLayer = createLayer({ space:"ui", zIndex:200, cache: staticCacheHint });
|
|
36
|
+
// overlayLayer = createLayer({ space:"ui", zIndex:300, cache: "never" });
|
|
37
|
+
// These tests run that exact minting against the real layer impl.
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
|
|
40
|
+
const Z_AXIS = 0;
|
|
41
|
+
const Z_BELOW_BASE = 10;
|
|
42
|
+
const Z_MARKS = 100;
|
|
43
|
+
const Z_ABOVE_BASE = 110;
|
|
44
|
+
const Z_HUD = 200;
|
|
45
|
+
const Z_OVERLAY = 300;
|
|
46
|
+
|
|
47
|
+
function staticCacheHint(partial: boolean): "auto" | "never" {
|
|
48
|
+
return partial ? "auto" : "never";
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function makeLayers(partial: boolean) {
|
|
52
|
+
const hint = staticCacheHint(partial);
|
|
53
|
+
const axisLayer = createLayer({ space: "ui", zIndex: Z_AXIS, cache: hint });
|
|
54
|
+
const marksLayer = createLayer({ space: "ui", zIndex: Z_MARKS, cache: hint });
|
|
55
|
+
const hudLayer = createLayer({ space: "ui", zIndex: Z_HUD, cache: hint });
|
|
56
|
+
const overlayLayer = createLayer({ space: "ui", zIndex: Z_OVERLAY, cache: "never" });
|
|
57
|
+
const cleanup = () => {
|
|
58
|
+
axisLayer.destroy();
|
|
59
|
+
marksLayer.destroy();
|
|
60
|
+
hudLayer.destroy();
|
|
61
|
+
overlayLayer.destroy();
|
|
62
|
+
};
|
|
63
|
+
return { axisLayer, marksLayer, hudLayer, overlayLayer, cleanup };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
describe("layer zIndex bands + cache hints", () => {
|
|
67
|
+
test("partial=true: static layers cache:'auto', overlay 'never'; bands ascend axis<marks<hud<overlay", () => {
|
|
68
|
+
const { axisLayer, marksLayer, hudLayer, overlayLayer, cleanup } = makeLayers(true);
|
|
69
|
+
|
|
70
|
+
expect(axisLayer.cache).toBe("auto");
|
|
71
|
+
expect(marksLayer.cache).toBe("auto");
|
|
72
|
+
expect(hudLayer.cache).toBe("auto");
|
|
73
|
+
expect(overlayLayer.cache).toBe("never");
|
|
74
|
+
|
|
75
|
+
expect(axisLayer.zIndex).toBe(0);
|
|
76
|
+
expect(marksLayer.zIndex).toBe(100);
|
|
77
|
+
expect(hudLayer.zIndex).toBe(200);
|
|
78
|
+
expect(overlayLayer.zIndex).toBe(300);
|
|
79
|
+
expect(axisLayer.zIndex! < marksLayer.zIndex!).toBe(true);
|
|
80
|
+
expect(marksLayer.zIndex! < hudLayer.zIndex!).toBe(true);
|
|
81
|
+
expect(hudLayer.zIndex! < overlayLayer.zIndex!).toBe(true);
|
|
82
|
+
|
|
83
|
+
cleanup();
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("partial=false: EVERY layer is cache:'never' (pure-live fallback)", () => {
|
|
87
|
+
const { axisLayer, marksLayer, hudLayer, overlayLayer, cleanup } = makeLayers(false);
|
|
88
|
+
|
|
89
|
+
expect(axisLayer.cache).toBe("never");
|
|
90
|
+
expect(marksLayer.cache).toBe("never");
|
|
91
|
+
expect(hudLayer.cache).toBe("never");
|
|
92
|
+
expect(overlayLayer.cache).toBe("never");
|
|
93
|
+
|
|
94
|
+
cleanup();
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test("overlay is ALWAYS cache:'never' regardless of partial", () => {
|
|
98
|
+
const a = makeLayers(true);
|
|
99
|
+
expect(a.overlayLayer.cache).toBe("never");
|
|
100
|
+
a.cleanup();
|
|
101
|
+
const b = makeLayers(false);
|
|
102
|
+
expect(b.overlayLayer.cache).toBe("never");
|
|
103
|
+
b.cleanup();
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("extra below/above layers slot into 10.. / 110.. bands (between fixed bands)", () => {
|
|
107
|
+
const below = [createLayer({ space: "ui" }), createLayer({ space: "ui" })];
|
|
108
|
+
const above = [createLayer({ space: "ui" })];
|
|
109
|
+
for (let i = 0; i < below.length; i++) below[i]!.zIndex = Z_BELOW_BASE + i;
|
|
110
|
+
for (let i = 0; i < above.length; i++) above[i]!.zIndex = Z_ABOVE_BASE + i;
|
|
111
|
+
|
|
112
|
+
expect(below.map((l) => l.zIndex)).toEqual([10, 11]);
|
|
113
|
+
expect(above.map((l) => l.zIndex)).toEqual([110]);
|
|
114
|
+
expect(below.every((l) => l.zIndex! > Z_AXIS && l.zIndex! < Z_MARKS)).toBe(true);
|
|
115
|
+
expect(above.every((l) => l.zIndex! > Z_MARKS && l.zIndex! < Z_HUD)).toBe(true);
|
|
116
|
+
|
|
117
|
+
for (const l of [...below, ...above]) l.destroy();
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
// ---------------------------------------------------------------------------
|
|
122
|
+
// Interaction-aware cache-hint flip (real `Layer.cache` mutation)
|
|
123
|
+
//
|
|
124
|
+
// mount.ts's `applyInteractionCacheHints()`:
|
|
125
|
+
// if (!partial) return;
|
|
126
|
+
// const interacting = !!binding && (binding.interacting || binding.flinging);
|
|
127
|
+
// const hint = interacting ? "never" : "auto";
|
|
128
|
+
// axisLayer.cache = hint; marksLayer.cache = hint; hudLayer.cache = hint;
|
|
129
|
+
// marksLayer.cache = marksCacheHint(interacting); // dim charts pin "never"
|
|
130
|
+
// // overlay stays "never".
|
|
131
|
+
// We run the same mutation against real layers (mutating Layer.cache between
|
|
132
|
+
// frames is a supported core contract).
|
|
133
|
+
// ---------------------------------------------------------------------------
|
|
134
|
+
|
|
135
|
+
interface StubBinding {
|
|
136
|
+
interacting: boolean;
|
|
137
|
+
flinging: boolean;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function applyInteractionCacheHints(
|
|
141
|
+
partial: boolean,
|
|
142
|
+
binding: StubBinding | null,
|
|
143
|
+
layers: ReturnType<typeof makeLayers>,
|
|
144
|
+
): void {
|
|
145
|
+
if (!partial) return;
|
|
146
|
+
const interacting = !!binding && (binding.interacting || binding.flinging);
|
|
147
|
+
const hint = interacting ? "never" : "auto";
|
|
148
|
+
layers.axisLayer.cache = hint;
|
|
149
|
+
layers.marksLayer.cache = hint;
|
|
150
|
+
layers.hudLayer.cache = hint;
|
|
151
|
+
// overlay untouched.
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
describe("interaction-aware cache-hint flip", () => {
|
|
155
|
+
test("dragging (interacting): static layers flip to 'never', overlay stays 'never'", () => {
|
|
156
|
+
const layers = makeLayers(true);
|
|
157
|
+
applyInteractionCacheHints(true, { interacting: true, flinging: false }, layers);
|
|
158
|
+
|
|
159
|
+
expect(layers.axisLayer.cache).toBe("never");
|
|
160
|
+
expect(layers.marksLayer.cache).toBe("never");
|
|
161
|
+
expect(layers.hudLayer.cache).toBe("never");
|
|
162
|
+
expect(layers.overlayLayer.cache).toBe("never");
|
|
163
|
+
|
|
164
|
+
layers.cleanup();
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
test("flinging also flips static layers to 'never'", () => {
|
|
168
|
+
const layers = makeLayers(true);
|
|
169
|
+
applyInteractionCacheHints(true, { interacting: false, flinging: true }, layers);
|
|
170
|
+
|
|
171
|
+
expect(layers.axisLayer.cache).toBe("never");
|
|
172
|
+
expect(layers.marksLayer.cache).toBe("never");
|
|
173
|
+
expect(layers.hudLayer.cache).toBe("never");
|
|
174
|
+
|
|
175
|
+
layers.cleanup();
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
test("settled (not interacting): static layers restore to 'auto'", () => {
|
|
179
|
+
const layers = makeLayers(true);
|
|
180
|
+
applyInteractionCacheHints(true, { interacting: true, flinging: false }, layers);
|
|
181
|
+
applyInteractionCacheHints(true, { interacting: false, flinging: false }, layers);
|
|
182
|
+
|
|
183
|
+
expect(layers.axisLayer.cache).toBe("auto");
|
|
184
|
+
expect(layers.marksLayer.cache).toBe("auto");
|
|
185
|
+
expect(layers.hudLayer.cache).toBe("auto");
|
|
186
|
+
expect(layers.overlayLayer.cache).toBe("never");
|
|
187
|
+
|
|
188
|
+
layers.cleanup();
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test("no binding (pan/zoom not configured): hints stay 'auto'", () => {
|
|
192
|
+
const layers = makeLayers(true);
|
|
193
|
+
applyInteractionCacheHints(true, null, layers);
|
|
194
|
+
|
|
195
|
+
expect(layers.axisLayer.cache).toBe("auto");
|
|
196
|
+
expect(layers.marksLayer.cache).toBe("auto");
|
|
197
|
+
expect(layers.hudLayer.cache).toBe("auto");
|
|
198
|
+
|
|
199
|
+
layers.cleanup();
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
test("partial=false: hint flip is a no-op (layers stay 'never')", () => {
|
|
203
|
+
const layers = makeLayers(false);
|
|
204
|
+
applyInteractionCacheHints(false, { interacting: true, flinging: false }, layers);
|
|
205
|
+
|
|
206
|
+
expect(layers.axisLayer.cache).toBe("never");
|
|
207
|
+
expect(layers.marksLayer.cache).toBe("never");
|
|
208
|
+
expect(layers.hudLayer.cache).toBe("never");
|
|
209
|
+
|
|
210
|
+
layers.cleanup();
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
test("drag → settle: 'never' during gesture, 'auto' after release", () => {
|
|
214
|
+
const layers = makeLayers(true);
|
|
215
|
+
|
|
216
|
+
applyInteractionCacheHints(true, { interacting: true, flinging: false }, layers);
|
|
217
|
+
expect(layers.marksLayer.cache).toBe("never");
|
|
218
|
+
|
|
219
|
+
applyInteractionCacheHints(true, { interacting: false, flinging: false }, layers);
|
|
220
|
+
expect(layers.marksLayer.cache).toBe("auto");
|
|
221
|
+
|
|
222
|
+
layers.cleanup();
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
// ---------------------------------------------------------------------------
|
|
227
|
+
// Fix D — per-tick emphasis ramp is REPAINT-ONLY: N full renders, ZERO recompiles.
|
|
228
|
+
//
|
|
229
|
+
// This composes the REAL emphasis driver with the mount's per-tick dispatch RULE
|
|
230
|
+
// (the thin-consumer glue that stays in mount.ts):
|
|
231
|
+
//
|
|
232
|
+
// if (emphasisAnimating()) { const req = driver.step(now);
|
|
233
|
+
// if (req.needsFrame) repaintPending = true; }
|
|
234
|
+
// const needsFull = inv.dirty; // real recompile pending
|
|
235
|
+
// const needsRepaint = repaintPending; // uniform-only full render
|
|
236
|
+
// if (needsFull) drawFull(); // runPipeline + bake + render (FULL)
|
|
237
|
+
// else if (needsRepaint) drawRepaint(); // render(currentLayers()) — NO pipeline
|
|
238
|
+
// else drawOverlay(); // regions/partial
|
|
239
|
+
//
|
|
240
|
+
// The pipeline + renderer boundaries are faked so we can count recompiles and
|
|
241
|
+
// classify the render kind of every emitted frame.
|
|
242
|
+
// ---------------------------------------------------------------------------
|
|
243
|
+
|
|
244
|
+
interface FrameLog {
|
|
245
|
+
kind: "full" | "repaint" | "overlay";
|
|
246
|
+
regions: boolean; // whether this render() carried `regions` (partial)
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/** A tick loop that drives the REAL driver through the production dispatch rule. */
|
|
250
|
+
function makeTickHarness(opts: { durationS: number }) {
|
|
251
|
+
let durationS = opts.durationS;
|
|
252
|
+
const emphasisWrites: EmphasisState[] = [];
|
|
253
|
+
let runPipelineCalls = 0;
|
|
254
|
+
let renderCalls = 0;
|
|
255
|
+
const frames: FrameLog[] = [];
|
|
256
|
+
|
|
257
|
+
const driver = createEmphasisDriver({
|
|
258
|
+
durationS: () => durationS,
|
|
259
|
+
dim: () => 0.45,
|
|
260
|
+
setEmphasis: (s) => emphasisWrites.push({ ...s }),
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
// --- faked mount boundaries ---
|
|
264
|
+
let invDirty = false;
|
|
265
|
+
let overlayDirty = false;
|
|
266
|
+
let repaintPending = false;
|
|
267
|
+
|
|
268
|
+
function runPipeline(): void {
|
|
269
|
+
runPipelineCalls++; // the expensive recompile + axis bake bump
|
|
270
|
+
}
|
|
271
|
+
function render(withRegions: boolean): void {
|
|
272
|
+
renderCalls++;
|
|
273
|
+
void withRegions;
|
|
274
|
+
}
|
|
275
|
+
function drawFull(): void {
|
|
276
|
+
invDirty = false;
|
|
277
|
+
overlayDirty = false;
|
|
278
|
+
runPipeline(); // a full frame recompiles
|
|
279
|
+
render(false);
|
|
280
|
+
frames.push({ kind: "full", regions: false });
|
|
281
|
+
}
|
|
282
|
+
function drawRepaint(): void {
|
|
283
|
+
render(false); // FULL render, NO runPipeline → packs stable → zero re-bakes
|
|
284
|
+
frames.push({ kind: "repaint", regions: false });
|
|
285
|
+
}
|
|
286
|
+
function drawOverlay(): void {
|
|
287
|
+
overlayDirty = false;
|
|
288
|
+
render(true); // regions/partial
|
|
289
|
+
frames.push({ kind: "overlay", regions: true });
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function tick(now: number): void {
|
|
293
|
+
if (driver.animating()) {
|
|
294
|
+
const req = driver.step(now);
|
|
295
|
+
if (req.needsFrame) repaintPending = true;
|
|
296
|
+
}
|
|
297
|
+
const needsFull = invDirty;
|
|
298
|
+
const needsRepaint = repaintPending;
|
|
299
|
+
const needsOverlay = overlayDirty;
|
|
300
|
+
if (needsFull || needsRepaint || needsOverlay) {
|
|
301
|
+
if (needsFull) drawFull();
|
|
302
|
+
else if (needsRepaint) drawRepaint();
|
|
303
|
+
else drawOverlay();
|
|
304
|
+
repaintPending = false;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
return {
|
|
309
|
+
driver,
|
|
310
|
+
tick,
|
|
311
|
+
onHover(key: number | null): void {
|
|
312
|
+
const req = driver.onHover(key);
|
|
313
|
+
if (req.needsFrame) {
|
|
314
|
+
if (req.full)
|
|
315
|
+
invDirty = true; // hover hit-change → full recompile
|
|
316
|
+
else overlayDirty = true;
|
|
317
|
+
}
|
|
318
|
+
},
|
|
319
|
+
markOverlayDirty: () => {
|
|
320
|
+
overlayDirty = true;
|
|
321
|
+
},
|
|
322
|
+
invalidate: () => {
|
|
323
|
+
invDirty = true;
|
|
324
|
+
},
|
|
325
|
+
setDuration: (s: number) => (durationS = s),
|
|
326
|
+
get runPipelineCalls() {
|
|
327
|
+
return runPipelineCalls;
|
|
328
|
+
},
|
|
329
|
+
get renderCalls() {
|
|
330
|
+
return renderCalls;
|
|
331
|
+
},
|
|
332
|
+
frames,
|
|
333
|
+
emphasisWrites,
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
describe("Fix D — emphasis ramp is repaint-only (no per-tick recompile)", () => {
|
|
338
|
+
test("N ramp steps produce N FULL renders and ZERO pipeline recompiles", () => {
|
|
339
|
+
const h = makeTickHarness({ durationS: 0.1 }); // 100ms ramp
|
|
340
|
+
h.onHover(1234); // enter → one full recompile frame
|
|
341
|
+
// Service the enter frame (drawFull).
|
|
342
|
+
h.tick(0);
|
|
343
|
+
expect(h.runPipelineCalls).toBe(1);
|
|
344
|
+
expect(h.frames.at(-1)!.kind).toBe("full");
|
|
345
|
+
|
|
346
|
+
// Now ramp: 5 steps of 25ms. The driver settles at t=1 partway, but each
|
|
347
|
+
// animating step requests a frame; once settled, step() requests nothing.
|
|
348
|
+
const recompilesBefore = h.runPipelineCalls;
|
|
349
|
+
let now = 0;
|
|
350
|
+
let repaintFrames = 0;
|
|
351
|
+
for (let i = 0; i < 5; i++) {
|
|
352
|
+
now += 25;
|
|
353
|
+
const framesBefore = h.frames.length;
|
|
354
|
+
h.tick(now);
|
|
355
|
+
const emitted = h.frames.length > framesBefore;
|
|
356
|
+
if (emitted) {
|
|
357
|
+
// Every emitted ramp frame is a repaint-only FULL render (no regions).
|
|
358
|
+
expect(h.frames.at(-1)!.kind).toBe("repaint");
|
|
359
|
+
expect(h.frames.at(-1)!.regions).toBe(false);
|
|
360
|
+
repaintFrames++;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
// The ramp emitted at least one repaint frame...
|
|
364
|
+
expect(repaintFrames).toBeGreaterThan(0);
|
|
365
|
+
// ...and NOT ONE of them recompiled the pipeline (Fix D core assertion).
|
|
366
|
+
expect(h.runPipelineCalls).toBe(recompilesBefore);
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
test("a real invalidation mid-ramp (resize/data) WINS → drawFull, not repaint", () => {
|
|
370
|
+
const h = makeTickHarness({ durationS: 0.2 });
|
|
371
|
+
h.onHover(1234);
|
|
372
|
+
h.tick(0); // enter full
|
|
373
|
+
h.tick(50); // ramp step (repaint)
|
|
374
|
+
expect(h.frames.at(-1)!.kind).toBe("repaint");
|
|
375
|
+
const recompiles = h.runPipelineCalls;
|
|
376
|
+
// A resize fires inv.invalidate() mid-ramp.
|
|
377
|
+
h.invalidate();
|
|
378
|
+
h.tick(75);
|
|
379
|
+
// inv.dirty outranks the repaint → a full recompile frame.
|
|
380
|
+
expect(h.frames.at(-1)!.kind).toBe("full");
|
|
381
|
+
expect(h.runPipelineCalls).toBe(recompiles + 1);
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
test("an overlay-only request is SUPPRESSED while the dim is animating (soundness)", () => {
|
|
385
|
+
const h = makeTickHarness({ durationS: 0.2 });
|
|
386
|
+
h.onHover(1234);
|
|
387
|
+
h.tick(0); // enter full
|
|
388
|
+
// Cursor moves (tooltip) mid-ramp → overlay requested.
|
|
389
|
+
h.markOverlayDirty();
|
|
390
|
+
h.tick(50);
|
|
391
|
+
// The animating ramp's repaint outranks the overlay → FULL repaint, not a
|
|
392
|
+
// partial regions frame (the uniform is global; a partial would ghost).
|
|
393
|
+
expect(h.frames.at(-1)!.kind).toBe("repaint");
|
|
394
|
+
expect(h.frames.at(-1)!.regions).toBe(false);
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
test("once settled, a pure overlay change rides a regions (overlay) frame again", () => {
|
|
398
|
+
const h = makeTickHarness({ durationS: 0.1 });
|
|
399
|
+
h.onHover(1234);
|
|
400
|
+
h.tick(0);
|
|
401
|
+
h.tick(50);
|
|
402
|
+
h.tick(100); // settle at t=1
|
|
403
|
+
expect(h.driver.animating()).toBe(false);
|
|
404
|
+
h.markOverlayDirty();
|
|
405
|
+
h.tick(116);
|
|
406
|
+
expect(h.frames.at(-1)!.kind).toBe("overlay");
|
|
407
|
+
expect(h.frames.at(-1)!.regions).toBe(true);
|
|
408
|
+
});
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
// ---------------------------------------------------------------------------
|
|
412
|
+
// Fix A — reduced-motion hover-exit forces a FULL frame (no stuck dim).
|
|
413
|
+
//
|
|
414
|
+
// Drives the REAL driver through the same dispatch glue. The bug: a reduced-
|
|
415
|
+
// motion exit snapped the uniform off (global pixel change) but routed to an
|
|
416
|
+
// overlay-only repaint, leaving the rest of the backbuffer dimmed.
|
|
417
|
+
// ---------------------------------------------------------------------------
|
|
418
|
+
|
|
419
|
+
describe("Fix A — reduced-motion exit forces a full frame", () => {
|
|
420
|
+
test("hover then exit under reduced-motion both produce FULL frames", () => {
|
|
421
|
+
const h = makeTickHarness({ durationS: 0 }); // reduced-motion
|
|
422
|
+
h.onHover(1234); // snap on
|
|
423
|
+
h.tick(0);
|
|
424
|
+
expect(h.frames.at(-1)!.kind).toBe("full");
|
|
425
|
+
expect(h.driver.state().t).toBe(1);
|
|
426
|
+
|
|
427
|
+
const fullsBefore = h.frames.filter((f) => f.kind === "full").length;
|
|
428
|
+
h.onHover(null); // snap off — MUST be a full frame, not overlay
|
|
429
|
+
h.tick(16);
|
|
430
|
+
const lastFrame = h.frames.at(-1)!;
|
|
431
|
+
expect(lastFrame.kind).toBe("full"); // <-- the fix (was "overlay" → stuck dim)
|
|
432
|
+
expect(lastFrame.regions).toBe(false);
|
|
433
|
+
expect(h.frames.filter((f) => f.kind === "full").length).toBe(fullsBefore + 1);
|
|
434
|
+
// Uniform snapped off → no residual dim.
|
|
435
|
+
expect(h.driver.state()).toEqual({ focusedKey: 0, t: 0, target: 0 });
|
|
436
|
+
expect(h.emphasisWrites.at(-1)!.t).toBe(0);
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
test("hover-enter under reduced-motion is a full frame (symmetric to exit)", () => {
|
|
440
|
+
const h = makeTickHarness({ durationS: 0 });
|
|
441
|
+
h.onHover(1234);
|
|
442
|
+
h.tick(0);
|
|
443
|
+
expect(h.frames.at(-1)!.kind).toBe("full");
|
|
444
|
+
expect(h.frames.at(-1)!.regions).toBe(false);
|
|
445
|
+
});
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
// ---------------------------------------------------------------------------
|
|
449
|
+
// Marks cache pin for dim charts (P5-T3) — mirror of mount.ts `marksCacheHint`.
|
|
450
|
+
// (A pure decision table; the dim-set membership it keys off is GPU_DIM_GEOM_KINDS,
|
|
451
|
+
// tested in geoms/emphasis.test.ts.)
|
|
452
|
+
// ---------------------------------------------------------------------------
|
|
453
|
+
|
|
454
|
+
function marksCacheHint(opts: {
|
|
455
|
+
partial: boolean;
|
|
456
|
+
hasDimGeom: boolean;
|
|
457
|
+
interacting: boolean;
|
|
458
|
+
}): "auto" | "never" {
|
|
459
|
+
if (!opts.partial) return "never";
|
|
460
|
+
if (opts.hasDimGeom) return "never";
|
|
461
|
+
return opts.interacting ? "never" : "auto";
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
describe("marks cache pin for dim charts (P5-T3)", () => {
|
|
465
|
+
test("a chart with a dim geom pins marks to 'never' even when settled", () => {
|
|
466
|
+
expect(marksCacheHint({ partial: true, hasDimGeom: true, interacting: false })).toBe("never");
|
|
467
|
+
expect(marksCacheHint({ partial: true, hasDimGeom: true, interacting: true })).toBe("never");
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
test("a pure-scatter chart (no dim geom) keeps 'auto' when settled, 'never' while interacting", () => {
|
|
471
|
+
expect(marksCacheHint({ partial: true, hasDimGeom: false, interacting: false })).toBe("auto");
|
|
472
|
+
expect(marksCacheHint({ partial: true, hasDimGeom: false, interacting: true })).toBe("never");
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
test("partial=false → always 'never' regardless of dim geom", () => {
|
|
476
|
+
expect(marksCacheHint({ partial: false, hasDimGeom: true, interacting: false })).toBe("never");
|
|
477
|
+
expect(marksCacheHint({ partial: false, hasDimGeom: false, interacting: false })).toBe("never");
|
|
478
|
+
});
|
|
479
|
+
});
|