@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
|
@@ -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,75 @@ 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
|
-
clear:
|
|
147
|
+
// The registration's own clear: it already reset what it drew.
|
|
148
|
+
clear: () => this.clearSource(sourceId, opts),
|
|
103
149
|
activate: () => this.#activate(sourceId),
|
|
104
150
|
destroy: () => {
|
|
105
|
-
|
|
106
|
-
|
|
151
|
+
// Once only: a second call must not remove a newer registration
|
|
152
|
+
// under the same id, or clear its clause.
|
|
153
|
+
if (!registered)
|
|
107
154
|
return;
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
155
|
+
registered = false;
|
|
156
|
+
if (registrations()?.size === 1) {
|
|
157
|
+
// The last one: drop the source's preagg tables first, so the
|
|
158
|
+
// re-query below cannot start reading one about to go. Then clear
|
|
159
|
+
// while the registration is still in place, so the removal is
|
|
160
|
+
// routed by it: the marks re-queried are those the clause filtered.
|
|
161
|
+
this.#engine?.forgetSource(this, sourceId);
|
|
162
|
+
// Quietly: the registration is going away, nothing of it to reset.
|
|
163
|
+
this.clearSource(sourceId, opts);
|
|
164
|
+
selection.removeRegistration(sourceId, opts);
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
selection.removeRegistration(sourceId, opts);
|
|
168
|
+
// Another registration keeps the clause; its chart may stop skipping it.
|
|
169
|
+
if (opts.chart && selection.isActive(sourceId)) {
|
|
170
|
+
this.#engine?.onChartRouteChange(this, opts.chart);
|
|
171
|
+
}
|
|
111
172
|
}
|
|
112
173
|
};
|
|
113
174
|
}
|
|
114
175
|
/**
|
|
115
|
-
* Hand the engine a snapshot of the source
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
* registration excludes are left out.
|
|
176
|
+
* Hand the engine a snapshot of the source: its dims, its view, and its
|
|
177
|
+
* categorical field, so activation can tell which marks it would filter
|
|
178
|
+
* before it has written a clause.
|
|
119
179
|
*/
|
|
120
180
|
#activate(sourceId) {
|
|
121
|
-
const registrations = this
|
|
181
|
+
const registrations = this.selection.registrationsOf(sourceId);
|
|
122
182
|
if (!this.#engine || !registrations?.size)
|
|
123
183
|
return;
|
|
124
|
-
let excludedByAll = null;
|
|
125
184
|
let dims = [];
|
|
126
185
|
let view;
|
|
186
|
+
let categoricalKey = null;
|
|
127
187
|
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
188
|
if (opts.activeDims && dims.length === 0)
|
|
133
189
|
dims = opts.activeDims();
|
|
134
190
|
view ??= opts.view;
|
|
191
|
+
categoricalKey ??= opts.categorical?.key ?? null;
|
|
135
192
|
}
|
|
136
|
-
this.#engine.activate(this, { sourceId,
|
|
193
|
+
this.#engine.activate(this, { sourceId, categoricalKey, dims, view });
|
|
137
194
|
}
|
|
138
195
|
// -----------------------------------------------------------------
|
|
139
196
|
// Toggles (legend/axis categorical selection)
|
|
@@ -147,22 +204,24 @@ export class FilterGroup {
|
|
|
147
204
|
* as the selection. Nothing else writes those ids (`upsertManual` prefixes
|
|
148
205
|
* its own with `manual:`).
|
|
149
206
|
*/
|
|
150
|
-
registerToggle(fields, opts) {
|
|
207
|
+
registerToggle(fields, opts = {}) {
|
|
151
208
|
const list = Array.isArray(fields) ? fields : [fields];
|
|
152
209
|
const sourceId = toggleSourceId(list);
|
|
153
210
|
const { selection } = this;
|
|
154
211
|
const schema = { kind: 'toggle', column: list[0], label: list.join(', ') };
|
|
212
|
+
// One field selects categories; a tuple does not (see Clause.categorical).
|
|
213
|
+
const categoricalKey = list.length === 1 ? (identityKeyFor(list[0])?.key ?? null) : null;
|
|
155
214
|
const source = this.register(sourceId, {
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
// Projection only makes sense for single-field toggles.
|
|
160
|
-
projection: list.length === 1 && opts.scales
|
|
215
|
+
chart: opts.chart ?? null,
|
|
216
|
+
discrete: true,
|
|
217
|
+
categorical: categoricalKey
|
|
161
218
|
? {
|
|
162
|
-
|
|
219
|
+
key: categoricalKey,
|
|
163
220
|
values: (value) => new Set(value.map((t) => t[0]))
|
|
164
221
|
}
|
|
165
|
-
: undefined
|
|
222
|
+
: undefined,
|
|
223
|
+
activeDims: () => list.map((field) => ({ kind: 'value', field })),
|
|
224
|
+
augment: () => schema
|
|
166
225
|
});
|
|
167
226
|
const tuplesOf = (clause) => clause?.value ?? [];
|
|
168
227
|
/**
|
|
@@ -170,11 +229,16 @@ export class FilterGroup {
|
|
|
170
229
|
* map's version, so a toggle with nothing selected is re-read on every one;
|
|
171
230
|
* returning the same set while the clause is unchanged (and one shared
|
|
172
231
|
* empty set without a clause) keeps that from re-rendering its readers.
|
|
232
|
+
*
|
|
233
|
+
* One field's values are the clause's own categorical set, built once per
|
|
234
|
+
* write. A tuple's keys are built here, once per clause.
|
|
173
235
|
*/
|
|
174
236
|
let last = null;
|
|
175
237
|
const keysOf = (clause) => {
|
|
176
238
|
if (!clause)
|
|
177
239
|
return NOTHING_SELECTED;
|
|
240
|
+
if (clause.categorical)
|
|
241
|
+
return clause.categorical.values;
|
|
178
242
|
if (last?.clause !== clause) {
|
|
179
243
|
last = { clause, keys: new Set(tuplesOf(clause).map(tupleKey)) };
|
|
180
244
|
}
|
|
@@ -213,6 +277,24 @@ export class FilterGroup {
|
|
|
213
277
|
write([...next.values()]);
|
|
214
278
|
},
|
|
215
279
|
set: (tuples) => write(tuples),
|
|
280
|
+
toggleOther: (otherId, facetId, multi) => {
|
|
281
|
+
if (list.length !== 1 || categoricalKey === null)
|
|
282
|
+
return;
|
|
283
|
+
const click = this.activity(categoricalKey).otherClick(this.index, otherId, facetId, keysOf(selection.get(sourceId)), multi);
|
|
284
|
+
if (!click)
|
|
285
|
+
return;
|
|
286
|
+
const next = new Map(tuplesOf(selection.get(sourceId)).map((t) => [tupleKey(t), t]));
|
|
287
|
+
if (click.action === 'replace')
|
|
288
|
+
next.clear();
|
|
289
|
+
for (const value of click.values) {
|
|
290
|
+
const tuple = [value];
|
|
291
|
+
if (click.action === 'remove')
|
|
292
|
+
next.delete(tupleKey(tuple));
|
|
293
|
+
else
|
|
294
|
+
next.set(tupleKey(tuple), tuple);
|
|
295
|
+
}
|
|
296
|
+
write([...next.values()]);
|
|
297
|
+
},
|
|
216
298
|
clear: () => write([]),
|
|
217
299
|
activate: source.activate,
|
|
218
300
|
destroy: source.destroy
|
|
@@ -233,19 +315,26 @@ export class FilterGroup {
|
|
|
233
315
|
// -----------------------------------------------------------------
|
|
234
316
|
// Manual entries (for DerivedInterval1D relay, programmatic filters)
|
|
235
317
|
// -----------------------------------------------------------------
|
|
236
|
-
upsertManual(key, predicate, value,
|
|
237
|
-
const
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
318
|
+
upsertManual(key, predicate, value, schema) {
|
|
319
|
+
const next = { predicate, value, schema, categorical: null };
|
|
320
|
+
const sourceId = `manual:${key}`;
|
|
321
|
+
this.#afterUpdate(sourceId, this.selection.update(sourceId, next), next);
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* Hold `clause` under a manual key, as given: the relay passes another
|
|
325
|
+
* group's clause on. The clause it already holds there changes nothing, so
|
|
326
|
+
* it neither re-projects nor re-queries: the relay runs on every change in
|
|
327
|
+
* its source group, on every brush frame there.
|
|
328
|
+
*/
|
|
329
|
+
relayClause(key, clause) {
|
|
330
|
+
const sourceId = `manual:${key}`;
|
|
331
|
+
if (this.selection.get(sourceId) === clause)
|
|
332
|
+
return;
|
|
333
|
+
this.#afterUpdate(sourceId, this.selection.update(sourceId, clause), clause);
|
|
245
334
|
}
|
|
246
335
|
removeManual(key) {
|
|
247
|
-
|
|
248
|
-
this.#afterUpdate();
|
|
336
|
+
const sourceId = `manual:${key}`;
|
|
337
|
+
this.#afterUpdate(sourceId, this.selection.update(sourceId, null), null);
|
|
249
338
|
}
|
|
250
339
|
// -----------------------------------------------------------------
|
|
251
340
|
// Clear
|
|
@@ -253,72 +342,82 @@ export class FilterGroup {
|
|
|
253
342
|
/**
|
|
254
343
|
* Remove a source's clause. Re-queries only when there was a clause to
|
|
255
344
|
* remove: an unmount or a clear with nothing selected changes no mark's data.
|
|
345
|
+
*
|
|
346
|
+
* The source's registrations are told (`onCleared`) so a control that drew the
|
|
347
|
+
* selection resets it, all of them from outside, and all but `by` when a
|
|
348
|
+
* registration is the one clearing (it has reset itself).
|
|
256
349
|
*/
|
|
257
|
-
clearSource(sourceId) {
|
|
350
|
+
clearSource(sourceId, by) {
|
|
258
351
|
if (!this.selection.isActive(sourceId))
|
|
259
352
|
return;
|
|
260
|
-
this.selection.update(sourceId, null);
|
|
261
|
-
this.#
|
|
353
|
+
this.#afterUpdate(sourceId, this.selection.update(sourceId, null), null);
|
|
354
|
+
this.#notifyCleared(sourceId, by);
|
|
262
355
|
}
|
|
356
|
+
/** Remove every clause, and tell the controls that drew them (`onCleared`). */
|
|
263
357
|
clearAll() {
|
|
358
|
+
const cleared = this.selection.items.map((item) => item.sourceId);
|
|
264
359
|
this.selection.clear();
|
|
265
|
-
this.#
|
|
360
|
+
for (const key of this.#selectedFields)
|
|
361
|
+
this.#activityOf(key).set(this.index, null);
|
|
362
|
+
this.#selectedFields.clear();
|
|
266
363
|
this.#engine?.onFilterChange(this);
|
|
364
|
+
for (const sourceId of cleared)
|
|
365
|
+
this.#notifyCleared(sourceId);
|
|
366
|
+
}
|
|
367
|
+
/** Tell a source's registrations, but `except`, that its clause is gone. */
|
|
368
|
+
#notifyCleared(sourceId, except) {
|
|
369
|
+
const registrations = this.selection.registrationsOf(sourceId);
|
|
370
|
+
if (!registrations)
|
|
371
|
+
return;
|
|
372
|
+
// A copy: a control resetting itself may register or destroy while this runs.
|
|
373
|
+
for (const opts of [...registrations]) {
|
|
374
|
+
if (opts !== except)
|
|
375
|
+
opts.onCleared?.();
|
|
376
|
+
}
|
|
267
377
|
}
|
|
268
378
|
// -----------------------------------------------------------------
|
|
269
|
-
//
|
|
379
|
+
// After a change
|
|
270
380
|
// -----------------------------------------------------------------
|
|
271
381
|
/**
|
|
272
|
-
* After
|
|
273
|
-
* the engine
|
|
382
|
+
* After a clause went from `prev` to `next`: update this group's selection on
|
|
383
|
+
* the field of either if it is categorical, and have the engine re-query the
|
|
384
|
+
* marks either clause applies to. A brush, which writes on every pointer
|
|
385
|
+
* move, touches no selection.
|
|
274
386
|
*/
|
|
275
|
-
#afterUpdate() {
|
|
276
|
-
|
|
277
|
-
|
|
387
|
+
#afterUpdate(sourceId, prev, next) {
|
|
388
|
+
if (prev?.categorical)
|
|
389
|
+
this.#syncActivity(prev.categorical.key);
|
|
390
|
+
if (next?.categorical && next.categorical.key !== prev?.categorical?.key) {
|
|
391
|
+
this.#syncActivity(next.categorical.key);
|
|
392
|
+
}
|
|
393
|
+
if (prev || next)
|
|
394
|
+
this.#engine?.onClauseChange(this, sourceId, prev, next);
|
|
278
395
|
}
|
|
279
396
|
/**
|
|
280
|
-
*
|
|
397
|
+
* Set this group's selection on field `key` from its clauses: the
|
|
398
|
+
* intersection of the selected values of every categorical clause on that
|
|
399
|
+
* field, because the filters AND, or nothing when there is none. Usually one
|
|
400
|
+
* clause; a native toggle and a relayed one make two. Runs on categorical
|
|
401
|
+
* clause changes only, never on a brush frame.
|
|
281
402
|
*/
|
|
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
|
-
}
|
|
403
|
+
#syncActivity(key) {
|
|
404
|
+
let active = null;
|
|
405
|
+
this.selection.forEach((clause) => {
|
|
406
|
+
const categorical = clause.categorical;
|
|
407
|
+
if (!categorical || categorical.key !== key)
|
|
408
|
+
return;
|
|
409
|
+
if (!active) {
|
|
410
|
+
active = new Set(categorical.values);
|
|
411
|
+
return;
|
|
304
412
|
}
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
this.#projectedScales = new Set(activeByScale.keys());
|
|
317
|
-
}
|
|
318
|
-
#clearAllScaleProjections() {
|
|
319
|
-
for (const scale of this.#projectedScales) {
|
|
320
|
-
scale.clearActiveValues();
|
|
321
|
-
}
|
|
322
|
-
this.#projectedScales.clear();
|
|
413
|
+
for (const value of active)
|
|
414
|
+
if (!categorical.values.has(value))
|
|
415
|
+
active.delete(value);
|
|
416
|
+
});
|
|
417
|
+
if (active)
|
|
418
|
+
this.#selectedFields.add(key);
|
|
419
|
+
else
|
|
420
|
+
this.#selectedFields.delete(key);
|
|
421
|
+
this.#activityOf(key).set(this.index, active);
|
|
323
422
|
}
|
|
324
423
|
}
|
|
@@ -4,15 +4,85 @@
|
|
|
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;
|
|
14
|
-
/**
|
|
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;
|
|
27
|
+
/**
|
|
28
|
+
* Get or create a FilterGroup by name. Undefined/null → "default".
|
|
29
|
+
*
|
|
30
|
+
* Untracked: a reader does not re-run because it called `get`. For a reactive
|
|
31
|
+
* read use `find`, `has` or `isActive`, which create nothing: a group created
|
|
32
|
+
* inside the derived that reads it is state that derived cannot track, so
|
|
33
|
+
* `$derived(get('brush').activeCount)` before any chart made the group never
|
|
34
|
+
* updates. Create groups from init, effects and handlers.
|
|
35
|
+
*/
|
|
15
36
|
get(name?: string | null): FilterGroup;
|
|
16
|
-
/**
|
|
17
|
-
|
|
37
|
+
/**
|
|
38
|
+
* The named group, or undefined if no chart has made it yet. Creates nothing.
|
|
39
|
+
* Reactive: a reader re-runs when the group is created.
|
|
40
|
+
*/
|
|
41
|
+
find(name?: string | null): FilterGroup | undefined;
|
|
42
|
+
/** Whether a named group exists, without creating it. Reactive, as `find`. */
|
|
43
|
+
has(name?: string | null): boolean;
|
|
44
|
+
/**
|
|
45
|
+
* Whether a named group has a selection (a brush, a toggle, a legend), without
|
|
46
|
+
* creating it: false for a group no chart has made yet. Reactive in both the
|
|
47
|
+
* group appearing and its clauses changing, so `$derived(groups.isActive('brush'))`
|
|
48
|
+
* is safe before any chart exists.
|
|
49
|
+
*/
|
|
50
|
+
isActive(name?: string | null): boolean;
|
|
51
|
+
/**
|
|
52
|
+
* The names of the groups, in creation order. Reactive: a reader re-runs when a
|
|
53
|
+
* group is created. A new array on each read. For app code that must reach every group the page's marks
|
|
54
|
+
* use, such as a clause that applies to the whole page.
|
|
55
|
+
*/
|
|
56
|
+
get names(): readonly string[];
|
|
57
|
+
/** The groups, in the order of `names`. Reactive in the same way. */
|
|
58
|
+
get groups(): readonly FilterGroup[];
|
|
59
|
+
/** How many groups the page has: the packer writes one activity half per group. */
|
|
60
|
+
get groupCount(): number;
|
|
61
|
+
/**
|
|
62
|
+
* The selection state of a categorical field, by identity key. Created on
|
|
63
|
+
* first ask, by a selection or by the coordinator for a field a scale draws.
|
|
64
|
+
*
|
|
65
|
+
* Never removed: a session that switches through many categorical columns
|
|
66
|
+
* keeps every field it drew, each one step of the coordinator's per-frame
|
|
67
|
+
* reconcile loop. Their buffer space is freed (the packer gives a block only
|
|
68
|
+
* to fields a scale draws), so this costs a loop step, not memory on the GPU.
|
|
69
|
+
* Dropping a field once no scale draws it and no group selects on it is a
|
|
70
|
+
* possible follow-up.
|
|
71
|
+
*/
|
|
72
|
+
field(key: string): FieldActivity;
|
|
73
|
+
/**
|
|
74
|
+
* How visible `value` of field `key` is now, for a label shown for marks of
|
|
75
|
+
* several groups: the least visible over `groups`, so it dims when any of
|
|
76
|
+
* those groups dims it. 1 for a field no group ever selected on. Pure, so
|
|
77
|
+
* safe in a template: it creates nothing and reconciles nothing.
|
|
78
|
+
*/
|
|
79
|
+
opacityOfValue(key: string, groups: readonly number[], value: unknown): number;
|
|
80
|
+
/**
|
|
81
|
+
* How visible the category with dim id `id` of field `key` is now: the id form
|
|
82
|
+
* of `opacityOfValue`, and the one a label for an Other bucket must use. The
|
|
83
|
+
* least visible over `groups`. Pure, so safe in a template.
|
|
84
|
+
*/
|
|
85
|
+
opacityOfId(key: string, groups: readonly number[], id: number): number;
|
|
86
|
+
/** Every field's activity, in creation order. Read every frame; do not mutate. */
|
|
87
|
+
get fields(): readonly FieldActivity[];
|
|
18
88
|
}
|