@fjader/svelte 0.2.3 → 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 (167) hide show
  1. package/README.md +3 -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 +27 -8
  9. package/dist/components/axes/AxisBottom.svelte.d.ts +10 -0
  10. package/dist/components/axes/AxisCenter.svelte +4 -2
  11. package/dist/components/axes/AxisLeft.svelte +81 -10
  12. package/dist/components/axes/AxisLeft.svelte.d.ts +17 -0
  13. package/dist/components/axes/BandAxisTicks.svelte +4 -3
  14. package/dist/components/axes/axis-title.d.ts +30 -0
  15. package/dist/components/axes/axis-title.js +77 -0
  16. package/dist/components/charts/ChartCell.svelte +33 -6
  17. package/dist/components/charts/ChartCell.svelte.d.ts +9 -0
  18. package/dist/components/charts/ChartContainer.svelte +55 -5
  19. package/dist/components/charts/ChartContainer.svelte.d.ts +14 -1
  20. package/dist/components/charts/ChartHover.svelte.d.ts +14 -0
  21. package/dist/components/charts/ChartHover.svelte.js +22 -0
  22. package/dist/components/charts/ChartVisibility.svelte.d.ts +43 -0
  23. package/dist/components/charts/ChartVisibility.svelte.js +67 -0
  24. package/dist/components/guides/Grid.svelte +12 -1
  25. package/dist/components/guides/legend/ContinuousColorLegend.svelte +7 -1
  26. package/dist/components/guides/legend/OrdinalColorLegend.svelte +8 -5
  27. package/dist/components/marks/AreaMarkX.svelte +18 -4
  28. package/dist/components/marks/AreaMarkY.svelte +18 -4
  29. package/dist/components/marks/BarMarkX.svelte +20 -4
  30. package/dist/components/marks/BarMarkY.svelte +20 -4
  31. package/dist/components/marks/DotsMark.svelte +8 -3
  32. package/dist/components/marks/LineMark.svelte +13 -6
  33. package/dist/components/marks/ParallelCoordinatesMark.svelte +11 -2
  34. package/dist/components/marks/ParallelCoordinatesMark.svelte.d.ts +4 -0
  35. package/dist/components/marks/RectMark.svelte +8 -3
  36. package/dist/components/marks/RectXMark.svelte +9 -3
  37. package/dist/components/marks/RectYMark.svelte +9 -3
  38. package/dist/components/marks/RuleMarkX.svelte +31 -25
  39. package/dist/components/marks/RuleMarkY.svelte +31 -25
  40. package/dist/components/marks/TextMark.svelte +52 -14
  41. package/dist/components/marks/lifecycle/MarkLifecycleV2.svelte.d.ts +22 -1
  42. package/dist/components/marks/lifecycle/MarkLifecycleV2.svelte.js +579 -66
  43. package/dist/components/marks/lifecycle/domain-queries.d.ts +13 -2
  44. package/dist/components/marks/lifecycle/domain-queries.js +43 -11
  45. package/dist/components/marks/lifecycle/domain-registration.d.ts +6 -1
  46. package/dist/components/marks/lifecycle/domain-registration.js +17 -20
  47. package/dist/components/marks/lifecycle/other-rows.svelte.d.ts +112 -0
  48. package/dist/components/marks/lifecycle/other-rows.svelte.js +193 -0
  49. package/dist/components/marks/lifecycle/other-split.d.ts +80 -0
  50. package/dist/components/marks/lifecycle/other-split.js +112 -0
  51. package/dist/components/marks/lifecycle/query-builder.d.ts +53 -3
  52. package/dist/components/marks/lifecycle/query-builder.js +46 -23
  53. package/dist/components/marks/utils/MarkColor.svelte.d.ts +3 -1
  54. package/dist/components/marks/utils/MarkColor.svelte.js +33 -3
  55. package/dist/components/marks/utils/MarkSeries.svelte.d.ts +14 -1
  56. package/dist/components/marks/utils/MarkSeries.svelte.js +25 -1
  57. package/dist/components/marks/utils/MarkSize.svelte.js +4 -0
  58. package/dist/components/marks/utils/reactive-config.d.ts +1 -1
  59. package/dist/components/marks/utils/reactive-config.js +9 -0
  60. package/dist/components/marks/utils/tooltip-helpers.d.ts +4 -1
  61. package/dist/components/marks/utils/tooltip-helpers.js +2 -1
  62. package/dist/components/rendering/Areas.svelte +57 -19
  63. package/dist/components/rendering/Areas.svelte.d.ts +12 -0
  64. package/dist/components/rendering/BaseGPURenderer.svelte.d.ts +3 -1
  65. package/dist/components/rendering/BaseGPURenderer.svelte.js +4 -2
  66. package/dist/components/rendering/Dots.svelte +23 -3
  67. package/dist/components/rendering/Dots.svelte.d.ts +6 -0
  68. package/dist/components/rendering/GridLine.svelte +13 -1
  69. package/dist/components/rendering/GridLine.svelte.d.ts +5 -0
  70. package/dist/components/rendering/Lines.svelte +48 -13
  71. package/dist/components/rendering/Lines.svelte.d.ts +12 -0
  72. package/dist/components/rendering/ParallelLines.svelte +5 -5
  73. package/dist/components/rendering/Rects.svelte +164 -30
  74. package/dist/components/rendering/Rects.svelte.d.ts +23 -2
  75. package/dist/components/rendering/RuleX.svelte +1 -1
  76. package/dist/components/rendering/RuleY.svelte +1 -1
  77. package/dist/components/rendering/WebGPUCanvas.svelte +4 -2
  78. package/dist/components/rendering/activity-reads.d.ts +47 -12
  79. package/dist/components/rendering/activity-reads.js +61 -19
  80. package/dist/components/rendering/uniform-sizes.d.ts +1 -1
  81. package/dist/components/rendering/uniform-sizes.js +1 -1
  82. package/dist/config/ChartConfig.svelte.d.ts +10 -1
  83. package/dist/config/ChartConfig.svelte.js +17 -0
  84. package/dist/gpu/GPUCoordinator.svelte.d.ts +31 -1
  85. package/dist/gpu/GPUCoordinator.svelte.js +164 -7
  86. package/dist/gpu/MorphedChannels.d.ts +11 -28
  87. package/dist/gpu/MorphedChannels.js +72 -8
  88. package/dist/gpu/ScaleTemplateSystem.d.ts +20 -2
  89. package/dist/gpu/ScaleTemplateSystem.js +83 -6
  90. package/dist/index.d.ts +19 -4
  91. package/dist/index.js +22 -4
  92. package/dist/interactors/Interval1D.svelte +6 -1
  93. package/dist/interactors/Interval2D.svelte +6 -1
  94. package/dist/interactors/ParallelAxisSource.svelte +10 -1
  95. package/dist/interactors/Toggle.svelte +32 -3
  96. package/dist/interactors/Tooltip.svelte +138 -41
  97. package/dist/interactors/TooltipManager.svelte.d.ts +20 -5
  98. package/dist/interactors/TooltipManager.svelte.js +46 -12
  99. package/dist/interactors/other-tooltip.d.ts +16 -0
  100. package/dist/interactors/other-tooltip.js +22 -0
  101. package/dist/query-engine/CrossfilterSelection.svelte.d.ts +4 -2
  102. package/dist/query-engine/CrossfilterSelection.svelte.js +2 -0
  103. package/dist/query-engine/FieldActivity.svelte.d.ts +49 -1
  104. package/dist/query-engine/FieldActivity.svelte.js +175 -5
  105. package/dist/query-engine/FilterGroup.svelte.d.ts +23 -1
  106. package/dist/query-engine/FilterGroup.svelte.js +43 -3
  107. package/dist/query-engine/FilterGroupRegistry.svelte.d.ts +31 -3
  108. package/dist/query-engine/FilterGroupRegistry.svelte.js +62 -15
  109. package/dist/query-engine/MarkHandle.svelte.d.ts +28 -1
  110. package/dist/query-engine/MarkHandle.svelte.js +33 -1
  111. package/dist/query-engine/QueryEngine.d.ts +225 -5
  112. package/dist/query-engine/QueryEngine.js +697 -47
  113. package/dist/query-engine/categorical-identity.d.ts +41 -0
  114. package/dist/query-engine/categorical-identity.js +145 -65
  115. package/dist/query-engine/debug/query-log.d.ts +39 -0
  116. package/dist/query-engine/debug/query-log.js +90 -0
  117. package/dist/query-engine/duckdb.d.ts +55 -8
  118. package/dist/query-engine/duckdb.js +82 -30
  119. package/dist/query-engine/index.d.ts +7 -4
  120. package/dist/query-engine/index.js +3 -3
  121. package/dist/query-engine/preagg/BuildQueue.d.ts +33 -13
  122. package/dist/query-engine/preagg/BuildQueue.js +51 -15
  123. package/dist/query-engine/preagg/Preagg.d.ts +46 -3
  124. package/dist/query-engine/preagg/Preagg.js +159 -31
  125. package/dist/query-engine/preagg/plan.d.ts +33 -1
  126. package/dist/query-engine/preagg/plan.js +158 -14
  127. package/dist/query-engine/sliced-lane.d.ts +108 -0
  128. package/dist/query-engine/sliced-lane.js +299 -0
  129. package/dist/query-engine/source-routing.d.ts +106 -0
  130. package/dist/query-engine/source-routing.js +189 -0
  131. package/dist/query-engine/toggle-predicate.d.ts +21 -0
  132. package/dist/query-engine/toggle-predicate.js +29 -1
  133. package/dist/scales/ActivityTable.d.ts +5 -1
  134. package/dist/scales/ActivityTable.js +12 -3
  135. package/dist/scales/BandScaleState.svelte.d.ts +22 -9
  136. package/dist/scales/BandScaleState.svelte.js +39 -21
  137. package/dist/scales/ChartScales.svelte.d.ts +3 -0
  138. package/dist/scales/ChartScales.svelte.js +26 -8
  139. package/dist/scales/ColorScaleState.svelte.d.ts +18 -2
  140. package/dist/scales/ColorScaleState.svelte.js +138 -26
  141. package/dist/scales/LinearScaleState.svelte.d.ts +7 -10
  142. package/dist/scales/LinearScaleState.svelte.js +21 -39
  143. package/dist/scales/ScaleGeneration.svelte.d.ts +48 -0
  144. package/dist/scales/ScaleGeneration.svelte.js +42 -0
  145. package/dist/scales/ScaleRegistry.svelte.js +3 -1
  146. package/dist/scales/SlotTransition.d.ts +15 -1
  147. package/dist/scales/SlotTransition.js +33 -4
  148. package/dist/scales/TimeScaleState.svelte.d.ts +7 -3
  149. package/dist/scales/TimeScaleState.svelte.js +7 -7
  150. package/dist/scales/color-utils.d.ts +0 -3
  151. package/dist/scales/color-utils.js +0 -3
  152. package/dist/scales/entered.d.ts +8 -0
  153. package/dist/scales/entered.js +10 -0
  154. package/dist/spec/LazyQueryEngine.js +2 -19
  155. package/dist/spec/MarkRegistry.d.ts +3 -1
  156. package/dist/spec/MarkRegistry.js +13 -1
  157. package/dist/spec/PlotFromSpec.svelte +1 -0
  158. package/dist/spec/export.js +11 -2
  159. package/dist/spec/types.d.ts +43 -13
  160. package/dist/sql/bin.d.ts +25 -0
  161. package/dist/sql/bin.js +108 -5
  162. package/dist/theme.css +5 -4
  163. package/dist/transforms/TransformCompiler.d.ts +47 -2
  164. package/dist/transforms/TransformCompiler.js +70 -1
  165. package/dist/transforms/index.js +176 -42
  166. package/dist/types.d.ts +177 -2
  167. package/package.json +4 -3
@@ -14,7 +14,16 @@ import type { ContinuousScaleState } from '../../../types';
14
14
  export declare function fetchCategoricalDomains(coordinator: QueryEngine, table: string, select: Record<string, unknown>, actualXScaleType: string, actualYScaleType: string, xScaleState: BandScaleState | null, yScaleState: BandScaleState | null, facetBandScale: BandScaleState | null | undefined, fx: unknown, colorChannel: string, colorScaleState: ColorScaleState | null, sizeChannel: string, sizeScaleState: SizeScaleState | null, seriesScaleState: ColorScaleState | null, seriesChannel: string | null, bandDomainFromData: boolean, markId: string, explicitDomain?: {
15
15
  x?: DimValue[];
16
16
  y?: DimValue[];
17
- } | null): Promise<number>;
17
+ } | null,
18
+ /** nulls="drop": the band and facet domains leave out the NULL category. */
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>;
18
27
  /**
19
28
  * Fetch global min/max extents for aggregated fields using a "sidecar" query.
20
29
  * Runs the same aggregation as the main query but without filters, wrapped
@@ -23,6 +32,8 @@ export declare function fetchCategoricalDomains(coordinator: QueryEngine, table:
23
32
  * Returns `{ globalAggregatesSet: true }` on success so the caller can
24
33
  * update its reactive state.
25
34
  */
26
- 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<{
27
38
  globalAggregatesSet: boolean;
28
39
  }>;
@@ -10,7 +10,20 @@ import { identityKeyFor } from '../../../query-engine/identity-key';
10
10
  *
11
11
  * Returns the number of fetches executed (for logging).
12
12
  */
13
- export async function fetchCategoricalDomains(coordinator, table, select, actualXScaleType, actualYScaleType, xScaleState, yScaleState, facetBandScale, fx, colorChannel, colorScaleState, sizeChannel, sizeScaleState, seriesScaleState, seriesChannel, bandDomainFromData, markId, explicitDomain) {
13
+ export async function fetchCategoricalDomains(coordinator, table, select, actualXScaleType, actualYScaleType, xScaleState, yScaleState, facetBandScale, fx, colorChannel, colorScaleState, sizeChannel, sizeScaleState, seriesScaleState, seriesChannel, bandDomainFromData, markId, explicitDomain,
14
+ /** nulls="drop": the band and facet domains leave out the NULL category. */
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) {
23
+ const categoriesOf = (key) => {
24
+ const categories = coordinator.categoryValues(key, table);
25
+ return dropNull ? categories.filter((c) => c !== null) : categories;
26
+ };
14
27
  const fetches = [];
15
28
  const setupFetch = (axis, field, scaleState) => {
16
29
  if (!field || !scaleState)
@@ -64,8 +77,10 @@ export async function fetchCategoricalDomains(coordinator, table, select, actual
64
77
  fetches.push((async () => {
65
78
  // Idempotent and coalesced — the mark's own setup calls this too.
66
79
  await coordinator.ensureColumns(table, { categorical: [identityField] });
80
+ if (stale())
81
+ return;
67
82
  // This table's categories: the dim spans every table on the field.
68
- const categories = coordinator.categoryValues(identityField.key, table);
83
+ const categories = categoriesOf(identityField.key);
69
84
  // A snapshot of the real categories. The scale adopts the live
70
85
  // identity itself in MarkLifecycleV2's #ensureIdentityColumns, which
71
86
  // also covers the explicit-domain axes that never reach this branch.
@@ -103,9 +118,8 @@ export async function fetchCategoricalDomains(coordinator, table, select, actual
103
118
  // Registered from the data.
104
119
  }
105
120
  else if (explicitDomain?.y && yScaleState) {
106
- // Y band scale maps index 0 → bottom, so reverse the user's
107
- // top-to-bottom array to match GPU rendering order.
108
- deferredExplicitY = [...explicitDomain.y].reverse();
121
+ // Slot 0 is at the top of a y band axis, so the list is used as given.
122
+ deferredExplicitY = explicitDomain.y;
109
123
  }
110
124
  else {
111
125
  setupFetch('y', select.y, yScaleState);
@@ -123,7 +137,9 @@ export async function fetchCategoricalDomains(coordinator, table, select, actual
123
137
  fetches.push((async () => {
124
138
  // Idempotent and coalesced — the mark's own setup calls this too.
125
139
  await coordinator.ensureColumns(table, { categorical: [facetKey] });
126
- const categories = coordinator.categoryValues(facetKey.key, table);
140
+ if (stale())
141
+ return;
142
+ const categories = categoriesOf(facetKey.key);
127
143
  if (categories.length > 0)
128
144
  facetBandScale.addMark(categories, markId);
129
145
  })());
@@ -145,7 +161,9 @@ export async function fetchCategoricalDomains(coordinator, table, select, actual
145
161
  min_time: min(colExpr),
146
162
  max_time: max(colExpr)
147
163
  });
148
- fetches.push(coordinator.query(q).then((result) => {
164
+ fetches.push(coordinator.query(q, { cache: true, source: table }).then((result) => {
165
+ if (stale())
166
+ return;
149
167
  const row = Array.from(result)[0];
150
168
  if (row && row.min_time != null && row.max_time != null) {
151
169
  console.log(`[fetchCategoricalDomains] Time extent for ${axis}:`, [
@@ -187,6 +205,8 @@ export async function fetchCategoricalDomains(coordinator, table, select, actual
187
205
  fetches.push((async () => {
188
206
  // Idempotent and coalesced — the mark's own setup calls this too.
189
207
  await coordinator.ensureColumns(table, { categorical: [colorIdentity] });
208
+ if (stale())
209
+ return;
190
210
  const categories = coordinator.categoryValues(colorIdentity.key);
191
211
  if (categories.length > 0) {
192
212
  colorScaleState.setDomain(categories);
@@ -200,7 +220,9 @@ export async function fetchCategoricalDomains(coordinator, table, select, actual
200
220
  min_color: cast(min(colExpr), 'REAL'),
201
221
  max_color: cast(max(colExpr), 'REAL')
202
222
  });
203
- fetches.push(coordinator.query(q).then((result) => {
223
+ fetches.push(coordinator.query(q, { cache: true, source: table }).then((result) => {
224
+ if (stale())
225
+ return;
204
226
  const row = Array.from(result)[0];
205
227
  if (row && row.min_color != null && row.max_color != null) {
206
228
  console.log(`[fetchCategoricalDomains] Color domain (sequential):`, [
@@ -221,7 +243,9 @@ export async function fetchCategoricalDomains(coordinator, table, select, actual
221
243
  min_radius: cast(min(sqlColumn(String(sizeField))), 'REAL'),
222
244
  max_radius: cast(max(sqlColumn(String(sizeField))), 'REAL')
223
245
  });
224
- fetches.push(coordinator.query(q).then((result) => {
246
+ fetches.push(coordinator.query(q, { cache: true, source: table }).then((result) => {
247
+ if (stale())
248
+ return;
225
249
  const row = Array.from(result)[0];
226
250
  if (row && row.min_radius != null && row.max_radius != null) {
227
251
  sizeScaleState.setDomain([row.min_radius, row.max_radius]);
@@ -236,6 +260,8 @@ export async function fetchCategoricalDomains(coordinator, table, select, actual
236
260
  if (seriesIdentity) {
237
261
  fetches.push((async () => {
238
262
  await coordinator.ensureColumns(table, { categorical: [seriesIdentity] });
263
+ if (stale())
264
+ return;
239
265
  const categories = coordinator.categoryValues(seriesIdentity.key);
240
266
  if (categories.length > 0) {
241
267
  seriesScaleState.setDomain(categories);
@@ -245,6 +271,8 @@ export async function fetchCategoricalDomains(coordinator, table, select, actual
245
271
  }
246
272
  if (fetches.length > 0)
247
273
  await Promise.all(fetches);
274
+ if (stale())
275
+ return fetches.length;
248
276
  // Apply explicit domains after all async fetches — ViewManager columns
249
277
  // from color/series scales are now registered, so the band view will
250
278
  // include all necessary index columns.
@@ -267,7 +295,9 @@ export async function fetchCategoricalDomains(coordinator, table, select, actual
267
295
  * Returns `{ globalAggregatesSet: true }` on success so the caller can
268
296
  * update its reactive state.
269
297
  */
270
- 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) {
271
301
  // Only proceed if we have channels needing data-derived domains
272
302
  if (channelsNeedingDataDomain.size === 0) {
273
303
  console.log(`[${markId}] No aggregated channels, skipping sidecar query`);
@@ -329,7 +359,9 @@ export async function fetchGlobalAggregates(coordinator, table, select, channels
329
359
  const sidecarQuery = Query.from(innerQuery).select(outerSelect);
330
360
  console.log(`📊 [${markId}] Sidecar query SQL:`, sidecarQuery.toString());
331
361
  // Step 3: Execute the query
332
- const result = await coordinator.query(sidecarQuery);
362
+ const result = await coordinator.query(sidecarQuery, { cache: true, source: table });
363
+ if (stale())
364
+ return { globalAggregatesSet: false };
333
365
  const extents = Array.from(result)[0];
334
366
  console.log(`✅ [${markId}] Sidecar results:`, extents);
335
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
@@ -193,12 +200,8 @@ export function registerDataDerivedDomains(data, channelsNeedingDataDomain, addi
193
200
  // order the ranked query had. See RANK_COLUMN.
194
201
  const ranked = data.getColumn(RANK_COLUMN) != null;
195
202
  const ids = extractIdsRankedFromData(data, axis, RANK_COLUMN);
196
- // A y band axis puts slot 0 at the bottom and a ranking is read
197
- // from the top. Reversed here, once, for every ranked axis —
198
- // topN's and the sort's alike — rather than by whichever transform
199
- // produced the rank.
200
- if (ranked && axis === 'y')
201
- ids.reverse();
203
+ // Used as ranked on both axes: slot 0 is at the left of x and the
204
+ // top of y, where a ranking is read from.
202
205
  if (ids.length === 0)
203
206
  continue;
204
207
  // `sorted` says this list is a ranking the axis should prefer over
@@ -221,7 +224,7 @@ export function registerDataDerivedDomains(data, channelsNeedingDataDomain, addi
221
224
  continue;
222
225
  bandScale.addMark(categories, markId);
223
226
  }
224
- bandScale.updateDomainFromExtents();
227
+ bandScale.updateDomainFromExtents({ instant: instantFor?.(bandScale) ?? false });
225
228
  }
226
229
  }
227
230
  // Color domain from query results (needed when sidecar is skipped, e.g. binned marks)
@@ -244,20 +247,14 @@ export function registerDataDerivedDomains(data, channelsNeedingDataDomain, addi
244
247
  // ---------------------------------------------------------------------------
245
248
  // Internal helper
246
249
  // ---------------------------------------------------------------------------
247
- function registerExtentOnScale(scaleState, scaleType, extent, markId, isRebinning, axisLabel) {
250
+ function registerExtentOnScale(scaleState, scaleType, extent, markId, instant, axisLabel) {
248
251
  if (scaleType === 'time' && scaleState?.addOffsetExtent) {
249
252
  // Time scale: data is in offset form, use addOffsetExtent
250
253
  console.log(`📊 [${markId}] Registering ${axisLabel} domain from data (time offsets):`, extent);
251
- scaleState.addOffsetExtent(extent, markId);
254
+ scaleState.addOffsetExtent(extent, markId, { instant });
252
255
  }
253
256
  else if (scaleState?.addMark) {
254
- // Linear/log/symlog scale: use addMark with instant flag when rebinning
255
- if (isRebinning) {
256
- console.log(`⚡ [${markId}] Instant ${axisLabel} domain update (rebinning):`, extent);
257
- }
258
- else {
259
- console.log(`📊 [${markId}] Registering ${axisLabel} domain from data:`, extent);
260
- }
261
- scaleState.addMark(extent, markId, { instant: isRebinning });
257
+ console.log(`📊 [${markId}] Registering ${axisLabel} domain from data:`, extent);
258
+ scaleState.addMark(extent, markId, { instant });
262
259
  }
263
260
  }
@@ -0,0 +1,112 @@
1
+ /**
2
+ * What a mark's Other rows hold, and what is done with it: the one owner of
3
+ * the three things that read an Other row's member lists.
4
+ *
5
+ * - `register`: hands the members to their field, so a click on the bucket
6
+ * selects them and the bucket is lit while the selection reaches into it.
7
+ * - `split`: a bar's selected share, for the bar renderer to draw as two parts.
8
+ * - `stats`: what that share is made of, for a tooltip.
9
+ *
10
+ * Takes getters, not values, so what it reads is tracked by whoever reads it.
11
+ * MarkLifecycleV2 holds one and says only what it is told by the mark.
12
+ */
13
+ import type { Table } from '@uwdata/flechette';
14
+ import type { FilterGroup } from '../../../query-engine/FilterGroup.svelte';
15
+ import type { QueryEngine } from '../../../query-engine/QueryEngine';
16
+ import type { ChannelIdentity } from '../../../types';
17
+ /** What the mark is, for whether its Other bar can be split. */
18
+ export interface SplitPlan {
19
+ markType: string;
20
+ /** The bar's length channel: `y` for vertical bars, `x` for horizontal. */
21
+ length: string | undefined;
22
+ /** The measure topN ranks by, null for its default (the length). */
23
+ by: string | null;
24
+ /** The length channel's select expression. */
25
+ expr: unknown;
26
+ /** The mark's topN has an Other bucket. */
27
+ hasOther: boolean;
28
+ /** JS transforms follow the query, so its rows no longer line up. */
29
+ hasJsTransforms: boolean;
30
+ }
31
+ /** What the bar renderer draws an Other split from. */
32
+ export interface SplitProp {
33
+ /** 0 on rows that are not an Other row, `1 + share` on one. */
34
+ data: Float32Array;
35
+ /** Whether the split travels with the bar's morph or lands at once. */
36
+ slides: boolean;
37
+ /** The band axis the bars are grouped on; the split runs along the other. */
38
+ axis: 'x' | 'y';
39
+ /** Colour is the band's own field, so an Other row's colour term reads its id too. */
40
+ colorOnBand: boolean;
41
+ }
42
+ /** One Other row's share, in numbers. */
43
+ export interface OtherRowStats {
44
+ picked: number;
45
+ total: number;
46
+ pickedCount: number;
47
+ memberCount: number;
48
+ /** The field has a selection; without one picked is everything and says nothing. */
49
+ active: boolean;
50
+ }
51
+ export interface OtherRowsDeps {
52
+ queryEngine: QueryEngine;
53
+ filterGroup: FilterGroup;
54
+ markId: string;
55
+ /** The mark's result, and the version it is at. */
56
+ table: () => Table | null;
57
+ dataVersion: () => number;
58
+ identity: () => Record<string, ChannelIdentity> | null;
59
+ /** The identity key of a band axis, or null when it has none. */
60
+ identityKey: (axis: 'x' | 'y') => string | null;
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
+ */
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;
71
+ /** The mark's plan, or null before its setup completes. */
72
+ plan: () => SplitPlan | null;
73
+ /** The identity key of the mark's ordinal colour scale, or null. */
74
+ colorKey: () => string | null;
75
+ /** The identity key of the chart's band scale on an axis, or null. */
76
+ bandScaleKey: (axis: 'x' | 'y') => string | null;
77
+ }
78
+ export declare class OtherRows {
79
+ #private;
80
+ constructor(deps: OtherRowsDeps);
81
+ /**
82
+ * Register this result's Other rows with their field: what each Other bucket
83
+ * holds, per facet cell, for a click on it to select (FieldActivity.setOther).
84
+ * Per result, so the members follow the filters the mark receives. A mark
85
+ * filtered by the selection (`'filter'`) registers with no group, which is
86
+ * how a click on its Other knows to drill down rather than toggle off.
87
+ */
88
+ register(): void;
89
+ /** Give every registered row up: the mark is going away or starting over. */
90
+ unregister(): void;
91
+ /**
92
+ * The bar renderer's split, or null: no split, the bar is lit or dim as a
93
+ * whole (see #splitAxis for when a bar has one).
94
+ *
95
+ * Per row, share = Σ values[k] · selected(ids[k]) / Σ values[k] over the
96
+ * row's own member lists: the part of the bar the selection holds. A weighted
97
+ * sum over values the query already returned, in JS, per result and per
98
+ * selection change: the alternative is re-querying a mark that dims, which is
99
+ * what dimming exists to avoid.
100
+ */
101
+ get split(): SplitProp | null;
102
+ /** What an Other row's share is made of, or null where it has no split. */
103
+ stats(row: number): OtherRowStats | null;
104
+ /**
105
+ * `stats` of the Other row a tooltip point names: its band id and, with
106
+ * facets, its cell (null: any). A tooltip's rows are not the result's, so it
107
+ * finds its row by identity. A scan of the result's rows, per hover change
108
+ * and not per frame; null where the point is not an Other row of this mark's
109
+ * split.
110
+ */
111
+ statsAt(bandId: number, facetId: number | null): OtherRowStats | null;
112
+ }
@@ -0,0 +1,193 @@
1
+ /**
2
+ * What a mark's Other rows hold, and what is done with it: the one owner of
3
+ * the three things that read an Other row's member lists.
4
+ *
5
+ * - `register`: hands the members to their field, so a click on the bucket
6
+ * selects them and the bucket is lit while the selection reaches into it.
7
+ * - `split`: a bar's selected share, for the bar renderer to draw as two parts.
8
+ * - `stats`: what that share is made of, for a tooltip.
9
+ *
10
+ * Takes getters, not values, so what it reads is tracked by whoever reads it.
11
+ * MarkLifecycleV2 holds one and says only what it is told by the mark.
12
+ */
13
+ import { OTHER_IDS_COLUMN, OTHER_VALUES_COLUMN } from '../../../transforms';
14
+ import { otherSplit, splittable } from './other-split';
15
+ export class OtherRows {
16
+ #deps;
17
+ /** The (field, Other id) pairs this mark has rows registered under. */
18
+ #registered = new Map();
19
+ /** The split last computed, and for which result: what a selection change holds against. */
20
+ #last = null;
21
+ constructor(deps) {
22
+ this.#deps = deps;
23
+ }
24
+ /**
25
+ * Register this result's Other rows with their field: what each Other bucket
26
+ * holds, per facet cell, for a click on it to select (FieldActivity.setOther).
27
+ * Per result, so the members follow the filters the mark receives. A mark
28
+ * filtered by the selection (`'filter'`) registers with no group, which is
29
+ * how a click on its Other knows to drill down rather than toggle off.
30
+ */
31
+ register() {
32
+ const { table: getTable, identity: getIdentity, filterGroup, markId } = this.#deps;
33
+ const table = getTable();
34
+ const identity = getIdentity();
35
+ const group = this.#deps.filtered() ? null : filterGroup.index;
36
+ const now = new Map();
37
+ const ids = table?.getChild(OTHER_IDS_COLUMN) ?? null;
38
+ if (table && identity && ids) {
39
+ const facets = table.getChild('__facet');
40
+ for (const axis of ['x', 'y']) {
41
+ const otherId = identity[axis]?.otherId;
42
+ const key = this.#deps.identityKey(axis);
43
+ const band = table.getChild(axis);
44
+ if (otherId == null || !key || !band)
45
+ continue;
46
+ const rows = [];
47
+ for (let i = 0; i < table.numRows; i++) {
48
+ if (Number(band.at(i)) !== otherId)
49
+ continue;
50
+ const members = ids.at(i);
51
+ if (!members)
52
+ continue;
53
+ rows.push({ facetId: facets ? Number(facets.at(i)) : null, ids: members });
54
+ }
55
+ filterGroup.activity(key).setOther(otherId, markId, { group, rows });
56
+ now.set(key, otherId);
57
+ }
58
+ }
59
+ // A field or bucket this mark no longer draws an Other on gives its rows up.
60
+ for (const [key, otherId] of this.#registered) {
61
+ if (now.get(key) !== otherId)
62
+ filterGroup.activity(key).setOther(otherId, markId, null);
63
+ }
64
+ this.#registered = now;
65
+ }
66
+ /** Give every registered row up: the mark is going away or starting over. */
67
+ unregister() {
68
+ for (const [key, otherId] of this.#registered) {
69
+ this.#deps.filterGroup.activity(key).setOther(otherId, this.#deps.markId, null);
70
+ }
71
+ this.#registered = new Map();
72
+ }
73
+ /**
74
+ * The bar renderer's split, or null: no split, the bar is lit or dim as a
75
+ * whole (see #splitAxis for when a bar has one).
76
+ *
77
+ * Per row, share = Σ values[k] · selected(ids[k]) / Σ values[k] over the
78
+ * row's own member lists: the part of the bar the selection holds. A weighted
79
+ * sum over values the query already returned, in JS, per result and per
80
+ * selection change: the alternative is re-querying a mark that dims, which is
81
+ * what dimming exists to avoid.
82
+ */
83
+ get split() {
84
+ const axis = this.#splitAxis;
85
+ const computed = this.#computed;
86
+ if (!axis || !computed)
87
+ return null;
88
+ return {
89
+ data: computed.split,
90
+ slides: computed.slides,
91
+ axis,
92
+ colorOnBand: this.#colorOnBand(axis)
93
+ };
94
+ }
95
+ /** What an Other row's share is made of, or null where it has no split. */
96
+ stats(row) {
97
+ const computed = this.#computed;
98
+ if (!computed || computed.memberCount[row] === 0)
99
+ return null;
100
+ return {
101
+ picked: computed.picked[row],
102
+ total: computed.total[row],
103
+ pickedCount: computed.pickedCount[row],
104
+ memberCount: computed.memberCount[row],
105
+ active: computed.active
106
+ };
107
+ }
108
+ /**
109
+ * `stats` of the Other row a tooltip point names: its band id and, with
110
+ * facets, its cell (null: any). A tooltip's rows are not the result's, so it
111
+ * finds its row by identity. A scan of the result's rows, per hover change
112
+ * and not per frame; null where the point is not an Other row of this mark's
113
+ * split.
114
+ */
115
+ statsAt(bandId, facetId) {
116
+ const axis = this.#splitAxis;
117
+ const table = this.#deps.table();
118
+ const otherId = axis ? this.#deps.identity()?.[axis]?.otherId : null;
119
+ if (!axis || !table || otherId == null || bandId !== otherId)
120
+ return null;
121
+ const band = table.getChild(axis);
122
+ const facets = facetId == null ? null : table.getChild('__facet');
123
+ if (!band)
124
+ return null;
125
+ for (let row = 0; row < table.numRows; row++) {
126
+ if (Number(band.at(row)) !== bandId)
127
+ continue;
128
+ if (facets && Number(facets.at(row)) !== facetId)
129
+ continue;
130
+ return this.stats(row);
131
+ }
132
+ return null;
133
+ }
134
+ /**
135
+ * The band axis whose Other bar splits, or null. Only where a share is what
136
+ * it looks like:
137
+ * - a bar mark ('bary', 'barx') with a topN Other bucket, dimming (not
138
+ * 'filter' or 'ignore', which never dim) and without JS transforms, whose rows would
139
+ * no longer line up with the query's;
140
+ * - whose length is the measure topN ranks by, which is what Other sums;
141
+ * - and that measure a bare COUNT or SUM, or a raw column: a mean or a max
142
+ * summed into Other is no total, and a share of it would mean nothing.
143
+ */
144
+ get #splitAxis() {
145
+ const plan = this.#deps.plan();
146
+ if (!plan || !this.#deps.dims())
147
+ return null;
148
+ if (!plan.hasOther || plan.hasJsTransforms)
149
+ return null;
150
+ if (plan.markType !== 'bary' && plan.markType !== 'barx')
151
+ return null;
152
+ return splittable(plan.length, plan.by, plan.expr)
153
+ ? plan.markType === 'barx'
154
+ ? 'y'
155
+ : 'x'
156
+ : null;
157
+ }
158
+ /** Whether colour is the band's own field, so an Other row's colour term reads its id too. */
159
+ #colorOnBand(axis) {
160
+ const color = this.#deps.colorKey();
161
+ return color != null && color === this.#deps.bandScaleKey(axis);
162
+ }
163
+ #computed = $derived.by(() => {
164
+ const axis = this.#splitAxis;
165
+ if (!axis)
166
+ return null;
167
+ const dataVersion = this.#deps.dataVersion();
168
+ const table = this.#deps.table();
169
+ const otherId = this.#deps.identity()?.[axis]?.otherId;
170
+ const key = this.#deps.identityKey(axis);
171
+ const ids = table?.getChild(OTHER_IDS_COLUMN) ?? null;
172
+ const values = table?.getChild(OTHER_VALUES_COLUMN) ?? null;
173
+ const band = table?.getChild(axis) ?? null;
174
+ if (!table || otherId == null || !key || !ids || !values || !band)
175
+ return null;
176
+ // The last split, for the same result only: a selection change holds a
177
+ // flat row's position. A new result starts afresh and travels with the bar.
178
+ const { filterGroup, queryEngine } = this.#deps;
179
+ const last = this.#last;
180
+ const next = otherSplit({
181
+ rows: table.numRows,
182
+ band,
183
+ otherId,
184
+ ids,
185
+ values,
186
+ selected: filterGroup.activity(key).selectedValues(filterGroup.index),
187
+ decode: queryEngine.identityFor(key).values,
188
+ previous: last && last.dataVersion === dataVersion ? last.split : null
189
+ });
190
+ this.#last = { dataVersion, split: next };
191
+ return next;
192
+ });
193
+ }
@@ -0,0 +1,80 @@
1
+ /**
2
+ * An Other bar's selected share, per row, and when a bar has one: the pure
3
+ * half of MarkLifecycleV2's `otherSplit`, so it can be tested without a mark.
4
+ */
5
+ /** A column of a result, as Arrow (flechette) hands it: one value per row. */
6
+ export interface RowColumn {
7
+ at(row: number): unknown;
8
+ }
9
+ /** One computed split: the channel, and which Other rows are flat. */
10
+ export interface OtherSplit {
11
+ /** 0 on rows that are not an Other row, `1 + share` (or a held position) on one. */
12
+ split: Float32Array;
13
+ /**
14
+ * 1 on an Other row whose two parts look the same: nothing selected (both
15
+ * lit), or none of its members selected (both dim). Its split position is
16
+ * invisible there, so it is held, and moving out of it lands rather than
17
+ * slides.
18
+ */
19
+ flat: Uint8Array;
20
+ /**
21
+ * Whether the split travels with this write (the renderer morphs it) or
22
+ * lands at once. It lands when a row leaves a flat state: both sides of it
23
+ * look the same at that moment, so a slide would be a movement the eye
24
+ * reads as meaning something; the fades show the change instead.
25
+ */
26
+ slides: boolean;
27
+ /**
28
+ * What the share is made of, per row (0 on a row with no split), for a
29
+ * tooltip to say: `picked` of `total` (Σ values over the selected members,
30
+ * over all), `pickedCount` of `memberCount` categories. With nothing
31
+ * selected everything counts as picked.
32
+ */
33
+ picked: Float64Array;
34
+ total: Float64Array;
35
+ pickedCount: Int32Array;
36
+ memberCount: Int32Array;
37
+ /** The field has a selection: without one, everything counts as picked, which says nothing. */
38
+ active: boolean;
39
+ }
40
+ /**
41
+ * The split channel for the bar renderer, per row: share = Σ values · selected
42
+ * / Σ values over the row's own member lists. Selection is by value (`decode`
43
+ * maps an id to its value), so the NULL category needs nothing special. A row
44
+ * whose members mix signs gets no split (0): a share of a sum that cancels
45
+ * means nothing.
46
+ *
47
+ * `previous`: the last split for the SAME rows (the same result), when only the
48
+ * selection changed. A flat row then holds its previous position, so going flat
49
+ * (a selection cleared, or one that no longer reaches into Other) is a fade,
50
+ * not a slide. Without it (a new result) every row takes its value, and the
51
+ * split travels with the bar's own morph.
52
+ *
53
+ * Cost, measured: about 22 ns per Other member per call (the `selected.has`),
54
+ * so 0.1 ms at 5 000 members and about 1.2 ms at 50 000. Fine per click. If
55
+ * selection ever changes continuously (a brush on a band axis) over a very large
56
+ * Other, test membership against a Uint8Array indexed by id, built once per
57
+ * change, rather than a Set. The arrays allocated here are sized by rows.
58
+ */
59
+ export declare function otherSplit(opts: {
60
+ rows: number;
61
+ band: RowColumn;
62
+ otherId: number;
63
+ ids: RowColumn;
64
+ values: RowColumn;
65
+ selected: ReadonlySet<unknown> | null;
66
+ decode: ArrayLike<unknown>;
67
+ previous?: OtherSplit | null;
68
+ }): OtherSplit;
69
+ /**
70
+ * Whether a bar's Other bucket can be split: its length must be what Other
71
+ * sums, and a total. `length` is the bar's length channel, `by` the channel
72
+ * topN ranks by (null: its default, the length), `expr` the length channel's
73
+ * select expression.
74
+ *
75
+ * Other sums `by`, so a length channel that is not `by` is 0 on the Other row.
76
+ * A bare COUNT or SUM, or a raw column (an ungrouped mark), is a total; a mean,
77
+ * a max, a ratio or a distinct count summed into Other is not, and a share of
78
+ * it would look exact while meaning nothing.
79
+ */
80
+ export declare function splittable(length: string | undefined, by: string | null, expr: unknown): boolean;