@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
@@ -0,0 +1,128 @@
1
+ import type { ClockLoop } from '../animation';
2
+ import { ActivityTable } from '../scales/ActivityTable';
3
+ import type { FieldIdentity } from './categorical-identity';
4
+ /** What a FieldActivity needs from the page around it. */
5
+ /** One mark's Other rows for one bucket: what a click on it selects. */
6
+ export interface OtherEntry {
7
+ /** The mark's filter group, or null when it is filtered by the selection (`'filter'`). */
8
+ group: number | null;
9
+ /** One per Other row: a facet cell (null without facets) and its folded ids. */
10
+ rows: {
11
+ facetId: number | null;
12
+ ids: ArrayLike<number>;
13
+ }[];
14
+ }
15
+ /** What a click on Other does to a selection: values to set, add or remove. */
16
+ export interface OtherClick {
17
+ action: 'replace' | 'add' | 'remove';
18
+ values: unknown[];
19
+ }
20
+ export interface FieldActivityHost {
21
+ /** Drives the fades; null in unit tests and before a coordinator exists, and fades land at once. */
22
+ readonly loop: ClockLoop | null;
23
+ /** Fill `out` with the charts drawing any scale on `key`, for the fade's redraws. */
24
+ chartsFor(key: string, out: string[]): string[];
25
+ /** The field's live identity, or null; must never create one. */
26
+ identityOf(key: string): FieldIdentity | null;
27
+ }
28
+ /** A host with no page: no loop, no charts, no identity. */
29
+ export declare const DETACHED_HOST: FieldActivityHost;
30
+ /**
31
+ * Which categories of one field are selected, per filter group, and how far
32
+ * each one has faded.
33
+ *
34
+ * Activity is indexed by the field's identity id, and FieldIdentity is one per
35
+ * field, so it belongs to the field, not to any scale on it: the carrier colour
36
+ * scale and every carrier band scale read the same activity. One fade per
37
+ * filter group, indexed by the group's page-wide index, because a selection
38
+ * belongs to the group it was made in: a click in group A dims group A's marks
39
+ * and leaves group B's alone.
40
+ *
41
+ * The packer writes these tables into the field's activity block, and legends
42
+ * and band labels read them through `opacityOfValue`.
43
+ *
44
+ * IDS AND IDENTITY. Ids are only meaningful within one identity: across a
45
+ * reload the same id can name another category. So the tables follow the
46
+ * field's LIVE identity, which `reconcile` compares with the one last applied:
47
+ * a different object is a reload, and every fade is dropped and snapped to its
48
+ * selection; the same object at a higher version is a dim that grew, and the
49
+ * existing ids keep their fades while new ones start in their state.
50
+ * `reconcile` runs from the packer and from `set`, never from a reader, so
51
+ * nothing is resized or retargeted while a template renders.
52
+ */
53
+ /**
54
+ * THE FREE SLOT. Each group's table holds one slot past the field's ids: 1 while
55
+ * the group selects nothing on the field, 0 while it selects anything, faded on
56
+ * the same clock as every id. The shader reads it as the last slot of the
57
+ * group's run (identityFreeActivity): an Other bar's unselected part dims by it,
58
+ * so clearing a selection fades it back in with the other bars, rather than its
59
+ * split sliding.
60
+ */
61
+ export declare class FieldActivity {
62
+ #private;
63
+ readonly key: string;
64
+ constructor(key: string, host?: FieldActivityHost);
65
+ /**
66
+ * How many slots the tables cover: one per id of the applied identity, and
67
+ * one past them (FREE_SLOT); 0 without an identity.
68
+ */
69
+ get count(): number;
70
+ /**
71
+ * Group `index`'s selected values, or null when it selects nothing on this
72
+ * field. Reactive: a reader re-runs when the selection changes.
73
+ */
74
+ selectedValues(index: number): ReadonlySet<unknown> | null;
75
+ /** Group `index`'s fade, or null when that group never selected on this field. */
76
+ fadeOf(index: number): ActivityTable | null;
77
+ /**
78
+ * Select `values` in group `index`, or nothing for null.
79
+ *
80
+ * The table is first brought up to the live identity under the selection it
81
+ * had, so the change from that selection to this one fades. Sizing it with
82
+ * the new values instead would put every id straight into its new state: a
83
+ * group's first click, or the first one after the dim grew, would snap.
84
+ */
85
+ set(index: number, values: ReadonlySet<unknown> | null): void;
86
+ /**
87
+ * Follow the field's live identity: snap every fade on a reload, extend them
88
+ * when the dim grew. Two comparisons when nothing moved, no allocation.
89
+ * Returns whether it re-applied the groups.
90
+ */
91
+ reconcile(): boolean;
92
+ /**
93
+ * How visible a category is now in group `index`: its fade when the value
94
+ * has an id in the current identity, and otherwise simply selected or not.
95
+ * Pure, so it is safe in a template: it never reconciles, and a table that
96
+ * belongs to an identity no longer live is not trusted.
97
+ */
98
+ opacityOfValue(index: number, value: unknown, inactiveOpacity?: number): number;
99
+ /**
100
+ * How visible the category with dim id `id` is now in group `index`: its fade.
101
+ * The id form of `opacityOfValue`, and the only one that reaches a synthetic
102
+ * id (an Other bucket): a bucket has a label, not a value, and several may
103
+ * share one label. Pure, so it is safe in a template.
104
+ *
105
+ * Before the group's table covers the id, falls back to the selected values
106
+ * for a real id, and to fully visible for a bucket or an identity that is not
107
+ * live: a label is never dimmed on a guess.
108
+ */
109
+ opacityOfId(index: number, id: number, inactiveOpacity?: number): number;
110
+ /** Register `markId`'s Other rows under `otherId`; null removes them. */
111
+ setOther(otherId: number, markId: string, entry: OtherEntry | null): void;
112
+ /**
113
+ * What a click on Other `otherId` does to group `group`'s selection, given
114
+ * the values it holds now (`current`), and records it. Null when nothing is
115
+ * registered under the id (no result yet) or it holds no members.
116
+ *
117
+ * - The members: every registered row under the id, or only facet cell
118
+ * `facetId`'s when the click came from a cell.
119
+ * - Shift-click adds them, or takes back what this Other added before if it
120
+ * is still selected.
121
+ * - A plain click on an Other whose marks all filter by the selection (no
122
+ * group: `'filter'`) drills down, replacing the selection: such an Other is
123
+ * computed inside the selection, so a second click means "the rest of it".
124
+ * - Otherwise a plain click toggles: it takes back what this Other added if
125
+ * that is still selected, and replaces the selection with the members if not.
126
+ */
127
+ otherClick(group: number, otherId: number, facetId: number | null, current: ReadonlySet<unknown>, multi: boolean): OtherClick | null;
128
+ }
@@ -0,0 +1,378 @@
1
+ import { ActivityTable, DEFAULT_INACTIVE_OPACITY, selectionOpacity } from '../scales/ActivityTable';
2
+ /** A host with no page: no loop, no charts, no identity. */
3
+ export const DETACHED_HOST = {
4
+ loop: null,
5
+ chartsFor: (_key, out) => out,
6
+ identityOf: () => null
7
+ };
8
+ /** One filter group's selection on one field, and its fade. */
9
+ class GroupFade {
10
+ table;
11
+ /** The selected values, or null for none. State, for the by-value fallback. */
12
+ values = $state.raw(null);
13
+ /** Bumped on every frame the fade is driven, and once at rest. */
14
+ frame = $state(0);
15
+ constructor(host, key, charts) {
16
+ this.table = new ActivityTable(host.loop, {
17
+ charts: () => {
18
+ charts.length = 0;
19
+ return host.chartsFor(key, charts);
20
+ },
21
+ onFrame: () => this.frame++
22
+ });
23
+ }
24
+ }
25
+ /**
26
+ * Which categories of one field are selected, per filter group, and how far
27
+ * each one has faded.
28
+ *
29
+ * Activity is indexed by the field's identity id, and FieldIdentity is one per
30
+ * field, so it belongs to the field, not to any scale on it: the carrier colour
31
+ * scale and every carrier band scale read the same activity. One fade per
32
+ * filter group, indexed by the group's page-wide index, because a selection
33
+ * belongs to the group it was made in: a click in group A dims group A's marks
34
+ * and leaves group B's alone.
35
+ *
36
+ * The packer writes these tables into the field's activity block, and legends
37
+ * and band labels read them through `opacityOfValue`.
38
+ *
39
+ * IDS AND IDENTITY. Ids are only meaningful within one identity: across a
40
+ * reload the same id can name another category. So the tables follow the
41
+ * field's LIVE identity, which `reconcile` compares with the one last applied:
42
+ * a different object is a reload, and every fade is dropped and snapped to its
43
+ * selection; the same object at a higher version is a dim that grew, and the
44
+ * existing ids keep their fades while new ones start in their state.
45
+ * `reconcile` runs from the packer and from `set`, never from a reader, so
46
+ * nothing is resized or retargeted while a template renders.
47
+ */
48
+ /**
49
+ * THE FREE SLOT. Each group's table holds one slot past the field's ids: 1 while
50
+ * the group selects nothing on the field, 0 while it selects anything, faded on
51
+ * the same clock as every id. The shader reads it as the last slot of the
52
+ * group's run (identityFreeActivity): an Other bar's unselected part dims by it,
53
+ * so clearing a selection fades it back in with the other bars, rather than its
54
+ * split sliding.
55
+ */
56
+ export class FieldActivity {
57
+ key;
58
+ #host;
59
+ #groups = [];
60
+ /**
61
+ * Bumped when a group's fade is created, so a reader that found none runs
62
+ * again once it exists. Fades are created by `set`, never while rendering.
63
+ */
64
+ #groupsVersion = $state(0);
65
+ /** The identity the tables' ids belong to, or null when they have none. */
66
+ #identity = null;
67
+ #version = -1;
68
+ /** Reused id list: selections are applied on every categorical click. */
69
+ #ids = [];
70
+ constructor(key, host = DETACHED_HOST) {
71
+ this.key = key;
72
+ this.#host = host;
73
+ }
74
+ /**
75
+ * How many slots the tables cover: one per id of the applied identity, and
76
+ * one past them (FREE_SLOT); 0 without an identity.
77
+ */
78
+ get count() {
79
+ return this.#identity ? this.#identity.values.length + 1 : 0;
80
+ }
81
+ /**
82
+ * Group `index`'s selected values, or null when it selects nothing on this
83
+ * field. Reactive: a reader re-runs when the selection changes.
84
+ */
85
+ selectedValues(index) {
86
+ void this.#groupsVersion;
87
+ return this.#groups[index]?.values ?? null;
88
+ }
89
+ /** Group `index`'s fade, or null when that group never selected on this field. */
90
+ fadeOf(index) {
91
+ return this.#groups[index]?.table ?? null;
92
+ }
93
+ /**
94
+ * Select `values` in group `index`, or nothing for null.
95
+ *
96
+ * The table is first brought up to the live identity under the selection it
97
+ * had, so the change from that selection to this one fades. Sizing it with
98
+ * the new values instead would put every id straight into its new state: a
99
+ * group's first click, or the first one after the dim grew, would snap.
100
+ */
101
+ set(index, values) {
102
+ const group = this.#group(index);
103
+ this.reconcile();
104
+ if (group.table.count !== this.count)
105
+ this.#apply(group);
106
+ group.values = values;
107
+ this.#apply(group);
108
+ }
109
+ /**
110
+ * Follow the field's live identity: snap every fade on a reload, extend them
111
+ * when the dim grew. Two comparisons when nothing moved, no allocation.
112
+ * Returns whether it re-applied the groups.
113
+ */
114
+ reconcile() {
115
+ const live = this.#host.identityOf(this.key);
116
+ const usable = live && live.values.length > 0 ? live : null;
117
+ if (usable === this.#identity) {
118
+ if (!usable || usable.version === this.#version)
119
+ return false;
120
+ // The same identity, grown.
121
+ this.#version = usable.version;
122
+ for (const group of this.#groups)
123
+ this.#apply(group);
124
+ return true;
125
+ }
126
+ // Another identity, or none: the old ids mean nothing now.
127
+ this.#identity = usable;
128
+ this.#version = usable ? usable.version : -1;
129
+ for (const group of this.#groups) {
130
+ group.table.resize(0);
131
+ this.#apply(group);
132
+ }
133
+ return true;
134
+ }
135
+ /**
136
+ * How visible a category is now in group `index`: its fade when the value
137
+ * has an id in the current identity, and otherwise simply selected or not.
138
+ * Pure, so it is safe in a template: it never reconciles, and a table that
139
+ * belongs to an identity no longer live is not trusted.
140
+ */
141
+ opacityOfValue(index, value, inactiveOpacity = DEFAULT_INACTIVE_OPACITY) {
142
+ void this.#groupsVersion;
143
+ const group = this.#groups[index];
144
+ if (!group)
145
+ return 1;
146
+ // Read first, so a reader that ran before identity landed already depends
147
+ // on the frame counter for the fade that follows.
148
+ void group.frame;
149
+ const identity = this.#identity;
150
+ if (identity && identity === this.#host.identityOf(this.key)) {
151
+ const id = identity.ids.get(value);
152
+ if (id != null)
153
+ return this.opacityOfId(index, id, inactiveOpacity);
154
+ }
155
+ const values = group.values;
156
+ return values == null || values.has(value) ? 1 : inactiveOpacity;
157
+ }
158
+ /**
159
+ * How visible the category with dim id `id` is now in group `index`: its fade.
160
+ * The id form of `opacityOfValue`, and the only one that reaches a synthetic
161
+ * id (an Other bucket): a bucket has a label, not a value, and several may
162
+ * share one label. Pure, so it is safe in a template.
163
+ *
164
+ * Before the group's table covers the id, falls back to the selected values
165
+ * for a real id, and to fully visible for a bucket or an identity that is not
166
+ * live: a label is never dimmed on a guess.
167
+ */
168
+ opacityOfId(index, id, inactiveOpacity = DEFAULT_INACTIVE_OPACITY) {
169
+ void this.#groupsVersion;
170
+ const group = this.#groups[index];
171
+ if (!group)
172
+ return 1;
173
+ void group.frame;
174
+ const values = group.values;
175
+ const identity = this.#identity;
176
+ if (!identity || identity !== this.#host.identityOf(this.key))
177
+ return 1;
178
+ if (id >= 0 && id < group.table.count) {
179
+ return selectionOpacity(group.table.activityOf(id), inactiveOpacity);
180
+ }
181
+ if (values == null || identity.synthetic.has(id) || id < 0 || id >= identity.values.length) {
182
+ return 1;
183
+ }
184
+ return values.has(identity.values[id]) ? 1 : inactiveOpacity;
185
+ }
186
+ // -----------------------------------------------------------------
187
+ // Other buckets: what each one holds, for a click to select
188
+ // -----------------------------------------------------------------
189
+ /**
190
+ * Every Other bucket's members, by its id and the mark that drew it. Each
191
+ * mark registers its Other rows per result (`setOther`): the folded ids its
192
+ * query returned, per facet cell. A click on Other reads them, so what it
193
+ * selects is exactly what the bar summed.
194
+ */
195
+ #others = new Map();
196
+ /**
197
+ * What an Other click contributed to a group's selection, by `group:otherId`:
198
+ * how a second click on the same Other knows what to take back. Dropped with
199
+ * the id's last entry, so a reused id starts with no memory.
200
+ */
201
+ #otherMemory = new Map();
202
+ /** Register `markId`'s Other rows under `otherId`; null removes them. */
203
+ setOther(otherId, markId, entry) {
204
+ let byMark = this.#others.get(otherId);
205
+ const before = byMark?.get(markId)?.group;
206
+ if (entry) {
207
+ if (!byMark)
208
+ this.#others.set(otherId, (byMark = new Map()));
209
+ byMark.set(markId, entry);
210
+ }
211
+ else {
212
+ if (!byMark?.delete(markId))
213
+ return;
214
+ if (byMark.size === 0) {
215
+ this.#others.delete(otherId);
216
+ for (const memKey of [...this.#otherMemory.keys()]) {
217
+ if (memKey.endsWith(`:${otherId}`))
218
+ this.#otherMemory.delete(memKey);
219
+ }
220
+ }
221
+ }
222
+ // The bucket's members moved (a new result), or it came or went: whether it
223
+ // is lit may have changed in the group its marks dim under. A fade it does
224
+ // not change costs nothing (#retarget returns early).
225
+ this.#reapply(before);
226
+ if (entry && entry.group !== before)
227
+ this.#reapply(entry.group);
228
+ }
229
+ #reapply(index) {
230
+ if (index == null)
231
+ return;
232
+ const group = this.#groups[index];
233
+ if (group && group.values != null)
234
+ this.#apply(group);
235
+ }
236
+ /**
237
+ * What a click on Other `otherId` does to group `group`'s selection, given
238
+ * the values it holds now (`current`), and records it. Null when nothing is
239
+ * registered under the id (no result yet) or it holds no members.
240
+ *
241
+ * - The members: every registered row under the id, or only facet cell
242
+ * `facetId`'s when the click came from a cell.
243
+ * - Shift-click adds them, or takes back what this Other added before if it
244
+ * is still selected.
245
+ * - A plain click on an Other whose marks all filter by the selection (no
246
+ * group: `'filter'`) drills down, replacing the selection: such an Other is
247
+ * computed inside the selection, so a second click means "the rest of it".
248
+ * - Otherwise a plain click toggles: it takes back what this Other added if
249
+ * that is still selected, and replaces the selection with the members if not.
250
+ */
251
+ otherClick(group, otherId, facetId, current, multi) {
252
+ const entries = this.#others.get(otherId);
253
+ const identity = this.#host.identityOf(this.key);
254
+ if (!entries || entries.size === 0 || !identity)
255
+ return null;
256
+ const members = new Set();
257
+ let drill = true;
258
+ for (const entry of entries.values()) {
259
+ if (entry.group !== null)
260
+ drill = false;
261
+ for (const row of entry.rows) {
262
+ if (facetId !== null && row.facetId !== facetId)
263
+ continue;
264
+ for (let i = 0; i < row.ids.length; i++) {
265
+ const id = row.ids[i];
266
+ if (identity.synthetic.has(id))
267
+ continue;
268
+ const value = identity.values[id];
269
+ if (value !== undefined)
270
+ members.add(value);
271
+ }
272
+ }
273
+ }
274
+ if (members.size === 0)
275
+ return null;
276
+ const memKey = `${group}:${otherId}`;
277
+ const before = this.#otherMemory.get(memKey);
278
+ const stillSelected = !!before && before.size > 0 && [...before].every((v) => current.has(v));
279
+ if (stillSelected && (multi || !drill)) {
280
+ this.#otherMemory.delete(memKey);
281
+ return { action: 'remove', values: [...before] };
282
+ }
283
+ if (!multi) {
284
+ // A replace: what any other Other added is gone from the selection.
285
+ for (const key of [...this.#otherMemory.keys()]) {
286
+ if (key.startsWith(`${group}:`))
287
+ this.#otherMemory.delete(key);
288
+ }
289
+ }
290
+ this.#otherMemory.set(memKey, members);
291
+ return { action: multi ? 'add' : 'replace', values: [...members] };
292
+ }
293
+ #group(index) {
294
+ let group = this.#groups[index];
295
+ if (!group) {
296
+ // Fill any gap below it too: the packer walks the groups by index.
297
+ for (let i = this.#groups.length; i <= index; i++) {
298
+ this.#groups[i] = new GroupFade(this.#host, this.key, []);
299
+ }
300
+ group = this.#groups[index];
301
+ this.#groupsVersion++;
302
+ }
303
+ return group;
304
+ }
305
+ /**
306
+ * Point a group's table at its selection under the applied identity. A new
307
+ * id starts in its state rather than fading in, which is what keeps a
308
+ * selection made before its ids existed from fading on arrival.
309
+ */
310
+ #apply(group) {
311
+ const identity = this.#identity;
312
+ if (!identity) {
313
+ group.table.resize(0);
314
+ return;
315
+ }
316
+ // Every id, and the free slot past them: clear() sets it to 1 and
317
+ // setActive() leaves it out, so it fades to 0 while anything is selected
318
+ // and back to 1 once nothing is (see FieldActivity's FREE_SLOT note).
319
+ const count = identity.values.length + 1;
320
+ const values = group.values;
321
+ if (values == null) {
322
+ group.table.resize(count, null, true);
323
+ group.table.clear();
324
+ return;
325
+ }
326
+ const ids = this.#ids;
327
+ ids.length = 0;
328
+ for (const value of values) {
329
+ const id = identity.ids.get(value);
330
+ if (id != null)
331
+ ids.push(id);
332
+ }
333
+ this.#lightOthers(this.#groups.indexOf(group), ids, count);
334
+ group.table.resize(count, ids, true);
335
+ group.table.setActive(ids);
336
+ }
337
+ /**
338
+ * Add to `ids` (the selected ids) every Other bucket that holds one of them,
339
+ * by the members its marks in group `index` registered: a bucket is lit while
340
+ * the selection reaches into it. In the same pass as the rest of the group's
341
+ * fade, so it is one retarget per update, never two. Marks filtered by the
342
+ * selection (no group) are skipped: they never dim.
343
+ */
344
+ #lightOthers(index, ids, count) {
345
+ if (this.#others.size === 0 || index < 0)
346
+ return;
347
+ let mask = this.#selectedMask;
348
+ if (mask.length < count)
349
+ mask = this.#selectedMask = new Uint8Array(count);
350
+ const selected = ids.length;
351
+ for (let i = 0; i < selected; i++)
352
+ mask[ids[i]] = 1;
353
+ for (const [otherId, entries] of this.#others) {
354
+ if (otherId >= count)
355
+ continue;
356
+ let lit = false;
357
+ for (const entry of entries.values()) {
358
+ if (entry.group !== index)
359
+ continue;
360
+ for (const row of entry.rows) {
361
+ for (let i = 0; i < row.ids.length && !lit; i++)
362
+ lit = mask[row.ids[i]] === 1;
363
+ if (lit)
364
+ break;
365
+ }
366
+ if (lit)
367
+ break;
368
+ }
369
+ if (lit)
370
+ ids.push(otherId);
371
+ }
372
+ // Unmark only what was marked: the selected ids, not the Others pushed after.
373
+ for (let i = 0; i < selected; i++)
374
+ mask[ids[i]] = 0;
375
+ }
376
+ /** Reused by #lightOthers: 1 at each selected id, 0 elsewhere between calls. */
377
+ #selectedMask = new Uint8Array(0);
378
+ }
@@ -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.
@@ -12,16 +12,9 @@
12
12
  * objects, no event listeners.
13
13
  */
14
14
  import { CrossfilterSelection } from './CrossfilterSelection.svelte';
15
- import type { FilterExpr, FilterSchema } from './CrossfilterSelection.svelte';
15
+ import type { Clause, FilterExpr, FilterSchema, SourceRegistration } from './CrossfilterSelection.svelte';
16
16
  import type { ActiveDim, QueryEngine } from './QueryEngine';
17
- /**
18
- * Narrow protocol for scale states that support active-value projection.
19
- * Implemented by ColorScaleState and BandScaleState.
20
- */
21
- export interface ActiveValueScale {
22
- setActiveValues(values: Set<unknown>): void;
23
- clearActiveValues(): void;
24
- }
17
+ import { FieldActivity } from './FieldActivity.svelte';
25
18
  export interface FilterItem {
26
19
  sourceId: string;
27
20
  predicate: FilterExpr;
@@ -29,10 +22,23 @@ export interface FilterItem {
29
22
  schema?: FilterSchema;
30
23
  }
31
24
  /** One registration of a selection source: a brush, a legend, a Toggle. */
32
- export interface SourceOpts {
33
- /** Getter for mark IDs this registration's writes exclude (chart-level cross-filtering).
34
- * A getter rather than a static Set so it stays current if marks are added/removed. */
35
- excludeMarkIds: () => Set<string>;
25
+ export interface SourceOpts extends SourceRegistration {
26
+ /**
27
+ * The chart this registration skips: its ChartConfig, or nothing. A chart
28
+ * holding a control for a selection is not filtered by it (crossfilter), so
29
+ * its marks that do not read the field keep their data; those that do, dim.
30
+ * Opt-in: a control that wants its own chart filtered passes none.
31
+ */
32
+ chart?: object | null;
33
+ /**
34
+ * For a source that selects categories of one field: that field's identity
35
+ * key, and how to read the selected values from a write. Every clause the
36
+ * source writes carries both (`Clause.categorical`).
37
+ */
38
+ categorical?: {
39
+ key: string;
40
+ values: (value: unknown) => ReadonlySet<unknown>;
41
+ };
36
42
  /** How the source selects, read at hover time for preagg activation. */
37
43
  activeDims?: () => ActiveDim[];
38
44
  /**
@@ -41,16 +47,24 @@ export interface SourceOpts {
41
47
  * while its bins are as fine as the pixels.
42
48
  */
43
49
  view?: (out: Float64Array) => void;
44
- /** Schema metadata for UI filter pills, derived from the clause value. */
50
+ /**
51
+ * Schema metadata for UI filter pills, derived from the clause value. Run
52
+ * once per write and stored on the clause.
53
+ */
45
54
  augment?: (value: unknown) => FilterSchema | null;
46
- /** The scales to dim, and the values to dim them by, taken from the clause value. */
47
- projection?: {
48
- scales: () => ActiveValueScale[];
49
- values: (value: unknown) => Set<unknown> | null;
50
- };
55
+ /**
56
+ * The source's clause was removed by someone other than this registration:
57
+ * `clearAll`, a `clearSource` from outside (a filter pill's remove button), or
58
+ * another registration under the same id clearing it. A control that keeps what
59
+ * it drew (a brush's rectangle) resets it here, because it reads no clause. Not
60
+ * called for the registration's own `clear()` or `destroy()`, which it initiated;
61
+ * and a control that clears itself from here does not loop, since the clause is
62
+ * already gone.
63
+ */
64
+ onCleared?: () => void;
51
65
  }
52
66
  export interface SourceHandle {
53
- /** Write the clause, excluding this registration's marks. */
67
+ /** Write the source's clause. */
54
68
  update(predicate: FilterExpr, value: unknown): void;
55
69
  clear(): void;
56
70
  /** Prepare preagg tables for this source, ahead of its first use. */
@@ -77,12 +91,34 @@ export interface ToggleHandle extends Omit<SourceHandle, 'update'> {
77
91
  toggle(tuple: unknown[], multi: boolean, total?: number): void;
78
92
  /** Select exactly these tuples; empty clears. */
79
93
  set(tuples: unknown[][]): void;
94
+ /**
95
+ * A click on Other bucket `otherId` (from facet cell `facetId`, or null):
96
+ * selects, adds or takes back the categories it holds, as its field's
97
+ * registry says (FieldActivity.otherClick). A one-field toggle only: a
98
+ * bucket names no value in a tuple of several.
99
+ */
100
+ toggleOther(otherId: number, facetId: number | null, multi: boolean): void;
80
101
  }
81
102
  export declare class FilterGroup {
82
103
  #private;
83
104
  readonly selection: CrossfilterSelection;
105
+ /**
106
+ * This group's page-wide index, assigned by FilterGroupRegistry and never
107
+ * reused: a mark stores it to read this group's half of an activity block.
108
+ */
109
+ readonly index: number;
110
+ /**
111
+ * A group made outside a registry (unit tests) keeps its field activity to
112
+ * itself, with no loop: its fades land at once.
113
+ */
114
+ constructor(index?: number, activityOf?: (key: string) => FieldActivity);
115
+ /** The selection state of a categorical field, by identity key: this group's fade is `index`. */
116
+ activity(key: string): FieldActivity;
84
117
  setEngine(engine: QueryEngine): void;
85
- /** Active filter items with schema metadata, for UI consumption. */
118
+ /**
119
+ * Active filter items with schema metadata, for UI consumption. The schema
120
+ * is the clause's own, computed when it was written (see `update`).
121
+ */
86
122
  get items(): FilterItem[];
87
123
  /** Number of active filters. */
88
124
  get activeCount(): number;
@@ -90,8 +126,7 @@ export declare class FilterGroup {
90
126
  fieldSql(field: string): string | null;
91
127
  /**
92
128
  * Register a selection source. Several registrations under one id are one
93
- * source: each writes the clause with its own exclusions, and the scales the
94
- * source dims are the union of theirs.
129
+ * source and write one clause.
95
130
  */
96
131
  register(sourceId: string, opts: SourceOpts): SourceHandle;
97
132
  /**
@@ -103,16 +138,27 @@ export declare class FilterGroup {
103
138
  * as the selection. Nothing else writes those ids (`upsertManual` prefixes
104
139
  * its own with `manual:`).
105
140
  */
106
- registerToggle(fields: string | string[], opts: {
107
- excludeMarkIds: () => Set<string>;
108
- scales?: () => ActiveValueScale[];
141
+ registerToggle(fields: string | string[], opts?: {
142
+ chart?: object | null;
109
143
  }): ToggleHandle;
110
- upsertManual(key: string, predicate: FilterExpr, value: unknown, excludeMarkIds: Set<string> | (() => Set<string>), schema?: FilterSchema): void;
144
+ upsertManual(key: string, predicate: FilterExpr, value: unknown, schema?: FilterSchema): void;
145
+ /**
146
+ * Hold `clause` under a manual key, as given: the relay passes another
147
+ * group's clause on. The clause it already holds there changes nothing, so
148
+ * it neither re-projects nor re-queries: the relay runs on every change in
149
+ * its source group, on every brush frame there.
150
+ */
151
+ relayClause(key: string, clause: Clause): void;
111
152
  removeManual(key: string): void;
112
153
  /**
113
154
  * Remove a source's clause. Re-queries only when there was a clause to
114
155
  * remove: an unmount or a clear with nothing selected changes no mark's data.
156
+ *
157
+ * The source's registrations are told (`onCleared`) so a control that drew the
158
+ * selection resets it, all of them from outside, and all but `by` when a
159
+ * registration is the one clearing (it has reset itself).
115
160
  */
116
- clearSource(sourceId: string): void;
161
+ clearSource(sourceId: string, by?: SourceOpts): void;
162
+ /** Remove every clause, and tell the controls that drew them (`onCleared`). */
117
163
  clearAll(): void;
118
164
  }