@fjader/svelte 0.2.2 → 0.2.3
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/dist/components/axes/BandAxisTicks.svelte +63 -24
- package/dist/components/axes/BandAxisTicks.svelte.d.ts +5 -5
- package/dist/components/charts/ChartContainer.svelte +2 -1
- package/dist/components/guides/legend/ContinuousColorLegend.svelte +2 -1
- package/dist/components/guides/legend/OrdinalColorLegend.svelte +92 -34
- package/dist/components/marks/AreaMarkX.svelte +19 -6
- package/dist/components/marks/AreaMarkY.svelte +19 -6
- package/dist/components/marks/BarMarkX.svelte +3 -0
- package/dist/components/marks/BarMarkY.svelte +3 -0
- package/dist/components/marks/DotsMark.svelte +3 -0
- package/dist/components/marks/LineMark.svelte +3 -0
- package/dist/components/marks/ParallelCoordinatesMark.svelte +6 -0
- package/dist/components/marks/ParallelCoordinatesMark.svelte.d.ts +3 -0
- package/dist/components/marks/RectMark.svelte +3 -0
- package/dist/components/marks/RectXMark.svelte +3 -0
- package/dist/components/marks/RectYMark.svelte +3 -0
- package/dist/components/marks/RuleMarkX.svelte +10 -2
- package/dist/components/marks/RuleMarkY.svelte +10 -2
- package/dist/components/marks/TextMark.svelte +68 -4
- package/dist/components/marks/lifecycle/MarkLifecycleV2.svelte.d.ts +15 -1
- package/dist/components/marks/lifecycle/MarkLifecycleV2.svelte.js +116 -27
- package/dist/components/marks/lifecycle/domain-queries.js +13 -11
- 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 +3 -0
- package/dist/components/rendering/Areas.svelte +30 -6
- package/dist/components/rendering/Areas.svelte.d.ts +2 -0
- package/dist/components/rendering/BaseGPURenderer.svelte.js +4 -1
- 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 +20 -5
- package/dist/components/rendering/Rects.svelte.d.ts +2 -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 +93 -0
- package/dist/components/rendering/activity-reads.js +135 -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 +2 -0
- 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/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 +86 -7
- 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.js +1 -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 +2 -1
- package/dist/interactors/Interval2D.svelte +2 -1
- package/dist/interactors/ParallelAxisSource.svelte +4 -4
- 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 +17 -36
- package/dist/interactors/Toggle.svelte.d.ts +4 -3
- package/dist/interactors/Tooltip.svelte +2 -1
- package/dist/interactors/TooltipManager.svelte.js +8 -3
- 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 +79 -0
- package/dist/query-engine/FieldActivity.svelte.js +207 -0
- package/dist/query-engine/FilterGroup.svelte.d.ts +53 -29
- package/dist/query-engine/FilterGroup.svelte.js +181 -122
- package/dist/query-engine/FilterGroupRegistry.svelte.d.ts +42 -0
- package/dist/query-engine/FilterGroupRegistry.svelte.js +103 -1
- 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 +90 -28
- package/dist/query-engine/categorical-identity.d.ts +99 -27
- package/dist/query-engine/categorical-identity.js +210 -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 +9 -5
- package/dist/query-engine/preagg/plan.js +13 -11
- package/dist/query-engine/toggle-predicate.js +12 -1
- package/dist/scales/ActivityTable.d.ts +99 -0
- package/dist/scales/ActivityTable.js +211 -0
- package/dist/scales/BandScaleState.svelte.d.ts +28 -7
- package/dist/scales/BandScaleState.svelte.js +55 -20
- 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 +2 -0
- package/dist/transforms/index.d.ts +27 -0
- package/dist/transforms/index.js +97 -3
- package/dist/types.d.ts +30 -0
- package/package.json +1 -1
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* FilterGroup — visual concerns layer on top of CrossfilterSelection.
|
|
3
3
|
*
|
|
4
4
|
* Manages the interactor API (register, registerToggle), schema augmentation
|
|
5
|
-
* for UI filter pills, and
|
|
6
|
-
* legends
|
|
5
|
+
* for UI filter pills, and this group's selection on each categorical field
|
|
6
|
+
* (FieldActivity), which marks, legends and band axes dim by.
|
|
7
7
|
*
|
|
8
8
|
* Owns clauses: a clause lives while at least one registration for its id
|
|
9
9
|
* exists. Components own their registrations through their own lifecycle.
|
|
@@ -14,6 +14,34 @@
|
|
|
14
14
|
import { column, sql as sqlTag } from '@uwdata/mosaic-sql';
|
|
15
15
|
import { CrossfilterSelection } from './CrossfilterSelection.svelte';
|
|
16
16
|
import { togglePredicate } from './toggle-predicate';
|
|
17
|
+
import { identityKeyFor } from './identity-key';
|
|
18
|
+
import { FieldActivity } from './FieldActivity.svelte';
|
|
19
|
+
/**
|
|
20
|
+
* A source's declaration of `key`: the writer's, or another registration's
|
|
21
|
+
* when the writer has none. Every registration under one id is one source, so
|
|
22
|
+
* any of them can say what its clause is.
|
|
23
|
+
*/
|
|
24
|
+
function declared(registrations, writer, key) {
|
|
25
|
+
if (writer[key])
|
|
26
|
+
return writer[key];
|
|
27
|
+
for (const opts of registrations ?? [])
|
|
28
|
+
if (opts[key])
|
|
29
|
+
return opts[key];
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* A clause's schema, computed once when it is written rather than on every
|
|
34
|
+
* read of `items`, and stored on the clause, so a reader of raw clauses (the
|
|
35
|
+
* relay looking for a range brush) sees it too.
|
|
36
|
+
*/
|
|
37
|
+
function schemaOf(registrations, writer, value) {
|
|
38
|
+
return declared(registrations, writer, 'augment')?.(value) ?? undefined;
|
|
39
|
+
}
|
|
40
|
+
/** A clause's categorical selection: the field's identity key and the selected values. */
|
|
41
|
+
function categoricalOf(registrations, writer, value) {
|
|
42
|
+
const categorical = declared(registrations, writer, 'categorical');
|
|
43
|
+
return categorical ? { key: categorical.key, values: categorical.values(value) } : null;
|
|
44
|
+
}
|
|
17
45
|
// ---------------------------------------------------------------------------
|
|
18
46
|
// Toggles
|
|
19
47
|
// ---------------------------------------------------------------------------
|
|
@@ -39,33 +67,47 @@ function toggleSourceId(fields) {
|
|
|
39
67
|
export class FilterGroup {
|
|
40
68
|
selection = new CrossfilterSelection();
|
|
41
69
|
#engine = null;
|
|
70
|
+
/**
|
|
71
|
+
* This group's page-wide index, assigned by FilterGroupRegistry and never
|
|
72
|
+
* reused: a mark stores it to read this group's half of an activity block.
|
|
73
|
+
*/
|
|
74
|
+
index;
|
|
75
|
+
/** The selection state of a field, shared with the other groups on the page. */
|
|
76
|
+
#activityOf;
|
|
77
|
+
/** Fields this group has an active categorical selection on, for clearAll. */
|
|
78
|
+
#selectedFields = new Set();
|
|
79
|
+
/**
|
|
80
|
+
* A group made outside a registry (unit tests) keeps its field activity to
|
|
81
|
+
* itself, with no loop: its fades land at once.
|
|
82
|
+
*/
|
|
83
|
+
constructor(index = 0, activityOf) {
|
|
84
|
+
this.index = index;
|
|
85
|
+
if (activityOf) {
|
|
86
|
+
this.#activityOf = activityOf;
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
const own = new Map();
|
|
90
|
+
this.#activityOf = (key) => {
|
|
91
|
+
let activity = own.get(key);
|
|
92
|
+
if (!activity)
|
|
93
|
+
own.set(key, (activity = new FieldActivity(key)));
|
|
94
|
+
return activity;
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
/** The selection state of a categorical field, by identity key: this group's fade is `index`. */
|
|
99
|
+
activity(key) {
|
|
100
|
+
return this.#activityOf(key);
|
|
101
|
+
}
|
|
42
102
|
setEngine(engine) {
|
|
43
103
|
this.#engine = engine;
|
|
44
104
|
}
|
|
45
|
-
/**
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
/** Active filter items with schema metadata, for UI consumption. */
|
|
105
|
+
/**
|
|
106
|
+
* Active filter items with schema metadata, for UI consumption. The schema
|
|
107
|
+
* is the clause's own, computed when it was written (see `update`).
|
|
108
|
+
*/
|
|
50
109
|
get items() {
|
|
51
|
-
|
|
52
|
-
const result = [];
|
|
53
|
-
for (const entry of clauses) {
|
|
54
|
-
let schema = entry.schema;
|
|
55
|
-
for (const opts of this.#sources.get(entry.sourceId) ?? []) {
|
|
56
|
-
if (!opts.augment)
|
|
57
|
-
continue;
|
|
58
|
-
schema = opts.augment(entry.value) ?? entry.schema;
|
|
59
|
-
break;
|
|
60
|
-
}
|
|
61
|
-
result.push({
|
|
62
|
-
sourceId: entry.sourceId,
|
|
63
|
-
predicate: entry.predicate,
|
|
64
|
-
value: entry.value,
|
|
65
|
-
schema
|
|
66
|
-
});
|
|
67
|
-
}
|
|
68
|
-
return result;
|
|
110
|
+
return this.selection.items;
|
|
69
111
|
}
|
|
70
112
|
/** Number of active filters. */
|
|
71
113
|
get activeCount() {
|
|
@@ -80,60 +122,73 @@ export class FilterGroup {
|
|
|
80
122
|
// -----------------------------------------------------------------
|
|
81
123
|
/**
|
|
82
124
|
* Register a selection source. Several registrations under one id are one
|
|
83
|
-
* source
|
|
84
|
-
* source dims are the union of theirs.
|
|
125
|
+
* source and write one clause.
|
|
85
126
|
*/
|
|
86
127
|
register(sourceId, opts) {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
128
|
+
const { selection } = this;
|
|
129
|
+
selection.addRegistration(sourceId, opts);
|
|
130
|
+
// A chart that starts skipping a selection already in force has marks
|
|
131
|
+
// whose filters just changed.
|
|
132
|
+
if (opts.chart && selection.isActive(sourceId)) {
|
|
133
|
+
this.#engine?.onChartRouteChange(this, opts.chart);
|
|
91
134
|
}
|
|
92
|
-
registrations.
|
|
135
|
+
const registrations = () => selection.registrationsOf(sourceId);
|
|
136
|
+
let registered = true;
|
|
93
137
|
return {
|
|
94
138
|
update: (predicate, value) => {
|
|
95
|
-
|
|
139
|
+
const next = {
|
|
96
140
|
predicate,
|
|
97
|
-
|
|
98
|
-
value
|
|
99
|
-
|
|
100
|
-
|
|
141
|
+
value,
|
|
142
|
+
schema: schemaOf(registrations(), opts, value),
|
|
143
|
+
categorical: categoricalOf(registrations(), opts, value)
|
|
144
|
+
};
|
|
145
|
+
this.#afterUpdate(sourceId, selection.update(sourceId, next), next);
|
|
101
146
|
},
|
|
102
147
|
clear: () => this.clearSource(sourceId),
|
|
103
148
|
activate: () => this.#activate(sourceId),
|
|
104
149
|
destroy: () => {
|
|
105
|
-
|
|
106
|
-
|
|
150
|
+
// Once only: a second call must not remove a newer registration
|
|
151
|
+
// under the same id, or clear its clause.
|
|
152
|
+
if (!registered)
|
|
153
|
+
return;
|
|
154
|
+
registered = false;
|
|
155
|
+
if (registrations()?.size === 1) {
|
|
156
|
+
// The last one: drop the source's preagg tables first, so the
|
|
157
|
+
// re-query below cannot start reading one about to go. Then clear
|
|
158
|
+
// while the registration is still in place, so the removal is
|
|
159
|
+
// routed by it: the marks re-queried are those the clause filtered.
|
|
160
|
+
this.#engine?.forgetSource(this, sourceId);
|
|
161
|
+
this.clearSource(sourceId);
|
|
162
|
+
selection.removeRegistration(sourceId, opts);
|
|
107
163
|
return;
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
164
|
+
}
|
|
165
|
+
selection.removeRegistration(sourceId, opts);
|
|
166
|
+
// Another registration keeps the clause; its chart may stop skipping it.
|
|
167
|
+
if (opts.chart && selection.isActive(sourceId)) {
|
|
168
|
+
this.#engine?.onChartRouteChange(this, opts.chart);
|
|
169
|
+
}
|
|
111
170
|
}
|
|
112
171
|
};
|
|
113
172
|
}
|
|
114
173
|
/**
|
|
115
|
-
* Hand the engine a snapshot of the source
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
* registration excludes are left out.
|
|
174
|
+
* Hand the engine a snapshot of the source: its dims, its view, and its
|
|
175
|
+
* categorical field, so activation can tell which marks it would filter
|
|
176
|
+
* before it has written a clause.
|
|
119
177
|
*/
|
|
120
178
|
#activate(sourceId) {
|
|
121
|
-
const registrations = this
|
|
179
|
+
const registrations = this.selection.registrationsOf(sourceId);
|
|
122
180
|
if (!this.#engine || !registrations?.size)
|
|
123
181
|
return;
|
|
124
|
-
let excludedByAll = null;
|
|
125
182
|
let dims = [];
|
|
126
183
|
let view;
|
|
184
|
+
let categoricalKey = null;
|
|
127
185
|
for (const opts of registrations) {
|
|
128
|
-
const excluded = opts.excludeMarkIds();
|
|
129
|
-
excludedByAll = excludedByAll
|
|
130
|
-
? new Set([...excludedByAll].filter((id) => excluded.has(id)))
|
|
131
|
-
: excluded;
|
|
132
186
|
if (opts.activeDims && dims.length === 0)
|
|
133
187
|
dims = opts.activeDims();
|
|
134
188
|
view ??= opts.view;
|
|
189
|
+
categoricalKey ??= opts.categorical?.key ?? null;
|
|
135
190
|
}
|
|
136
|
-
this.#engine.activate(this, { sourceId,
|
|
191
|
+
this.#engine.activate(this, { sourceId, categoricalKey, dims, view });
|
|
137
192
|
}
|
|
138
193
|
// -----------------------------------------------------------------
|
|
139
194
|
// Toggles (legend/axis categorical selection)
|
|
@@ -147,22 +202,24 @@ export class FilterGroup {
|
|
|
147
202
|
* as the selection. Nothing else writes those ids (`upsertManual` prefixes
|
|
148
203
|
* its own with `manual:`).
|
|
149
204
|
*/
|
|
150
|
-
registerToggle(fields, opts) {
|
|
205
|
+
registerToggle(fields, opts = {}) {
|
|
151
206
|
const list = Array.isArray(fields) ? fields : [fields];
|
|
152
207
|
const sourceId = toggleSourceId(list);
|
|
153
208
|
const { selection } = this;
|
|
154
209
|
const schema = { kind: 'toggle', column: list[0], label: list.join(', ') };
|
|
210
|
+
// One field selects categories; a tuple does not (see Clause.categorical).
|
|
211
|
+
const categoricalKey = list.length === 1 ? (identityKeyFor(list[0])?.key ?? null) : null;
|
|
155
212
|
const source = this.register(sourceId, {
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
// Projection only makes sense for single-field toggles.
|
|
160
|
-
projection: list.length === 1 && opts.scales
|
|
213
|
+
chart: opts.chart ?? null,
|
|
214
|
+
discrete: true,
|
|
215
|
+
categorical: categoricalKey
|
|
161
216
|
? {
|
|
162
|
-
|
|
217
|
+
key: categoricalKey,
|
|
163
218
|
values: (value) => new Set(value.map((t) => t[0]))
|
|
164
219
|
}
|
|
165
|
-
: undefined
|
|
220
|
+
: undefined,
|
|
221
|
+
activeDims: () => list.map((field) => ({ kind: 'value', field })),
|
|
222
|
+
augment: () => schema
|
|
166
223
|
});
|
|
167
224
|
const tuplesOf = (clause) => clause?.value ?? [];
|
|
168
225
|
/**
|
|
@@ -170,11 +227,16 @@ export class FilterGroup {
|
|
|
170
227
|
* map's version, so a toggle with nothing selected is re-read on every one;
|
|
171
228
|
* returning the same set while the clause is unchanged (and one shared
|
|
172
229
|
* empty set without a clause) keeps that from re-rendering its readers.
|
|
230
|
+
*
|
|
231
|
+
* One field's values are the clause's own categorical set, built once per
|
|
232
|
+
* write. A tuple's keys are built here, once per clause.
|
|
173
233
|
*/
|
|
174
234
|
let last = null;
|
|
175
235
|
const keysOf = (clause) => {
|
|
176
236
|
if (!clause)
|
|
177
237
|
return NOTHING_SELECTED;
|
|
238
|
+
if (clause.categorical)
|
|
239
|
+
return clause.categorical.values;
|
|
178
240
|
if (last?.clause !== clause) {
|
|
179
241
|
last = { clause, keys: new Set(tuplesOf(clause).map(tupleKey)) };
|
|
180
242
|
}
|
|
@@ -233,19 +295,26 @@ export class FilterGroup {
|
|
|
233
295
|
// -----------------------------------------------------------------
|
|
234
296
|
// Manual entries (for DerivedInterval1D relay, programmatic filters)
|
|
235
297
|
// -----------------------------------------------------------------
|
|
236
|
-
upsertManual(key, predicate, value,
|
|
237
|
-
const
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
298
|
+
upsertManual(key, predicate, value, schema) {
|
|
299
|
+
const next = { predicate, value, schema, categorical: null };
|
|
300
|
+
const sourceId = `manual:${key}`;
|
|
301
|
+
this.#afterUpdate(sourceId, this.selection.update(sourceId, next), next);
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* Hold `clause` under a manual key, as given: the relay passes another
|
|
305
|
+
* group's clause on. The clause it already holds there changes nothing, so
|
|
306
|
+
* it neither re-projects nor re-queries: the relay runs on every change in
|
|
307
|
+
* its source group, on every brush frame there.
|
|
308
|
+
*/
|
|
309
|
+
relayClause(key, clause) {
|
|
310
|
+
const sourceId = `manual:${key}`;
|
|
311
|
+
if (this.selection.get(sourceId) === clause)
|
|
312
|
+
return;
|
|
313
|
+
this.#afterUpdate(sourceId, this.selection.update(sourceId, clause), clause);
|
|
245
314
|
}
|
|
246
315
|
removeManual(key) {
|
|
247
|
-
|
|
248
|
-
this.#afterUpdate();
|
|
316
|
+
const sourceId = `manual:${key}`;
|
|
317
|
+
this.#afterUpdate(sourceId, this.selection.update(sourceId, null), null);
|
|
249
318
|
}
|
|
250
319
|
// -----------------------------------------------------------------
|
|
251
320
|
// Clear
|
|
@@ -257,68 +326,58 @@ export class FilterGroup {
|
|
|
257
326
|
clearSource(sourceId) {
|
|
258
327
|
if (!this.selection.isActive(sourceId))
|
|
259
328
|
return;
|
|
260
|
-
this.selection.update(sourceId, null);
|
|
261
|
-
this.#afterUpdate();
|
|
329
|
+
this.#afterUpdate(sourceId, this.selection.update(sourceId, null), null);
|
|
262
330
|
}
|
|
263
331
|
clearAll() {
|
|
264
332
|
this.selection.clear();
|
|
265
|
-
this.#
|
|
333
|
+
for (const key of this.#selectedFields)
|
|
334
|
+
this.#activityOf(key).set(this.index, null);
|
|
335
|
+
this.#selectedFields.clear();
|
|
266
336
|
this.#engine?.onFilterChange(this);
|
|
267
337
|
}
|
|
268
338
|
// -----------------------------------------------------------------
|
|
269
|
-
//
|
|
339
|
+
// After a change
|
|
270
340
|
// -----------------------------------------------------------------
|
|
271
341
|
/**
|
|
272
|
-
* After
|
|
273
|
-
* the engine
|
|
342
|
+
* After a clause went from `prev` to `next`: update this group's selection on
|
|
343
|
+
* the field of either if it is categorical, and have the engine re-query the
|
|
344
|
+
* marks either clause applies to. A brush, which writes on every pointer
|
|
345
|
+
* move, touches no selection.
|
|
274
346
|
*/
|
|
275
|
-
#afterUpdate() {
|
|
276
|
-
|
|
277
|
-
|
|
347
|
+
#afterUpdate(sourceId, prev, next) {
|
|
348
|
+
if (prev?.categorical)
|
|
349
|
+
this.#syncActivity(prev.categorical.key);
|
|
350
|
+
if (next?.categorical && next.categorical.key !== prev?.categorical?.key) {
|
|
351
|
+
this.#syncActivity(next.categorical.key);
|
|
352
|
+
}
|
|
353
|
+
if (prev || next)
|
|
354
|
+
this.#engine?.onClauseChange(this, sourceId, prev, next);
|
|
278
355
|
}
|
|
279
356
|
/**
|
|
280
|
-
*
|
|
357
|
+
* Set this group's selection on field `key` from its clauses: the
|
|
358
|
+
* intersection of the selected values of every categorical clause on that
|
|
359
|
+
* field, because the filters AND, or nothing when there is none. Usually one
|
|
360
|
+
* clause; a native toggle and a relayed one make two. Runs on categorical
|
|
361
|
+
* clause changes only, never on a brush frame.
|
|
281
362
|
*/
|
|
282
|
-
#
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
const
|
|
286
|
-
if (!
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
const values = projection.values(clause.value);
|
|
292
|
-
if (!values?.size)
|
|
293
|
-
continue;
|
|
294
|
-
for (const scale of projection.scales()) {
|
|
295
|
-
const existing = activeByScale.get(scale);
|
|
296
|
-
if (existing) {
|
|
297
|
-
for (const v of values)
|
|
298
|
-
existing.add(v);
|
|
299
|
-
}
|
|
300
|
-
else {
|
|
301
|
-
activeByScale.set(scale, new Set(values));
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
// Set active values on scales that have them
|
|
307
|
-
for (const [scale, values] of activeByScale) {
|
|
308
|
-
scale.setActiveValues(values);
|
|
309
|
-
}
|
|
310
|
-
// Clear scales that are no longer active
|
|
311
|
-
for (const scale of this.#projectedScales) {
|
|
312
|
-
if (!activeByScale.has(scale)) {
|
|
313
|
-
scale.clearActiveValues();
|
|
363
|
+
#syncActivity(key) {
|
|
364
|
+
let active = null;
|
|
365
|
+
this.selection.forEach((clause) => {
|
|
366
|
+
const categorical = clause.categorical;
|
|
367
|
+
if (!categorical || categorical.key !== key)
|
|
368
|
+
return;
|
|
369
|
+
if (!active) {
|
|
370
|
+
active = new Set(categorical.values);
|
|
371
|
+
return;
|
|
314
372
|
}
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
373
|
+
for (const value of active)
|
|
374
|
+
if (!categorical.values.has(value))
|
|
375
|
+
active.delete(value);
|
|
376
|
+
});
|
|
377
|
+
if (active)
|
|
378
|
+
this.#selectedFields.add(key);
|
|
379
|
+
else
|
|
380
|
+
this.#selectedFields.delete(key);
|
|
381
|
+
this.#activityOf(key).set(this.index, active);
|
|
323
382
|
}
|
|
324
383
|
}
|
|
@@ -4,15 +4,57 @@
|
|
|
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 type { ClockLoop } from '../animation';
|
|
8
13
|
import { FilterGroup } from './FilterGroup.svelte';
|
|
14
|
+
import { FieldActivity } from './FieldActivity.svelte';
|
|
9
15
|
import type { QueryEngine } from './QueryEngine';
|
|
10
16
|
export declare class FilterGroupRegistry {
|
|
11
17
|
#private;
|
|
18
|
+
constructor();
|
|
12
19
|
/** Wire the engine to all existing and future groups. */
|
|
13
20
|
setEngine(engine: QueryEngine): void;
|
|
21
|
+
/**
|
|
22
|
+
* Wire the fades to the animation loop and to the charts to redraw while
|
|
23
|
+
* they run. Called by the GPU coordinator. A fade created before this lands
|
|
24
|
+
* at once.
|
|
25
|
+
*/
|
|
26
|
+
setAnimation(loop: ClockLoop, chartsFor: (key: string, out: string[]) => string[]): void;
|
|
14
27
|
/** Get or create a FilterGroup by name. Undefined/null → "default". */
|
|
15
28
|
get(name?: string | null): FilterGroup;
|
|
16
29
|
/** Check if a named group exists (without creating it). */
|
|
17
30
|
has(name: string): boolean;
|
|
31
|
+
/** How many groups the page has: the packer writes one activity half per group. */
|
|
32
|
+
get groupCount(): number;
|
|
33
|
+
/**
|
|
34
|
+
* The selection state of a categorical field, by identity key. Created on
|
|
35
|
+
* first ask, by a selection or by the coordinator for a field a scale draws.
|
|
36
|
+
*
|
|
37
|
+
* Never removed: a session that switches through many categorical columns
|
|
38
|
+
* keeps every field it drew, each one step of the coordinator's per-frame
|
|
39
|
+
* reconcile loop. Their buffer space is freed (the packer gives a block only
|
|
40
|
+
* to fields a scale draws), so this costs a loop step, not memory on the GPU.
|
|
41
|
+
* Dropping a field once no scale draws it and no group selects on it is a
|
|
42
|
+
* possible follow-up.
|
|
43
|
+
*/
|
|
44
|
+
field(key: string): FieldActivity;
|
|
45
|
+
/**
|
|
46
|
+
* How visible `value` of field `key` is now, for a label shown for marks of
|
|
47
|
+
* several groups: the least visible over `groups`, so it dims when any of
|
|
48
|
+
* those groups dims it. 1 for a field no group ever selected on. Pure, so
|
|
49
|
+
* safe in a template: it creates nothing and reconciles nothing.
|
|
50
|
+
*/
|
|
51
|
+
opacityOfValue(key: string, groups: readonly number[], value: unknown): number;
|
|
52
|
+
/**
|
|
53
|
+
* How visible the category with dim id `id` of field `key` is now: the id form
|
|
54
|
+
* of `opacityOfValue`, and the one a label for an Other bucket must use. The
|
|
55
|
+
* least visible over `groups`. Pure, so safe in a template.
|
|
56
|
+
*/
|
|
57
|
+
opacityOfId(key: string, groups: readonly number[], id: number): number;
|
|
58
|
+
/** Every field's activity, in creation order. Read every frame; do not mutate. */
|
|
59
|
+
get fields(): readonly FieldActivity[];
|
|
18
60
|
}
|
|
@@ -4,11 +4,40 @@
|
|
|
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
|
*/
|
|
8
12
|
import { FilterGroup } from './FilterGroup.svelte';
|
|
13
|
+
import { FieldActivity } from './FieldActivity.svelte';
|
|
9
14
|
export class FilterGroupRegistry {
|
|
10
15
|
#groups = new Map();
|
|
11
16
|
#engine = null;
|
|
17
|
+
#fields = new Map();
|
|
18
|
+
/**
|
|
19
|
+
* Bumped when a field is created, so a reader that found none (and so read
|
|
20
|
+
* no fade) runs again once it exists. Fields are created by selections and by
|
|
21
|
+
* the coordinator for every field a scale draws, never while a template
|
|
22
|
+
* renders.
|
|
23
|
+
*/
|
|
24
|
+
#fieldsVersion = $state(0);
|
|
25
|
+
/** The same activities as an array, for the packer's per-frame loop. */
|
|
26
|
+
#fieldList = [];
|
|
27
|
+
#loop = null;
|
|
28
|
+
#chartsFor = (_key, out) => out;
|
|
29
|
+
/** Shared by every FieldActivity: reads the registry's current wiring. */
|
|
30
|
+
#host;
|
|
31
|
+
constructor() {
|
|
32
|
+
const registry = this;
|
|
33
|
+
this.#host = {
|
|
34
|
+
get loop() {
|
|
35
|
+
return registry.#loop;
|
|
36
|
+
},
|
|
37
|
+
chartsFor: (key, out) => this.#chartsFor(key, out),
|
|
38
|
+
identityOf: (key) => this.#engine?.peekIdentity(key) ?? null
|
|
39
|
+
};
|
|
40
|
+
}
|
|
12
41
|
/** Wire the engine to all existing and future groups. */
|
|
13
42
|
setEngine(engine) {
|
|
14
43
|
this.#engine = engine;
|
|
@@ -16,12 +45,23 @@ export class FilterGroupRegistry {
|
|
|
16
45
|
group.setEngine(engine);
|
|
17
46
|
}
|
|
18
47
|
}
|
|
48
|
+
/**
|
|
49
|
+
* Wire the fades to the animation loop and to the charts to redraw while
|
|
50
|
+
* they run. Called by the GPU coordinator. A fade created before this lands
|
|
51
|
+
* at once.
|
|
52
|
+
*/
|
|
53
|
+
setAnimation(loop, chartsFor) {
|
|
54
|
+
this.#loop = loop;
|
|
55
|
+
this.#chartsFor = chartsFor;
|
|
56
|
+
}
|
|
19
57
|
/** Get or create a FilterGroup by name. Undefined/null → "default". */
|
|
20
58
|
get(name) {
|
|
21
59
|
const key = name ?? 'default';
|
|
22
60
|
let g = this.#groups.get(key);
|
|
23
61
|
if (!g) {
|
|
24
|
-
|
|
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));
|
|
25
65
|
if (this.#engine)
|
|
26
66
|
g.setEngine(this.#engine);
|
|
27
67
|
this.#groups.set(key, g);
|
|
@@ -32,4 +72,66 @@ export class FilterGroupRegistry {
|
|
|
32
72
|
has(name) {
|
|
33
73
|
return this.#groups.has(name);
|
|
34
74
|
}
|
|
75
|
+
/** How many groups the page has: the packer writes one activity half per group. */
|
|
76
|
+
get groupCount() {
|
|
77
|
+
return this.#groups.size;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* The selection state of a categorical field, by identity key. Created on
|
|
81
|
+
* first ask, by a selection or by the coordinator for a field a scale draws.
|
|
82
|
+
*
|
|
83
|
+
* Never removed: a session that switches through many categorical columns
|
|
84
|
+
* keeps every field it drew, each one step of the coordinator's per-frame
|
|
85
|
+
* reconcile loop. Their buffer space is freed (the packer gives a block only
|
|
86
|
+
* to fields a scale draws), so this costs a loop step, not memory on the GPU.
|
|
87
|
+
* Dropping a field once no scale draws it and no group selects on it is a
|
|
88
|
+
* possible follow-up.
|
|
89
|
+
*/
|
|
90
|
+
field(key) {
|
|
91
|
+
let activity = this.#fields.get(key);
|
|
92
|
+
if (!activity) {
|
|
93
|
+
activity = new FieldActivity(key, this.#host);
|
|
94
|
+
this.#fields.set(key, activity);
|
|
95
|
+
this.#fieldList.push(activity);
|
|
96
|
+
this.#fieldsVersion++;
|
|
97
|
+
}
|
|
98
|
+
return activity;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* How visible `value` of field `key` is now, for a label shown for marks of
|
|
102
|
+
* several groups: the least visible over `groups`, so it dims when any of
|
|
103
|
+
* those groups dims it. 1 for a field no group ever selected on. Pure, so
|
|
104
|
+
* safe in a template: it creates nothing and reconciles nothing.
|
|
105
|
+
*/
|
|
106
|
+
opacityOfValue(key, groups, value) {
|
|
107
|
+
void this.#fieldsVersion;
|
|
108
|
+
const activity = this.#fields.get(key);
|
|
109
|
+
if (!activity)
|
|
110
|
+
return 1;
|
|
111
|
+
let opacity = 1;
|
|
112
|
+
for (let i = 0; i < groups.length; i++) {
|
|
113
|
+
opacity = Math.min(opacity, activity.opacityOfValue(groups[i], value));
|
|
114
|
+
}
|
|
115
|
+
return opacity;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* How visible the category with dim id `id` of field `key` is now: the id form
|
|
119
|
+
* of `opacityOfValue`, and the one a label for an Other bucket must use. The
|
|
120
|
+
* least visible over `groups`. Pure, so safe in a template.
|
|
121
|
+
*/
|
|
122
|
+
opacityOfId(key, groups, id) {
|
|
123
|
+
void this.#fieldsVersion;
|
|
124
|
+
const activity = this.#fields.get(key);
|
|
125
|
+
if (!activity)
|
|
126
|
+
return 1;
|
|
127
|
+
let opacity = 1;
|
|
128
|
+
for (let i = 0; i < groups.length; i++) {
|
|
129
|
+
opacity = Math.min(opacity, activity.opacityOfId(groups[i], id));
|
|
130
|
+
}
|
|
131
|
+
return opacity;
|
|
132
|
+
}
|
|
133
|
+
/** Every field's activity, in creation order. Read every frame; do not mutate. */
|
|
134
|
+
get fields() {
|
|
135
|
+
return this.#fieldList;
|
|
136
|
+
}
|
|
35
137
|
}
|
|
@@ -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) {
|