@fjader/svelte 0.2.3 → 0.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/dist/components/axes/AxisBottom.svelte +23 -6
- package/dist/components/axes/AxisBottom.svelte.d.ts +10 -0
- package/dist/components/axes/AxisLeft.svelte +77 -8
- package/dist/components/axes/AxisLeft.svelte.d.ts +17 -0
- package/dist/components/axes/BandAxisTicks.svelte +1 -1
- package/dist/components/axes/axis-title.d.ts +30 -0
- package/dist/components/axes/axis-title.js +77 -0
- package/dist/components/charts/ChartCell.svelte +1 -1
- package/dist/components/charts/ChartContainer.svelte +10 -2
- package/dist/components/charts/ChartContainer.svelte.d.ts +7 -0
- package/dist/components/guides/legend/ContinuousColorLegend.svelte +7 -1
- package/dist/components/guides/legend/OrdinalColorLegend.svelte +8 -5
- package/dist/components/marks/AreaMarkX.svelte +2 -0
- package/dist/components/marks/AreaMarkY.svelte +2 -0
- package/dist/components/marks/BarMarkX.svelte +13 -1
- package/dist/components/marks/BarMarkY.svelte +13 -1
- package/dist/components/marks/DotsMark.svelte +2 -0
- package/dist/components/marks/LineMark.svelte +5 -1
- package/dist/components/marks/ParallelCoordinatesMark.svelte +4 -0
- package/dist/components/marks/ParallelCoordinatesMark.svelte.d.ts +2 -0
- package/dist/components/marks/RectMark.svelte +2 -0
- package/dist/components/marks/RectXMark.svelte +2 -0
- package/dist/components/marks/RectYMark.svelte +2 -0
- package/dist/components/marks/RuleMarkX.svelte +2 -0
- package/dist/components/marks/RuleMarkY.svelte +2 -0
- package/dist/components/marks/TextMark.svelte +8 -9
- package/dist/components/marks/lifecycle/MarkLifecycleV2.svelte.d.ts +6 -1
- package/dist/components/marks/lifecycle/MarkLifecycleV2.svelte.js +187 -18
- package/dist/components/marks/lifecycle/domain-queries.d.ts +3 -1
- package/dist/components/marks/lifecycle/domain-queries.js +11 -6
- package/dist/components/marks/lifecycle/domain-registration.js +2 -6
- package/dist/components/marks/lifecycle/other-rows.svelte.d.ts +104 -0
- package/dist/components/marks/lifecycle/other-rows.svelte.js +193 -0
- package/dist/components/marks/lifecycle/other-split.d.ts +80 -0
- package/dist/components/marks/lifecycle/other-split.js +112 -0
- package/dist/components/marks/lifecycle/query-builder.d.ts +26 -1
- package/dist/components/marks/lifecycle/query-builder.js +25 -5
- package/dist/components/marks/utils/reactive-config.d.ts +1 -1
- package/dist/components/marks/utils/reactive-config.js +6 -0
- package/dist/components/marks/utils/tooltip-helpers.d.ts +4 -1
- package/dist/components/marks/utils/tooltip-helpers.js +2 -1
- package/dist/components/rendering/BaseGPURenderer.svelte.d.ts +3 -1
- package/dist/components/rendering/BaseGPURenderer.svelte.js +4 -2
- package/dist/components/rendering/Rects.svelte +117 -23
- package/dist/components/rendering/Rects.svelte.d.ts +7 -0
- package/dist/components/rendering/activity-reads.d.ts +18 -0
- package/dist/components/rendering/activity-reads.js +19 -0
- package/dist/config/ChartConfig.svelte.d.ts +3 -1
- package/dist/gpu/MorphedChannels.d.ts +6 -32
- package/dist/gpu/MorphedChannels.js +45 -3
- package/dist/gpu/ScaleTemplateSystem.js +25 -4
- package/dist/index.d.ts +2 -0
- package/dist/index.js +3 -0
- package/dist/interactors/Interval1D.svelte +6 -1
- package/dist/interactors/Interval2D.svelte +6 -1
- package/dist/interactors/ParallelAxisSource.svelte +10 -1
- package/dist/interactors/Toggle.svelte +32 -3
- package/dist/interactors/Tooltip.svelte +138 -41
- package/dist/interactors/TooltipManager.svelte.d.ts +20 -5
- package/dist/interactors/TooltipManager.svelte.js +32 -7
- package/dist/interactors/other-tooltip.d.ts +16 -0
- package/dist/interactors/other-tooltip.js +22 -0
- package/dist/query-engine/FieldActivity.svelte.d.ts +50 -1
- package/dist/query-engine/FieldActivity.svelte.js +176 -5
- package/dist/query-engine/FilterGroup.svelte.d.ts +23 -1
- package/dist/query-engine/FilterGroup.svelte.js +43 -3
- package/dist/query-engine/FilterGroupRegistry.svelte.d.ts +31 -3
- package/dist/query-engine/FilterGroupRegistry.svelte.js +62 -15
- package/dist/query-engine/QueryEngine.js +26 -6
- package/dist/query-engine/categorical-identity.d.ts +15 -0
- package/dist/query-engine/categorical-identity.js +15 -0
- package/dist/query-engine/preagg/plan.d.ts +5 -0
- package/dist/query-engine/preagg/plan.js +31 -7
- package/dist/query-engine/toggle-predicate.d.ts +21 -0
- package/dist/query-engine/toggle-predicate.js +29 -1
- package/dist/scales/ActivityTable.d.ts +5 -1
- package/dist/scales/ActivityTable.js +12 -3
- package/dist/scales/BandScaleState.svelte.d.ts +13 -8
- package/dist/scales/BandScaleState.svelte.js +23 -16
- package/dist/spec/types.d.ts +2 -0
- package/dist/theme.css +5 -4
- package/dist/transforms/TransformCompiler.d.ts +37 -2
- package/dist/transforms/TransformCompiler.js +44 -1
- package/dist/transforms/index.js +32 -18
- package/dist/types.d.ts +41 -0
- package/package.json +3 -2
|
@@ -242,7 +242,9 @@ export class BaseGPURenderer {
|
|
|
242
242
|
* the band axis's scale is whose clock these channels adopt. See
|
|
243
243
|
* MorphedChannels for the fold, the snap and the clock.
|
|
244
244
|
*/
|
|
245
|
-
writeMorphed(bandAxis, bandIds, channels
|
|
245
|
+
writeMorphed(bandAxis, bandIds, channels,
|
|
246
|
+
/** Bindings that land at once rather than travel (see MorphedChannels.write). */
|
|
247
|
+
land) {
|
|
246
248
|
if (!this.#device)
|
|
247
249
|
return;
|
|
248
250
|
if (!MorphedChannels.supported) {
|
|
@@ -283,7 +285,7 @@ export class BaseGPURenderer {
|
|
|
283
285
|
}
|
|
284
286
|
}
|
|
285
287
|
this.#morphAxis = bandAxis;
|
|
286
|
-
if (this.#morphed.write(bandIds, channels) || rebind) {
|
|
288
|
+
if (this.#morphed.write(bandIds, channels, land) || rebind) {
|
|
287
289
|
this.#scheduleBindGroupUpdate();
|
|
288
290
|
}
|
|
289
291
|
else if (!this.#bindGroupUpdateScheduled) {
|
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
import { getGPUContext } from '../../gpu';
|
|
6
6
|
import { DEFAULT_FILL_COLOR } from '../../scales/color-utils';
|
|
7
7
|
import type { ChartContext, WebGPUCanvasContext } from '../../types';
|
|
8
|
-
import {
|
|
8
|
+
import type { SplitProp } from '../marks/lifecycle/other-rows.svelte';
|
|
9
|
+
import { activityReads, markActivityWGSL, otherRowActivityWGSL } from './activity-reads';
|
|
9
10
|
|
|
10
11
|
const rectRendererDefinition = {
|
|
11
12
|
x: { concept: 'extent', channels: ['x0', 'x1'] },
|
|
@@ -21,7 +22,18 @@
|
|
|
21
22
|
Y1: 3,
|
|
22
23
|
COLOR: 4,
|
|
23
24
|
ALPHA: 5,
|
|
24
|
-
CONSTANTS: 6
|
|
25
|
+
CONSTANTS: 6,
|
|
26
|
+
/**
|
|
27
|
+
* An Other bar's selected share, per row: 0 on rows that are not an Other
|
|
28
|
+
* (so the zero-filled stand-in buffer means "no split anywhere"), `1 +
|
|
29
|
+
* share` on one. Morphed with the bar's length.
|
|
30
|
+
*
|
|
31
|
+
* The vertex stage's 8th storage buffer: six channels, the identity slot
|
|
32
|
+
* table and this fill WebGPU's default maxStorageBuffersPerShaderStage. A
|
|
33
|
+
* further per-row channel needs another container (an instance vertex
|
|
34
|
+
* attribute) or a raised device limit.
|
|
35
|
+
*/
|
|
36
|
+
SHARE: 7
|
|
25
37
|
};
|
|
26
38
|
|
|
27
39
|
// Facet uses vertex buffer slot 1 (not a storage binding)
|
|
@@ -35,8 +47,9 @@
|
|
|
35
47
|
GROUP_INDEX: 12,
|
|
36
48
|
CONSTANT_COLOR: 16,
|
|
37
49
|
DX: 32,
|
|
38
|
-
DY: 36
|
|
39
|
-
|
|
50
|
+
DY: 36,
|
|
51
|
+
OTHER_COLOR_ON_BAND: 40,
|
|
52
|
+
SPLIT_ALONG_X: 44
|
|
40
53
|
};
|
|
41
54
|
|
|
42
55
|
interface Props {
|
|
@@ -46,6 +59,12 @@
|
|
|
46
59
|
y1Data: Float32Array;
|
|
47
60
|
colorValueData: Float32Array | null;
|
|
48
61
|
alphaData: Float32Array | null;
|
|
62
|
+
/**
|
|
63
|
+
* A bar's selected share per row (see BINDINGS.SHARE), or null for none: a
|
|
64
|
+
* bar mark with a topN Other bucket passes its lifecycle's `otherRows.split`.
|
|
65
|
+
* The fragment draws the bar above the share dimmed.
|
|
66
|
+
*/
|
|
67
|
+
share?: SplitProp | null;
|
|
49
68
|
facetData?: Float32Array | null;
|
|
50
69
|
dataVersion: number;
|
|
51
70
|
zIndex?: number;
|
|
@@ -77,6 +96,7 @@
|
|
|
77
96
|
y1Data,
|
|
78
97
|
colorValueData,
|
|
79
98
|
alphaData,
|
|
99
|
+
share = null,
|
|
80
100
|
facetData = null,
|
|
81
101
|
dataVersion,
|
|
82
102
|
zIndex = 0,
|
|
@@ -91,6 +111,11 @@
|
|
|
91
111
|
bandAxis = null
|
|
92
112
|
}: Props = $props();
|
|
93
113
|
|
|
114
|
+
const splitData = $derived(share?.data ?? null);
|
|
115
|
+
const splitSlides = $derived(share?.slides ?? true);
|
|
116
|
+
// Bars grouped on y run along x, so their split does too.
|
|
117
|
+
const splitAlongX = $derived(share?.axis === 'y');
|
|
118
|
+
const otherColorOnBand = $derived(share?.colorOnBand ?? false);
|
|
94
119
|
const morphX = $derived(bandAxis === 'y');
|
|
95
120
|
const morphY = $derived(bandAxis === 'x');
|
|
96
121
|
|
|
@@ -137,8 +162,8 @@
|
|
|
137
162
|
constantColor: vec4<f32>, // Used when colorMode = 0
|
|
138
163
|
dx: f32, // Horizontal pixel offset
|
|
139
164
|
dy: f32, // Vertical pixel offset
|
|
140
|
-
|
|
141
|
-
|
|
165
|
+
otherColorOnBand: u32, // 1: an Other row skips its colour activity too
|
|
166
|
+
splitAlongX: u32, // 1: an Other bar splits along x (horizontal bars)
|
|
142
167
|
};
|
|
143
168
|
@group(1) @binding(6) var<uniform> markConstants: MarkConstants;
|
|
144
169
|
|
|
@@ -146,6 +171,13 @@
|
|
|
146
171
|
@builtin(position) position: vec4<f32>,
|
|
147
172
|
@location(0) colorValue: f32, // Raw value for scale lookup
|
|
148
173
|
@location(1) alpha: f32,
|
|
174
|
+
// The Other bar's selected share, or -1 on every other row; and where this
|
|
175
|
+
// fragment sits along the bar, 0 at its base, 1 at its end.
|
|
176
|
+
@location(2) @interpolate(flat) share: f32,
|
|
177
|
+
@location(3) along: f32,
|
|
178
|
+
// An Other bar's unselected part: dimmed by the other fields' activity and
|
|
179
|
+
// by the field's free slot, so it fades as the rest of the chart does.
|
|
180
|
+
@location(4) alphaRest: f32,
|
|
149
181
|
};
|
|
150
182
|
|
|
151
183
|
@group(1) @binding(0) var<storage, read> x0Values: array<f32>;
|
|
@@ -154,6 +186,7 @@
|
|
|
154
186
|
@group(1) @binding(3) var<storage, read> y1Values: array<f32>;
|
|
155
187
|
@group(1) @binding(4) var<storage, read> colorValues: array<f32>;
|
|
156
188
|
@group(1) @binding(5) var<storage, read> alphaValues: array<f32>;
|
|
189
|
+
@group(1) @binding(7) var<storage, read> splitValues: array<f32>;
|
|
157
190
|
|
|
158
191
|
@vertex
|
|
159
192
|
fn vertexMain(
|
|
@@ -190,19 +223,47 @@
|
|
|
190
223
|
var output: VertexOutput;
|
|
191
224
|
output.position = vec4<f32>(normalizedPos.x, -normalizedPos.y, 0.0, 1.0);
|
|
192
225
|
output.colorValue = colorValue;
|
|
193
|
-
|
|
194
|
-
|
|
226
|
+
// An Other row (split > 0) is drawn in two parts, mixed in the fragment at
|
|
227
|
+
// its share. Below: as any bar, by its full activity, its own field's
|
|
228
|
+
// included, so it dims and lights with the bucket's lit state (#23). Above:
|
|
229
|
+
// by the other fields' activity and the field's free slot, so it is lit
|
|
230
|
+
// only while nothing is selected. Both fade on the selection clock, and the
|
|
231
|
+
// split only moves when the share moves between two parts that differ.
|
|
232
|
+
let split = splitValues[instanceIdx];
|
|
233
|
+
let isOther = split > 0.5;
|
|
234
|
+
output.share = select(-1.0, split - 1.0, isOther);
|
|
235
|
+
output.along = select(rectCorner.y, rectCorner.x, markConstants.splitAlongX == 1u);
|
|
236
|
+
let activity = ${markActivityWGSL(ACTIVITY_READS, {
|
|
237
|
+
color: 'markConstants.colorMode, markConstants.colorScaleIndex, colorValue',
|
|
238
|
+
x: 'x0',
|
|
239
|
+
y: 'y0',
|
|
240
|
+
group: 'markConstants.groupIndex'
|
|
241
|
+
})};
|
|
242
|
+
output.alpha = alpha * selectionDim(activity);
|
|
243
|
+
// The unselected part is drawn only on an Other row: every other bar skips
|
|
244
|
+
// its activity reads.
|
|
245
|
+
output.alphaRest = output.alpha;
|
|
246
|
+
if (isOther) {
|
|
247
|
+
let otherActivity = ${otherRowActivityWGSL(ACTIVITY_READS, {
|
|
195
248
|
color: 'markConstants.colorMode, markConstants.colorScaleIndex, colorValue',
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
249
|
+
group: 'markConstants.groupIndex',
|
|
250
|
+
skipColor: 'markConstants.otherColorOnBand == 1u'
|
|
251
|
+
})};
|
|
252
|
+
let free = select(
|
|
253
|
+
bandFreeActivityX(markConstants.groupIndex),
|
|
254
|
+
bandFreeActivityY(markConstants.groupIndex),
|
|
255
|
+
markConstants.splitAlongX == 1u
|
|
256
|
+
);
|
|
257
|
+
output.alphaRest = alpha * selectionDim(min(otherActivity, free));
|
|
258
|
+
}
|
|
201
259
|
return output;
|
|
202
260
|
}
|
|
203
261
|
|
|
204
262
|
@fragment
|
|
205
263
|
fn fragmentMain(input: VertexOutput) -> @location(0) vec4<f32> {
|
|
264
|
+
// Before any branch: a derivative must be taken in uniform control flow.
|
|
265
|
+
let aa = max(fwidth(input.along), 1e-4);
|
|
266
|
+
|
|
206
267
|
// Get color based on mode
|
|
207
268
|
var baseColor: vec4<f32>;
|
|
208
269
|
if (markConstants.colorMode == 0u) {
|
|
@@ -217,7 +278,14 @@
|
|
|
217
278
|
);
|
|
218
279
|
}
|
|
219
280
|
|
|
220
|
-
|
|
281
|
+
var finalAlpha = baseColor.a * input.alpha;
|
|
282
|
+
// An Other bar's split: its own part up to the selected share, from its
|
|
283
|
+
// base, its unselected part above, with an anti-aliased edge. At share 1
|
|
284
|
+
// the whole bar is its own part, with no half-dimmed top row.
|
|
285
|
+
if (input.share >= 0.0 && input.share < 1.0) {
|
|
286
|
+
let below = 1.0 - smoothstep(input.share - aa, input.share + aa, input.along);
|
|
287
|
+
finalAlpha = baseColor.a * mix(input.alphaRest, input.alpha, below);
|
|
288
|
+
}
|
|
221
289
|
return vec4<f32>(baseColor.rgb * finalAlpha, finalAlpha);
|
|
222
290
|
}
|
|
223
291
|
`
|
|
@@ -248,6 +316,7 @@
|
|
|
248
316
|
{ binding: BINDINGS.Y1, type: 'read-only-storage', visibility: GPUShaderStage.VERTEX },
|
|
249
317
|
{ binding: BINDINGS.COLOR, type: 'read-only-storage', visibility: GPUShaderStage.VERTEX },
|
|
250
318
|
{ binding: BINDINGS.ALPHA, type: 'read-only-storage', visibility: GPUShaderStage.VERTEX },
|
|
319
|
+
{ binding: BINDINGS.SHARE, type: 'read-only-storage', visibility: GPUShaderStage.VERTEX },
|
|
251
320
|
{
|
|
252
321
|
binding: BINDINGS.CONSTANTS,
|
|
253
322
|
type: 'uniform',
|
|
@@ -266,16 +335,40 @@
|
|
|
266
335
|
// a fraction that the slot table resolves to a DIFFERENT category.
|
|
267
336
|
$effect(() => {
|
|
268
337
|
if (!renderer?.resourcesCreated || !bandAxis) return;
|
|
338
|
+
// The Other share travels with the bar's length, on the same clock, so the
|
|
339
|
+
// split and the bar land together. A share that changes alone (a click)
|
|
340
|
+
// starts a transition of its own on that clock.
|
|
341
|
+
const split = splitData ? ([[BINDINGS.SHARE, splitData]] as const) : [];
|
|
342
|
+
const land = splitData && !splitSlides ? [BINDINGS.SHARE] : undefined;
|
|
269
343
|
if (bandAxis === 'x') {
|
|
270
|
-
renderer.writeMorphed(
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
344
|
+
renderer.writeMorphed(
|
|
345
|
+
'x',
|
|
346
|
+
x0Data,
|
|
347
|
+
[[BINDINGS.Y0, y0Data], [BINDINGS.Y1, y1Data], ...split],
|
|
348
|
+
land
|
|
349
|
+
);
|
|
274
350
|
} else {
|
|
275
|
-
renderer.writeMorphed(
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
351
|
+
renderer.writeMorphed(
|
|
352
|
+
'y',
|
|
353
|
+
y0Data,
|
|
354
|
+
[[BINDINGS.X0, x0Data], [BINDINGS.X1, x1Data], ...split],
|
|
355
|
+
land
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
});
|
|
359
|
+
// Without a band axis the share is written as is. A mark that had shares and
|
|
360
|
+
// has none now writes one array of zeros, which also drops a morphed channel:
|
|
361
|
+
// left alone, the shader would keep reading the last shares. A mark that
|
|
362
|
+
// never had any writes nothing and reads the zero-filled stand-in.
|
|
363
|
+
let splitWritten = false;
|
|
364
|
+
$effect(() => {
|
|
365
|
+
if (!renderer?.resourcesCreated) return;
|
|
366
|
+
if (splitData) {
|
|
367
|
+
if (!bandAxis) renderer.writeAttribute(BINDINGS.SHARE, splitData);
|
|
368
|
+
splitWritten = true;
|
|
369
|
+
} else if (splitWritten) {
|
|
370
|
+
renderer.writeAttribute(BINDINGS.SHARE, new Float32Array(Math.max(1, x0Data.length)));
|
|
371
|
+
splitWritten = false;
|
|
279
372
|
}
|
|
280
373
|
});
|
|
281
374
|
$effect(() => {
|
|
@@ -324,7 +417,8 @@
|
|
|
324
417
|
view.setFloat32(UNIFORM_OFFSETS.CONSTANT_COLOR + 12, constantColor[3], LE);
|
|
325
418
|
view.setFloat32(UNIFORM_OFFSETS.DX, dx, LE);
|
|
326
419
|
view.setFloat32(UNIFORM_OFFSETS.DY, dy, LE);
|
|
327
|
-
|
|
420
|
+
view.setUint32(UNIFORM_OFFSETS.OTHER_COLOR_ON_BAND, otherColorOnBand ? 1 : 0, LE);
|
|
421
|
+
view.setUint32(UNIFORM_OFFSETS.SPLIT_ALONG_X, splitAlongX ? 1 : 0, LE);
|
|
328
422
|
|
|
329
423
|
renderer.writeUniform(BINDINGS.CONSTANTS, new Uint8Array(buffer));
|
|
330
424
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { SplitProp } from '../marks/lifecycle/other-rows.svelte';
|
|
1
2
|
interface Props {
|
|
2
3
|
x0Data: Float32Array;
|
|
3
4
|
x1Data: Float32Array;
|
|
@@ -5,6 +6,12 @@ interface Props {
|
|
|
5
6
|
y1Data: Float32Array;
|
|
6
7
|
colorValueData: Float32Array | null;
|
|
7
8
|
alphaData: Float32Array | null;
|
|
9
|
+
/**
|
|
10
|
+
* A bar's selected share per row (see BINDINGS.SHARE), or null for none: a
|
|
11
|
+
* bar mark with a topN Other bucket passes its lifecycle's `otherRows.split`.
|
|
12
|
+
* The fragment draws the bar above the share dimmed.
|
|
13
|
+
*/
|
|
14
|
+
share?: SplitProp | null;
|
|
8
15
|
facetData?: Float32Array | null;
|
|
9
16
|
dataVersion: number;
|
|
10
17
|
zIndex?: number;
|
|
@@ -31,6 +31,12 @@ export interface ActivityReads {
|
|
|
31
31
|
* What a renderer reads. `hasExtent` is a rule's: RuleX with y1/y2, RuleY with
|
|
32
32
|
* x1/x2. Lines, areas and parallel coordinates dim by colour only: a line over
|
|
33
33
|
* a band axis spans its categories.
|
|
34
|
+
*
|
|
35
|
+
* Not yet supported: a line or area grouped by `z` that is not its colour
|
|
36
|
+
* field. Its series ids reach the shader, but only to break the line between
|
|
37
|
+
* series; no activity is read through them, and the series field is not among
|
|
38
|
+
* the mark's reads. So a selection on that field filters the mark in other
|
|
39
|
+
* charts and does nothing in its own, instead of dimming the other series.
|
|
34
40
|
*/
|
|
35
41
|
export declare function activityReads(renderer: ActivityRenderer, hasExtent?: boolean): ActivityReads;
|
|
36
42
|
/**
|
|
@@ -85,6 +91,18 @@ export declare function colorActivityWGSL(reads: ActivityReads, args: {
|
|
|
85
91
|
* three arguments; `x` and `y` are the band channel values; `group` the mark's
|
|
86
92
|
* group index.
|
|
87
93
|
*/
|
|
94
|
+
/**
|
|
95
|
+
* The activity of an Other row: the same terms as markActivityWGSL without the
|
|
96
|
+
* band axes', and without colour's when colour is the band's own field
|
|
97
|
+
* (`skipColor`, a WGSL bool). Those read the Other bucket's own fade, and the
|
|
98
|
+
* bar's split already shows how much of it is selected (Rects); multiplying
|
|
99
|
+
* them in as well would dim it twice. Activity on any other field stays.
|
|
100
|
+
*/
|
|
101
|
+
export declare function otherRowActivityWGSL(reads: ActivityReads, args: {
|
|
102
|
+
color: string;
|
|
103
|
+
group: string;
|
|
104
|
+
skipColor: string;
|
|
105
|
+
}): string;
|
|
88
106
|
export declare function markActivityWGSL(reads: ActivityReads, args: {
|
|
89
107
|
color: string;
|
|
90
108
|
x: string;
|
|
@@ -26,6 +26,12 @@ const COLOR_ONLY = { color: true, x: false, y: false };
|
|
|
26
26
|
* What a renderer reads. `hasExtent` is a rule's: RuleX with y1/y2, RuleY with
|
|
27
27
|
* x1/x2. Lines, areas and parallel coordinates dim by colour only: a line over
|
|
28
28
|
* a band axis spans its categories.
|
|
29
|
+
*
|
|
30
|
+
* Not yet supported: a line or area grouped by `z` that is not its colour
|
|
31
|
+
* field. Its series ids reach the shader, but only to break the line between
|
|
32
|
+
* series; no activity is read through them, and the series field is not among
|
|
33
|
+
* the mark's reads. So a selection on that field filters the mark in other
|
|
34
|
+
* charts and does nothing in its own, instead of dimming the other series.
|
|
29
35
|
*/
|
|
30
36
|
export function activityReads(renderer, hasExtent = false) {
|
|
31
37
|
switch (renderer) {
|
|
@@ -127,6 +133,19 @@ export function colorActivityWGSL(reads, args) {
|
|
|
127
133
|
* three arguments; `x` and `y` are the band channel values; `group` the mark's
|
|
128
134
|
* group index.
|
|
129
135
|
*/
|
|
136
|
+
/**
|
|
137
|
+
* The activity of an Other row: the same terms as markActivityWGSL without the
|
|
138
|
+
* band axes', and without colour's when colour is the band's own field
|
|
139
|
+
* (`skipColor`, a WGSL bool). Those read the Other bucket's own fade, and the
|
|
140
|
+
* bar's split already shows how much of it is selected (Rects); multiplying
|
|
141
|
+
* them in as well would dim it twice. Activity on any other field stays.
|
|
142
|
+
*/
|
|
143
|
+
export function otherRowActivityWGSL(reads, args) {
|
|
144
|
+
const color = reads.color
|
|
145
|
+
? `select(markColorActivity(${args.color}, ${args.group}), 1.0, ${args.skipColor})`
|
|
146
|
+
: '1.0';
|
|
147
|
+
return `markActivity(${color}, 1.0, 1.0)`;
|
|
148
|
+
}
|
|
130
149
|
export function markActivityWGSL(reads, args) {
|
|
131
150
|
const color = reads.color ? `markColorActivity(${args.color}, ${args.group})` : '1.0';
|
|
132
151
|
const x = reads.x ? `bandActivityX(${args.x}, ${args.group})` : '1.0';
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { SelectionResponse } from '../components/rendering/activity-reads';
|
|
2
|
-
import type { FieldRef, LODConfig, SortConfig, ColorScheme, CurveType, Transform } from '../types';
|
|
2
|
+
import type { FieldRef, LODConfig, SortConfig, ColorScheme, CurveType, NullsResponse, Transform } from '../types';
|
|
3
3
|
import type { MarkLODSurface } from '../lod/rung-types';
|
|
4
4
|
export interface MarkEntry {
|
|
5
5
|
readonly id: string;
|
|
@@ -25,6 +25,8 @@ export interface MarkEntry {
|
|
|
25
25
|
transforms?: Transform[];
|
|
26
26
|
selection?: string;
|
|
27
27
|
selectionResponse?: SelectionResponse;
|
|
28
|
+
contributeDomain?: boolean;
|
|
29
|
+
nulls?: NullsResponse;
|
|
28
30
|
limit?: number;
|
|
29
31
|
preferredXScaleType?: string | null;
|
|
30
32
|
preferredYScaleType?: string | null;
|
|
@@ -1,35 +1,4 @@
|
|
|
1
1
|
import { TransitionClock, type ClockLoop } from '../animation/TransitionClock';
|
|
2
|
-
/**
|
|
3
|
-
* Channels of one mark that morph between data updates rather than replacing
|
|
4
|
-
* outright.
|
|
5
|
-
*
|
|
6
|
-
* Each holds three buffers where a static channel holds one: `from` and `to` are
|
|
7
|
-
* read by a compute pass, and `resolved = mix(from, to, t)` is what the vertex
|
|
8
|
-
* stage binds — in place of the static buffer, not in addition to it. So
|
|
9
|
-
* animating a channel costs the vertex stage nothing, which is what makes it
|
|
10
|
-
* affordable on a mark already at the device's storage-buffer limit.
|
|
11
|
-
*
|
|
12
|
-
* THE FOLD. When a transition restarts, whatever is on screen becomes the new
|
|
13
|
-
* `from`. On screen is the resolved buffer as last dispatched, so the fold is a
|
|
14
|
-
* buffer copy, and it reads neither `from` nor `to` — which is what lets data
|
|
15
|
-
* land at any moment, in any order relative to anything else. Writing it as
|
|
16
|
-
* `from = mix(from, to, t)` would need the OLD `to`, making the order of two
|
|
17
|
-
* writes load-bearing and silently wrong if they ever swap.
|
|
18
|
-
*
|
|
19
|
-
* THE SNAP. Some updates have nothing to travel from: the first one, a buffer
|
|
20
|
-
* that had to be reallocated, and a row set that is not the one being animated.
|
|
21
|
-
* Those copy `to` over `from`, which makes the mix the identity for every t.
|
|
22
|
-
*
|
|
23
|
-
* THE CLOCK. A mark ADOPTS the band scale's clock when it has one, so a bar's
|
|
24
|
-
* height travels on the same number as its slot — two clocks with two velocities
|
|
25
|
-
* desynchronise on a retarget, and the bar reaches its rank before or after it
|
|
26
|
-
* finishes growing. It OWNS one otherwise. Adoption is safe across marks because
|
|
27
|
-
* a mark with no new data folds to where it already is and does not move.
|
|
28
|
-
*/
|
|
29
|
-
/**
|
|
30
|
-
* The coordinator's slice this needs. Structural, so tests can supply a fake.
|
|
31
|
-
* It is also the loop an owned clock joins — the clock does that itself.
|
|
32
|
-
*/
|
|
33
2
|
export interface MorphHost extends ClockLoop {
|
|
34
3
|
registerComputeWork(key: string, fn: (encoder: GPUCommandEncoder) => void): void;
|
|
35
4
|
removeComputeWork(key: string): void;
|
|
@@ -85,8 +54,13 @@ export declare class MorphedChannels {
|
|
|
85
54
|
*
|
|
86
55
|
* All channels in one call, so one update starts one transition. Returns
|
|
87
56
|
* whether any buffer was (re)allocated, which the caller must rebind.
|
|
57
|
+
*
|
|
58
|
+
* `land`: bindings whose new values take effect at once while the rest
|
|
59
|
+
* travel. For a channel whose change is shown some other way at that moment
|
|
60
|
+
* (an Other bar's split, when both sides of it look the same): sliding it
|
|
61
|
+
* would draw a movement nothing asked for.
|
|
88
62
|
*/
|
|
89
|
-
write(rowIds: ArrayLike<number>, channels: readonly (readonly [number, AllowSharedBufferSource])[]): boolean;
|
|
63
|
+
write(rowIds: ArrayLike<number>, channels: readonly (readonly [number, AllowSharedBufferSource])[], land?: readonly number[]): boolean;
|
|
90
64
|
/**
|
|
91
65
|
* Stop morphing a channel and release its buffers. Returns whether it was
|
|
92
66
|
* morphing, in which case the caller's binding has lost its buffer.
|
|
@@ -1,6 +1,39 @@
|
|
|
1
1
|
import { RESOLVE_IMMEDIATE_SIZE, RESOLVE_WORKGROUP, resolvePipelineFor } from './ResolveShader';
|
|
2
2
|
import { TransitionClock } from '../animation/TransitionClock';
|
|
3
3
|
import { immediatesSupported } from './immediates';
|
|
4
|
+
/**
|
|
5
|
+
* Channels of one mark that morph between data updates rather than replacing
|
|
6
|
+
* outright.
|
|
7
|
+
*
|
|
8
|
+
* Each holds three buffers where a static channel holds one: `from` and `to` are
|
|
9
|
+
* read by a compute pass, and `resolved = mix(from, to, t)` is what the vertex
|
|
10
|
+
* stage binds — in place of the static buffer, not in addition to it. So
|
|
11
|
+
* animating a channel costs the vertex stage nothing, which is what makes it
|
|
12
|
+
* affordable on a mark already at the device's storage-buffer limit.
|
|
13
|
+
*
|
|
14
|
+
* THE FOLD. When a transition restarts, whatever is on screen becomes the new
|
|
15
|
+
* `from`. On screen is the resolved buffer as last dispatched, so the fold is a
|
|
16
|
+
* buffer copy, and it reads neither `from` nor `to` — which is what lets data
|
|
17
|
+
* land at any moment, in any order relative to anything else. Writing it as
|
|
18
|
+
* `from = mix(from, to, t)` would need the OLD `to`, making the order of two
|
|
19
|
+
* writes load-bearing and silently wrong if they ever swap.
|
|
20
|
+
*
|
|
21
|
+
* THE SNAP. Some updates have nothing to travel from: the first one, a buffer
|
|
22
|
+
* that had to be reallocated, and a row set that is not the one being animated.
|
|
23
|
+
* Those copy `to` over `from`, which makes the mix the identity for every t.
|
|
24
|
+
*
|
|
25
|
+
* THE CLOCK. A mark ADOPTS the band scale's clock when it has one, so a bar's
|
|
26
|
+
* height travels on the same number as its slot — two clocks with two velocities
|
|
27
|
+
* desynchronise on a retarget, and the bar reaches its rank before or after it
|
|
28
|
+
* finishes growing. It OWNS one otherwise. Adoption is safe across marks because
|
|
29
|
+
* a mark with no new data folds to where it already is and does not move.
|
|
30
|
+
*/
|
|
31
|
+
/**
|
|
32
|
+
* The coordinator's slice this needs. Structural, so tests can supply a fake.
|
|
33
|
+
* It is also the loop an owned clock joins — the clock does that itself.
|
|
34
|
+
*/
|
|
35
|
+
/** No bindings: the default for `write`'s `land`. */
|
|
36
|
+
const NO_BINDINGS = [];
|
|
4
37
|
/**
|
|
5
38
|
* A cheap fingerprint of the band ids, per update.
|
|
6
39
|
*
|
|
@@ -87,8 +120,13 @@ export class MorphedChannels {
|
|
|
87
120
|
*
|
|
88
121
|
* All channels in one call, so one update starts one transition. Returns
|
|
89
122
|
* whether any buffer was (re)allocated, which the caller must rebind.
|
|
123
|
+
*
|
|
124
|
+
* `land`: bindings whose new values take effect at once while the rest
|
|
125
|
+
* travel. For a channel whose change is shown some other way at that moment
|
|
126
|
+
* (an Other bar's split, when both sides of it look the same): sliding it
|
|
127
|
+
* would draw a movement nothing asked for.
|
|
90
128
|
*/
|
|
91
|
-
write(rowIds, channels) {
|
|
129
|
+
write(rowIds, channels, land = NO_BINDINGS) {
|
|
92
130
|
const key = alignmentKey(rowIds);
|
|
93
131
|
let travels = this.#alignmentKey === key;
|
|
94
132
|
this.#alignmentKey = key;
|
|
@@ -100,6 +138,7 @@ export class MorphedChannels {
|
|
|
100
138
|
reallocated = true;
|
|
101
139
|
}
|
|
102
140
|
channel.count = data.byteLength >> 2;
|
|
141
|
+
channel.land = land.includes(binding);
|
|
103
142
|
this.#device.queue.writeBuffer(channel.to, 0, data);
|
|
104
143
|
}
|
|
105
144
|
// A fresh buffer holds nothing that was ever on screen, so there is
|
|
@@ -158,8 +197,10 @@ export class MorphedChannels {
|
|
|
158
197
|
return;
|
|
159
198
|
for (let i = 0; i < channels.length; i++) {
|
|
160
199
|
const channel = channels[i];
|
|
161
|
-
if (snap) {
|
|
200
|
+
if (snap || channel.land) {
|
|
201
|
+
// A landed channel: from = to, so the mix is its new value at any t.
|
|
162
202
|
encoder.copyBufferToBuffer(channel.to, 0, channel.from, 0, channel.size);
|
|
203
|
+
channel.land = false;
|
|
163
204
|
}
|
|
164
205
|
else if (this.#foldPending) {
|
|
165
206
|
// A transition interrupted mid-flight continues from where it is,
|
|
@@ -225,7 +266,8 @@ export class MorphedChannels {
|
|
|
225
266
|
resolved: make('resolved'),
|
|
226
267
|
size,
|
|
227
268
|
count: 0,
|
|
228
|
-
bindGroup: null
|
|
269
|
+
bindGroup: null,
|
|
270
|
+
land: false
|
|
229
271
|
};
|
|
230
272
|
this.#channels.set(binding, channel);
|
|
231
273
|
this.#channelList = [...this.#channels.values()];
|
|
@@ -178,10 +178,10 @@ const SCALE_FRAGMENTS = {
|
|
|
178
178
|
rangeMax
|
|
179
179
|
);
|
|
180
180
|
|
|
181
|
-
// Return center of band
|
|
182
|
-
//
|
|
183
|
-
//
|
|
184
|
-
|
|
181
|
+
// Return center of band, slot 0 at the start of the range on both
|
|
182
|
+
// axes: the left on x, the top on y, so a y band axis reads top-down
|
|
183
|
+
// like a list and its exit (one past the last slot) is the list's end.
|
|
184
|
+
return rangeMin + bandLayout.x + bandLayout.y * 0.5;
|
|
185
185
|
}
|
|
186
186
|
`,
|
|
187
187
|
bufferLayout: {
|
|
@@ -465,6 +465,9 @@ export class ScaleTemplateSystem {
|
|
|
465
465
|
if (type !== 'band' && type !== 'point') {
|
|
466
466
|
return `fn bandActivity${axis}(value: f32, group: u32) -> f32 {
|
|
467
467
|
return 1.0;
|
|
468
|
+
}
|
|
469
|
+
fn bandFreeActivity${axis}(group: u32) -> f32 {
|
|
470
|
+
return 1.0;
|
|
468
471
|
}`;
|
|
469
472
|
}
|
|
470
473
|
return `fn bandActivity${axis}(value: f32, group: u32) -> f32 {
|
|
@@ -472,6 +475,9 @@ export class ScaleTemplateSystem {
|
|
|
472
475
|
return 1.0;
|
|
473
476
|
}
|
|
474
477
|
return identityActivity(u32(chartScale${axis}().p4), group, u32(value));
|
|
478
|
+
}
|
|
479
|
+
fn bandFreeActivity${axis}(group: u32) -> f32 {
|
|
480
|
+
return identityFreeActivity(u32(chartScale${axis}().p4), group);
|
|
475
481
|
}`;
|
|
476
482
|
};
|
|
477
483
|
return `
|
|
@@ -554,6 +560,21 @@ export class ScaleTemplateSystem {
|
|
|
554
560
|
}
|
|
555
561
|
return identitySlots[actRef + n * group + id];
|
|
556
562
|
}
|
|
563
|
+
|
|
564
|
+
fn identityFreeActivity(base: u32, group: u32) -> f32 {
|
|
565
|
+
// The last slot of the group's run, past every id: 1 while the group
|
|
566
|
+
// selects nothing on the field, 0 while it selects anything, at the
|
|
567
|
+
// selection clock (FieldActivity's FREE SLOT). 1 without a block.
|
|
568
|
+
if (group == ${NO_GROUP}u) {
|
|
569
|
+
return 1.0;
|
|
570
|
+
}
|
|
571
|
+
let actRef = u32(identitySlots[base + 1u]);
|
|
572
|
+
let n = u32(identitySlots[base + 2u]);
|
|
573
|
+
if (n == 0u) {
|
|
574
|
+
return 1.0;
|
|
575
|
+
}
|
|
576
|
+
return identitySlots[actRef + n * group + n - 1u];
|
|
577
|
+
}
|
|
557
578
|
`;
|
|
558
579
|
}
|
|
559
580
|
_buildBandHelpers(scaleTypes) {
|
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
|
@@ -25,5 +25,8 @@ export * from './components';
|
|
|
25
25
|
export * from './transforms';
|
|
26
26
|
export * from './sql/bin';
|
|
27
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';
|
|
28
31
|
// Spec-driven rendering API
|
|
29
32
|
export { Fjader, render } from './spec';
|
|
@@ -97,7 +97,12 @@
|
|
|
97
97
|
column: typeof resolvedField === 'string' ? resolvedField : (resolvedField?.toString() ?? ''),
|
|
98
98
|
label: typeof resolvedField === 'string' ? resolvedField : resolvedField?.toString(),
|
|
99
99
|
values: value ? [(value as number[])[0], (value as number[])[1]] : []
|
|
100
|
-
})
|
|
100
|
+
}),
|
|
101
|
+
// The clause was removed from outside (clearAll, a filter pill): take the
|
|
102
|
+
// rectangle down too, which only this control holds.
|
|
103
|
+
onCleared: () => {
|
|
104
|
+
if (brush.active) clearBrush();
|
|
105
|
+
}
|
|
101
106
|
});
|
|
102
107
|
|
|
103
108
|
// Pointer state (reused to avoid GC)
|
|
@@ -97,7 +97,12 @@
|
|
|
97
97
|
yColumn: resolvedYField,
|
|
98
98
|
xValues: (value as unknown[][])?.[0],
|
|
99
99
|
yValues: (value as unknown[][])?.[1]
|
|
100
|
-
})
|
|
100
|
+
}),
|
|
101
|
+
// The clause was removed from outside (clearAll, a filter pill): take the
|
|
102
|
+
// rectangle down too, which only this control holds.
|
|
103
|
+
onCleared: () => {
|
|
104
|
+
if (brush.active) clearBrush();
|
|
105
|
+
}
|
|
101
106
|
});
|
|
102
107
|
|
|
103
108
|
// Pointer state (reused to avoid GC)
|
|
@@ -83,7 +83,16 @@
|
|
|
83
83
|
column,
|
|
84
84
|
label: column,
|
|
85
85
|
values: value ? [(value as number[])[0], (value as number[])[1]] : []
|
|
86
|
-
})
|
|
86
|
+
}),
|
|
87
|
+
// The clause was removed from outside (clearAll, a filter pill): take this
|
|
88
|
+
// axis's rectangle down too, which only ParallelBrushes' state holds.
|
|
89
|
+
onCleared: () => {
|
|
90
|
+
const axis = axes.get(column);
|
|
91
|
+
if (!axis) return;
|
|
92
|
+
axis.brush.clear();
|
|
93
|
+
axis.lastEmitted.min = null;
|
|
94
|
+
axis.lastEmitted.max = null;
|
|
95
|
+
}
|
|
87
96
|
});
|
|
88
97
|
|
|
89
98
|
axes.set(column, { handle, brush: new BrushState1D(), lastEmitted: { min: null, max: null } });
|