@fjader/svelte 0.2.2 → 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (118) hide show
  1. package/dist/components/axes/BandAxisTicks.svelte +63 -24
  2. package/dist/components/axes/BandAxisTicks.svelte.d.ts +5 -5
  3. package/dist/components/charts/ChartContainer.svelte +2 -1
  4. package/dist/components/guides/legend/ContinuousColorLegend.svelte +2 -1
  5. package/dist/components/guides/legend/OrdinalColorLegend.svelte +92 -34
  6. package/dist/components/marks/AreaMarkX.svelte +19 -6
  7. package/dist/components/marks/AreaMarkY.svelte +19 -6
  8. package/dist/components/marks/BarMarkX.svelte +3 -0
  9. package/dist/components/marks/BarMarkY.svelte +3 -0
  10. package/dist/components/marks/DotsMark.svelte +3 -0
  11. package/dist/components/marks/LineMark.svelte +3 -0
  12. package/dist/components/marks/ParallelCoordinatesMark.svelte +6 -0
  13. package/dist/components/marks/ParallelCoordinatesMark.svelte.d.ts +3 -0
  14. package/dist/components/marks/RectMark.svelte +3 -0
  15. package/dist/components/marks/RectXMark.svelte +3 -0
  16. package/dist/components/marks/RectYMark.svelte +3 -0
  17. package/dist/components/marks/RuleMarkX.svelte +10 -2
  18. package/dist/components/marks/RuleMarkY.svelte +10 -2
  19. package/dist/components/marks/TextMark.svelte +68 -4
  20. package/dist/components/marks/lifecycle/MarkLifecycleV2.svelte.d.ts +15 -1
  21. package/dist/components/marks/lifecycle/MarkLifecycleV2.svelte.js +116 -27
  22. package/dist/components/marks/lifecycle/domain-queries.js +13 -11
  23. package/dist/components/marks/lifecycle/transform-scale-key.d.ts +16 -0
  24. package/dist/components/marks/lifecycle/transform-scale-key.js +27 -0
  25. package/dist/components/marks/utils/area-outline.d.ts +24 -0
  26. package/dist/components/marks/utils/area-outline.js +10 -0
  27. package/dist/components/marks/utils/reactive-config.d.ts +1 -1
  28. package/dist/components/marks/utils/reactive-config.js +3 -0
  29. package/dist/components/rendering/Areas.svelte +30 -6
  30. package/dist/components/rendering/Areas.svelte.d.ts +2 -0
  31. package/dist/components/rendering/BaseGPURenderer.svelte.js +4 -1
  32. package/dist/components/rendering/Dots.svelte +24 -3
  33. package/dist/components/rendering/Dots.svelte.d.ts +2 -0
  34. package/dist/components/rendering/GridLine.svelte +2 -1
  35. package/dist/components/rendering/Lines.svelte +49 -7
  36. package/dist/components/rendering/Lines.svelte.d.ts +9 -0
  37. package/dist/components/rendering/ParallelLines.svelte +26 -7
  38. package/dist/components/rendering/ParallelLines.svelte.d.ts +2 -0
  39. package/dist/components/rendering/Rects.svelte +20 -5
  40. package/dist/components/rendering/Rects.svelte.d.ts +2 -0
  41. package/dist/components/rendering/RuleX.svelte +20 -5
  42. package/dist/components/rendering/RuleX.svelte.d.ts +2 -0
  43. package/dist/components/rendering/RuleY.svelte +20 -5
  44. package/dist/components/rendering/RuleY.svelte.d.ts +2 -0
  45. package/dist/components/rendering/activity-reads.d.ts +93 -0
  46. package/dist/components/rendering/activity-reads.js +135 -0
  47. package/dist/components/rendering/uniform-sizes.d.ts +26 -0
  48. package/dist/components/rendering/uniform-sizes.js +28 -0
  49. package/dist/config/ChartConfig.svelte.d.ts +2 -0
  50. package/dist/gpu/ColorScaleShaderLib.d.ts +13 -1
  51. package/dist/gpu/ColorScaleShaderLib.js +56 -5
  52. package/dist/gpu/GPUCoordinator.svelte.d.ts +8 -0
  53. package/dist/gpu/GPUCoordinator.svelte.js +68 -32
  54. package/dist/gpu/ResolveShader.d.ts +2 -2
  55. package/dist/gpu/ResolveShader.js +2 -2
  56. package/dist/gpu/ScaleTemplateSystem.d.ts +19 -0
  57. package/dist/gpu/ScaleTemplateSystem.js +86 -7
  58. package/dist/gpu/SizeScaleShaderLib.d.ts +4 -4
  59. package/dist/gpu/SizeScaleShaderLib.js +4 -4
  60. package/dist/gpu/identity-slot-packing.d.ts +157 -40
  61. package/dist/gpu/identity-slot-packing.js +262 -62
  62. package/dist/gpu/tracked-scales.d.ts +14 -0
  63. package/dist/gpu/tracked-scales.js +18 -0
  64. package/dist/index.js +1 -0
  65. package/dist/interactors/BandLabelToggle.svelte +5 -9
  66. package/dist/interactors/BandLabelToggle.svelte.d.ts +3 -3
  67. package/dist/interactors/DerivedInterval1D.svelte +26 -54
  68. package/dist/interactors/Interval1D.svelte +2 -1
  69. package/dist/interactors/Interval2D.svelte +2 -1
  70. package/dist/interactors/ParallelAxisSource.svelte +4 -4
  71. package/dist/interactors/ParallelAxisSource.svelte.d.ts +2 -1
  72. package/dist/interactors/ParallelBrushes.svelte +8 -9
  73. package/dist/interactors/ParallelBrushes.svelte.d.ts +3 -3
  74. package/dist/interactors/Toggle.svelte +17 -36
  75. package/dist/interactors/Toggle.svelte.d.ts +4 -3
  76. package/dist/interactors/Tooltip.svelte +2 -1
  77. package/dist/interactors/TooltipManager.svelte.js +8 -3
  78. package/dist/interactors/relay-clauses.d.ts +23 -0
  79. package/dist/interactors/relay-clauses.js +60 -0
  80. package/dist/query-engine/CrossfilterSelection.svelte.d.ts +88 -15
  81. package/dist/query-engine/CrossfilterSelection.svelte.js +94 -20
  82. package/dist/query-engine/FieldActivity.svelte.d.ts +79 -0
  83. package/dist/query-engine/FieldActivity.svelte.js +207 -0
  84. package/dist/query-engine/FilterGroup.svelte.d.ts +53 -29
  85. package/dist/query-engine/FilterGroup.svelte.js +181 -122
  86. package/dist/query-engine/FilterGroupRegistry.svelte.d.ts +42 -0
  87. package/dist/query-engine/FilterGroupRegistry.svelte.js +103 -1
  88. package/dist/query-engine/MarkHandle.svelte.d.ts +9 -1
  89. package/dist/query-engine/MarkHandle.svelte.js +12 -2
  90. package/dist/query-engine/QueryEngine.d.ts +52 -16
  91. package/dist/query-engine/QueryEngine.js +90 -28
  92. package/dist/query-engine/categorical-identity.d.ts +99 -27
  93. package/dist/query-engine/categorical-identity.js +210 -51
  94. package/dist/query-engine/index.d.ts +2 -1
  95. package/dist/query-engine/index.js +1 -0
  96. package/dist/query-engine/preagg/Preagg.d.ts +1 -1
  97. package/dist/query-engine/preagg/Preagg.js +11 -9
  98. package/dist/query-engine/preagg/plan.d.ts +9 -5
  99. package/dist/query-engine/preagg/plan.js +13 -11
  100. package/dist/query-engine/toggle-predicate.js +12 -1
  101. package/dist/scales/ActivityTable.d.ts +99 -0
  102. package/dist/scales/ActivityTable.js +211 -0
  103. package/dist/scales/BandScaleState.svelte.d.ts +28 -7
  104. package/dist/scales/BandScaleState.svelte.js +55 -20
  105. package/dist/scales/ChartScales.svelte.d.ts +2 -2
  106. package/dist/scales/ColorScaleState.svelte.d.ts +36 -8
  107. package/dist/scales/ColorScaleState.svelte.js +118 -24
  108. package/dist/scales/DomainResolver.d.ts +2 -0
  109. package/dist/scales/DomainResolver.js +15 -8
  110. package/dist/scales/ScaleRegistry.svelte.d.ts +9 -0
  111. package/dist/scales/ScaleRegistry.svelte.js +22 -1
  112. package/dist/scales/TimeScaleState.svelte.js +7 -1
  113. package/dist/spec/index.js +1 -0
  114. package/dist/spec/types.d.ts +2 -0
  115. package/dist/transforms/index.d.ts +27 -0
  116. package/dist/transforms/index.js +97 -3
  117. package/dist/types.d.ts +30 -0
  118. package/package.json +1 -1
@@ -42,10 +42,11 @@ export async function fetchCategoricalDomains(coordinator, table, select, actual
42
42
  //
43
43
  // - Order. DISTINCT has no ORDER BY, so today's domain is whatever DuckDB
44
44
  // returned, then "first-appearance order" in updateDomainFromExtents.
45
- // Arbitrary. Dim labels are ordered by id, so the domain is
46
- // deterministic — and position-in-domain now EQUALS the dim id, which
47
- // is the invariant that lets the query switch to the view's id column
48
- // without moving a single bar.
45
+ // Arbitrary. Dim values are listed in id order, so the domain is
46
+ // deterministic. It is the real categories only (`categoryValues`
47
+ // leaves out Other buckets), so position-in-domain equals the dim id
48
+ // only until a bucket's row: the scale encodes each value to its id
49
+ // rather than relying on position.
49
50
  // - Multi-table. The dim spans every table registering the field, so two
50
51
  // marks on one shared scale agree on the category set by construction.
51
52
  //
@@ -63,10 +64,11 @@ export async function fetchCategoricalDomains(coordinator, table, select, actual
63
64
  fetches.push((async () => {
64
65
  // Idempotent and coalesced — the mark's own setup calls this too.
65
66
  await coordinator.ensureColumns(table, { categorical: [identityField] });
66
- const categories = coordinator.identityValues(identityField.key);
67
- // The scale adopts this same array in MarkLifecycleV2's
68
- // #ensureIdentityColumns, which also covers the explicit-domain axes
69
- // that never reach this branch.
67
+ // This table's categories: the dim spans every table on the field.
68
+ const categories = coordinator.categoryValues(identityField.key, table);
69
+ // A snapshot of the real categories. The scale adopts the live
70
+ // identity itself in MarkLifecycleV2's #ensureIdentityColumns, which
71
+ // also covers the explicit-domain axes that never reach this branch.
70
72
  if (categories.length > 0)
71
73
  scaleState.addMark(categories, markId);
72
74
  })());
@@ -121,7 +123,7 @@ export async function fetchCategoricalDomains(coordinator, table, select, actual
121
123
  fetches.push((async () => {
122
124
  // Idempotent and coalesced — the mark's own setup calls this too.
123
125
  await coordinator.ensureColumns(table, { categorical: [facetKey] });
124
- const categories = coordinator.identityValues(facetKey.key);
126
+ const categories = coordinator.categoryValues(facetKey.key, table);
125
127
  if (categories.length > 0)
126
128
  facetBandScale.addMark(categories, markId);
127
129
  })());
@@ -185,7 +187,7 @@ export async function fetchCategoricalDomains(coordinator, table, select, actual
185
187
  fetches.push((async () => {
186
188
  // Idempotent and coalesced — the mark's own setup calls this too.
187
189
  await coordinator.ensureColumns(table, { categorical: [colorIdentity] });
188
- const categories = coordinator.identityValues(colorIdentity.key);
190
+ const categories = coordinator.categoryValues(colorIdentity.key);
189
191
  if (categories.length > 0) {
190
192
  colorScaleState.setDomain(categories);
191
193
  }
@@ -234,7 +236,7 @@ export async function fetchCategoricalDomains(coordinator, table, select, actual
234
236
  if (seriesIdentity) {
235
237
  fetches.push((async () => {
236
238
  await coordinator.ensureColumns(table, { categorical: [seriesIdentity] });
237
- const categories = coordinator.identityValues(seriesIdentity.key);
239
+ const categories = coordinator.categoryValues(seriesIdentity.key);
238
240
  if (categories.length > 0) {
239
241
  seriesScaleState.setDomain(categories);
240
242
  }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * The `transformKey` a mark with a category-reducing transform (topN) adds to
3
+ * its band scale's config, which is part of the scale's registry key.
4
+ *
5
+ * It names the table, the transform (N included) and the chart. A topN axis
6
+ * shows the categories its own query returned, and a band scale resolves the
7
+ * union of every mark registered on it, so two charts may share one only if
8
+ * they always return the same categories. Two charts with the identical topN
9
+ * do not: they receive different filters (a brush skips its own chart), and a
10
+ * shared scale would show each the other's top N as empty slots, and share one
11
+ * Other bucket between two different sets of members.
12
+ *
13
+ * The chart is its ChartConfig object, which a faceted chart's cells share, so
14
+ * facets keep one axis; a bar and its value labels in one chart share one too.
15
+ */
16
+ export declare function transformScaleKey(table: string, transformKey: string, chart: object | null | undefined): string;
@@ -0,0 +1,27 @@
1
+ /**
2
+ * The `transformKey` a mark with a category-reducing transform (topN) adds to
3
+ * its band scale's config, which is part of the scale's registry key.
4
+ *
5
+ * It names the table, the transform (N included) and the chart. A topN axis
6
+ * shows the categories its own query returned, and a band scale resolves the
7
+ * union of every mark registered on it, so two charts may share one only if
8
+ * they always return the same categories. Two charts with the identical topN
9
+ * do not: they receive different filters (a brush skips its own chart), and a
10
+ * shared scale would show each the other's top N as empty slots, and share one
11
+ * Other bucket between two different sets of members.
12
+ *
13
+ * The chart is its ChartConfig object, which a faceted chart's cells share, so
14
+ * facets keep one axis; a bar and its value labels in one chart share one too.
15
+ */
16
+ const chartKeys = new WeakMap();
17
+ let nextChartKey = 0;
18
+ /** A name for a chart, stable while its ChartConfig lives, shared by its cells. */
19
+ function chartKeyOf(chart) {
20
+ let key = chartKeys.get(chart);
21
+ if (!key)
22
+ chartKeys.set(chart, (key = `chart${nextChartKey++}`));
23
+ return key;
24
+ }
25
+ export function transformScaleKey(table, transformKey, chart) {
26
+ return chart ? `${table}:${transformKey}@${chartKeyOf(chart)}` : `${table}:${transformKey}`;
27
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * How an area's outline is coloured, and which colour its selection activity
3
+ * is read through.
4
+ *
5
+ * The outline is drawn in the fill's colour, or in a constant `stroke`. Either
6
+ * way its activity is the fill's: the area dims as one shape when a selection
7
+ * on its colour field dims the fill, outline included. Before activity was per
8
+ * field the unselected areas were filtered away, outlines with them; drawn in a
9
+ * constant colour, an outline reading its own colour would read no activity and
10
+ * stay opaque over its dimmed fill.
11
+ */
12
+ export interface OutlineColor {
13
+ /** The values the outline's colour buffer holds: the fill's, always. */
14
+ colorValueData: Float32Array | null;
15
+ colorMode: number;
16
+ colorScaleIndex: number;
17
+ activityColorMode: number;
18
+ activityColorScaleIndex: number;
19
+ }
20
+ export declare function outlineColor(stroke: unknown, fill: {
21
+ data: Float32Array | null;
22
+ colorMode: number;
23
+ colorScaleIndex: number;
24
+ }): OutlineColor;
@@ -0,0 +1,10 @@
1
+ export function outlineColor(stroke, fill) {
2
+ const constant = !!stroke;
3
+ return {
4
+ colorValueData: fill.data,
5
+ colorMode: constant ? 0 : fill.colorMode,
6
+ colorScaleIndex: constant ? 0 : fill.colorScaleIndex,
7
+ activityColorMode: fill.colorMode,
8
+ activityColorScaleIndex: fill.colorScaleIndex
9
+ };
10
+ }
@@ -12,6 +12,6 @@ import type { MarkEntry } from '../../../config/ChartConfig.svelte';
12
12
  * @param entry - The mark entry (reactive props from ChartConfig)
13
13
  * @param opts.resolveFitDomain - Getter for resolved fitDomain (deferred read)
14
14
  */
15
- export declare function buildReactiveConfig(entry: Partial<Pick<MarkEntry, 'sort' | 'limit' | 'domain' | 'tooltip' | 'tip' | 'fitDomain'>>, opts?: {
15
+ export declare function buildReactiveConfig(entry: Partial<Pick<MarkEntry, 'sort' | 'limit' | 'domain' | 'tooltip' | 'tip' | 'fitDomain' | 'selectionResponse'>>, opts?: {
16
16
  resolveFitDomain?: () => 'x' | 'y' | boolean | undefined;
17
17
  }): ReactiveConfig;
@@ -30,6 +30,9 @@ export function buildReactiveConfig(entry, opts) {
30
30
  },
31
31
  get tip() {
32
32
  return entry.tip;
33
+ },
34
+ get selectionResponse() {
35
+ return entry.selectionResponse;
33
36
  }
34
37
  };
35
38
  }
@@ -1,4 +1,6 @@
1
1
  <script lang="ts">
2
+ import { activityReads, colorActivityWGSL } from './activity-reads';
3
+ import { UNIFORM_BYTES } from './uniform-sizes';
2
4
  import { getContext, onMount, onDestroy } from 'svelte';
3
5
  import { RendererHost } from './RendererHost.svelte';
4
6
  import { getGPUContext } from '../../gpu';
@@ -25,7 +27,7 @@
25
27
  FACET: 6
26
28
  };
27
29
 
28
- // Uniform struct offsets (matching WGSL MarkConstants layout, 48 bytes)
30
+ // Uniform struct offsets (matching WGSL MarkConstants; size in uniform-sizes.ts)
29
31
  const UNIFORM_OFFSETS = {
30
32
  POINT_COUNT: 0, // u32
31
33
  COLOR_MODE: 4, // u32
@@ -35,7 +37,8 @@
35
37
  FILL_OPACITY: 32, // f32
36
38
  DOT_RADIUS: 36, // f32
37
39
  SHOW_BOTTOM_DOTS: 40, // u32 (0 or 1)
38
- FULL_DOMAIN_SPAN: 44 // f32
40
+ FULL_DOMAIN_SPAN: 44, // f32
41
+ GROUP_INDEX: 48 // u32
39
42
  };
40
43
 
41
44
  interface Props {
@@ -55,6 +58,8 @@
55
58
  orientation?: 'vertical' | 'horizontal';
56
59
  colorMode?: number;
57
60
  colorScaleIndex?: number;
61
+ /** The page-wide index of this mark's filter group: which run of a field's activity block it reads. */
62
+ groupIndex?: number;
58
63
  constantColor?: [number, number, number, number];
59
64
  markId?: string;
60
65
  }
@@ -76,6 +81,7 @@
76
81
  orientation = 'vertical',
77
82
  colorMode = 0,
78
83
  colorScaleIndex = 0,
84
+ groupIndex = 0,
79
85
  constantColor = DEFAULT_FILL_COLOR,
80
86
  markId = ''
81
87
  }: Props = $props();
@@ -87,6 +93,9 @@
87
93
  const baseMarkType = curve === 'linear' ? 'areas' : `areas-${curve}`;
88
94
  const effectiveMarkType = faceted ? `${baseMarkType}-faceted` : baseMarkType;
89
95
 
96
+ /** The selection activity this shader reads; routing reads the same table. */
97
+ const ACTIVITY_READS = activityReads('areas');
98
+
90
99
  // --- Facet compile-time specialization ---
91
100
  // Areas uses a storage buffer (not a vertex attribute) for facet indices because
92
101
  // the boundary check needs random access: facetIndices[nextIdx] to detect when
@@ -141,7 +150,11 @@
141
150
  fillOpacity: f32,
142
151
  dotRadius: f32,
143
152
  showBottomDots: u32,
144
- fullDomainSpan: f32
153
+ fullDomainSpan: f32,
154
+ groupIndex: u32, // Page-wide filter group index, for activity reads
155
+ _pad0: u32,
156
+ _pad1: u32,
157
+ _pad2: u32
145
158
  };
146
159
  @group(1) @binding(5) var<uniform> markConstants: MarkConstants;
147
160
 
@@ -241,7 +254,12 @@
241
254
  var output: VertexOutput;
242
255
  output.position = pixelsToClip(pos);
243
256
  output.colorValue = colorValues[instanceIdx];
244
- output.alpha = 1.0;
257
+ output.alpha = selectionDim(
258
+ ${colorActivityWGSL(ACTIVITY_READS, {
259
+ color: 'markConstants.colorMode, markConstants.colorScaleIndex, colorValues[instanceIdx]',
260
+ group: 'markConstants.groupIndex'
261
+ })}
262
+ );
245
263
  output.circleUV = quadCorner;
246
264
  output.circleRadius = circleR;
247
265
  return output;
@@ -341,7 +359,12 @@
341
359
  var output: VertexOutput;
342
360
  output.position = pixelsToClip(finalPos);
343
361
  output.colorValue = colorValues[currIdx];
344
- output.alpha = markConstants.fillOpacity;
362
+ output.alpha = markConstants.fillOpacity * selectionDim(
363
+ ${colorActivityWGSL(ACTIVITY_READS, {
364
+ color: 'markConstants.colorMode, markConstants.colorScaleIndex, colorValues[currIdx]',
365
+ group: 'markConstants.groupIndex'
366
+ })}
367
+ );
345
368
  output.circleUV = vec2<f32>(-2.0, -2.0);
346
369
  output.circleRadius = 0.0;
347
370
  return output;
@@ -440,13 +463,14 @@
440
463
  // re-fires once data loads.
441
464
  if (colorMode === 1 && !colorValueData) return;
442
465
 
443
- const buffer = new ArrayBuffer(48);
466
+ const buffer = new ArrayBuffer(UNIFORM_BYTES.areasMarkConstants);
444
467
  const view = new DataView(buffer);
445
468
  const LE = true;
446
469
 
447
470
  view.setUint32(UNIFORM_OFFSETS.POINT_COUNT, dimData ? dimData.length : 0, LE);
448
471
  view.setUint32(UNIFORM_OFFSETS.COLOR_MODE, colorMode, LE);
449
472
  view.setUint32(UNIFORM_OFFSETS.COLOR_SCALE_INDEX, colorScaleIndex, LE);
473
+ view.setUint32(UNIFORM_OFFSETS.GROUP_INDEX, groupIndex, LE);
450
474
  view.setUint32(UNIFORM_OFFSETS.ORIENTATION, orientation === 'horizontal' ? 1 : 0, LE);
451
475
  view.setFloat32(UNIFORM_OFFSETS.CONSTANT_COLOR + 0, constantColor[0], LE);
452
476
  view.setFloat32(UNIFORM_OFFSETS.CONSTANT_COLOR + 4, constantColor[1], LE);
@@ -16,6 +16,8 @@ interface Props {
16
16
  orientation?: 'vertical' | 'horizontal';
17
17
  colorMode?: number;
18
18
  colorScaleIndex?: number;
19
+ /** The page-wide index of this mark's filter group: which run of a field's activity block it reads. */
20
+ groupIndex?: number;
19
21
  constantColor?: [number, number, number, number];
20
22
  markId?: string;
21
23
  }
@@ -1,3 +1,4 @@
1
+ import { UNIFORM_STAND_IN_BYTES } from './uniform-sizes';
1
2
  import { MorphedChannels } from '../../gpu/MorphedChannels';
2
3
  /** Warned once per page, not per mark. */
3
4
  let warnedMorphUnsupported = false;
@@ -385,7 +386,9 @@ export class BaseGPURenderer {
385
386
  get #sharedDummyBuffer() {
386
387
  if (!this.#dummyBuffer) {
387
388
  this.#dummyBuffer = this.#device.createBuffer({
388
- size: 48, // Must fit the largest uniform struct (e.g. Rects MarkConstants = 48 bytes)
389
+ // Bound to any binding not written yet, uniforms included: as large as
390
+ // the largest uniform struct, or a draw before the first write fails.
391
+ size: UNIFORM_STAND_IN_BYTES,
389
392
  usage: GPUBufferUsage.STORAGE | GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
390
393
  label: `${this.#markType}-dummy`
391
394
  });
@@ -1,9 +1,11 @@
1
1
  <script lang="ts">
2
+ import { UNIFORM_BYTES } from './uniform-sizes';
2
3
  import { getContext, onMount, onDestroy } from 'svelte';
3
4
  import { RendererHost } from './RendererHost.svelte';
4
5
  import { getGPUContext } from '../../gpu';
5
6
  import { DEFAULT_STROKE_COLOR } from '../../scales/color-utils';
6
7
  import type { ChartContext, WebGPUCanvasContext } from '../../types';
8
+ import { activityReads, markActivityWGSL } from './activity-reads';
7
9
 
8
10
  const dotRendererDefinition = {
9
11
  x: { concept: 'position', channels: ['x'] },
@@ -29,7 +31,8 @@
29
31
  COLOR_SCALE_INDEX: 4,
30
32
  SIZE_MODE: 8,
31
33
  SIZE_SCALE_INDEX: 12,
32
- CONSTANT_COLOR: 16
34
+ CONSTANT_COLOR: 16,
35
+ GROUP_INDEX: 32
33
36
  };
34
37
 
35
38
  interface Props {
@@ -43,6 +46,8 @@
43
46
  zIndex?: number;
44
47
  colorMode?: number;
45
48
  colorScaleIndex?: number;
49
+ /** The page-wide index of this mark's filter group: which run of a field's activity block it reads. */
50
+ groupIndex?: number;
46
51
  constantColor?: [number, number, number, number];
47
52
  sizeMode?: number;
48
53
  sizeScaleIndex?: number;
@@ -72,6 +77,7 @@
72
77
  zIndex = 0,
73
78
  colorMode = 0,
74
79
  colorScaleIndex = 0,
80
+ groupIndex = 0,
75
81
  constantColor = DEFAULT_STROKE_COLOR,
76
82
  sizeMode = 0,
77
83
  sizeScaleIndex = 0,
@@ -112,6 +118,9 @@
112
118
  return degen;
113
119
  }`;
114
120
 
121
+ /** The selection activities this shader reads; routing reads the same table. */
122
+ const ACTIVITY_READS = activityReads('dots');
123
+
115
124
  const dotsShaderCode = `
116
125
  // MarkConstants uniform for per-mark configuration
117
126
  struct MarkConstants {
@@ -120,6 +129,10 @@
120
129
  sizeMode: u32, // 0 = raw value, 1 = scale
121
130
  sizeScaleIndex: u32, // Index into sizeConfigs
122
131
  constantColor: vec4<f32>, // Used when colorMode = 0
132
+ groupIndex: u32, // Page-wide filter group index, for activity reads
133
+ _pad0: u32,
134
+ _pad1: u32,
135
+ _pad2: u32,
123
136
  };
124
137
  @group(1) @binding(5) var<uniform> markConstants: MarkConstants;
125
138
 
@@ -175,7 +188,14 @@
175
188
  output.colorValue = colorValue;
176
189
  output.uv = quadPosition;
177
190
  output.radius = displayRadius;
178
- output.alpha = alpha;
191
+ output.alpha = alpha * selectionDim(
192
+ ${markActivityWGSL(ACTIVITY_READS, {
193
+ color: 'markConstants.colorMode, markConstants.colorScaleIndex, colorValue',
194
+ x: 'x',
195
+ y: 'y',
196
+ group: 'markConstants.groupIndex'
197
+ })}
198
+ );
179
199
  return output;
180
200
  }
181
201
 
@@ -283,12 +303,13 @@
283
303
  // which re-triggers the guard and avoids stale-buffer flashes.
284
304
  if (colorMode === 1 && !colorValueData) return;
285
305
 
286
- const buffer = new ArrayBuffer(32);
306
+ const buffer = new ArrayBuffer(UNIFORM_BYTES.dotsMarkConstants);
287
307
  const view = new DataView(buffer);
288
308
  const LE = true;
289
309
 
290
310
  view.setUint32(UNIFORM_OFFSETS.COLOR_MODE, colorMode, LE);
291
311
  view.setUint32(UNIFORM_OFFSETS.COLOR_SCALE_INDEX, colorScaleIndex, LE);
312
+ view.setUint32(UNIFORM_OFFSETS.GROUP_INDEX, groupIndex, LE);
292
313
  view.setUint32(UNIFORM_OFFSETS.SIZE_MODE, sizeMode, LE);
293
314
  view.setUint32(UNIFORM_OFFSETS.SIZE_SCALE_INDEX, sizeScaleIndex, LE);
294
315
  view.setFloat32(UNIFORM_OFFSETS.CONSTANT_COLOR + 0, constantColor[0], LE);
@@ -9,6 +9,8 @@ interface Props {
9
9
  zIndex?: number;
10
10
  colorMode?: number;
11
11
  colorScaleIndex?: number;
12
+ /** The page-wide index of this mark's filter group: which run of a field's activity block it reads. */
13
+ groupIndex?: number;
12
14
  constantColor?: [number, number, number, number];
13
15
  sizeMode?: number;
14
16
  sizeScaleIndex?: number;
@@ -1,4 +1,5 @@
1
1
  <script lang="ts">
2
+ import { UNIFORM_BYTES } from './uniform-sizes';
2
3
  import { getContext, onMount, onDestroy } from 'svelte';
3
4
  import { RendererHost } from './RendererHost.svelte';
4
5
  import { getGPUContext } from '../../gpu';
@@ -194,7 +195,7 @@ ${placement}
194
195
  // Update uniform constants
195
196
  $effect(() => {
196
197
  if (renderer?.resourcesCreated) {
197
- const buffer = new ArrayBuffer(32);
198
+ const buffer = new ArrayBuffer(UNIFORM_BYTES.gridLineRuleConstants);
198
199
  const view = new DataView(buffer);
199
200
  const LE = true;
200
201
 
@@ -1,4 +1,6 @@
1
1
  <script lang="ts">
2
+ import { activityReads, colorActivityWGSL } from './activity-reads';
3
+ import { UNIFORM_BYTES } from './uniform-sizes';
2
4
  import { getContext, onMount, onDestroy } from 'svelte';
3
5
  import { RendererHost } from './RendererHost.svelte';
4
6
  import { getGPUContext } from '../../gpu';
@@ -25,7 +27,7 @@
25
27
  FACET: 5
26
28
  };
27
29
 
28
- // Uniform struct offsets (matching WGSL MarkConstants layout, 48 bytes)
30
+ // Uniform struct offsets (matching WGSL MarkConstants; size in uniform-sizes.ts)
29
31
  const UNIFORM_OFFSETS = {
30
32
  STROKE_WIDTH: 0, // f32
31
33
  POINT_COUNT: 4, // u32
@@ -33,7 +35,10 @@
33
35
  COLOR_SCALE_INDEX: 12, // u32
34
36
  CONSTANT_COLOR: 16, // vec4<f32>
35
37
  DOT_RADIUS: 32, // f32
36
- GLOBAL_ALPHA: 36 // f32
38
+ GLOBAL_ALPHA: 36, // f32
39
+ GROUP_INDEX: 40, // u32
40
+ ACTIVITY_COLOR_MODE: 44, // u32
41
+ ACTIVITY_COLOR_SCALE_INDEX: 48 // u32
37
42
  };
38
43
 
39
44
  interface Props {
@@ -49,6 +54,15 @@
49
54
  dotRadius?: number;
50
55
  colorMode?: number;
51
56
  colorScaleIndex?: number;
57
+ /** The page-wide index of this mark's filter group: which run of a field's activity block it reads. */
58
+ groupIndex?: number;
59
+ /**
60
+ * The colour mode and scale index selection activity is read through,
61
+ * from `colorValueData`; null for the line's own colour. An area's
62
+ * outline in a constant stroke passes its fill's, with the fill's values.
63
+ */
64
+ activityColorMode?: number | null;
65
+ activityColorScaleIndex?: number | null;
52
66
  constantColor?: [number, number, number, number];
53
67
  m4Decimator?: M4Decimator | null;
54
68
  dotsRung?: import('../../lod/DotsRung.svelte').DotsRung | null;
@@ -69,6 +83,9 @@
69
83
  dotRadius = 0,
70
84
  colorMode = 0,
71
85
  colorScaleIndex = 0,
86
+ groupIndex = 0,
87
+ activityColorMode = null,
88
+ activityColorScaleIndex = null,
72
89
  constantColor = DEFAULT_STROKE_COLOR,
73
90
  m4Decimator = null,
74
91
  dotsRung = null,
@@ -83,6 +100,9 @@
83
100
  const baseMarkType = curve === 'linear' ? 'lines' : `lines-${curve}`;
84
101
  const effectiveMarkType = faceted ? `${baseMarkType}-faceted` : baseMarkType;
85
102
 
103
+ /** The selection activity this shader reads; routing reads the same table. */
104
+ const ACTIVITY_READS = activityReads('lines');
105
+
86
106
  // --- Facet compile-time specialization ---
87
107
  // Lines uses a storage buffer (not a vertex attribute) for facet indices because
88
108
  // the boundary check needs random access: facetIndices[nextIdx] to detect when
@@ -139,7 +159,12 @@
139
159
  constantColor: vec4<f32>,
140
160
  dotRadius: f32,
141
161
  globalAlpha: f32,
142
- _pad1: u32
162
+ groupIndex: u32, // Page-wide filter group index, for activity reads
163
+ // Which colour scale this line's selection activity is read through. Its
164
+ // own colour by default; an area's outline, drawn in a constant stroke,
165
+ // reads its fill's so it dims with the fill.
166
+ activityColorMode: u32,
167
+ activityColorScaleIndex: u32
143
168
  };
144
169
  @group(1) @binding(4) var<uniform> markConstants: MarkConstants;
145
170
 
@@ -196,7 +221,13 @@
196
221
  var output: VertexOutput;
197
222
  output.position = pixelsToClip(pos);
198
223
  output.colorValue = colorValues[instanceIdx];
199
- output.alpha = markConstants.globalAlpha;
224
+ output.alpha = markConstants.globalAlpha * selectionDim(
225
+ ${colorActivityWGSL(ACTIVITY_READS, {
226
+ color:
227
+ 'markConstants.activityColorMode, markConstants.activityColorScaleIndex, colorValues[instanceIdx]',
228
+ group: 'markConstants.groupIndex'
229
+ })}
230
+ );
200
231
  output.circleUV = quadPosition;
201
232
  output.circleRadius = circleR;
202
233
  return output;
@@ -291,7 +322,13 @@
291
322
  var output: VertexOutput;
292
323
  output.position = pixelsToClip(finalPos);
293
324
  output.colorValue = colorValues[currIdx];
294
- output.alpha = markConstants.globalAlpha;
325
+ output.alpha = markConstants.globalAlpha * selectionDim(
326
+ ${colorActivityWGSL(ACTIVITY_READS, {
327
+ color:
328
+ 'markConstants.activityColorMode, markConstants.activityColorScaleIndex, colorValues[currIdx]',
329
+ group: 'markConstants.groupIndex'
330
+ })}
331
+ );
295
332
  output.circleUV = vec2<f32>(-2.0, -2.0);
296
333
  output.circleRadius = 0.0;
297
334
  return output;
@@ -385,9 +422,11 @@
385
422
  // Guard: skip uniform write when colorMode=1 but color data hasn't
386
423
  // arrived yet. colorValueData is tracked reactively so the effect
387
424
  // re-fires once data loads.
388
- if (colorMode === 1 && !colorValueData) return;
425
+ const activityMode = activityColorMode ?? colorMode;
426
+ const activityScale = activityColorScaleIndex ?? colorScaleIndex;
427
+ if ((colorMode === 1 || activityMode === 1) && !colorValueData) return;
389
428
 
390
- const buffer = new ArrayBuffer(48);
429
+ const buffer = new ArrayBuffer(UNIFORM_BYTES.linesMarkConstants);
391
430
  const view = new DataView(buffer);
392
431
  const LE = true;
393
432
 
@@ -401,6 +440,9 @@
401
440
  view.setUint32(UNIFORM_OFFSETS.POINT_COUNT, pointCount, LE);
402
441
  view.setUint32(UNIFORM_OFFSETS.COLOR_MODE, colorMode, LE);
403
442
  view.setUint32(UNIFORM_OFFSETS.COLOR_SCALE_INDEX, colorScaleIndex, LE);
443
+ view.setUint32(UNIFORM_OFFSETS.GROUP_INDEX, groupIndex, LE);
444
+ view.setUint32(UNIFORM_OFFSETS.ACTIVITY_COLOR_MODE, activityMode, LE);
445
+ view.setUint32(UNIFORM_OFFSETS.ACTIVITY_COLOR_SCALE_INDEX, activityScale, LE);
404
446
  view.setFloat32(UNIFORM_OFFSETS.CONSTANT_COLOR + 0, constantColor[0], LE);
405
447
  view.setFloat32(UNIFORM_OFFSETS.CONSTANT_COLOR + 4, constantColor[1], LE);
406
448
  view.setFloat32(UNIFORM_OFFSETS.CONSTANT_COLOR + 8, constantColor[2], LE);
@@ -13,6 +13,15 @@ interface Props {
13
13
  dotRadius?: number;
14
14
  colorMode?: number;
15
15
  colorScaleIndex?: number;
16
+ /** The page-wide index of this mark's filter group: which run of a field's activity block it reads. */
17
+ groupIndex?: number;
18
+ /**
19
+ * The colour mode and scale index selection activity is read through,
20
+ * from `colorValueData`; null for the line's own colour. An area's
21
+ * outline in a constant stroke passes its fill's, with the fill's values.
22
+ */
23
+ activityColorMode?: number | null;
24
+ activityColorScaleIndex?: number | null;
16
25
  constantColor?: [number, number, number, number];
17
26
  m4Decimator?: M4Decimator | null;
18
27
  dotsRung?: import('../../lod/DotsRung.svelte').DotsRung | null;
@@ -1,4 +1,6 @@
1
1
  <script lang="ts">
2
+ import { activityReads, colorActivityWGSL } from './activity-reads';
3
+ import { PARALLEL_MAX_AXES, UNIFORM_BYTES } from './uniform-sizes';
2
4
  import { getContext, onMount, onDestroy } from 'svelte';
3
5
  import { RendererHost } from './RendererHost.svelte';
4
6
  import { getGPUContext } from '../../gpu';
@@ -7,10 +9,10 @@
7
9
  import { DEFAULT_STROKE_COLOR } from '../../scales/color-utils';
8
10
  import type { ChartContext, CurveType, WebGPUCanvasContext } from '../../types';
9
11
 
10
- const MAX_AXES = 16;
11
- const UNIFORM_SIZE = 176; // bytes, must match WGSL struct layout
12
+ const MAX_AXES = PARALLEL_MAX_AXES;
13
+ const UNIFORM_SIZE = UNIFORM_BYTES.parallelConfig;
12
14
  // One ScaleParam (16 bytes) per axis, filled by the coordinator.
13
- const AXIS_SCALES_SIZE = MAX_AXES * 16;
15
+ const AXIS_SCALES_SIZE = UNIFORM_BYTES.parallelAxisScales;
14
16
 
15
17
  // Both x and y so ScaleTemplateSystem generates scaleX(), scaleLinearY(), pixelsToClip()
16
18
  const rendererDefinition = {
@@ -32,6 +34,8 @@
32
34
  colorValueData?: Float32Array | null;
33
35
  colorMode?: number;
34
36
  colorScaleIndex?: number;
37
+ /** The page-wide index of this mark's filter group: which run of a field's activity block it reads. */
38
+ groupIndex?: number;
35
39
  constantColor?: [number, number, number, number];
36
40
  axisOrder: number[];
37
41
  /** Each axis's scale id, in data (not visual) order. */
@@ -51,6 +55,7 @@
51
55
  colorValueData = null,
52
56
  colorMode = 0,
53
57
  colorScaleIndex = 0,
58
+ groupIndex = 0,
54
59
  constantColor = DEFAULT_STROKE_COLOR,
55
60
  axisOrder,
56
61
  axisScaleIds,
@@ -87,6 +92,9 @@
87
92
  // (N-1) segments × resolution sub-quads × 6 verts + N circles × 6 verts
88
93
  const vertexCount = $derived((axisCount - 1) * 6 * resolution + axisCount * 6);
89
94
 
95
+ /** The selection activity this shader reads; routing reads the same table. */
96
+ const ACTIVITY_READS = activityReads('parallel');
97
+
90
98
  // evaluateCurve() and CURVE_RESOLUTION are prepended from CurveShaderLib.
91
99
  // With linear (resolution=1): single quad per segment, identical to original.
92
100
  // With bump-x (resolution=128): smooth S-curves between axes, adaptive.
@@ -101,7 +109,7 @@
101
109
  opacity: f32,
102
110
  colorMode: u32,
103
111
  colorScaleIndex: u32,
104
- _pad0: u32,
112
+ groupIndex: u32,
105
113
  _pad1: u32,
106
114
  constantColor: vec4<f32>,
107
115
  axisOrder: array<vec4<u32>, 4>,
@@ -191,7 +199,12 @@
191
199
  var output: VertexOutput;
192
200
  output.position = pixelsToClip(pos);
193
201
  output.colorValue = colorValues[rowIdx];
194
- output.alpha = config.opacity;
202
+ output.alpha = config.opacity * selectionDim(
203
+ ${colorActivityWGSL(ACTIVITY_READS, {
204
+ color: 'config.colorMode, config.colorScaleIndex, colorValues[rowIdx]',
205
+ group: 'config.groupIndex'
206
+ })}
207
+ );
195
208
  output.circleUV = quadPosition;
196
209
  output.circleRadius = halfWidth;
197
210
  return output;
@@ -270,7 +283,12 @@
270
283
  var output: VertexOutput;
271
284
  output.position = pixelsToClip(finalPos);
272
285
  output.colorValue = colorValues[rowIdx];
273
- output.alpha = config.opacity;
286
+ output.alpha = config.opacity * selectionDim(
287
+ ${colorActivityWGSL(ACTIVITY_READS, {
288
+ color: 'config.colorMode, config.colorScaleIndex, colorValues[rowIdx]',
289
+ group: 'config.groupIndex'
290
+ })}
291
+ );
274
292
  output.circleUV = vec2<f32>(-2.0, -2.0);
275
293
  output.circleRadius = 0.0;
276
294
  return output;
@@ -361,7 +379,8 @@
361
379
  f32[3] = opacity;
362
380
  u32[4] = colorMode;
363
381
  u32[5] = colorScaleIndex;
364
- // u32[6], u32[7] = padding
382
+ u32[6] = groupIndex;
383
+ // u32[7] = padding
365
384
  f32[8] = constantColor[0];
366
385
  f32[9] = constantColor[1];
367
386
  f32[10] = constantColor[2];
@@ -6,6 +6,8 @@ interface Props {
6
6
  colorValueData?: Float32Array | null;
7
7
  colorMode?: number;
8
8
  colorScaleIndex?: number;
9
+ /** The page-wide index of this mark's filter group: which run of a field's activity block it reads. */
10
+ groupIndex?: number;
9
11
  constantColor?: [number, number, number, number];
10
12
  axisOrder: number[];
11
13
  /** Each axis's scale id, in data (not visual) order. */