@fjader/svelte 0.2.4 → 0.2.5

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 (139) hide show
  1. package/README.md +1 -1
  2. package/dist/animation/EntranceGate.d.ts +52 -0
  3. package/dist/animation/EntranceGate.js +93 -0
  4. package/dist/animation/TransitionClock.d.ts +61 -30
  5. package/dist/animation/TransitionClock.js +67 -29
  6. package/dist/animation/transition-log.d.ts +41 -0
  7. package/dist/animation/transition-log.js +57 -0
  8. package/dist/components/axes/AxisBottom.svelte +4 -2
  9. package/dist/components/axes/AxisCenter.svelte +4 -2
  10. package/dist/components/axes/AxisLeft.svelte +4 -2
  11. package/dist/components/axes/BandAxisTicks.svelte +3 -2
  12. package/dist/components/charts/ChartCell.svelte +32 -5
  13. package/dist/components/charts/ChartCell.svelte.d.ts +9 -0
  14. package/dist/components/charts/ChartContainer.svelte +45 -3
  15. package/dist/components/charts/ChartContainer.svelte.d.ts +7 -1
  16. package/dist/components/charts/ChartHover.svelte.d.ts +14 -0
  17. package/dist/components/charts/ChartHover.svelte.js +22 -0
  18. package/dist/components/charts/ChartVisibility.svelte.d.ts +43 -0
  19. package/dist/components/charts/ChartVisibility.svelte.js +67 -0
  20. package/dist/components/guides/Grid.svelte +12 -1
  21. package/dist/components/marks/AreaMarkX.svelte +16 -4
  22. package/dist/components/marks/AreaMarkY.svelte +16 -4
  23. package/dist/components/marks/BarMarkX.svelte +7 -3
  24. package/dist/components/marks/BarMarkY.svelte +7 -3
  25. package/dist/components/marks/DotsMark.svelte +6 -3
  26. package/dist/components/marks/LineMark.svelte +11 -8
  27. package/dist/components/marks/ParallelCoordinatesMark.svelte +7 -2
  28. package/dist/components/marks/ParallelCoordinatesMark.svelte.d.ts +2 -0
  29. package/dist/components/marks/RectMark.svelte +6 -3
  30. package/dist/components/marks/RectXMark.svelte +7 -3
  31. package/dist/components/marks/RectYMark.svelte +7 -3
  32. package/dist/components/marks/RuleMarkX.svelte +30 -26
  33. package/dist/components/marks/RuleMarkY.svelte +30 -26
  34. package/dist/components/marks/TextMark.svelte +44 -5
  35. package/dist/components/marks/lifecycle/MarkLifecycleV2.svelte.d.ts +16 -0
  36. package/dist/components/marks/lifecycle/MarkLifecycleV2.svelte.js +398 -54
  37. package/dist/components/marks/lifecycle/domain-queries.d.ts +11 -2
  38. package/dist/components/marks/lifecycle/domain-queries.js +33 -6
  39. package/dist/components/marks/lifecycle/domain-registration.d.ts +6 -1
  40. package/dist/components/marks/lifecycle/domain-registration.js +15 -14
  41. package/dist/components/marks/lifecycle/other-rows.svelte.d.ts +9 -1
  42. package/dist/components/marks/lifecycle/other-rows.svelte.js +2 -2
  43. package/dist/components/marks/lifecycle/query-builder.d.ts +27 -2
  44. package/dist/components/marks/lifecycle/query-builder.js +23 -20
  45. package/dist/components/marks/utils/MarkColor.svelte.d.ts +3 -1
  46. package/dist/components/marks/utils/MarkColor.svelte.js +33 -3
  47. package/dist/components/marks/utils/MarkSeries.svelte.d.ts +14 -1
  48. package/dist/components/marks/utils/MarkSeries.svelte.js +25 -1
  49. package/dist/components/marks/utils/MarkSize.svelte.js +4 -0
  50. package/dist/components/marks/utils/reactive-config.d.ts +1 -1
  51. package/dist/components/marks/utils/reactive-config.js +3 -0
  52. package/dist/components/rendering/Areas.svelte +57 -19
  53. package/dist/components/rendering/Areas.svelte.d.ts +12 -0
  54. package/dist/components/rendering/BaseGPURenderer.svelte.d.ts +1 -1
  55. package/dist/components/rendering/Dots.svelte +23 -3
  56. package/dist/components/rendering/Dots.svelte.d.ts +6 -0
  57. package/dist/components/rendering/GridLine.svelte +13 -1
  58. package/dist/components/rendering/GridLine.svelte.d.ts +5 -0
  59. package/dist/components/rendering/Lines.svelte +48 -13
  60. package/dist/components/rendering/Lines.svelte.d.ts +12 -0
  61. package/dist/components/rendering/ParallelLines.svelte +5 -5
  62. package/dist/components/rendering/Rects.svelte +51 -11
  63. package/dist/components/rendering/Rects.svelte.d.ts +16 -2
  64. package/dist/components/rendering/RuleX.svelte +1 -1
  65. package/dist/components/rendering/RuleY.svelte +1 -1
  66. package/dist/components/rendering/WebGPUCanvas.svelte +4 -2
  67. package/dist/components/rendering/activity-reads.d.ts +41 -24
  68. package/dist/components/rendering/activity-reads.js +54 -31
  69. package/dist/components/rendering/uniform-sizes.d.ts +1 -1
  70. package/dist/components/rendering/uniform-sizes.js +1 -1
  71. package/dist/config/ChartConfig.svelte.d.ts +7 -0
  72. package/dist/config/ChartConfig.svelte.js +17 -0
  73. package/dist/gpu/GPUCoordinator.svelte.d.ts +31 -1
  74. package/dist/gpu/GPUCoordinator.svelte.js +164 -7
  75. package/dist/gpu/MorphedChannels.d.ts +11 -2
  76. package/dist/gpu/MorphedChannels.js +33 -11
  77. package/dist/gpu/ScaleTemplateSystem.d.ts +20 -2
  78. package/dist/gpu/ScaleTemplateSystem.js +58 -2
  79. package/dist/index.d.ts +17 -4
  80. package/dist/index.js +19 -4
  81. package/dist/interactors/TooltipManager.svelte.js +14 -5
  82. package/dist/query-engine/CrossfilterSelection.svelte.d.ts +4 -2
  83. package/dist/query-engine/CrossfilterSelection.svelte.js +2 -0
  84. package/dist/query-engine/FieldActivity.svelte.d.ts +2 -3
  85. package/dist/query-engine/FieldActivity.svelte.js +1 -2
  86. package/dist/query-engine/MarkHandle.svelte.d.ts +28 -1
  87. package/dist/query-engine/MarkHandle.svelte.js +33 -1
  88. package/dist/query-engine/QueryEngine.d.ts +225 -5
  89. package/dist/query-engine/QueryEngine.js +671 -41
  90. package/dist/query-engine/categorical-identity.d.ts +34 -8
  91. package/dist/query-engine/categorical-identity.js +138 -73
  92. package/dist/query-engine/debug/query-log.d.ts +39 -0
  93. package/dist/query-engine/debug/query-log.js +90 -0
  94. package/dist/query-engine/duckdb.d.ts +55 -8
  95. package/dist/query-engine/duckdb.js +82 -30
  96. package/dist/query-engine/index.d.ts +7 -4
  97. package/dist/query-engine/index.js +3 -3
  98. package/dist/query-engine/preagg/BuildQueue.d.ts +33 -13
  99. package/dist/query-engine/preagg/BuildQueue.js +51 -15
  100. package/dist/query-engine/preagg/Preagg.d.ts +46 -3
  101. package/dist/query-engine/preagg/Preagg.js +159 -31
  102. package/dist/query-engine/preagg/plan.d.ts +28 -1
  103. package/dist/query-engine/preagg/plan.js +128 -8
  104. package/dist/query-engine/sliced-lane.d.ts +108 -0
  105. package/dist/query-engine/sliced-lane.js +299 -0
  106. package/dist/query-engine/source-routing.d.ts +106 -0
  107. package/dist/query-engine/source-routing.js +189 -0
  108. package/dist/scales/BandScaleState.svelte.d.ts +9 -1
  109. package/dist/scales/BandScaleState.svelte.js +16 -5
  110. package/dist/scales/ChartScales.svelte.d.ts +3 -0
  111. package/dist/scales/ChartScales.svelte.js +26 -8
  112. package/dist/scales/ColorScaleState.svelte.d.ts +18 -2
  113. package/dist/scales/ColorScaleState.svelte.js +138 -26
  114. package/dist/scales/LinearScaleState.svelte.d.ts +7 -10
  115. package/dist/scales/LinearScaleState.svelte.js +21 -39
  116. package/dist/scales/ScaleGeneration.svelte.d.ts +48 -0
  117. package/dist/scales/ScaleGeneration.svelte.js +42 -0
  118. package/dist/scales/ScaleRegistry.svelte.js +3 -1
  119. package/dist/scales/SlotTransition.d.ts +15 -1
  120. package/dist/scales/SlotTransition.js +33 -4
  121. package/dist/scales/TimeScaleState.svelte.d.ts +7 -3
  122. package/dist/scales/TimeScaleState.svelte.js +7 -7
  123. package/dist/scales/color-utils.d.ts +0 -3
  124. package/dist/scales/color-utils.js +0 -3
  125. package/dist/scales/entered.d.ts +8 -0
  126. package/dist/scales/entered.js +10 -0
  127. package/dist/spec/LazyQueryEngine.js +2 -19
  128. package/dist/spec/MarkRegistry.d.ts +3 -1
  129. package/dist/spec/MarkRegistry.js +13 -1
  130. package/dist/spec/PlotFromSpec.svelte +1 -0
  131. package/dist/spec/export.js +11 -2
  132. package/dist/spec/types.d.ts +41 -13
  133. package/dist/sql/bin.d.ts +25 -0
  134. package/dist/sql/bin.js +108 -5
  135. package/dist/transforms/TransformCompiler.d.ts +10 -0
  136. package/dist/transforms/TransformCompiler.js +26 -0
  137. package/dist/transforms/index.js +144 -24
  138. package/dist/types.d.ts +136 -2
  139. package/package.json +2 -2
@@ -16,7 +16,14 @@ export declare function fetchCategoricalDomains(coordinator: QueryEngine, table:
16
16
  y?: DimValue[];
17
17
  } | null,
18
18
  /** nulls="drop": the band and facet domains leave out the NULL category. */
19
- dropNull?: boolean): Promise<number>;
19
+ dropNull?: boolean,
20
+ /**
21
+ * True once the setup run that called this has been superseded or its mark
22
+ * destroyed. Checked at every write, after each query returns: the scales
23
+ * it would write may have been dropped with a ChartScales the chart has
24
+ * since replaced.
25
+ */
26
+ stale?: () => boolean): Promise<number>;
20
27
  /**
21
28
  * Fetch global min/max extents for aggregated fields using a "sidecar" query.
22
29
  * Runs the same aggregation as the main query but without filters, wrapped
@@ -25,6 +32,8 @@ dropNull?: boolean): Promise<number>;
25
32
  * Returns `{ globalAggregatesSet: true }` on success so the caller can
26
33
  * update its reactive state.
27
34
  */
28
- export declare function fetchGlobalAggregates(coordinator: QueryEngine, table: string, select: Record<string, unknown>, channelsNeedingDataDomain: Set<string>, autoBinNodeCount: number, buildQueryFn: (table: string, config: unknown, filter: unknown[], forMosaic: boolean) => Query, config: unknown, xScaleState: ContinuousScaleState | null, yScaleState: ContinuousScaleState | null, colorScaleState: ColorScaleState | null, markId: string): Promise<{
35
+ export declare function fetchGlobalAggregates(coordinator: QueryEngine, table: string, select: Record<string, unknown>, channelsNeedingDataDomain: Set<string>, autoBinNodeCount: number, buildQueryFn: (table: string, config: unknown, filter: unknown[], forMosaic: boolean) => Query, config: unknown, xScaleState: ContinuousScaleState | null, yScaleState: ContinuousScaleState | null, colorScaleState: ColorScaleState | null, markId: string,
36
+ /** As for fetchCategoricalDomains: checked before writing the extents. */
37
+ stale?: () => boolean): Promise<{
29
38
  globalAggregatesSet: boolean;
30
39
  }>;
@@ -12,7 +12,14 @@ import { identityKeyFor } from '../../../query-engine/identity-key';
12
12
  */
13
13
  export async function fetchCategoricalDomains(coordinator, table, select, actualXScaleType, actualYScaleType, xScaleState, yScaleState, facetBandScale, fx, colorChannel, colorScaleState, sizeChannel, sizeScaleState, seriesScaleState, seriesChannel, bandDomainFromData, markId, explicitDomain,
14
14
  /** nulls="drop": the band and facet domains leave out the NULL category. */
15
- dropNull = false) {
15
+ dropNull = false,
16
+ /**
17
+ * True once the setup run that called this has been superseded or its mark
18
+ * destroyed. Checked at every write, after each query returns: the scales
19
+ * it would write may have been dropped with a ChartScales the chart has
20
+ * since replaced.
21
+ */
22
+ stale = () => false) {
16
23
  const categoriesOf = (key) => {
17
24
  const categories = coordinator.categoryValues(key, table);
18
25
  return dropNull ? categories.filter((c) => c !== null) : categories;
@@ -70,6 +77,8 @@ dropNull = false) {
70
77
  fetches.push((async () => {
71
78
  // Idempotent and coalesced — the mark's own setup calls this too.
72
79
  await coordinator.ensureColumns(table, { categorical: [identityField] });
80
+ if (stale())
81
+ return;
73
82
  // This table's categories: the dim spans every table on the field.
74
83
  const categories = categoriesOf(identityField.key);
75
84
  // A snapshot of the real categories. The scale adopts the live
@@ -128,6 +137,8 @@ dropNull = false) {
128
137
  fetches.push((async () => {
129
138
  // Idempotent and coalesced — the mark's own setup calls this too.
130
139
  await coordinator.ensureColumns(table, { categorical: [facetKey] });
140
+ if (stale())
141
+ return;
131
142
  const categories = categoriesOf(facetKey.key);
132
143
  if (categories.length > 0)
133
144
  facetBandScale.addMark(categories, markId);
@@ -150,7 +161,9 @@ dropNull = false) {
150
161
  min_time: min(colExpr),
151
162
  max_time: max(colExpr)
152
163
  });
153
- fetches.push(coordinator.query(q).then((result) => {
164
+ fetches.push(coordinator.query(q, { cache: true, source: table }).then((result) => {
165
+ if (stale())
166
+ return;
154
167
  const row = Array.from(result)[0];
155
168
  if (row && row.min_time != null && row.max_time != null) {
156
169
  console.log(`[fetchCategoricalDomains] Time extent for ${axis}:`, [
@@ -192,6 +205,8 @@ dropNull = false) {
192
205
  fetches.push((async () => {
193
206
  // Idempotent and coalesced — the mark's own setup calls this too.
194
207
  await coordinator.ensureColumns(table, { categorical: [colorIdentity] });
208
+ if (stale())
209
+ return;
195
210
  const categories = coordinator.categoryValues(colorIdentity.key);
196
211
  if (categories.length > 0) {
197
212
  colorScaleState.setDomain(categories);
@@ -205,7 +220,9 @@ dropNull = false) {
205
220
  min_color: cast(min(colExpr), 'REAL'),
206
221
  max_color: cast(max(colExpr), 'REAL')
207
222
  });
208
- fetches.push(coordinator.query(q).then((result) => {
223
+ fetches.push(coordinator.query(q, { cache: true, source: table }).then((result) => {
224
+ if (stale())
225
+ return;
209
226
  const row = Array.from(result)[0];
210
227
  if (row && row.min_color != null && row.max_color != null) {
211
228
  console.log(`[fetchCategoricalDomains] Color domain (sequential):`, [
@@ -226,7 +243,9 @@ dropNull = false) {
226
243
  min_radius: cast(min(sqlColumn(String(sizeField))), 'REAL'),
227
244
  max_radius: cast(max(sqlColumn(String(sizeField))), 'REAL')
228
245
  });
229
- fetches.push(coordinator.query(q).then((result) => {
246
+ fetches.push(coordinator.query(q, { cache: true, source: table }).then((result) => {
247
+ if (stale())
248
+ return;
230
249
  const row = Array.from(result)[0];
231
250
  if (row && row.min_radius != null && row.max_radius != null) {
232
251
  sizeScaleState.setDomain([row.min_radius, row.max_radius]);
@@ -241,6 +260,8 @@ dropNull = false) {
241
260
  if (seriesIdentity) {
242
261
  fetches.push((async () => {
243
262
  await coordinator.ensureColumns(table, { categorical: [seriesIdentity] });
263
+ if (stale())
264
+ return;
244
265
  const categories = coordinator.categoryValues(seriesIdentity.key);
245
266
  if (categories.length > 0) {
246
267
  seriesScaleState.setDomain(categories);
@@ -250,6 +271,8 @@ dropNull = false) {
250
271
  }
251
272
  if (fetches.length > 0)
252
273
  await Promise.all(fetches);
274
+ if (stale())
275
+ return fetches.length;
253
276
  // Apply explicit domains after all async fetches — ViewManager columns
254
277
  // from color/series scales are now registered, so the band view will
255
278
  // include all necessary index columns.
@@ -272,7 +295,9 @@ dropNull = false) {
272
295
  * Returns `{ globalAggregatesSet: true }` on success so the caller can
273
296
  * update its reactive state.
274
297
  */
275
- export async function fetchGlobalAggregates(coordinator, table, select, channelsNeedingDataDomain, autoBinNodeCount, buildQueryFn, config, xScaleState, yScaleState, colorScaleState, markId) {
298
+ export async function fetchGlobalAggregates(coordinator, table, select, channelsNeedingDataDomain, autoBinNodeCount, buildQueryFn, config, xScaleState, yScaleState, colorScaleState, markId,
299
+ /** As for fetchCategoricalDomains: checked before writing the extents. */
300
+ stale = () => false) {
276
301
  // Only proceed if we have channels needing data-derived domains
277
302
  if (channelsNeedingDataDomain.size === 0) {
278
303
  console.log(`[${markId}] No aggregated channels, skipping sidecar query`);
@@ -334,7 +359,9 @@ export async function fetchGlobalAggregates(coordinator, table, select, channels
334
359
  const sidecarQuery = Query.from(innerQuery).select(outerSelect);
335
360
  console.log(`📊 [${markId}] Sidecar query SQL:`, sidecarQuery.toString());
336
361
  // Step 3: Execute the query
337
- const result = await coordinator.query(sidecarQuery);
362
+ const result = await coordinator.query(sidecarQuery, { cache: true, source: table });
363
+ if (stale())
364
+ return { globalAggregatesSet: false };
338
365
  const extents = Array.from(result)[0];
339
366
  console.log(`✅ [${markId}] Sidecar results:`, extents);
340
367
  // Step 4: Apply extents to scales
@@ -39,5 +39,10 @@ export declare function setInitialContinuousDomains(fieldInfo: Record<string, Fi
39
39
  export declare function registerDataDerivedDomains(data: DataAccessor, channelsNeedingDataDomain: Set<string>, additionalChannels: string[], skipDomainUpdateForAxis: string | null, zeroBaseline: {
40
40
  x?: boolean;
41
41
  y?: boolean;
42
- } | undefined, actualXScaleType: string, actualYScaleType: string, xScaleState: ContinuousScaleState | null, yScaleState: ContinuousScaleState | null, bandDomainFromData: boolean, xBandScaleState: BandScaleState | null, yBandScaleState: BandScaleState | null, markId: string, isRebinning: boolean, colorScaleState?: ColorScaleState | null): RegisterDomainsResult;
42
+ } | undefined, actualXScaleType: string, actualYScaleType: string, xScaleState: ContinuousScaleState | null, yScaleState: ContinuousScaleState | null, bandDomainFromData: boolean, xBandScaleState: BandScaleState | null, yBandScaleState: BandScaleState | null, markId: string,
43
+ /**
44
+ * Whether a scale's change from this result lands without animating: a
45
+ * catch-up result on a scale no other visible chart draws. Null: animate.
46
+ */
47
+ instantFor: ((scale: object) => boolean) | null, colorScaleState?: ColorScaleState | null): RegisterDomainsResult;
43
48
  export {};
@@ -100,7 +100,12 @@ export function setInitialContinuousDomains(fieldInfo, scaleMap, select, actualX
100
100
  * Called from Effect 7 on each data update. Uses positional args to avoid
101
101
  * object allocation on the crossfilter hot path.
102
102
  */
103
- export function registerDataDerivedDomains(data, channelsNeedingDataDomain, additionalChannels, skipDomainUpdateForAxis, zeroBaseline, actualXScaleType, actualYScaleType, xScaleState, yScaleState, bandDomainFromData, xBandScaleState, yBandScaleState, markId, isRebinning, colorScaleState) {
103
+ export function registerDataDerivedDomains(data, channelsNeedingDataDomain, additionalChannels, skipDomainUpdateForAxis, zeroBaseline, actualXScaleType, actualYScaleType, xScaleState, yScaleState, bandDomainFromData, xBandScaleState, yBandScaleState, markId,
104
+ /**
105
+ * Whether a scale's change from this result lands without animating: a
106
+ * catch-up result on a scale no other visible chart draws. Null: animate.
107
+ */
108
+ instantFor, colorScaleState) {
104
109
  if (!data || data.length === 0) {
105
110
  return { dataDerivedDomainsSet: false, skipDomainUpdateForAxis };
106
111
  }
@@ -152,7 +157,8 @@ export function registerDataDerivedDomains(data, channelsNeedingDataDomain, addi
152
157
  if (zeroBaseline?.x) {
153
158
  extent = [Math.min(0, extent[0]), Math.max(0, extent[1])];
154
159
  }
155
- registerExtentOnScale(xScaleState, actualXScaleType, extent, markId, isRebinning, 'X');
160
+ const instant = !!xScaleState && (instantFor?.(xScaleState) ?? false);
161
+ registerExtentOnScale(xScaleState, actualXScaleType, extent, markId, instant, 'X');
156
162
  }
157
163
  }
158
164
  // Compute and register Y domain
@@ -166,7 +172,8 @@ export function registerDataDerivedDomains(data, channelsNeedingDataDomain, addi
166
172
  if (zeroBaseline?.y) {
167
173
  extent = [Math.min(0, extent[0]), Math.max(0, extent[1])];
168
174
  }
169
- registerExtentOnScale(yScaleState, actualYScaleType, extent, markId, isRebinning, 'Y');
175
+ const instant = !!yScaleState && (instantFor?.(yScaleState) ?? false);
176
+ registerExtentOnScale(yScaleState, actualYScaleType, extent, markId, instant, 'Y');
170
177
  }
171
178
  }
172
179
  // Band scale domain from the query result, for a mark whose axis is decided
@@ -217,7 +224,7 @@ export function registerDataDerivedDomains(data, channelsNeedingDataDomain, addi
217
224
  continue;
218
225
  bandScale.addMark(categories, markId);
219
226
  }
220
- bandScale.updateDomainFromExtents();
227
+ bandScale.updateDomainFromExtents({ instant: instantFor?.(bandScale) ?? false });
221
228
  }
222
229
  }
223
230
  // Color domain from query results (needed when sidecar is skipped, e.g. binned marks)
@@ -240,20 +247,14 @@ export function registerDataDerivedDomains(data, channelsNeedingDataDomain, addi
240
247
  // ---------------------------------------------------------------------------
241
248
  // Internal helper
242
249
  // ---------------------------------------------------------------------------
243
- function registerExtentOnScale(scaleState, scaleType, extent, markId, isRebinning, axisLabel) {
250
+ function registerExtentOnScale(scaleState, scaleType, extent, markId, instant, axisLabel) {
244
251
  if (scaleType === 'time' && scaleState?.addOffsetExtent) {
245
252
  // Time scale: data is in offset form, use addOffsetExtent
246
253
  console.log(`📊 [${markId}] Registering ${axisLabel} domain from data (time offsets):`, extent);
247
- scaleState.addOffsetExtent(extent, markId);
254
+ scaleState.addOffsetExtent(extent, markId, { instant });
248
255
  }
249
256
  else if (scaleState?.addMark) {
250
- // Linear/log/symlog scale: use addMark with instant flag when rebinning
251
- if (isRebinning) {
252
- console.log(`⚡ [${markId}] Instant ${axisLabel} domain update (rebinning):`, extent);
253
- }
254
- else {
255
- console.log(`📊 [${markId}] Registering ${axisLabel} domain from data:`, extent);
256
- }
257
- scaleState.addMark(extent, markId, { instant: isRebinning });
257
+ console.log(`📊 [${markId}] Registering ${axisLabel} domain from data:`, extent);
258
+ scaleState.addMark(extent, markId, { instant });
258
259
  }
259
260
  }
@@ -58,8 +58,16 @@ export interface OtherRowsDeps {
58
58
  identity: () => Record<string, ChannelIdentity> | null;
59
59
  /** The identity key of a band axis, or null when it has none. */
60
60
  identityKey: (axis: 'x' | 'y') => string | null;
61
- /** The mark is filtered by the selection rather than dimmed by it. */
61
+ /**
62
+ * The mark is filtered by every selection (`'filter'`): its Other is
63
+ * recomputed inside the selection, so a click on it drills down.
64
+ */
62
65
  filtered: () => boolean;
66
+ /**
67
+ * A selection can dim the mark (`'auto'`). Not under `'filter'` or
68
+ * `'ignore'`, whose Other bars get no selected share.
69
+ */
70
+ dims: () => boolean;
63
71
  /** The mark's plan, or null before its setup completes. */
64
72
  plan: () => SplitPlan | null;
65
73
  /** The identity key of the mark's ordinal colour scale, or null. */
@@ -135,7 +135,7 @@ export class OtherRows {
135
135
  * The band axis whose Other bar splits, or null. Only where a share is what
136
136
  * it looks like:
137
137
  * - a bar mark ('bary', 'barx') with a topN Other bucket, dimming (not
138
- * 'filter', which never dims) and without JS transforms, whose rows would
138
+ * 'filter' or 'ignore', which never dim) and without JS transforms, whose rows would
139
139
  * no longer line up with the query's;
140
140
  * - whose length is the measure topN ranks by, which is what Other sums;
141
141
  * - and that measure a bare COUNT or SUM, or a raw column: a mean or a max
@@ -143,7 +143,7 @@ export class OtherRows {
143
143
  */
144
144
  get #splitAxis() {
145
145
  const plan = this.#deps.plan();
146
- if (!plan || this.#deps.filtered())
146
+ if (!plan || !this.#deps.dims())
147
147
  return null;
148
148
  if (!plan.hasOther || plan.hasJsTransforms)
149
149
  return null;
@@ -1,6 +1,7 @@
1
1
  import { sum, Query, WindowNode, type ExprNode } from '@uwdata/mosaic-sql';
2
2
  import type { TransformCompiler } from '../../../transforms/TransformCompiler';
3
3
  import type { ColumnSchema } from '../../../query-engine/QueryEngine';
4
+ import type { BinPadding } from '../../../types';
4
5
  interface OrderConfig {
5
6
  seriesChannel: string | null;
6
7
  dimensionChannel: string;
@@ -18,6 +19,14 @@ export interface QueryParts {
18
19
  * combination, and cannot recover from a query topN has already wrapped.
19
20
  */
20
21
  groupKeys: string[] | null;
22
+ /**
23
+ * The numeric `bin()` channels and their bins, set by the mark from the
24
+ * same resolution its bin expressions came from. With them `dense()` pads a
25
+ * histogram to every bin; preagg reads copy them, so both paths pad alike.
26
+ */
27
+ bins?: Record<string, BinPadding> | null;
28
+ /** The query reads a preagg table that already holds its padding rows. */
29
+ padded?: boolean;
21
30
  orderChannels: string[] | null;
22
31
  aliasMap: Map<string, string>;
23
32
  /** Channels whose raw config value was a column-name passthrough (string
@@ -74,8 +83,8 @@ export interface AssembleOptions {
74
83
  /**
75
84
  * Wrap an expression so DuckDB returns a Float32-compatible REAL with NULLs
76
85
  * mapped to NaN: COALESCE(CAST(expr AS REAL), 'NaN'::REAL). Arrow columns then
77
- * have nullCount=0 and clean buffers for zero-copy GPU upload; shaders detect
78
- * NaN and discard.
86
+ * have nullCount=0 and clean Float32 buffers that upload to the GPU as they are,
87
+ * with no per-row JS pass; shaders detect NaN and discard.
79
88
  */
80
89
  export declare function castReal(expr: unknown): ExprNode;
81
90
  /**
@@ -159,5 +168,21 @@ export declare function buildQueryParts(select: Record<string, unknown>, orderCo
159
168
  * discarded one level up.
160
169
  */
161
170
  export declare function outerOrder(preference: string[] | null, alignment: string[] | null): string[] | null;
171
+ /**
172
+ * Assemble a Query from cached parts + per-call filter.
173
+ *
174
+ * Order is fixed and shared by the flat, window-CTE, and preagg paths:
175
+ *
176
+ * SELECT parts.selectDef FROM table [GROUP BY dims]
177
+ * WHERE filter AND nullDrops — on the base scan, so transform CTEs
178
+ * (topN ranking etc.) see filtered rows
179
+ * → SQL transforms (CTE wrapping)
180
+ * → [window CTE: outer SELECT applies window fns over the inner result]
181
+ * → ORDER BY outerOrder(...) — alignment, then orderChannels; outermost
182
+ * → LIMIT
183
+ *
184
+ * Callers decide what is CAST-wrapped by preparing parts.selectDef with
185
+ * castNumeric(); assembly never inspects expression types.
186
+ */
162
187
  export declare function assembleQuery(opts: AssembleOptions): Query;
163
188
  export {};
@@ -23,8 +23,8 @@ export function nullDropPredicates(drops, selectDef) {
23
23
  /**
24
24
  * Wrap an expression so DuckDB returns a Float32-compatible REAL with NULLs
25
25
  * mapped to NaN: COALESCE(CAST(expr AS REAL), 'NaN'::REAL). Arrow columns then
26
- * have nullCount=0 and clean buffers for zero-copy GPU upload; shaders detect
27
- * NaN and discard.
26
+ * have nullCount=0 and clean Float32 buffers that upload to the GPU as they are,
27
+ * with no per-row JS pass; shaders detect NaN and discard.
28
28
  */
29
29
  export function castReal(expr) {
30
30
  return coalesce(cast(expr, 'REAL'), sql `'NaN'::REAL`);
@@ -219,22 +219,6 @@ export function buildQueryParts(select, orderConfig, hasSQLTransforms, passthrou
219
219
  // ---------------------------------------------------------------------------
220
220
  // Assembly — the one sequence every query path follows
221
221
  // ---------------------------------------------------------------------------
222
- /**
223
- * Assemble a Query from cached parts + per-call filter.
224
- *
225
- * Order is fixed and shared by the flat, window-CTE, and preagg paths:
226
- *
227
- * SELECT parts.selectDef FROM table [GROUP BY dims]
228
- * WHERE filter AND nullDrops — on the base scan, so transform CTEs
229
- * (topN ranking etc.) see filtered rows
230
- * → SQL transforms (CTE wrapping)
231
- * → [window CTE: outer SELECT applies window fns over the inner result]
232
- * → ORDER BY outerOrder(...) — alignment, then orderChannels; outermost
233
- * → LIMIT
234
- *
235
- * Callers decide what is CAST-wrapped by preparing parts.selectDef with
236
- * castNumeric(); assembly never inspects expression types.
237
- */
238
222
  /** Output aliases a query exposes to whatever wraps it. */
239
223
  function selectAliases(query) {
240
224
  const sel = query._select;
@@ -283,6 +267,22 @@ export function outerOrder(preference, alignment) {
283
267
  add(channel);
284
268
  return order;
285
269
  }
270
+ /**
271
+ * Assemble a Query from cached parts + per-call filter.
272
+ *
273
+ * Order is fixed and shared by the flat, window-CTE, and preagg paths:
274
+ *
275
+ * SELECT parts.selectDef FROM table [GROUP BY dims]
276
+ * WHERE filter AND nullDrops — on the base scan, so transform CTEs
277
+ * (topN ranking etc.) see filtered rows
278
+ * → SQL transforms (CTE wrapping)
279
+ * → [window CTE: outer SELECT applies window fns over the inner result]
280
+ * → ORDER BY outerOrder(...) — alignment, then orderChannels; outermost
281
+ * → LIMIT
282
+ *
283
+ * Callers decide what is CAST-wrapped by preparing parts.selectDef with
284
+ * castNumeric(); assembly never inspects expression types.
285
+ */
286
286
  export function assembleQuery(opts) {
287
287
  const { table, parts, filter = [], transformCompiler, limit, window, skipRowFilters } = opts;
288
288
  const { selectDef, nonAggregateGroupBy, hasAggregates, hasSQLTransforms, orderChannels } = parts;
@@ -299,7 +299,7 @@ export function assembleQuery(opts) {
299
299
  }
300
300
  let result = q;
301
301
  if (hasSQLTransforms) {
302
- result = transformCompiler.applyToQuery(q, { groupKeys: parts.groupKeys }, { skipRowFilters });
302
+ result = transformCompiler.applyToQuery(q, { groupKeys: parts.groupKeys, bins: parts.bins ?? null, padded: parts.padded ?? false }, { skipRowFilters });
303
303
  }
304
304
  if (window) {
305
305
  const outerSelect = {};
@@ -327,7 +327,10 @@ export function assembleQuery(opts) {
327
327
  .with({ [window.cteName]: result });
328
328
  }
329
329
  const alignment = hasSQLTransforms
330
- ? transformCompiler.alignment(Object.keys(selectDef), { groupKeys: parts.groupKeys })
330
+ ? transformCompiler.alignment(Object.keys(selectDef), {
331
+ groupKeys: parts.groupKeys,
332
+ bins: parts.bins ?? null
333
+ })
331
334
  : null;
332
335
  const order = outerOrder(orderChannels, alignment);
333
336
  if (order) {
@@ -1,5 +1,5 @@
1
1
  import type { ColorScaleState } from '../../../scales/ColorScaleState.svelte';
2
- import type { ColorProp, ChartContext } from '../../../types';
2
+ import type { ColorDomainValue, ColorProp, ChartContext } from '../../../types';
3
3
  import type { ScaleRegistry } from '../../../scales/ScaleRegistry.svelte';
4
4
  import type { MarkSetupState } from './mark-types';
5
5
  interface ColorConfig {
@@ -11,6 +11,8 @@ interface ColorConfig {
11
11
  explicitType?: string;
12
12
  constant?: number;
13
13
  exponent?: number;
14
+ range?: readonly string[];
15
+ pinnedDomain?: readonly ColorDomainValue[];
14
16
  }
15
17
  export declare class MarkColor {
16
18
  #private;
@@ -30,6 +30,10 @@ export class MarkColor {
30
30
  const mode = this.#mode;
31
31
  const field = this.#field;
32
32
  const type = this.config.scaleType;
33
+ // Again when the chart's scales are replaced: the registry dropped
34
+ // this one with the old ChartScales (its GPU slots may already be
35
+ // someone else's), and holding it would write to a destroyed scale.
36
+ void this.#chartContext.scaleGeneration;
33
37
  // Only create scale when type is known (not null)
34
38
  if (mode === 'field' && field && this.#scaleRegistry && type !== null) {
35
39
  untrack(() => {
@@ -42,7 +46,9 @@ export class MarkColor {
42
46
  type: type,
43
47
  scheme: this.config.scheme,
44
48
  constant: this.config.constant,
45
- exponent: this.config.exponent
49
+ exponent: this.config.exponent,
50
+ range: this.config.range,
51
+ pinnedDomain: this.config.pinnedDomain
46
52
  }, this.#chartContext.chartId);
47
53
  });
48
54
  }
@@ -66,6 +72,21 @@ export class MarkColor {
66
72
  this.scaleState.setScheme(scheme);
67
73
  }
68
74
  });
75
+ // 2a. Lifecycle: Update explicit colours (Fast Path - no scale recreation)
76
+ // Like the scheme, a mark without them leaves a shared scale's alone; one
77
+ // that drops them (an edited prop) clears what it set.
78
+ let hadRange = false;
79
+ $effect(() => {
80
+ const range = this.config.range;
81
+ const pinned = this.config.pinnedDomain;
82
+ const scale = this.scaleState;
83
+ if (!scale?.isOrdinal)
84
+ return;
85
+ const has = !!(range?.length || pinned?.length);
86
+ if (has || hadRange)
87
+ scale.setRange(range ?? null, pinned ?? null);
88
+ hadRange = has;
89
+ });
69
90
  // 2b. Lifecycle: Update Transform (Fast Path - no scale recreation)
70
91
  $effect(() => {
71
92
  const type = this.config.explicitType;
@@ -148,6 +169,8 @@ export class MarkColor {
148
169
  let explicitType;
149
170
  let constant;
150
171
  let exponent;
172
+ let range;
173
+ let pinnedDomain;
151
174
  // Handle object wrapper format { field: "species", scheme: "dark2", type: "symlog" }
152
175
  if (typeof c === 'object' && 'field' in c && !isNode(c)) {
153
176
  field = c.field;
@@ -155,10 +178,15 @@ export class MarkColor {
155
178
  explicitType = c.type;
156
179
  constant = c.constant;
157
180
  exponent = c.exponent;
181
+ range = c.range;
182
+ pinnedDomain = c.domain;
158
183
  }
159
184
  // 1. Determine Type from field semantics + schema (may return null if waiting for schema)
160
185
  // If an explicit type is given (log/symlog/sqrt/pow), use it directly.
161
- const scaleType = explicitType || this.#resolveScaleType(field);
186
+ // Explicit colours or pinned values mean categories, whatever the column
187
+ // type: a 0/1 integer column would otherwise read as continuous.
188
+ const scaleType = explicitType ||
189
+ (range?.length || pinnedDomain?.length ? 'ordinal' : this.#resolveScaleType(field));
162
190
  // 2. Pass explicit scheme if provided, otherwise undefined (registry will apply default)
163
191
  return {
164
192
  mode: 'field',
@@ -167,7 +195,9 @@ export class MarkColor {
167
195
  scheme: explicitScheme, // undefined if not specified
168
196
  explicitType,
169
197
  constant,
170
- exponent
198
+ exponent,
199
+ range,
200
+ pinnedDomain
171
201
  };
172
202
  });
173
203
  // Separate derived for mode (independent of scheme to avoid unnecessary tracking)
@@ -12,8 +12,21 @@ import type { MarkSetupState } from './mark-types';
12
12
  export declare class MarkSeries {
13
13
  #private;
14
14
  scaleState: ColorScaleState | null;
15
- constructor(getSeries: () => string | object | null, channelName: string, getSetup: () => MarkSetupState, scaleRegistry: ScaleRegistry, chartContext: ChartContext);
15
+ constructor(getSeries: () => string | object | null, channelName: string, getSetup: () => MarkSetupState, scaleRegistry: ScaleRegistry, chartContext: ChartContext, getColorScale?: () => ColorScaleState | null);
16
16
  selectField: string | object | null;
17
17
  data: Float32Array | null;
18
+ /**
19
+ * Whether the renderer reads selection activity through the series scale:
20
+ * 1 for a `z` of its own, a field that carries ids, so a selection on it
21
+ * dims the other series. 0 without `z` (the series is the colour, read
22
+ * already), for a `z` on the colour field (the registry hands both
23
+ * channels one scale, and reading it twice is a load for nothing), and
24
+ * before the scale has an identity, while its values are not ids yet.
25
+ * Routing (MarkLifecycleV2's reads) lists the same field in the same
26
+ * cases, so the mark dims rather than filters exactly when this is 1.
27
+ */
28
+ activityMode: number;
29
+ /** The series scale's colour config index, read when `activityMode` is 1. */
30
+ activityScaleIndex: number;
18
31
  ready: boolean;
19
32
  }
@@ -15,17 +15,23 @@ export class MarkSeries {
15
15
  #setupState;
16
16
  #scaleRegistry;
17
17
  #chartContext;
18
+ #colorScale;
18
19
  scaleState = $state(null);
19
- constructor(getSeries, channelName, getSetup, scaleRegistry, chartContext) {
20
+ constructor(getSeries, channelName, getSetup, scaleRegistry, chartContext, getColorScale = () => null) {
20
21
  this.#seriesProp = getSeries;
21
22
  this.#channelName = channelName;
22
23
  this.#setupState = getSetup;
23
24
  this.#scaleRegistry = scaleRegistry;
24
25
  this.#chartContext = chartContext;
26
+ this.#colorScale = getColorScale;
25
27
  // Lifecycle: Create/Destroy Scale
26
28
  // Series grouping is always ordinal
27
29
  $effect(() => {
28
30
  const field = this.#field;
31
+ // Again when the chart's scales are replaced: the registry dropped
32
+ // this one with the old ChartScales (its GPU slots may already be
33
+ // someone else's), and holding it would write to a destroyed scale.
34
+ void this.#chartContext.scaleGeneration;
29
35
  if (field && this.#scaleRegistry) {
30
36
  untrack(() => {
31
37
  // Use raw field name - registry deduplicates automatically
@@ -89,6 +95,24 @@ export class MarkSeries {
89
95
  }
90
96
  return this.scaleState.processDataForGPU(raw);
91
97
  });
98
+ /**
99
+ * Whether the renderer reads selection activity through the series scale:
100
+ * 1 for a `z` of its own, a field that carries ids, so a selection on it
101
+ * dims the other series. 0 without `z` (the series is the colour, read
102
+ * already), for a `z` on the colour field (the registry hands both
103
+ * channels one scale, and reading it twice is a load for nothing), and
104
+ * before the scale has an identity, while its values are not ids yet.
105
+ * Routing (MarkLifecycleV2's reads) lists the same field in the same
106
+ * cases, so the mark dims rather than filters exactly when this is 1.
107
+ */
108
+ activityMode = $derived.by(() => {
109
+ const scale = this.scaleState;
110
+ if (this.#channelName !== 'z' || !scale?.hasIdentity)
111
+ return 0;
112
+ return scale === this.#colorScale() ? 0 : 1;
113
+ });
114
+ /** The series scale's colour config index, read when `activityMode` is 1. */
115
+ activityScaleIndex = $derived(this.scaleState?.gpuIndex ?? 0);
92
116
  // Ready when: no field (single series) or scale is ready
93
117
  ready = $derived(!this.#field || (this.scaleState?.ready ?? false));
94
118
  }
@@ -18,6 +18,10 @@ export class MarkSize {
18
18
  $effect(() => {
19
19
  const mode = this.#mode;
20
20
  const field = this.#field;
21
+ // Again when the chart's scales are replaced: the registry dropped
22
+ // this one with the old ChartScales (its GPU slots may already be
23
+ // someone else's), and holding it would write to a destroyed scale.
24
+ void this.#chartContext.scaleGeneration;
21
25
  if (mode === 'field' && field && this.#scaleRegistry) {
22
26
  untrack(() => {
23
27
  this.scaleState = this.#scaleRegistry.getOrCreateScale(field, 'size', { range: this.#rangeProp() }, this.#chartContext.chartId);
@@ -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' | 'selectionResponse' | 'nulls' | 'contributeDomain'>>, opts?: {
15
+ export declare function buildReactiveConfig(entry: Partial<Pick<MarkEntry, 'sort' | 'limit' | 'domain' | 'tooltip' | 'tip' | 'fitDomain' | 'selectionResponse' | 'nulls' | 'contributeDomain' | 'holdsEntrance'>>, opts?: {
16
16
  resolveFitDomain?: () => 'x' | 'y' | boolean | undefined;
17
17
  }): ReactiveConfig;
@@ -19,6 +19,9 @@ export function buildReactiveConfig(entry, opts) {
19
19
  get limit() {
20
20
  return entry.limit;
21
21
  },
22
+ get holdsEntrance() {
23
+ return entry.holdsEntrance;
24
+ },
22
25
  get domain() {
23
26
  return entry.domain ?? null;
24
27
  },