@fjader/svelte 0.2.3 → 0.2.5

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.
Files changed (167) hide show
  1. package/README.md +3 -1
  2. package/dist/animation/EntranceGate.d.ts +52 -0
  3. package/dist/animation/EntranceGate.js +93 -0
  4. package/dist/animation/TransitionClock.d.ts +61 -30
  5. package/dist/animation/TransitionClock.js +67 -29
  6. package/dist/animation/transition-log.d.ts +41 -0
  7. package/dist/animation/transition-log.js +57 -0
  8. package/dist/components/axes/AxisBottom.svelte +27 -8
  9. package/dist/components/axes/AxisBottom.svelte.d.ts +10 -0
  10. package/dist/components/axes/AxisCenter.svelte +4 -2
  11. package/dist/components/axes/AxisLeft.svelte +81 -10
  12. package/dist/components/axes/AxisLeft.svelte.d.ts +17 -0
  13. package/dist/components/axes/BandAxisTicks.svelte +4 -3
  14. package/dist/components/axes/axis-title.d.ts +30 -0
  15. package/dist/components/axes/axis-title.js +77 -0
  16. package/dist/components/charts/ChartCell.svelte +33 -6
  17. package/dist/components/charts/ChartCell.svelte.d.ts +9 -0
  18. package/dist/components/charts/ChartContainer.svelte +55 -5
  19. package/dist/components/charts/ChartContainer.svelte.d.ts +14 -1
  20. package/dist/components/charts/ChartHover.svelte.d.ts +14 -0
  21. package/dist/components/charts/ChartHover.svelte.js +22 -0
  22. package/dist/components/charts/ChartVisibility.svelte.d.ts +43 -0
  23. package/dist/components/charts/ChartVisibility.svelte.js +67 -0
  24. package/dist/components/guides/Grid.svelte +12 -1
  25. package/dist/components/guides/legend/ContinuousColorLegend.svelte +7 -1
  26. package/dist/components/guides/legend/OrdinalColorLegend.svelte +8 -5
  27. package/dist/components/marks/AreaMarkX.svelte +18 -4
  28. package/dist/components/marks/AreaMarkY.svelte +18 -4
  29. package/dist/components/marks/BarMarkX.svelte +20 -4
  30. package/dist/components/marks/BarMarkY.svelte +20 -4
  31. package/dist/components/marks/DotsMark.svelte +8 -3
  32. package/dist/components/marks/LineMark.svelte +13 -6
  33. package/dist/components/marks/ParallelCoordinatesMark.svelte +11 -2
  34. package/dist/components/marks/ParallelCoordinatesMark.svelte.d.ts +4 -0
  35. package/dist/components/marks/RectMark.svelte +8 -3
  36. package/dist/components/marks/RectXMark.svelte +9 -3
  37. package/dist/components/marks/RectYMark.svelte +9 -3
  38. package/dist/components/marks/RuleMarkX.svelte +31 -25
  39. package/dist/components/marks/RuleMarkY.svelte +31 -25
  40. package/dist/components/marks/TextMark.svelte +52 -14
  41. package/dist/components/marks/lifecycle/MarkLifecycleV2.svelte.d.ts +22 -1
  42. package/dist/components/marks/lifecycle/MarkLifecycleV2.svelte.js +579 -66
  43. package/dist/components/marks/lifecycle/domain-queries.d.ts +13 -2
  44. package/dist/components/marks/lifecycle/domain-queries.js +43 -11
  45. package/dist/components/marks/lifecycle/domain-registration.d.ts +6 -1
  46. package/dist/components/marks/lifecycle/domain-registration.js +17 -20
  47. package/dist/components/marks/lifecycle/other-rows.svelte.d.ts +112 -0
  48. package/dist/components/marks/lifecycle/other-rows.svelte.js +193 -0
  49. package/dist/components/marks/lifecycle/other-split.d.ts +80 -0
  50. package/dist/components/marks/lifecycle/other-split.js +112 -0
  51. package/dist/components/marks/lifecycle/query-builder.d.ts +53 -3
  52. package/dist/components/marks/lifecycle/query-builder.js +46 -23
  53. package/dist/components/marks/utils/MarkColor.svelte.d.ts +3 -1
  54. package/dist/components/marks/utils/MarkColor.svelte.js +33 -3
  55. package/dist/components/marks/utils/MarkSeries.svelte.d.ts +14 -1
  56. package/dist/components/marks/utils/MarkSeries.svelte.js +25 -1
  57. package/dist/components/marks/utils/MarkSize.svelte.js +4 -0
  58. package/dist/components/marks/utils/reactive-config.d.ts +1 -1
  59. package/dist/components/marks/utils/reactive-config.js +9 -0
  60. package/dist/components/marks/utils/tooltip-helpers.d.ts +4 -1
  61. package/dist/components/marks/utils/tooltip-helpers.js +2 -1
  62. package/dist/components/rendering/Areas.svelte +57 -19
  63. package/dist/components/rendering/Areas.svelte.d.ts +12 -0
  64. package/dist/components/rendering/BaseGPURenderer.svelte.d.ts +3 -1
  65. package/dist/components/rendering/BaseGPURenderer.svelte.js +4 -2
  66. package/dist/components/rendering/Dots.svelte +23 -3
  67. package/dist/components/rendering/Dots.svelte.d.ts +6 -0
  68. package/dist/components/rendering/GridLine.svelte +13 -1
  69. package/dist/components/rendering/GridLine.svelte.d.ts +5 -0
  70. package/dist/components/rendering/Lines.svelte +48 -13
  71. package/dist/components/rendering/Lines.svelte.d.ts +12 -0
  72. package/dist/components/rendering/ParallelLines.svelte +5 -5
  73. package/dist/components/rendering/Rects.svelte +164 -30
  74. package/dist/components/rendering/Rects.svelte.d.ts +23 -2
  75. package/dist/components/rendering/RuleX.svelte +1 -1
  76. package/dist/components/rendering/RuleY.svelte +1 -1
  77. package/dist/components/rendering/WebGPUCanvas.svelte +4 -2
  78. package/dist/components/rendering/activity-reads.d.ts +47 -12
  79. package/dist/components/rendering/activity-reads.js +61 -19
  80. package/dist/components/rendering/uniform-sizes.d.ts +1 -1
  81. package/dist/components/rendering/uniform-sizes.js +1 -1
  82. package/dist/config/ChartConfig.svelte.d.ts +10 -1
  83. package/dist/config/ChartConfig.svelte.js +17 -0
  84. package/dist/gpu/GPUCoordinator.svelte.d.ts +31 -1
  85. package/dist/gpu/GPUCoordinator.svelte.js +164 -7
  86. package/dist/gpu/MorphedChannels.d.ts +11 -28
  87. package/dist/gpu/MorphedChannels.js +72 -8
  88. package/dist/gpu/ScaleTemplateSystem.d.ts +20 -2
  89. package/dist/gpu/ScaleTemplateSystem.js +83 -6
  90. package/dist/index.d.ts +19 -4
  91. package/dist/index.js +22 -4
  92. package/dist/interactors/Interval1D.svelte +6 -1
  93. package/dist/interactors/Interval2D.svelte +6 -1
  94. package/dist/interactors/ParallelAxisSource.svelte +10 -1
  95. package/dist/interactors/Toggle.svelte +32 -3
  96. package/dist/interactors/Tooltip.svelte +138 -41
  97. package/dist/interactors/TooltipManager.svelte.d.ts +20 -5
  98. package/dist/interactors/TooltipManager.svelte.js +46 -12
  99. package/dist/interactors/other-tooltip.d.ts +16 -0
  100. package/dist/interactors/other-tooltip.js +22 -0
  101. package/dist/query-engine/CrossfilterSelection.svelte.d.ts +4 -2
  102. package/dist/query-engine/CrossfilterSelection.svelte.js +2 -0
  103. package/dist/query-engine/FieldActivity.svelte.d.ts +49 -1
  104. package/dist/query-engine/FieldActivity.svelte.js +175 -5
  105. package/dist/query-engine/FilterGroup.svelte.d.ts +23 -1
  106. package/dist/query-engine/FilterGroup.svelte.js +43 -3
  107. package/dist/query-engine/FilterGroupRegistry.svelte.d.ts +31 -3
  108. package/dist/query-engine/FilterGroupRegistry.svelte.js +62 -15
  109. package/dist/query-engine/MarkHandle.svelte.d.ts +28 -1
  110. package/dist/query-engine/MarkHandle.svelte.js +33 -1
  111. package/dist/query-engine/QueryEngine.d.ts +225 -5
  112. package/dist/query-engine/QueryEngine.js +697 -47
  113. package/dist/query-engine/categorical-identity.d.ts +41 -0
  114. package/dist/query-engine/categorical-identity.js +145 -65
  115. package/dist/query-engine/debug/query-log.d.ts +39 -0
  116. package/dist/query-engine/debug/query-log.js +90 -0
  117. package/dist/query-engine/duckdb.d.ts +55 -8
  118. package/dist/query-engine/duckdb.js +82 -30
  119. package/dist/query-engine/index.d.ts +7 -4
  120. package/dist/query-engine/index.js +3 -3
  121. package/dist/query-engine/preagg/BuildQueue.d.ts +33 -13
  122. package/dist/query-engine/preagg/BuildQueue.js +51 -15
  123. package/dist/query-engine/preagg/Preagg.d.ts +46 -3
  124. package/dist/query-engine/preagg/Preagg.js +159 -31
  125. package/dist/query-engine/preagg/plan.d.ts +33 -1
  126. package/dist/query-engine/preagg/plan.js +158 -14
  127. package/dist/query-engine/sliced-lane.d.ts +108 -0
  128. package/dist/query-engine/sliced-lane.js +299 -0
  129. package/dist/query-engine/source-routing.d.ts +106 -0
  130. package/dist/query-engine/source-routing.js +189 -0
  131. package/dist/query-engine/toggle-predicate.d.ts +21 -0
  132. package/dist/query-engine/toggle-predicate.js +29 -1
  133. package/dist/scales/ActivityTable.d.ts +5 -1
  134. package/dist/scales/ActivityTable.js +12 -3
  135. package/dist/scales/BandScaleState.svelte.d.ts +22 -9
  136. package/dist/scales/BandScaleState.svelte.js +39 -21
  137. package/dist/scales/ChartScales.svelte.d.ts +3 -0
  138. package/dist/scales/ChartScales.svelte.js +26 -8
  139. package/dist/scales/ColorScaleState.svelte.d.ts +18 -2
  140. package/dist/scales/ColorScaleState.svelte.js +138 -26
  141. package/dist/scales/LinearScaleState.svelte.d.ts +7 -10
  142. package/dist/scales/LinearScaleState.svelte.js +21 -39
  143. package/dist/scales/ScaleGeneration.svelte.d.ts +48 -0
  144. package/dist/scales/ScaleGeneration.svelte.js +42 -0
  145. package/dist/scales/ScaleRegistry.svelte.js +3 -1
  146. package/dist/scales/SlotTransition.d.ts +15 -1
  147. package/dist/scales/SlotTransition.js +33 -4
  148. package/dist/scales/TimeScaleState.svelte.d.ts +7 -3
  149. package/dist/scales/TimeScaleState.svelte.js +7 -7
  150. package/dist/scales/color-utils.d.ts +0 -3
  151. package/dist/scales/color-utils.js +0 -3
  152. package/dist/scales/entered.d.ts +8 -0
  153. package/dist/scales/entered.js +10 -0
  154. package/dist/spec/LazyQueryEngine.js +2 -19
  155. package/dist/spec/MarkRegistry.d.ts +3 -1
  156. package/dist/spec/MarkRegistry.js +13 -1
  157. package/dist/spec/PlotFromSpec.svelte +1 -0
  158. package/dist/spec/export.js +11 -2
  159. package/dist/spec/types.d.ts +43 -13
  160. package/dist/sql/bin.d.ts +25 -0
  161. package/dist/sql/bin.js +108 -5
  162. package/dist/theme.css +5 -4
  163. package/dist/transforms/TransformCompiler.d.ts +47 -2
  164. package/dist/transforms/TransformCompiler.js +70 -1
  165. package/dist/transforms/index.js +176 -42
  166. package/dist/types.d.ts +177 -2
  167. package/package.json +4 -3
package/README.md CHANGED
@@ -16,12 +16,14 @@ pip install fjader # Python: marimo and Jupyter
16
16
  pip install "fjader[streamlit]" # Python: Streamlit
17
17
  ```
18
18
 
19
+ `@uwdata/mosaic-sql` is a peer dependency (0.34 or 0.35), so your app and fjader share one copy. Two copies would not recognise each other's expressions: an aggregate such as `sum(...)` written with one would be misread by the other.
20
+
19
21
  fjader needs Chrome 149 or later, or another Chromium browser on that version. It relies on two recent features other browsers don't ship yet: WebGPU's `immediate_address_space`, through which every chart passes its scales to the shaders (so every zoom and pan uses it), and the `Temporal` API, used for calendar offsets. There is no fallback renderer.
20
22
 
21
23
  Three things to know before using it in a deployed app:
22
24
 
23
25
  - **Data goes to the browser in full.** The Python widget and the Streamlit component send every column of a DataFrame to the browser, where DuckDB-WASM queries it. Anyone viewing the app can extract it, so pass only the columns you mean to show. In Jupyter, saved widget state can also store the data in the notebook file.
24
- - **DuckDB-WASM loads from jsDelivr at runtime.** The browser fetches it from the CDN, so locked-down networks that block it cannot render charts.
26
+ - **DuckDB-WASM loads from jsDelivr at runtime by default.** A Svelte app can serve the files itself with `initFjader({ bundles })`, or pass a DuckDB instance it already has with `initFjader(connectionFrom(conn))`. `Fjader.render()`, the Python widget and Streamlit always load from the CDN, so locked-down networks that block it cannot render charts with them.
25
27
  - **Specs are trusted input.** A spec's `{ "sql": "…" }` field runs as raw SQL in the viewer's DuckDB-WASM. Don't build specs from untrusted input.
26
28
 
27
29
  ## Svelte
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Holds a chart's entrance until its marks have their first data.
3
+ *
4
+ * Without it the entrance started on the FIRST mark to land, which had two
5
+ * visible costs. Later marks widened the domain while the chart was entering,
6
+ * so its axes animated to the wider extent: a small version of the camera
7
+ * move the entrance replaced. And a mark landing mid-entrance joined it part
8
+ * way. Held, every mark's extent is in before anything shows, the scales snap
9
+ * to the union, and the chart enters once, whole.
10
+ *
11
+ * A data mark HOLDS when it mounts and RELEASES when its first result has
12
+ * registered its domains, when its query fails, or when it unmounts. A mark
13
+ * the author knows to be slow can opt out (`holdsEntrance={false}`): it then
14
+ * joins the entrance where it is, or appears after it.
15
+ *
16
+ * Nothing needs hiding while a chart waits: before its entrance starts the
17
+ * chart's entrance value is 0, so marks that have landed draw at zero height
18
+ * or zero opacity.
19
+ *
20
+ * OPEN is one-way. The gate opens when its last hold is released, when the
21
+ * coordinator starts the entrance (a chart that never had a hold), or on a
22
+ * timeout. The timeout is armed by the FIRST release rather than the first
23
+ * hold: the wait for DuckDB to start, which can take seconds on a cold page,
24
+ * is not a slow mark, and a chart that has not answered at all has nothing to
25
+ * show yet anyway. A gate stuck closed would leave a chart invisible, which is
26
+ * far worse than a late mark appearing after the rest.
27
+ */
28
+ /** How long the rest of a chart waits for its slowest mark, from its fastest. */
29
+ export declare const ENTRANCE_HOLD_TIMEOUT_MS = 1000;
30
+ export declare class EntranceGate {
31
+ #private;
32
+ /** True once the gate has opened. One-way. */
33
+ get opened(): boolean;
34
+ /**
35
+ * Whether the chart may enter: open, or nothing is holding it. The second
36
+ * covers a chart with no data marks, which the coordinator then opens.
37
+ */
38
+ get canEnter(): boolean;
39
+ /** A mark waits on its first data. Ignored once the gate has opened. */
40
+ hold(id: string): void;
41
+ /** A mark has its first data, failed, or left. Unknown ids are ignored. */
42
+ release(id: string): void;
43
+ /** Open now, and tell whoever is waiting. Idempotent. */
44
+ open(): void;
45
+ /**
46
+ * Run `fn` when the gate opens: at once if it already has. Returns an
47
+ * unsubscribe, for a listener torn down first.
48
+ */
49
+ onOpen(fn: () => void): () => void;
50
+ /** Drop the timer and listeners, for a chart being torn down. */
51
+ destroy(): void;
52
+ }
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Holds a chart's entrance until its marks have their first data.
3
+ *
4
+ * Without it the entrance started on the FIRST mark to land, which had two
5
+ * visible costs. Later marks widened the domain while the chart was entering,
6
+ * so its axes animated to the wider extent: a small version of the camera
7
+ * move the entrance replaced. And a mark landing mid-entrance joined it part
8
+ * way. Held, every mark's extent is in before anything shows, the scales snap
9
+ * to the union, and the chart enters once, whole.
10
+ *
11
+ * A data mark HOLDS when it mounts and RELEASES when its first result has
12
+ * registered its domains, when its query fails, or when it unmounts. A mark
13
+ * the author knows to be slow can opt out (`holdsEntrance={false}`): it then
14
+ * joins the entrance where it is, or appears after it.
15
+ *
16
+ * Nothing needs hiding while a chart waits: before its entrance starts the
17
+ * chart's entrance value is 0, so marks that have landed draw at zero height
18
+ * or zero opacity.
19
+ *
20
+ * OPEN is one-way. The gate opens when its last hold is released, when the
21
+ * coordinator starts the entrance (a chart that never had a hold), or on a
22
+ * timeout. The timeout is armed by the FIRST release rather than the first
23
+ * hold: the wait for DuckDB to start, which can take seconds on a cold page,
24
+ * is not a slow mark, and a chart that has not answered at all has nothing to
25
+ * show yet anyway. A gate stuck closed would leave a chart invisible, which is
26
+ * far worse than a late mark appearing after the rest.
27
+ */
28
+ /** How long the rest of a chart waits for its slowest mark, from its fastest. */
29
+ export const ENTRANCE_HOLD_TIMEOUT_MS = 1000;
30
+ export class EntranceGate {
31
+ #holds = new Set();
32
+ #opened = false;
33
+ #timer = null;
34
+ #listeners = new Set();
35
+ /** True once the gate has opened. One-way. */
36
+ get opened() {
37
+ return this.#opened;
38
+ }
39
+ /**
40
+ * Whether the chart may enter: open, or nothing is holding it. The second
41
+ * covers a chart with no data marks, which the coordinator then opens.
42
+ */
43
+ get canEnter() {
44
+ return this.#opened || this.#holds.size === 0;
45
+ }
46
+ /** A mark waits on its first data. Ignored once the gate has opened. */
47
+ hold(id) {
48
+ if (this.#opened)
49
+ return;
50
+ this.#holds.add(id);
51
+ }
52
+ /** A mark has its first data, failed, or left. Unknown ids are ignored. */
53
+ release(id) {
54
+ if (!this.#holds.delete(id))
55
+ return;
56
+ if (this.#holds.size === 0)
57
+ this.open();
58
+ else
59
+ this.#timer ??= setTimeout(() => this.open(), ENTRANCE_HOLD_TIMEOUT_MS);
60
+ }
61
+ /** Open now, and tell whoever is waiting. Idempotent. */
62
+ open() {
63
+ if (this.#opened)
64
+ return;
65
+ this.#opened = true;
66
+ this.#holds.clear();
67
+ if (this.#timer !== null)
68
+ clearTimeout(this.#timer);
69
+ this.#timer = null;
70
+ for (const fn of this.#listeners)
71
+ fn();
72
+ this.#listeners.clear();
73
+ }
74
+ /**
75
+ * Run `fn` when the gate opens: at once if it already has. Returns an
76
+ * unsubscribe, for a listener torn down first.
77
+ */
78
+ onOpen(fn) {
79
+ if (this.#opened) {
80
+ fn();
81
+ return () => { };
82
+ }
83
+ this.#listeners.add(fn);
84
+ return () => this.#listeners.delete(fn);
85
+ }
86
+ /** Drop the timer and listeners, for a chart being torn down. */
87
+ destroy() {
88
+ if (this.#timer !== null)
89
+ clearTimeout(this.#timer);
90
+ this.#timer = null;
91
+ this.#listeners.clear();
92
+ }
93
+ }
@@ -1,3 +1,47 @@
1
+ /**
2
+ * Stiffness for a data-update transition, and how to think about changing it.
3
+ *
4
+ * The spring is critically damped, so it never overshoots and the only knob is
5
+ * how fast it converges: `omega = sqrt(stiffness)`, and it is within about a
6
+ * percent of its destination after roughly `5 / omega` seconds. So 120 settles
7
+ * in ~0.46s, 240 in ~0.32s, 400 in ~0.25s. Quadrupling the number halves the
8
+ * time; it is not a linear dial.
9
+ *
10
+ * ONE NUMBER FOR SLOT AND VALUE, deliberately. A bar's position and its height
11
+ * share a clock so they arrive together — separate springs desynchronise on a
12
+ * retarget and the bar reaches its rank visibly before or after it finishes
13
+ * growing. Wanting them to differ is a design change, not a constant.
14
+ *
15
+ * Note this is also the floor on how responsive a drag feels: `t` is
16
+ * normalised, so settle DURATION is independent of distance and a one-pixel
17
+ * correction takes as long as a full reorder.
18
+ */
19
+ export declare const TRANSITION_STIFFNESS = 240;
20
+ /**
21
+ * Stiffness for an entrance FADE: softer than a transition, since opacity has
22
+ * no distance to travel and a fast fade reads as a pop. From rest it is a
23
+ * gentle S-curve: 30% at 100 ms, 64% at 200 ms, 96% by 450 ms.
24
+ */
25
+ export declare const ENTRANCE_FADE_STIFFNESS = 120;
26
+ /**
27
+ * Stiffness for an entrance's GROWTH, launched at its own speed (see
28
+ * `restartEaseOut`): 1 − e^(−ωt), 67% at 100 ms, 89% at 200 ms, 96% by 300 ms.
29
+ * Slower than a data transition: growing from nothing is the chart's first
30
+ * statement of magnitude, and at TRANSITION_STIFFNESS it was over before the
31
+ * eye had settled on it.
32
+ */
33
+ export declare const ENTRANCE_GROW_STIFFNESS = 120;
34
+ /** Sub-pixel, so rest is declared when the difference stops being visible. */
35
+ export declare const TRANSITION_PRECISION = 0.0005;
36
+ /**
37
+ * The animation loop, as a clock sees it. The coordinator's fixed-timestep loop
38
+ * is the one implementation; structurally typed so this module does not import
39
+ * the GPU layer.
40
+ */
41
+ export interface ClockLoop {
42
+ addAnimatedSource(source: TransitionClock): void;
43
+ removeAnimatedSource(source: TransitionClock): void;
44
+ }
1
45
  /**
2
46
  * One data-update transition: a spring driving a normalised `t` from 0 to 1.
3
47
  *
@@ -36,36 +80,6 @@
36
80
  * restarting it every frame makes this consumer re-fold every frame, so it
37
81
  * converges by decelerating rather than along the curve the spring intended.
38
82
  */
39
- /**
40
- * Stiffness for a data-update transition, and how to think about changing it.
41
- *
42
- * The spring is critically damped, so it never overshoots and the only knob is
43
- * how fast it converges: `omega = sqrt(stiffness)`, and it is within about a
44
- * percent of its destination after roughly `5 / omega` seconds. So 120 settles
45
- * in ~0.46s, 240 in ~0.32s, 400 in ~0.25s. Quadrupling the number halves the
46
- * time; it is not a linear dial.
47
- *
48
- * ONE NUMBER FOR SLOT AND VALUE, deliberately. A bar's position and its height
49
- * share a clock so they arrive together — separate springs desynchronise on a
50
- * retarget and the bar reaches its rank visibly before or after it finishes
51
- * growing. Wanting them to differ is a design change, not a constant.
52
- *
53
- * Note this is also the floor on how responsive a drag feels: `t` is
54
- * normalised, so settle DURATION is independent of distance and a one-pixel
55
- * correction takes as long as a full reorder.
56
- */
57
- export declare const TRANSITION_STIFFNESS = 240;
58
- /** Sub-pixel, so rest is declared when the difference stops being visible. */
59
- export declare const TRANSITION_PRECISION = 0.0005;
60
- /**
61
- * The animation loop, as a clock sees it. The coordinator's fixed-timestep loop
62
- * is the one implementation; structurally typed so this module does not import
63
- * the GPU layer.
64
- */
65
- export interface ClockLoop {
66
- addAnimatedSource(source: TransitionClock): void;
67
- removeAnimatedSource(source: TransitionClock): void;
68
- }
69
83
  export declare class TransitionClock {
70
84
  #private;
71
85
  /**
@@ -106,6 +120,17 @@ export declare class TransitionClock {
106
120
  * returns — the same end state the loop would reach, reached now.
107
121
  */
108
122
  restart(): void;
123
+ /**
124
+ * Begin from 0 at the spring's own speed, ω, for an ENTRANCE.
125
+ *
126
+ * From rest a critically damped spring starts slowly: its position grows as
127
+ * t² for the first frames, about 18% after 50 ms at the default stiffness.
128
+ * Between two states that is the right shape; for something appearing it
129
+ * reads as lag. Launched at ω the same spring follows 1 − e^(−ωt) exactly:
130
+ * an exponential ease-out, 54% after 50 ms, still never overshooting, and
131
+ * settling in the same time.
132
+ */
133
+ restartEaseOut(): void;
109
134
  /**
110
135
  * Leave the loop without settling, for an owner being torn down. Nothing
111
136
  * reads the clock afterwards, so there is nothing to land.
@@ -153,6 +178,12 @@ export declare class TransitionClock {
153
178
  * then tells the owner — which may restart it, and rejoin, from `onRest`.
154
179
  */
155
180
  _markAtRest(): void;
181
+ /**
182
+ * End the transition now, as the loop would at rest: off the loop, `t` on
183
+ * 1, `onRest` called. For an update that lands without animating (a
184
+ * catch-up result), including one that lands mid-flight.
185
+ */
186
+ finish(): void;
156
187
  /**
157
188
  * Whether the loop can stop driving this.
158
189
  *
@@ -1,4 +1,47 @@
1
1
  import { Spring } from './Spring';
2
+ /**
3
+ * Stiffness for a data-update transition, and how to think about changing it.
4
+ *
5
+ * The spring is critically damped, so it never overshoots and the only knob is
6
+ * how fast it converges: `omega = sqrt(stiffness)`, and it is within about a
7
+ * percent of its destination after roughly `5 / omega` seconds. So 120 settles
8
+ * in ~0.46s, 240 in ~0.32s, 400 in ~0.25s. Quadrupling the number halves the
9
+ * time; it is not a linear dial.
10
+ *
11
+ * ONE NUMBER FOR SLOT AND VALUE, deliberately. A bar's position and its height
12
+ * share a clock so they arrive together — separate springs desynchronise on a
13
+ * retarget and the bar reaches its rank visibly before or after it finishes
14
+ * growing. Wanting them to differ is a design change, not a constant.
15
+ *
16
+ * Note this is also the floor on how responsive a drag feels: `t` is
17
+ * normalised, so settle DURATION is independent of distance and a one-pixel
18
+ * correction takes as long as a full reorder.
19
+ */
20
+ export const TRANSITION_STIFFNESS = 240;
21
+ /**
22
+ * Stiffness for an entrance FADE: softer than a transition, since opacity has
23
+ * no distance to travel and a fast fade reads as a pop. From rest it is a
24
+ * gentle S-curve: 30% at 100 ms, 64% at 200 ms, 96% by 450 ms.
25
+ */
26
+ export const ENTRANCE_FADE_STIFFNESS = 120;
27
+ /**
28
+ * Stiffness for an entrance's GROWTH, launched at its own speed (see
29
+ * `restartEaseOut`): 1 − e^(−ωt), 67% at 100 ms, 89% at 200 ms, 96% by 300 ms.
30
+ * Slower than a data transition: growing from nothing is the chart's first
31
+ * statement of magnitude, and at TRANSITION_STIFFNESS it was over before the
32
+ * eye had settled on it.
33
+ */
34
+ export const ENTRANCE_GROW_STIFFNESS = 120;
35
+ /** Sub-pixel, so rest is declared when the difference stops being visible. */
36
+ export const TRANSITION_PRECISION = 0.0005;
37
+ /**
38
+ * The empty redraw list, shared.
39
+ *
40
+ * A clock whose owner supplies no charts is asked for this on every frame the
41
+ * loop drives it, so a fresh array here was an allocation per frame for a
42
+ * reason that never changes. Never mutated: the loop only reads it.
43
+ */
44
+ const NO_CHARTS = [];
2
45
  /**
3
46
  * One data-update transition: a spring driving a normalised `t` from 0 to 1.
4
47
  *
@@ -37,35 +80,6 @@ import { Spring } from './Spring';
37
80
  * restarting it every frame makes this consumer re-fold every frame, so it
38
81
  * converges by decelerating rather than along the curve the spring intended.
39
82
  */
40
- /**
41
- * Stiffness for a data-update transition, and how to think about changing it.
42
- *
43
- * The spring is critically damped, so it never overshoots and the only knob is
44
- * how fast it converges: `omega = sqrt(stiffness)`, and it is within about a
45
- * percent of its destination after roughly `5 / omega` seconds. So 120 settles
46
- * in ~0.46s, 240 in ~0.32s, 400 in ~0.25s. Quadrupling the number halves the
47
- * time; it is not a linear dial.
48
- *
49
- * ONE NUMBER FOR SLOT AND VALUE, deliberately. A bar's position and its height
50
- * share a clock so they arrive together — separate springs desynchronise on a
51
- * retarget and the bar reaches its rank visibly before or after it finishes
52
- * growing. Wanting them to differ is a design change, not a constant.
53
- *
54
- * Note this is also the floor on how responsive a drag feels: `t` is
55
- * normalised, so settle DURATION is independent of distance and a one-pixel
56
- * correction takes as long as a full reorder.
57
- */
58
- export const TRANSITION_STIFFNESS = 240;
59
- /** Sub-pixel, so rest is declared when the difference stops being visible. */
60
- export const TRANSITION_PRECISION = 0.0005;
61
- /**
62
- * The empty redraw list, shared.
63
- *
64
- * A clock whose owner supplies no charts is asked for this on every frame the
65
- * loop drives it, so a fresh array here was an allocation per frame for a
66
- * reason that never changes. Never mutated: the loop only reads it.
67
- */
68
- const NO_CHARTS = [];
69
83
  export class TransitionClock {
70
84
  #spring;
71
85
  #precision;
@@ -149,6 +163,20 @@ export class TransitionClock {
149
163
  this.#loop.addAnimatedSource(this);
150
164
  }
151
165
  }
166
+ /**
167
+ * Begin from 0 at the spring's own speed, ω, for an ENTRANCE.
168
+ *
169
+ * From rest a critically damped spring starts slowly: its position grows as
170
+ * t² for the first frames, about 18% after 50 ms at the default stiffness.
171
+ * Between two states that is the right shape; for something appearing it
172
+ * reads as lag. Launched at ω the same spring follows 1 − e^(−ωt) exactly:
173
+ * an exponential ease-out, 54% after 50 ms, still never overshooting, and
174
+ * settling in the same time.
175
+ */
176
+ restartEaseOut() {
177
+ this.restart();
178
+ this.#spring.velocity = this.#spring.omega;
179
+ }
152
180
  /**
153
181
  * Leave the loop without settling, for an owner being torn down. Nothing
154
182
  * reads the clock afterwards, so there is nothing to land.
@@ -218,6 +246,16 @@ export class TransitionClock {
218
246
  this.settle();
219
247
  this.#onRest?.();
220
248
  }
249
+ /**
250
+ * End the transition now, as the loop would at rest: off the loop, `t` on
251
+ * 1, `onRest` called. For an update that lands without animating (a
252
+ * catch-up result), including one that lands mid-flight.
253
+ */
254
+ finish() {
255
+ if (this.#running)
256
+ this.#loop?.removeAnimatedSource(this);
257
+ this._markAtRest();
258
+ }
221
259
  /**
222
260
  * Whether the loop can stop driving this.
223
261
  *
@@ -0,0 +1,41 @@
1
+ /**
2
+ * A debug record of every transition that starts, and whether it animates.
3
+ *
4
+ * For test pages that check an animation claim, such as "a chart catching up
5
+ * on its return animates nothing of its own", without reading it off the
6
+ * screen: a spring that plays off screen and two springs to one place both
7
+ * look like nothing happened. Reported where a transition actually starts:
8
+ *
9
+ * - `domain`: a continuous scale's domain (data extents and fitDomain alike)
10
+ * - `morph`: a mark's morphed values (bars, dots on a band axis)
11
+ * - `slots`: a band axis's categories moving to new slots
12
+ *
13
+ * The cause is not recorded: the emitters do not know it, and the page does
14
+ * (it knows what it just did).
15
+ *
16
+ * MODULE-GLOBAL: one log per page (per module instance), not per coordinator.
17
+ * Two coordinators in one document (notebook cells, Streamlit iframes sharing
18
+ * it) see each other's records; filter by chart id. Fine for a debug hook.
19
+ *
20
+ * Free while nothing observes: each emitter asks `transitionsObserved()`
21
+ * before building a record.
22
+ */
23
+ export type TransitionPart = 'domain' | 'morph' | 'slots';
24
+ export interface TransitionRecord {
25
+ part: TransitionPart;
26
+ /** True when it landed at once, false when it animates. */
27
+ snap: boolean;
28
+ /** The charts it moves something in. */
29
+ charts: readonly string[];
30
+ /** What moved: a scale name, a renderer label. */
31
+ label: string;
32
+ /** performance.now() */
33
+ time: number;
34
+ }
35
+ /** Listen to every transition started; returns the unsubscribe. */
36
+ export declare function observeTransitions(listener: (record: TransitionRecord) => void): () => void;
37
+ /** Whether anything listens: emitters check this before building a record. */
38
+ export declare function transitionsObserved(): boolean;
39
+ export declare function noteTransition(part: TransitionPart, snap: boolean,
40
+ /** Read untracked, now: the emitter is usually inside an effect. */
41
+ charts: () => readonly string[], label: string): void;
@@ -0,0 +1,57 @@
1
+ /**
2
+ * A debug record of every transition that starts, and whether it animates.
3
+ *
4
+ * For test pages that check an animation claim, such as "a chart catching up
5
+ * on its return animates nothing of its own", without reading it off the
6
+ * screen: a spring that plays off screen and two springs to one place both
7
+ * look like nothing happened. Reported where a transition actually starts:
8
+ *
9
+ * - `domain`: a continuous scale's domain (data extents and fitDomain alike)
10
+ * - `morph`: a mark's morphed values (bars, dots on a band axis)
11
+ * - `slots`: a band axis's categories moving to new slots
12
+ *
13
+ * The cause is not recorded: the emitters do not know it, and the page does
14
+ * (it knows what it just did).
15
+ *
16
+ * MODULE-GLOBAL: one log per page (per module instance), not per coordinator.
17
+ * Two coordinators in one document (notebook cells, Streamlit iframes sharing
18
+ * it) see each other's records; filter by chart id. Fine for a debug hook.
19
+ *
20
+ * Free while nothing observes: each emitter asks `transitionsObserved()`
21
+ * before building a record.
22
+ */
23
+ import { untrack } from 'svelte';
24
+ const observers = new Set();
25
+ /** Noted and not yet delivered: see noteTransition. */
26
+ let queued = [];
27
+ /** Listen to every transition started; returns the unsubscribe. */
28
+ export function observeTransitions(listener) {
29
+ observers.add(listener);
30
+ return () => observers.delete(listener);
31
+ }
32
+ /** Whether anything listens: emitters check this before building a record. */
33
+ export function transitionsObserved() {
34
+ return observers.size > 0;
35
+ }
36
+ export function noteTransition(part, snap,
37
+ /** Read untracked, now: the emitter is usually inside an effect. */
38
+ charts, label) {
39
+ if (observers.size === 0)
40
+ return;
41
+ const list = untrack(() => [...charts()]);
42
+ queued.push({ part, snap, charts: list, label, time: performance.now() });
43
+ // Delivered in a microtask, never in the emitter's own call: transitions
44
+ // start inside effects (a renderer's write, a scale update), and a
45
+ // listener run there that reads state makes that effect depend on it. A
46
+ // page appending to a $state log then re-ran the emitting effect on every
47
+ // record, which logged again, and never stopped.
48
+ if (queued.length === 1)
49
+ queueMicrotask(deliver);
50
+ }
51
+ function deliver() {
52
+ const records = queued;
53
+ queued = [];
54
+ for (const record of records)
55
+ for (const observe of observers)
56
+ observe(record);
57
+ }
@@ -1,4 +1,5 @@
1
1
  <script lang="ts">
2
+ import { hasEntered } from '../../scales/entered';
2
3
  import { getContext } from 'svelte';
3
4
  import type { ChartContext } from '../../types';
4
5
  import type { BandScaleState } from '../../scales/BandScaleState.svelte';
@@ -8,9 +9,24 @@
8
9
  interface Props {
9
10
  ticksNumber?: number;
10
11
  tickFormat?: ((value: unknown) => string) | null;
12
+ /**
13
+ * Draw the scale's label as the axis title (default). `false` draws none; the
14
+ * label still names the scale, so charts sharing it still link their zoom.
15
+ */
16
+ title?: boolean;
17
+ /**
18
+ * Where along the axis the title sits: `'center'` (default), or at the plot's
19
+ * `'start'` (left) or `'end'` (right) edge.
20
+ */
21
+ titleAnchor?: 'start' | 'center' | 'end';
11
22
  }
12
23
 
13
- let { ticksNumber = 5, tickFormat = null }: Props = $props();
24
+ let {
25
+ ticksNumber = 5,
26
+ tickFormat = null,
27
+ title = true,
28
+ titleAnchor = 'center'
29
+ }: Props = $props();
14
30
  const c = getContext<ChartContext>('chart');
15
31
 
16
32
  // Propagate custom format to scale state once it exists (for MarginLabel, tooltips, etc.)
@@ -20,9 +36,10 @@
20
36
  (s as ContinuousScaleState).setCustomFormat(tickFormat);
21
37
  });
22
38
 
23
- // Continuous scales (linear, time): use scaleState.getTicks()
39
+ // Continuous scales (linear, time): use scaleState.getTicks(). None until the
40
+ // scale has entered: before that its domain is a placeholder.
24
41
  const continuousTicks = $derived(
25
- c.xScaleState?.getTicks ? c.xScaleState.getTicks(ticksNumber) : []
42
+ c.xScaleState?.getTicks && hasEntered(c.xScaleState) ? c.xScaleState.getTicks(ticksNumber) : []
26
43
  );
27
44
 
28
45
  // Band scales: the categories on screen NOW, at their current fractional slots,
@@ -47,10 +64,6 @@
47
64
  {tickFormatter(tick)}
48
65
  </text>
49
66
  {/each}
50
-
51
- <text text-anchor="middle" x={c.innerWidth / 2} y={28}>
52
- {c.xLabel}
53
- </text>
54
67
  {:else if c.scaleTypes.x === 'band' && c.xScale}
55
68
  <BandAxisTicks
56
69
  scale={c.xScaleState as BandScaleState}
@@ -60,8 +73,14 @@
60
73
  tickLength={5}
61
74
  labelGap={16}
62
75
  />
76
+ {/if}
63
77
 
64
- <text text-anchor="middle" x={c.innerWidth / 2} y={28}>
78
+ {#if title && c.xScale}
79
+ <text
80
+ text-anchor={titleAnchor === 'start' ? 'start' : titleAnchor === 'end' ? 'end' : 'middle'}
81
+ x={titleAnchor === 'start' ? 0 : titleAnchor === 'end' ? c.innerWidth : c.innerWidth / 2}
82
+ y={28}
83
+ >
65
84
  {c.xLabel}
66
85
  </text>
67
86
  {/if}
@@ -1,6 +1,16 @@
1
1
  interface Props {
2
2
  ticksNumber?: number;
3
3
  tickFormat?: ((value: unknown) => string) | null;
4
+ /**
5
+ * Draw the scale's label as the axis title (default). `false` draws none; the
6
+ * label still names the scale, so charts sharing it still link their zoom.
7
+ */
8
+ title?: boolean;
9
+ /**
10
+ * Where along the axis the title sits: `'center'` (default), or at the plot's
11
+ * `'start'` (left) or `'end'` (right) edge.
12
+ */
13
+ titleAnchor?: 'start' | 'center' | 'end';
4
14
  }
5
15
  declare const AxisBottom: import("svelte").Component<Props, {}, "">;
6
16
  type AxisBottom = ReturnType<typeof AxisBottom>;
@@ -1,4 +1,5 @@
1
1
  <script lang="ts">
2
+ import { hasEntered } from '../../scales/entered';
2
3
  import { getContext } from 'svelte';
3
4
  import type { ChartContext } from '../../types';
4
5
  import type { BandScaleState } from '../../scales/BandScaleState.svelte';
@@ -39,9 +40,10 @@
39
40
  const tickDir = $derived(labelSide === 'right' ? 1 : -1);
40
41
  const textAnchor = $derived(labelSide === 'right' ? 'start' : 'end');
41
42
 
42
- // Continuous scales (linear, time): use scaleState.getTicks()
43
+ // Continuous scales (linear, time): use scaleState.getTicks(). None until the
44
+ // scale has entered: before that its domain is a placeholder.
43
45
  const continuousTicks = $derived(
44
- c.yScaleState?.getTicks ? c.yScaleState.getTicks(ticksNumber) : []
46
+ c.yScaleState?.getTicks && hasEntered(c.yScaleState) ? c.yScaleState.getTicks(ticksNumber) : []
45
47
  );
46
48
 
47
49
  // Band scales: the categories on screen NOW, at their current fractional slots —