@fjader/svelte 0.2.2 → 0.2.4

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 (148) hide show
  1. package/README.md +2 -0
  2. package/dist/components/axes/AxisBottom.svelte +23 -6
  3. package/dist/components/axes/AxisBottom.svelte.d.ts +10 -0
  4. package/dist/components/axes/AxisLeft.svelte +77 -8
  5. package/dist/components/axes/AxisLeft.svelte.d.ts +17 -0
  6. package/dist/components/axes/BandAxisTicks.svelte +64 -25
  7. package/dist/components/axes/BandAxisTicks.svelte.d.ts +5 -5
  8. package/dist/components/axes/axis-title.d.ts +30 -0
  9. package/dist/components/axes/axis-title.js +77 -0
  10. package/dist/components/charts/ChartCell.svelte +1 -1
  11. package/dist/components/charts/ChartContainer.svelte +12 -3
  12. package/dist/components/charts/ChartContainer.svelte.d.ts +7 -0
  13. package/dist/components/guides/legend/ContinuousColorLegend.svelte +9 -2
  14. package/dist/components/guides/legend/OrdinalColorLegend.svelte +96 -35
  15. package/dist/components/marks/AreaMarkX.svelte +21 -6
  16. package/dist/components/marks/AreaMarkY.svelte +21 -6
  17. package/dist/components/marks/BarMarkX.svelte +16 -1
  18. package/dist/components/marks/BarMarkY.svelte +16 -1
  19. package/dist/components/marks/DotsMark.svelte +5 -0
  20. package/dist/components/marks/LineMark.svelte +8 -1
  21. package/dist/components/marks/ParallelCoordinatesMark.svelte +10 -0
  22. package/dist/components/marks/ParallelCoordinatesMark.svelte.d.ts +5 -0
  23. package/dist/components/marks/RectMark.svelte +5 -0
  24. package/dist/components/marks/RectXMark.svelte +5 -0
  25. package/dist/components/marks/RectYMark.svelte +5 -0
  26. package/dist/components/marks/RuleMarkX.svelte +12 -2
  27. package/dist/components/marks/RuleMarkY.svelte +12 -2
  28. package/dist/components/marks/TextMark.svelte +76 -13
  29. package/dist/components/marks/lifecycle/MarkLifecycleV2.svelte.d.ts +21 -2
  30. package/dist/components/marks/lifecycle/MarkLifecycleV2.svelte.js +300 -42
  31. package/dist/components/marks/lifecycle/domain-queries.d.ts +3 -1
  32. package/dist/components/marks/lifecycle/domain-queries.js +22 -15
  33. package/dist/components/marks/lifecycle/domain-registration.js +2 -6
  34. package/dist/components/marks/lifecycle/other-rows.svelte.d.ts +104 -0
  35. package/dist/components/marks/lifecycle/other-rows.svelte.js +193 -0
  36. package/dist/components/marks/lifecycle/other-split.d.ts +80 -0
  37. package/dist/components/marks/lifecycle/other-split.js +112 -0
  38. package/dist/components/marks/lifecycle/query-builder.d.ts +26 -1
  39. package/dist/components/marks/lifecycle/query-builder.js +25 -5
  40. package/dist/components/marks/lifecycle/transform-scale-key.d.ts +16 -0
  41. package/dist/components/marks/lifecycle/transform-scale-key.js +27 -0
  42. package/dist/components/marks/utils/area-outline.d.ts +24 -0
  43. package/dist/components/marks/utils/area-outline.js +10 -0
  44. package/dist/components/marks/utils/reactive-config.d.ts +1 -1
  45. package/dist/components/marks/utils/reactive-config.js +9 -0
  46. package/dist/components/marks/utils/tooltip-helpers.d.ts +4 -1
  47. package/dist/components/marks/utils/tooltip-helpers.js +2 -1
  48. package/dist/components/rendering/Areas.svelte +30 -6
  49. package/dist/components/rendering/Areas.svelte.d.ts +2 -0
  50. package/dist/components/rendering/BaseGPURenderer.svelte.d.ts +3 -1
  51. package/dist/components/rendering/BaseGPURenderer.svelte.js +8 -3
  52. package/dist/components/rendering/Dots.svelte +24 -3
  53. package/dist/components/rendering/Dots.svelte.d.ts +2 -0
  54. package/dist/components/rendering/GridLine.svelte +2 -1
  55. package/dist/components/rendering/Lines.svelte +49 -7
  56. package/dist/components/rendering/Lines.svelte.d.ts +9 -0
  57. package/dist/components/rendering/ParallelLines.svelte +26 -7
  58. package/dist/components/rendering/ParallelLines.svelte.d.ts +2 -0
  59. package/dist/components/rendering/Rects.svelte +129 -20
  60. package/dist/components/rendering/Rects.svelte.d.ts +9 -0
  61. package/dist/components/rendering/RuleX.svelte +20 -5
  62. package/dist/components/rendering/RuleX.svelte.d.ts +2 -0
  63. package/dist/components/rendering/RuleY.svelte +20 -5
  64. package/dist/components/rendering/RuleY.svelte.d.ts +2 -0
  65. package/dist/components/rendering/activity-reads.d.ts +111 -0
  66. package/dist/components/rendering/activity-reads.js +154 -0
  67. package/dist/components/rendering/uniform-sizes.d.ts +26 -0
  68. package/dist/components/rendering/uniform-sizes.js +28 -0
  69. package/dist/config/ChartConfig.svelte.d.ts +5 -1
  70. package/dist/gpu/ColorScaleShaderLib.d.ts +13 -1
  71. package/dist/gpu/ColorScaleShaderLib.js +56 -5
  72. package/dist/gpu/GPUCoordinator.svelte.d.ts +8 -0
  73. package/dist/gpu/GPUCoordinator.svelte.js +68 -32
  74. package/dist/gpu/MorphedChannels.d.ts +6 -32
  75. package/dist/gpu/MorphedChannels.js +45 -3
  76. package/dist/gpu/ResolveShader.d.ts +2 -2
  77. package/dist/gpu/ResolveShader.js +2 -2
  78. package/dist/gpu/ScaleTemplateSystem.d.ts +19 -0
  79. package/dist/gpu/ScaleTemplateSystem.js +111 -11
  80. package/dist/gpu/SizeScaleShaderLib.d.ts +4 -4
  81. package/dist/gpu/SizeScaleShaderLib.js +4 -4
  82. package/dist/gpu/identity-slot-packing.d.ts +157 -40
  83. package/dist/gpu/identity-slot-packing.js +262 -62
  84. package/dist/gpu/tracked-scales.d.ts +14 -0
  85. package/dist/gpu/tracked-scales.js +18 -0
  86. package/dist/index.d.ts +2 -0
  87. package/dist/index.js +4 -0
  88. package/dist/interactors/BandLabelToggle.svelte +5 -9
  89. package/dist/interactors/BandLabelToggle.svelte.d.ts +3 -3
  90. package/dist/interactors/DerivedInterval1D.svelte +26 -54
  91. package/dist/interactors/Interval1D.svelte +8 -2
  92. package/dist/interactors/Interval2D.svelte +8 -2
  93. package/dist/interactors/ParallelAxisSource.svelte +14 -5
  94. package/dist/interactors/ParallelAxisSource.svelte.d.ts +2 -1
  95. package/dist/interactors/ParallelBrushes.svelte +8 -9
  96. package/dist/interactors/ParallelBrushes.svelte.d.ts +3 -3
  97. package/dist/interactors/Toggle.svelte +48 -38
  98. package/dist/interactors/Toggle.svelte.d.ts +4 -3
  99. package/dist/interactors/Tooltip.svelte +140 -42
  100. package/dist/interactors/TooltipManager.svelte.d.ts +20 -5
  101. package/dist/interactors/TooltipManager.svelte.js +40 -10
  102. package/dist/interactors/other-tooltip.d.ts +16 -0
  103. package/dist/interactors/other-tooltip.js +22 -0
  104. package/dist/interactors/relay-clauses.d.ts +23 -0
  105. package/dist/interactors/relay-clauses.js +60 -0
  106. package/dist/query-engine/CrossfilterSelection.svelte.d.ts +88 -15
  107. package/dist/query-engine/CrossfilterSelection.svelte.js +94 -20
  108. package/dist/query-engine/FieldActivity.svelte.d.ts +128 -0
  109. package/dist/query-engine/FieldActivity.svelte.js +378 -0
  110. package/dist/query-engine/FilterGroup.svelte.d.ts +76 -30
  111. package/dist/query-engine/FilterGroup.svelte.js +223 -124
  112. package/dist/query-engine/FilterGroupRegistry.svelte.d.ts +73 -3
  113. package/dist/query-engine/FilterGroupRegistry.svelte.js +161 -12
  114. package/dist/query-engine/MarkHandle.svelte.d.ts +9 -1
  115. package/dist/query-engine/MarkHandle.svelte.js +12 -2
  116. package/dist/query-engine/QueryEngine.d.ts +52 -16
  117. package/dist/query-engine/QueryEngine.js +116 -34
  118. package/dist/query-engine/categorical-identity.d.ts +114 -27
  119. package/dist/query-engine/categorical-identity.js +225 -51
  120. package/dist/query-engine/index.d.ts +2 -1
  121. package/dist/query-engine/index.js +1 -0
  122. package/dist/query-engine/preagg/Preagg.d.ts +1 -1
  123. package/dist/query-engine/preagg/Preagg.js +11 -9
  124. package/dist/query-engine/preagg/plan.d.ts +14 -5
  125. package/dist/query-engine/preagg/plan.js +44 -18
  126. package/dist/query-engine/toggle-predicate.d.ts +21 -0
  127. package/dist/query-engine/toggle-predicate.js +41 -2
  128. package/dist/scales/ActivityTable.d.ts +103 -0
  129. package/dist/scales/ActivityTable.js +220 -0
  130. package/dist/scales/BandScaleState.svelte.d.ts +41 -15
  131. package/dist/scales/BandScaleState.svelte.js +78 -36
  132. package/dist/scales/ChartScales.svelte.d.ts +2 -2
  133. package/dist/scales/ColorScaleState.svelte.d.ts +36 -8
  134. package/dist/scales/ColorScaleState.svelte.js +118 -24
  135. package/dist/scales/DomainResolver.d.ts +2 -0
  136. package/dist/scales/DomainResolver.js +15 -8
  137. package/dist/scales/ScaleRegistry.svelte.d.ts +9 -0
  138. package/dist/scales/ScaleRegistry.svelte.js +22 -1
  139. package/dist/scales/TimeScaleState.svelte.js +7 -1
  140. package/dist/spec/index.js +1 -0
  141. package/dist/spec/types.d.ts +4 -0
  142. package/dist/theme.css +5 -4
  143. package/dist/transforms/TransformCompiler.d.ts +37 -2
  144. package/dist/transforms/TransformCompiler.js +44 -1
  145. package/dist/transforms/index.d.ts +27 -0
  146. package/dist/transforms/index.js +127 -19
  147. package/dist/types.d.ts +71 -0
  148. package/package.json +3 -2
@@ -4,11 +4,48 @@
4
4
  * Marks and interactors specify which group to use via their `selection`
5
5
  * prop (e.g. selection="brush"). Groups are created on first access.
6
6
  * Routes that don't use named selections get the "default" group.
7
+ *
8
+ * Also owns what is shared between the groups: each group's page-wide index,
9
+ * and one FieldActivity per categorical field, which holds every group's
10
+ * selection fade on that field.
7
11
  */
12
+ import { untrack } from 'svelte';
13
+ import { SvelteMap } from 'svelte/reactivity';
8
14
  import { FilterGroup } from './FilterGroup.svelte';
15
+ import { FieldActivity } from './FieldActivity.svelte';
9
16
  export class FilterGroupRegistry {
10
- #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();
11
24
  #engine = null;
25
+ #fields = new Map();
26
+ /**
27
+ * Bumped when a field is created, so a reader that found none (and so read
28
+ * no fade) runs again once it exists. Fields are created by selections and by
29
+ * the coordinator for every field a scale draws, never while a template
30
+ * renders.
31
+ */
32
+ #fieldsVersion = $state(0);
33
+ /** The same activities as an array, for the packer's per-frame loop. */
34
+ #fieldList = [];
35
+ #loop = null;
36
+ #chartsFor = (_key, out) => out;
37
+ /** Shared by every FieldActivity: reads the registry's current wiring. */
38
+ #host;
39
+ constructor() {
40
+ const registry = this;
41
+ this.#host = {
42
+ get loop() {
43
+ return registry.#loop;
44
+ },
45
+ chartsFor: (key, out) => this.#chartsFor(key, out),
46
+ identityOf: (key) => this.#engine?.peekIdentity(key) ?? null
47
+ };
48
+ }
12
49
  /** Wire the engine to all existing and future groups. */
13
50
  setEngine(engine) {
14
51
  this.#engine = engine;
@@ -16,20 +53,132 @@ export class FilterGroupRegistry {
16
53
  group.setEngine(engine);
17
54
  }
18
55
  }
19
- /** Get or create a FilterGroup by name. Undefined/null → "default". */
56
+ /**
57
+ * Wire the fades to the animation loop and to the charts to redraw while
58
+ * they run. Called by the GPU coordinator. A fade created before this lands
59
+ * at once.
60
+ */
61
+ setAnimation(loop, chartsFor) {
62
+ this.#loop = loop;
63
+ this.#chartsFor = chartsFor;
64
+ }
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
+ */
20
74
  get(name) {
21
75
  const key = name ?? 'default';
22
- let g = this.#groups.get(key);
23
- if (!g) {
24
- g = new FilterGroup();
25
- if (this.#engine)
26
- g.setEngine(this.#engine);
27
- this.#groups.set(key, g);
28
- }
29
- 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
+ });
30
88
  }
31
- /** Check if a named group exists (without creating it). */
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');
95
+ }
96
+ /** Whether a named group exists, without creating it. Reactive, as `find`. */
32
97
  has(name) {
33
- 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()];
121
+ }
122
+ /** How many groups the page has: the packer writes one activity half per group. */
123
+ get groupCount() {
124
+ return untrack(() => this.#groups.size);
125
+ }
126
+ /**
127
+ * The selection state of a categorical field, by identity key. Created on
128
+ * first ask, by a selection or by the coordinator for a field a scale draws.
129
+ *
130
+ * Never removed: a session that switches through many categorical columns
131
+ * keeps every field it drew, each one step of the coordinator's per-frame
132
+ * reconcile loop. Their buffer space is freed (the packer gives a block only
133
+ * to fields a scale draws), so this costs a loop step, not memory on the GPU.
134
+ * Dropping a field once no scale draws it and no group selects on it is a
135
+ * possible follow-up.
136
+ */
137
+ field(key) {
138
+ let activity = this.#fields.get(key);
139
+ if (!activity) {
140
+ activity = new FieldActivity(key, this.#host);
141
+ this.#fields.set(key, activity);
142
+ this.#fieldList.push(activity);
143
+ this.#fieldsVersion++;
144
+ }
145
+ return activity;
146
+ }
147
+ /**
148
+ * How visible `value` of field `key` is now, for a label shown for marks of
149
+ * several groups: the least visible over `groups`, so it dims when any of
150
+ * those groups dims it. 1 for a field no group ever selected on. Pure, so
151
+ * safe in a template: it creates nothing and reconciles nothing.
152
+ */
153
+ opacityOfValue(key, groups, value) {
154
+ void this.#fieldsVersion;
155
+ const activity = this.#fields.get(key);
156
+ if (!activity)
157
+ return 1;
158
+ let opacity = 1;
159
+ for (let i = 0; i < groups.length; i++) {
160
+ opacity = Math.min(opacity, activity.opacityOfValue(groups[i], value));
161
+ }
162
+ return opacity;
163
+ }
164
+ /**
165
+ * How visible the category with dim id `id` of field `key` is now: the id form
166
+ * of `opacityOfValue`, and the one a label for an Other bucket must use. The
167
+ * least visible over `groups`. Pure, so safe in a template.
168
+ */
169
+ opacityOfId(key, groups, id) {
170
+ void this.#fieldsVersion;
171
+ const activity = this.#fields.get(key);
172
+ if (!activity)
173
+ return 1;
174
+ let opacity = 1;
175
+ for (let i = 0; i < groups.length; i++) {
176
+ opacity = Math.min(opacity, activity.opacityOfId(groups[i], id));
177
+ }
178
+ return opacity;
179
+ }
180
+ /** Every field's activity, in creation order. Read every frame; do not mutate. */
181
+ get fields() {
182
+ return this.#fieldList;
34
183
  }
35
184
  }
@@ -24,7 +24,15 @@ export declare class MarkHandle {
24
24
  /** Called by mark lifecycle when sort/limit change. */
25
25
  updateParams(params: QueryParams): void;
26
26
  _onParamsChange: (() => void) | null;
27
- /** Engine-internal: called to deliver query results. */
27
+ /**
28
+ * Engine-internal: called to deliver query results.
29
+ *
30
+ * The table it already holds changes nothing, so it bumps nothing: a cache
31
+ * hit hands back the same object, and bumping `dataVersion` for it reran
32
+ * every effect that reads it (the tooltip table, data-following domains).
33
+ * The error is cleared either way, so a success after a failure still
34
+ * clears it.
35
+ */
28
36
  _setResult(table: Table): void;
29
37
  /** Engine-internal: called on query error. */
30
38
  _setError(error: Error): void;
@@ -21,11 +21,21 @@ export class MarkHandle {
21
21
  // Engine sets this callback during registration.
22
22
  // Conventional underscore — engine needs external access.
23
23
  _onParamsChange = null;
24
- /** Engine-internal: called to deliver query results. */
24
+ /**
25
+ * Engine-internal: called to deliver query results.
26
+ *
27
+ * The table it already holds changes nothing, so it bumps nothing: a cache
28
+ * hit hands back the same object, and bumping `dataVersion` for it reran
29
+ * every effect that reads it (the tooltip table, data-following domains).
30
+ * The error is cleared either way, so a success after a failure still
31
+ * clears it.
32
+ */
25
33
  _setResult(table) {
34
+ this.error = null;
35
+ if (table === this.arrowTable)
36
+ return;
26
37
  this.arrowTable = table;
27
38
  this.dataVersion++;
28
- this.error = null;
29
39
  }
30
40
  /** Engine-internal: called on query error. */
31
41
  _setError(error) {
@@ -3,8 +3,9 @@
3
3
  * mark registration for batch filter-driven re-queries.
4
4
  *
5
5
  * The mark layer's only interface to DuckDB. All query execution goes through
6
- * queryEngine.query(sql, opts) for ad-hoc queries, or registerMark() +
7
- * onFilterChange() for the reactive registration path.
6
+ * queryEngine.query(sql, opts) for ad-hoc queries, or registerMark() for the
7
+ * reactive registration path: FilterGroup calls onClauseChange() when one
8
+ * clause changes, and onFilterChange() when all of a group's do (clearAll).
8
9
  *
9
10
  * v1: Direct execution (await connection.query(sql))
10
11
  * v2: Cancel-on-supersede — keyed on callerId, drops stale in-flight queries.
@@ -22,8 +23,9 @@ import { type QueryParts } from '../components/marks/lifecycle/query-builder';
22
23
  import type { ActivationSource } from './preagg/plan';
23
24
  export type { ActiveDim, ActivationSource } from './preagg/plan';
24
25
  import { MarkHandle } from './MarkHandle.svelte';
25
- import { type DimValue, type FieldIdentity, type IdentityField } from './categorical-identity';
26
+ import { type DimValue, type FieldIdentity, type IdentityField, type SyntheticId } from './categorical-identity';
26
27
  import type { FilterGroup } from './FilterGroup.svelte';
28
+ import { type Clause, type MarkRoute } from './CrossfilterSelection.svelte';
27
29
  import type { TransformCompiler } from '../transforms/TransformCompiler';
28
30
  export interface QueryOpts {
29
31
  /** Caller identifier for cancel-on-supersede. When a new query arrives
@@ -82,6 +84,8 @@ export interface QueryDescriptor {
82
84
  parts: QueryParts;
83
85
  schema: Map<string, ColumnSchema>;
84
86
  transformCompiler: TransformCompiler;
87
+ /** What routing needs to know about the mark. Absent in tests that route nothing. */
88
+ route?: MarkRoute;
85
89
  }
86
90
  export declare class QueryEngine {
87
91
  #private;
@@ -127,27 +131,40 @@ export declare class QueryEngine {
127
131
  */
128
132
  dimTableFor(field: string): string;
129
133
  /**
130
- * Dim values for `field`, indexed by id — the decode side of the string -> id
131
- * encode. Not display labels: formatting belongs to the scale/axis layer.
134
+ * The field's categories as of now, in id order, without the synthetic Other
135
+ * buckets: a bucket is not a category of the data, and another chart's bucket
136
+ * must not appear on an axis, a facet or a colour legend that has none.
132
137
  *
133
- * The LIVE array — hold the reference, do not snapshot it. A second table
134
- * registering the same field appends to the dim afterwards and this array
135
- * grows in place.
138
+ * With `table`, only the categories that table holds: the dim spans every
139
+ * table registering the field, and an axis over one table must not show a
140
+ * slot for a category only another has. Without it (a scale shared per field,
141
+ * like ordinal colour), every category of the dim.
142
+ *
143
+ * A snapshot, not the live array: for decoding ids, hold `identityFor(field)`.
144
+ */
145
+ categoryValues(field: string, table?: string): DimValue[];
146
+ /**
147
+ * The table of ids `table` holds for `field`, for `dense()` to pad from, or
148
+ * null before the table has folded into the field's dim.
136
149
  */
137
- identityValues(field: string): DimValue[];
150
+ presentTableFor(field: string, table: string): string | null;
138
151
  /**
139
152
  * The field's id <-> value bijection. One live object owning both
140
153
  * directions, so a holder cannot end up with a value array and a reverse map
141
- * that disagree. Prefer this over identityValues() anywhere the reverse
154
+ * that disagree. Prefer this over categoryValues() anywhere the reverse
142
155
  * direction is needed; do not rebuild it locally.
143
156
  */
144
157
  identityFor(field: string): FieldIdentity;
158
+ /** The field's current bijection, or null; never creates one (see CategoricalIdentity). */
159
+ peekIdentity(field: string): FieldIdentity | null;
145
160
  /**
146
- * Reserve an id for a value present in no table — the synthetic "Other"
147
- * bucket from `topN({ includeOthers })`. Call after ensureColumns.
148
- * Returns an existing id when a real category already carries the label.
161
+ * Acquire an id for a value present in no table — the synthetic "Other"
162
+ * bucket from `topN({ includeOthers })` — with the label it displays. Call
163
+ * after ensureColumns; release it when its holder goes.
149
164
  */
150
- reserveSyntheticIdentity(field: string, label: string): Promise<number>;
165
+ acquireSyntheticIdentity(field: string, label: string): Promise<SyntheticId>;
166
+ /** Give back an id from `acquireSyntheticIdentity`, for the next acquire to reuse. */
167
+ releaseSyntheticIdentity(synthetic: SyntheticId): void;
151
168
  /**
152
169
  * Subscribe to identity resets. Returns an unsubscribe function.
153
170
  *
@@ -234,10 +251,29 @@ export declare class QueryEngine {
234
251
  /** When a filter last changed, in any group, for settling (performance.now()). */
235
252
  get lastFilterChangeAt(): number;
236
253
  /**
237
- * Called by FilterGroup when any filter clause changes.
238
- * Iterates registered marks for this group and batch-queries them.
254
+ * Called by FilterGroup when its clauses change wholesale (clearAll):
255
+ * re-queries every mark in the group.
239
256
  */
240
257
  onFilterChange(group: FilterGroup): void;
258
+ /**
259
+ * Called by FilterGroup when the clause of `sourceId` changes: `prev` is the
260
+ * clause it replaced or removed, `next` the one now in force (null when
261
+ * removed).
262
+ *
263
+ * Re-queries only the group's marks that either one filters. A mark neither
264
+ * filters has the same SQL as before, so querying it would only assemble that
265
+ * SQL to find the cached result, on every brush frame. A mark that reads the
266
+ * clause's categorical field dims by it instead, and a mark in a chart that
267
+ * skips the source is not filtered by it: neither is re-queried.
268
+ */
269
+ onClauseChange(group: FilterGroup, sourceId: string, prev: Clause | undefined, next: Clause | null): void;
270
+ /**
271
+ * Called by FilterGroup when a registration that skips `chart` comes or
272
+ * goes while its source has a clause: re-queries the group's marks in that
273
+ * chart, whose filters may have changed. A mark whose SQL did not change
274
+ * stops at the cache and the same-Table guard.
275
+ */
276
+ onChartRouteChange(group: FilterGroup, chart: object): void;
241
277
  /**
242
278
  * Pre-aggregate marks for a filter group before a source is used: builds a
243
279
  * preagg table per mark the source filters (see Preagg.activate). Called
@@ -3,8 +3,9 @@
3
3
  * mark registration for batch filter-driven re-queries.
4
4
  *
5
5
  * The mark layer's only interface to DuckDB. All query execution goes through
6
- * queryEngine.query(sql, opts) for ad-hoc queries, or registerMark() +
7
- * onFilterChange() for the reactive registration path.
6
+ * queryEngine.query(sql, opts) for ad-hoc queries, or registerMark() for the
7
+ * reactive registration path: FilterGroup calls onClauseChange() when one
8
+ * clause changes, and onFilterChange() when all of a group's do (clearAll).
8
9
  *
9
10
  * v1: Direct execution (await connection.query(sql))
10
11
  * v2: Cancel-on-supersede — keyed on callerId, drops stale in-flight queries.
@@ -21,6 +22,7 @@ import { assembleQuery, castNumeric, splitWindowSelect } from '../components/mar
21
22
  import { Preagg } from './preagg/Preagg';
22
23
  import { MarkHandle } from './MarkHandle.svelte';
23
24
  import { CategoricalIdentity } from './categorical-identity';
25
+ import { NO_ROUTE } from './CrossfilterSelection.svelte';
24
26
  /**
25
27
  * How long a queued build waits behind mark queries before it is sent anyway,
26
28
  * by default. During a drag on the base table a mark query is always waiting
@@ -95,7 +97,15 @@ export class QueryEngine {
95
97
  #preagg;
96
98
  /** When a filter last changed, in any group (performance.now()). */
97
99
  #lastFilterChangeAt = 0;
98
- /** LRU query cache: SQL string → Arrow Table (or Promise<Table> while inflight). */
100
+ /**
101
+ * LRU query cache: Arrow Table (or Promise<Table> while inflight), keyed by
102
+ * `#cacheKey`, the SQL text and the dims' version. A mark's query reads dim
103
+ * tables (dense padding pads every category the dim holds), and dims grow
104
+ * after the first queries run: a table folding in, a topN mark reserving its
105
+ * Other. The text does not change when they do, so keyed by text alone the
106
+ * cache served a result from before, with fewer rows: the row set changed
107
+ * between updates and the bars snapped instead of morphing.
108
+ */
99
109
  #queryCache = lruCache({ max: 1000, ttl: 3 * 60 * 60 * 1000 });
100
110
  /**
101
111
  * Per-instance suffix for every table and view this engine creates.
@@ -197,32 +207,64 @@ export class QueryEngine {
197
207
  return this.#identity.dimTable(field);
198
208
  }
199
209
  /**
200
- * Dim values for `field`, indexed by id — the decode side of the string -> id
201
- * encode. Not display labels: formatting belongs to the scale/axis layer.
210
+ * The field's categories as of now, in id order, without the synthetic Other
211
+ * buckets: a bucket is not a category of the data, and another chart's bucket
212
+ * must not appear on an axis, a facet or a colour legend that has none.
202
213
  *
203
- * The LIVE array — hold the reference, do not snapshot it. A second table
204
- * registering the same field appends to the dim afterwards and this array
205
- * grows in place.
214
+ * With `table`, only the categories that table holds: the dim spans every
215
+ * table registering the field, and an axis over one table must not show a
216
+ * slot for a category only another has. Without it (a scale shared per field,
217
+ * like ordinal colour), every category of the dim.
218
+ *
219
+ * A snapshot, not the live array: for decoding ids, hold `identityFor(field)`.
206
220
  */
207
- identityValues(field) {
208
- return this.#identity.values(field);
221
+ categoryValues(field, table) {
222
+ const identity = this.#identity.identity(field);
223
+ const values = identity.values;
224
+ const present = table ? this.#identity.presentIds(field, table) : null;
225
+ if (identity.synthetic.size === 0 && !present)
226
+ return values.slice();
227
+ const out = [];
228
+ for (let id = 0; id < values.length; id++) {
229
+ if (identity.synthetic.has(id))
230
+ continue;
231
+ if (present && !present.has(id))
232
+ continue;
233
+ out.push(values[id]);
234
+ }
235
+ return out;
236
+ }
237
+ /**
238
+ * The table of ids `table` holds for `field`, for `dense()` to pad from, or
239
+ * null before the table has folded into the field's dim.
240
+ */
241
+ presentTableFor(field, table) {
242
+ return this.#identity.presentTable(field, table);
209
243
  }
210
244
  /**
211
245
  * The field's id <-> value bijection. One live object owning both
212
246
  * directions, so a holder cannot end up with a value array and a reverse map
213
- * that disagree. Prefer this over identityValues() anywhere the reverse
247
+ * that disagree. Prefer this over categoryValues() anywhere the reverse
214
248
  * direction is needed; do not rebuild it locally.
215
249
  */
216
250
  identityFor(field) {
217
251
  return this.#identity.identity(field);
218
252
  }
253
+ /** The field's current bijection, or null; never creates one (see CategoricalIdentity). */
254
+ peekIdentity(field) {
255
+ return this.#identity.peekIdentity(field);
256
+ }
219
257
  /**
220
- * Reserve an id for a value present in no table — the synthetic "Other"
221
- * bucket from `topN({ includeOthers })`. Call after ensureColumns.
222
- * Returns an existing id when a real category already carries the label.
258
+ * Acquire an id for a value present in no table — the synthetic "Other"
259
+ * bucket from `topN({ includeOthers })` — with the label it displays. Call
260
+ * after ensureColumns; release it when its holder goes.
223
261
  */
224
- reserveSyntheticIdentity(field, label) {
225
- return this.#identity.reserveSynthetic(field, label);
262
+ acquireSyntheticIdentity(field, label) {
263
+ return this.#identity.acquireSynthetic(field, label);
264
+ }
265
+ /** Give back an id from `acquireSyntheticIdentity`, for the next acquire to reuse. */
266
+ releaseSyntheticIdentity(synthetic) {
267
+ this.#identity.releaseSynthetic(synthetic);
226
268
  }
227
269
  /**
228
270
  * Subscribe to identity resets. Returns an unsubscribe function.
@@ -295,7 +337,7 @@ export class QueryEngine {
295
337
  // Opt out with cache: false for queries against volatile temp tables.
296
338
  const useCache = opts?.cache !== false;
297
339
  if (useCache) {
298
- const cached = this.#queryCache.get(sqlStr);
340
+ const cached = this.#queryCache.get(this.#cacheKey(sqlStr));
299
341
  if (cached !== undefined) {
300
342
  return cached instanceof Promise ? cached : Promise.resolve(cached);
301
343
  }
@@ -332,9 +374,17 @@ export class QueryEngine {
332
374
  }
333
375
  return this.#queryRaw(sqlStr);
334
376
  }
377
+ /**
378
+ * The cache key for a query: its text and the dims' version, so a result read
379
+ * against a dim that has since grown is not served again.
380
+ */
381
+ #cacheKey(sqlStr) {
382
+ return `${this.#identity.version}\u0000${sqlStr}`;
383
+ }
335
384
  async #executeForCaller(callerId, sqlStr, signal, cache = true) {
336
385
  const controller = new AbortController();
337
386
  this.#inflight.set(callerId, controller);
387
+ let key = null;
338
388
  if (signal) {
339
389
  signal.addEventListener('abort', () => controller.abort(), { once: true });
340
390
  }
@@ -342,24 +392,28 @@ export class QueryEngine {
342
392
  if (controller.signal.aborted) {
343
393
  throw new DOMException('Query superseded', 'AbortError');
344
394
  }
395
+ // Keyed when it runs, not when it was asked for: a pending query runs
396
+ // later, against whatever the dims hold by then.
397
+ key = this.#cacheKey(sqlStr);
345
398
  // Cache the Promise so concurrent identical queries share one inflight
346
399
  const queryPromise = this.#queryRaw(sqlStr);
347
400
  if (cache)
348
- this.#queryCache.set(sqlStr, queryPromise);
401
+ this.#queryCache.set(key, queryPromise);
349
402
  const result = await queryPromise;
350
403
  // Replace Promise with resolved data for future lookups
351
404
  if (cache)
352
- this.#queryCache.set(sqlStr, result);
405
+ this.#queryCache.set(key, result);
353
406
  if (controller.signal.aborted) {
354
407
  // Don't cache results from aborted queries
355
- this.#queryCache.delete(sqlStr);
408
+ this.#queryCache.delete(key);
356
409
  throw new DOMException('Query superseded', 'AbortError');
357
410
  }
358
411
  return result;
359
412
  }
360
413
  catch (e) {
361
414
  // Remove failed query from cache so we don't serve a rejected Promise
362
- this.#queryCache.delete(sqlStr);
415
+ if (key !== null)
416
+ this.#queryCache.delete(key);
363
417
  throw e;
364
418
  }
365
419
  finally {
@@ -559,27 +613,55 @@ export class QueryEngine {
559
613
  return this.#lastFilterChangeAt;
560
614
  }
561
615
  /**
562
- * Called by FilterGroup when any filter clause changes.
563
- * Iterates registered marks for this group and batch-queries them.
616
+ * Called by FilterGroup when its clauses change wholesale (clearAll):
617
+ * re-queries every mark in the group.
564
618
  */
565
619
  onFilterChange(group) {
566
620
  this.#lastFilterChangeAt = performance.now();
567
- const batch = [];
568
621
  for (const [markId, reg] of this.#marks) {
569
- if (reg.filterGroup === group) {
570
- batch.push([markId, reg]);
571
- }
622
+ if (reg.filterGroup === group)
623
+ this.#queryMark(markId, reg);
572
624
  }
573
- if (batch.length > 0) {
574
- this.#executeBatch(batch);
625
+ }
626
+ /**
627
+ * Called by FilterGroup when the clause of `sourceId` changes: `prev` is the
628
+ * clause it replaced or removed, `next` the one now in force (null when
629
+ * removed).
630
+ *
631
+ * Re-queries only the group's marks that either one filters. A mark neither
632
+ * filters has the same SQL as before, so querying it would only assemble that
633
+ * SQL to find the cached result, on every brush frame. A mark that reads the
634
+ * clause's categorical field dims by it instead, and a mark in a chart that
635
+ * skips the source is not filtered by it: neither is re-queried.
636
+ */
637
+ onClauseChange(group, sourceId, prev, next) {
638
+ this.#lastFilterChangeAt = performance.now();
639
+ const { selection } = group;
640
+ for (const [markId, reg] of this.#marks) {
641
+ if (reg.filterGroup !== group)
642
+ continue;
643
+ const route = reg.descriptor.route ?? NO_ROUTE;
644
+ if ((prev && selection.applies(sourceId, prev, route)) ||
645
+ (next && selection.applies(sourceId, next, route))) {
646
+ this.#queryMark(markId, reg);
647
+ }
575
648
  }
576
649
  }
577
- #executeBatch(batch) {
578
- for (const [markId, reg] of batch)
579
- this.#queryMark(markId, reg);
650
+ /**
651
+ * Called by FilterGroup when a registration that skips `chart` comes or
652
+ * goes while its source has a clause: re-queries the group's marks in that
653
+ * chart, whose filters may have changed. A mark whose SQL did not change
654
+ * stops at the cache and the same-Table guard.
655
+ */
656
+ onChartRouteChange(group, chart) {
657
+ for (const [markId, reg] of this.#marks) {
658
+ if (reg.filterGroup === group && reg.descriptor.route?.chart === chart) {
659
+ this.#queryMark(markId, reg);
660
+ }
661
+ }
580
662
  }
581
663
  /**
582
- * Query a single mark: on a filter change (via #executeBatch) or a params
664
+ * Query a single mark: on a filter change or a params
583
665
  * change (via _onParamsChange). Reads a preagg table when one answers; if
584
666
  * that read fails, the table is set aside and the base table is queried once.
585
667
  */
@@ -611,7 +693,7 @@ export class QueryEngine {
611
693
  /** The query a mark runs on its base table, applying every clause. */
612
694
  #baseSQL(markId, reg) {
613
695
  const { descriptor, handle } = reg;
614
- return this.#assembleSQL(descriptor, descriptor.table, reg.filterGroup.selection.predicate(markId), handle.params);
696
+ return this.#assembleSQL(descriptor, descriptor.table, reg.filterGroup.selection.predicate(reg.descriptor.route ?? NO_ROUTE), handle.params);
615
697
  }
616
698
  /**
617
699
  * Assemble SQL from a query descriptor + filter + per-mark params.