@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.
- package/README.md +2 -0
- package/dist/components/axes/AxisBottom.svelte +23 -6
- package/dist/components/axes/AxisBottom.svelte.d.ts +10 -0
- package/dist/components/axes/AxisLeft.svelte +77 -8
- package/dist/components/axes/AxisLeft.svelte.d.ts +17 -0
- package/dist/components/axes/BandAxisTicks.svelte +64 -25
- package/dist/components/axes/BandAxisTicks.svelte.d.ts +5 -5
- package/dist/components/axes/axis-title.d.ts +30 -0
- package/dist/components/axes/axis-title.js +77 -0
- package/dist/components/charts/ChartCell.svelte +1 -1
- package/dist/components/charts/ChartContainer.svelte +12 -3
- package/dist/components/charts/ChartContainer.svelte.d.ts +7 -0
- package/dist/components/guides/legend/ContinuousColorLegend.svelte +9 -2
- package/dist/components/guides/legend/OrdinalColorLegend.svelte +96 -35
- package/dist/components/marks/AreaMarkX.svelte +21 -6
- package/dist/components/marks/AreaMarkY.svelte +21 -6
- package/dist/components/marks/BarMarkX.svelte +16 -1
- package/dist/components/marks/BarMarkY.svelte +16 -1
- package/dist/components/marks/DotsMark.svelte +5 -0
- package/dist/components/marks/LineMark.svelte +8 -1
- package/dist/components/marks/ParallelCoordinatesMark.svelte +10 -0
- package/dist/components/marks/ParallelCoordinatesMark.svelte.d.ts +5 -0
- package/dist/components/marks/RectMark.svelte +5 -0
- package/dist/components/marks/RectXMark.svelte +5 -0
- package/dist/components/marks/RectYMark.svelte +5 -0
- package/dist/components/marks/RuleMarkX.svelte +12 -2
- package/dist/components/marks/RuleMarkY.svelte +12 -2
- package/dist/components/marks/TextMark.svelte +76 -13
- package/dist/components/marks/lifecycle/MarkLifecycleV2.svelte.d.ts +21 -2
- package/dist/components/marks/lifecycle/MarkLifecycleV2.svelte.js +300 -42
- package/dist/components/marks/lifecycle/domain-queries.d.ts +3 -1
- package/dist/components/marks/lifecycle/domain-queries.js +22 -15
- package/dist/components/marks/lifecycle/domain-registration.js +2 -6
- package/dist/components/marks/lifecycle/other-rows.svelte.d.ts +104 -0
- package/dist/components/marks/lifecycle/other-rows.svelte.js +193 -0
- package/dist/components/marks/lifecycle/other-split.d.ts +80 -0
- package/dist/components/marks/lifecycle/other-split.js +112 -0
- package/dist/components/marks/lifecycle/query-builder.d.ts +26 -1
- package/dist/components/marks/lifecycle/query-builder.js +25 -5
- package/dist/components/marks/lifecycle/transform-scale-key.d.ts +16 -0
- package/dist/components/marks/lifecycle/transform-scale-key.js +27 -0
- package/dist/components/marks/utils/area-outline.d.ts +24 -0
- package/dist/components/marks/utils/area-outline.js +10 -0
- package/dist/components/marks/utils/reactive-config.d.ts +1 -1
- package/dist/components/marks/utils/reactive-config.js +9 -0
- package/dist/components/marks/utils/tooltip-helpers.d.ts +4 -1
- package/dist/components/marks/utils/tooltip-helpers.js +2 -1
- package/dist/components/rendering/Areas.svelte +30 -6
- package/dist/components/rendering/Areas.svelte.d.ts +2 -0
- package/dist/components/rendering/BaseGPURenderer.svelte.d.ts +3 -1
- package/dist/components/rendering/BaseGPURenderer.svelte.js +8 -3
- package/dist/components/rendering/Dots.svelte +24 -3
- package/dist/components/rendering/Dots.svelte.d.ts +2 -0
- package/dist/components/rendering/GridLine.svelte +2 -1
- package/dist/components/rendering/Lines.svelte +49 -7
- package/dist/components/rendering/Lines.svelte.d.ts +9 -0
- package/dist/components/rendering/ParallelLines.svelte +26 -7
- package/dist/components/rendering/ParallelLines.svelte.d.ts +2 -0
- package/dist/components/rendering/Rects.svelte +129 -20
- package/dist/components/rendering/Rects.svelte.d.ts +9 -0
- package/dist/components/rendering/RuleX.svelte +20 -5
- package/dist/components/rendering/RuleX.svelte.d.ts +2 -0
- package/dist/components/rendering/RuleY.svelte +20 -5
- package/dist/components/rendering/RuleY.svelte.d.ts +2 -0
- package/dist/components/rendering/activity-reads.d.ts +111 -0
- package/dist/components/rendering/activity-reads.js +154 -0
- package/dist/components/rendering/uniform-sizes.d.ts +26 -0
- package/dist/components/rendering/uniform-sizes.js +28 -0
- package/dist/config/ChartConfig.svelte.d.ts +5 -1
- package/dist/gpu/ColorScaleShaderLib.d.ts +13 -1
- package/dist/gpu/ColorScaleShaderLib.js +56 -5
- package/dist/gpu/GPUCoordinator.svelte.d.ts +8 -0
- package/dist/gpu/GPUCoordinator.svelte.js +68 -32
- package/dist/gpu/MorphedChannels.d.ts +6 -32
- package/dist/gpu/MorphedChannels.js +45 -3
- package/dist/gpu/ResolveShader.d.ts +2 -2
- package/dist/gpu/ResolveShader.js +2 -2
- package/dist/gpu/ScaleTemplateSystem.d.ts +19 -0
- package/dist/gpu/ScaleTemplateSystem.js +111 -11
- package/dist/gpu/SizeScaleShaderLib.d.ts +4 -4
- package/dist/gpu/SizeScaleShaderLib.js +4 -4
- package/dist/gpu/identity-slot-packing.d.ts +157 -40
- package/dist/gpu/identity-slot-packing.js +262 -62
- package/dist/gpu/tracked-scales.d.ts +14 -0
- package/dist/gpu/tracked-scales.js +18 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +4 -0
- package/dist/interactors/BandLabelToggle.svelte +5 -9
- package/dist/interactors/BandLabelToggle.svelte.d.ts +3 -3
- package/dist/interactors/DerivedInterval1D.svelte +26 -54
- package/dist/interactors/Interval1D.svelte +8 -2
- package/dist/interactors/Interval2D.svelte +8 -2
- package/dist/interactors/ParallelAxisSource.svelte +14 -5
- package/dist/interactors/ParallelAxisSource.svelte.d.ts +2 -1
- package/dist/interactors/ParallelBrushes.svelte +8 -9
- package/dist/interactors/ParallelBrushes.svelte.d.ts +3 -3
- package/dist/interactors/Toggle.svelte +48 -38
- package/dist/interactors/Toggle.svelte.d.ts +4 -3
- package/dist/interactors/Tooltip.svelte +140 -42
- package/dist/interactors/TooltipManager.svelte.d.ts +20 -5
- package/dist/interactors/TooltipManager.svelte.js +40 -10
- package/dist/interactors/other-tooltip.d.ts +16 -0
- package/dist/interactors/other-tooltip.js +22 -0
- package/dist/interactors/relay-clauses.d.ts +23 -0
- package/dist/interactors/relay-clauses.js +60 -0
- package/dist/query-engine/CrossfilterSelection.svelte.d.ts +88 -15
- package/dist/query-engine/CrossfilterSelection.svelte.js +94 -20
- package/dist/query-engine/FieldActivity.svelte.d.ts +128 -0
- package/dist/query-engine/FieldActivity.svelte.js +378 -0
- package/dist/query-engine/FilterGroup.svelte.d.ts +76 -30
- package/dist/query-engine/FilterGroup.svelte.js +223 -124
- package/dist/query-engine/FilterGroupRegistry.svelte.d.ts +73 -3
- package/dist/query-engine/FilterGroupRegistry.svelte.js +161 -12
- package/dist/query-engine/MarkHandle.svelte.d.ts +9 -1
- package/dist/query-engine/MarkHandle.svelte.js +12 -2
- package/dist/query-engine/QueryEngine.d.ts +52 -16
- package/dist/query-engine/QueryEngine.js +116 -34
- package/dist/query-engine/categorical-identity.d.ts +114 -27
- package/dist/query-engine/categorical-identity.js +225 -51
- package/dist/query-engine/index.d.ts +2 -1
- package/dist/query-engine/index.js +1 -0
- package/dist/query-engine/preagg/Preagg.d.ts +1 -1
- package/dist/query-engine/preagg/Preagg.js +11 -9
- package/dist/query-engine/preagg/plan.d.ts +14 -5
- package/dist/query-engine/preagg/plan.js +44 -18
- package/dist/query-engine/toggle-predicate.d.ts +21 -0
- package/dist/query-engine/toggle-predicate.js +41 -2
- package/dist/scales/ActivityTable.d.ts +103 -0
- package/dist/scales/ActivityTable.js +220 -0
- package/dist/scales/BandScaleState.svelte.d.ts +41 -15
- package/dist/scales/BandScaleState.svelte.js +78 -36
- package/dist/scales/ChartScales.svelte.d.ts +2 -2
- package/dist/scales/ColorScaleState.svelte.d.ts +36 -8
- package/dist/scales/ColorScaleState.svelte.js +118 -24
- package/dist/scales/DomainResolver.d.ts +2 -0
- package/dist/scales/DomainResolver.js +15 -8
- package/dist/scales/ScaleRegistry.svelte.d.ts +9 -0
- package/dist/scales/ScaleRegistry.svelte.js +22 -1
- package/dist/scales/TimeScaleState.svelte.js +7 -1
- package/dist/spec/index.js +1 -0
- package/dist/spec/types.d.ts +4 -0
- package/dist/theme.css +5 -4
- package/dist/transforms/TransformCompiler.d.ts +37 -2
- package/dist/transforms/TransformCompiler.js +44 -1
- package/dist/transforms/index.d.ts +27 -0
- package/dist/transforms/index.js +127 -19
- package/dist/types.d.ts +71 -0
- 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
|
-
|
|
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
|
-
/**
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
*
|
|
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
|
-
*
|
|
131
|
-
*
|
|
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
|
-
*
|
|
134
|
-
* registering the
|
|
135
|
-
*
|
|
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
|
-
|
|
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
|
|
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
|
-
*
|
|
147
|
-
* bucket from `topN({ includeOthers })
|
|
148
|
-
*
|
|
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
|
-
|
|
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
|
|
238
|
-
*
|
|
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
|
-
*
|
|
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
|
-
/**
|
|
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
|
-
*
|
|
201
|
-
*
|
|
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
|
-
*
|
|
204
|
-
* registering the
|
|
205
|
-
*
|
|
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
|
-
|
|
208
|
-
|
|
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
|
|
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
|
-
*
|
|
221
|
-
* bucket from `topN({ includeOthers })
|
|
222
|
-
*
|
|
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
|
-
|
|
225
|
-
return this.#identity.
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
|
563
|
-
*
|
|
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
|
-
|
|
571
|
-
}
|
|
622
|
+
if (reg.filterGroup === group)
|
|
623
|
+
this.#queryMark(markId, reg);
|
|
572
624
|
}
|
|
573
|
-
|
|
574
|
-
|
|
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
|
-
|
|
578
|
-
|
|
579
|
-
|
|
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
|
|
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(
|
|
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.
|