@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
@@ -0,0 +1,112 @@
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
+ /**
6
+ * The split channel for the bar renderer, per row: share = Σ values · selected
7
+ * / Σ values over the row's own member lists. Selection is by value (`decode`
8
+ * maps an id to its value), so the NULL category needs nothing special. A row
9
+ * whose members mix signs gets no split (0): a share of a sum that cancels
10
+ * means nothing.
11
+ *
12
+ * `previous`: the last split for the SAME rows (the same result), when only the
13
+ * selection changed. A flat row then holds its previous position, so going flat
14
+ * (a selection cleared, or one that no longer reaches into Other) is a fade,
15
+ * not a slide. Without it (a new result) every row takes its value, and the
16
+ * split travels with the bar's own morph.
17
+ *
18
+ * Cost, measured: about 22 ns per Other member per call (the `selected.has`),
19
+ * so 0.1 ms at 5 000 members and about 1.2 ms at 50 000. Fine per click. If
20
+ * selection ever changes continuously (a brush on a band axis) over a very large
21
+ * Other, test membership against a Uint8Array indexed by id, built once per
22
+ * change, rather than a Set. The arrays allocated here are sized by rows.
23
+ */
24
+ export function otherSplit(opts) {
25
+ const { rows, band, otherId, ids, values, selected, decode } = opts;
26
+ const previous = opts.previous && opts.previous.split.length === rows ? opts.previous : null;
27
+ const split = new Float32Array(rows);
28
+ const flat = new Uint8Array(rows);
29
+ const pickedSums = new Float64Array(rows);
30
+ const totals = new Float64Array(rows);
31
+ const pickedCount = new Int32Array(rows);
32
+ const memberCount = new Int32Array(rows);
33
+ let slides = true;
34
+ for (let row = 0; row < rows; row++) {
35
+ if (Number(band.at(row)) !== otherId)
36
+ continue;
37
+ const memberIds = ids.at(row);
38
+ const memberValues = values.at(row);
39
+ if (!memberIds || !memberValues)
40
+ continue;
41
+ let total = 0;
42
+ let picked = 0;
43
+ let count = 0;
44
+ let positive = false;
45
+ let negative = false;
46
+ for (let k = 0; k < memberIds.length; k++) {
47
+ const v = memberValues[k];
48
+ if (v > 0)
49
+ positive = true;
50
+ else if (v < 0)
51
+ negative = true;
52
+ total += v;
53
+ if (selected == null || selected.has(decode[memberIds[k]])) {
54
+ picked += v;
55
+ count++;
56
+ }
57
+ }
58
+ if (positive && negative)
59
+ continue;
60
+ pickedSums[row] = picked;
61
+ totals[row] = total;
62
+ pickedCount[row] = count;
63
+ memberCount[row] = memberIds.length;
64
+ const share = total === 0 ? (selected == null ? 1 : 0) : Math.min(1, Math.max(0, picked / total));
65
+ const isFlat = selected == null || share === 0;
66
+ flat[row] = isFlat ? 1 : 0;
67
+ if (isFlat && previous) {
68
+ const held = previous.split[row];
69
+ split[row] = held > 0.5 ? held : 2;
70
+ }
71
+ else {
72
+ split[row] = 1 + share;
73
+ }
74
+ if (previous && previous.flat[row] === 1 && !isFlat)
75
+ slides = false;
76
+ }
77
+ return {
78
+ split,
79
+ flat,
80
+ slides,
81
+ picked: pickedSums,
82
+ total: totals,
83
+ pickedCount,
84
+ memberCount,
85
+ active: selected != null
86
+ };
87
+ }
88
+ /**
89
+ * Whether a bar's Other bucket can be split: its length must be what Other
90
+ * sums, and a total. `length` is the bar's length channel, `by` the channel
91
+ * topN ranks by (null: its default, the length), `expr` the length channel's
92
+ * select expression.
93
+ *
94
+ * Other sums `by`, so a length channel that is not `by` is 0 on the Other row.
95
+ * A bare COUNT or SUM, or a raw column (an ungrouped mark), is a total; a mean,
96
+ * a max, a ratio or a distinct count summed into Other is not, and a share of
97
+ * it would look exact while meaning nothing.
98
+ */
99
+ export function splittable(length, by, expr) {
100
+ if (!length || (by ?? length) !== length)
101
+ return false;
102
+ if (typeof expr === 'string')
103
+ return true;
104
+ const node = expr;
105
+ if (!node || typeof node !== 'object')
106
+ return false;
107
+ if (node.type === 'COLUMN_REF')
108
+ return true;
109
+ if (node.type !== 'AGGREGATE' || node.isDistinct)
110
+ return false;
111
+ return node.name === 'count' || node.name === 'sum';
112
+ }
@@ -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
@@ -26,7 +35,31 @@ export interface QueryParts {
26
35
  * CAST for ORDER BY, ordinal CASE for PARTITION BY). Used by preagg to
27
36
  * fold redundant window col refs into channel aliases. */
28
37
  passthroughChannels: Map<string, string>;
38
+ /**
39
+ * `nulls="drop"`: the categorical channels whose NULL the mark leaves out.
40
+ * Stated by CHANNEL, not by source column, so every path filters in the
41
+ * terms of its own select: `assembleQuery` reads each channel's expression
42
+ * from the `selectDef` it is given — the id column on the base table, the
43
+ * preagg table's column when preagg rewrote it — and ANDs the predicate
44
+ * into the base scan, before any transform.
45
+ */
46
+ nullDrops?: readonly NullDrop[];
47
+ }
48
+ /**
49
+ * One channel whose NULL a mark drops. With `nullId`, the channel carries dim
50
+ * ids and the NULL category is that id: `<channel> <> nullId`. Without (a raw
51
+ * value, or a dim with no NULL row): `<channel> IS NOT NULL`.
52
+ */
53
+ export interface NullDrop {
54
+ channel: string;
55
+ nullId: number | null;
29
56
  }
57
+ /**
58
+ * The predicates for `drops`, each on its channel's expression in `selectDef`.
59
+ * A dropped channel must be a row-level group key of the query; one the query
60
+ * does not select would be left unfiltered, so that is an error, not a skip.
61
+ */
62
+ export declare function nullDropPredicates(drops: readonly NullDrop[], selectDef: Record<string, unknown>): ExprNode[];
30
63
  /** Window functions to apply in an outer CTE over the assembled inner query. */
31
64
  export interface WindowWrap {
32
65
  /** channel → outer WindowNode (already rewritten to reference channel aliases). */
@@ -38,9 +71,10 @@ export interface AssembleOptions {
38
71
  parts: QueryParts;
39
72
  /** Per-call filter predicates (crossfilter, bin filters). */
40
73
  filter?: unknown[];
41
- /** Columns to add IS NOT NULL predicates for. */
42
74
  transformCompiler: TransformCompiler;
43
75
  limit?: number | null;
76
+ /** The table already has the mark's row filters (`filter()`) applied: a preagg table. */
77
+ skipRowFilters?: boolean;
44
78
  /** When set, the inner query is wrapped in a CTE and these window
45
79
  * functions are applied in the outer SELECT. ORDER BY / LIMIT move to
46
80
  * the outer query. */
@@ -49,8 +83,8 @@ export interface AssembleOptions {
49
83
  /**
50
84
  * Wrap an expression so DuckDB returns a Float32-compatible REAL with NULLs
51
85
  * mapped to NaN: COALESCE(CAST(expr AS REAL), 'NaN'::REAL). Arrow columns then
52
- * have nullCount=0 and clean buffers for zero-copy GPU upload; shaders detect
53
- * 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.
54
88
  */
55
89
  export declare function castReal(expr: unknown): ExprNode;
56
90
  /**
@@ -134,5 +168,21 @@ export declare function buildQueryParts(select: Record<string, unknown>, orderCo
134
168
  * discarded one level up.
135
169
  */
136
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
+ */
137
187
  export declare function assembleQuery(opts: AssembleOptions): Query;
138
188
  export {};
@@ -1,13 +1,30 @@
1
- import { isNode, isAggregateExpression, isColumnRef, isColumnParam, column as sqlColumn, cast, coalesce, collectColumns, rewrite, sql, isNotNull, and, sum, avg, min, max, Query, WindowNode } from '@uwdata/mosaic-sql';
1
+ import { isNode, isAggregateExpression, isColumnRef, isColumnParam, column as sqlColumn, cast, coalesce, collectColumns, rewrite, sql, isNotNull, neq, literal, and, sum, avg, min, max, Query, WindowNode } from '@uwdata/mosaic-sql';
2
2
  import { isWindowExpression, isVerbatimWindowExpression } from '../../../sql/expression-utils';
3
+ /**
4
+ * The predicates for `drops`, each on its channel's expression in `selectDef`.
5
+ * A dropped channel must be a row-level group key of the query; one the query
6
+ * does not select would be left unfiltered, so that is an error, not a skip.
7
+ */
8
+ export function nullDropPredicates(drops, selectDef) {
9
+ const out = [];
10
+ for (const { channel, nullId } of drops) {
11
+ const value = selectDef[channel];
12
+ if (value === undefined) {
13
+ throw new Error(`nulls="drop" filters the "${channel}" channel, which this query does not select.`);
14
+ }
15
+ const expr = (typeof value === 'string' ? sqlColumn(value) : value);
16
+ out.push(nullId === null ? isNotNull(expr) : neq(expr, literal(nullId)));
17
+ }
18
+ return out;
19
+ }
3
20
  // ---------------------------------------------------------------------------
4
21
  // Numeric CAST policy (single rule for every assembly path)
5
22
  // ---------------------------------------------------------------------------
6
23
  /**
7
24
  * Wrap an expression so DuckDB returns a Float32-compatible REAL with NULLs
8
25
  * mapped to NaN: COALESCE(CAST(expr AS REAL), 'NaN'::REAL). Arrow columns then
9
- * have nullCount=0 and clean buffers for zero-copy GPU upload; shaders detect
10
- * 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.
11
28
  */
12
29
  export function castReal(expr) {
13
30
  return coalesce(cast(expr, 'REAL'), sql `'NaN'::REAL`);
@@ -202,22 +219,6 @@ export function buildQueryParts(select, orderConfig, hasSQLTransforms, passthrou
202
219
  // ---------------------------------------------------------------------------
203
220
  // Assembly — the one sequence every query path follows
204
221
  // ---------------------------------------------------------------------------
205
- /**
206
- * Assemble a Query from cached parts + per-call filter.
207
- *
208
- * Order is fixed and shared by the flat, window-CTE, and preagg paths:
209
- *
210
- * SELECT parts.selectDef FROM table [GROUP BY dims]
211
- * WHERE filter AND nulls — on the base scan, so transform CTEs
212
- * (topN ranking etc.) see filtered rows
213
- * → SQL transforms (CTE wrapping)
214
- * → [window CTE: outer SELECT applies window fns over the inner result]
215
- * → ORDER BY outerOrder(...) — alignment, then orderChannels; outermost
216
- * → LIMIT
217
- *
218
- * Callers decide what is CAST-wrapped by preparing parts.selectDef with
219
- * castNumeric(); assembly never inspects expression types.
220
- */
221
222
  /** Output aliases a query exposes to whatever wraps it. */
222
223
  function selectAliases(query) {
223
224
  const sel = query._select;
@@ -266,20 +267,39 @@ export function outerOrder(preference, alignment) {
266
267
  add(channel);
267
268
  return order;
268
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
+ */
269
286
  export function assembleQuery(opts) {
270
- const { table, parts, filter = [], transformCompiler, limit, window } = opts;
287
+ const { table, parts, filter = [], transformCompiler, limit, window, skipRowFilters } = opts;
271
288
  const { selectDef, nonAggregateGroupBy, hasAggregates, hasSQLTransforms, orderChannels } = parts;
272
289
  const q = Query.from(table).select(selectDef);
273
290
  if (hasAggregates && nonAggregateGroupBy.length > 0) {
274
291
  q.groupby(nonAggregateGroupBy);
275
292
  }
276
- const allFilters = [...filter];
293
+ const allFilters = [
294
+ ...filter,
295
+ ...nullDropPredicates(parts.nullDrops ?? [], selectDef)
296
+ ];
277
297
  if (allFilters.length > 0) {
278
298
  q.where(and(allFilters));
279
299
  }
280
300
  let result = q;
281
301
  if (hasSQLTransforms) {
282
- result = transformCompiler.applyToQuery(q, { groupKeys: parts.groupKeys });
302
+ result = transformCompiler.applyToQuery(q, { groupKeys: parts.groupKeys, bins: parts.bins ?? null, padded: parts.padded ?? false }, { skipRowFilters });
283
303
  }
284
304
  if (window) {
285
305
  const outerSelect = {};
@@ -307,7 +327,10 @@ export function assembleQuery(opts) {
307
327
  .with({ [window.cteName]: result });
308
328
  }
309
329
  const alignment = hasSQLTransforms
310
- ? transformCompiler.alignment(Object.keys(selectDef), { groupKeys: parts.groupKeys })
330
+ ? transformCompiler.alignment(Object.keys(selectDef), {
331
+ groupKeys: parts.groupKeys,
332
+ bins: parts.bins ?? null
333
+ })
311
334
  : null;
312
335
  const order = outerOrder(orderChannels, alignment);
313
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'>>, 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
  },
@@ -33,6 +36,12 @@ export function buildReactiveConfig(entry, opts) {
33
36
  },
34
37
  get selectionResponse() {
35
38
  return entry.selectionResponse;
39
+ },
40
+ get nulls() {
41
+ return entry.nulls;
42
+ },
43
+ get contributeDomain() {
44
+ return entry.contributeDomain;
36
45
  }
37
46
  };
38
47
  }
@@ -1,3 +1,4 @@
1
+ import type { OtherRowStats } from '../lifecycle/other-rows.svelte';
1
2
  import type { ChartContext } from '../../../types';
2
3
  import type { ColorScaleState } from '../../../scales/ColorScaleState.svelte';
3
4
  import type { DimValue } from '../../../query-engine/categorical-identity';
@@ -34,6 +35,7 @@ interface TooltipConfigParams {
34
35
  constantRadius?: number;
35
36
  sizeScaleState?: SizeScaleState | null;
36
37
  displayFields?: string[] | null;
38
+ otherStats?: TooltipConfig['otherStats'];
37
39
  }
38
40
  interface TooltipConfig {
39
41
  tempTable: string;
@@ -61,7 +63,8 @@ interface TooltipConfig {
61
63
  constantRadius?: number;
62
64
  sizeScaleState?: SizeScaleState | null;
63
65
  displayFields?: string[] | null;
66
+ otherStats?: (point: Record<string, unknown>) => OtherRowStats | null;
64
67
  enabled: boolean;
65
68
  }
66
- export declare function buildTooltipConfig({ tempTable, markType, chartContext, channelFields, columns, bandAxes, colorColumn, markColor, seriesColumn, markSeries, proximityAxis, constantRadius, sizeScaleState, displayFields }: TooltipConfigParams): TooltipConfig;
69
+ export declare function buildTooltipConfig({ tempTable, markType, chartContext, channelFields, columns, bandAxes, colorColumn, markColor, seriesColumn, markSeries, proximityAxis, constantRadius, sizeScaleState, displayFields, otherStats }: TooltipConfigParams): TooltipConfig;
67
70
  export {};
@@ -1,4 +1,4 @@
1
- export function buildTooltipConfig({ tempTable, markType, chartContext, channelFields, columns, bandAxes, colorColumn, markColor, seriesColumn, markSeries, proximityAxis, constantRadius, sizeScaleState, displayFields }) {
1
+ export function buildTooltipConfig({ tempTable, markType, chartContext, channelFields, columns, bandAxes, colorColumn, markColor, seriesColumn, markSeries, proximityAxis, constantRadius, sizeScaleState, displayFields, otherStats }) {
2
2
  const colorScaleState = markColor.scaleState;
3
3
  const constantColor = markColor.constantCssColor;
4
4
  return {
@@ -33,6 +33,7 @@ export function buildTooltipConfig({ tempTable, markType, chartContext, channelF
33
33
  ...(constantRadius != null && { constantRadius }),
34
34
  ...(sizeScaleState && { sizeScaleState }),
35
35
  ...(displayFields && { displayFields }),
36
+ ...(otherStats && { otherStats }),
36
37
  enabled: true
37
38
  };
38
39
  }