@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,189 @@
1
+ /**
2
+ * Where an interaction read runs, by the source it reads: the main connection
3
+ * (`runQuery`: no polling, no chunked fetch) or a sliced lane, where a slow
4
+ * read interleaves with everyone else's instead of holding the worker.
5
+ *
6
+ * A COST RECORD PER SOURCE (the table a statement actually reads: a base table,
7
+ * a view, or a preagg table): unknown, fast or slow.
8
+ *
9
+ * - Unknown and slow sources read on a lane; fast ones on the main connection.
10
+ * - LANES ARE FOR SLOW SOURCES ONLY. A lane read costs 20-27 ms where the main
11
+ * connection takes 2-5 ms (measured on /test/entrance at load): it needs at
12
+ * least three round trips (start, fetch, a final empty fetch), and each
13
+ * waits behind a slice of every other busy lane, a cost that grows with the
14
+ * number of busy lanes (so the pool stays small).
15
+ * - Hence the PRIOR, where a source starts: first from the DDL that made it
16
+ * through the engine (QueryEngine #noteDDL: a table starts fast, a view
17
+ * unknown), else from the catalog, looked up off the critical path (a
18
+ * table in an in-memory catalog starts fast; anything else unknown). A
19
+ * preagg table, which fjader builds in memory, starts fast. fjader's own
20
+ * identity views are never a source: a mark's reads name its base table.
21
+ * An app can override the prior (QueryEngine.sourceCost), and a
22
+ * measurement corrects either.
23
+ * - TEMP sources never leave the main connection: they exist only on the
24
+ * connection that made it (QueryEngine #localSources).
25
+ * - Only DATA reads update the record. A source's first statements are a
26
+ * DESCRIBE or a range query, quick even when its data query (a GROUP BY, a
27
+ * window, dense padding) is not.
28
+ * - On a lane the measure is the poll count, taken in the worker: finished in
29
+ * its first slice or the next marks the source FAST, whatever its record
30
+ * was. Symmetric on purpose: the usual slow read is a cold one (remote
31
+ * parquet, a view slow the first time), and brushing that source later is
32
+ * quick; a "slow" that stuck would slice every brush frame for the session.
33
+ * - More slices than that marks it SLOW, unless the result was large and its
34
+ * execution short: a 2M-row read passes two 5 ms slices on transfer and
35
+ * decode alone, the fetch phase barely holds the worker, and on a lane it
36
+ * would come back in ~1000 chunks (+~40 ms a frame). Large results stay on
37
+ * the main connection unless execution alone is long.
38
+ * - On the main connection the measure is send-to-bytes time, taken on the
39
+ * main thread, so a busy frame inflates it: a fast source turns slow only
40
+ * after TWO consecutive slow reads.
41
+ *
42
+ * ORDER when a source changes lane. Its next read may go to the main
43
+ * connection while an earlier one is still queued on its lane. Mark data
44
+ * cannot overtake itself: execute-latest keeps at most one read of a mark in
45
+ * flight, and the next waits for it. Setup reads are cached and order-free.
46
+ * A future read without a callerId that cares about order must keep its
47
+ * source's lane.
48
+ */
49
+ /** Past this many polls (slices after the first) a lane read was slow. */
50
+ export const FAST_POLLS = 1;
51
+ /** A main-connection read slower than this, twice running, turns its source slow. */
52
+ export const SLOW_MAIN_MS = 50;
53
+ /** A result this large is "large": it stays on the main connection... */
54
+ export const LARGE_ROWS = 200_000;
55
+ /** ...unless its execution alone takes longer than this. */
56
+ export const LARGE_EXEC_MS = 100;
57
+ export class SourceRouting {
58
+ #records = new Map();
59
+ #laneCount;
60
+ /** Sources per lane, to place a new one on the least used. */
61
+ #laneLoad;
62
+ constructor(laneCount) {
63
+ this.#laneCount = laneCount;
64
+ this.#laneLoad = new Array(laneCount).fill(0);
65
+ }
66
+ #of(source) {
67
+ let r = this.#records.get(source);
68
+ if (!r) {
69
+ let lane = 0;
70
+ for (let i = 1; i < this.#laneCount; i++) {
71
+ if (this.#laneLoad[i] < this.#laneLoad[lane])
72
+ lane = i;
73
+ }
74
+ this.#laneLoad[lane]++;
75
+ r = { cost: 'unknown', hinted: false, mainSlow: 0, lane };
76
+ this.#records.set(source, r);
77
+ }
78
+ return r;
79
+ }
80
+ cost(source) {
81
+ return this.#records.get(source)?.cost ?? 'unknown';
82
+ }
83
+ /**
84
+ * The prior for a source not yet measured: what it is (see the module
85
+ * comment). Applies only while its record is unknown and not hinted.
86
+ */
87
+ prior(source, cost) {
88
+ const r = this.#of(source);
89
+ if (r.cost === 'unknown' && !r.hinted)
90
+ r.cost = cost;
91
+ }
92
+ /**
93
+ * The source was dropped or replaced by something else: back to unknown,
94
+ * unless the app hinted it.
95
+ */
96
+ forget(source) {
97
+ const r = this.#records.get(source);
98
+ if (!r || r.hinted)
99
+ return;
100
+ r.cost = 'unknown';
101
+ r.mainSlow = 0;
102
+ }
103
+ /** The app knows better than the prior: a slow table, a cheap view. */
104
+ hint(source, cost) {
105
+ const r = this.#of(source);
106
+ r.cost = cost;
107
+ r.hinted = true;
108
+ r.mainSlow = 0;
109
+ }
110
+ /** The lane a source sticks to, whatever its cost (a read forced sliced). */
111
+ laneOf(source) {
112
+ return this.#of(source).lane;
113
+ }
114
+ /** The lane index to read `source` on, or null for the main connection. */
115
+ route(source) {
116
+ const r = this.#of(source);
117
+ return r.cost === 'fast' ? null : r.lane;
118
+ }
119
+ /**
120
+ * A data read finished on a lane: `polls` slices after the first, `execMs`
121
+ * until its result header, `rows` in the result.
122
+ */
123
+ noteLaneRead(source, polls, execMs, rows) {
124
+ const r = this.#of(source);
125
+ r.mainSlow = 0;
126
+ if (polls <= FAST_POLLS)
127
+ r.cost = 'fast';
128
+ else if (rows > LARGE_ROWS && execMs <= LARGE_EXEC_MS)
129
+ r.cost = 'fast';
130
+ else
131
+ r.cost = 'slow';
132
+ }
133
+ /** A data read finished on the main connection, `ms` from send to its bytes. */
134
+ noteMainRead(source, ms, rows) {
135
+ const r = this.#of(source);
136
+ const slow = rows > LARGE_ROWS ? ms > LARGE_EXEC_MS : ms > SLOW_MAIN_MS;
137
+ if (!slow) {
138
+ r.mainSlow = 0;
139
+ if (r.cost === 'unknown')
140
+ r.cost = 'fast';
141
+ return;
142
+ }
143
+ if (++r.mainSlow >= 2) {
144
+ r.cost = 'slow';
145
+ r.mainSlow = 0;
146
+ }
147
+ }
148
+ /**
149
+ * A data reload: every measured cost is unknown again (the prior is set
150
+ * again by the next describe); an app's hint stays.
151
+ */
152
+ reset() {
153
+ for (const r of this.#records.values()) {
154
+ if (r.hinted)
155
+ continue;
156
+ r.cost = 'unknown';
157
+ r.mainSlow = 0;
158
+ }
159
+ }
160
+ }
161
+ /**
162
+ * What a statement does to the set of sources, read from its text: a table or
163
+ * view created under a plain name, or one dropped. Narrow on purpose: a wrong
164
+ * "table" (fast) is the expensive mistake, a wrong "unknown" costs one sliced
165
+ * read (~20 ms) before the first quick one corrects it. Qualified names
166
+ * (an attached database), CREATE MACRO, TYPE, SEQUENCE and anything else
167
+ * return null.
168
+ */
169
+ export function sourceDDL(sql) {
170
+ const name = String.raw `("(?:[^"]|"")+"|[A-Za-z_][A-Za-z0-9_$]*)`;
171
+ const end = String.raw `(?=\s|\(|;|$)`;
172
+ const create = new RegExp(String.raw `^\s*CREATE\s+(?:OR\s+REPLACE\s+)?((?:TEMP|TEMPORARY)\s+)?(TABLE|VIEW)\s+(?:IF\s+NOT\s+EXISTS\s+)?` +
173
+ name +
174
+ end, 'i').exec(sql);
175
+ if (create)
176
+ return {
177
+ kind: create[2].toUpperCase() === 'TABLE' ? 'table' : 'view',
178
+ name: unquote(create[3]),
179
+ // TEMP objects belong to the connection that made them.
180
+ temp: !!create[1]
181
+ };
182
+ const drop = new RegExp(String.raw `^\s*DROP\s+(?:TABLE|VIEW)\s+(?:IF\s+EXISTS\s+)?` + name + end, 'i').exec(sql);
183
+ if (drop)
184
+ return { kind: 'drop', name: unquote(drop[1]) };
185
+ return null;
186
+ }
187
+ function unquote(name) {
188
+ return name.startsWith('"') ? name.slice(1, -1).replaceAll('""', '"') : name;
189
+ }
@@ -8,3 +8,24 @@ import { type ExprNode } from '@uwdata/mosaic-sql';
8
8
  * so a predicate is never rewritten after the fact.
9
9
  */
10
10
  export declare function togglePredicate(exprs: ExprNode[], tuples: unknown[][]): ExprNode;
11
+ /**
12
+ * Above this many ids, a selection over an id column is a bitmap, not an `IN`
13
+ * list (see idBitmapPredicate). The crossover measured in DuckDB-WASM 1.30 lies
14
+ * between 500 values (`IN`, 29 ms on 2 M rows) and the bitmap (26 ms).
15
+ */
16
+ export declare const BITMAP_MIN_IDS = 256;
17
+ /**
18
+ * `expr`'s id is one of `ids`, as one constant-size literal:
19
+ * `get_bit('<bits>'::BIT, <index>) = 1`.
20
+ *
21
+ * Never `NOT IN`, whatever the selection's size: DuckDB evaluates it as a chain
22
+ * of `<>`, 2.8 s on 2 M rows for 1 000 ids. And never a long `IN`, which it
23
+ * spends most of its time parsing.
24
+ *
25
+ * Bit `id + 1` is set for each selected id; bit 0 is id -1 ("not in the dim
26
+ * yet") and is never set; the last bit, `size + 1`, is a sentinel kept 0. The
27
+ * index is clamped onto [0, sentinel], so an id appended to the dim after the
28
+ * selection was made reads the sentinel (it was not selected: a snapshot), and
29
+ * get_bit never sees an index out of range, which is an error rather than a 0.
30
+ */
31
+ export declare function idBitmapPredicate(expr: ExprNode, ids: readonly number[], size: number): ExprNode;
@@ -1,4 +1,4 @@
1
- import { and as sqlAnd, isIn, isNotDistinct, literal, or } from '@uwdata/mosaic-sql';
1
+ import { and as sqlAnd, isIn, isNotDistinct, literal, or, sql as sqlTag } from '@uwdata/mosaic-sql';
2
2
  /**
3
3
  * A toggle's clause over its selected tuples: `IN` for one field, an `OR` of
4
4
  * `IS NOT DISTINCT FROM` tuples for several, so a NULL member matches NULL.
@@ -25,3 +25,31 @@ export function togglePredicate(exprs, tuples) {
25
25
  const clauses = tuples.map((t) => sqlAnd(t.map((v, i) => isNotDistinct(exprs[i], literal(v)))));
26
26
  return clauses.length === 1 ? clauses[0] : or(clauses);
27
27
  }
28
+ /**
29
+ * Above this many ids, a selection over an id column is a bitmap, not an `IN`
30
+ * list (see idBitmapPredicate). The crossover measured in DuckDB-WASM 1.30 lies
31
+ * between 500 values (`IN`, 29 ms on 2 M rows) and the bitmap (26 ms).
32
+ */
33
+ export const BITMAP_MIN_IDS = 256;
34
+ /**
35
+ * `expr`'s id is one of `ids`, as one constant-size literal:
36
+ * `get_bit('<bits>'::BIT, <index>) = 1`.
37
+ *
38
+ * Never `NOT IN`, whatever the selection's size: DuckDB evaluates it as a chain
39
+ * of `<>`, 2.8 s on 2 M rows for 1 000 ids. And never a long `IN`, which it
40
+ * spends most of its time parsing.
41
+ *
42
+ * Bit `id + 1` is set for each selected id; bit 0 is id -1 ("not in the dim
43
+ * yet") and is never set; the last bit, `size + 1`, is a sentinel kept 0. The
44
+ * index is clamped onto [0, sentinel], so an id appended to the dim after the
45
+ * selection was made reads the sentinel (it was not selected: a snapshot), and
46
+ * get_bit never sees an index out of range, which is an error rather than a 0.
47
+ */
48
+ export function idBitmapPredicate(expr, ids, size) {
49
+ const bits = new Array(size + 2).fill('0');
50
+ for (const id of ids)
51
+ if (id >= 0 && id < size)
52
+ bits[id + 1] = '1';
53
+ const sentinel = size + 1;
54
+ return sqlTag `get_bit(${literal(bits.join(''))}::BIT, CAST(greatest(least(${expr} + 1, ${sentinel}), 0) AS INTEGER)) = 1`;
55
+ }
@@ -85,8 +85,12 @@ export declare class ActivityTable {
85
85
  * fade down from 1.
86
86
  *
87
87
  * `resize(0)` drops every id, for ids that stopped meaning their category.
88
+ *
89
+ * `trailing`: the last slot is not an id but a slot of its own that stays
90
+ * last (FieldActivity's free slot). Its fade moves to the new end, and the
91
+ * slot it leaves is a new id like any other, rather than inheriting it.
88
92
  */
89
- resize(count: number, active?: readonly number[] | null): void;
93
+ resize(count: number, active?: readonly number[] | null, trailing?: boolean): void;
90
94
  /**
91
95
  * Select exactly these ids; every other id fades out. Ids outside the table
92
96
  * are ignored. Does nothing when the selection is the one already headed to.
@@ -117,11 +117,18 @@ export class ActivityTable {
117
117
  * fade down from 1.
118
118
  *
119
119
  * `resize(0)` drops every id, for ids that stopped meaning their category.
120
+ *
121
+ * `trailing`: the last slot is not an id but a slot of its own that stays
122
+ * last (FieldActivity's free slot). Its fade moves to the new end, and the
123
+ * slot it leaves is a new id like any other, rather than inheriting it.
120
124
  */
121
- resize(count, active) {
125
+ resize(count, active, trailing = false) {
122
126
  if (count === this.#to.length)
123
127
  return;
124
- const keep = Math.min(this.#to.length, count);
128
+ const oldLength = this.#to.length;
129
+ const tail = trailing && oldLength > 0 && count > 0;
130
+ const keep = tail ? Math.min(oldLength - 1, count - 1) : Math.min(oldLength, count);
131
+ const ids = tail ? count - 1 : count;
125
132
  const selecting = active === undefined ? this.#selecting : active !== null;
126
133
  const fill = selecting ? 0 : 1;
127
134
  const grown = (old) => {
@@ -130,9 +137,11 @@ export class ActivityTable {
130
137
  next.set(old.subarray(0, keep), 0);
131
138
  if (active) {
132
139
  for (const id of active)
133
- if (id >= keep && id < count)
140
+ if (id >= keep && id < ids)
134
141
  next[id] = 1;
135
142
  }
143
+ if (tail)
144
+ next[count - 1] = old[oldLength - 1];
136
145
  return next;
137
146
  };
138
147
  this.#from = grown(this.#from);
@@ -211,6 +211,8 @@ export declare class BandScaleState {
211
211
  * undisplayed Other is ignored.
212
212
  */
213
213
  setOtherId(id: number | null): void;
214
+ /** This axis's Other bucket id, while a mark on it has one; null otherwise. */
215
+ get otherId(): number | null;
214
216
  /**
215
217
  * This axis's Other bucket, shared by every mark on it that has one.
216
218
  *
@@ -308,7 +310,9 @@ export declare class BandScaleState {
308
310
  * reports what CHANGED rather than notifying anyone: nothing below runs for
309
311
  * an answer identical to the one already on screen.
310
312
  */
311
- updateDomainFromExtents(): void;
313
+ updateDomainFromExtents(options?: {
314
+ instant?: boolean;
315
+ }): void;
312
316
  /**
313
317
  * The band channel as GPU-ready floats.
314
318
  *
@@ -331,7 +335,7 @@ export declare class BandScaleState {
331
335
  /**
332
336
  * Get D3 band scale configured for rendering
333
337
  */
334
- getBaseScale(axis: string, innerWidthOrHeight: number): ScaleBand<DimValue>;
338
+ getBaseScale(_axis: string, innerWidthOrHeight: number): ScaleBand<DimValue>;
335
339
  /**
336
340
  * Update scale domain (for D3 scale)
337
341
  */
@@ -360,6 +364,12 @@ export declare class BandScaleState {
360
364
  * Changes only when the dim or the domain does.
361
365
  */
362
366
  getTicks(): Float32Array;
367
+ /**
368
+ * The slots' latest layout if it landed at once (a catch-up result), else 0:
369
+ * what follows from it, like `tickVisibility`, lands with it. Reactive
370
+ * through `_version`, which every layout bumps.
371
+ */
372
+ get slotsLandedLayout(): number;
363
373
  /**
364
374
  * Where each grid line is headed: 1 for a displayed category, 0 otherwise,
365
375
  * row-aligned with `getTicks`. A grid morphs its lines' opacity toward this on
@@ -403,19 +413,22 @@ export declare class BandScaleState {
403
413
  /**
404
414
  * Where a tick's label sits now, in pixels along the axis: the centre of its
405
415
  * category's current slot, so it travels with the bar. Slot 0 is at the start
406
- * of the range on x and at the bottom on y, as in the shader and d3's
407
- * reversed range. Per frame, O(1).
416
+ * of the range on both axes: the left on x, the top on y, as in the shader
417
+ * and d3's range. Per frame, O(1).
408
418
  */
409
- tickPosition(key: number, rangeSize: number, axis: 'x' | 'y'): number;
419
+ tickPosition(key: number, rangeSize: number): number;
410
420
  /**
411
421
  * The same, for a caller that already has the slot.
412
422
  *
413
423
  * A consumer that has to test the slot first — an SVG label, which must not
414
424
  * draw a category the axis is not displaying — would otherwise ask for it
415
- * twice per item per frame. The axis flip lives here rather than being
416
- * re-derived by such a caller.
425
+ * twice per item per frame.
426
+ *
427
+ * The same on both axes. A y band axis reads top-down, like a list: slot 0
428
+ * at the top, so a ranking and an explicit domain need no reversing, and
429
+ * the exit one past the last slot is the end of the list on either axis.
417
430
  */
418
- slotPosition(slot: number, rangeSize: number, axis: 'x' | 'y'): number;
431
+ slotPosition(slot: number, rangeSize: number): number;
419
432
  /**
420
433
  * Centre of a (possibly fractional) slot, measured from the start of the
421
434
  * range — the same layout the shader's computeBandLayout uses, so a label
@@ -452,7 +465,7 @@ export declare class BandScaleState {
452
465
  * Returns the 0-based index of the band under the cursor, or null if
453
466
  * no bands exist.
454
467
  */
455
- invertScreenToData(screenVal: number, rangeSize: number, isYAxis?: boolean): number | null;
468
+ invertScreenToData(screenVal: number, rangeSize: number, _isYAxis?: boolean): number | null;
456
469
  /**
457
470
  * Get all mark extents (for debugging). Ids when the scale has identity.
458
471
  */
@@ -327,6 +327,10 @@ export class BandScaleState {
327
327
  setOtherId(id) {
328
328
  this.#slots.otherId = id;
329
329
  }
330
+ /** This axis's Other bucket id, while a mark on it has one; null otherwise. */
331
+ get otherId() {
332
+ return this.#slots.otherId;
333
+ }
330
334
  /**
331
335
  * This axis's Other bucket, shared by every mark on it that has one.
332
336
  *
@@ -447,12 +451,12 @@ export class BandScaleState {
447
451
  * all-NULL column that renders nothing — because an expression-backed axis
448
452
  * gets a dim like any other since `c55e719`.
449
453
  */
450
- #rebuildSlotTable() {
454
+ #rebuildSlotTable(instant = false) {
451
455
  if (this.hasIdentity) {
452
- this.#slots.layout('ids', this.#displayOrder, this.#resolver.idCount);
456
+ this.#slots.layout('ids', this.#displayOrder, this.#resolver.idCount, instant);
453
457
  }
454
458
  else {
455
- this.#slots.layout('positions', null, this.#domain.length);
459
+ this.#slots.layout('positions', null, this.#domain.length, instant);
456
460
  }
457
461
  }
458
462
  // =========================================================================
@@ -548,7 +552,7 @@ export class BandScaleState {
548
552
  * reports what CHANGED rather than notifying anyone: nothing below runs for
549
553
  * an answer identical to the one already on screen.
550
554
  */
551
- updateDomainFromExtents() {
555
+ updateDomainFromExtents(options = {}) {
552
556
  if (!this.#enableUpdates) {
553
557
  return;
554
558
  }
@@ -562,7 +566,9 @@ export class BandScaleState {
562
566
  return;
563
567
  this.#domain = values;
564
568
  this.#displayOrder = ids;
565
- this.#rebuildSlotTable();
569
+ // Instant: the categories take their new slots at once (a catch-up
570
+ // result, see SlotTransition.layout).
571
+ this.#rebuildSlotTable(options.instant);
566
572
  this._version++;
567
573
  // Notify GPU to re-render charts using this scale
568
574
  this.#registry?.invalidateChartsUsingScale(this);
@@ -621,8 +627,10 @@ export class BandScaleState {
621
627
  /**
622
628
  * Get D3 band scale configured for rendering
623
629
  */
624
- getBaseScale(axis, innerWidthOrHeight) {
625
- this.#sharedScale.range(axis === 'x' ? [0, innerWidthOrHeight] : [innerWidthOrHeight, 0]);
630
+ getBaseScale(_axis, innerWidthOrHeight) {
631
+ // The same on both axes, not reversed on y: slot 0 is at the top, as in
632
+ // the shader (slotPosition).
633
+ this.#sharedScale.range([0, innerWidthOrHeight]);
626
634
  if (this.#paddingInner !== null && this.#paddingOuter !== null) {
627
635
  this.#sharedScale.paddingInner(this.#paddingInner).paddingOuter(this.#paddingOuter);
628
636
  }
@@ -678,6 +686,15 @@ export class BandScaleState {
678
686
  ticks[i] = i;
679
687
  return ticks;
680
688
  }
689
+ /**
690
+ * The slots' latest layout if it landed at once (a catch-up result), else 0:
691
+ * what follows from it, like `tickVisibility`, lands with it. Reactive
692
+ * through `_version`, which every layout bumps.
693
+ */
694
+ get slotsLandedLayout() {
695
+ void this._version;
696
+ return this.#slots.landedLayout;
697
+ }
681
698
  /**
682
699
  * Where each grid line is headed: 1 for a displayed category, 0 otherwise,
683
700
  * row-aligned with `getTicks`. A grid morphs its lines' opacity toward this on
@@ -759,23 +776,25 @@ export class BandScaleState {
759
776
  /**
760
777
  * Where a tick's label sits now, in pixels along the axis: the centre of its
761
778
  * category's current slot, so it travels with the bar. Slot 0 is at the start
762
- * of the range on x and at the bottom on y, as in the shader and d3's
763
- * reversed range. Per frame, O(1).
779
+ * of the range on both axes: the left on x, the top on y, as in the shader
780
+ * and d3's range. Per frame, O(1).
764
781
  */
765
- tickPosition(key, rangeSize, axis) {
766
- return this.slotPosition(this.tickSlot(key), rangeSize, axis);
782
+ tickPosition(key, rangeSize) {
783
+ return this.slotPosition(this.tickSlot(key), rangeSize);
767
784
  }
768
785
  /**
769
786
  * The same, for a caller that already has the slot.
770
787
  *
771
788
  * A consumer that has to test the slot first — an SVG label, which must not
772
789
  * draw a category the axis is not displaying — would otherwise ask for it
773
- * twice per item per frame. The axis flip lives here rather than being
774
- * re-derived by such a caller.
790
+ * twice per item per frame.
791
+ *
792
+ * The same on both axes. A y band axis reads top-down, like a list: slot 0
793
+ * at the top, so a ranking and an explicit domain need no reversing, and
794
+ * the exit one past the last slot is the end of the list on either axis.
775
795
  */
776
- slotPosition(slot, rangeSize, axis) {
777
- const centre = this.slotCenter(slot, rangeSize);
778
- return axis === 'y' ? rangeSize - centre : centre;
796
+ slotPosition(slot, rangeSize) {
797
+ return this.slotCenter(slot, rangeSize);
779
798
  }
780
799
  /**
781
800
  * Centre of a (possibly fractional) slot, measured from the start of the
@@ -854,7 +873,9 @@ export class BandScaleState {
854
873
  * Returns the 0-based index of the band under the cursor, or null if
855
874
  * no bands exist.
856
875
  */
857
- invertScreenToData(screenVal, rangeSize, isYAxis = false) {
876
+ invertScreenToData(screenVal, rangeSize,
877
+ // Band slots run from the start of the range on both axes (slotPosition).
878
+ _isYAxis = false) {
858
879
  const config = this.getConfig();
859
880
  const n = config.categoryCount;
860
881
  if (n === 0)
@@ -863,10 +884,7 @@ export class BandScaleState {
863
884
  if (stepSize === 0)
864
885
  return null;
865
886
  const offset = config.paddingOuter * stepSize;
866
- // GPU band scale renders Y categories bottom-to-top (matching D3),
867
- // so flip screen coordinate for Y axis inversion.
868
- const pos = isYAxis ? rangeSize - screenVal : screenVal;
869
- const bandIndex = Math.floor((pos - offset) / stepSize);
887
+ const bandIndex = Math.floor((screenVal - offset) / stepSize);
870
888
  // Clamped to a WHOLE slot. Mid-transition the count is fractional, so a
871
889
  // bare `n - 1` hands back a fraction for a pointer past the last band —
872
890
  // and a fractional slot indexes nothing, so idAtSlot and the domain both
@@ -1,3 +1,4 @@
1
+ import type { EntranceGate } from '../animation/EntranceGate';
1
2
  import type { RegisteredScaleRequirement, ResolvedScaleConfiguration } from './ScalePriorities';
2
3
  import type { ScaleRegistry } from './ScaleRegistry.svelte';
3
4
  import type { GPUCoordinator } from '../gpu/GPUCoordinator.svelte';
@@ -32,6 +33,8 @@ interface ChartScalesConfig {
32
33
  yDisableZoom?: boolean;
33
34
  useXSelection?: boolean;
34
35
  useYSelection?: boolean;
36
+ /** The chart's entrance gate: its scales snap, and show no ticks, until it opens. */
37
+ entranceGate?: EntranceGate | null;
35
38
  }
36
39
  export declare class ChartScales {
37
40
  #private;
@@ -55,6 +55,7 @@ export class ChartScales {
55
55
  this.gpuCoordinator = config.gpuCoordinator;
56
56
  this.#scaleRegistry = config.scaleRegistry;
57
57
  this.#getFacetChartIds = config.getFacetChartIds ?? null;
58
+ this.#entranceGateOff = config.entranceGate?.onOpen(() => this.#markScalesEntered()) ?? null;
58
59
  if (!this.#scaleRegistry) {
59
60
  console.error('ChartScales initialized without scaleRegistry!');
60
61
  }
@@ -271,14 +272,10 @@ export class ChartScales {
271
272
  xScaleConfig.__fitDomain = this.chartId;
272
273
  const neededXScale = this.#scaleRegistry.getOrCreateScale(xKey, xConfig.type, xScaleConfig, this.chartId);
273
274
  if (this.#xScaleInstance !== neededXScale) {
274
- const wasReplacement = !!this.#xScaleInstance;
275
275
  if (this.#xScaleInstance) {
276
276
  this.#scaleRegistry.deregisterChart(this.#xScaleInstance, this.chartId);
277
277
  }
278
278
  this.#xScaleInstance = neededXScale;
279
- if (wasReplacement && neededXScale instanceof LinearScaleState) {
280
- neededXScale.zoomInEffect.enabled = false;
281
- }
282
279
  }
283
280
  }
284
281
  if (yConfig) {
@@ -290,16 +287,13 @@ export class ChartScales {
290
287
  yScaleConfig.__fitDomain = this.chartId;
291
288
  const neededYScale = this.#scaleRegistry.getOrCreateScale(yKey, yConfig.type, yScaleConfig, this.chartId);
292
289
  if (this.#yScaleInstance !== neededYScale) {
293
- const wasReplacement = !!this.#yScaleInstance;
294
290
  if (this.#yScaleInstance) {
295
291
  this.#scaleRegistry.deregisterChart(this.#yScaleInstance, this.chartId);
296
292
  }
297
293
  this.#yScaleInstance = neededYScale;
298
- if (wasReplacement && neededYScale instanceof LinearScaleState) {
299
- neededYScale.zoomInEffect.enabled = false;
300
- }
301
294
  }
302
295
  }
296
+ this.#markScalesEntered();
303
297
  }
304
298
  scaleConfigurations = $derived.by(() => {
305
299
  try {
@@ -438,6 +432,7 @@ export class ChartScales {
438
432
  this.#scaleRegistry.deregisterChart(scale, this.chartId);
439
433
  }
440
434
  this.#parallelScaleInstances = next;
435
+ this.#markScalesEntered();
441
436
  this.#parallelAxisOrder = wanted.map((_, i) => i);
442
437
  if (next.size > 0)
443
438
  this.#ensureReorder().resetSprings();
@@ -764,7 +759,30 @@ export class ChartScales {
764
759
  setBinStepMultiplier: (v) => coordinator.setBinStepMultiplier(v)
765
760
  };
766
761
  }
762
+ /**
763
+ * A scale snaps every extent change until a chart drawing it has its marks'
764
+ * first data in, so the domain is that chart's whole extent when it enters,
765
+ * and its axes show no ticks before then (the domain is a placeholder).
766
+ * Marked here when the chart's entrance gate opens, and again whenever the
767
+ * scale instances change after that.
768
+ */
769
+ #entranceGateOff = null;
770
+ #markScalesEntered() {
771
+ const gate = this.config.entranceGate;
772
+ if (gate && !gate.opened)
773
+ return;
774
+ for (const scale of [
775
+ this.#xScaleInstance,
776
+ this.#yScaleInstance,
777
+ ...this.#parallelScaleInstances.values()
778
+ ]) {
779
+ if (scale instanceof LinearScaleState && !scale.entered)
780
+ scale.entered = true;
781
+ }
782
+ }
767
783
  destroy() {
784
+ this.#entranceGateOff?.();
785
+ this.#entranceGateOff = null;
768
786
  this.#reorder?.destroy();
769
787
  this.#reorder = null;
770
788
  if (this.#scaleRegistry) {
@@ -1,6 +1,6 @@
1
1
  import type { RGBA, D3ColorScale } from './color-utils';
2
2
  import type { FieldIdentity } from '../query-engine/categorical-identity';
3
- import type { ColorScaleType } from '../types';
3
+ import type { ColorDomainValue, ColorScaleType } from '../types';
4
4
  import type { ScaleRegistry } from './ScaleRegistry.svelte';
5
5
  import { type GPUCoordinator } from '../gpu/GPUCoordinator.svelte';
6
6
  import type { SlotSource } from '../gpu/identity-slot-packing';
@@ -13,6 +13,10 @@ interface ColorScaleConfig {
13
13
  midpoint?: number;
14
14
  constant?: number;
15
15
  exponent?: number;
16
+ /** Explicit palette colours, in place of the scheme's. */
17
+ range?: readonly string[] | null;
18
+ /** Values pinned to palette positions: pinnedDomain[i] draws range[i]. */
19
+ pinnedDomain?: readonly ColorDomainValue[] | null;
16
20
  }
17
21
  /**
18
22
  * Reactive state for a GPU-backed color scale.
@@ -118,9 +122,9 @@ export declare class ColorScaleState {
118
122
  * the new one and simply mean other categories.
119
123
  */
120
124
  setIdentity(identity: FieldIdentity): void;
121
- /** True once this scale's channel carries dim ids rather than palette indices. */
122
125
  /** The field's live id <-> value bijection, once adopted. */
123
126
  get identity(): FieldIdentity | null;
127
+ /** True once this scale's channel carries dim ids rather than palette indices. */
124
128
  get hasIdentity(): boolean;
125
129
  /**
126
130
  * Source value for a channel id — the decode side of the encode the query
@@ -134,6 +138,18 @@ export declare class ColorScaleState {
134
138
  * Update the scheme. Triggers GPU re-upload.
135
139
  */
136
140
  setScheme(newScheme: string): void;
141
+ /**
142
+ * Set explicit palette colours and pinned values, or clear them with nulls.
143
+ * Triggers GPU re-upload.
144
+ *
145
+ * The ranks are cleared: they were handed out under the old mapping, and a
146
+ * new pin means the author has just said which value takes which colour.
147
+ */
148
+ setRange(range: readonly string[] | null, pinnedDomain: readonly ColorDomainValue[] | null): void;
149
+ /** The explicit palette colours, or null for the scheme's. */
150
+ get range(): readonly string[] | null;
151
+ /** The pinned values, or null when ranks follow the domain's sorted order. */
152
+ get pinnedDomain(): readonly ColorDomainValue[] | null;
137
153
  /**
138
154
  * Update the normalization transform. Triggers GPU re-upload.
139
155
  * Called when the user changes the color scale type (e.g., linear → symlog)