@fjader/svelte 0.2.2 → 0.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (148) hide show
  1. package/README.md +2 -0
  2. package/dist/components/axes/AxisBottom.svelte +23 -6
  3. package/dist/components/axes/AxisBottom.svelte.d.ts +10 -0
  4. package/dist/components/axes/AxisLeft.svelte +77 -8
  5. package/dist/components/axes/AxisLeft.svelte.d.ts +17 -0
  6. package/dist/components/axes/BandAxisTicks.svelte +64 -25
  7. package/dist/components/axes/BandAxisTicks.svelte.d.ts +5 -5
  8. package/dist/components/axes/axis-title.d.ts +30 -0
  9. package/dist/components/axes/axis-title.js +77 -0
  10. package/dist/components/charts/ChartCell.svelte +1 -1
  11. package/dist/components/charts/ChartContainer.svelte +12 -3
  12. package/dist/components/charts/ChartContainer.svelte.d.ts +7 -0
  13. package/dist/components/guides/legend/ContinuousColorLegend.svelte +9 -2
  14. package/dist/components/guides/legend/OrdinalColorLegend.svelte +96 -35
  15. package/dist/components/marks/AreaMarkX.svelte +21 -6
  16. package/dist/components/marks/AreaMarkY.svelte +21 -6
  17. package/dist/components/marks/BarMarkX.svelte +16 -1
  18. package/dist/components/marks/BarMarkY.svelte +16 -1
  19. package/dist/components/marks/DotsMark.svelte +5 -0
  20. package/dist/components/marks/LineMark.svelte +8 -1
  21. package/dist/components/marks/ParallelCoordinatesMark.svelte +10 -0
  22. package/dist/components/marks/ParallelCoordinatesMark.svelte.d.ts +5 -0
  23. package/dist/components/marks/RectMark.svelte +5 -0
  24. package/dist/components/marks/RectXMark.svelte +5 -0
  25. package/dist/components/marks/RectYMark.svelte +5 -0
  26. package/dist/components/marks/RuleMarkX.svelte +12 -2
  27. package/dist/components/marks/RuleMarkY.svelte +12 -2
  28. package/dist/components/marks/TextMark.svelte +76 -13
  29. package/dist/components/marks/lifecycle/MarkLifecycleV2.svelte.d.ts +21 -2
  30. package/dist/components/marks/lifecycle/MarkLifecycleV2.svelte.js +300 -42
  31. package/dist/components/marks/lifecycle/domain-queries.d.ts +3 -1
  32. package/dist/components/marks/lifecycle/domain-queries.js +22 -15
  33. package/dist/components/marks/lifecycle/domain-registration.js +2 -6
  34. package/dist/components/marks/lifecycle/other-rows.svelte.d.ts +104 -0
  35. package/dist/components/marks/lifecycle/other-rows.svelte.js +193 -0
  36. package/dist/components/marks/lifecycle/other-split.d.ts +80 -0
  37. package/dist/components/marks/lifecycle/other-split.js +112 -0
  38. package/dist/components/marks/lifecycle/query-builder.d.ts +26 -1
  39. package/dist/components/marks/lifecycle/query-builder.js +25 -5
  40. package/dist/components/marks/lifecycle/transform-scale-key.d.ts +16 -0
  41. package/dist/components/marks/lifecycle/transform-scale-key.js +27 -0
  42. package/dist/components/marks/utils/area-outline.d.ts +24 -0
  43. package/dist/components/marks/utils/area-outline.js +10 -0
  44. package/dist/components/marks/utils/reactive-config.d.ts +1 -1
  45. package/dist/components/marks/utils/reactive-config.js +9 -0
  46. package/dist/components/marks/utils/tooltip-helpers.d.ts +4 -1
  47. package/dist/components/marks/utils/tooltip-helpers.js +2 -1
  48. package/dist/components/rendering/Areas.svelte +30 -6
  49. package/dist/components/rendering/Areas.svelte.d.ts +2 -0
  50. package/dist/components/rendering/BaseGPURenderer.svelte.d.ts +3 -1
  51. package/dist/components/rendering/BaseGPURenderer.svelte.js +8 -3
  52. package/dist/components/rendering/Dots.svelte +24 -3
  53. package/dist/components/rendering/Dots.svelte.d.ts +2 -0
  54. package/dist/components/rendering/GridLine.svelte +2 -1
  55. package/dist/components/rendering/Lines.svelte +49 -7
  56. package/dist/components/rendering/Lines.svelte.d.ts +9 -0
  57. package/dist/components/rendering/ParallelLines.svelte +26 -7
  58. package/dist/components/rendering/ParallelLines.svelte.d.ts +2 -0
  59. package/dist/components/rendering/Rects.svelte +129 -20
  60. package/dist/components/rendering/Rects.svelte.d.ts +9 -0
  61. package/dist/components/rendering/RuleX.svelte +20 -5
  62. package/dist/components/rendering/RuleX.svelte.d.ts +2 -0
  63. package/dist/components/rendering/RuleY.svelte +20 -5
  64. package/dist/components/rendering/RuleY.svelte.d.ts +2 -0
  65. package/dist/components/rendering/activity-reads.d.ts +111 -0
  66. package/dist/components/rendering/activity-reads.js +154 -0
  67. package/dist/components/rendering/uniform-sizes.d.ts +26 -0
  68. package/dist/components/rendering/uniform-sizes.js +28 -0
  69. package/dist/config/ChartConfig.svelte.d.ts +5 -1
  70. package/dist/gpu/ColorScaleShaderLib.d.ts +13 -1
  71. package/dist/gpu/ColorScaleShaderLib.js +56 -5
  72. package/dist/gpu/GPUCoordinator.svelte.d.ts +8 -0
  73. package/dist/gpu/GPUCoordinator.svelte.js +68 -32
  74. package/dist/gpu/MorphedChannels.d.ts +6 -32
  75. package/dist/gpu/MorphedChannels.js +45 -3
  76. package/dist/gpu/ResolveShader.d.ts +2 -2
  77. package/dist/gpu/ResolveShader.js +2 -2
  78. package/dist/gpu/ScaleTemplateSystem.d.ts +19 -0
  79. package/dist/gpu/ScaleTemplateSystem.js +111 -11
  80. package/dist/gpu/SizeScaleShaderLib.d.ts +4 -4
  81. package/dist/gpu/SizeScaleShaderLib.js +4 -4
  82. package/dist/gpu/identity-slot-packing.d.ts +157 -40
  83. package/dist/gpu/identity-slot-packing.js +262 -62
  84. package/dist/gpu/tracked-scales.d.ts +14 -0
  85. package/dist/gpu/tracked-scales.js +18 -0
  86. package/dist/index.d.ts +2 -0
  87. package/dist/index.js +4 -0
  88. package/dist/interactors/BandLabelToggle.svelte +5 -9
  89. package/dist/interactors/BandLabelToggle.svelte.d.ts +3 -3
  90. package/dist/interactors/DerivedInterval1D.svelte +26 -54
  91. package/dist/interactors/Interval1D.svelte +8 -2
  92. package/dist/interactors/Interval2D.svelte +8 -2
  93. package/dist/interactors/ParallelAxisSource.svelte +14 -5
  94. package/dist/interactors/ParallelAxisSource.svelte.d.ts +2 -1
  95. package/dist/interactors/ParallelBrushes.svelte +8 -9
  96. package/dist/interactors/ParallelBrushes.svelte.d.ts +3 -3
  97. package/dist/interactors/Toggle.svelte +48 -38
  98. package/dist/interactors/Toggle.svelte.d.ts +4 -3
  99. package/dist/interactors/Tooltip.svelte +140 -42
  100. package/dist/interactors/TooltipManager.svelte.d.ts +20 -5
  101. package/dist/interactors/TooltipManager.svelte.js +40 -10
  102. package/dist/interactors/other-tooltip.d.ts +16 -0
  103. package/dist/interactors/other-tooltip.js +22 -0
  104. package/dist/interactors/relay-clauses.d.ts +23 -0
  105. package/dist/interactors/relay-clauses.js +60 -0
  106. package/dist/query-engine/CrossfilterSelection.svelte.d.ts +88 -15
  107. package/dist/query-engine/CrossfilterSelection.svelte.js +94 -20
  108. package/dist/query-engine/FieldActivity.svelte.d.ts +128 -0
  109. package/dist/query-engine/FieldActivity.svelte.js +378 -0
  110. package/dist/query-engine/FilterGroup.svelte.d.ts +76 -30
  111. package/dist/query-engine/FilterGroup.svelte.js +223 -124
  112. package/dist/query-engine/FilterGroupRegistry.svelte.d.ts +73 -3
  113. package/dist/query-engine/FilterGroupRegistry.svelte.js +161 -12
  114. package/dist/query-engine/MarkHandle.svelte.d.ts +9 -1
  115. package/dist/query-engine/MarkHandle.svelte.js +12 -2
  116. package/dist/query-engine/QueryEngine.d.ts +52 -16
  117. package/dist/query-engine/QueryEngine.js +116 -34
  118. package/dist/query-engine/categorical-identity.d.ts +114 -27
  119. package/dist/query-engine/categorical-identity.js +225 -51
  120. package/dist/query-engine/index.d.ts +2 -1
  121. package/dist/query-engine/index.js +1 -0
  122. package/dist/query-engine/preagg/Preagg.d.ts +1 -1
  123. package/dist/query-engine/preagg/Preagg.js +11 -9
  124. package/dist/query-engine/preagg/plan.d.ts +14 -5
  125. package/dist/query-engine/preagg/plan.js +44 -18
  126. package/dist/query-engine/toggle-predicate.d.ts +21 -0
  127. package/dist/query-engine/toggle-predicate.js +41 -2
  128. package/dist/scales/ActivityTable.d.ts +103 -0
  129. package/dist/scales/ActivityTable.js +220 -0
  130. package/dist/scales/BandScaleState.svelte.d.ts +41 -15
  131. package/dist/scales/BandScaleState.svelte.js +78 -36
  132. package/dist/scales/ChartScales.svelte.d.ts +2 -2
  133. package/dist/scales/ColorScaleState.svelte.d.ts +36 -8
  134. package/dist/scales/ColorScaleState.svelte.js +118 -24
  135. package/dist/scales/DomainResolver.d.ts +2 -0
  136. package/dist/scales/DomainResolver.js +15 -8
  137. package/dist/scales/ScaleRegistry.svelte.d.ts +9 -0
  138. package/dist/scales/ScaleRegistry.svelte.js +22 -1
  139. package/dist/scales/TimeScaleState.svelte.js +7 -1
  140. package/dist/spec/index.js +1 -0
  141. package/dist/spec/types.d.ts +4 -0
  142. package/dist/theme.css +5 -4
  143. package/dist/transforms/TransformCompiler.d.ts +37 -2
  144. package/dist/transforms/TransformCompiler.js +44 -1
  145. package/dist/transforms/index.d.ts +27 -0
  146. package/dist/transforms/index.js +127 -19
  147. package/dist/types.d.ts +71 -0
  148. package/package.json +3 -2
@@ -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 scale projection (dimming unselected categories in
6
- * legends/axes).
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
- /** Registrations per source id. Several under one id are one source. */
46
- #sources = new Map();
47
- // Track which scales we projected to, so we can clear them
48
- #projectedScales = new Set();
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
- const clauses = this.selection.items;
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: each writes the clause with its own exclusions, and the scales the
84
- * source dims are the union of theirs.
125
+ * source and write one clause.
85
126
  */
86
127
  register(sourceId, opts) {
87
- let registrations = this.#sources.get(sourceId);
88
- if (!registrations) {
89
- registrations = new Set();
90
- this.#sources.set(sourceId, registrations);
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.add(opts);
135
+ const registrations = () => selection.registrationsOf(sourceId);
136
+ let registered = true;
93
137
  return {
94
138
  update: (predicate, value) => {
95
- this.selection.update(sourceId, {
139
+ const next = {
96
140
  predicate,
97
- excludeMarkIds: opts.excludeMarkIds(),
98
- value
99
- });
100
- this.#afterUpdate();
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: () => this.clearSource(sourceId),
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
- registrations.delete(opts);
106
- if (registrations.size > 0)
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
- this.#sources.delete(sourceId);
109
- this.#engine?.forgetSource(this, sourceId);
110
- this.clearSource(sourceId);
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. It activates every mark some
116
- * registration filters: a table for the source is correct for a mark whether
117
- * or not the clause currently excludes it, so only marks that every
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.#sources.get(sourceId);
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, excludedByAll: excludedByAll, dims, view });
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
- excludeMarkIds: opts.excludeMarkIds,
157
- activeDims: () => list.map((field) => ({ kind: 'value', field })),
158
- augment: () => schema,
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
- scales: opts.scales,
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, excludeMarkIds, schema) {
237
- const resolved = typeof excludeMarkIds === 'function' ? excludeMarkIds() : excludeMarkIds;
238
- this.selection.update(`manual:${key}`, {
239
- predicate,
240
- excludeMarkIds: resolved,
241
- value,
242
- schema
243
- });
244
- this.#afterUpdate();
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
- this.selection.update(`manual:${key}`, null);
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.#afterUpdate();
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.#clearAllScaleProjections();
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
- // Scale projection
379
+ // After a change
270
380
  // -----------------------------------------------------------------
271
381
  /**
272
- * After any filter update: project to scales (visual dimming) and notify
273
- * the engine to re-query affected marks.
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
- this.#projectToScales();
277
- this.#engine?.onFilterChange(this);
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
- * Project active filter values onto scales (dim unselected categories).
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
- #projectToScales() {
283
- const activeByScale = new Map();
284
- for (const [sourceId, registrations] of this.#sources) {
285
- const clause = this.selection.get(sourceId);
286
- if (!clause)
287
- continue;
288
- for (const { projection } of registrations) {
289
- if (!projection)
290
- continue;
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
- // 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();
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
- /** Get or create a FilterGroup by name. Undefined/null → "default". */
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
- /** Check if a named group exists (without creating it). */
17
- has(name: string): boolean;
37
+ /**
38
+ * The named group, or undefined if no chart has made it yet. Creates nothing.
39
+ * Reactive: a reader re-runs when the group is created.
40
+ */
41
+ find(name?: string | null): FilterGroup | undefined;
42
+ /** Whether a named group exists, without creating it. Reactive, as `find`. */
43
+ has(name?: string | null): boolean;
44
+ /**
45
+ * Whether a named group has a selection (a brush, a toggle, a legend), without
46
+ * creating it: false for a group no chart has made yet. Reactive in both the
47
+ * group appearing and its clauses changing, so `$derived(groups.isActive('brush'))`
48
+ * is safe before any chart exists.
49
+ */
50
+ isActive(name?: string | null): boolean;
51
+ /**
52
+ * The names of the groups, in creation order. Reactive: a reader re-runs when a
53
+ * group is created. A new array on each read. For app code that must reach every group the page's marks
54
+ * use, such as a clause that applies to the whole page.
55
+ */
56
+ get names(): readonly string[];
57
+ /** The groups, in the order of `names`. Reactive in the same way. */
58
+ get groups(): readonly FilterGroup[];
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
  }