@fjader/svelte 0.2.2 → 0.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (148) hide show
  1. package/README.md +2 -0
  2. package/dist/components/axes/AxisBottom.svelte +23 -6
  3. package/dist/components/axes/AxisBottom.svelte.d.ts +10 -0
  4. package/dist/components/axes/AxisLeft.svelte +77 -8
  5. package/dist/components/axes/AxisLeft.svelte.d.ts +17 -0
  6. package/dist/components/axes/BandAxisTicks.svelte +64 -25
  7. package/dist/components/axes/BandAxisTicks.svelte.d.ts +5 -5
  8. package/dist/components/axes/axis-title.d.ts +30 -0
  9. package/dist/components/axes/axis-title.js +77 -0
  10. package/dist/components/charts/ChartCell.svelte +1 -1
  11. package/dist/components/charts/ChartContainer.svelte +12 -3
  12. package/dist/components/charts/ChartContainer.svelte.d.ts +7 -0
  13. package/dist/components/guides/legend/ContinuousColorLegend.svelte +9 -2
  14. package/dist/components/guides/legend/OrdinalColorLegend.svelte +96 -35
  15. package/dist/components/marks/AreaMarkX.svelte +21 -6
  16. package/dist/components/marks/AreaMarkY.svelte +21 -6
  17. package/dist/components/marks/BarMarkX.svelte +16 -1
  18. package/dist/components/marks/BarMarkY.svelte +16 -1
  19. package/dist/components/marks/DotsMark.svelte +5 -0
  20. package/dist/components/marks/LineMark.svelte +8 -1
  21. package/dist/components/marks/ParallelCoordinatesMark.svelte +10 -0
  22. package/dist/components/marks/ParallelCoordinatesMark.svelte.d.ts +5 -0
  23. package/dist/components/marks/RectMark.svelte +5 -0
  24. package/dist/components/marks/RectXMark.svelte +5 -0
  25. package/dist/components/marks/RectYMark.svelte +5 -0
  26. package/dist/components/marks/RuleMarkX.svelte +12 -2
  27. package/dist/components/marks/RuleMarkY.svelte +12 -2
  28. package/dist/components/marks/TextMark.svelte +76 -13
  29. package/dist/components/marks/lifecycle/MarkLifecycleV2.svelte.d.ts +21 -2
  30. package/dist/components/marks/lifecycle/MarkLifecycleV2.svelte.js +300 -42
  31. package/dist/components/marks/lifecycle/domain-queries.d.ts +3 -1
  32. package/dist/components/marks/lifecycle/domain-queries.js +22 -15
  33. package/dist/components/marks/lifecycle/domain-registration.js +2 -6
  34. package/dist/components/marks/lifecycle/other-rows.svelte.d.ts +104 -0
  35. package/dist/components/marks/lifecycle/other-rows.svelte.js +193 -0
  36. package/dist/components/marks/lifecycle/other-split.d.ts +80 -0
  37. package/dist/components/marks/lifecycle/other-split.js +112 -0
  38. package/dist/components/marks/lifecycle/query-builder.d.ts +26 -1
  39. package/dist/components/marks/lifecycle/query-builder.js +25 -5
  40. package/dist/components/marks/lifecycle/transform-scale-key.d.ts +16 -0
  41. package/dist/components/marks/lifecycle/transform-scale-key.js +27 -0
  42. package/dist/components/marks/utils/area-outline.d.ts +24 -0
  43. package/dist/components/marks/utils/area-outline.js +10 -0
  44. package/dist/components/marks/utils/reactive-config.d.ts +1 -1
  45. package/dist/components/marks/utils/reactive-config.js +9 -0
  46. package/dist/components/marks/utils/tooltip-helpers.d.ts +4 -1
  47. package/dist/components/marks/utils/tooltip-helpers.js +2 -1
  48. package/dist/components/rendering/Areas.svelte +30 -6
  49. package/dist/components/rendering/Areas.svelte.d.ts +2 -0
  50. package/dist/components/rendering/BaseGPURenderer.svelte.d.ts +3 -1
  51. package/dist/components/rendering/BaseGPURenderer.svelte.js +8 -3
  52. package/dist/components/rendering/Dots.svelte +24 -3
  53. package/dist/components/rendering/Dots.svelte.d.ts +2 -0
  54. package/dist/components/rendering/GridLine.svelte +2 -1
  55. package/dist/components/rendering/Lines.svelte +49 -7
  56. package/dist/components/rendering/Lines.svelte.d.ts +9 -0
  57. package/dist/components/rendering/ParallelLines.svelte +26 -7
  58. package/dist/components/rendering/ParallelLines.svelte.d.ts +2 -0
  59. package/dist/components/rendering/Rects.svelte +129 -20
  60. package/dist/components/rendering/Rects.svelte.d.ts +9 -0
  61. package/dist/components/rendering/RuleX.svelte +20 -5
  62. package/dist/components/rendering/RuleX.svelte.d.ts +2 -0
  63. package/dist/components/rendering/RuleY.svelte +20 -5
  64. package/dist/components/rendering/RuleY.svelte.d.ts +2 -0
  65. package/dist/components/rendering/activity-reads.d.ts +111 -0
  66. package/dist/components/rendering/activity-reads.js +154 -0
  67. package/dist/components/rendering/uniform-sizes.d.ts +26 -0
  68. package/dist/components/rendering/uniform-sizes.js +28 -0
  69. package/dist/config/ChartConfig.svelte.d.ts +5 -1
  70. package/dist/gpu/ColorScaleShaderLib.d.ts +13 -1
  71. package/dist/gpu/ColorScaleShaderLib.js +56 -5
  72. package/dist/gpu/GPUCoordinator.svelte.d.ts +8 -0
  73. package/dist/gpu/GPUCoordinator.svelte.js +68 -32
  74. package/dist/gpu/MorphedChannels.d.ts +6 -32
  75. package/dist/gpu/MorphedChannels.js +45 -3
  76. package/dist/gpu/ResolveShader.d.ts +2 -2
  77. package/dist/gpu/ResolveShader.js +2 -2
  78. package/dist/gpu/ScaleTemplateSystem.d.ts +19 -0
  79. package/dist/gpu/ScaleTemplateSystem.js +111 -11
  80. package/dist/gpu/SizeScaleShaderLib.d.ts +4 -4
  81. package/dist/gpu/SizeScaleShaderLib.js +4 -4
  82. package/dist/gpu/identity-slot-packing.d.ts +157 -40
  83. package/dist/gpu/identity-slot-packing.js +262 -62
  84. package/dist/gpu/tracked-scales.d.ts +14 -0
  85. package/dist/gpu/tracked-scales.js +18 -0
  86. package/dist/index.d.ts +2 -0
  87. package/dist/index.js +4 -0
  88. package/dist/interactors/BandLabelToggle.svelte +5 -9
  89. package/dist/interactors/BandLabelToggle.svelte.d.ts +3 -3
  90. package/dist/interactors/DerivedInterval1D.svelte +26 -54
  91. package/dist/interactors/Interval1D.svelte +8 -2
  92. package/dist/interactors/Interval2D.svelte +8 -2
  93. package/dist/interactors/ParallelAxisSource.svelte +14 -5
  94. package/dist/interactors/ParallelAxisSource.svelte.d.ts +2 -1
  95. package/dist/interactors/ParallelBrushes.svelte +8 -9
  96. package/dist/interactors/ParallelBrushes.svelte.d.ts +3 -3
  97. package/dist/interactors/Toggle.svelte +48 -38
  98. package/dist/interactors/Toggle.svelte.d.ts +4 -3
  99. package/dist/interactors/Tooltip.svelte +140 -42
  100. package/dist/interactors/TooltipManager.svelte.d.ts +20 -5
  101. package/dist/interactors/TooltipManager.svelte.js +40 -10
  102. package/dist/interactors/other-tooltip.d.ts +16 -0
  103. package/dist/interactors/other-tooltip.js +22 -0
  104. package/dist/interactors/relay-clauses.d.ts +23 -0
  105. package/dist/interactors/relay-clauses.js +60 -0
  106. package/dist/query-engine/CrossfilterSelection.svelte.d.ts +88 -15
  107. package/dist/query-engine/CrossfilterSelection.svelte.js +94 -20
  108. package/dist/query-engine/FieldActivity.svelte.d.ts +128 -0
  109. package/dist/query-engine/FieldActivity.svelte.js +378 -0
  110. package/dist/query-engine/FilterGroup.svelte.d.ts +76 -30
  111. package/dist/query-engine/FilterGroup.svelte.js +223 -124
  112. package/dist/query-engine/FilterGroupRegistry.svelte.d.ts +73 -3
  113. package/dist/query-engine/FilterGroupRegistry.svelte.js +161 -12
  114. package/dist/query-engine/MarkHandle.svelte.d.ts +9 -1
  115. package/dist/query-engine/MarkHandle.svelte.js +12 -2
  116. package/dist/query-engine/QueryEngine.d.ts +52 -16
  117. package/dist/query-engine/QueryEngine.js +116 -34
  118. package/dist/query-engine/categorical-identity.d.ts +114 -27
  119. package/dist/query-engine/categorical-identity.js +225 -51
  120. package/dist/query-engine/index.d.ts +2 -1
  121. package/dist/query-engine/index.js +1 -0
  122. package/dist/query-engine/preagg/Preagg.d.ts +1 -1
  123. package/dist/query-engine/preagg/Preagg.js +11 -9
  124. package/dist/query-engine/preagg/plan.d.ts +14 -5
  125. package/dist/query-engine/preagg/plan.js +44 -18
  126. package/dist/query-engine/toggle-predicate.d.ts +21 -0
  127. package/dist/query-engine/toggle-predicate.js +41 -2
  128. package/dist/scales/ActivityTable.d.ts +103 -0
  129. package/dist/scales/ActivityTable.js +220 -0
  130. package/dist/scales/BandScaleState.svelte.d.ts +41 -15
  131. package/dist/scales/BandScaleState.svelte.js +78 -36
  132. package/dist/scales/ChartScales.svelte.d.ts +2 -2
  133. package/dist/scales/ColorScaleState.svelte.d.ts +36 -8
  134. package/dist/scales/ColorScaleState.svelte.js +118 -24
  135. package/dist/scales/DomainResolver.d.ts +2 -0
  136. package/dist/scales/DomainResolver.js +15 -8
  137. package/dist/scales/ScaleRegistry.svelte.d.ts +9 -0
  138. package/dist/scales/ScaleRegistry.svelte.js +22 -1
  139. package/dist/scales/TimeScaleState.svelte.js +7 -1
  140. package/dist/spec/index.js +1 -0
  141. package/dist/spec/types.d.ts +4 -0
  142. package/dist/theme.css +5 -4
  143. package/dist/transforms/TransformCompiler.d.ts +37 -2
  144. package/dist/transforms/TransformCompiler.js +44 -1
  145. package/dist/transforms/index.d.ts +27 -0
  146. package/dist/transforms/index.js +127 -19
  147. package/dist/types.d.ts +71 -0
  148. package/package.json +3 -2
@@ -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()];
@@ -6,8 +6,8 @@
6
6
  * are bound to the compute pipeline; the render stages bind `resolved` alone.
7
7
  * So an animated channel REPLACES a vertex binding rather than adding one, and
8
8
  * animating any number of channels leaves that stage's budget where it was —
9
- * which is what makes this affordable on a mark that already sits at the
10
- * default limit of 8.
9
+ * which is what makes this affordable on a mark that already uses 7 of the
10
+ * default 8 storage buffers (Rects).
11
11
  *
12
12
  * ONE KERNEL FOR BOTH, because the two differ only in where a range starts. A
13
13
  * value channel is one mark's rows in buffers of their own, so it starts at 0.
@@ -7,8 +7,8 @@ import { isNanExpr } from './NanShaderLib';
7
7
  * are bound to the compute pipeline; the render stages bind `resolved` alone.
8
8
  * So an animated channel REPLACES a vertex binding rather than adding one, and
9
9
  * animating any number of channels leaves that stage's budget where it was —
10
- * which is what makes this affordable on a mark that already sits at the
11
- * default limit of 8.
10
+ * which is what makes this affordable on a mark that already uses 7 of the
11
+ * default 8 storage buffers (Rects).
12
12
  *
13
13
  * ONE KERNEL FOR BOTH, because the two differ only in where a range starts. A
14
14
  * value channel is one mark's rows in buffers of their own, so it starts at 0.
@@ -36,6 +36,9 @@ interface ShaderResult {
36
36
  * `ScaleParam`, 16 bytes each. Every layout declares the same size because a
37
37
  * draw requires every declared byte to have been set, and the pass sets them
38
38
  * once, before its first draw.
39
+ *
40
+ * Immediates carry state shared across the pass and read by several marks: the
41
+ * chart's scales and settings. 64 bytes are available on M-series Macs.
39
42
  */
40
43
  export declare const CHART_SCALE_IMMEDIATE_SIZE = 32;
41
44
  /**
@@ -66,6 +69,22 @@ export declare class ScaleTemplateSystem {
66
69
  * @private
67
70
  */
68
71
  _buildDeclarations(xFragment: ScaleFragment, yFragment: ScaleFragment): string;
72
+ /**
73
+ * `bandActivityX(value, group)` and `bandActivityY(value, group)`: how
74
+ * selected a band or point axis's category is in the mark's filter group,
75
+ * 0..1, for selection dimming.
76
+ *
77
+ * Emitted for every shader, so a renderer can call both without knowing its
78
+ * scale types: an axis that is not band or point returns 1.0. On a band or
79
+ * point axis the value is an identity id and the chart's scale carries its
80
+ * block base in p4, as for the slot lookup. A missing category (-1) or a NaN
81
+ * is fully active, checked before the cast, since u32() of a negative or NaN
82
+ * float is indeterminate in WGSL.
83
+ */
84
+ _buildBandActivityHelpers(scaleTypes: {
85
+ x: string;
86
+ y: string;
87
+ }): string;
69
88
  /**
70
89
  * The shared slot table, and the lookup into it.
71
90
  *
@@ -1,6 +1,9 @@
1
1
  // src/lib/gpu/ScaleTemplateSystem.ts
2
2
  import { COLOR_SCALE_STRUCTS, COLOR_SCALE_BINDINGS, COLOR_SCALE_FUNCTIONS } from './ColorScaleShaderLib';
3
3
  import { SIZE_SCALE_STRUCTS, SIZE_SCALE_BINDINGS, SIZE_SCALE_FUNCTIONS } from './SizeScaleShaderLib';
4
+ import { NO_GROUP } from './identity-slot-packing';
5
+ import { isNanExpr } from './NanShaderLib';
6
+ import { DEFAULT_INACTIVE_OPACITY } from '../scales/ActivityTable';
4
7
  /**
5
8
  * Scale fragment definitions
6
9
  * Each scale type provides: declaration, function call, and implementation
@@ -175,10 +178,10 @@ const SCALE_FRAGMENTS = {
175
178
  rangeMax
176
179
  );
177
180
 
178
- // Return center of band
179
- // X: left to right (index 0 at left)
180
- // Y: bottom to top (index 0 at bottom, matching D3 convention)
181
- ${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;
182
185
  }
183
186
  `,
184
187
  bufferLayout: {
@@ -280,6 +283,9 @@ function _scaleFunctionName(scaleType, axis) {
280
283
  * `ScaleParam`, 16 bytes each. Every layout declares the same size because a
281
284
  * draw requires every declared byte to have been set, and the pass sets them
282
285
  * once, before its first draw.
286
+ *
287
+ * Immediates carry state shared across the pass and read by several marks: the
288
+ * chart's scales and settings. 64 bytes are available on M-series Macs.
283
289
  */
284
290
  export const CHART_SCALE_IMMEDIATE_SIZE = 32;
285
291
  /**
@@ -370,6 +376,7 @@ export class ScaleTemplateSystem {
370
376
  // ===== 3. HELPER FUNCTIONS =====
371
377
 
372
378
  ${this._buildSlotTableHelpers()}
379
+ ${this._buildBandActivityHelpers(scaleTypes)}
373
380
  ${bandHelpers}
374
381
  ${implementations}
375
382
  ${scaleFunctions}
@@ -409,6 +416,24 @@ export class ScaleTemplateSystem {
409
416
  };
410
417
  var<immediate> chartScales: ChartScales;
411
418
 
419
+ // Selection dimming: a mark's activity mapped to an opacity, with
420
+ // inactive categories at DEFAULT_INACTIVE_OPACITY. selectionOpacity in
421
+ // ActivityTable.ts is its CPU twin, for band labels and legend entries,
422
+ // and both read the same constant so they cannot drift apart.
423
+ const SELECTION_DIM_STRENGTH: f32 = ${(1 - DEFAULT_INACTIVE_OPACITY).toFixed(6)};
424
+ fn selectionDim(activity: f32) -> f32 {
425
+ return 1.0 - SELECTION_DIM_STRENGTH * (1.0 - activity);
426
+ }
427
+
428
+ // A mark's activities combined: colour, and band for marks on band axes.
429
+ // The least active wins rather than the product, so a bar whose colour
430
+ // and band are the same field fades on the same curve as its label
431
+ // instead of twice as fast. At rest every activity is 0 or 1, where the
432
+ // two agree.
433
+ fn markActivity(color: f32, bandX: f32, bandY: f32) -> f32 {
434
+ return min(color, min(bandX, bandY));
435
+ }
436
+
412
437
  // The ONE place that says where a chart's scales come from. Every
413
438
  // consumer reads through these — scaleX/scaleY, the bandwidth helpers,
414
439
  // and mark code that needs the raw params. A mark with scales of its own
@@ -423,6 +448,45 @@ export class ScaleTemplateSystem {
423
448
  `;
424
449
  return `${structDef}\n${chartScaleDeclaration}`;
425
450
  }
451
+ /**
452
+ * `bandActivityX(value, group)` and `bandActivityY(value, group)`: how
453
+ * selected a band or point axis's category is in the mark's filter group,
454
+ * 0..1, for selection dimming.
455
+ *
456
+ * Emitted for every shader, so a renderer can call both without knowing its
457
+ * scale types: an axis that is not band or point returns 1.0. On a band or
458
+ * point axis the value is an identity id and the chart's scale carries its
459
+ * block base in p4, as for the slot lookup. A missing category (-1) or a NaN
460
+ * is fully active, checked before the cast, since u32() of a negative or NaN
461
+ * float is indeterminate in WGSL.
462
+ */
463
+ _buildBandActivityHelpers(scaleTypes) {
464
+ const helper = (axis, type) => {
465
+ if (type !== 'band' && type !== 'point') {
466
+ return `fn bandActivity${axis}(value: f32, group: u32) -> f32 {
467
+ return 1.0;
468
+ }
469
+ fn bandFreeActivity${axis}(group: u32) -> f32 {
470
+ return 1.0;
471
+ }`;
472
+ }
473
+ return `fn bandActivity${axis}(value: f32, group: u32) -> f32 {
474
+ if (value < 0.0 || ${isNanExpr('value')}) {
475
+ return 1.0;
476
+ }
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);
481
+ }`;
482
+ };
483
+ return `
484
+ // ===== BAND SELECTION ACTIVITY =====
485
+ ${helper('X', scaleTypes.x)}
486
+
487
+ ${helper('Y', scaleTypes.y)}
488
+ `;
489
+ }
426
490
  /**
427
491
  * The shared slot table, and the lookup into it.
428
492
  *
@@ -459,12 +523,12 @@ export class ScaleTemplateSystem {
459
523
  }
460
524
 
461
525
  fn identitySlot(base: u32, id: u32) -> f32 {
462
- // Block layout: [count, slot0, slot1, ...]. The count is what makes a
463
- // block self-describing, so an id past this scale's end is discarded
464
- // instead of reading into the next scale's slots. It survives the resolve
465
- // exactly because it holds the same value in both halves, and the kernel
466
- // returns equal ends as they are — u32() truncates, so an inexact mix
467
- // would drop the last id.
526
+ // Block layout: [count, actRef, n, slot0, slot1, ...]. The count is what
527
+ // makes a block self-describing, so an id past this scale's end is
528
+ // discarded instead of reading into the next scale's slots. It survives
529
+ // the resolve exactly because it holds the same value in both halves, and
530
+ // the kernel returns equal ends as they are — u32() truncates, so an
531
+ // inexact mix would drop the last id.
468
532
  let count = u32(identitySlots[base]);
469
533
  if (id >= count) {
470
534
  return -1.0;
@@ -473,7 +537,43 @@ export class ScaleTemplateSystem {
473
537
  // both halves, so the resolve wrote -1 without a branch; one that is
474
538
  // leaving holds a real slot in both, which is what lets it travel. The
475
539
  // scale normalises that before packing, so nothing here re-derives it.
476
- return identitySlots[base + 1u + id];
540
+ return identitySlots[base + 3u + id];
541
+ }
542
+
543
+ fn identityActivity(base: u32, group: u32, id: u32) -> f32 {
544
+ // How selected this id is in the mark's filter group, 1 active and 0
545
+ // inactive, already resolved at that group's selection clock. The scale
546
+ // block's header points at its field's activity block (actRef) and says
547
+ // how many ids it covers (n); the block holds one run of n per group.
548
+ // Header first, so this is two dependent reads. Past n, or in the empty
549
+ // block where n is 0, an id is fully active.
550
+ //
551
+ // NO_GROUP (a mark whose selectionResponse is 'filter') is fully active,
552
+ // checked before anything is read, so it never becomes an offset.
553
+ if (group == ${NO_GROUP}u) {
554
+ return 1.0;
555
+ }
556
+ let actRef = u32(identitySlots[base + 1u]);
557
+ let n = u32(identitySlots[base + 2u]);
558
+ if (id >= n) {
559
+ return 1.0;
560
+ }
561
+ return identitySlots[actRef + n * group + id];
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];
477
577
  }
478
578
  `;
479
579
  }
@@ -19,10 +19,10 @@ export declare const MAX_SIZE_SLOTS = 64;
19
19
  * changes rather than per frame. Every property that makes a uniform buffer the
20
20
  * right container was already true of it.
21
21
  *
22
- * What it buys: the vertex stage sits at the default
23
- * maxStorageBuffersPerShaderStage of 8 (six mark channels, scale params, and
24
- * this), and animating a value channel needs a `from` and a `to` where there is
25
- * now one buffer. Uniform buffers are nowhere near their own default of 12.
22
+ * What it buys: a slot in the vertex stage's storage budget, the default
23
+ * maxStorageBuffersPerShaderStage of 8, of which Rects uses 7 (six mark
24
+ * channels and the identity slot table). Uniform buffers are nowhere near
25
+ * their own default of 12.
26
26
  *
27
27
  * Dynamic indexing — `sizeConfigs[scaleIndex]` — is permitted on uniform arrays
28
28
  * in WGSL, and the shared slot table already relies on the same thing.
@@ -28,10 +28,10 @@ export const MAX_SIZE_SLOTS = 64;
28
28
  * changes rather than per frame. Every property that makes a uniform buffer the
29
29
  * right container was already true of it.
30
30
  *
31
- * What it buys: the vertex stage sits at the default
32
- * maxStorageBuffersPerShaderStage of 8 (six mark channels, scale params, and
33
- * this), and animating a value channel needs a `from` and a `to` where there is
34
- * now one buffer. Uniform buffers are nowhere near their own default of 12.
31
+ * What it buys: a slot in the vertex stage's storage budget, the default
32
+ * maxStorageBuffersPerShaderStage of 8, of which Rects uses 7 (six mark
33
+ * channels and the identity slot table). Uniform buffers are nowhere near
34
+ * their own default of 12.
35
35
  *
36
36
  * Dynamic indexing — `sizeConfigs[scaleIndex]` — is permitted on uniform arrays
37
37
  * in WGSL, and the shared slot table already relies on the same thing.
@@ -18,12 +18,45 @@
18
18
  * on it. It is a pure function over plain arrays and maps, which is
19
19
  * what it should have been.
20
20
  *
21
- * Layout: index 0 is a permanent zero-length block, then one block per scale of
22
- * `[count, slot0, slot1, ...]`. The count header makes each block
23
- * self-describing, so the shader rejects an id past this scale's end instead of
24
- * reading into the next scale's slots — ids and the slot table can disagree
25
- * transiently while a dim appends. Budget is therefore
26
- * `capacity - 1 - (1 per scale)` slots.
21
+ * LAYOUT. Index 0 is a permanent empty block `[0, 0, 0]`. Then one SLOT
22
+ * block per scale, then one ACTIVITY block per live categorical field:
23
+ *
24
+ * scale block: [count, actRef, n, slot0 … slot(count-1)]
25
+ * field block: [group 0: act0 … act(n-1), group 1: act0 … act(n-1), …]
26
+ *
27
+ * Slots come first so that when the buffer runs out it is activity that is
28
+ * lost (a mark draws undimmed), never an axis. A field has a block while a
29
+ * scale draws it, selected on or not, and a field no chart draws any more
30
+ * frees its space. Tying the block to the selection instead saved that space
31
+ * on pages with nothing selected, but every select and clear then moved the
32
+ * block and rewrote every scale block pointing at it, slots included:
33
+ * measured at about 105000 floats uploaded per click for 20 scales on a
34
+ * 5000-category field, against 5000 when the block stays.
35
+ *
36
+ * KNOWN LIMIT: a field block that does not fit leaves a selection on that
37
+ * field doing nothing for the marks that dim by it. Routing (the mark's reads,
38
+ * see activity-reads.ts) still sends them to dim rather than filter, and their
39
+ * shader finds no activity. Routing does not ask the packer whether a field
40
+ * got a block, which would tie the query layer to the GPU layout; the packer
41
+ * warns instead, on load. It takes fields of tens of thousands of ids across
42
+ * several groups to reach. A buffer that grows on demand is the real fix.
43
+ *
44
+ * A scale block's header says how many ids its slots cover (`count`), where its
45
+ * field's activity block starts (`actRef`) and how many ids that block covers
46
+ * (`n`). The count is what makes a block self-describing, so the shader rejects
47
+ * an id past this scale's end instead of reading into the next scale's slots —
48
+ * ids and the slot table can disagree transiently while a dim appends.
49
+ *
50
+ * ACTIVITY says how selected each id is in each filter group (1 active, 0
51
+ * inactive; see FieldActivity). It belongs to the field, not to the scale:
52
+ * every scale on one field reads the same block, a mark choosing its group's
53
+ * half by its group index. So a click rewrites and resolves one half of one
54
+ * field block, however many scales draw that field. The header comes first so
55
+ * the shader finds `actRef` and `n` without reading `count`: the activity read
56
+ * is two dependent loads, as it was when the activity sat after the slots. No
57
+ * mark holds `actRef`; the packer writes it in the same pass that moves the
58
+ * field block, so it cannot go stale. A scale with no field to dim points at
59
+ * the empty block, where `n` is 0 and every id is active.
27
60
  *
28
61
  * TWO buffers share one set of offsets: `from` is where each id's current
29
62
  * transition started and `to` where it is going, and the resolve kernel writes
@@ -31,28 +64,43 @@
31
64
  * bindings rather than two runs in one buffer, so the halves line up at the
32
65
  * same base and one dispatch per block covers both.
33
66
  *
34
- * `t` is not in the buffers. It is PER SCALE — each band scale has its own
35
- * driver — and reaches the kernel as an immediate on that scale's dispatch, so
36
- * a frame where only `t` moved uploads nothing at all.
67
+ * `t` is not in the buffers. It is per scale (its data clock) and per field
68
+ * half (that group's selection clock), and reaches the kernel as an immediate
69
+ * on that range's dispatch, so a frame where only `t` moved uploads nothing.
37
70
  */
38
71
  /**
39
72
  * Floats GLOBALLY across every band and point scale on the page, headers
40
- * included.
73
+ * included, and every live field's activity.
41
74
  *
42
75
  * No longer a binding limit: the tables are storage buffers, bound only to the
43
- * resolve pass, and the render stages read one resolved storage buffer. The
44
- * number is left over from when they were 64 KB uniforms, and making the
45
- * allocation grow on demand (recreate the buffers, rebuild the bind groups) is
46
- * what would retire it.
76
+ * resolve pass, and the render stages read one resolved storage buffer. Making
77
+ * the allocation grow on demand (recreate the buffers, rebuild the bind groups)
78
+ * is what would retire it.
79
+ *
80
+ * Each scale takes `3 + count`; each live field `n × groups`. 65536 fits a
81
+ * 5000-category field selected on across 3 groups (15000) with its scales
82
+ * (about 5000 each) several times over. It costs 256 KB per buffer, 768 KB for
83
+ * the three; the resolve pass dispatches per block, sized by the block, so a
84
+ * larger buffer costs no GPU time.
47
85
  */
48
- export declare const IDENTITY_SLOT_CAPACITY = 16384;
86
+ export declare const IDENTITY_SLOT_CAPACITY = 65536;
49
87
  /**
50
- * Base of the permanent zero-length block. A scale that cannot be placed points
51
- * here, so every lookup finds count 0 and discards — which is what the capacity
52
- * warning claims happens. Pointing it at a stale base instead left the scale
53
- * rendering, from another scale's slots.
88
+ * Base of the permanent empty block `[0, 0, 0]`. A scale that cannot be placed
89
+ * points here, so every lookup finds count 0 and discards, and every activity
90
+ * read finds n 0 and is fully active. Pointing it at a stale base instead left
91
+ * the scale rendering, from another scale's slots.
54
92
  */
55
93
  export declare 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 declare const NO_GROUP = 4294967295;
102
+ /** Floats before a scale block's slots: count, actRef, n. */
103
+ export declare const IDENTITY_SLOT_HEADER = 3;
56
104
  /**
57
105
  * One scale's slot table as the packer reads it: identity id -> display slot,
58
106
  * as the two ends of a transition and where between them it is.
@@ -73,17 +121,51 @@ export interface SlotSource {
73
121
  /** Bumped whenever `from` or `to` move; the packer re-uploads on it alone. */
74
122
  readonly version: number;
75
123
  }
76
- /** A scale with a slot table, as the packer needs it. */
124
+ /**
125
+ * One group's activity on one field as the packer reads it: 1 active, 0
126
+ * inactive, as the two ends of a fade. ActivityTable has this shape.
127
+ *
128
+ * Its length is not trusted: the packer takes `n` from the field and pads with
129
+ * 1 or truncates, so a moment where the two disagree cannot shift the half onto
130
+ * the wrong ids.
131
+ */
132
+ export interface ActivitySource {
133
+ readonly from: Float32Array;
134
+ readonly to: Float32Array;
135
+ /** 0 -> from, 1 -> to. Its own clock, not the slots'. */
136
+ readonly t: number;
137
+ /** Bumped whenever `from` or `to` change. */
138
+ readonly version: number;
139
+ }
140
+ /**
141
+ * A scale with a slot table, as the packer needs it: its slots, and the identity
142
+ * key of its field, whose activity block its header points at. No field (or a
143
+ * field with no activity block) points it at the empty block.
144
+ */
77
145
  export interface IdentitySlotScale {
78
146
  id: string;
79
147
  slots: SlotSource;
148
+ field?: string | null;
80
149
  }
81
150
  /**
82
- * One block to resolve this frame: `length` floats from `base`, at `t`.
151
+ * A categorical field's selection activity, as the packer reads it: one fade
152
+ * per filter group, over `count` ids. FieldActivity has this shape.
153
+ */
154
+ export interface ActivityField {
155
+ readonly key: string;
156
+ /** Ids covered: the block's `n`. */
157
+ readonly count: number;
158
+ /** Group `index`'s fade, or null when that group never selected here (all 1). */
159
+ fadeOf(index: number): ActivitySource | null;
160
+ }
161
+ /**
162
+ * One range to resolve this frame: `length` floats from `base`, at `t`. A
163
+ * scale's slots and each group's half of a field block are separate ranges,
164
+ * since each has its own `t`.
83
165
  *
84
- * The count header is inside the range rather than skipped: it holds the same
85
- * value in both halves, so mixing it returns it, and the render stages read
86
- * their bound from the resolved copy. Pooled in the state and overwritten by
166
+ * The header is inside the slot range rather than skipped: it holds the same
167
+ * values in both buffers, so the kernel returns them exactly, and the render
168
+ * stages read it from the resolved copy. Pooled in the state and overwritten by
87
169
  * the next pack.
88
170
  */
89
171
  export interface ResolveBlock {
@@ -116,30 +198,65 @@ export interface IdentitySlotState {
116
198
  offsets: Map<string, number>;
117
199
  /** Last packed slotVersion per scale, so a static chart re-uploads nothing. */
118
200
  versions: Map<string, number>;
201
+ /** Last packed count per scale. */
202
+ counts: Map<string, number>;
203
+ /** Last packed actRef and n per scale: a field block that moved rewrites the header. */
204
+ refs: Map<string, number>;
205
+ ns: Map<string, number>;
206
+ /**
207
+ * The `t` each scale's slots were last resolved at, so a still frame
208
+ * dispatches nothing. A typed array per scale, created once, rather than a
209
+ * `Map<string, number>`: a fractional `t` stored as a Map value is boxed,
210
+ * which allocated on every frame of every transition.
211
+ */
212
+ resolvedT: Map<string, Float64Array>;
213
+ /** Per field: its block's base, `n` and the group count it was packed for. */
214
+ fieldBases: Map<string, number>;
215
+ fieldNs: Map<string, number>;
216
+ fieldGroups: Map<string, number>;
217
+ /** Per field, per group: the fade version last written and the `t` last resolved. */
218
+ fieldVersions: Map<string, Float64Array>;
219
+ fieldT: Map<string, Float64Array>;
220
+ /** The pass a scale last drew each field in: a field no scale draws gets no block. */
221
+ fieldSeen: Map<string, number>;
222
+ /** Fields whose block did not fit, warned about once until they fit again. */
223
+ warnedFields: Set<string>;
119
224
  /**
120
- * The `t` each scale's block was last resolved at, so a still frame
121
- * dispatches nothing and an animating one dispatches only the scales that
122
- * are moving.
225
+ * The pass each scale was last seen in, for the eviction sweep.
226
+ *
227
+ * A pass counter rather than a Set cleared every pack: clearing a Set
228
+ * allocates a new table in V8, which made every frame allocate ~370 bytes
229
+ * (measured, 20 scales) even when nothing moved. Re-setting an existing key
230
+ * to a small integer allocates nothing.
123
231
  */
124
- resolvedT: Map<string, number>;
125
- /** Scratch for the eviction sweep; cleared, not reallocated, per pack. */
126
- seen: Set<string>;
232
+ seenPass: Map<string, number>;
233
+ /** The current pass, a small integer that wraps. */
234
+ pass: number;
127
235
  /** Returned by every pack and overwritten by the next — read it immediately. */
128
236
  result: PackResult;
129
237
  }
130
238
  /** A state with the permanent empty block in place. */
131
239
  export declare function createIdentitySlotState(capacity?: number): IdentitySlotState;
132
240
  /**
133
- * Assign every scale a base, write the blocks that moved, and list what to
134
- * upload and what to resolve.
241
+ * Assign every field and scale a base, write the ranges that moved, and list
242
+ * what to upload and what to resolve.
243
+ *
244
+ * | Range | Rewritten when | Resolved when |
245
+ * |---|---|---|
246
+ * | 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 |
247
+ * | A scale's header + slots | its base, slot version, or its field block's base or `n` moved | rewritten, or slot `t` moved |
248
+ *
249
+ * A range that is rewritten must be resolved even when its `t` did not move:
250
+ * the render stages read the resolved buffer, not these. A parked scale is
251
+ * neither rewritten nor resolved: it has no block, and the empty block it
252
+ * points at is never written.
135
253
  *
136
- * A block is REWRITTEN when its base or its version moved, and RESOLVED when it
137
- * was rewritten or its `t` moved. A parked scale is neither: it has no block,
138
- * and the empty block it points at is never written.
254
+ * Offsets are recomputed on every call because a block appearing or
255
+ * disappearing shifts every block after it. That is a running sum over a
256
+ * handful of blocks, and the alternative — a free list — would have to survive
257
+ * a block changing size, which happens whenever a dim appends. A field block
258
+ * that moves rewrites the headers pointing at it in the same pass.
139
259
  *
140
- * Offsets are recomputed on every call because a scale appearing or
141
- * disappearing shifts every scale after it. That is a running sum over a
142
- * handful of scales, and the alternative — a free list — would have to survive
143
- * a scale changing size, which happens whenever a dim appends.
260
+ * `fields` and `groups` default to none, which packs the slots alone.
144
261
  */
145
- export declare function packIdentitySlots(scales: readonly IdentitySlotScale[], state: IdentitySlotState): PackResult;
262
+ export declare function packIdentitySlots(scales: readonly IdentitySlotScale[], state: IdentitySlotState, fields?: readonly ActivityField[], groups?: number): PackResult;