@fjader/svelte 0.2.2 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/axes/BandAxisTicks.svelte +63 -24
- package/dist/components/axes/BandAxisTicks.svelte.d.ts +5 -5
- package/dist/components/charts/ChartContainer.svelte +2 -1
- package/dist/components/guides/legend/ContinuousColorLegend.svelte +2 -1
- package/dist/components/guides/legend/OrdinalColorLegend.svelte +92 -34
- package/dist/components/marks/AreaMarkX.svelte +19 -6
- package/dist/components/marks/AreaMarkY.svelte +19 -6
- package/dist/components/marks/BarMarkX.svelte +3 -0
- package/dist/components/marks/BarMarkY.svelte +3 -0
- package/dist/components/marks/DotsMark.svelte +3 -0
- package/dist/components/marks/LineMark.svelte +3 -0
- package/dist/components/marks/ParallelCoordinatesMark.svelte +6 -0
- package/dist/components/marks/ParallelCoordinatesMark.svelte.d.ts +3 -0
- package/dist/components/marks/RectMark.svelte +3 -0
- package/dist/components/marks/RectXMark.svelte +3 -0
- package/dist/components/marks/RectYMark.svelte +3 -0
- package/dist/components/marks/RuleMarkX.svelte +10 -2
- package/dist/components/marks/RuleMarkY.svelte +10 -2
- package/dist/components/marks/TextMark.svelte +68 -4
- package/dist/components/marks/lifecycle/MarkLifecycleV2.svelte.d.ts +15 -1
- package/dist/components/marks/lifecycle/MarkLifecycleV2.svelte.js +116 -27
- package/dist/components/marks/lifecycle/domain-queries.js +13 -11
- package/dist/components/marks/lifecycle/transform-scale-key.d.ts +16 -0
- package/dist/components/marks/lifecycle/transform-scale-key.js +27 -0
- package/dist/components/marks/utils/area-outline.d.ts +24 -0
- package/dist/components/marks/utils/area-outline.js +10 -0
- package/dist/components/marks/utils/reactive-config.d.ts +1 -1
- package/dist/components/marks/utils/reactive-config.js +3 -0
- package/dist/components/rendering/Areas.svelte +30 -6
- package/dist/components/rendering/Areas.svelte.d.ts +2 -0
- package/dist/components/rendering/BaseGPURenderer.svelte.js +4 -1
- package/dist/components/rendering/Dots.svelte +24 -3
- package/dist/components/rendering/Dots.svelte.d.ts +2 -0
- package/dist/components/rendering/GridLine.svelte +2 -1
- package/dist/components/rendering/Lines.svelte +49 -7
- package/dist/components/rendering/Lines.svelte.d.ts +9 -0
- package/dist/components/rendering/ParallelLines.svelte +26 -7
- package/dist/components/rendering/ParallelLines.svelte.d.ts +2 -0
- package/dist/components/rendering/Rects.svelte +20 -5
- package/dist/components/rendering/Rects.svelte.d.ts +2 -0
- package/dist/components/rendering/RuleX.svelte +20 -5
- package/dist/components/rendering/RuleX.svelte.d.ts +2 -0
- package/dist/components/rendering/RuleY.svelte +20 -5
- package/dist/components/rendering/RuleY.svelte.d.ts +2 -0
- package/dist/components/rendering/activity-reads.d.ts +93 -0
- package/dist/components/rendering/activity-reads.js +135 -0
- package/dist/components/rendering/uniform-sizes.d.ts +26 -0
- package/dist/components/rendering/uniform-sizes.js +28 -0
- package/dist/config/ChartConfig.svelte.d.ts +2 -0
- package/dist/gpu/ColorScaleShaderLib.d.ts +13 -1
- package/dist/gpu/ColorScaleShaderLib.js +56 -5
- package/dist/gpu/GPUCoordinator.svelte.d.ts +8 -0
- package/dist/gpu/GPUCoordinator.svelte.js +68 -32
- package/dist/gpu/ResolveShader.d.ts +2 -2
- package/dist/gpu/ResolveShader.js +2 -2
- package/dist/gpu/ScaleTemplateSystem.d.ts +19 -0
- package/dist/gpu/ScaleTemplateSystem.js +86 -7
- package/dist/gpu/SizeScaleShaderLib.d.ts +4 -4
- package/dist/gpu/SizeScaleShaderLib.js +4 -4
- package/dist/gpu/identity-slot-packing.d.ts +157 -40
- package/dist/gpu/identity-slot-packing.js +262 -62
- package/dist/gpu/tracked-scales.d.ts +14 -0
- package/dist/gpu/tracked-scales.js +18 -0
- package/dist/index.js +1 -0
- package/dist/interactors/BandLabelToggle.svelte +5 -9
- package/dist/interactors/BandLabelToggle.svelte.d.ts +3 -3
- package/dist/interactors/DerivedInterval1D.svelte +26 -54
- package/dist/interactors/Interval1D.svelte +2 -1
- package/dist/interactors/Interval2D.svelte +2 -1
- package/dist/interactors/ParallelAxisSource.svelte +4 -4
- package/dist/interactors/ParallelAxisSource.svelte.d.ts +2 -1
- package/dist/interactors/ParallelBrushes.svelte +8 -9
- package/dist/interactors/ParallelBrushes.svelte.d.ts +3 -3
- package/dist/interactors/Toggle.svelte +17 -36
- package/dist/interactors/Toggle.svelte.d.ts +4 -3
- package/dist/interactors/Tooltip.svelte +2 -1
- package/dist/interactors/TooltipManager.svelte.js +8 -3
- package/dist/interactors/relay-clauses.d.ts +23 -0
- package/dist/interactors/relay-clauses.js +60 -0
- package/dist/query-engine/CrossfilterSelection.svelte.d.ts +88 -15
- package/dist/query-engine/CrossfilterSelection.svelte.js +94 -20
- package/dist/query-engine/FieldActivity.svelte.d.ts +79 -0
- package/dist/query-engine/FieldActivity.svelte.js +207 -0
- package/dist/query-engine/FilterGroup.svelte.d.ts +53 -29
- package/dist/query-engine/FilterGroup.svelte.js +181 -122
- package/dist/query-engine/FilterGroupRegistry.svelte.d.ts +42 -0
- package/dist/query-engine/FilterGroupRegistry.svelte.js +103 -1
- package/dist/query-engine/MarkHandle.svelte.d.ts +9 -1
- package/dist/query-engine/MarkHandle.svelte.js +12 -2
- package/dist/query-engine/QueryEngine.d.ts +52 -16
- package/dist/query-engine/QueryEngine.js +90 -28
- package/dist/query-engine/categorical-identity.d.ts +99 -27
- package/dist/query-engine/categorical-identity.js +210 -51
- package/dist/query-engine/index.d.ts +2 -1
- package/dist/query-engine/index.js +1 -0
- package/dist/query-engine/preagg/Preagg.d.ts +1 -1
- package/dist/query-engine/preagg/Preagg.js +11 -9
- package/dist/query-engine/preagg/plan.d.ts +9 -5
- package/dist/query-engine/preagg/plan.js +13 -11
- package/dist/query-engine/toggle-predicate.js +12 -1
- package/dist/scales/ActivityTable.d.ts +99 -0
- package/dist/scales/ActivityTable.js +211 -0
- package/dist/scales/BandScaleState.svelte.d.ts +28 -7
- package/dist/scales/BandScaleState.svelte.js +55 -20
- package/dist/scales/ChartScales.svelte.d.ts +2 -2
- package/dist/scales/ColorScaleState.svelte.d.ts +36 -8
- package/dist/scales/ColorScaleState.svelte.js +118 -24
- package/dist/scales/DomainResolver.d.ts +2 -0
- package/dist/scales/DomainResolver.js +15 -8
- package/dist/scales/ScaleRegistry.svelte.d.ts +9 -0
- package/dist/scales/ScaleRegistry.svelte.js +22 -1
- package/dist/scales/TimeScaleState.svelte.js +7 -1
- package/dist/spec/index.js +1 -0
- package/dist/spec/types.d.ts +2 -0
- package/dist/transforms/index.d.ts +27 -0
- package/dist/transforms/index.js +97 -3
- package/dist/types.d.ts +30 -0
- package/package.json +1 -1
|
@@ -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
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
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
|
|
35
|
-
*
|
|
36
|
-
* a frame where only `t` moved uploads nothing
|
|
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.
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
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 =
|
|
86
|
+
export const IDENTITY_SLOT_CAPACITY = 65536;
|
|
49
87
|
/**
|
|
50
|
-
* Base of the permanent
|
|
51
|
-
* here, so every lookup finds count 0 and discards
|
|
52
|
-
*
|
|
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,
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
80
|
-
*
|
|
81
|
-
*
|
|
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
|
|
84
|
-
* disappearing shifts every
|
|
85
|
-
* handful of
|
|
86
|
-
* a
|
|
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
|
-
//
|
|
93
|
-
let offset =
|
|
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
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
|
|
253
|
+
if (seenPass.get(scale.id) !== pass) {
|
|
254
|
+
seenPass.set(scale.id, pass);
|
|
255
|
+
seenCount++;
|
|
256
|
+
}
|
|
103
257
|
const slots = scale.slots;
|
|
104
|
-
|
|
105
|
-
const need =
|
|
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 (
|
|
274
|
+
if (dirty) {
|
|
113
275
|
if (fits) {
|
|
114
|
-
buffer[base] =
|
|
115
|
-
buffer
|
|
116
|
-
|
|
117
|
-
|
|
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]
|
|
127
|
-
`scale "${scale.id}" (${
|
|
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 (
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
|
148
|
-
// construction — every scale above is written — so comparing sizes
|
|
149
|
-
// sound test for set equality and not just a cheap approximation of one.
|
|
150
|
-
|
|
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 (
|
|
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.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
|
}
|
|
@@ -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
|
-
|
|
26
|
-
|
|
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,
|
|
28
|
+
const { filterGroup, field, chart: skips, handles }: Props = $props();
|
|
33
29
|
|
|
34
30
|
const chart = getContext<ChartContext>('chart');
|
|
35
|
-
const toggle = filterGroup.registerToggle(field, {
|
|
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 {
|
|
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
|
-
|
|
17
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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
|
-
|
|
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
|
|
174
|
-
//
|
|
175
|
-
//
|
|
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
|
|
178
|
-
const items = sourceGroup.items;
|
|
177
|
+
const enabled = relayClauses;
|
|
179
178
|
const fieldStr = resolvedField ?? '';
|
|
180
|
-
|
|
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
|
-
|
|
214
|
-
relayedKeys = new Set();
|
|
186
|
+
relay.clear();
|
|
215
187
|
};
|
|
216
188
|
});
|
|
217
189
|
</script>
|
|
@@ -62,7 +62,8 @@
|
|
|
62
62
|
);
|
|
63
63
|
|
|
64
64
|
const brushHandle: SourceHandle = filterGroup.register(source, {
|
|
65
|
-
|
|
65
|
+
// Its own chart is not filtered by it (crossfilter).
|
|
66
|
+
chart: chart.config,
|
|
66
67
|
activeDims: () => {
|
|
67
68
|
const scale = getScale();
|
|
68
69
|
if (!scale || !columnRef) return [];
|
|
@@ -54,7 +54,8 @@
|
|
|
54
54
|
state instanceof LinearScaleState ? state.domain.target : (scale.domain() as [number, number]);
|
|
55
55
|
|
|
56
56
|
const brushHandle: SourceHandle = filterGroup.register(source, {
|
|
57
|
-
|
|
57
|
+
// Its own chart is not filtered by it (crossfilter).
|
|
58
|
+
chart: chart.config,
|
|
58
59
|
activeDims: () => {
|
|
59
60
|
if (!xColumnRef || !yColumnRef) return [];
|
|
60
61
|
return [
|