@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
@@ -15,7 +15,7 @@ export const PARALLEL_MAX_AXES = 16;
15
15
  const SCALE_PARAM_BYTES = 16;
16
16
  export const UNIFORM_BYTES = {
17
17
  dotsMarkConstants: 48,
18
- rectsMarkConstants: 48,
18
+ rectsMarkConstants: 64,
19
19
  ruleXMarkConstants: 48,
20
20
  ruleYMarkConstants: 48,
21
21
  linesMarkConstants: 64,
@@ -1,5 +1,6 @@
1
+ import { EntranceGate } from '../animation/EntranceGate';
1
2
  import type { SelectionResponse } from '../components/rendering/activity-reads';
2
- import type { FieldRef, LODConfig, SortConfig, ColorScheme, CurveType, Transform } from '../types';
3
+ import type { FieldRef, LODConfig, SortConfig, ColorScheme, CurveType, NullsResponse, Transform } from '../types';
3
4
  import type { MarkLODSurface } from '../lod/rung-types';
4
5
  export interface MarkEntry {
5
6
  readonly id: string;
@@ -25,6 +26,9 @@ export interface MarkEntry {
25
26
  transforms?: Transform[];
26
27
  selection?: string;
27
28
  selectionResponse?: SelectionResponse;
29
+ contributeDomain?: boolean;
30
+ nulls?: NullsResponse;
31
+ holdsEntrance?: boolean;
28
32
  limit?: number;
29
33
  preferredXScaleType?: string | null;
30
34
  preferredYScaleType?: string | null;
@@ -59,6 +63,11 @@ export interface ChartProps {
59
63
  }
60
64
  export declare class ChartConfig {
61
65
  #private;
66
+ /**
67
+ * Holds the chart's entrance until its marks have their first data. One per
68
+ * container, shared by its facet cells, as this config is.
69
+ */
70
+ readonly entrance: EntranceGate;
62
71
  /**
63
72
  * Register a mark with prop getters for each configurable property.
64
73
  * Returns the reactive entry that the mark reads from.
@@ -12,10 +12,17 @@
12
12
  // The entry outlives MarkLifecycle — registerMark returns the existing entry
13
13
  // on re-registration, preventing infinite loops.
14
14
  // =============================================================================
15
+ import { untrack } from 'svelte';
15
16
  import { SvelteMap } from 'svelte/reactivity';
17
+ import { EntranceGate } from '../animation/EntranceGate';
16
18
  import { createOverridable } from './overridable.svelte';
17
19
  // ---- ChartConfig ----
18
20
  export class ChartConfig {
21
+ /**
22
+ * Holds the chart's entrance until its marks have their first data. One per
23
+ * container, shared by its facet cells, as this config is.
24
+ */
25
+ entrance = new EntranceGate();
19
26
  /** Overridable channels per mark, for reset support. */
20
27
  #channels = new Map();
21
28
  /** Reactive entries — SvelteMap so iteration/size reads are tracked. */
@@ -45,6 +52,14 @@ export class ChartConfig {
45
52
  const existing = this.#entries.get(id);
46
53
  if (existing)
47
54
  return existing;
55
+ // A data mark holds the chart's entrance from the moment it mounts, which
56
+ // is now: its lifecycle is built later, in an effect that waits for the
57
+ // chart's scales, and until something holds the gate the chart looks
58
+ // like one with no data marks and enters on its first drawable layer.
59
+ if (untrack(() => propGetters.table?.()) &&
60
+ untrack(() => propGetters.holdsEntrance?.()) !== false) {
61
+ this.entrance.hold(id);
62
+ }
48
63
  const channels = new Map();
49
64
  const entry = { id, markType };
50
65
  for (const [key, getter] of Object.entries(propGetters)) {
@@ -66,6 +81,8 @@ export class ChartConfig {
66
81
  return entry;
67
82
  }
68
83
  unregisterMark(id) {
84
+ // A mark that leaves before its first data must not hold the chart.
85
+ this.entrance.release(id);
69
86
  this.#entries.delete(id);
70
87
  this.#channels.delete(id);
71
88
  }
@@ -1,3 +1,6 @@
1
+ import { type TransitionRecord } from '../animation/transition-log';
2
+ import type { EntranceMode } from '../types';
3
+ import type { EntranceGate } from '../animation/EntranceGate';
1
4
  import type { RendererDefinition } from './ScaleTemplateSystem';
2
5
  import type { ScaleRegistry } from '../scales/ScaleRegistry.svelte';
3
6
  import type { FilterGroupRegistry } from '../query-engine/FilterGroupRegistry.svelte';
@@ -88,6 +91,18 @@ interface ChartRegistrationConfig {
88
91
  zIndex?: number;
89
92
  facetIndex?: number;
90
93
  facetScale?: FacetScaleRef | null;
94
+ /** How the chart's marks enter when its data first draws. Default 'auto'. */
95
+ entrance?: EntranceMode;
96
+ /** Holds the entrance until the chart's marks have their first data. */
97
+ entranceGate?: EntranceGate | null;
98
+ }
99
+ /** A chart's entrance starting, for observeEntrances. */
100
+ export interface ChartEntrance {
101
+ chartId: string;
102
+ /** The chart's canvas, to find it in the page (a label on an ancestor). */
103
+ canvas: HTMLCanvasElement;
104
+ /** performance.now() when the entrance started. */
105
+ time: number;
91
106
  }
92
107
  /**
93
108
  * One chart's pixels as the coordinator rendered them, for the scale-source
@@ -142,6 +157,12 @@ export declare class GPUCoordinator {
142
157
  * left alone.
143
158
  */
144
159
  writeScaleParams(scaleId: string, view: Float32Array, offset: number): boolean;
160
+ /**
161
+ * Listen to every chart's entrance as it starts: when its marks have their
162
+ * first data and it first draws. For measuring load (how soon charts enter);
163
+ * returns the unsubscribe. Costs nothing while no one listens.
164
+ */
165
+ observeEntrances(listener: (entrance: ChartEntrance) => void): () => void;
145
166
  /**
146
167
  * Configure canvases so the coordinator can copy from them. Test-only: set
147
168
  * it before the charts mount, since a canvas reads it when it configures
@@ -263,10 +284,19 @@ export declare class GPUCoordinator {
263
284
  removePhysicsCallback(fn: () => void): void;
264
285
  registerChart(chartId: string, canvas: HTMLCanvasElement, config: ChartRegistrationConfig): ChartRegistrationResult;
265
286
  unregisterChart(chartId: string): void;
287
+ /**
288
+ * Debug: every transition that starts, and whether it animates or lands at
289
+ * once (see transition-log). Returns the unsubscribe. Forwards to the
290
+ * page-wide log: records from every coordinator in the document arrive
291
+ * here, so filter by chart id.
292
+ */
293
+ observeTransitions(listener: (record: TransitionRecord) => void): () => void;
294
+ /** Whether a registered chart is visible: in view (with the 200 px margin) and the page shown. */
295
+ isChartVisible(chartId: string): boolean;
266
296
  /**
267
297
  * Register a callback invoked when a chart's effective visibility changes.
268
298
  * Effective visibility = IntersectionObserver visible AND page visible.
269
- * Used by ChartCell to toggle Mosaic client.enabled.
299
+ * Used by ChartCell to report into the chart's ChartVisibility.
270
300
  */
271
301
  setVisibilityCallback(chartId: string, callback: (visible: boolean) => void): () => void;
272
302
  invalidateChart(chartId: string): void;
@@ -1,5 +1,7 @@
1
+ import { observeTransitions } from '../animation/transition-log';
1
2
  import { setContext, getContext } from 'svelte';
2
3
  import { AnimationLoop } from '../animation';
4
+ import { TransitionClock, ENTRANCE_FADE_STIFFNESS, ENTRANCE_GROW_STIFFNESS } from '../animation/TransitionClock';
3
5
  import { scaleTemplateSystem, CHART_SCALE_IMMEDIATE_SIZE } from './ScaleTemplateSystem';
4
6
  import { isCategoricalScale } from '../scales/scale-categories';
5
7
  import { MAX_SIZE_SLOTS } from './SizeScaleShaderLib';
@@ -19,8 +21,23 @@ export const COLOR_RAMP_RESOLUTION = 256;
19
21
  function layerCanDraw(layer, chartScalesKnown) {
20
22
  return (!!layer.renderer?.perDataBindGroup && (chartScalesKnown || layer.readsChartScales === false));
21
23
  }
24
+ /** A guide (a grid line) rather than data: it neither enters nor starts the entrance. */
25
+ function isGuideLayer(layer) {
26
+ return layer.markType.startsWith('grid-line-');
27
+ }
22
28
  /** No fields to pack, for a coordinator without filter groups. */
23
29
  const NO_FIELDS = [];
30
+ /**
31
+ * A chart's entrance once the viewer's preference is applied: under reduced
32
+ * motion, 'auto' fades rather than grows. Opacity is not motion, so a fade
33
+ * stays; an author's explicit 'fade' or 'none' is kept as is.
34
+ */
35
+ function resolveEntrance(mode) {
36
+ const chosen = mode ?? 'auto';
37
+ if (chosen !== 'auto' || typeof matchMedia === 'undefined')
38
+ return chosen;
39
+ return matchMedia('(prefers-reduced-motion: reduce)').matches ? 'fade' : 'auto';
40
+ }
24
41
  // =============================================================================
25
42
  // HELPER CLASSES
26
43
  // =============================================================================
@@ -477,10 +494,6 @@ export class GPUCoordinator {
477
494
  usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
478
495
  });
479
496
  }
480
- /**
481
- * Fill CPU buffer with current scale GPU state.
482
- * Zero-allocation hot path.
483
- */
484
497
  /**
485
498
  * Buffers for the slot table and its per-frame resolve.
486
499
  *
@@ -746,8 +759,115 @@ export class GPUCoordinator {
746
759
  this.#device.queue.writeBuffer(buffer, 0, view);
747
760
  last.set(view);
748
761
  }
749
- /** Reused per pass: the chart's x and y ScaleParam, pushed as immediates. */
762
+ /** Reused per pass: the chart's x and y ScaleParam and entrance, pushed as immediates. */
750
763
  #chartScaleImmediates = new Float32Array(CHART_SCALE_IMMEDIATE_SIZE / 4);
764
+ /** Debug listeners for chart entrances (observeEntrances). */
765
+ #entranceObservers = new Set();
766
+ /**
767
+ * Listen to every chart's entrance as it starts: when its marks have their
768
+ * first data and it first draws. For measuring load (how soon charts enter);
769
+ * returns the unsubscribe. Costs nothing while no one listens.
770
+ */
771
+ observeEntrances(listener) {
772
+ this.#entranceObservers.add(listener);
773
+ return () => this.#entranceObservers.delete(listener);
774
+ }
775
+ /**
776
+ * Begin a chart's entrance on the first pass that draws data in it.
777
+ *
778
+ * Per chart, not per mark: every mark in the chart reads the same immediate,
779
+ * so a bar chart and the line drawn over it enter together. Marks normally
780
+ * land in one query batch; one landing mid-entrance joins at the current t,
781
+ * and one landing after it simply appears.
782
+ *
783
+ * Started here rather than on registration, so an off-screen chart enters
784
+ * when it is first drawn, which is when it can be seen.
785
+ */
786
+ #startEntranceIfDrawing(chart, scalesKnown) {
787
+ // Every mark's first data first, so the chart enters once, whole. Only
788
+ // this chart's marks: a chart sharing a scale that lands later springs
789
+ // the domain to the combined extent, as any later data would.
790
+ const gate = chart.entranceGate;
791
+ if (gate) {
792
+ if (!gate.canEnter)
793
+ return;
794
+ gate.open();
795
+ }
796
+ const plan = chart.renderPlan;
797
+ let drawing = false;
798
+ for (let i = 0; i < plan.length; i++) {
799
+ // Data marks only: a grid line draws as soon as the scales exist, which
800
+ // is usually before the query has answered, and would start the
801
+ // entrance with nothing in it.
802
+ if (isGuideLayer(plan[i]))
803
+ continue;
804
+ if (layerCanDraw(plan[i], scalesKnown)) {
805
+ drawing = true;
806
+ break;
807
+ }
808
+ }
809
+ if (!drawing)
810
+ return;
811
+ chart.entranceStarted = true;
812
+ if (this.#entranceObservers.size > 0) {
813
+ const entrance = {
814
+ chartId: chart.chartId,
815
+ canvas: chart.canvas,
816
+ time: performance.now()
817
+ };
818
+ for (const observe of this.#entranceObservers) {
819
+ try {
820
+ observe(entrance);
821
+ }
822
+ catch (e) {
823
+ console.error('[GPUCoordinator] entrance observer threw:', e);
824
+ }
825
+ }
826
+ }
827
+ if (chart.entranceMode === 'none')
828
+ return;
829
+ const clock = new TransitionClock({
830
+ loop: this,
831
+ stiffness: ENTRANCE_GROW_STIFFNESS,
832
+ charts: () => chart.entranceCharts,
833
+ onRest: () => {
834
+ if (chart.entranceClock === clock)
835
+ chart.entranceClock = null;
836
+ }
837
+ });
838
+ chart.entranceClock = clock;
839
+ // Ease-out: growth should move from its first frame.
840
+ clock.restartEaseOut();
841
+ const fade = new TransitionClock({
842
+ loop: this,
843
+ stiffness: ENTRANCE_FADE_STIFFNESS,
844
+ charts: () => chart.entranceCharts,
845
+ onRest: () => {
846
+ if (chart.entranceFadeClock === fade)
847
+ chart.entranceFadeClock = null;
848
+ }
849
+ });
850
+ chart.entranceFadeClock = fade;
851
+ // From rest: a soft S-curve for opacity.
852
+ fade.restart();
853
+ }
854
+ /**
855
+ * The entrance immediates: x = growth, y = a growing mark's opacity, z = a
856
+ * fading mark's opacity. The mode is folded in here, so a mark only says
857
+ * whether it grows or fades.
858
+ */
859
+ #writeEntrance(chart, view) {
860
+ // Before the entrance starts the chart draws nothing, so 0 is never
861
+ // seen; after it, the clocks are gone and everything is at 1.
862
+ const idle = chart.entranceStarted ? 1 : 0;
863
+ const grow = chart.entranceClock ? chart.entranceClock.t : idle;
864
+ const fade = chart.entranceFadeClock ? chart.entranceFadeClock.t : idle;
865
+ const mode = chart.entranceMode;
866
+ view[8] = mode === 'auto' ? grow : 1;
867
+ view[9] = mode === 'fade' ? fade : 1;
868
+ view[10] = mode === 'none' ? 1 : fade;
869
+ view[11] = 0;
870
+ }
751
871
  // =========================================================================
752
872
  // READBACK (test-only)
753
873
  // =========================================================================
@@ -996,6 +1116,9 @@ export class GPUCoordinator {
996
1116
  // known: a layer that does not read them still needs every byte set
997
1117
  // before it may draw.
998
1118
  const scalesKnown = this.#writeChartScales(chartConfig, this.#chartScaleImmediates);
1119
+ if (!chartConfig.entranceStarted)
1120
+ this.#startEntranceIfDrawing(chartConfig, scalesKnown);
1121
+ this.#writeEntrance(chartConfig, this.#chartScaleImmediates);
999
1122
  passEncoder.setImmediates(0, this.#chartScaleImmediates);
1000
1123
  if (chartConfig.perFrameBindGroup) {
1001
1124
  passEncoder.setBindGroup(0, chartConfig.perFrameBindGroup);
@@ -1501,8 +1624,19 @@ export class GPUCoordinator {
1501
1624
  renderPassDescriptor: null,
1502
1625
  msaa: null,
1503
1626
  _lastRenderFrame: -1,
1504
- _isDirty: false
1627
+ _isDirty: false,
1628
+ entranceMode: resolveEntrance(config.entrance),
1629
+ entranceStarted: false,
1630
+ entranceClock: null,
1631
+ entranceFadeClock: null,
1632
+ entranceCharts: [chartId],
1633
+ entranceGate: config.entranceGate ?? null,
1634
+ entranceGateOff: null
1505
1635
  };
1636
+ // The gate opens on a mark's data or a timeout, neither of which need
1637
+ // redraw this chart. Without a frame the entrance would wait for one.
1638
+ chartConfig.entranceGateOff =
1639
+ chartConfig.entranceGate?.onOpen(() => this.invalidateChart(chartId)) ?? null;
1506
1640
  this.#registeredCharts.set(chartId, chartConfig);
1507
1641
  // Observe canvas for viewport visibility
1508
1642
  if (this.#visibilityObserver) {
@@ -1552,6 +1686,12 @@ export class GPUCoordinator {
1552
1686
  const chartConfig = this.#registeredCharts.get(chartId);
1553
1687
  if (chartConfig) {
1554
1688
  chartConfig._isDirty = false;
1689
+ chartConfig.entranceClock?.stop();
1690
+ chartConfig.entranceClock = null;
1691
+ chartConfig.entranceFadeClock?.stop();
1692
+ chartConfig.entranceFadeClock = null;
1693
+ chartConfig.entranceGateOff?.();
1694
+ chartConfig.entranceGateOff = null;
1555
1695
  if (chartConfig.frameUniformsBuffer) {
1556
1696
  chartConfig.frameUniformsBuffer.destroy();
1557
1697
  }
@@ -1580,13 +1720,30 @@ export class GPUCoordinator {
1580
1720
  // =========================================================================
1581
1721
  // VIEWPORT VISIBILITY
1582
1722
  // =========================================================================
1723
+ /**
1724
+ * Debug: every transition that starts, and whether it animates or lands at
1725
+ * once (see transition-log). Returns the unsubscribe. Forwards to the
1726
+ * page-wide log: records from every coordinator in the document arrive
1727
+ * here, so filter by chart id.
1728
+ */
1729
+ observeTransitions(listener) {
1730
+ return observeTransitions(listener);
1731
+ }
1732
+ /** Whether a registered chart is visible: in view (with the 200 px margin) and the page shown. */
1733
+ isChartVisible(chartId) {
1734
+ return this.#registeredCharts.get(chartId)?.visible ?? false;
1735
+ }
1583
1736
  /**
1584
1737
  * Register a callback invoked when a chart's effective visibility changes.
1585
1738
  * Effective visibility = IntersectionObserver visible AND page visible.
1586
- * Used by ChartCell to toggle Mosaic client.enabled.
1739
+ * Used by ChartCell to report into the chart's ChartVisibility.
1587
1740
  */
1588
1741
  setVisibilityCallback(chartId, callback) {
1589
1742
  this.#visibilityCallbacks.set(chartId, callback);
1743
+ // Callbacks hear changes only. One set after the chart was already found
1744
+ // off screen would otherwise go on assuming it visible.
1745
+ if (this.#registeredCharts.get(chartId)?.visible === false)
1746
+ callback(false);
1590
1747
  return () => this.#visibilityCallbacks.delete(chartId);
1591
1748
  }
1592
1749
  #handleVisibilityChange(chartId, ioVisible) {
@@ -1,31 +1,4 @@
1
1
  import { TransitionClock, type ClockLoop } from '../animation/TransitionClock';
2
- /**
3
- * Channels of one mark that morph between data updates rather than replacing
4
- * outright.
5
- *
6
- * Each holds three buffers where a static channel holds one: `from` and `to` are
7
- * read by a compute pass, and `resolved = mix(from, to, t)` is what the vertex
8
- * stage binds — in place of the static buffer, not in addition to it. So
9
- * animating a channel costs the vertex stage nothing, which is what makes it
10
- * affordable on a mark already at the device's storage-buffer limit.
11
- *
12
- * THE FOLD. When a transition restarts, whatever is on screen becomes the new
13
- * `from`. On screen is the resolved buffer as last dispatched, so the fold is a
14
- * buffer copy, and it reads neither `from` nor `to` — which is what lets data
15
- * land at any moment, in any order relative to anything else. Writing it as
16
- * `from = mix(from, to, t)` would need the OLD `to`, making the order of two
17
- * writes load-bearing and silently wrong if they ever swap.
18
- *
19
- * THE SNAP. Some updates have nothing to travel from: the first one, a buffer
20
- * that had to be reallocated, and a row set that is not the one being animated.
21
- * Those copy `to` over `from`, which makes the mix the identity for every t.
22
- *
23
- * THE CLOCK. A mark ADOPTS the band scale's clock when it has one, so a bar's
24
- * height travels on the same number as its slot — two clocks with two velocities
25
- * desynchronise on a retarget, and the bar reaches its rank before or after it
26
- * finishes growing. It OWNS one otherwise. Adoption is safe across marks because
27
- * a mark with no new data folds to where it already is and does not move.
28
- */
29
2
  /**
30
3
  * The coordinator's slice this needs. Structural, so tests can supply a fake.
31
4
  * It is also the loop an owned clock joins — the clock does that itself.
@@ -61,6 +34,10 @@ export interface MorphedChannelsOptions {
61
34
  * travelling to its neighbour's value and back. So the claim is checked, and a
62
35
  * change snaps rather than interpolates. A hash rather than a comparison
63
36
  * because it costs one pass and no retained copy.
37
+ *
38
+ * Over the values' bits, read through a view of the same buffer: a
39
+ * histogram's rows are keyed by bin edges, fractions a sum would truncate
40
+ * away, so a re-bin could otherwise hash like the bins it replaced.
64
41
  */
65
42
  export declare function alignmentKey(ids: ArrayLike<number>): number;
66
43
  export declare class MorphedChannels {
@@ -85,8 +62,14 @@ export declare class MorphedChannels {
85
62
  *
86
63
  * All channels in one call, so one update starts one transition. Returns
87
64
  * whether any buffer was (re)allocated, which the caller must rebind.
65
+ *
66
+ * `land`: bindings whose new values take effect at once while the rest
67
+ * travel. For a channel whose change is shown some other way at that moment
68
+ * (an Other bar's split, when both sides of it look the same): sliding it
69
+ * would draw a movement nothing asked for. `'all'`: the whole update lands,
70
+ * for a catch-up result (MarkHandle.resultCause).
88
71
  */
89
- write(rowIds: ArrayLike<number>, channels: readonly (readonly [number, AllowSharedBufferSource])[]): boolean;
72
+ write(rowIds: ArrayLike<number>, channels: readonly (readonly [number, AllowSharedBufferSource])[], land?: readonly number[] | 'all'): boolean;
90
73
  /**
91
74
  * Stop morphing a channel and release its buffers. Returns whether it was
92
75
  * morphing, in which case the caller's binding has lost its buffer.
@@ -1,6 +1,36 @@
1
+ import { noteTransition, transitionsObserved } from '../animation/transition-log';
1
2
  import { RESOLVE_IMMEDIATE_SIZE, RESOLVE_WORKGROUP, resolvePipelineFor } from './ResolveShader';
2
3
  import { TransitionClock } from '../animation/TransitionClock';
3
4
  import { immediatesSupported } from './immediates';
5
+ /**
6
+ * Channels of one mark that morph between data updates rather than replacing
7
+ * outright.
8
+ *
9
+ * Each holds three buffers where a static channel holds one: `from` and `to` are
10
+ * read by a compute pass, and `resolved = mix(from, to, t)` is what the vertex
11
+ * stage binds — in place of the static buffer, not in addition to it. So
12
+ * animating a channel costs the vertex stage nothing, which is what makes it
13
+ * affordable on a mark already at the device's storage-buffer limit.
14
+ *
15
+ * THE FOLD. When a transition restarts, whatever is on screen becomes the new
16
+ * `from`. On screen is the resolved buffer as last dispatched, so the fold is a
17
+ * buffer copy, and it reads neither `from` nor `to` — which is what lets data
18
+ * land at any moment, in any order relative to anything else. Writing it as
19
+ * `from = mix(from, to, t)` would need the OLD `to`, making the order of two
20
+ * writes load-bearing and silently wrong if they ever swap.
21
+ *
22
+ * THE SNAP. Some updates have nothing to travel from: the first one, a buffer
23
+ * that had to be reallocated, and a row set that is not the one being animated.
24
+ * Those copy `to` over `from`, which makes the mix the identity for every t.
25
+ *
26
+ * THE CLOCK. A mark ADOPTS the band scale's clock when it has one, so a bar's
27
+ * height travels on the same number as its slot — two clocks with two velocities
28
+ * desynchronise on a retarget, and the bar reaches its rank before or after it
29
+ * finishes growing. It OWNS one otherwise. Adoption is safe across marks because
30
+ * a mark with no new data folds to where it already is and does not move.
31
+ */
32
+ /** No bindings: the default for `write`'s `land`. */
33
+ const NO_BINDINGS = [];
4
34
  /**
5
35
  * A cheap fingerprint of the band ids, per update.
6
36
  *
@@ -10,11 +40,16 @@ import { immediatesSupported } from './immediates';
10
40
  * travelling to its neighbour's value and back. So the claim is checked, and a
11
41
  * change snaps rather than interpolates. A hash rather than a comparison
12
42
  * because it costs one pass and no retained copy.
43
+ *
44
+ * Over the values' bits, read through a view of the same buffer: a
45
+ * histogram's rows are keyed by bin edges, fractions a sum would truncate
46
+ * away, so a re-bin could otherwise hash like the bins it replaced.
13
47
  */
14
48
  export function alignmentKey(ids) {
15
- let hash = ids.length;
16
- for (let i = 0; i < ids.length; i++)
17
- hash = (hash * 31 + ids[i]) | 0;
49
+ const bits = ids instanceof Float32Array ? new Uint32Array(ids.buffer, ids.byteOffset, ids.length) : ids;
50
+ let hash = bits.length;
51
+ for (let i = 0; i < bits.length; i++)
52
+ hash = (Math.imul(hash, 31) + bits[i]) | 0;
18
53
  return hash;
19
54
  }
20
55
  export class MorphedChannels {
@@ -87,8 +122,14 @@ export class MorphedChannels {
87
122
  *
88
123
  * All channels in one call, so one update starts one transition. Returns
89
124
  * whether any buffer was (re)allocated, which the caller must rebind.
125
+ *
126
+ * `land`: bindings whose new values take effect at once while the rest
127
+ * travel. For a channel whose change is shown some other way at that moment
128
+ * (an Other bar's split, when both sides of it look the same): sliding it
129
+ * would draw a movement nothing asked for. `'all'`: the whole update lands,
130
+ * for a catch-up result (MarkHandle.resultCause).
90
131
  */
91
- write(rowIds, channels) {
132
+ write(rowIds, channels, land = NO_BINDINGS) {
92
133
  const key = alignmentKey(rowIds);
93
134
  let travels = this.#alignmentKey === key;
94
135
  this.#alignmentKey = key;
@@ -100,6 +141,7 @@ export class MorphedChannels {
100
141
  reallocated = true;
101
142
  }
102
143
  channel.count = data.byteLength >> 2;
144
+ channel.land = land === 'all' || land.includes(binding);
103
145
  this.#device.queue.writeBuffer(channel.to, 0, data);
104
146
  }
105
147
  // A fresh buffer holds nothing that was ever on screen, so there is
@@ -107,10 +149,29 @@ export class MorphedChannels {
107
149
  if (reallocated)
108
150
  travels = false;
109
151
  this.#dirty = true;
110
- if (travels)
152
+ // Every channel landing travels nothing, so it starts no transition: on
153
+ // a shared band clock a restart would set every sibling folding for a
154
+ // transition with nowhere to go.
155
+ // A loop, not `every`: this runs on each data update of a brush, and
156
+ // the callback would allocate per write.
157
+ let allLand = land === 'all';
158
+ if (land !== 'all' && land.length > 0) {
159
+ allLand = true;
160
+ for (let i = 0; i < channels.length; i++) {
161
+ if (!land.includes(channels[i][0])) {
162
+ allLand = false;
163
+ break;
164
+ }
165
+ }
166
+ }
167
+ if (travels && !allLand)
111
168
  this.#startTransition();
112
- else
169
+ else if (!travels)
113
170
  this.#snapPending = true;
171
+ if (transitionsObserved()) {
172
+ // Snaps when the row set changed or a buffer grew, or every channel lands.
173
+ noteTransition('morph', !travels || allLand, this.#affectedCharts, this.#label);
174
+ }
114
175
  if (!this.#registered) {
115
176
  this.#registered = true;
116
177
  this.#host.registerComputeWork(this.#label, (encoder) => this.encode(encoder));
@@ -158,8 +219,10 @@ export class MorphedChannels {
158
219
  return;
159
220
  for (let i = 0; i < channels.length; i++) {
160
221
  const channel = channels[i];
161
- if (snap) {
222
+ if (snap || channel.land) {
223
+ // A landed channel: from = to, so the mix is its new value at any t.
162
224
  encoder.copyBufferToBuffer(channel.to, 0, channel.from, 0, channel.size);
225
+ channel.land = false;
163
226
  }
164
227
  else if (this.#foldPending) {
165
228
  // A transition interrupted mid-flight continues from where it is,
@@ -225,7 +288,8 @@ export class MorphedChannels {
225
288
  resolved: make('resolved'),
226
289
  size,
227
290
  count: 0,
228
- bindGroup: null
291
+ bindGroup: null,
292
+ land: false
229
293
  };
230
294
  this.#channels.set(binding, channel);
231
295
  this.#channelList = [...this.#channels.values()];
@@ -33,14 +33,14 @@ interface ShaderResult {
33
33
  }
34
34
  /**
35
35
  * Bytes of immediate data every render pipeline declares: the chart's x and y
36
- * `ScaleParam`, 16 bytes each. Every layout declares the same size because a
36
+ * `ScaleParam`, 16 bytes each, and its entrance, a vec4. Every layout declares the same size because a
37
37
  * draw requires every declared byte to have been set, and the pass sets them
38
38
  * once, before its first draw.
39
39
  *
40
40
  * Immediates carry state shared across the pass and read by several marks: the
41
41
  * chart's scales and settings. 64 bytes are available on M-series Macs.
42
42
  */
43
- export declare const CHART_SCALE_IMMEDIATE_SIZE = 32;
43
+ export declare const CHART_SCALE_IMMEDIATE_SIZE = 48;
44
44
  /**
45
45
  * Main template system class
46
46
  */
@@ -69,6 +69,24 @@ export declare class ScaleTemplateSystem {
69
69
  * @private
70
70
  */
71
71
  _buildDeclarations(xFragment: ScaleFragment, yFragment: ScaleFragment): string;
72
+ /**
73
+ * The chart's entrance, for marks to apply in the vertex stage.
74
+ *
75
+ * A mark either GROWS along one axis (bars, histograms and areas, from the
76
+ * value 0 on their measure axis) or FADES (everything else). Which one is
77
+ * the mark's to say, as `axis`: 0 fades, 1 grows along x, 2 along y. The
78
+ * chart's mode is already folded into the immediates, so a chart set to
79
+ * fade hands a growing mark g = 1 and its opacity instead.
80
+ *
81
+ * The base a mark grows from is where 0 sits on screen, clamped to the plot:
82
+ * a domain that leaves 0 out grows from the edge, which is where its bars
83
+ * visibly start anyway. A log axis has no 0 and grows from its low edge.
84
+ * Decided per scale type here, so no shader evaluates log(0).
85
+ */
86
+ _buildEntranceHelpers(scaleTypes: {
87
+ x: string;
88
+ y: string;
89
+ }): string;
72
90
  /**
73
91
  * `bandActivityX(value, group)` and `bandActivityY(value, group)`: how
74
92
  * selected a band or point axis's category is in the mark's filter group,