@fjader/svelte 0.2.1 → 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 (123) 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 +32 -9
  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 +51 -10
  36. package/dist/components/rendering/Lines.svelte.d.ts +9 -0
  37. package/dist/components/rendering/ParallelLines.svelte +28 -8
  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/data/DataAccessor.js +1 -1
  51. package/dist/gpu/ColorScaleShaderLib.d.ts +14 -2
  52. package/dist/gpu/ColorScaleShaderLib.js +59 -7
  53. package/dist/gpu/GPUCoordinator.svelte.d.ts +8 -0
  54. package/dist/gpu/GPUCoordinator.svelte.js +68 -32
  55. package/dist/gpu/NanShaderLib.d.ts +30 -0
  56. package/dist/gpu/NanShaderLib.js +42 -0
  57. package/dist/gpu/ResolveShader.d.ts +3 -3
  58. package/dist/gpu/ResolveShader.js +6 -5
  59. package/dist/gpu/ScaleTemplateSystem.d.ts +19 -0
  60. package/dist/gpu/ScaleTemplateSystem.js +86 -7
  61. package/dist/gpu/SizeScaleShaderLib.d.ts +5 -5
  62. package/dist/gpu/SizeScaleShaderLib.js +6 -5
  63. package/dist/gpu/decimation/M4Shader.d.ts +13 -7
  64. package/dist/gpu/decimation/M4Shader.js +79 -64
  65. package/dist/gpu/identity-slot-packing.d.ts +157 -40
  66. package/dist/gpu/identity-slot-packing.js +262 -62
  67. package/dist/gpu/tracked-scales.d.ts +14 -0
  68. package/dist/gpu/tracked-scales.js +18 -0
  69. package/dist/index.js +1 -0
  70. package/dist/interactors/BandLabelToggle.svelte +5 -9
  71. package/dist/interactors/BandLabelToggle.svelte.d.ts +3 -3
  72. package/dist/interactors/DerivedInterval1D.svelte +26 -54
  73. package/dist/interactors/Interval1D.svelte +2 -1
  74. package/dist/interactors/Interval2D.svelte +2 -1
  75. package/dist/interactors/ParallelAxisSource.svelte +4 -4
  76. package/dist/interactors/ParallelAxisSource.svelte.d.ts +2 -1
  77. package/dist/interactors/ParallelBrushes.svelte +8 -9
  78. package/dist/interactors/ParallelBrushes.svelte.d.ts +3 -3
  79. package/dist/interactors/Toggle.svelte +17 -36
  80. package/dist/interactors/Toggle.svelte.d.ts +4 -3
  81. package/dist/interactors/Tooltip.svelte +2 -1
  82. package/dist/interactors/TooltipManager.svelte.js +8 -3
  83. package/dist/interactors/relay-clauses.d.ts +23 -0
  84. package/dist/interactors/relay-clauses.js +60 -0
  85. package/dist/query-engine/CrossfilterSelection.svelte.d.ts +88 -15
  86. package/dist/query-engine/CrossfilterSelection.svelte.js +94 -20
  87. package/dist/query-engine/FieldActivity.svelte.d.ts +79 -0
  88. package/dist/query-engine/FieldActivity.svelte.js +207 -0
  89. package/dist/query-engine/FilterGroup.svelte.d.ts +53 -29
  90. package/dist/query-engine/FilterGroup.svelte.js +181 -122
  91. package/dist/query-engine/FilterGroupRegistry.svelte.d.ts +42 -0
  92. package/dist/query-engine/FilterGroupRegistry.svelte.js +103 -1
  93. package/dist/query-engine/MarkHandle.svelte.d.ts +9 -1
  94. package/dist/query-engine/MarkHandle.svelte.js +12 -2
  95. package/dist/query-engine/QueryEngine.d.ts +52 -16
  96. package/dist/query-engine/QueryEngine.js +90 -28
  97. package/dist/query-engine/categorical-identity.d.ts +99 -27
  98. package/dist/query-engine/categorical-identity.js +210 -51
  99. package/dist/query-engine/index.d.ts +2 -1
  100. package/dist/query-engine/index.js +1 -0
  101. package/dist/query-engine/preagg/Preagg.d.ts +1 -1
  102. package/dist/query-engine/preagg/Preagg.js +11 -9
  103. package/dist/query-engine/preagg/plan.d.ts +9 -5
  104. package/dist/query-engine/preagg/plan.js +13 -11
  105. package/dist/query-engine/toggle-predicate.js +12 -1
  106. package/dist/scales/ActivityTable.d.ts +99 -0
  107. package/dist/scales/ActivityTable.js +211 -0
  108. package/dist/scales/BandScaleState.svelte.d.ts +28 -7
  109. package/dist/scales/BandScaleState.svelte.js +55 -20
  110. package/dist/scales/ChartScales.svelte.d.ts +2 -2
  111. package/dist/scales/ColorScaleState.svelte.d.ts +36 -8
  112. package/dist/scales/ColorScaleState.svelte.js +118 -24
  113. package/dist/scales/DomainResolver.d.ts +2 -0
  114. package/dist/scales/DomainResolver.js +15 -8
  115. package/dist/scales/ScaleRegistry.svelte.d.ts +9 -0
  116. package/dist/scales/ScaleRegistry.svelte.js +22 -1
  117. package/dist/scales/TimeScaleState.svelte.js +7 -1
  118. package/dist/spec/index.js +1 -0
  119. package/dist/spec/types.d.ts +2 -0
  120. package/dist/transforms/index.d.ts +27 -0
  121. package/dist/transforms/index.js +97 -3
  122. package/dist/types.d.ts +30 -0
  123. package/package.json +1 -1
@@ -0,0 +1,28 @@
1
+ /**
2
+ * The byte size of every renderer's uniform struct, as WGSL lays it out.
3
+ *
4
+ * Renderers allocate their uniform buffers from these, and BaseGPURenderer
5
+ * sizes its stand-in from the largest: a binding not yet written is bound to
6
+ * the stand-in, and WebGPU rejects a draw whose uniform buffer is smaller than
7
+ * the struct it declares, which loses every chart's frame in that submit. A
8
+ * comment saying "must fit the largest struct" went stale the first time a
9
+ * struct grew, so uniform-sizes.test.ts derives each size from the shader
10
+ * source and fails when one drifts.
11
+ */
12
+ /** Axes a parallel coordinates chart can hold: its axis arrays are sized for it. */
13
+ export const PARALLEL_MAX_AXES = 16;
14
+ /** One ScaleParam: four f32. */
15
+ const SCALE_PARAM_BYTES = 16;
16
+ export const UNIFORM_BYTES = {
17
+ dotsMarkConstants: 48,
18
+ rectsMarkConstants: 48,
19
+ ruleXMarkConstants: 48,
20
+ ruleYMarkConstants: 48,
21
+ linesMarkConstants: 64,
22
+ areasMarkConstants: 64,
23
+ gridLineRuleConstants: 32,
24
+ parallelConfig: 176,
25
+ parallelAxisScales: PARALLEL_MAX_AXES * SCALE_PARAM_BYTES
26
+ };
27
+ /** The stand-in uniform buffer: as large as the largest struct any renderer binds. */
28
+ export const UNIFORM_STAND_IN_BYTES = Math.max(...Object.values(UNIFORM_BYTES));
@@ -1,3 +1,4 @@
1
+ import type { SelectionResponse } from '../components/rendering/activity-reads';
1
2
  import type { FieldRef, LODConfig, SortConfig, ColorScheme, CurveType, Transform } from '../types';
2
3
  import type { MarkLODSurface } from '../lod/rung-types';
3
4
  export interface MarkEntry {
@@ -23,6 +24,7 @@ export interface MarkEntry {
23
24
  scheme?: ColorScheme;
24
25
  transforms?: Transform[];
25
26
  selection?: string;
27
+ selectionResponse?: SelectionResponse;
26
28
  limit?: number;
27
29
  preferredXScaleType?: string | null;
28
30
  preferredYScaleType?: string | null;
@@ -29,7 +29,7 @@ export class ArrowDataAccessor {
29
29
  // FAST PATH: Single chunk numeric - try to return underlying buffer directly.
30
30
  // NULL handling: Query-level COALESCE(CAST(expr AS REAL), 'NaN'::REAL)
31
31
  // converts SQL NULLs to the NaN float value, so Arrow columns from Fjader
32
- // queries have nullCount=0 and clean buffers. GPU shaders detect NaN (v != v)
32
+ // queries have nullCount=0 and clean buffers. GPU shaders detect NaN by its bit pattern (NanShaderLib)
33
33
  // and discard those vertices to create gaps in lines/areas.
34
34
  if (!isStringType && col.data.length === 1) {
35
35
  const chunk = col.data[0];
@@ -15,6 +15,18 @@ export declare const COLOR_CONFIG_BYTES = 48;
15
15
  export declare const COLOR_CONFIG_SLOT_BASE_OFFSET = 32;
16
16
  /** `slotBase` meaning "this channel already carries a palette index". */
17
17
  export declare const NO_SLOT_TABLE = 4294967295;
18
+ /**
19
+ * The rank of a category an ordinal colour scale draws in the null colour: it
20
+ * takes no palette slot, so it reads as missing data and shifts no category's
21
+ * colour. The NULL category and the Other buckets. -1, by contrast, is a category
22
+ * outside this scale's domain, which draws transparent.
23
+ */
24
+ export declare const NULL_COLOR_RANK = -2;
25
+ /**
26
+ * How many colour configs the buffer holds. A uniform array has a fixed length
27
+ * in WGSL, so the shader declaration and the buffer size both read this.
28
+ */
29
+ export declare const MAX_COLOR_SLOTS = 64;
18
30
  export declare const COLOR_SCALE_STRUCTS = "\nstruct ColorConfig {\n domainMin: f32, // byte 0 \u2014 pre-transformed for non-linear types\n domainMax: f32, // byte 4 \u2014 pre-transformed for non-linear types\n rampOffset: u32, // byte 8\n paletteOffset: u32, // byte 12\n paletteSize: u32, // byte 16\n scaleType: u32, // byte 20 \u2014 scheme rendering type (ordinal/sequential/diverging/cyclical)\n transformType: u32, // byte 24 \u2014 normalization transform (linear/log/symlog/sqrt/pow)\n transformParam: f32, // byte 28 \u2014 symlog constant C, or pow exponent\n slotBase: u32, // byte 32 \u2014 base into identitySlots, or NO_SLOT_TABLE\n _pad0: u32, // byte 36\n _pad1: u32, // byte 40\n _pad2: u32, // byte 44\n}\n\n/**\n * slotBase when this scale's channel already carries a palette index rather\n * than an identity id. Distinct from base 0, which is a real block \u2014 the\n * permanently empty one \u2014 so it cannot be confused with \"not placed\".\n */\nconst NO_SLOT_TABLE: u32 = 0xFFFFFFFFu;\n\nconst COLOR_SCALE_ORDINAL: u32 = 0u;\nconst COLOR_SCALE_SEQUENTIAL: u32 = 1u;\nconst COLOR_SCALE_DIVERGING: u32 = 2u;\nconst COLOR_SCALE_CYCLICAL: u32 = 3u;\n\nconst COLOR_TRANSFORM_LINEAR: u32 = 0u;\nconst COLOR_TRANSFORM_LOG: u32 = 1u;\nconst COLOR_TRANSFORM_SYMLOG: u32 = 2u;\nconst COLOR_TRANSFORM_SQRT: u32 = 3u;\nconst COLOR_TRANSFORM_POW: u32 = 4u;\n";
19
- export declare const COLOR_SCALE_BINDINGS = "\n@group(2) @binding(1) var<storage, read> colorConfigs: array<ColorConfig>;\n@group(2) @binding(2) var<storage, read> colorRamps: array<vec4<f32>>;\n@group(2) @binding(3) var<storage, read> ordinalPalettes: array<vec4<f32>>;\n";
20
- export declare const COLOR_SCALE_FUNCTIONS = "\nfn applyColorTransform(config: ColorConfig, value: f32) -> f32 {\n switch config.transformType {\n case COLOR_TRANSFORM_LOG: {\n return log(max(value, 1e-38));\n }\n case COLOR_TRANSFORM_SYMLOG: {\n let c = config.transformParam;\n return sign(value) * log(1.0 + abs(value / c));\n }\n case COLOR_TRANSFORM_SQRT: {\n return sign(value) * sqrt(abs(value));\n }\n case COLOR_TRANSFORM_POW: {\n return sign(value) * pow(abs(value), config.transformParam);\n }\n default: {\n return value;\n }\n }\n}\n\nfn normalizeValue(config: ColorConfig, value: f32) -> f32 {\n let transformed = applyColorTransform(config, value);\n\n let range = config.domainMax - config.domainMin;\n\n if (abs(range) < 0.0001) {\n return 0.5;\n }\n\n let t_raw = (transformed - config.domainMin) / range;\n\n if (config.scaleType == COLOR_SCALE_CYCLICAL) {\n return fract(t_raw);\n }\n\n return clamp(t_raw, 0.0, 1.0);\n}\n\nfn sampleRamp(config: ColorConfig, t: f32) -> vec4<f32> {\n let idx = min(u32(t * 255.0), 255u); // Must match COLOR_RAMP_RESOLUTION (256 stops)\n return colorRamps[config.rampOffset + idx];\n}\n\nfn samplePalette(config: ColorConfig, categoryIndex: u32) -> vec4<f32> {\n // The channel carries an identity id, which is stable and has nothing to do\n // with this scale's palette. Resolve it to this scale's own rank first; the\n // palette is indexed by rank, so the plotted categories take palette colours\n // in order and the wrap below is over the PLOTTED count, not the dim's.\n var idx = categoryIndex;\n if (config.slotBase != NO_SLOT_TABLE) {\n let slot = identitySlot(config.slotBase, categoryIndex);\n if (slot < 0.0) {\n // Not ranked by this scale: a category outside its domain.\n return vec4<f32>(0.0, 0.0, 0.0, 0.0);\n }\n idx = u32(slot);\n }\n return ordinalPalettes[config.paletteOffset + (idx % config.paletteSize)];\n}\n\nfn getColorFromScale(scaleIndex: u32, value: f32, nullColor: vec4<f32>) -> vec4<f32> {\n let config = colorConfigs[scaleIndex];\n\n if (config.scaleType == COLOR_SCALE_ORDINAL) {\n // 0-based indexing: -1 = null, 0+ = valid categories\n if (value < 0.0) {\n return nullColor;\n }\n let catIdx = u32(value);\n return samplePalette(config, catIdx);\n }\n\n if (value != value) {\n return nullColor;\n }\n\n // Log requires strictly positive values \u2014 treat \u22640 as unmappable\n if (config.transformType == COLOR_TRANSFORM_LOG && value <= 0.0) {\n return nullColor;\n }\n\n let t = normalizeValue(config, value);\n return sampleRamp(config, t);\n}\n\nfn getOrdinalColor(scaleIndex: u32, categoryIndex: u32, nullColor: vec4<f32>) -> vec4<f32> {\n // Note: This function expects u32, so caller must handle -1 check before casting\n let config = colorConfigs[scaleIndex];\n return samplePalette(config, categoryIndex);\n}\n\nfn getContinuousColor(scaleIndex: u32, value: f32, nullColor: vec4<f32>) -> vec4<f32> {\n if (value != value) {\n return nullColor;\n }\n let config = colorConfigs[scaleIndex];\n if (config.transformType == COLOR_TRANSFORM_LOG && value <= 0.0) {\n return nullColor;\n }\n let t = normalizeValue(config, value);\n return sampleRamp(config, t);\n}\n";
31
+ export declare const COLOR_SCALE_BINDINGS = "\n@group(2) @binding(1) var<uniform> colorConfigs: array<ColorConfig, 64>;\n@group(2) @binding(2) var<storage, read> colorRamps: array<vec4<f32>>;\n@group(2) @binding(3) var<storage, read> ordinalPalettes: array<vec4<f32>>;\n";
32
+ export declare const COLOR_SCALE_FUNCTIONS: string;
@@ -1,3 +1,4 @@
1
+ import { isNanExpr } from './NanShaderLib';
1
2
  // src/lib/gpu/ColorScaleShaderLib.ts
2
3
  // ---------------------------------------------------------------------------
3
4
  // Design note: branching vs JIT-compiled shaders for color normalization
@@ -35,6 +36,18 @@ export const COLOR_CONFIG_BYTES = 48;
35
36
  export const COLOR_CONFIG_SLOT_BASE_OFFSET = 32;
36
37
  /** `slotBase` meaning "this channel already carries a palette index". */
37
38
  export const NO_SLOT_TABLE = 0xffffffff;
39
+ /**
40
+ * The rank of a category an ordinal colour scale draws in the null colour: it
41
+ * takes no palette slot, so it reads as missing data and shifts no category's
42
+ * colour. The NULL category and the Other buckets. -1, by contrast, is a category
43
+ * outside this scale's domain, which draws transparent.
44
+ */
45
+ export const NULL_COLOR_RANK = -2;
46
+ /**
47
+ * How many colour configs the buffer holds. A uniform array has a fixed length
48
+ * in WGSL, so the shader declaration and the buffer size both read this.
49
+ */
50
+ export const MAX_COLOR_SLOTS = 64;
38
51
  export const COLOR_SCALE_STRUCTS = /* wgsl */ `
39
52
  struct ColorConfig {
40
53
  domainMin: f32, // byte 0 — pre-transformed for non-linear types
@@ -69,8 +82,14 @@ const COLOR_TRANSFORM_SYMLOG: u32 = 2u;
69
82
  const COLOR_TRANSFORM_SQRT: u32 = 3u;
70
83
  const COLOR_TRANSFORM_POW: u32 = 4u;
71
84
  `;
85
+ // colorConfigs is a UNIFORM, visible to the vertex stage as well: selection
86
+ // dimming reads a colour scale's slotBase per vertex. As storage it would take
87
+ // the last of the vertex stage's default 8 storage buffers on Rects, which uses
88
+ // 7 (six channels and the identity slot table); as a uniform it leaves that
89
+ // slot free. 64 × 48 bytes is 3 KB, written when a scale changes, which is what
90
+ // uniform buffers are for.
72
91
  export const COLOR_SCALE_BINDINGS = /* wgsl */ `
73
- @group(2) @binding(1) var<storage, read> colorConfigs: array<ColorConfig>;
92
+ @group(2) @binding(1) var<uniform> colorConfigs: array<ColorConfig, ${MAX_COLOR_SLOTS}>;
74
93
  @group(2) @binding(2) var<storage, read> colorRamps: array<vec4<f32>>;
75
94
  @group(2) @binding(3) var<storage, read> ordinalPalettes: array<vec4<f32>>;
76
95
  `;
@@ -119,7 +138,7 @@ fn sampleRamp(config: ColorConfig, t: f32) -> vec4<f32> {
119
138
  return colorRamps[config.rampOffset + idx];
120
139
  }
121
140
 
122
- fn samplePalette(config: ColorConfig, categoryIndex: u32) -> vec4<f32> {
141
+ fn samplePalette(config: ColorConfig, categoryIndex: u32, nullColor: vec4<f32>) -> vec4<f32> {
123
142
  // The channel carries an identity id, which is stable and has nothing to do
124
143
  // with this scale's palette. Resolve it to this scale's own rank first; the
125
144
  // palette is indexed by rank, so the plotted categories take palette colours
@@ -128,7 +147,12 @@ fn samplePalette(config: ColorConfig, categoryIndex: u32) -> vec4<f32> {
128
147
  if (config.slotBase != NO_SLOT_TABLE) {
129
148
  let slot = identitySlot(config.slotBase, categoryIndex);
130
149
  if (slot < 0.0) {
131
- // Not ranked by this scale: a category outside its domain.
150
+ // ${NULL_COLOR_RANK}: a category drawn in the null colour, which takes no
151
+ // palette slot (the NULL category, an Other bucket).
152
+ if (slot < -1.5) {
153
+ return nullColor;
154
+ }
155
+ // -1: not ranked by this scale, a category outside its domain.
132
156
  return vec4<f32>(0.0, 0.0, 0.0, 0.0);
133
157
  }
134
158
  idx = u32(slot);
@@ -145,10 +169,10 @@ fn getColorFromScale(scaleIndex: u32, value: f32, nullColor: vec4<f32>) -> vec4<
145
169
  return nullColor;
146
170
  }
147
171
  let catIdx = u32(value);
148
- return samplePalette(config, catIdx);
172
+ return samplePalette(config, catIdx, nullColor);
149
173
  }
150
174
 
151
- if (value != value) {
175
+ if (${isNanExpr('value')}) {
152
176
  return nullColor;
153
177
  }
154
178
 
@@ -164,11 +188,11 @@ fn getColorFromScale(scaleIndex: u32, value: f32, nullColor: vec4<f32>) -> vec4<
164
188
  fn getOrdinalColor(scaleIndex: u32, categoryIndex: u32, nullColor: vec4<f32>) -> vec4<f32> {
165
189
  // Note: This function expects u32, so caller must handle -1 check before casting
166
190
  let config = colorConfigs[scaleIndex];
167
- return samplePalette(config, categoryIndex);
191
+ return samplePalette(config, categoryIndex, nullColor);
168
192
  }
169
193
 
170
194
  fn getContinuousColor(scaleIndex: u32, value: f32, nullColor: vec4<f32>) -> vec4<f32> {
171
- if (value != value) {
195
+ if (${isNanExpr('value')}) {
172
196
  return nullColor;
173
197
  }
174
198
  let config = colorConfigs[scaleIndex];
@@ -178,4 +202,32 @@ fn getContinuousColor(scaleIndex: u32, value: f32, nullColor: vec4<f32>) -> vec4
178
202
  let t = normalizeValue(config, value);
179
203
  return sampleRamp(config, t);
180
204
  }
205
+
206
+ /**
207
+ * How selected this value's category is on an ordinal colour scale, 0..1.
208
+ *
209
+ * Read per vertex for selection dimming. 1.0 for anything that has no category
210
+ * to select: a continuous scale, an ordinal scale whose channel does not carry
211
+ * identity ids yet (NO_SLOT_TABLE), and a missing category. The -1 and NaN
212
+ * checks come before the cast, because u32() of a negative or NaN float is
213
+ * indeterminate in WGSL and would read an arbitrary activity.
214
+ */
215
+ fn colorActivity(scaleIndex: u32, value: f32, group: u32) -> f32 {
216
+ if (value < 0.0 || ${isNanExpr('value')}) {
217
+ return 1.0;
218
+ }
219
+ let config = colorConfigs[scaleIndex];
220
+ if (config.scaleType != COLOR_SCALE_ORDINAL || config.slotBase == NO_SLOT_TABLE) {
221
+ return 1.0;
222
+ }
223
+ return identityActivity(config.slotBase, group, u32(value));
224
+ }
225
+
226
+ /** A mark's colour activity in its filter group: 1.0 unless it colours through a scale. */
227
+ fn markColorActivity(colorMode: u32, colorScaleIndex: u32, colorValue: f32, group: u32) -> f32 {
228
+ if (colorMode != 1u) {
229
+ return 1.0;
230
+ }
231
+ return colorActivity(colorScaleIndex, colorValue, group);
232
+ }
181
233
  `;
@@ -1,5 +1,6 @@
1
1
  import type { RendererDefinition } from './ScaleTemplateSystem';
2
2
  import type { ScaleRegistry } from '../scales/ScaleRegistry.svelte';
3
+ import type { FilterGroupRegistry } from '../query-engine/FilterGroupRegistry.svelte';
3
4
  /** Number of color stops per ramp slot. Shared with ColorScaleState for bakeGradient(). */
4
5
  export declare const COLOR_RAMP_RESOLUTION = 256;
5
6
  interface LayerConfig {
@@ -182,6 +183,13 @@ export declare class GPUCoordinator {
182
183
  */
183
184
  releaseRenderer(rendererId: string): void;
184
185
  setScaleRegistry(scaleRegistry: ScaleRegistry): void;
186
+ /**
187
+ * The page's filter groups, whose field activity the packer writes into the
188
+ * identity slot table. Their fades run on this loop and redraw the charts
189
+ * drawing any scale on their field. Set before any selection, so no fade is
190
+ * created without the loop.
191
+ */
192
+ setFilterGroups(filterGroups: FilterGroupRegistry): void;
185
193
  registerScale(scaleId: string, scaleType?: string): void;
186
194
  unregisterScale(scaleId: string): void;
187
195
  scheduleTopologyUpdate(): void;
@@ -7,7 +7,8 @@ import { RESOLVE_IMMEDIATE_SIZE, RESOLVE_WORKGROUP, resolvePipelineFor } from '.
7
7
  import { IDENTITY_SLOT_CAPACITY, IDENTITY_SLOT_EMPTY, createIdentitySlotState, packIdentitySlots } from './identity-slot-packing';
8
8
  import { frameUniformIsStale } from './frame-uniform-sync';
9
9
  import { immediatesSupported, IMMEDIATES_MISSING_MESSAGE } from './immediates';
10
- import { COLOR_CONFIG_BYTES, COLOR_CONFIG_SLOT_BASE_OFFSET } from './ColorScaleShaderLib';
10
+ import { COLOR_CONFIG_BYTES, COLOR_CONFIG_SLOT_BASE_OFFSET, MAX_COLOR_SLOTS } from './ColorScaleShaderLib';
11
+ import { trackScalesById } from './tracked-scales';
11
12
  /** Number of color stops per ramp slot. Shared with ColorScaleState for bakeGradient(). */
12
13
  export const COLOR_RAMP_RESOLUTION = 256;
13
14
  /**
@@ -18,6 +19,8 @@ export const COLOR_RAMP_RESOLUTION = 256;
18
19
  function layerCanDraw(layer, chartScalesKnown) {
19
20
  return (!!layer.renderer?.perDataBindGroup && (chartScalesKnown || layer.readsChartScales === false));
20
21
  }
22
+ /** No fields to pack, for a coordinator without filter groups. */
23
+ const NO_FIELDS = [];
21
24
  // =============================================================================
22
25
  // HELPER CLASSES
23
26
  // =============================================================================
@@ -113,6 +116,7 @@ export class GPUCoordinator {
113
116
  #animatedSources = new Set(); // Set for O(1) membership check
114
117
  #animatedSourcesArray = []; // Array for O(1) iteration (no allocation)
115
118
  #scaleRegistry = null;
119
+ #filterGroups = null;
116
120
  // =========================================================================
117
121
  // HOVER STATE (Keep loop warm while any chart is hovered)
118
122
  // =========================================================================
@@ -196,7 +200,7 @@ export class GPUCoordinator {
196
200
  #sizeConfigBuffer = null; // MAX_SIZE_SLOTS × 16 bytes = 1KB
197
201
  #maxSizeSlots = MAX_SIZE_SLOTS;
198
202
  // Constants
199
- #maxColorSlots = 64;
203
+ #maxColorSlots = MAX_COLOR_SLOTS;
200
204
  #rampColorsPerSlot = COLOR_RAMP_RESOLUTION;
201
205
  #paletteColorsPerSlot = 32;
202
206
  // =========================================================================
@@ -363,10 +367,17 @@ export class GPUCoordinator {
363
367
  label: 'scale-resources-layout',
364
368
  entries: [
365
369
  {
366
- // Binding 1: Color scale configs (fragment only)
370
+ // Binding 1: Color scale configs.
371
+ //
372
+ // Uniform, and visible to the VERTEX stage as well: selection
373
+ // dimming reads a colour scale's slotBase per vertex. As storage
374
+ // it would take the last of the vertex stage's default 8 storage
375
+ // buffers on Rects, which uses 7; as a uniform it counts against
376
+ // the separate uniform limit and leaves that slot free. Same move
377
+ // as binding 4.
367
378
  binding: 1,
368
- visibility: GPUShaderStage.FRAGMENT, // ← Changed
369
- buffer: { type: 'read-only-storage' }
379
+ visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT,
380
+ buffer: { type: 'uniform' }
370
381
  },
371
382
  {
372
383
  // Binding 2: Color ramps (fragment only)
@@ -400,8 +411,9 @@ export class GPUCoordinator {
400
411
  // buffer and do no interpolation.
401
412
  //
402
413
  // Storage, where it used to be two uniforms. It costs the vertex
403
- // stage one binding where the pair cost none — Rects lands at 8 of
404
- // 8, the default maxStorageBuffersPerShaderStage — and in exchange
414
+ // stage one binding where the pair cost none — Rects lands at 7 of
415
+ // the default maxStorageBuffersPerShaderStage of 8, its six channels
416
+ // and this — and in exchange
405
417
  // the vec4 packing, the 64 KB uniform binding cap and the
406
418
  // 16384-float global ceiling all go away with the address space.
407
419
  //
@@ -409,8 +421,9 @@ export class GPUCoordinator {
409
421
  // the compute pipeline, which has its own budget.
410
422
  //
411
423
  // FRAGMENT as well as VERTEX: band axes resolve a slot in the
412
- // vertex stage, ordinal colour carries the same ids and resolves
413
- // in the fragment stage.
424
+ // vertex stage, and ordinal colour resolves its palette rank in
425
+ // the fragment stage. Both read their selection activity, the
426
+ // second half of each block, in the vertex stage.
414
427
  binding: 5,
415
428
  visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT,
416
429
  buffer: { type: 'read-only-storage' }
@@ -435,11 +448,12 @@ export class GPUCoordinator {
435
448
  }
436
449
  #createColorScaleBuffers() {
437
450
  const device = this.#device;
438
- // ColorConfig buffer: 64 slots × 48 bytes each
451
+ // ColorConfig buffer: 64 slots × 48 bytes each. A uniform, so the vertex
452
+ // stage can read it without a storage binding — see COLOR_SCALE_BINDINGS.
439
453
  this.#colorConfigBuffer = device.createBuffer({
440
- label: 'ColorConfig SSBO',
454
+ label: 'ColorConfig uniform',
441
455
  size: this.#maxColorSlots * COLOR_CONFIG_BYTES,
442
- usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST
456
+ usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
443
457
  });
444
458
  // Color ramp buffer: 64 slots × 256 colors × 4 floats × 4 bytes
445
459
  this.#colorRampBuffer = device.createBuffer({
@@ -578,9 +592,16 @@ export class GPUCoordinator {
578
592
  const slots = scale.slots;
579
593
  if (!slots)
580
594
  continue;
581
- const entry = (pool[scales.length] ??= { id: '', slots });
595
+ const entry = (pool[scales.length] ??= { id: '', slots, field: null });
582
596
  entry.id = scale.id;
583
597
  entry.slots = slots;
598
+ // The field whose activity block this scale's header points at. Every
599
+ // field a scale draws has one, so a selection moves nothing; the
600
+ // registry creates its FieldActivity once (a Map lookup after that).
601
+ const field = scale.identityKey ?? null;
602
+ entry.field = field;
603
+ if (field)
604
+ this.#filterGroups?.field(field);
584
605
  scales.push(entry);
585
606
  // Index-aligned with `scales`, so the base can be handed back after
586
607
  // packing without searching for the instance again.
@@ -588,7 +609,14 @@ export class GPUCoordinator {
588
609
  ? scale
589
610
  : null);
590
611
  }
591
- const { uploadStart, uploadEnd } = packIdentitySlots(scales, state);
612
+ // Every field follows its live identity here, where nothing is reactive:
613
+ // a reload snaps its fades, a grown dim extends them. Two comparisons a
614
+ // field when nothing moved.
615
+ const registry = this.#filterGroups;
616
+ const fields = registry?.fields ?? NO_FIELDS;
617
+ for (let i = 0; i < fields.length; i++)
618
+ fields[i].reconcile();
619
+ const { uploadStart, uploadEnd } = packIdentitySlots(scales, state, fields, registry?.groupCount ?? 0);
592
620
  // Hand back the base to any scale that reads it from its own config rather
593
621
  // than from p4. A band scale's base is refilled into p4 every frame; an
594
622
  // ordinal colour scale's lives in a config uploaded only when the scale
@@ -960,12 +988,13 @@ export class GPUCoordinator {
960
988
  }
961
989
  const passEncoder = commandEncoder.beginRenderPass(this.#createRenderPassDescriptor(view, chartConfig));
962
990
  // The chart's scales, written once for the whole pass and before the
963
- // first draw. Every pipeline declares the same 32 bytes and a draw
964
- // requires every declared byte to have been set, so this is the whole
965
- // range, every pass. Immediate data survives setPipeline between
966
- // layouts of the same size, which is what makes once enough. Pushed even
967
- // when the chart's scales are not both known: a layer that does not
968
- // read them still needs every byte set before it may draw.
991
+ // first draw. Every pipeline declares the same
992
+ // CHART_SCALE_IMMEDIATE_SIZE and a draw requires every declared byte to
993
+ // have been set, so this is the whole range, every pass. Immediate data
994
+ // survives setPipeline between layouts of the same size, which is what
995
+ // makes once enough. Pushed even when the chart's scales are not both
996
+ // known: a layer that does not read them still needs every byte set
997
+ // before it may draw.
969
998
  const scalesKnown = this.#writeChartScales(chartConfig, this.#chartScaleImmediates);
970
999
  passEncoder.setImmediates(0, this.#chartScaleImmediates);
971
1000
  if (chartConfig.perFrameBindGroup) {
@@ -1226,6 +1255,16 @@ export class GPUCoordinator {
1226
1255
  setScaleRegistry(scaleRegistry) {
1227
1256
  this.#scaleRegistry = scaleRegistry;
1228
1257
  }
1258
+ /**
1259
+ * The page's filter groups, whose field activity the packer writes into the
1260
+ * identity slot table. Their fades run on this loop and redraw the charts
1261
+ * drawing any scale on their field. Set before any selection, so no fade is
1262
+ * created without the loop.
1263
+ */
1264
+ setFilterGroups(filterGroups) {
1265
+ this.#filterGroups = filterGroups;
1266
+ filterGroups.setAnimation(this, (key, out) => this.#scaleRegistry ? this.#scaleRegistry.chartsDrawingField(key, out) : out);
1267
+ }
1229
1268
  registerScale(scaleId, scaleType = 'linear') {
1230
1269
  if (!this.#initialized)
1231
1270
  return;
@@ -1249,18 +1288,15 @@ export class GPUCoordinator {
1249
1288
  const usageMap = this.#scaleRegistry.scaleUsage;
1250
1289
  if (!usageMap || typeof usageMap[Symbol.iterator] !== 'function')
1251
1290
  return;
1252
- // Clear and rebuild
1253
- this.#scaleIdToInstance.clear();
1254
- for (const [scaleInstance] of usageMap) {
1255
- // Only positional scales (Linear, Band, Time) carry an id.
1256
- const scale = scaleInstance;
1257
- if (!scale?.id)
1258
- continue;
1259
- // The redraw list is NOT rebuilt here any more. It belongs to the
1260
- // registry, derived from the same usage map this reads, and a scale
1261
- // asks for it. Copying it from a second owner is how it went stale.
1262
- this.#scaleIdToInstance.set(scale.id, scale);
1263
- }
1291
+ // Clear and rebuild: every scale with an id — positional scales, and
1292
+ // ordinal colour, whose slots the packer needs too. The id map is only
1293
+ // looked up with a chart's x and y scale ids, so a colour scale in it is
1294
+ // never mistaken for a positional one.
1295
+ //
1296
+ // The redraw list is NOT rebuilt here any more. It belongs to the
1297
+ // registry, derived from the same usage map this reads, and a scale asks
1298
+ // for it. Copying it from a second owner is how it went stale.
1299
+ trackScalesById(usageMap.keys(), this.#scaleIdToInstance);
1264
1300
  this.#scales = [...this.#scaleIdToInstance.values()];
1265
1301
  }
1266
1302
  // =========================================================================
@@ -0,0 +1,30 @@
1
+ /**
2
+ * NaN tests for WGSL that a shader compiler cannot remove.
3
+ *
4
+ * fjader sends nulls to the GPU as NaN. WGSL lets an implementation assume
5
+ * floats are finite, and Apple's Metal compilers act on that: `v != v` and
6
+ * `!(v == v)` can compile to `false`, so a NaN goes undetected. Whether that
7
+ * happens depends on the GPU generation and on the code around the test, so a
8
+ * check that works on one Mac can be gone on another. /test/nan-probe shows
9
+ * what a given GPU does.
10
+ *
11
+ * These test the bit pattern instead: an exponent of all ones and a non-zero
12
+ * mantissa. That is integer arithmetic, which no float assumption touches.
13
+ *
14
+ * `NAN_BITS` is the quiet NaN written wherever a shader needs to produce one.
15
+ * Writing it as bits into a u32 view of the buffer keeps it exact; producing
16
+ * a NaN through float arithmetic is not guaranteed to.
17
+ */
18
+ /** The quiet NaN fjader writes, as bits. */
19
+ export declare const NAN_BITS_HEX = "0x7FC00000u";
20
+ /**
21
+ * `isNanBits(u)` for a value read through a u32 view, and `isNaN(v)` for one
22
+ * already loaded as f32. Prefer the u32 view where a shader can read the
23
+ * buffer that way: it never loads the NaN as a float at all.
24
+ */
25
+ export declare const NAN_WGSL = "\nconst NAN_BITS: u32 = 0x7FC00000u;\n\nfn isNanBits(u: u32) -> bool {\n\treturn (u & 0x7fffffffu) > 0x7f800000u;\n}\n\nfn isNaN(v: f32) -> bool {\n\treturn isNanBits(bitcast<u32>(v));\n}\n";
26
+ /**
27
+ * The same test as an expression, for shader libraries that are pasted into
28
+ * other shaders and so cannot define functions those shaders may define too.
29
+ */
30
+ export declare function isNanExpr(f32Expr: string): string;
@@ -0,0 +1,42 @@
1
+ /**
2
+ * NaN tests for WGSL that a shader compiler cannot remove.
3
+ *
4
+ * fjader sends nulls to the GPU as NaN. WGSL lets an implementation assume
5
+ * floats are finite, and Apple's Metal compilers act on that: `v != v` and
6
+ * `!(v == v)` can compile to `false`, so a NaN goes undetected. Whether that
7
+ * happens depends on the GPU generation and on the code around the test, so a
8
+ * check that works on one Mac can be gone on another. /test/nan-probe shows
9
+ * what a given GPU does.
10
+ *
11
+ * These test the bit pattern instead: an exponent of all ones and a non-zero
12
+ * mantissa. That is integer arithmetic, which no float assumption touches.
13
+ *
14
+ * `NAN_BITS` is the quiet NaN written wherever a shader needs to produce one.
15
+ * Writing it as bits into a u32 view of the buffer keeps it exact; producing
16
+ * a NaN through float arithmetic is not guaranteed to.
17
+ */
18
+ /** The quiet NaN fjader writes, as bits. */
19
+ export const NAN_BITS_HEX = '0x7FC00000u';
20
+ /**
21
+ * `isNanBits(u)` for a value read through a u32 view, and `isNaN(v)` for one
22
+ * already loaded as f32. Prefer the u32 view where a shader can read the
23
+ * buffer that way: it never loads the NaN as a float at all.
24
+ */
25
+ export const NAN_WGSL = /* wgsl */ `
26
+ const NAN_BITS: u32 = ${NAN_BITS_HEX};
27
+
28
+ fn isNanBits(u: u32) -> bool {
29
+ return (u & 0x7fffffffu) > 0x7f800000u;
30
+ }
31
+
32
+ fn isNaN(v: f32) -> bool {
33
+ return isNanBits(bitcast<u32>(v));
34
+ }
35
+ `;
36
+ /**
37
+ * The same test as an expression, for shader libraries that are pasted into
38
+ * other shaders and so cannot define functions those shaders may define too.
39
+ */
40
+ export function isNanExpr(f32Expr) {
41
+ return `((bitcast<u32>(${f32Expr}) & 0x7fffffffu) > 0x7f800000u)`;
42
+ }
@@ -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.
@@ -69,7 +69,7 @@ export declare const RESOLVE_IMMEDIATE_SIZE = 12;
69
69
  * hand every dispatch the last one's values. Immediates are pass state, set in
70
70
  * command order, so each dispatch reads its own.
71
71
  */
72
- export declare const RESOLVE_SHADER = "\nrequires immediate_address_space;\n\nstruct Params {\n // Elements in this range. The rest of the buffer is left alone.\n count: u32,\n // 0 -> from, 1 -> to. Shared by every element of the range.\n t: f32,\n // Where the range starts, in all three buffers alike.\n offset: u32,\n};\n\n@group(0) @binding(0) var<storage, read> valueFrom: array<f32>;\n@group(0) @binding(1) var<storage, read> valueTo: array<f32>;\n@group(0) @binding(2) var<storage, read_write> resolved: array<f32>;\nvar<immediate> params: Params;\n\n@compute @workgroup_size(64)\nfn main(@builtin(global_invocation_id) gid: vec3<u32>) {\n if (gid.x >= params.count) {\n return;\n }\n let j = params.offset + gid.x;\n let a = valueFrom[j];\n let b = valueTo[j];\n // A NaN on either side means one end of this transition is not a number, so\n // there is nothing to interpolate along. Take the destination and be done.\n // Note the test has to be explicit: every comparison against NaN is false, so\n // mix() would silently propagate it instead.\n // Equal ends take b too, so they come out exact on any driver \u2014 see above.\n let takeTo = a == b || a != a || b != b;\n resolved[j] = select(mix(a, b, params.t), b, takeTo);\n}\n";
72
+ export declare const RESOLVE_SHADER: string;
73
73
  /**
74
74
  * The same expression in JS, and the reason it exists.
75
75
  *
@@ -1,3 +1,4 @@
1
+ import { isNanExpr } from './NanShaderLib';
1
2
  /**
2
3
  * The resolve kernel: `resolved[j] = mix(from[j], to[j], t)` over one range.
3
4
  *
@@ -6,8 +7,8 @@
6
7
  * are bound to the compute pipeline; the render stages bind `resolved` alone.
7
8
  * So an animated channel REPLACES a vertex binding rather than adding one, and
8
9
  * 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.
10
+ * which is what makes this affordable on a mark that already uses 7 of the
11
+ * default 8 storage buffers (Rects).
11
12
  *
12
13
  * ONE KERNEL FOR BOTH, because the two differ only in where a range starts. A
13
14
  * value channel is one mark's rows in buffers of their own, so it starts at 0.
@@ -96,10 +97,10 @@ fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
96
97
  let b = valueTo[j];
97
98
  // A NaN on either side means one end of this transition is not a number, so
98
99
  // there is nothing to interpolate along. Take the destination and be done.
99
- // Note the test has to be explicit: every comparison against NaN is false, so
100
- // mix() would silently propagate it instead.
100
+ // Note the test has to be explicit, and on the bits: mix() would silently
101
+ // propagate a NaN, and a compiler may fold a float test like a != a to false.
101
102
  // Equal ends take b too, so they come out exact on any driver — see above.
102
- let takeTo = a == b || a != a || b != b;
103
+ let takeTo = a == b || ${isNanExpr('a')} || ${isNanExpr('b')};
103
104
  resolved[j] = select(mix(a, b, params.t), b, takeTo);
104
105
  }
105
106
  `;
@@ -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
  *