@fjader/svelte 0.2.4 → 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 (139) hide show
  1. package/README.md +1 -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 +4 -2
  9. package/dist/components/axes/AxisCenter.svelte +4 -2
  10. package/dist/components/axes/AxisLeft.svelte +4 -2
  11. package/dist/components/axes/BandAxisTicks.svelte +3 -2
  12. package/dist/components/charts/ChartCell.svelte +32 -5
  13. package/dist/components/charts/ChartCell.svelte.d.ts +9 -0
  14. package/dist/components/charts/ChartContainer.svelte +45 -3
  15. package/dist/components/charts/ChartContainer.svelte.d.ts +7 -1
  16. package/dist/components/charts/ChartHover.svelte.d.ts +14 -0
  17. package/dist/components/charts/ChartHover.svelte.js +22 -0
  18. package/dist/components/charts/ChartVisibility.svelte.d.ts +43 -0
  19. package/dist/components/charts/ChartVisibility.svelte.js +67 -0
  20. package/dist/components/guides/Grid.svelte +12 -1
  21. package/dist/components/marks/AreaMarkX.svelte +16 -4
  22. package/dist/components/marks/AreaMarkY.svelte +16 -4
  23. package/dist/components/marks/BarMarkX.svelte +7 -3
  24. package/dist/components/marks/BarMarkY.svelte +7 -3
  25. package/dist/components/marks/DotsMark.svelte +6 -3
  26. package/dist/components/marks/LineMark.svelte +11 -8
  27. package/dist/components/marks/ParallelCoordinatesMark.svelte +7 -2
  28. package/dist/components/marks/ParallelCoordinatesMark.svelte.d.ts +2 -0
  29. package/dist/components/marks/RectMark.svelte +6 -3
  30. package/dist/components/marks/RectXMark.svelte +7 -3
  31. package/dist/components/marks/RectYMark.svelte +7 -3
  32. package/dist/components/marks/RuleMarkX.svelte +30 -26
  33. package/dist/components/marks/RuleMarkY.svelte +30 -26
  34. package/dist/components/marks/TextMark.svelte +44 -5
  35. package/dist/components/marks/lifecycle/MarkLifecycleV2.svelte.d.ts +16 -0
  36. package/dist/components/marks/lifecycle/MarkLifecycleV2.svelte.js +398 -54
  37. package/dist/components/marks/lifecycle/domain-queries.d.ts +11 -2
  38. package/dist/components/marks/lifecycle/domain-queries.js +33 -6
  39. package/dist/components/marks/lifecycle/domain-registration.d.ts +6 -1
  40. package/dist/components/marks/lifecycle/domain-registration.js +15 -14
  41. package/dist/components/marks/lifecycle/other-rows.svelte.d.ts +9 -1
  42. package/dist/components/marks/lifecycle/other-rows.svelte.js +2 -2
  43. package/dist/components/marks/lifecycle/query-builder.d.ts +27 -2
  44. package/dist/components/marks/lifecycle/query-builder.js +23 -20
  45. package/dist/components/marks/utils/MarkColor.svelte.d.ts +3 -1
  46. package/dist/components/marks/utils/MarkColor.svelte.js +33 -3
  47. package/dist/components/marks/utils/MarkSeries.svelte.d.ts +14 -1
  48. package/dist/components/marks/utils/MarkSeries.svelte.js +25 -1
  49. package/dist/components/marks/utils/MarkSize.svelte.js +4 -0
  50. package/dist/components/marks/utils/reactive-config.d.ts +1 -1
  51. package/dist/components/marks/utils/reactive-config.js +3 -0
  52. package/dist/components/rendering/Areas.svelte +57 -19
  53. package/dist/components/rendering/Areas.svelte.d.ts +12 -0
  54. package/dist/components/rendering/BaseGPURenderer.svelte.d.ts +1 -1
  55. package/dist/components/rendering/Dots.svelte +23 -3
  56. package/dist/components/rendering/Dots.svelte.d.ts +6 -0
  57. package/dist/components/rendering/GridLine.svelte +13 -1
  58. package/dist/components/rendering/GridLine.svelte.d.ts +5 -0
  59. package/dist/components/rendering/Lines.svelte +48 -13
  60. package/dist/components/rendering/Lines.svelte.d.ts +12 -0
  61. package/dist/components/rendering/ParallelLines.svelte +5 -5
  62. package/dist/components/rendering/Rects.svelte +51 -11
  63. package/dist/components/rendering/Rects.svelte.d.ts +16 -2
  64. package/dist/components/rendering/RuleX.svelte +1 -1
  65. package/dist/components/rendering/RuleY.svelte +1 -1
  66. package/dist/components/rendering/WebGPUCanvas.svelte +4 -2
  67. package/dist/components/rendering/activity-reads.d.ts +41 -24
  68. package/dist/components/rendering/activity-reads.js +54 -31
  69. package/dist/components/rendering/uniform-sizes.d.ts +1 -1
  70. package/dist/components/rendering/uniform-sizes.js +1 -1
  71. package/dist/config/ChartConfig.svelte.d.ts +7 -0
  72. package/dist/config/ChartConfig.svelte.js +17 -0
  73. package/dist/gpu/GPUCoordinator.svelte.d.ts +31 -1
  74. package/dist/gpu/GPUCoordinator.svelte.js +164 -7
  75. package/dist/gpu/MorphedChannels.d.ts +11 -2
  76. package/dist/gpu/MorphedChannels.js +33 -11
  77. package/dist/gpu/ScaleTemplateSystem.d.ts +20 -2
  78. package/dist/gpu/ScaleTemplateSystem.js +58 -2
  79. package/dist/index.d.ts +17 -4
  80. package/dist/index.js +19 -4
  81. package/dist/interactors/TooltipManager.svelte.js +14 -5
  82. package/dist/query-engine/CrossfilterSelection.svelte.d.ts +4 -2
  83. package/dist/query-engine/CrossfilterSelection.svelte.js +2 -0
  84. package/dist/query-engine/FieldActivity.svelte.d.ts +2 -3
  85. package/dist/query-engine/FieldActivity.svelte.js +1 -2
  86. package/dist/query-engine/MarkHandle.svelte.d.ts +28 -1
  87. package/dist/query-engine/MarkHandle.svelte.js +33 -1
  88. package/dist/query-engine/QueryEngine.d.ts +225 -5
  89. package/dist/query-engine/QueryEngine.js +671 -41
  90. package/dist/query-engine/categorical-identity.d.ts +34 -8
  91. package/dist/query-engine/categorical-identity.js +138 -73
  92. package/dist/query-engine/debug/query-log.d.ts +39 -0
  93. package/dist/query-engine/debug/query-log.js +90 -0
  94. package/dist/query-engine/duckdb.d.ts +55 -8
  95. package/dist/query-engine/duckdb.js +82 -30
  96. package/dist/query-engine/index.d.ts +7 -4
  97. package/dist/query-engine/index.js +3 -3
  98. package/dist/query-engine/preagg/BuildQueue.d.ts +33 -13
  99. package/dist/query-engine/preagg/BuildQueue.js +51 -15
  100. package/dist/query-engine/preagg/Preagg.d.ts +46 -3
  101. package/dist/query-engine/preagg/Preagg.js +159 -31
  102. package/dist/query-engine/preagg/plan.d.ts +28 -1
  103. package/dist/query-engine/preagg/plan.js +128 -8
  104. package/dist/query-engine/sliced-lane.d.ts +108 -0
  105. package/dist/query-engine/sliced-lane.js +299 -0
  106. package/dist/query-engine/source-routing.d.ts +106 -0
  107. package/dist/query-engine/source-routing.js +189 -0
  108. package/dist/scales/BandScaleState.svelte.d.ts +9 -1
  109. package/dist/scales/BandScaleState.svelte.js +16 -5
  110. package/dist/scales/ChartScales.svelte.d.ts +3 -0
  111. package/dist/scales/ChartScales.svelte.js +26 -8
  112. package/dist/scales/ColorScaleState.svelte.d.ts +18 -2
  113. package/dist/scales/ColorScaleState.svelte.js +138 -26
  114. package/dist/scales/LinearScaleState.svelte.d.ts +7 -10
  115. package/dist/scales/LinearScaleState.svelte.js +21 -39
  116. package/dist/scales/ScaleGeneration.svelte.d.ts +48 -0
  117. package/dist/scales/ScaleGeneration.svelte.js +42 -0
  118. package/dist/scales/ScaleRegistry.svelte.js +3 -1
  119. package/dist/scales/SlotTransition.d.ts +15 -1
  120. package/dist/scales/SlotTransition.js +33 -4
  121. package/dist/scales/TimeScaleState.svelte.d.ts +7 -3
  122. package/dist/scales/TimeScaleState.svelte.js +7 -7
  123. package/dist/scales/color-utils.d.ts +0 -3
  124. package/dist/scales/color-utils.js +0 -3
  125. package/dist/scales/entered.d.ts +8 -0
  126. package/dist/scales/entered.js +10 -0
  127. package/dist/spec/LazyQueryEngine.js +2 -19
  128. package/dist/spec/MarkRegistry.d.ts +3 -1
  129. package/dist/spec/MarkRegistry.js +13 -1
  130. package/dist/spec/PlotFromSpec.svelte +1 -0
  131. package/dist/spec/export.js +11 -2
  132. package/dist/spec/types.d.ts +41 -13
  133. package/dist/sql/bin.d.ts +25 -0
  134. package/dist/sql/bin.js +108 -5
  135. package/dist/transforms/TransformCompiler.d.ts +10 -0
  136. package/dist/transforms/TransformCompiler.js +26 -0
  137. package/dist/transforms/index.js +144 -24
  138. package/dist/types.d.ts +136 -2
  139. package/package.json +2 -2
@@ -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,4 +1,8 @@
1
1
  import { TransitionClock, type ClockLoop } from '../animation/TransitionClock';
2
+ /**
3
+ * The coordinator's slice this needs. Structural, so tests can supply a fake.
4
+ * It is also the loop an owned clock joins — the clock does that itself.
5
+ */
2
6
  export interface MorphHost extends ClockLoop {
3
7
  registerComputeWork(key: string, fn: (encoder: GPUCommandEncoder) => void): void;
4
8
  removeComputeWork(key: string): void;
@@ -30,6 +34,10 @@ export interface MorphedChannelsOptions {
30
34
  * travelling to its neighbour's value and back. So the claim is checked, and a
31
35
  * change snaps rather than interpolates. A hash rather than a comparison
32
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.
33
41
  */
34
42
  export declare function alignmentKey(ids: ArrayLike<number>): number;
35
43
  export declare class MorphedChannels {
@@ -58,9 +66,10 @@ export declare class MorphedChannels {
58
66
  * `land`: bindings whose new values take effect at once while the rest
59
67
  * travel. For a channel whose change is shown some other way at that moment
60
68
  * (an Other bar's split, when both sides of it look the same): sliding it
61
- * would draw a movement nothing asked for.
69
+ * would draw a movement nothing asked for. `'all'`: the whole update lands,
70
+ * for a catch-up result (MarkHandle.resultCause).
62
71
  */
63
- write(rowIds: ArrayLike<number>, channels: readonly (readonly [number, AllowSharedBufferSource])[], land?: readonly number[]): boolean;
72
+ write(rowIds: ArrayLike<number>, channels: readonly (readonly [number, AllowSharedBufferSource])[], land?: readonly number[] | 'all'): boolean;
64
73
  /**
65
74
  * Stop morphing a channel and release its buffers. Returns whether it was
66
75
  * morphing, in which case the caller's binding has lost its buffer.
@@ -1,3 +1,4 @@
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';
@@ -28,10 +29,6 @@ import { immediatesSupported } from './immediates';
28
29
  * finishes growing. It OWNS one otherwise. Adoption is safe across marks because
29
30
  * a mark with no new data folds to where it already is and does not move.
30
31
  */
31
- /**
32
- * The coordinator's slice this needs. Structural, so tests can supply a fake.
33
- * It is also the loop an owned clock joins — the clock does that itself.
34
- */
35
32
  /** No bindings: the default for `write`'s `land`. */
36
33
  const NO_BINDINGS = [];
37
34
  /**
@@ -43,11 +40,16 @@ const NO_BINDINGS = [];
43
40
  * travelling to its neighbour's value and back. So the claim is checked, and a
44
41
  * change snaps rather than interpolates. A hash rather than a comparison
45
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.
46
47
  */
47
48
  export function alignmentKey(ids) {
48
- let hash = ids.length;
49
- for (let i = 0; i < ids.length; i++)
50
- 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;
51
53
  return hash;
52
54
  }
53
55
  export class MorphedChannels {
@@ -124,7 +126,8 @@ export class MorphedChannels {
124
126
  * `land`: bindings whose new values take effect at once while the rest
125
127
  * travel. For a channel whose change is shown some other way at that moment
126
128
  * (an Other bar's split, when both sides of it look the same): sliding it
127
- * would draw a movement nothing asked for.
129
+ * would draw a movement nothing asked for. `'all'`: the whole update lands,
130
+ * for a catch-up result (MarkHandle.resultCause).
128
131
  */
129
132
  write(rowIds, channels, land = NO_BINDINGS) {
130
133
  const key = alignmentKey(rowIds);
@@ -138,7 +141,7 @@ export class MorphedChannels {
138
141
  reallocated = true;
139
142
  }
140
143
  channel.count = data.byteLength >> 2;
141
- channel.land = land.includes(binding);
144
+ channel.land = land === 'all' || land.includes(binding);
142
145
  this.#device.queue.writeBuffer(channel.to, 0, data);
143
146
  }
144
147
  // A fresh buffer holds nothing that was ever on screen, so there is
@@ -146,10 +149,29 @@ export class MorphedChannels {
146
149
  if (reallocated)
147
150
  travels = false;
148
151
  this.#dirty = true;
149
- 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)
150
168
  this.#startTransition();
151
- else
169
+ else if (!travels)
152
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
+ }
153
175
  if (!this.#registered) {
154
176
  this.#registered = true;
155
177
  this.#host.registerComputeWork(this.#label, (encoder) => this.encode(encoder));
@@ -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,
@@ -280,14 +280,14 @@ function _scaleFunctionName(scaleType, axis) {
280
280
  }
281
281
  /**
282
282
  * Bytes of immediate data every render pipeline declares: the chart's x and y
283
- * `ScaleParam`, 16 bytes each. Every layout declares the same size because a
283
+ * `ScaleParam`, 16 bytes each, and its entrance, a vec4. Every layout declares the same size because a
284
284
  * draw requires every declared byte to have been set, and the pass sets them
285
285
  * once, before its first draw.
286
286
  *
287
287
  * Immediates carry state shared across the pass and read by several marks: the
288
288
  * chart's scales and settings. 64 bytes are available on M-series Macs.
289
289
  */
290
- export const CHART_SCALE_IMMEDIATE_SIZE = 32;
290
+ export const CHART_SCALE_IMMEDIATE_SIZE = 48;
291
291
  /**
292
292
  * Main template system class
293
293
  */
@@ -376,6 +376,7 @@ export class ScaleTemplateSystem {
376
376
  // ===== 3. HELPER FUNCTIONS =====
377
377
 
378
378
  ${this._buildSlotTableHelpers()}
379
+ ${this._buildEntranceHelpers(scaleTypes)}
379
380
  ${this._buildBandActivityHelpers(scaleTypes)}
380
381
  ${bandHelpers}
381
382
  ${implementations}
@@ -413,6 +414,10 @@ export class ScaleTemplateSystem {
413
414
  struct ChartScales {
414
415
  x: ScaleParam,
415
416
  y: ScaleParam,
417
+ // The chart's entrance, written per pass from its entrance clock:
418
+ // x = how far a growing mark has grown, y = a growing mark's opacity,
419
+ // z = a fading mark's opacity. All 1 once the chart has entered.
420
+ entrance: vec4<f32>,
416
421
  };
417
422
  var<immediate> chartScales: ChartScales;
418
423
 
@@ -448,6 +453,57 @@ export class ScaleTemplateSystem {
448
453
  `;
449
454
  return `${structDef}\n${chartScaleDeclaration}`;
450
455
  }
456
+ /**
457
+ * The chart's entrance, for marks to apply in the vertex stage.
458
+ *
459
+ * A mark either GROWS along one axis (bars, histograms and areas, from the
460
+ * value 0 on their measure axis) or FADES (everything else). Which one is
461
+ * the mark's to say, as `axis`: 0 fades, 1 grows along x, 2 along y. The
462
+ * chart's mode is already folded into the immediates, so a chart set to
463
+ * fade hands a growing mark g = 1 and its opacity instead.
464
+ *
465
+ * The base a mark grows from is where 0 sits on screen, clamped to the plot:
466
+ * a domain that leaves 0 out grows from the edge, which is where its bars
467
+ * visibly start anyway. A log axis has no 0 and grows from its low edge.
468
+ * Decided per scale type here, so no shader evaluates log(0).
469
+ */
470
+ _buildEntranceHelpers(scaleTypes) {
471
+ const base = (axis, type) => {
472
+ const a = axis.toLowerCase();
473
+ const lo = `frameUniforms.rangeMin.${a}`;
474
+ const hi = `frameUniforms.rangeMax.${a}`;
475
+ if (type === 'band' || type === 'point')
476
+ return '0.0';
477
+ // Screen y runs downward: the low edge of a y axis is rangeMax.
478
+ if (type === 'log')
479
+ return axis === 'X' ? lo : hi;
480
+ return `clamp(${_scaleFunctionName(type, axis)}From(0.0, chartScale${axis}()), ${lo}, ${hi})`;
481
+ };
482
+ return `
483
+ // ===== ENTRANCE =====
484
+ fn entranceBaseX() -> f32 {
485
+ return ${base('X', scaleTypes.x)};
486
+ }
487
+ fn entranceBaseY() -> f32 {
488
+ return ${base('Y', scaleTypes.y)};
489
+ }
490
+ // A screen position as it is during the entrance: pulled toward the base
491
+ // on the growing axis, untouched otherwise.
492
+ fn entrancePosition(p: vec2<f32>, axis: u32) -> vec2<f32> {
493
+ let g = chartScales.entrance.x;
494
+ if (axis == 1u) {
495
+ return vec2<f32>(mix(entranceBaseX(), p.x, g), p.y);
496
+ }
497
+ if (axis == 2u) {
498
+ return vec2<f32>(p.x, mix(entranceBaseY(), p.y, g));
499
+ }
500
+ return p;
501
+ }
502
+ fn entranceAlpha(axis: u32) -> f32 {
503
+ return select(chartScales.entrance.z, chartScales.entrance.y, axis != 0u);
504
+ }
505
+ `;
506
+ }
451
507
  /**
452
508
  * `bandActivityX(value, group)` and `bandActivityY(value, group)`: how
453
509
  * selected a band or point axis's category is in the mark's filter group,
package/dist/index.d.ts CHANGED
@@ -1,4 +1,11 @@
1
- import type { DuckDBConnection } from './query-engine';
1
+ import type { DuckDBConnection, InitDuckDBOptions, QueryEngineOptions } from './query-engine';
2
+ export interface FjaderOptions extends InitDuckDBOptions {
3
+ /** An existing connection (or a promise of one); skips internal DuckDB-WASM init. */
4
+ connection?: DuckDBConnection | Promise<DuckDBConnection>;
5
+ /** Advanced: query engine tuning (see QueryEngineOptions). */
6
+ engine?: QueryEngineOptions;
7
+ }
8
+ type FjaderInit = DuckDBConnection | Promise<DuckDBConnection> | FjaderOptions;
2
9
  /**
3
10
  * Initialize Fjader: QueryEngine, ScaleRegistry, GPUCoordinator, FilterGroups.
4
11
  * Call once in a top-level component's <script> block.
@@ -6,10 +13,14 @@ import type { DuckDBConnection } from './query-engine';
6
13
  * import { initFjader } from '@fjader/svelte';
7
14
  * const { queryEngine } = initFjader();
8
15
  *
9
- * Pass an existing DuckDB connection to skip internal DuckDB-WASM init:
10
- * const { queryEngine } = initFjader(myConnection);
16
+ * Serve the DuckDB-WASM bundles yourself instead of loading them from jsDelivr:
17
+ * initFjader({ bundles: { mvp: { mainModule, mainWorker }, eh: { … } } });
18
+ *
19
+ * Pass an existing DuckDB connection to skip internal DuckDB-WASM init, e.g. to
20
+ * keep the AsyncDuckDB instance for registerFileBuffer:
21
+ * const { queryEngine } = initFjader(connectionFrom(await db.connect()));
11
22
  */
12
- export declare function initFjader(connection?: DuckDBConnection | Promise<DuckDBConnection>): {
23
+ export declare function initFjader(init?: FjaderInit): {
13
24
  queryEngine: import("./query-engine").QueryEngine;
14
25
  filterGroups: import("./query-engine").FilterGroupRegistry;
15
26
  scaleRegistry: import("./scales").ScaleRegistry;
@@ -20,6 +31,8 @@ export * from './transforms';
20
31
  export * from './sql/bin';
21
32
  export * from './types';
22
33
  export { getFilterGroups } from './query-engine';
34
+ export { connectionFrom } from './query-engine';
35
+ export type { DuckDBConnection, InitDuckDBOptions } from './query-engine';
23
36
  export type { FilterGroupRegistry, FilterGroup, FilterItem } from './query-engine';
24
37
  export { Fjader, render } from './spec';
25
38
  export type { FjaderSpec, FjaderHandle, RenderOptions } from './spec';
package/dist/index.js CHANGED
@@ -2,6 +2,14 @@
2
2
  import { setScaleRegistry } from './scales';
3
3
  import { setGPUCoordinator } from './gpu';
4
4
  import { setQueryContext, initDuckDB } from './query-engine';
5
+ /** A connection has query(), a promise has then(); anything else is options. */
6
+ function toOptions(init) {
7
+ if (!init)
8
+ return {};
9
+ if ('query' in init || 'then' in init)
10
+ return { connection: init };
11
+ return init;
12
+ }
5
13
  /**
6
14
  * Initialize Fjader: QueryEngine, ScaleRegistry, GPUCoordinator, FilterGroups.
7
15
  * Call once in a top-level component's <script> block.
@@ -9,11 +17,16 @@ import { setQueryContext, initDuckDB } from './query-engine';
9
17
  * import { initFjader } from '@fjader/svelte';
10
18
  * const { queryEngine } = initFjader();
11
19
  *
12
- * Pass an existing DuckDB connection to skip internal DuckDB-WASM init:
13
- * const { queryEngine } = initFjader(myConnection);
20
+ * Serve the DuckDB-WASM bundles yourself instead of loading them from jsDelivr:
21
+ * initFjader({ bundles: { mvp: { mainModule, mainWorker }, eh: { … } } });
22
+ *
23
+ * Pass an existing DuckDB connection to skip internal DuckDB-WASM init, e.g. to
24
+ * keep the AsyncDuckDB instance for registerFileBuffer:
25
+ * const { queryEngine } = initFjader(connectionFrom(await db.connect()));
14
26
  */
15
- export function initFjader(connection) {
16
- const { queryEngine, filterGroups } = setQueryContext(connection ?? initDuckDB());
27
+ export function initFjader(init) {
28
+ const { connection, bundles, engine } = toOptions(init);
29
+ const { queryEngine, filterGroups } = setQueryContext(connection ?? initDuckDB({ bundles }), engine);
17
30
  const scaleRegistry = setScaleRegistry();
18
31
  const gpuCoordinator = setGPUCoordinator({ msaaSampleCount: 4 });
19
32
  gpuCoordinator.setScaleRegistry(scaleRegistry);
@@ -28,5 +41,7 @@ export * from './types';
28
41
  // Selection state: the registry initFjader() returns, for code outside it (is anything
29
42
  // brushed? which groups exist?). Read from the same context.
30
43
  export { getFilterGroups } from './query-engine';
44
+ // DuckDB: adapt an app-owned duckdb-wasm connection for initFjader(connection).
45
+ export { connectionFrom } from './query-engine';
31
46
  // Spec-driven rendering API
32
47
  export { Fjader, render } from './spec';
@@ -186,7 +186,16 @@ export class TooltipManager {
186
186
  if (!sql)
187
187
  continue;
188
188
  queryPromises.push(this.#queryEngine
189
- .query(sql, { callerId: 'tooltip' })
189
+ // Per mark: one shared caller id let execute-latest drop every
190
+ // lookup but the first and last when a chart has three or more
191
+ // marks. Not cached: the tooltip table is rewritten on each data
192
+ // change while a lookup's SQL can stay the same.
193
+ .query(sql, {
194
+ callerId: `tooltip:${markId}`,
195
+ cache: false,
196
+ // The tooltip table is TEMP, on the main connection.
197
+ connectionLocal: true
198
+ })
190
199
  .then((result) => {
191
200
  const rows = Array.from(result);
192
201
  return rows.length > 0 ? { markId, config, rows } : null;
@@ -500,10 +509,6 @@ export class TooltipManager {
500
509
  // =========================================================================
501
510
  // LABEL RESOLUTION
502
511
  // =========================================================================
503
- /**
504
- * Resolve band indices, time offsets, and color indices back to human-readable labels.
505
- * Renames columns from aliases (x, y, color) to original field names.
506
- */
507
512
  /** Resolve a single value: band index → label, time offset → formatted date. */
508
513
  #resolveValue(axis, key, value, config) {
509
514
  const { scaleTypes, bandDomains } = config;
@@ -534,6 +539,10 @@ export class TooltipManager {
534
539
  }
535
540
  return String(value ?? '');
536
541
  }
542
+ /**
543
+ * Resolve band indices, time offsets, and color indices back to human-readable labels.
544
+ * Renames columns from aliases (x, y, color) to original field names.
545
+ */
537
546
  #resolveLabels(row, config) {
538
547
  const { scaleTypes, channelFields, bandDomains, colorDomain, columns } = config;
539
548
  const point = {};
@@ -16,6 +16,7 @@
16
16
  * Both are computed when asked, from the clause, the mark's route and the
17
17
  * live registrations: nothing is cached, so nothing goes stale.
18
18
  */
19
+ import type { SelectionResponse } from '../components/rendering/activity-reads';
19
20
  import type { ExprNode } from '@uwdata/mosaic-sql';
20
21
  export type FilterExpr = ExprNode | string | boolean;
21
22
  export interface FilterSchema {
@@ -53,12 +54,13 @@ export interface Clause {
53
54
  * faceted chart's cells have ids of their own, but share one config.
54
55
  * `response` 'filter' makes every selection filter the mark, and a click
55
56
  * control's (a discrete registration's) in its own chart too; brushes still
56
- * skip their own chart.
57
+ * skip their own chart. 'ignore' makes no selection filter it, so no clause
58
+ * change re-queries it and preagg builds nothing for it.
57
59
  */
58
60
  export interface MarkRoute {
59
61
  readonly reads: readonly string[];
60
62
  readonly chart: object | null;
61
- readonly response?: 'auto' | 'filter';
63
+ readonly response?: SelectionResponse;
62
64
  }
63
65
  /**
64
66
  * What routing reads from one registration of a source: the chart it skips,
@@ -57,6 +57,8 @@ export class CrossfilterSelection {
57
57
  * no allocation, as it runs per mark per clause on every brush frame.
58
58
  */
59
59
  filters(route, sourceId, key) {
60
+ if (route.response === 'ignore')
61
+ return false;
60
62
  if (key !== null && route.reads.includes(key))
61
63
  return false;
62
64
  const chart = route.chart;