@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.
- package/README.md +3 -1
- package/dist/animation/EntranceGate.d.ts +52 -0
- package/dist/animation/EntranceGate.js +93 -0
- package/dist/animation/TransitionClock.d.ts +61 -30
- package/dist/animation/TransitionClock.js +67 -29
- package/dist/animation/transition-log.d.ts +41 -0
- package/dist/animation/transition-log.js +57 -0
- package/dist/components/axes/AxisBottom.svelte +27 -8
- package/dist/components/axes/AxisBottom.svelte.d.ts +10 -0
- package/dist/components/axes/AxisCenter.svelte +4 -2
- package/dist/components/axes/AxisLeft.svelte +81 -10
- package/dist/components/axes/AxisLeft.svelte.d.ts +17 -0
- package/dist/components/axes/BandAxisTicks.svelte +4 -3
- package/dist/components/axes/axis-title.d.ts +30 -0
- package/dist/components/axes/axis-title.js +77 -0
- package/dist/components/charts/ChartCell.svelte +33 -6
- package/dist/components/charts/ChartCell.svelte.d.ts +9 -0
- package/dist/components/charts/ChartContainer.svelte +55 -5
- package/dist/components/charts/ChartContainer.svelte.d.ts +14 -1
- package/dist/components/charts/ChartHover.svelte.d.ts +14 -0
- package/dist/components/charts/ChartHover.svelte.js +22 -0
- package/dist/components/charts/ChartVisibility.svelte.d.ts +43 -0
- package/dist/components/charts/ChartVisibility.svelte.js +67 -0
- package/dist/components/guides/Grid.svelte +12 -1
- package/dist/components/guides/legend/ContinuousColorLegend.svelte +7 -1
- package/dist/components/guides/legend/OrdinalColorLegend.svelte +8 -5
- package/dist/components/marks/AreaMarkX.svelte +18 -4
- package/dist/components/marks/AreaMarkY.svelte +18 -4
- package/dist/components/marks/BarMarkX.svelte +20 -4
- package/dist/components/marks/BarMarkY.svelte +20 -4
- package/dist/components/marks/DotsMark.svelte +8 -3
- package/dist/components/marks/LineMark.svelte +13 -6
- package/dist/components/marks/ParallelCoordinatesMark.svelte +11 -2
- package/dist/components/marks/ParallelCoordinatesMark.svelte.d.ts +4 -0
- package/dist/components/marks/RectMark.svelte +8 -3
- package/dist/components/marks/RectXMark.svelte +9 -3
- package/dist/components/marks/RectYMark.svelte +9 -3
- package/dist/components/marks/RuleMarkX.svelte +31 -25
- package/dist/components/marks/RuleMarkY.svelte +31 -25
- package/dist/components/marks/TextMark.svelte +52 -14
- package/dist/components/marks/lifecycle/MarkLifecycleV2.svelte.d.ts +22 -1
- package/dist/components/marks/lifecycle/MarkLifecycleV2.svelte.js +579 -66
- package/dist/components/marks/lifecycle/domain-queries.d.ts +13 -2
- package/dist/components/marks/lifecycle/domain-queries.js +43 -11
- package/dist/components/marks/lifecycle/domain-registration.d.ts +6 -1
- package/dist/components/marks/lifecycle/domain-registration.js +17 -20
- package/dist/components/marks/lifecycle/other-rows.svelte.d.ts +112 -0
- package/dist/components/marks/lifecycle/other-rows.svelte.js +193 -0
- package/dist/components/marks/lifecycle/other-split.d.ts +80 -0
- package/dist/components/marks/lifecycle/other-split.js +112 -0
- package/dist/components/marks/lifecycle/query-builder.d.ts +53 -3
- package/dist/components/marks/lifecycle/query-builder.js +46 -23
- package/dist/components/marks/utils/MarkColor.svelte.d.ts +3 -1
- package/dist/components/marks/utils/MarkColor.svelte.js +33 -3
- package/dist/components/marks/utils/MarkSeries.svelte.d.ts +14 -1
- package/dist/components/marks/utils/MarkSeries.svelte.js +25 -1
- package/dist/components/marks/utils/MarkSize.svelte.js +4 -0
- package/dist/components/marks/utils/reactive-config.d.ts +1 -1
- package/dist/components/marks/utils/reactive-config.js +9 -0
- package/dist/components/marks/utils/tooltip-helpers.d.ts +4 -1
- package/dist/components/marks/utils/tooltip-helpers.js +2 -1
- package/dist/components/rendering/Areas.svelte +57 -19
- package/dist/components/rendering/Areas.svelte.d.ts +12 -0
- package/dist/components/rendering/BaseGPURenderer.svelte.d.ts +3 -1
- package/dist/components/rendering/BaseGPURenderer.svelte.js +4 -2
- package/dist/components/rendering/Dots.svelte +23 -3
- package/dist/components/rendering/Dots.svelte.d.ts +6 -0
- package/dist/components/rendering/GridLine.svelte +13 -1
- package/dist/components/rendering/GridLine.svelte.d.ts +5 -0
- package/dist/components/rendering/Lines.svelte +48 -13
- package/dist/components/rendering/Lines.svelte.d.ts +12 -0
- package/dist/components/rendering/ParallelLines.svelte +5 -5
- package/dist/components/rendering/Rects.svelte +164 -30
- package/dist/components/rendering/Rects.svelte.d.ts +23 -2
- package/dist/components/rendering/RuleX.svelte +1 -1
- package/dist/components/rendering/RuleY.svelte +1 -1
- package/dist/components/rendering/WebGPUCanvas.svelte +4 -2
- package/dist/components/rendering/activity-reads.d.ts +47 -12
- package/dist/components/rendering/activity-reads.js +61 -19
- package/dist/components/rendering/uniform-sizes.d.ts +1 -1
- package/dist/components/rendering/uniform-sizes.js +1 -1
- package/dist/config/ChartConfig.svelte.d.ts +10 -1
- package/dist/config/ChartConfig.svelte.js +17 -0
- package/dist/gpu/GPUCoordinator.svelte.d.ts +31 -1
- package/dist/gpu/GPUCoordinator.svelte.js +164 -7
- package/dist/gpu/MorphedChannels.d.ts +11 -28
- package/dist/gpu/MorphedChannels.js +72 -8
- package/dist/gpu/ScaleTemplateSystem.d.ts +20 -2
- package/dist/gpu/ScaleTemplateSystem.js +83 -6
- package/dist/index.d.ts +19 -4
- package/dist/index.js +22 -4
- package/dist/interactors/Interval1D.svelte +6 -1
- package/dist/interactors/Interval2D.svelte +6 -1
- package/dist/interactors/ParallelAxisSource.svelte +10 -1
- package/dist/interactors/Toggle.svelte +32 -3
- package/dist/interactors/Tooltip.svelte +138 -41
- package/dist/interactors/TooltipManager.svelte.d.ts +20 -5
- package/dist/interactors/TooltipManager.svelte.js +46 -12
- package/dist/interactors/other-tooltip.d.ts +16 -0
- package/dist/interactors/other-tooltip.js +22 -0
- package/dist/query-engine/CrossfilterSelection.svelte.d.ts +4 -2
- package/dist/query-engine/CrossfilterSelection.svelte.js +2 -0
- package/dist/query-engine/FieldActivity.svelte.d.ts +49 -1
- package/dist/query-engine/FieldActivity.svelte.js +175 -5
- package/dist/query-engine/FilterGroup.svelte.d.ts +23 -1
- package/dist/query-engine/FilterGroup.svelte.js +43 -3
- package/dist/query-engine/FilterGroupRegistry.svelte.d.ts +31 -3
- package/dist/query-engine/FilterGroupRegistry.svelte.js +62 -15
- package/dist/query-engine/MarkHandle.svelte.d.ts +28 -1
- package/dist/query-engine/MarkHandle.svelte.js +33 -1
- package/dist/query-engine/QueryEngine.d.ts +225 -5
- package/dist/query-engine/QueryEngine.js +697 -47
- package/dist/query-engine/categorical-identity.d.ts +41 -0
- package/dist/query-engine/categorical-identity.js +145 -65
- package/dist/query-engine/debug/query-log.d.ts +39 -0
- package/dist/query-engine/debug/query-log.js +90 -0
- package/dist/query-engine/duckdb.d.ts +55 -8
- package/dist/query-engine/duckdb.js +82 -30
- package/dist/query-engine/index.d.ts +7 -4
- package/dist/query-engine/index.js +3 -3
- package/dist/query-engine/preagg/BuildQueue.d.ts +33 -13
- package/dist/query-engine/preagg/BuildQueue.js +51 -15
- package/dist/query-engine/preagg/Preagg.d.ts +46 -3
- package/dist/query-engine/preagg/Preagg.js +159 -31
- package/dist/query-engine/preagg/plan.d.ts +33 -1
- package/dist/query-engine/preagg/plan.js +158 -14
- package/dist/query-engine/sliced-lane.d.ts +108 -0
- package/dist/query-engine/sliced-lane.js +299 -0
- package/dist/query-engine/source-routing.d.ts +106 -0
- package/dist/query-engine/source-routing.js +189 -0
- package/dist/query-engine/toggle-predicate.d.ts +21 -0
- package/dist/query-engine/toggle-predicate.js +29 -1
- package/dist/scales/ActivityTable.d.ts +5 -1
- package/dist/scales/ActivityTable.js +12 -3
- package/dist/scales/BandScaleState.svelte.d.ts +22 -9
- package/dist/scales/BandScaleState.svelte.js +39 -21
- package/dist/scales/ChartScales.svelte.d.ts +3 -0
- package/dist/scales/ChartScales.svelte.js +26 -8
- package/dist/scales/ColorScaleState.svelte.d.ts +18 -2
- package/dist/scales/ColorScaleState.svelte.js +138 -26
- package/dist/scales/LinearScaleState.svelte.d.ts +7 -10
- package/dist/scales/LinearScaleState.svelte.js +21 -39
- package/dist/scales/ScaleGeneration.svelte.d.ts +48 -0
- package/dist/scales/ScaleGeneration.svelte.js +42 -0
- package/dist/scales/ScaleRegistry.svelte.js +3 -1
- package/dist/scales/SlotTransition.d.ts +15 -1
- package/dist/scales/SlotTransition.js +33 -4
- package/dist/scales/TimeScaleState.svelte.d.ts +7 -3
- package/dist/scales/TimeScaleState.svelte.js +7 -7
- package/dist/scales/color-utils.d.ts +0 -3
- package/dist/scales/color-utils.js +0 -3
- package/dist/scales/entered.d.ts +8 -0
- package/dist/scales/entered.js +10 -0
- package/dist/spec/LazyQueryEngine.js +2 -19
- package/dist/spec/MarkRegistry.d.ts +3 -1
- package/dist/spec/MarkRegistry.js +13 -1
- package/dist/spec/PlotFromSpec.svelte +1 -0
- package/dist/spec/export.js +11 -2
- package/dist/spec/types.d.ts +43 -13
- package/dist/sql/bin.d.ts +25 -0
- package/dist/sql/bin.js +108 -5
- package/dist/theme.css +5 -4
- package/dist/transforms/TransformCompiler.d.ts +47 -2
- package/dist/transforms/TransformCompiler.js +70 -1
- package/dist/transforms/index.js +176 -42
- package/dist/types.d.ts +177 -2
- 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
|
|
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 <
|
|
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(
|
|
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(
|
|
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
|
|
407
|
-
*
|
|
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
|
|
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.
|
|
416
|
-
*
|
|
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
|
|
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,
|
|
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
|
-
|
|
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(
|
|
625
|
-
|
|
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
|
|
763
|
-
*
|
|
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
|
|
766
|
-
return this.slotPosition(this.tickSlot(key), rangeSize
|
|
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.
|
|
774
|
-
*
|
|
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
|
|
777
|
-
|
|
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,
|
|
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
|
-
|
|
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)
|