@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.
Files changed (87) 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 +1 -1
  7. package/dist/components/axes/axis-title.d.ts +30 -0
  8. package/dist/components/axes/axis-title.js +77 -0
  9. package/dist/components/charts/ChartCell.svelte +1 -1
  10. package/dist/components/charts/ChartContainer.svelte +10 -2
  11. package/dist/components/charts/ChartContainer.svelte.d.ts +7 -0
  12. package/dist/components/guides/legend/ContinuousColorLegend.svelte +7 -1
  13. package/dist/components/guides/legend/OrdinalColorLegend.svelte +8 -5
  14. package/dist/components/marks/AreaMarkX.svelte +2 -0
  15. package/dist/components/marks/AreaMarkY.svelte +2 -0
  16. package/dist/components/marks/BarMarkX.svelte +13 -1
  17. package/dist/components/marks/BarMarkY.svelte +13 -1
  18. package/dist/components/marks/DotsMark.svelte +2 -0
  19. package/dist/components/marks/LineMark.svelte +5 -1
  20. package/dist/components/marks/ParallelCoordinatesMark.svelte +4 -0
  21. package/dist/components/marks/ParallelCoordinatesMark.svelte.d.ts +2 -0
  22. package/dist/components/marks/RectMark.svelte +2 -0
  23. package/dist/components/marks/RectXMark.svelte +2 -0
  24. package/dist/components/marks/RectYMark.svelte +2 -0
  25. package/dist/components/marks/RuleMarkX.svelte +2 -0
  26. package/dist/components/marks/RuleMarkY.svelte +2 -0
  27. package/dist/components/marks/TextMark.svelte +8 -9
  28. package/dist/components/marks/lifecycle/MarkLifecycleV2.svelte.d.ts +6 -1
  29. package/dist/components/marks/lifecycle/MarkLifecycleV2.svelte.js +187 -18
  30. package/dist/components/marks/lifecycle/domain-queries.d.ts +3 -1
  31. package/dist/components/marks/lifecycle/domain-queries.js +11 -6
  32. package/dist/components/marks/lifecycle/domain-registration.js +2 -6
  33. package/dist/components/marks/lifecycle/other-rows.svelte.d.ts +104 -0
  34. package/dist/components/marks/lifecycle/other-rows.svelte.js +193 -0
  35. package/dist/components/marks/lifecycle/other-split.d.ts +80 -0
  36. package/dist/components/marks/lifecycle/other-split.js +112 -0
  37. package/dist/components/marks/lifecycle/query-builder.d.ts +26 -1
  38. package/dist/components/marks/lifecycle/query-builder.js +25 -5
  39. package/dist/components/marks/utils/reactive-config.d.ts +1 -1
  40. package/dist/components/marks/utils/reactive-config.js +6 -0
  41. package/dist/components/marks/utils/tooltip-helpers.d.ts +4 -1
  42. package/dist/components/marks/utils/tooltip-helpers.js +2 -1
  43. package/dist/components/rendering/BaseGPURenderer.svelte.d.ts +3 -1
  44. package/dist/components/rendering/BaseGPURenderer.svelte.js +4 -2
  45. package/dist/components/rendering/Rects.svelte +117 -23
  46. package/dist/components/rendering/Rects.svelte.d.ts +7 -0
  47. package/dist/components/rendering/activity-reads.d.ts +18 -0
  48. package/dist/components/rendering/activity-reads.js +19 -0
  49. package/dist/config/ChartConfig.svelte.d.ts +3 -1
  50. package/dist/gpu/MorphedChannels.d.ts +6 -32
  51. package/dist/gpu/MorphedChannels.js +45 -3
  52. package/dist/gpu/ScaleTemplateSystem.js +25 -4
  53. package/dist/index.d.ts +2 -0
  54. package/dist/index.js +3 -0
  55. package/dist/interactors/Interval1D.svelte +6 -1
  56. package/dist/interactors/Interval2D.svelte +6 -1
  57. package/dist/interactors/ParallelAxisSource.svelte +10 -1
  58. package/dist/interactors/Toggle.svelte +32 -3
  59. package/dist/interactors/Tooltip.svelte +138 -41
  60. package/dist/interactors/TooltipManager.svelte.d.ts +20 -5
  61. package/dist/interactors/TooltipManager.svelte.js +32 -7
  62. package/dist/interactors/other-tooltip.d.ts +16 -0
  63. package/dist/interactors/other-tooltip.js +22 -0
  64. package/dist/query-engine/FieldActivity.svelte.d.ts +50 -1
  65. package/dist/query-engine/FieldActivity.svelte.js +176 -5
  66. package/dist/query-engine/FilterGroup.svelte.d.ts +23 -1
  67. package/dist/query-engine/FilterGroup.svelte.js +43 -3
  68. package/dist/query-engine/FilterGroupRegistry.svelte.d.ts +31 -3
  69. package/dist/query-engine/FilterGroupRegistry.svelte.js +62 -15
  70. package/dist/query-engine/QueryEngine.js +26 -6
  71. package/dist/query-engine/categorical-identity.d.ts +15 -0
  72. package/dist/query-engine/categorical-identity.js +15 -0
  73. package/dist/query-engine/preagg/plan.d.ts +5 -0
  74. package/dist/query-engine/preagg/plan.js +31 -7
  75. package/dist/query-engine/toggle-predicate.d.ts +21 -0
  76. package/dist/query-engine/toggle-predicate.js +29 -1
  77. package/dist/scales/ActivityTable.d.ts +5 -1
  78. package/dist/scales/ActivityTable.js +12 -3
  79. package/dist/scales/BandScaleState.svelte.d.ts +13 -8
  80. package/dist/scales/BandScaleState.svelte.js +23 -16
  81. package/dist/spec/types.d.ts +2 -0
  82. package/dist/theme.css +5 -4
  83. package/dist/transforms/TransformCompiler.d.ts +37 -2
  84. package/dist/transforms/TransformCompiler.js +44 -1
  85. package/dist/transforms/index.js +32 -18
  86. package/dist/types.d.ts +41 -0
  87. 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 { activityReads, markActivityWGSL } from './activity-reads';
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
- // 40-47: padding for 16-byte alignment
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
- _pad1: f32,
141
- _pad2: f32,
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
- output.alpha = alpha * selectionDim(
194
- ${markActivityWGSL(ACTIVITY_READS, {
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
- x: 'x0',
197
- y: 'y0',
198
- group: 'markConstants.groupIndex'
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
- let finalAlpha = baseColor.a * input.alpha;
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('x', x0Data, [
271
- [BINDINGS.Y0, y0Data],
272
- [BINDINGS.Y1, y1Data]
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('y', y0Data, [
276
- [BINDINGS.X0, x0Data],
277
- [BINDINGS.X1, x1Data]
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
- // padding at 40-47
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
- // X: left to right (index 0 at left)
183
- // Y: bottom to top (index 0 at bottom, matching D3 convention)
184
- ${axis === 'y' ? 'return rangeMax - bandLayout.x - bandLayout.y * 0.5;' : 'return rangeMin + bandLayout.x + bandLayout.y * 0.5;'}
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 } });