@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
@@ -18,12 +18,45 @@
18
18
  * on it. It is a pure function over plain arrays and maps, which is
19
19
  * what it should have been.
20
20
  *
21
- * Layout: index 0 is a permanent zero-length block, then one block per scale of
22
- * `[count, slot0, slot1, ...]`. The count header makes each block
23
- * self-describing, so the shader rejects an id past this scale's end instead of
24
- * reading into the next scale's slots — ids and the slot table can disagree
25
- * transiently while a dim appends. Budget is therefore
26
- * `capacity - 1 - (1 per scale)` slots.
21
+ * LAYOUT. Index 0 is a permanent empty block `[0, 0, 0]`. Then one SLOT
22
+ * block per scale, then one ACTIVITY block per live categorical field:
23
+ *
24
+ * scale block: [count, actRef, n, slot0 … slot(count-1)]
25
+ * field block: [group 0: act0 … act(n-1), group 1: act0 … act(n-1), …]
26
+ *
27
+ * Slots come first so that when the buffer runs out it is activity that is
28
+ * lost (a mark draws undimmed), never an axis. A field has a block while a
29
+ * scale draws it, selected on or not, and a field no chart draws any more
30
+ * frees its space. Tying the block to the selection instead saved that space
31
+ * on pages with nothing selected, but every select and clear then moved the
32
+ * block and rewrote every scale block pointing at it, slots included:
33
+ * measured at about 105000 floats uploaded per click for 20 scales on a
34
+ * 5000-category field, against 5000 when the block stays.
35
+ *
36
+ * KNOWN LIMIT: a field block that does not fit leaves a selection on that
37
+ * field doing nothing for the marks that dim by it. Routing (the mark's reads,
38
+ * see activity-reads.ts) still sends them to dim rather than filter, and their
39
+ * shader finds no activity. Routing does not ask the packer whether a field
40
+ * got a block, which would tie the query layer to the GPU layout; the packer
41
+ * warns instead, on load. It takes fields of tens of thousands of ids across
42
+ * several groups to reach. A buffer that grows on demand is the real fix.
43
+ *
44
+ * A scale block's header says how many ids its slots cover (`count`), where its
45
+ * field's activity block starts (`actRef`) and how many ids that block covers
46
+ * (`n`). The count is what makes a block self-describing, so the shader rejects
47
+ * an id past this scale's end instead of reading into the next scale's slots —
48
+ * ids and the slot table can disagree transiently while a dim appends.
49
+ *
50
+ * ACTIVITY says how selected each id is in each filter group (1 active, 0
51
+ * inactive; see FieldActivity). It belongs to the field, not to the scale:
52
+ * every scale on one field reads the same block, a mark choosing its group's
53
+ * half by its group index. So a click rewrites and resolves one half of one
54
+ * field block, however many scales draw that field. The header comes first so
55
+ * the shader finds `actRef` and `n` without reading `count`: the activity read
56
+ * is two dependent loads, as it was when the activity sat after the slots. No
57
+ * mark holds `actRef`; the packer writes it in the same pass that moves the
58
+ * field block, so it cannot go stale. A scale with no field to dim points at
59
+ * the empty block, where `n` is 0 and every id is active.
27
60
  *
28
61
  * TWO buffers share one set of offsets: `from` is where each id's current
29
62
  * transition started and `to` where it is going, and the resolve kernel writes
@@ -31,128 +64,276 @@
31
64
  * bindings rather than two runs in one buffer, so the halves line up at the
32
65
  * same base and one dispatch per block covers both.
33
66
  *
34
- * `t` is not in the buffers. It is PER SCALE — each band scale has its own
35
- * driver — and reaches the kernel as an immediate on that scale's dispatch, so
36
- * a frame where only `t` moved uploads nothing at all.
67
+ * `t` is not in the buffers. It is per scale (its data clock) and per field
68
+ * half (that group's selection clock), and reaches the kernel as an immediate
69
+ * on that range's dispatch, so a frame where only `t` moved uploads nothing.
37
70
  */
38
71
  /**
39
72
  * Floats GLOBALLY across every band and point scale on the page, headers
40
- * included.
73
+ * included, and every live field's activity.
41
74
  *
42
75
  * No longer a binding limit: the tables are storage buffers, bound only to the
43
- * resolve pass, and the render stages read one resolved storage buffer. The
44
- * number is left over from when they were 64 KB uniforms, and making the
45
- * allocation grow on demand (recreate the buffers, rebuild the bind groups) is
46
- * what would retire it.
76
+ * resolve pass, and the render stages read one resolved storage buffer. Making
77
+ * the allocation grow on demand (recreate the buffers, rebuild the bind groups)
78
+ * is what would retire it.
79
+ *
80
+ * Each scale takes `3 + count`; each live field `n × groups`. 65536 fits a
81
+ * 5000-category field selected on across 3 groups (15000) with its scales
82
+ * (about 5000 each) several times over. It costs 256 KB per buffer, 768 KB for
83
+ * the three; the resolve pass dispatches per block, sized by the block, so a
84
+ * larger buffer costs no GPU time.
47
85
  */
48
- export const IDENTITY_SLOT_CAPACITY = 16384;
86
+ export const IDENTITY_SLOT_CAPACITY = 65536;
49
87
  /**
50
- * Base of the permanent zero-length block. A scale that cannot be placed points
51
- * here, so every lookup finds count 0 and discards — which is what the capacity
52
- * warning claims happens. Pointing it at a stale base instead left the scale
53
- * rendering, from another scale's slots.
88
+ * Base of the permanent empty block `[0, 0, 0]`. A scale that cannot be placed
89
+ * points here, so every lookup finds count 0 and discards, and every activity
90
+ * read finds n 0 and is fully active. Pointing it at a stale base instead left
91
+ * the scale rendering, from another scale's slots.
54
92
  */
55
93
  export const IDENTITY_SLOT_EMPTY = 0;
94
+ /**
95
+ * The group index of a mark that reads no selection activity: identityActivity
96
+ * returns fully active for it before computing any offset, so it never indexes
97
+ * past a field block. A mark whose selectionResponse is 'filter' gets it: in
98
+ * the chart holding a selection's control it keeps its data (the chart skips
99
+ * its own selection), and this keeps it from dimming there too.
100
+ */
101
+ export const NO_GROUP = 0xffffffff;
102
+ /** Floats before a scale block's slots: count, actRef, n. */
103
+ export const IDENTITY_SLOT_HEADER = 3;
56
104
  /** A state with the permanent empty block in place. */
57
105
  export function createIdentitySlotState(capacity = IDENTITY_SLOT_CAPACITY) {
58
106
  const buffer = new Float32Array(capacity);
59
107
  const bufferTo = new Float32Array(capacity);
60
108
  buffer.fill(-1);
61
109
  bufferTo.fill(-1);
62
- // The permanent empty block: count 0, so it holds no slots.
63
- buffer[IDENTITY_SLOT_EMPTY] = 0;
64
- bufferTo[IDENTITY_SLOT_EMPTY] = 0;
110
+ // The permanent empty block: count 0, actRef 0, n 0. Never -1: u32(-1.0)
111
+ // is undefined in WGSL.
112
+ buffer.fill(0, IDENTITY_SLOT_EMPTY, IDENTITY_SLOT_EMPTY + IDENTITY_SLOT_HEADER);
113
+ bufferTo.fill(0, IDENTITY_SLOT_EMPTY, IDENTITY_SLOT_EMPTY + IDENTITY_SLOT_HEADER);
65
114
  return {
66
115
  buffer,
67
116
  bufferTo,
68
117
  offsets: new Map(),
69
118
  versions: new Map(),
119
+ counts: new Map(),
120
+ refs: new Map(),
121
+ ns: new Map(),
70
122
  resolvedT: new Map(),
71
- seen: new Set(),
123
+ fieldBases: new Map(),
124
+ fieldNs: new Map(),
125
+ fieldGroups: new Map(),
126
+ fieldVersions: new Map(),
127
+ fieldT: new Map(),
128
+ fieldSeen: new Map(),
129
+ warnedFields: new Set(),
130
+ seenPass: new Map(),
131
+ pass: 0,
72
132
  result: { uploadStart: 0, uploadEnd: 0, resolves: [], resolveCount: 0 }
73
133
  };
74
134
  }
75
135
  /**
76
- * Assign every scale a base, write the blocks that moved, and list what to
77
- * upload and what to resolve.
136
+ * Assign every field and scale a base, write the ranges that moved, and list
137
+ * what to upload and what to resolve.
138
+ *
139
+ * | Range | Rewritten when | Resolved when |
140
+ * |---|---|---|
141
+ * | A field's group half | its base, `n` or the group count moved, or that group's fade version | rewritten, or that fade's `t` moved |
142
+ * | A scale's header + slots | its base, slot version, or its field block's base or `n` moved | rewritten, or slot `t` moved |
78
143
  *
79
- * A block is REWRITTEN when its base or its version moved, and RESOLVED when it
80
- * was rewritten or its `t` moved. A parked scale is neither: it has no block,
81
- * and the empty block it points at is never written.
144
+ * A range that is rewritten must be resolved even when its `t` did not move:
145
+ * the render stages read the resolved buffer, not these. A parked scale is
146
+ * neither rewritten nor resolved: it has no block, and the empty block it
147
+ * points at is never written.
82
148
  *
83
- * Offsets are recomputed on every call because a scale appearing or
84
- * disappearing shifts every scale after it. That is a running sum over a
85
- * handful of scales, and the alternative — a free list — would have to survive
86
- * a scale changing size, which happens whenever a dim appends.
149
+ * Offsets are recomputed on every call because a block appearing or
150
+ * disappearing shifts every block after it. That is a running sum over a
151
+ * handful of blocks, and the alternative — a free list — would have to survive
152
+ * a block changing size, which happens whenever a dim appends. A field block
153
+ * that moves rewrites the headers pointing at it in the same pass.
154
+ *
155
+ * `fields` and `groups` default to none, which packs the slots alone.
87
156
  */
88
- export function packIdentitySlots(scales, state) {
89
- const { buffer, bufferTo, offsets, versions, resolvedT, result } = state;
157
+ export function packIdentitySlots(scales, state, fields = NO_FIELDS, groups = 0) {
158
+ const { buffer, bufferTo, offsets, versions, counts, refs, ns, resolvedT, result } = state;
159
+ const { fieldBases, fieldNs, fieldGroups, fieldVersions, fieldT, seenPass } = state;
160
+ const { fieldSeen, warnedFields } = state;
90
161
  const capacity = buffer.length;
91
162
  const resolves = result.resolves;
92
- // Index 0 is the permanent empty block; real blocks start after it.
93
- let offset = 1;
163
+ // The permanent empty block comes first; real blocks start after it.
164
+ let offset = IDENTITY_SLOT_EMPTY + IDENTITY_SLOT_HEADER;
94
165
  let uploadStart = capacity;
95
166
  let uploadEnd = 0;
96
167
  let resolveCount = 0;
97
- // Runs every frame: the scratch set and the result are the state's own.
98
- const seen = state.seen;
99
- seen.clear();
168
+ // Runs every frame, so nothing here may allocate on a frame where no block
169
+ // appeared: the pass stamp replaces a Set cleared every pack. Kept a small
170
+ // integer (a Smi) so storing it never boxes.
171
+ const pass = (state.pass = state.pass >= 0x3fffffff ? 1 : state.pass + 1);
172
+ let seenCount = 0;
173
+ // --- Where the scale blocks end, and which fields a scale draws ---
174
+ // The same running sum the scale loop below makes, so the field blocks can
175
+ // be placed after the scales before the scale headers are written.
176
+ let fieldOffset = offset;
177
+ for (let i = 0; i < scales.length; i++) {
178
+ const need = IDENTITY_SLOT_HEADER + scales[i].slots.count;
179
+ if (fieldOffset + need <= capacity)
180
+ fieldOffset += need;
181
+ const key = scales[i].field;
182
+ if (key)
183
+ fieldSeen.set(key, pass);
184
+ }
185
+ // --- Field blocks: one run per group, after the scales ---
186
+ for (let f = 0; f < fields.length; f++) {
187
+ const field = fields[f];
188
+ const key = field.key;
189
+ const count = field.count;
190
+ const live = fieldSeen.get(key) === pass;
191
+ const need = live ? count * groups : 0;
192
+ const fits = need > 0 && fieldOffset + need <= capacity;
193
+ const base = fits ? fieldOffset : IDENTITY_SLOT_EMPTY;
194
+ const n = fits ? count : 0;
195
+ if (fits)
196
+ fieldOffset += need;
197
+ if (need > 0 && !fits) {
198
+ // On the transition only: this runs every frame.
199
+ if (!warnedFields.has(key)) {
200
+ warnedFields.add(key);
201
+ console.warn(`[GPUCoordinator] Identity slot capacity (${capacity}) exhausted; ` +
202
+ `"${key}" (${count} ids × ${groups} groups) will not dim: a selection on it ` +
203
+ `leaves the marks that dim by it unchanged.`);
204
+ }
205
+ }
206
+ else if (fits && warnedFields.size > 0) {
207
+ warnedFields.delete(key);
208
+ }
209
+ const moved = fieldBases.get(key) !== base || fieldNs.get(key) !== n || fieldGroups.get(key) !== groups;
210
+ let written = fieldVersions.get(key);
211
+ let lastT = fieldT.get(key);
212
+ if (!written || written.length !== groups) {
213
+ // Created when a field first packs or the page gains a group: rare.
214
+ written = new Float64Array(groups).fill(NaN);
215
+ lastT = new Float64Array(groups).fill(NaN);
216
+ fieldVersions.set(key, written);
217
+ fieldT.set(key, lastT);
218
+ }
219
+ if (moved) {
220
+ fieldBases.set(key, base);
221
+ fieldNs.set(key, n);
222
+ fieldGroups.set(key, groups);
223
+ }
224
+ if (!fits)
225
+ continue;
226
+ for (let g = 0; g < groups; g++) {
227
+ const fade = field.fadeOf(g);
228
+ const version = fade ? fade.version : -1;
229
+ const start = base + n * g;
230
+ let rewritten = false;
231
+ if (moved || written[g] !== version) {
232
+ writeActivity(buffer, bufferTo, start, n, fade);
233
+ written[g] = version;
234
+ if (start < uploadStart)
235
+ uploadStart = start;
236
+ if (start + n > uploadEnd)
237
+ uploadEnd = start + n;
238
+ rewritten = true;
239
+ }
240
+ const t = fade ? fade.t : 1;
241
+ if (rewritten || lastT[g] !== t) {
242
+ lastT[g] = t;
243
+ const block = (resolves[resolveCount++] ??= { base: 0, length: 0, t: 0 });
244
+ block.base = start;
245
+ block.length = n;
246
+ block.t = t;
247
+ }
248
+ }
249
+ }
250
+ // --- Scale blocks: [count, actRef, n, slots…] ---
100
251
  for (let i = 0; i < scales.length; i++) {
101
252
  const scale = scales[i];
102
- seen.add(scale.id);
253
+ if (seenPass.get(scale.id) !== pass) {
254
+ seenPass.set(scale.id, pass);
255
+ seenCount++;
256
+ }
103
257
  const slots = scale.slots;
104
- // One extra float for the block's count header.
105
- const need = slots.count + 1;
258
+ const count = slots.count;
259
+ const need = IDENTITY_SLOT_HEADER + count;
106
260
  const fits = offset + need <= capacity;
107
261
  const base = fits ? offset : IDENTITY_SLOT_EMPTY;
108
262
  if (fits)
109
263
  offset += need;
264
+ const fieldKey = scale.field;
265
+ const ref = fieldKey ? (fieldBases.get(fieldKey) ?? IDENTITY_SLOT_EMPTY) : IDENTITY_SLOT_EMPTY;
266
+ const n = ref === IDENTITY_SLOT_EMPTY ? 0 : (fieldNs.get(fieldKey) ?? 0);
110
267
  const version = slots.version;
268
+ const dirty = offsets.get(scale.id) !== base ||
269
+ versions.get(scale.id) !== version ||
270
+ counts.get(scale.id) !== count ||
271
+ refs.get(scale.id) !== ref ||
272
+ ns.get(scale.id) !== n;
111
273
  let rewritten = false;
112
- if (offsets.get(scale.id) !== base || versions.get(scale.id) !== version) {
274
+ if (dirty) {
113
275
  if (fits) {
114
- buffer[base] = slots.count;
115
- buffer.set(slots.from, base + 1);
116
- bufferTo[base] = slots.count;
117
- bufferTo.set(slots.to, base + 1);
276
+ buffer[base] = count;
277
+ buffer[base + 1] = ref;
278
+ buffer[base + 2] = n;
279
+ buffer.set(slots.from, base + IDENTITY_SLOT_HEADER);
280
+ bufferTo[base] = count;
281
+ bufferTo[base + 1] = ref;
282
+ bufferTo[base + 2] = n;
283
+ bufferTo.set(slots.to, base + IDENTITY_SLOT_HEADER);
118
284
  if (base < uploadStart)
119
285
  uploadStart = base;
120
286
  if (base + need > uploadEnd)
121
287
  uploadEnd = base + need;
122
288
  rewritten = true;
123
289
  }
124
- else {
290
+ else if (offsets.get(scale.id) !== base || versions.get(scale.id) !== version) {
125
291
  // Warn on the transition only — the caller runs this every frame.
126
- console.warn(`[GPUCoordinator] Band slot capacity (${capacity}) exhausted; ` +
127
- `scale "${scale.id}" (${slots.count} ids) will not render.`);
292
+ console.warn(`[GPUCoordinator] Identity slot capacity (${capacity}) exhausted; ` +
293
+ `scale "${scale.id}" (${count} ids) will not render.`);
128
294
  }
129
295
  offsets.set(scale.id, base);
130
296
  versions.set(scale.id, version);
297
+ counts.set(scale.id, count);
298
+ refs.set(scale.id, ref);
299
+ ns.set(scale.id, n);
131
300
  }
132
301
  // No break: a later scale with a smaller table may still fit, and one
133
302
  // that does not is parked on the empty block rather than left holding a
134
303
  // base that now belongs to someone else.
135
304
  if (!fits)
136
305
  continue;
306
+ // Created once per scale; NaN so the first comparison always differs.
307
+ let lastT = resolvedT.get(scale.id);
308
+ if (!lastT) {
309
+ lastT = new Float64Array(1).fill(NaN);
310
+ resolvedT.set(scale.id, lastT);
311
+ }
137
312
  const t = slots.t;
138
- if (!rewritten && resolvedT.get(scale.id) === t)
139
- continue;
140
- resolvedT.set(scale.id, t);
141
- const block = (resolves[resolveCount++] ??= { base: 0, length: 0, t: 0 });
142
- block.base = base;
143
- block.length = need;
144
- block.t = t;
313
+ if (rewritten || lastT[0] !== t) {
314
+ lastT[0] = t;
315
+ const block = (resolves[resolveCount++] ??= { base: 0, length: 0, t: 0 });
316
+ block.base = base;
317
+ block.length = need;
318
+ block.t = t;
319
+ }
145
320
  }
146
321
  // Drop entries for scales that have gone away, so a re-registered id cannot
147
- // inherit a base from a previous life. `offsets` is a superset of `seen` by
148
- // construction — every scale above is written — so comparing sizes is a
149
- // sound test for set equality and not just a cheap approximation of one.
150
- if (offsets.size !== seen.size) {
322
+ // inherit a base from a previous life. `offsets` is a superset of this pass's
323
+ // scales by construction — every scale above is written — so comparing sizes
324
+ // is a sound test for set equality and not just a cheap approximation of one.
325
+ // The sweep allocates, but only on a frame where a scale went away. Fields
326
+ // are not swept: the registry keeps every field it created.
327
+ if (offsets.size !== seenCount) {
151
328
  for (const id of [...offsets.keys()]) {
152
- if (!seen.has(id)) {
329
+ if (seenPass.get(id) !== pass) {
153
330
  offsets.delete(id);
154
331
  versions.delete(id);
332
+ counts.delete(id);
333
+ refs.delete(id);
334
+ ns.delete(id);
155
335
  resolvedT.delete(id);
336
+ seenPass.delete(id);
156
337
  }
157
338
  }
158
339
  }
@@ -161,3 +342,22 @@ export function packIdentitySlots(scales, state) {
161
342
  result.resolveCount = resolveCount;
162
343
  return result;
163
344
  }
345
+ const NO_FIELDS = [];
346
+ /**
347
+ * Write one group's half of a field block: `count` floats from `start`, in
348
+ * both buffers.
349
+ *
350
+ * The length is the block's `n`, not the fade's: ids past the fade's end are
351
+ * fully active, and entries past `n` are dropped. A plain loop, so a rewrite
352
+ * allocates nothing. With no fade every id is active.
353
+ */
354
+ function writeActivity(buffer, bufferTo, start, count, activity) {
355
+ const from = activity?.from;
356
+ const to = activity?.to;
357
+ const fromLen = from ? from.length : 0;
358
+ const toLen = to ? to.length : 0;
359
+ for (let i = 0; i < count; i++) {
360
+ buffer[start + i] = i < fromLen ? from[i] : 1;
361
+ bufferTo[start + i] = i < toLen ? to[i] : 1;
362
+ }
363
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Which of the registry's scales the coordinator tracks: every scale with an
3
+ * id, keyed by it.
4
+ *
5
+ * Tracking is what puts a scale in front of the identity slot packer each
6
+ * frame, so a scale that offers `slots` but has no `id` is silently never
7
+ * packed. Ordinal colour was that case: its rank table and selection activity
8
+ * never reached the GPU, the shader read the dim id as a palette index, and
9
+ * nothing failed visibly. Pulled out of the coordinator so the rule can be
10
+ * tested without a device.
11
+ */
12
+ export declare function trackScalesById<T extends {
13
+ id?: string;
14
+ }>(scales: Iterable<T>, into: Map<string, T>): void;
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Which of the registry's scales the coordinator tracks: every scale with an
3
+ * id, keyed by it.
4
+ *
5
+ * Tracking is what puts a scale in front of the identity slot packer each
6
+ * frame, so a scale that offers `slots` but has no `id` is silently never
7
+ * packed. Ordinal colour was that case: its rank table and selection activity
8
+ * never reached the GPU, the shader read the dim id as a palette index, and
9
+ * nothing failed visibly. Pulled out of the coordinator so the rule can be
10
+ * tested without a device.
11
+ */
12
+ export function trackScalesById(scales, into) {
13
+ into.clear();
14
+ for (const scale of scales) {
15
+ if (scale?.id)
16
+ into.set(scale.id, scale);
17
+ }
18
+ }
package/dist/index.d.ts CHANGED
@@ -19,5 +19,7 @@ export * from './components';
19
19
  export * from './transforms';
20
20
  export * from './sql/bin';
21
21
  export * from './types';
22
+ export { getFilterGroups } from './query-engine';
23
+ export type { FilterGroupRegistry, FilterGroup, FilterItem } from './query-engine';
22
24
  export { Fjader, render } from './spec';
23
25
  export type { FjaderSpec, FjaderHandle, RenderOptions } from './spec';
package/dist/index.js CHANGED
@@ -17,6 +17,7 @@ export function initFjader(connection) {
17
17
  const scaleRegistry = setScaleRegistry();
18
18
  const gpuCoordinator = setGPUCoordinator({ msaaSampleCount: 4 });
19
19
  gpuCoordinator.setScaleRegistry(scaleRegistry);
20
+ gpuCoordinator.setFilterGroups(filterGroups);
20
21
  scaleRegistry.setGPUCoordinator(gpuCoordinator);
21
22
  return { queryEngine, filterGroups, scaleRegistry, gpuCoordinator };
22
23
  }
@@ -24,5 +25,8 @@ export * from './components';
24
25
  export * from './transforms';
25
26
  export * from './sql/bin';
26
27
  export * from './types';
28
+ // Selection state: the registry initFjader() returns, for code outside it (is anything
29
+ // brushed? which groups exist?). Read from the same context.
30
+ export { getFilterGroups } from './query-engine';
27
31
  // Spec-driven rendering API
28
32
  export { Fjader, render } from './spec';
@@ -1,11 +1,7 @@
1
1
  <script lang="ts">
2
2
  import { getContext, onDestroy } from 'svelte';
3
3
  import type { ChartContext } from '../types';
4
- import type {
5
- ActiveValueScale,
6
- FilterGroup,
7
- ToggleHandle
8
- } from '../query-engine/FilterGroup.svelte';
4
+ import type { FilterGroup, ToggleHandle } from '../query-engine/FilterGroup.svelte';
9
5
 
10
6
  /**
11
7
  * A Toggle's registration for one band axis field, so label clicks toggle
@@ -22,17 +18,17 @@
22
18
  interface Props {
23
19
  filterGroup: FilterGroup;
24
20
  field: string;
25
- excludeMarkIds: () => Set<string>;
26
- scales: () => ActiveValueScale[];
21
+ /** The chart its toggle skips (see Toggle). */
22
+ chart: object;
27
23
  /** Where the parent's click handler finds this field's toggle. */
28
24
  handles: Map<string, ToggleHandle>;
29
25
  }
30
26
 
31
27
  // Keyed by field in the parent, so none of these change for an instance.
32
- const { filterGroup, field, excludeMarkIds, scales, handles }: Props = $props();
28
+ const { filterGroup, field, chart: skips, handles }: Props = $props();
33
29
 
34
30
  const chart = getContext<ChartContext>('chart');
35
- const toggle = filterGroup.registerToggle(field, { excludeMarkIds, scales });
31
+ const toggle = filterGroup.registerToggle(field, { chart: skips });
36
32
 
37
33
  handles.set(field, toggle);
38
34
  chart.activators.add(toggle.activate);
@@ -1,4 +1,4 @@
1
- import type { ActiveValueScale, FilterGroup, ToggleHandle } from '../query-engine/FilterGroup.svelte';
1
+ import type { FilterGroup, ToggleHandle } from '../query-engine/FilterGroup.svelte';
2
2
  /**
3
3
  * A Toggle's registration for one band axis field, so label clicks toggle
4
4
  * the axis's own field. Renders nothing: it exists for its lifecycle.
@@ -13,8 +13,8 @@ import type { ActiveValueScale, FilterGroup, ToggleHandle } from '../query-engin
13
13
  interface Props {
14
14
  filterGroup: FilterGroup;
15
15
  field: string;
16
- excludeMarkIds: () => Set<string>;
17
- scales: () => ActiveValueScale[];
16
+ /** The chart its toggle skips (see Toggle). */
17
+ chart: object;
18
18
  /** Where the parent's click handler finds this field's toggle. */
19
19
  handles: Map<string, ToggleHandle>;
20
20
  }
@@ -15,6 +15,8 @@
15
15
  import type { ChartContext } from '../types';
16
16
  import { TimeScaleState } from '../scales/TimeScaleState.svelte';
17
17
  import type { FilterGroupRegistry } from '../query-engine/FilterGroupRegistry.svelte';
18
+ import type { Clause } from '../query-engine/CrossfilterSelection.svelte';
19
+ import { ClauseRelay } from './relay-clauses';
18
20
 
19
21
  interface Props {
20
22
  sourceSelection: string;
@@ -63,13 +65,15 @@
63
65
  // Filter Bridge: Watch source group items → compute derived extent
64
66
  // =========================================================================
65
67
 
66
- // Extract primary range from source group's active filter items
68
+ // The primary range: the source group's first range clause, found by the
69
+ // schema its brush stored when writing it.
67
70
  const primaryRange = $derived.by((): [number, number] | null => {
68
- const items = sourceGroup.items;
69
- const rangeItem = items.find((item) => item.schema?.kind === 'range');
70
- if (!rangeItem?.schema?.values?.length) return null;
71
- const values = rangeItem.schema.values as number[];
72
- return [values[0], values[1]];
71
+ let rangeClause: Clause | null = null;
72
+ sourceGroup.selection.forEach((clause) => {
73
+ if (!rangeClause && clause.schema?.kind === 'range') rangeClause = clause;
74
+ });
75
+ const values = (rangeClause as Clause | null)?.schema?.values as number[] | undefined;
76
+ return values?.length ? [values[0], values[1]] : null;
73
77
  });
74
78
 
75
79
  // Compute derived extent via Temporal calendar offset
@@ -149,69 +153,37 @@
149
153
 
150
154
  if (!ref) return;
151
155
 
152
- targetGroup.upsertManual(
153
- sourceKey,
154
- isBetween(ref, [dataMin, dataMax]),
155
- [dataMin, dataMax],
156
- new Set<string>(), // No excludeMarkIds — target group only has comparison marks
157
- {
158
- kind: 'range',
159
- column: fieldStr,
160
- label: fieldStr,
161
- values: [dataMin, dataMax]
162
- }
163
- );
156
+ targetGroup.upsertManual(sourceKey, isBetween(ref, [dataMin, dataMax]), [dataMin, dataMax], {
157
+ kind: 'range',
158
+ column: fieldStr,
159
+ label: fieldStr,
160
+ values: [dataMin, dataMax]
161
+ });
164
162
  }
165
163
 
166
164
  // =========================================================================
167
165
  // Relay non-axis clauses from source → target
168
166
  // =========================================================================
169
167
 
170
- let relayedKeys = new Set<string>();
168
+ const relay = new ClauseRelay(sourceGroup, targetGroup);
169
+ /** Does nothing: forEach(track) only subscribes the effect to every clause. */
170
+ const track = () => {};
171
171
 
172
172
  // Tracks only the source group's clauses and what decides which to relay.
173
- // The writes to the target group run untracked, as above: tracked, they
174
- // made this effect re-run itself without end once the source had a clause
175
- // to relay (a day-of-week toggle beside the brush).
173
+ // The writes to the target group run untracked: tracked, they made this
174
+ // effect re-run itself without end once the source had a clause to relay
175
+ // (a day-of-week toggle beside the brush).
176
176
  $effect(() => {
177
- const relay = relayClauses;
178
- const items = sourceGroup.items;
177
+ const enabled = relayClauses;
179
178
  const fieldStr = resolvedField ?? '';
180
- untrack(() => relayItems(relay ? items : null, fieldStr));
179
+ sourceGroup.selection.forEach(track);
180
+ untrack(() => relay.sync(enabled, fieldStr));
181
181
  });
182
182
 
183
- /** Mirror the source's clauses, but the axis range, into the target group. */
184
- function relayItems(items: typeof sourceGroup.items | null, fieldStr: string) {
185
- const currentKeys = new Set<string>();
186
- for (const item of items ?? []) {
187
- // Skip the range clause on the axis column — we replace it with the shifted version
188
- if (item.schema?.kind === 'range' && item.schema?.column === fieldStr) continue;
189
-
190
- if (item.predicate) {
191
- const key = `relay:${item.sourceId}`;
192
- currentKeys.add(key);
193
- targetGroup.upsertManual(
194
- key,
195
- item.predicate,
196
- item.value,
197
- new Set<string>(),
198
- item.schema ?? undefined
199
- );
200
- }
201
- }
202
-
203
- // Remove stale relayed clauses
204
- for (const key of relayedKeys) {
205
- if (!currentKeys.has(key)) targetGroup.removeManual(key);
206
- }
207
- relayedKeys = currentKeys;
208
- }
209
-
210
183
  onMount(() => {
211
184
  return () => {
212
185
  targetGroup.removeManual(sourceKey);
213
- for (const key of relayedKeys) targetGroup.removeManual(key);
214
- relayedKeys = new Set();
186
+ relay.clear();
215
187
  };
216
188
  });
217
189
  </script>