@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
@@ -144,7 +144,8 @@ export class FilterGroup {
144
144
  };
145
145
  this.#afterUpdate(sourceId, selection.update(sourceId, next), next);
146
146
  },
147
- clear: () => this.clearSource(sourceId),
147
+ // The registration's own clear: it already reset what it drew.
148
+ clear: () => this.clearSource(sourceId, opts),
148
149
  activate: () => this.#activate(sourceId),
149
150
  destroy: () => {
150
151
  // Once only: a second call must not remove a newer registration
@@ -158,7 +159,8 @@ export class FilterGroup {
158
159
  // while the registration is still in place, so the removal is
159
160
  // routed by it: the marks re-queried are those the clause filtered.
160
161
  this.#engine?.forgetSource(this, sourceId);
161
- this.clearSource(sourceId);
162
+ // Quietly: the registration is going away, nothing of it to reset.
163
+ this.clearSource(sourceId, opts);
162
164
  selection.removeRegistration(sourceId, opts);
163
165
  return;
164
166
  }
@@ -275,6 +277,24 @@ export class FilterGroup {
275
277
  write([...next.values()]);
276
278
  },
277
279
  set: (tuples) => write(tuples),
280
+ toggleOther: (otherId, facetId, multi) => {
281
+ if (list.length !== 1 || categoricalKey === null)
282
+ return;
283
+ const click = this.activity(categoricalKey).otherClick(this.index, otherId, facetId, keysOf(selection.get(sourceId)), multi);
284
+ if (!click)
285
+ return;
286
+ const next = new Map(tuplesOf(selection.get(sourceId)).map((t) => [tupleKey(t), t]));
287
+ if (click.action === 'replace')
288
+ next.clear();
289
+ for (const value of click.values) {
290
+ const tuple = [value];
291
+ if (click.action === 'remove')
292
+ next.delete(tupleKey(tuple));
293
+ else
294
+ next.set(tupleKey(tuple), tuple);
295
+ }
296
+ write([...next.values()]);
297
+ },
278
298
  clear: () => write([]),
279
299
  activate: source.activate,
280
300
  destroy: source.destroy
@@ -322,18 +342,38 @@ export class FilterGroup {
322
342
  /**
323
343
  * Remove a source's clause. Re-queries only when there was a clause to
324
344
  * remove: an unmount or a clear with nothing selected changes no mark's data.
345
+ *
346
+ * The source's registrations are told (`onCleared`) so a control that drew the
347
+ * selection resets it, all of them from outside, and all but `by` when a
348
+ * registration is the one clearing (it has reset itself).
325
349
  */
326
- clearSource(sourceId) {
350
+ clearSource(sourceId, by) {
327
351
  if (!this.selection.isActive(sourceId))
328
352
  return;
329
353
  this.#afterUpdate(sourceId, this.selection.update(sourceId, null), null);
354
+ this.#notifyCleared(sourceId, by);
330
355
  }
356
+ /** Remove every clause, and tell the controls that drew them (`onCleared`). */
331
357
  clearAll() {
358
+ const cleared = this.selection.items.map((item) => item.sourceId);
332
359
  this.selection.clear();
333
360
  for (const key of this.#selectedFields)
334
361
  this.#activityOf(key).set(this.index, null);
335
362
  this.#selectedFields.clear();
336
363
  this.#engine?.onFilterChange(this);
364
+ for (const sourceId of cleared)
365
+ this.#notifyCleared(sourceId);
366
+ }
367
+ /** Tell a source's registrations, but `except`, that its clause is gone. */
368
+ #notifyCleared(sourceId, except) {
369
+ const registrations = this.selection.registrationsOf(sourceId);
370
+ if (!registrations)
371
+ return;
372
+ // A copy: a control resetting itself may register or destroy while this runs.
373
+ for (const opts of [...registrations]) {
374
+ if (opts !== except)
375
+ opts.onCleared?.();
376
+ }
337
377
  }
338
378
  // -----------------------------------------------------------------
339
379
  // After a change
@@ -24,10 +24,38 @@ export declare class FilterGroupRegistry {
24
24
  * at once.
25
25
  */
26
26
  setAnimation(loop: ClockLoop, chartsFor: (key: string, out: string[]) => string[]): void;
27
- /** Get or create a FilterGroup by name. Undefined/null → "default". */
27
+ /**
28
+ * Get or create a FilterGroup by name. Undefined/null → "default".
29
+ *
30
+ * Untracked: a reader does not re-run because it called `get`. For a reactive
31
+ * read use `find`, `has` or `isActive`, which create nothing: a group created
32
+ * inside the derived that reads it is state that derived cannot track, so
33
+ * `$derived(get('brush').activeCount)` before any chart made the group never
34
+ * updates. Create groups from init, effects and handlers.
35
+ */
28
36
  get(name?: string | null): FilterGroup;
29
- /** Check if a named group exists (without creating it). */
30
- has(name: string): boolean;
37
+ /**
38
+ * The named group, or undefined if no chart has made it yet. Creates nothing.
39
+ * Reactive: a reader re-runs when the group is created.
40
+ */
41
+ find(name?: string | null): FilterGroup | undefined;
42
+ /** Whether a named group exists, without creating it. Reactive, as `find`. */
43
+ has(name?: string | null): boolean;
44
+ /**
45
+ * Whether a named group has a selection (a brush, a toggle, a legend), without
46
+ * creating it: false for a group no chart has made yet. Reactive in both the
47
+ * group appearing and its clauses changing, so `$derived(groups.isActive('brush'))`
48
+ * is safe before any chart exists.
49
+ */
50
+ isActive(name?: string | null): boolean;
51
+ /**
52
+ * The names of the groups, in creation order. Reactive: a reader re-runs when a
53
+ * group is created. A new array on each read. For app code that must reach every group the page's marks
54
+ * use, such as a clause that applies to the whole page.
55
+ */
56
+ get names(): readonly string[];
57
+ /** The groups, in the order of `names`. Reactive in the same way. */
58
+ get groups(): readonly FilterGroup[];
31
59
  /** How many groups the page has: the packer writes one activity half per group. */
32
60
  get groupCount(): number;
33
61
  /**
@@ -9,10 +9,18 @@
9
9
  * and one FieldActivity per categorical field, which holds every group's
10
10
  * selection fade on that field.
11
11
  */
12
+ import { untrack } from 'svelte';
13
+ import { SvelteMap } from 'svelte/reactivity';
12
14
  import { FilterGroup } from './FilterGroup.svelte';
13
15
  import { FieldActivity } from './FieldActivity.svelte';
14
16
  export class FilterGroupRegistry {
15
- #groups = new Map();
17
+ /**
18
+ * The groups by name, in creation order. Reactive, so `has`, `find`, `isActive`,
19
+ * `names` and `groups` re-run when a group is created. `get` reads and writes it
20
+ * untracked: templates and deriveds call `get`, and a tracked write there throws
21
+ * (`state_unsafe_mutation`) or re-runs the reader on its own write.
22
+ */
23
+ #groups = new SvelteMap();
16
24
  #engine = null;
17
25
  #fields = new Map();
18
26
  /**
@@ -54,27 +62,66 @@ export class FilterGroupRegistry {
54
62
  this.#loop = loop;
55
63
  this.#chartsFor = chartsFor;
56
64
  }
57
- /** Get or create a FilterGroup by name. Undefined/null → "default". */
65
+ /**
66
+ * Get or create a FilterGroup by name. Undefined/null → "default".
67
+ *
68
+ * Untracked: a reader does not re-run because it called `get`. For a reactive
69
+ * read use `find`, `has` or `isActive`, which create nothing: a group created
70
+ * inside the derived that reads it is state that derived cannot track, so
71
+ * `$derived(get('brush').activeCount)` before any chart made the group never
72
+ * updates. Create groups from init, effects and handlers.
73
+ */
58
74
  get(name) {
59
75
  const key = name ?? 'default';
60
- let g = this.#groups.get(key);
61
- if (!g) {
62
- // Indices are never reused or compacted: a mark keeps its group's
63
- // index from mount, so renumbering would point it at another group.
64
- g = new FilterGroup(this.#groups.size, (field) => this.field(field));
65
- if (this.#engine)
66
- g.setEngine(this.#engine);
67
- this.#groups.set(key, g);
68
- }
69
- return g;
76
+ return untrack(() => {
77
+ let g = this.#groups.get(key);
78
+ if (!g) {
79
+ // Indices are never reused or compacted: a mark keeps its group's
80
+ // index from mount, so renumbering would point it at another group.
81
+ g = new FilterGroup(this.#groups.size, (field) => this.field(field));
82
+ if (this.#engine)
83
+ g.setEngine(this.#engine);
84
+ this.#groups.set(key, g);
85
+ }
86
+ return g;
87
+ });
88
+ }
89
+ /**
90
+ * The named group, or undefined if no chart has made it yet. Creates nothing.
91
+ * Reactive: a reader re-runs when the group is created.
92
+ */
93
+ find(name) {
94
+ return this.#groups.get(name ?? 'default');
70
95
  }
71
- /** Check if a named group exists (without creating it). */
96
+ /** Whether a named group exists, without creating it. Reactive, as `find`. */
72
97
  has(name) {
73
- return this.#groups.has(name);
98
+ return this.#groups.has(name ?? 'default');
99
+ }
100
+ /**
101
+ * Whether a named group has a selection (a brush, a toggle, a legend), without
102
+ * creating it: false for a group no chart has made yet. Reactive in both the
103
+ * group appearing and its clauses changing, so `$derived(groups.isActive('brush'))`
104
+ * is safe before any chart exists.
105
+ */
106
+ isActive(name) {
107
+ const group = this.find(name);
108
+ return group ? group.activeCount > 0 : false;
109
+ }
110
+ /**
111
+ * The names of the groups, in creation order. Reactive: a reader re-runs when a
112
+ * group is created. A new array on each read. For app code that must reach every group the page's marks
113
+ * use, such as a clause that applies to the whole page.
114
+ */
115
+ get names() {
116
+ return [...this.#groups.keys()];
117
+ }
118
+ /** The groups, in the order of `names`. Reactive in the same way. */
119
+ get groups() {
120
+ return [...this.#groups.values()];
74
121
  }
75
122
  /** How many groups the page has: the packer writes one activity half per group. */
76
123
  get groupCount() {
77
- return this.#groups.size;
124
+ return untrack(() => this.#groups.size);
78
125
  }
79
126
  /**
80
127
  * The selection state of a categorical field, by identity key. Created on
@@ -15,15 +15,42 @@ export interface QueryParams {
15
15
  sort?: NormalizedSortConfig;
16
16
  limit?: number;
17
17
  }
18
+ /**
19
+ * Why a result was queried. `catchUp`: the mark came back into view stale (see
20
+ * `active`), so the result brings it up to date with changes it missed, which
21
+ * nobody just made; it lands without animating. `update`: anything else.
22
+ */
23
+ export type ResultCause = 'update' | 'catchUp';
18
24
  export declare class MarkHandle {
19
25
  #private;
20
26
  arrowTable: Table<import("@uwdata/flechette").TypeMap>;
21
27
  dataVersion: number;
28
+ /**
29
+ * The cause of the result `dataVersion` counts, set with it. Plain, not
30
+ * $state: read by whatever reacts to `dataVersion`, never on its own.
31
+ */
32
+ resultCause: ResultCause;
22
33
  error: Error;
23
34
  get params(): QueryParams;
24
35
  /** Called by mark lifecycle when sort/limit change. */
25
36
  updateParams(params: QueryParams): void;
26
37
  _onParamsChange: (() => void) | null;
38
+ /**
39
+ * Whether the mark is on screen (or about to be). The engine does not
40
+ * re-query an inactive mark that has its first result: a filter or params
41
+ * change only marks it stale, and it queries once, with whatever is current,
42
+ * when it becomes active again. Its first query always runs, since the
43
+ * first result can feed domains other charts share. Not reactive: the
44
+ * engine reads it when it would send.
45
+ */
46
+ get active(): boolean;
47
+ /**
48
+ * Returns whether this call sent a catch-up query: the mark came back
49
+ * stale. A return with no query means the data it holds is current, so
50
+ * the caller can act on its return now rather than after a result.
51
+ */
52
+ setActive(active: boolean): boolean;
53
+ _onActivate: (() => boolean) | null;
27
54
  /**
28
55
  * Engine-internal: called to deliver query results.
29
56
  *
@@ -33,7 +60,7 @@ export declare class MarkHandle {
33
60
  * The error is cleared either way, so a success after a failure still
34
61
  * clears it.
35
62
  */
36
- _setResult(table: Table): void;
63
+ _setResult(table: Table, cause?: ResultCause): void;
37
64
  /** Engine-internal: called on query error. */
38
65
  _setError(error: Error): void;
39
66
  /** Clean up registration. Set by engine during registerMark. */
@@ -8,6 +8,11 @@
8
8
  export class MarkHandle {
9
9
  arrowTable = $state(null);
10
10
  dataVersion = $state(0);
11
+ /**
12
+ * The cause of the result `dataVersion` counts, set with it. Plain, not
13
+ * $state: read by whatever reacts to `dataVersion`, never on its own.
14
+ */
15
+ resultCause = 'update';
11
16
  error = $state(null);
12
17
  #params = $state({ sort: undefined, limit: undefined });
13
18
  get params() {
@@ -21,6 +26,32 @@ export class MarkHandle {
21
26
  // Engine sets this callback during registration.
22
27
  // Conventional underscore — engine needs external access.
23
28
  _onParamsChange = null;
29
+ /**
30
+ * Whether the mark is on screen (or about to be). The engine does not
31
+ * re-query an inactive mark that has its first result: a filter or params
32
+ * change only marks it stale, and it queries once, with whatever is current,
33
+ * when it becomes active again. Its first query always runs, since the
34
+ * first result can feed domains other charts share. Not reactive: the
35
+ * engine reads it when it would send.
36
+ */
37
+ get active() {
38
+ return this.#active;
39
+ }
40
+ /**
41
+ * Returns whether this call sent a catch-up query: the mark came back
42
+ * stale. A return with no query means the data it holds is current, so
43
+ * the caller can act on its return now rather than after a result.
44
+ */
45
+ setActive(active) {
46
+ if (active === this.#active)
47
+ return false;
48
+ this.#active = active;
49
+ return active ? (this._onActivate?.() ?? false) : false;
50
+ }
51
+ #active = true;
52
+ // Engine sets this during registration: query a mark that went stale, and
53
+ // say whether it did.
54
+ _onActivate = null;
24
55
  /**
25
56
  * Engine-internal: called to deliver query results.
26
57
  *
@@ -30,10 +61,11 @@ export class MarkHandle {
30
61
  * The error is cleared either way, so a success after a failure still
31
62
  * clears it.
32
63
  */
33
- _setResult(table) {
64
+ _setResult(table, cause = 'update') {
34
65
  this.error = null;
35
66
  if (table === this.arrowTable)
36
67
  return;
68
+ this.resultCause = cause;
37
69
  this.arrowTable = table;
38
70
  this.dataVersion++;
39
71
  }
@@ -20,9 +20,11 @@
20
20
  import { type Table } from '@uwdata/flechette';
21
21
  export type { BrushActivation } from './sufficient-statistics';
22
22
  import { type QueryParts } from '../components/marks/lifecycle/query-builder';
23
+ import { type BuildEvent } from './preagg/Preagg';
23
24
  import type { ActivationSource } from './preagg/plan';
24
25
  export type { ActiveDim, ActivationSource } from './preagg/plan';
25
26
  import { MarkHandle } from './MarkHandle.svelte';
27
+ import { type LaneHold, type SlicedLane } from './sliced-lane';
26
28
  import { type DimValue, type FieldIdentity, type IdentityField, type SyntheticId } from './categorical-identity';
27
29
  import type { FilterGroup } from './FilterGroup.svelte';
28
30
  import { type Clause, type MarkRoute } from './CrossfilterSelection.svelte';
@@ -33,13 +35,141 @@ export interface QueryOpts {
33
35
  callerId?: string;
34
36
  /** AbortSignal for external cancellation. */
35
37
  signal?: AbortSignal;
36
- /** Enable query result caching. Default true. Set false for queries against
37
- * volatile temp tables whose contents change without the SQL changing. */
38
+ /**
39
+ * Result caching, and sharing an identical query already running.
40
+ *
41
+ * - Mark queries (with a `callerId`): on by default; `false` for queries
42
+ * against tables whose contents change without the SQL changing.
43
+ * - Ad-hoc queries (no `callerId`): off unless `true`, so an app re-reading
44
+ * a table it writes to sees its writes. fjader's own setup queries pass
45
+ * `true`: they read base tables, which change only on a data reload, after
46
+ * which `clearQueryCache()` is the contract.
47
+ *
48
+ * The key includes the dims' version (see #cacheKey).
49
+ */
38
50
  cache?: boolean;
51
+ /**
52
+ * Who is waiting for it; see StatementClass. Default 'interaction'.
53
+ *
54
+ * `'background'` runs on another connection when the engine has one (a
55
+ * sliced lane on DuckDB-WASM), so a TEMP table created with `exec` on the
56
+ * main connection is invisible there and the query fails: pass
57
+ * `connectionLocal: true` for statements on connection-local state.
58
+ */
59
+ class?: StatementClass;
60
+ /** Reads or writes connection-local state; see StatementPlacement. */
61
+ connectionLocal?: boolean;
62
+ /**
63
+ * The table this read reads (base table, view or preagg table). An
64
+ * interaction read with one is routed by its source's cost (see
65
+ * SourceRouting): known-fast on the main connection, unknown or slow sliced
66
+ * on that source's lane. Without one it stays on the main connection.
67
+ */
68
+ source?: string;
69
+ /** A mark's data read: its cost updates the source's record. */
70
+ data?: boolean;
71
+ /**
72
+ * This read is known to be slow (a heavy query on whatever table, as a
73
+ * mark with `holdsEntrance={false}` says). The engine reads it where a
74
+ * slow read cannot hold up others (on DuckDB-WASM, sliced on its source's
75
+ * lane) and does not learn the source's cost from it: the source's other
76
+ * reads may be quick. A backend with one connection can ignore it.
77
+ */
78
+ slow?: boolean;
79
+ }
80
+ /**
81
+ * Where a statement may run, beside who waits for it.
82
+ *
83
+ * `connectionLocal`: it reads or writes state that belongs to one connection
84
+ * (a TEMP table, a session setting), so it stays on the main connection
85
+ * whatever its class. fjader's tooltip tables are TEMP: their writes, drops,
86
+ * hover lookups and the fits that read them pass it. Each table is written
87
+ * through one connection only, so statements on it keep their order. A
88
+ * backend with one connection, or truly shared state, can ignore it.
89
+ */
90
+ export interface StatementPlacement {
91
+ class: StatementClass;
92
+ connectionLocal?: boolean;
93
+ /** See QueryOpts.source. */
94
+ source?: string;
95
+ /** See QueryOpts.data. */
96
+ data?: boolean;
97
+ /**
98
+ * When the read was asked for (query() called): for a mark's
99
+ * execute-latest read, the state it reflects. Recorded, for data age.
100
+ */
101
+ requestedAt?: number;
102
+ /** The caller (a mark's id), for the log. */
103
+ callerId?: string;
104
+ /** See QueryOpts.slow. */
105
+ slow?: boolean;
106
+ }
107
+ /**
108
+ * Who is waiting for a statement. Callers state a class, never a connection:
109
+ * which connection (or later, which backend) serves it is the engine's
110
+ * business, decided in one place, `#send`.
111
+ *
112
+ * - `'interaction'`: someone is looking at the result or about to: mark data,
113
+ * hover lookups, a mark's setup (domains, dims). The default.
114
+ * - `'background'`: nobody waits for it directly: preagg builds, tooltip-table
115
+ * writes, cleanup. On DuckDB-WASM it runs on a sliced lane (another
116
+ * connection, see SlicedLane) unless `connectionLocal`, so a slow build no
117
+ * longer holds up interaction. Without a lane, preagg builds yield to
118
+ * interaction statements, not to each other's background work.
119
+ */
120
+ export type StatementClass = 'interaction' | 'background';
121
+ /**
122
+ * For `observeIdentityRequests`: a mark asking for categorical identity
123
+ * columns, or a table's drain starting (when its dims actually begin to build,
124
+ * which is what a chart's first read waits on).
125
+ */
126
+ export interface IdentityRequest {
127
+ /** 'request': a mark's ensureColumns call; 'drain': one pass of a table's drain started. */
128
+ kind: 'request' | 'drain';
129
+ table: string;
130
+ /** For a request, the fields asked for; for a drain pass, the fields it took. */
131
+ fields: string[];
132
+ /** performance.now() when asked. */
133
+ time: number;
134
+ }
135
+ /** One statement as it reached the connection, for `observeStatements`. */
136
+ export interface StatementRecord {
137
+ sql: string;
138
+ /** How it was sent: a query returning rows, a statement, a DESCRIBE, an Arrow insert. */
139
+ kind: 'query' | 'exec' | 'describe' | 'insert' | 'batch';
140
+ /** For a batch (execBatch): how many statements it ran as one message. */
141
+ count?: number;
142
+ class: StatementClass;
143
+ /** Which connection ran it: the main one, or a sliced lane. */
144
+ connection: 'main' | 'lane';
145
+ /** The source it was routed by, when it named one. */
146
+ source?: string;
147
+ /**
148
+ * When the read was asked for (StatementPlacement.requestedAt). With
149
+ * `end`, how old the state a result reflects is when it lands.
150
+ */
151
+ requested?: number;
152
+ /** Who asked (a mark's id), when it said. */
153
+ caller?: string;
154
+ /** performance.now() when handed to the connection, and when it settled. */
155
+ start: number;
156
+ end: number;
157
+ error?: unknown;
39
158
  }
40
159
  export interface QueryEngineOptions {
41
160
  /** Overrides BUILD_MAX_WAIT_MS; tests use it to make the queue deterministic. */
42
161
  buildMaxWaitMs?: number;
162
+ /**
163
+ * Debug. Send an execBatch's statements one by one, for per-statement
164
+ * timing. It changes what it measures: each statement then re-enters the
165
+ * queue behind whatever was sent meanwhile, which a batch avoids.
166
+ */
167
+ unbatch?: boolean;
168
+ /**
169
+ * Advanced. While the main connection works, all sliced lanes together get
170
+ * one slice per this many ms (see SLICE_SHARE_MS, the default).
171
+ */
172
+ sliceShareMs?: number;
43
173
  }
44
174
  export interface ColumnInfo {
45
175
  name: string;
@@ -58,11 +188,15 @@ export declare function buildSchemaMap(columns: ColumnInfo[]): Map<string, Colum
58
188
  /**
59
189
  * Minimal connection interface.
60
190
  * Supports two query paths:
61
- * - queryIPC (fast): returns raw Arrow IPC bytes, decoded by flechette (zero-copy)
191
+ * - queryIPC (fast): returns raw Arrow IPC bytes, decoded by flechette into typed arrays
62
192
  * - query (fallback): returns a Table directly (for mocks/testing)
63
193
  */
64
194
  export interface DuckDBConnection {
65
- /** Fast path: raw Arrow IPC bytes → flechette tableFromIPC → zero-copy typed arrays. */
195
+ /**
196
+ * Fast path: raw Arrow IPC bytes → flechette tableFromIPC → typed arrays. Views on
197
+ * the bytes within one record batch (~2k rows from DuckDB-WASM); past that,
198
+ * flechette concatenates each column once.
199
+ */
66
200
  queryIPC?(sql: string): Promise<Uint8Array>;
67
201
  /** Fallback query path. Used when queryIPC is not available (mocks, testing). */
68
202
  query(sql: string): Promise<Table>;
@@ -75,6 +209,17 @@ export interface DuckDBConnection {
75
209
  name: string;
76
210
  create: boolean;
77
211
  }): Promise<void>;
212
+ /**
213
+ * Open another connection on the same database whose statements run
214
+ * sliced (see SlicedLane): a slow read there no longer blocks this
215
+ * connection's queries. DuckDB-WASM via `connectionFrom`; a connection
216
+ * without it keeps everything on itself. The engine opens lanes itself and
217
+ * sends through them from `#send`, so every statement keeps its record,
218
+ * class and accounting; nothing hands a raw lane to an app.
219
+ */
220
+ openSlicedLane?(options?: {
221
+ hold?: LaneHold;
222
+ }): Promise<SlicedLane>;
78
223
  }
79
224
  export interface QueryDescriptor {
80
225
  /** The table the mark queries: its identity view when it has categorical channels. */
@@ -86,7 +231,29 @@ export interface QueryDescriptor {
86
231
  transformCompiler: TransformCompiler;
87
232
  /** What routing needs to know about the mark. Absent in tests that route nothing. */
88
233
  route?: MarkRoute;
234
+ /**
235
+ * The mark is known to be slow (`holdsEntrance={false}`): its data reads
236
+ * go sliced from the first, whatever its source's cost. The mark, not its
237
+ * source: other marks on a fast table keep reading on the main connection.
238
+ */
239
+ slow?: boolean;
89
240
  }
241
+ /**
242
+ * How a mark's data read is placed. Routed by the table it reads
243
+ * (SourceRouting): the preagg table when one answers, else the mark's BASE
244
+ * table (its identity view reads the same rows, and its range and stats reads
245
+ * name the base table: one record per source). `slow` (holdsEntrance={false})
246
+ * applies to the base query only: a preagg read is ~1 ms, and forced onto a
247
+ * lane during a brush it would cost ~20 ms of round trips in exactly the case
248
+ * preagg exists to make fast.
249
+ */
250
+ export declare function markReadOptions(markId: string, descriptor: Pick<QueryDescriptor, 'baseTable' | 'slow'>, preaggTable: string | null): QueryOpts;
251
+ /**
252
+ * DuckDB's catalog error for a table or view this connection cannot see.
253
+ * Matched by its wording, which an upgrade could change: the integration
254
+ * test missing-relation.integration.test.ts checks it against real DuckDB.
255
+ */
256
+ export declare function isMissingRelation(e: unknown): boolean;
90
257
  export declare class QueryEngine {
91
258
  #private;
92
259
  constructor(connection: DuckDBConnection | Promise<DuckDBConnection>, options?: QueryEngineOptions);
@@ -178,6 +345,29 @@ export declare class QueryEngine {
178
345
  * dropped state still invalidates every id handed out before it.
179
346
  */
180
347
  onIdentityReset(listener: () => void): () => void;
348
+ /**
349
+ * Debug: every categorical identity request (a mark's ensureColumns call),
350
+ * with its table, fields and time, before deduplication, and every drain
351
+ * start (a table's dims beginning to build, with the fields it found
352
+ * pending). Returns the unsubscribe; costs nothing while no one listens, and
353
+ * a listener that throws is logged without failing identity work.
354
+ */
355
+ observeIdentityRequests(listener: (request: IdentityRequest) => void): () => void;
356
+ /**
357
+ * Debug: every step of every preagg build (BuildEvent): planned or
358
+ * skipped (with the size guard's numbers), queued (with its hover batch),
359
+ * started, then built, failed or dropped (with the check that failed and
360
+ * the values it compared). Returns the unsubscribe; costs nothing while no
361
+ * one listens, and a listener that throws is logged without failing the
362
+ * build.
363
+ */
364
+ observeBuilds(listener: (event: BuildEvent) => void): () => void;
365
+ /**
366
+ * Listen to every statement sent to the connection, as it settles. For
367
+ * debugging and measurement; returns the unsubscribe. Costs nothing while
368
+ * no one listens.
369
+ */
370
+ observeStatements(listener: (record: StatementRecord) => void): () => void;
181
371
  /**
182
372
  * Execute a query and return the Arrow result table.
183
373
  * Accepts a SQL string or any object with toString() (e.g. mosaic-sql Query).
@@ -190,17 +380,47 @@ export declare class QueryEngine {
190
380
  query(sql: string | {
191
381
  toString(): string;
192
382
  }, opts?: QueryOpts): Promise<Table>;
383
+ /**
384
+ * Run several statements as ONE message on the main connection, returning
385
+ * the last statement's rows. Each statement commits on its own and
386
+ * execution stops at the first failure, as when sent one by one (measured
387
+ * on DuckDB-WASM, /test/query-slicing §13); the error does not say which
388
+ * statement failed.
389
+ *
390
+ * For statements on tables the main connection owns (fjader's identity
391
+ * dims, present tables, views): one connection per table. Deliberately no
392
+ * class, source or routing options: it always runs on the main connection
393
+ * as interaction (so lanes hold their slices while it runs). What it saves
394
+ * is queue entries: a dependent chain sent one by one goes to the back of
395
+ * the queue at every step.
396
+ *
397
+ * Logged as one record, kind 'batch', with the joined text and `count`.
398
+ * Without `queryIPC` (mocks, custom connections), or with
399
+ * `QueryEngineOptions.unbatch`, the statements go one by one.
400
+ */
401
+ execBatch(statements: string[]): Promise<Table>;
193
402
  /**
194
403
  * Execute DDL/DML statements (CREATE TABLE, DROP TABLE, INSERT, etc.).
195
404
  * Accepts a single SQL string or an array of statements to execute
196
405
  * sequentially.
197
406
  */
198
- exec(sql: string | string[]): Promise<void>;
407
+ exec(sql: string | string[], opts?: {
408
+ class?: StatementClass;
409
+ connectionLocal?: boolean;
410
+ signal?: AbortSignal;
411
+ }): Promise<void>;
199
412
  /**
200
413
  * Get column info for a table via DESCRIBE.
201
414
  * Results are cached — same table returns the same promise.
202
415
  */
203
416
  describe(table: string): Promise<ColumnInfo[]>;
417
+ /**
418
+ * Tell the engine how a source reads, overriding its prior: `'slow'` for a
419
+ * table that is slow to query (it reads sliced, so it never holds up
420
+ * other charts), `'fast'` for a view that is cheap (it reads on the main
421
+ * connection from its first query). A measured read still corrects it.
422
+ */
423
+ sourceCost(source: string, cost: 'fast' | 'slow'): void;
204
424
  /** Clear cached DESCRIBE results. Pass a table name to clear one, or omit for all. */
205
425
  clearDescribeCache(table?: string): void;
206
426
  /**