@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
@@ -47,6 +47,7 @@
47
47
  let {
48
48
  table,
49
49
  selection = undefined,
50
+ selectionResponse = undefined,
50
51
  x = null,
51
52
  x1: x1Prop = null,
52
53
  x2: x2Prop = null,
@@ -95,6 +96,7 @@
95
96
  opacity: () => opacity,
96
97
  transforms: () => transforms,
97
98
  selection: () => selection,
99
+ selectionResponse: () => selectionResponse,
98
100
  limit: () => limit,
99
101
  preferredXScaleType: () => preferredXScaleType,
100
102
  preferredYScaleType: () => preferredYScaleType,
@@ -360,6 +362,7 @@
360
362
  alphaData={opacityData}
361
363
  colorMode={markColor.colorMode}
362
364
  colorScaleIndex={markColor.colorScaleIndex}
365
+ groupIndex={setup?.groupIndex ?? 0}
363
366
  constantColor={markColor.constantColor}
364
367
  facetData={setup?.facetData}
365
368
  dataVersion={setup?.dataVersion || 0}
@@ -47,6 +47,7 @@
47
47
  let {
48
48
  table,
49
49
  selection = undefined,
50
+ selectionResponse = undefined,
50
51
  x,
51
52
  x1: x1Prop,
52
53
  x2: x2Prop,
@@ -90,6 +91,7 @@
90
91
  opacity: () => opacity,
91
92
  transforms: () => transforms,
92
93
  selection: () => selection,
94
+ selectionResponse: () => selectionResponse,
93
95
  limit: () => limit,
94
96
  preferredYScaleType: () => preferredYScaleType,
95
97
  fitDomain: () => fitDomain,
@@ -351,6 +353,7 @@
351
353
  alphaData={opacityData}
352
354
  colorMode={markColor.colorMode}
353
355
  colorScaleIndex={markColor.colorScaleIndex}
356
+ groupIndex={setup?.groupIndex ?? 0}
354
357
  constantColor={markColor.constantColor}
355
358
  facetData={setup?.facetData}
356
359
  dataVersion={setup?.dataVersion || 0}
@@ -48,6 +48,7 @@
48
48
  let {
49
49
  table,
50
50
  selection = undefined,
51
+ selectionResponse = undefined,
51
52
  x = null,
52
53
  x1: x1Prop = null,
53
54
  x2: x2Prop = null,
@@ -92,6 +93,7 @@
92
93
  opacity: () => opacity,
93
94
  transforms: () => transforms,
94
95
  selection: () => selection,
96
+ selectionResponse: () => selectionResponse,
95
97
  limit: () => limit,
96
98
  preferredXScaleType: () => preferredXScaleType,
97
99
  fitDomain: () => fitDomain,
@@ -358,6 +360,7 @@
358
360
  alphaData={opacityData}
359
361
  colorMode={markColor.colorMode}
360
362
  colorScaleIndex={markColor.colorScaleIndex}
363
+ groupIndex={setup?.groupIndex ?? 0}
361
364
  constantColor={markColor.constantColor}
362
365
  facetData={setup?.facetData}
363
366
  dataVersion={setup?.dataVersion || 0}
@@ -1,4 +1,5 @@
1
1
  <script lang="ts">
2
+ import { ruleHasExtent } from '../rendering/activity-reads';
2
3
  import { getContext, onDestroy, untrack } from 'svelte';
3
4
  import { MarkLifecycleV2 } from './lifecycle/MarkLifecycleV2.svelte';
4
5
  import { RuleX, GridLine } from '../rendering';
@@ -34,6 +35,7 @@
34
35
  let {
35
36
  table = undefined,
36
37
  selection = undefined,
38
+ selectionResponse = undefined,
37
39
  x,
38
40
  y: yShorthand = undefined,
39
41
  y1: y1Prop = undefined,
@@ -93,7 +95,7 @@
93
95
  return undefined;
94
96
  });
95
97
 
96
- const hasExtent = $derived(y1 !== undefined && y2 !== undefined);
98
+ const hasExtent = $derived(ruleHasExtent(y1, y2));
97
99
 
98
100
  let setup = $state<MarkLifecycleV2 | null>(null);
99
101
 
@@ -121,6 +123,7 @@
121
123
  opacity: () => opacity,
122
124
  transforms: () => transforms,
123
125
  selection: () => selection,
126
+ selectionResponse: () => selectionResponse,
124
127
  limit: () => limit,
125
128
  sort: () => sort,
126
129
  domain: () => domain,
@@ -174,7 +177,11 @@
174
177
  ...(hasExtent && { y1: entry.y1, y2: entry.y2 })
175
178
  }),
176
179
  // When y shorthand is used, include 0 in domain
177
- zeroBaseline: yShorthand !== undefined ? { y: true } : null
180
+ zeroBaseline: yShorthand !== undefined ? { y: true } : null,
181
+ // The value the renderer's y0Data is built from: routing must agree.
182
+ ruleExtent: hasExtent,
183
+ // Coloured by stroke: the lifecycle requests that channel's identity.
184
+ colorChannel: 'stroke'
178
185
  };
179
186
  });
180
187
 
@@ -301,6 +308,7 @@
301
308
  alphaData={opacityData}
302
309
  colorMode={markColor.colorMode}
303
310
  colorScaleIndex={markColor.colorScaleIndex}
311
+ groupIndex={setup?.groupIndex ?? 0}
304
312
  constantColor={markColor.constantColor}
305
313
  {strokeWidth}
306
314
  facetData={setup?.facetData}
@@ -1,4 +1,5 @@
1
1
  <script lang="ts">
2
+ import { ruleHasExtent } from '../rendering/activity-reads';
2
3
  import { getContext, onDestroy, untrack } from 'svelte';
3
4
  import { MarkLifecycleV2 } from './lifecycle/MarkLifecycleV2.svelte';
4
5
  import { RuleY, GridLine } from '../rendering';
@@ -34,6 +35,7 @@
34
35
  let {
35
36
  table = undefined,
36
37
  selection = undefined,
38
+ selectionResponse = undefined,
37
39
  y,
38
40
  x: xShorthand = undefined,
39
41
  x1: x1Prop = undefined,
@@ -92,7 +94,7 @@
92
94
  return undefined;
93
95
  });
94
96
 
95
- const hasExtent = $derived(x1 !== undefined && x2 !== undefined);
97
+ const hasExtent = $derived(ruleHasExtent(x1, x2));
96
98
 
97
99
  let setup = $state<MarkLifecycleV2 | null>(null);
98
100
 
@@ -119,6 +121,7 @@
119
121
  opacity: () => opacity,
120
122
  transforms: () => transforms,
121
123
  selection: () => selection,
124
+ selectionResponse: () => selectionResponse,
122
125
  limit: () => limit,
123
126
  sort: () => sort,
124
127
  domain: () => domain,
@@ -170,7 +173,11 @@
170
173
  y: entry.y,
171
174
  ...(hasExtent && { x1: entry.x1, x2: entry.x2 })
172
175
  }),
173
- zeroBaseline: xShorthand !== undefined ? { x: true } : null
176
+ zeroBaseline: xShorthand !== undefined ? { x: true } : null,
177
+ // The value the renderer's x0Data is built from: routing must agree.
178
+ ruleExtent: hasExtent,
179
+ // Coloured by stroke: the lifecycle requests that channel's identity.
180
+ colorChannel: 'stroke'
174
181
  };
175
182
  });
176
183
 
@@ -297,6 +304,7 @@
297
304
  alphaData={opacityData}
298
305
  colorMode={markColor.colorMode}
299
306
  colorScaleIndex={markColor.colorScaleIndex}
307
+ groupIndex={setup?.groupIndex ?? 0}
300
308
  constantColor={markColor.constantColor}
301
309
  {strokeWidth}
302
310
  facetData={setup?.facetData}
@@ -1,4 +1,5 @@
1
1
  <script lang="ts">
2
+ import { NO_GROUP } from '../rendering/activity-reads';
2
3
  import { getContext, onDestroy, untrack } from 'svelte';
3
4
  import { MarkLifecycleV2 } from './lifecycle/MarkLifecycleV2.svelte';
4
5
  import { SCALE_PRIORITIES } from '../../scales/ScalePriorities';
@@ -41,6 +42,7 @@
41
42
  let {
42
43
  table,
43
44
  selection = undefined,
45
+ selectionResponse = undefined,
44
46
  x,
45
47
  y,
46
48
  text,
@@ -126,6 +128,9 @@
126
128
  get sort() {
127
129
  return sort;
128
130
  },
131
+ get selectionResponse() {
132
+ return selectionResponse;
133
+ },
129
134
  get limit() {
130
135
  return limit;
131
136
  },
@@ -203,18 +208,22 @@
203
208
 
204
209
  // --- Color Resolution (pre-resolved once, not per-label) ---
205
210
 
206
- const colorCSSValues = $derived.by(() => {
211
+ /** The raw fill channel, once per data change: colours and dimming both read it. */
212
+ const fillRaw = $derived.by(() => {
207
213
  if (!fill) return null;
208
214
  if (typeof fill === 'string' && isColorConstant(fill)) return null;
209
215
  if (!markColor.scaleState || !instanceCount) return null;
210
-
211
- const raw = resolveChannel({
216
+ return resolveChannel({
212
217
  value: fill,
213
218
  channel: 'fill',
214
219
  dataAccessor: setup?.data,
215
220
  instanceCount
216
221
  });
217
- if (!raw) return null;
222
+ });
223
+
224
+ const colorCSSValues = $derived.by(() => {
225
+ const raw = fillRaw;
226
+ if (!raw || !markColor.scaleState) return null;
218
227
 
219
228
  const scale = markColor.scaleState;
220
229
  const colors = new Array(instanceCount);
@@ -253,6 +262,60 @@
253
262
  return result;
254
263
  });
255
264
 
265
+ // --- Selection dimming ---
266
+ //
267
+ // A label dims with the bars it labels: by its band axes and its ordinal
268
+ // colour, in its own filter group, the fields activity-reads.ts lists for
269
+ // text. Routing reads the same list, so a categorical selection on one of
270
+ // these fields dims the labels instead of filtering them away.
271
+
272
+ /**
273
+ * Per-label dimming sources, resolved once per data change, not per frame.
274
+ * Each is a field's identity key and the label's dim ids on it: a scale has an
275
+ * identity key only while its channel carries ids, so the raw column is the
276
+ * ids, and reading the fade by id is what reaches an Other bucket.
277
+ */
278
+ const dimming = $derived.by(() => {
279
+ // A 'filter' mark (NO_GROUP) never dims, as its GPU marks do not.
280
+ if (!setup || !filterGroups || setup.groupIndex === NO_GROUP) return null;
281
+ const groups = [setup.groupIndex];
282
+ const sources: Array<{ key: string; ids: ArrayLike<unknown> }> = [];
283
+ for (const axis of ['x', 'y'] as const) {
284
+ const type = chartContext.scaleTypes?.[axis];
285
+ if (type !== 'band' && type !== 'point') continue;
286
+ const band = chartContext[`${axis}ScaleState`] as BandScaleState | null;
287
+ const raw = axis === 'x' ? xRaw : yRaw;
288
+ // Identity keys only, as routing reads them: a field without identity
289
+ // filters the labels, so they do not dim by it too.
290
+ const key = band?.identityKey;
291
+ if (!key || !raw) continue;
292
+ sources.push({ key, ids: raw as ArrayLike<unknown> });
293
+ }
294
+ const color = markColor.scaleState;
295
+ const raw = fillRaw;
296
+ const colorKey = color?.isOrdinal ? color.identityKey : null;
297
+ if (colorKey && raw) sources.push({ key: colorKey, ids: raw as ArrayLike<unknown> });
298
+ return sources.length ? { groups, sources } : null;
299
+ });
300
+
301
+ /** How visible label `idx` is now: the least visible of its categories. */
302
+ function labelOpacity(idx: number): number {
303
+ const d = dimming;
304
+ if (!d) return 1;
305
+ let opacity = 1;
306
+ for (const source of d.sources) {
307
+ const raw = source.ids[idx];
308
+ // An id reads its fade by id. Anything else (a string from a channel
309
+ // that never carried ids, a null) is a value, read as one: never coerced
310
+ // to a number, where a string becomes NaN and null becomes category 0.
311
+ const selected = Number.isInteger(raw)
312
+ ? filterGroups.opacityOfId(source.key, d.groups, raw as number)
313
+ : filterGroups.opacityOfValue(source.key, d.groups, raw);
314
+ opacity = Math.min(opacity, selected);
315
+ }
316
+ return opacity;
317
+ }
318
+
256
319
  // --- Animation-aware positioning ---
257
320
 
258
321
  let useFadeMode = $derived(labelItems.length > FADE_THRESHOLD);
@@ -427,6 +490,7 @@
427
490
  {#if pos}
428
491
  <text
429
492
  fill={item.color}
493
+ fill-opacity={labelOpacity(item.idx)}
430
494
  font-size="{fontSize}px"
431
495
  font-weight={fontWeight}
432
496
  font-family={fontFamily}
@@ -11,7 +11,8 @@
11
11
  * → setupComplete = true
12
12
  *
13
13
  * Registration: watches #resolvedQueryParts, registers with engine.
14
- * Engine handles filter-driven re-queries via onFilterChange().
14
+ * Engine handles filter-driven re-queries via onClauseChange() and
15
+ * onFilterChange().
15
16
  *
16
17
  * Params: watches sort/limit on ReactiveConfig.
17
18
  * Changes trigger re-query for this mark only, no teardown.
@@ -23,6 +24,7 @@ import type { ColorScaleState } from '../../../scales/ColorScaleState.svelte';
23
24
  import type { SizeScaleState } from '../../../scales/SizeScaleState.svelte';
24
25
  import type { QueryEngine } from '../../../query-engine/QueryEngine';
25
26
  import type { FilterGroup } from '../../../query-engine/FilterGroup.svelte';
27
+ import { type SelectionResponse } from '../../rendering/activity-reads';
26
28
  import type { MarkLODSurface } from '../../../lod/rung-types';
27
29
  import type { GPUCoordinator } from '../../../gpu/GPUCoordinator.svelte';
28
30
  export interface MarkLifecycleV2Config {
@@ -63,6 +65,12 @@ export interface MarkLifecycleV2Config {
63
65
  x?: boolean;
64
66
  y?: boolean;
65
67
  };
68
+ /**
69
+ * A rule's: whether it spans an extent on its cross axis (RuleX with
70
+ * y1/y2, RuleY with x1/x2, constants included). Decides whether it reads
71
+ * that axis's selection activity.
72
+ */
73
+ ruleExtent?: boolean;
66
74
  passiveScales?: boolean;
67
75
  parallelMode?: boolean;
68
76
  parallelColumns?: string[];
@@ -84,6 +92,7 @@ export interface ReactiveConfig {
84
92
  };
85
93
  readonly fitDomain: 'x' | 'y' | boolean | undefined;
86
94
  readonly tip: (unknown | '*')[] | false | undefined;
95
+ readonly selectionResponse: SelectionResponse | undefined;
87
96
  }
88
97
  export declare class MarkLifecycleV2 {
89
98
  #private;
@@ -125,6 +134,11 @@ export declare class MarkLifecycleV2 {
125
134
  get tooltipTableName(): string;
126
135
  get tooltipEnabled(): boolean;
127
136
  get channelFieldMap(): Record<string, string>;
137
+ /**
138
+ * The page-wide index of this mark's filter group. Its renderer stores it in
139
+ * MarkConstants to read its group's run of each field's activity block.
140
+ */
141
+ get groupIndex(): number;
128
142
  get dataVersion(): number;
129
143
  get hasCategoryReducingTransforms(): boolean;
130
144
  data: DataAccessor | null;
@@ -11,7 +11,8 @@
11
11
  * → setupComplete = true
12
12
  *
13
13
  * Registration: watches #resolvedQueryParts, registers with engine.
14
- * Engine handles filter-driven re-queries via onFilterChange().
14
+ * Engine handles filter-driven re-queries via onClauseChange() and
15
+ * onFilterChange().
15
16
  *
16
17
  * Params: watches sort/limit on ReactiveConfig.
17
18
  * Changes trigger re-query for this mark only, no teardown.
@@ -19,7 +20,7 @@
19
20
  import { untrack } from 'svelte';
20
21
  import { Query, isNode, isAggregateExpression, isColumnRef, isColumnParam, sql, column as sqlColumn, cast, min, max, isNotNull, and } from '@uwdata/mosaic-sql';
21
22
  import { TransformCompiler } from '../../../transforms/TransformCompiler';
22
- import { withDenseCategories, withRanking } from '../../../transforms';
23
+ import { withDenseCategories, withoutOtherMembers, withRanking } from '../../../transforms';
23
24
  import { ArrowDataAccessor } from '../../../data/DataAccessor';
24
25
  import { MarkHandle } from '../../../query-engine/MarkHandle.svelte';
25
26
  import { isContinuousScale, isNumericScale } from '../../../scales/scale-categories';
@@ -28,6 +29,7 @@ import { isBinExpression } from '../../../sql/bin';
28
29
  import { isRollingAvgExpression } from '../../../sql/rolling-avg';
29
30
  import { isWindowExpression } from '../../../sql/expression-utils';
30
31
  import { identityKeyFor } from '../../../query-engine/identity-key';
32
+ import { activityReads, groupIndexForResponse, NO_READS, readsFor, readsForResponse, rendererForMark } from '../../rendering/activity-reads';
31
33
  import { AdaptiveBinRung } from '../../../lod/AdaptiveBinRung.svelte';
32
34
  /** A SQL identifier, quoted, with any double quote in it doubled. */
33
35
  function ident(name) {
@@ -39,6 +41,7 @@ import { detectChannelsNeedingDataDomain } from './domain-computation';
39
41
  import { buildQueryParts, assembleQuery, castReal } from './query-builder';
40
42
  import { setScalesReady, markDeferredScalesReady, setInitialContinuousDomains, registerDataDerivedDomains } from './domain-registration';
41
43
  import { fetchCategoricalDomains, fetchGlobalAggregates } from './domain-queries';
44
+ import { transformScaleKey } from './transform-scale-key';
42
45
  // ---------------------------------------------------------------------------
43
46
  // MarkLifecycleV2
44
47
  // ---------------------------------------------------------------------------
@@ -338,6 +341,10 @@ export class MarkLifecycleV2 {
338
341
  if (this.#destroyed)
339
342
  return;
340
343
  this.#identityFields.clear();
344
+ // The Other ids name rows of the dropped dims. Every holder lets go, so
345
+ // the scale acquires afresh when setup re-runs.
346
+ for (const scale of [...this.#otherScales])
347
+ this.#releaseOther(scale);
341
348
  // Every dim it names just went away — topN must not bucket into one.
342
349
  this.#setIdentity(null);
343
350
  this.setupComplete = false;
@@ -359,8 +366,12 @@ export class MarkLifecycleV2 {
359
366
  let config = scaleRequirements.x.config || {};
360
367
  if (scaleRequirements.x.type === 'band' && hasCatReducing) {
361
368
  const key = this.#transformCompiler.getCategoryTransformKey();
362
- if (key)
363
- config = { ...config, transformKey: `${table}:${key}` };
369
+ if (key) {
370
+ config = {
371
+ ...config,
372
+ transformKey: transformScaleKey(table, key, this.#chartContext.config)
373
+ };
374
+ }
364
375
  }
365
376
  this.#chartContext.registerScaleType('x', scaleRequirements.x.type, scaleRequirements.x.priority, this.#markId, config, inferredScaleColumns?.x);
366
377
  }
@@ -368,8 +379,12 @@ export class MarkLifecycleV2 {
368
379
  let config = scaleRequirements.y.config || {};
369
380
  if (scaleRequirements.y.type === 'band' && hasCatReducing) {
370
381
  const key = this.#transformCompiler.getCategoryTransformKey();
371
- if (key)
372
- config = { ...config, transformKey: `${table}:${key}` };
382
+ if (key) {
383
+ config = {
384
+ ...config,
385
+ transformKey: transformScaleKey(table, key, this.#chartContext.config)
386
+ };
387
+ }
373
388
  }
374
389
  this.#chartContext.registerScaleType('y', scaleRequirements.y.type, scaleRequirements.y.priority, this.#markId, config, inferredScaleColumns?.y);
375
390
  }
@@ -421,7 +436,14 @@ export class MarkLifecycleV2 {
421
436
  const schema = this.#schema;
422
437
  if (!schema)
423
438
  return;
439
+ // Read after setup, once the scales have adopted identity. A change of
440
+ // reads or of selectionResponse re-registers the mark: a selection
441
+ // that dimmed it may now filter it. The key is a string, so reads
442
+ // rebuilt with the same contents do not.
443
+ void this.#readsKey;
444
+ const response = this.#reactiveConfig.selectionResponse;
424
445
  untrack(() => {
446
+ this.#route = { reads: this.#reads, chart: this.#chartContext.config, response };
425
447
  // registerMark handles cleanup of previous registration for this
426
448
  // markId and reuses the existing handle (preserves arrowTable
427
449
  // during re-query so the chart doesn't flash empty on rebin).
@@ -430,7 +452,8 @@ export class MarkLifecycleV2 {
430
452
  baseTable: this.#config.table,
431
453
  parts,
432
454
  schema,
433
- transformCompiler: this.#transformCompiler
455
+ transformCompiler: this.#transformCompiler,
456
+ route: this.#route
434
457
  }, this.#filterGroup);
435
458
  });
436
459
  });
@@ -983,10 +1006,12 @@ export class MarkLifecycleV2 {
983
1006
  // one id, which does not move a row limit that coarse.
984
1007
  const identity = {};
985
1008
  const describe = (channel, key, positional) => {
1009
+ const present = this.#queryEngine.presentTableFor(key, this.#config.table);
986
1010
  identity[channel] = {
987
1011
  dim: this.#queryEngine.dimTableFor(key),
988
1012
  size: this.#queryEngine.identityFor(key).values.length,
989
- positional
1013
+ positional,
1014
+ ...(present ? { present } : {})
990
1015
  };
991
1016
  };
992
1017
  for (const { axis, key } of axes)
@@ -1057,26 +1082,33 @@ export class MarkLifecycleV2 {
1057
1082
  // be reserved before a transform can write it. Reserved for every
1058
1083
  // categorical field of this mark rather than guessing which axis topN
1059
1084
  // will pick — at most two, and the transform looks its own field up.
1085
+ const otherScales = new Set();
1060
1086
  if (this.#transformCompiler.hasSyntheticOtherBucket()) {
1061
1087
  const otherIds = {};
1062
1088
  for (const { axis, key } of axes) {
1063
- const id = await this.#queryEngine.reserveSyntheticIdentity(key, 'Other');
1064
- otherIds[axis] = id;
1065
- // Re-adopt: the reservation appended to the value array, and a scale
1066
- // that adopted before it must see the new entry.
1067
1089
  const scale = (axis === 'x' ? this.#chartContext.xScaleState : this.#chartContext.yScaleState);
1068
- scale?.setIdentity(this.#queryEngine.identityFor(key));
1069
- // And tell it WHICH id is the bucket, so an entrance can come out of
1070
- // Other and an exit can go into it rather than past the end of the
1071
- // axis. With an Other bucket that is where the value actually goes.
1090
+ // Always there by now: `axes` holds only axes whose resolved type is
1091
+ // band or point, and ChartScales resolves the scale instance eagerly
1092
+ // from registerScaleType, in this mark's constructor, so the scale
1093
+ // exists before setup runs. The guard is for the type, not a case.
1094
+ if (!scale)
1095
+ continue;
1096
+ // One Other per band scale, shared by every mark on it (see
1097
+ // retainOther). The scale also takes it as its Other id, so an
1098
+ // entrance can come out of Other and an exit go into it rather than
1099
+ // past the end of the axis.
1072
1100
  //
1073
- // Set on EVERY categorical axis, like the reservation above, rather
1074
- // than re-deriving which axis topN picked — that derivation living
1075
- // in two places is what silently dropped the Other row once already.
1076
- // The axes topN did not pick are unaffected: their reserved id is on
1077
- // no row, so it is never displayed, and setOtherId documents why an
1078
- // undisplayed id cannot act as a gateway.
1079
- scale?.setOtherId(id);
1101
+ // Held on EVERY categorical axis rather than re-deriving which axis
1102
+ // topN picked — that derivation living in two places is what silently
1103
+ // dropped the Other row once already. The axes topN did not pick are
1104
+ // unaffected: their id is on no row, so it is never displayed.
1105
+ otherScales.add(scale);
1106
+ this.#otherScales.add(scale);
1107
+ const other = await scale.retainOther(this.#markId, () => this.#queryEngine.acquireSyntheticIdentity(key, 'Other'));
1108
+ otherIds[axis] = other.id;
1109
+ // Re-adopt: acquiring may have appended to the value array, and a
1110
+ // scale that adopted before it must see the new entry.
1111
+ scale.setIdentity(this.#queryEngine.identityFor(key));
1080
1112
  }
1081
1113
  // topN buckets past its cutoff into these ids. The query parts rebuild:
1082
1114
  // anything assembled before the reservation had no id and left Other
@@ -1097,6 +1129,18 @@ export class MarkLifecycleV2 {
1097
1129
  this.#setIdentity(withOther);
1098
1130
  }
1099
1131
  }
1132
+ // Scales this mark no longer draws an Other on (its axis or transform
1133
+ // changed) get their claim back.
1134
+ for (const scale of this.#otherScales) {
1135
+ if (!otherScales.has(scale))
1136
+ this.#releaseOther(scale);
1137
+ }
1138
+ }
1139
+ /** The band scales whose Other bucket this mark holds a claim on. */
1140
+ #otherScales = new Set();
1141
+ #releaseOther(scale) {
1142
+ this.#otherScales.delete(scale);
1143
+ scale.releaseOther(this.#markId, (other) => this.#queryEngine.releaseSyntheticIdentity(other));
1100
1144
  }
1101
1145
  /**
1102
1146
  * The one writer of identity: the reactive copy the query parts read, and
@@ -1173,7 +1217,7 @@ export class MarkLifecycleV2 {
1173
1217
  buildViewportCountQuery(xLo, xHi, yLo, yHi) {
1174
1218
  if (!this.setupComplete || !this.#resolvedQueryParts)
1175
1219
  return null;
1176
- const filter = this.#filterGroup.selection.predicate(this.#markId) ?? [];
1220
+ const filter = this.#filterGroup.selection.predicate(this.#route) ?? [];
1177
1221
  const q = assembleQuery({
1178
1222
  table: this.#sourceTable,
1179
1223
  parts: this.#resolvedQueryParts,
@@ -1210,6 +1254,43 @@ export class MarkLifecycleV2 {
1210
1254
  get channelFieldMap() {
1211
1255
  return buildChannelFieldMap(this.#config.select);
1212
1256
  }
1257
+ // --- Selection routing ---
1258
+ /**
1259
+ * The identity keys of the categorical fields this mark's renderer reads
1260
+ * selection activity for, sorted: its ordinal colour and its band axes, as
1261
+ * activity-reads.ts says the renderer reads them. A categorical selection on
1262
+ * one of these dims the mark; on any other field it filters it.
1263
+ */
1264
+ #reads = $derived.by(() => {
1265
+ const renderer = rendererForMark(this.#config.markType);
1266
+ if (!renderer)
1267
+ return NO_READS;
1268
+ const color = this.#colorScaleState;
1269
+ const bandKey = (axis) => {
1270
+ const type = axis === 'x' ? this.actualXScaleType : this.actualYScaleType;
1271
+ if (type !== 'band' && type !== 'point')
1272
+ return null;
1273
+ const band = this.#chartContext[`${axis}ScaleState`];
1274
+ return band?.identityKey ?? null;
1275
+ };
1276
+ return readsForResponse(this.#reactiveConfig.selectionResponse, readsFor(activityReads(renderer, this.#config.ruleExtent ?? false), {
1277
+ color: color?.isOrdinal ? color.identityKey : null,
1278
+ x: bandKey('x'),
1279
+ y: bandKey('y')
1280
+ }));
1281
+ });
1282
+ /** `#reads` as one string, so the registration effect re-runs only on a real change. */
1283
+ #readsKey = $derived(this.#reads.join('\u001f'));
1284
+ /** Built at registration; the engine and every predicate call hold this object. */
1285
+ #route = { reads: NO_READS, chart: null };
1286
+ /**
1287
+ * The page-wide index of this mark's filter group. Its renderer stores it in
1288
+ * MarkConstants to read its group's run of each field's activity block.
1289
+ */
1290
+ get groupIndex() {
1291
+ // NO_GROUP for selectionResponse 'filter', which never dims.
1292
+ return groupIndexForResponse(this.#reactiveConfig.selectionResponse, this.#filterGroup.index);
1293
+ }
1213
1294
  get dataVersion() {
1214
1295
  return this.#handle?.dataVersion ?? 0;
1215
1296
  }
@@ -1409,9 +1490,15 @@ export class MarkLifecycleV2 {
1409
1490
  if (orderCol)
1410
1491
  orderClause = ` ORDER BY ${orderCol}`;
1411
1492
  }
1493
+ // Without Other's member lists: every hover query is `SELECT *` from
1494
+ // this table, so lists of up to the field's cardinality would ship on
1495
+ // each one. Only those, not every `__dplot_` column as Path B drops:
1496
+ // the rest are scalars (the rank, id columns), cheap to keep, and the
1497
+ // lists are the only ones whose size grows with the field.
1498
+ const withoutMembers = withoutOtherMembers(tooltipQuery);
1412
1499
  await this.#queryEngine.exec([
1413
1500
  `DROP TABLE IF EXISTS "${tableName}"`,
1414
- `CREATE TEMP TABLE "${tableName}" AS SELECT *${replaceClause} FROM (${tooltipQuery})${orderClause}`
1501
+ `CREATE TEMP TABLE "${tableName}" AS SELECT *${replaceClause} FROM (${withoutMembers})${orderClause}`
1415
1502
  ]);
1416
1503
  }
1417
1504
  else {
@@ -1480,7 +1567,7 @@ export class MarkLifecycleV2 {
1480
1567
  #buildTooltipQuery() {
1481
1568
  if (!this.#resolvedQueryParts)
1482
1569
  return null;
1483
- const filter = this.#filterGroup.selection.predicate(this.#markId) ?? [];
1570
+ const filter = this.#filterGroup.selection.predicate(this.#route) ?? [];
1484
1571
  const q = assembleQuery({
1485
1572
  table: this.#sourceTable,
1486
1573
  parts: this.#resolvedQueryParts,
@@ -1537,7 +1624,7 @@ export class MarkLifecycleV2 {
1537
1624
  }
1538
1625
  Object.assign(sidecarSelect, tipExtras);
1539
1626
  const sidecarParts = buildQueryParts(sidecarSelect, undefined, this.#transformCompiler.hasSQLTransforms());
1540
- const filter = this.#filterGroup.selection.predicate(this.#markId) ?? [];
1627
+ const filter = this.#filterGroup.selection.predicate(this.#route) ?? [];
1541
1628
  const sidecarQuery = assembleQuery({
1542
1629
  table: this.#sourceTable,
1543
1630
  parts: sidecarParts,
@@ -1735,6 +1822,8 @@ export class MarkLifecycleV2 {
1735
1822
  this.#destroyed = true;
1736
1823
  this.#identityResetOff?.();
1737
1824
  this.#identityResetOff = null;
1825
+ for (const scale of [...this.#otherScales])
1826
+ this.#releaseOther(scale);
1738
1827
  this.#handle?.destroy();
1739
1828
  clearTimeout(this.#fitDomainTimeout);
1740
1829
  clearTimeout(this.#tooltipWriteTimeout);