@fjader/svelte 0.2.4 → 0.2.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -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 +4 -2
- package/dist/components/axes/AxisCenter.svelte +4 -2
- package/dist/components/axes/AxisLeft.svelte +4 -2
- package/dist/components/axes/BandAxisTicks.svelte +3 -2
- package/dist/components/charts/ChartCell.svelte +32 -5
- package/dist/components/charts/ChartCell.svelte.d.ts +9 -0
- package/dist/components/charts/ChartContainer.svelte +45 -3
- package/dist/components/charts/ChartContainer.svelte.d.ts +7 -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/marks/AreaMarkX.svelte +16 -4
- package/dist/components/marks/AreaMarkY.svelte +16 -4
- package/dist/components/marks/BarMarkX.svelte +7 -3
- package/dist/components/marks/BarMarkY.svelte +7 -3
- package/dist/components/marks/DotsMark.svelte +6 -3
- package/dist/components/marks/LineMark.svelte +11 -8
- package/dist/components/marks/ParallelCoordinatesMark.svelte +7 -2
- package/dist/components/marks/ParallelCoordinatesMark.svelte.d.ts +2 -0
- package/dist/components/marks/RectMark.svelte +6 -3
- package/dist/components/marks/RectXMark.svelte +7 -3
- package/dist/components/marks/RectYMark.svelte +7 -3
- package/dist/components/marks/RuleMarkX.svelte +30 -26
- package/dist/components/marks/RuleMarkY.svelte +30 -26
- package/dist/components/marks/TextMark.svelte +44 -5
- package/dist/components/marks/lifecycle/MarkLifecycleV2.svelte.d.ts +16 -0
- package/dist/components/marks/lifecycle/MarkLifecycleV2.svelte.js +398 -54
- package/dist/components/marks/lifecycle/domain-queries.d.ts +11 -2
- package/dist/components/marks/lifecycle/domain-queries.js +33 -6
- package/dist/components/marks/lifecycle/domain-registration.d.ts +6 -1
- package/dist/components/marks/lifecycle/domain-registration.js +15 -14
- package/dist/components/marks/lifecycle/other-rows.svelte.d.ts +9 -1
- package/dist/components/marks/lifecycle/other-rows.svelte.js +2 -2
- package/dist/components/marks/lifecycle/query-builder.d.ts +27 -2
- package/dist/components/marks/lifecycle/query-builder.js +23 -20
- 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 +3 -0
- 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 +1 -1
- 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 +51 -11
- package/dist/components/rendering/Rects.svelte.d.ts +16 -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 +41 -24
- package/dist/components/rendering/activity-reads.js +54 -31
- 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 +7 -0
- 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 -2
- package/dist/gpu/MorphedChannels.js +33 -11
- package/dist/gpu/ScaleTemplateSystem.d.ts +20 -2
- package/dist/gpu/ScaleTemplateSystem.js +58 -2
- package/dist/index.d.ts +17 -4
- package/dist/index.js +19 -4
- package/dist/interactors/TooltipManager.svelte.js +14 -5
- 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 +2 -3
- package/dist/query-engine/FieldActivity.svelte.js +1 -2
- 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 +671 -41
- package/dist/query-engine/categorical-identity.d.ts +34 -8
- package/dist/query-engine/categorical-identity.js +138 -73
- 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 +28 -1
- package/dist/query-engine/preagg/plan.js +128 -8
- 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/scales/BandScaleState.svelte.d.ts +9 -1
- package/dist/scales/BandScaleState.svelte.js +16 -5
- 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 +41 -13
- package/dist/sql/bin.d.ts +25 -0
- package/dist/sql/bin.js +108 -5
- package/dist/transforms/TransformCompiler.d.ts +10 -0
- package/dist/transforms/TransformCompiler.js +26 -0
- package/dist/transforms/index.js +144 -24
- package/dist/types.d.ts +136 -2
- package/package.json +2 -2
|
@@ -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)
|
|
@@ -78,6 +78,17 @@ export class ColorScaleState {
|
|
|
78
78
|
* stop meaning anything and holding colours to them would be wrong.
|
|
79
79
|
*/
|
|
80
80
|
#paletteRank = new Map();
|
|
81
|
+
/**
|
|
82
|
+
* Explicit palette colours (`range` on the colour prop), or null for the
|
|
83
|
+
* scheme's. Ordinal only.
|
|
84
|
+
*/
|
|
85
|
+
#range = null;
|
|
86
|
+
/**
|
|
87
|
+
* Values pinned to palette positions (`domain` on the colour prop): value i
|
|
88
|
+
* takes rank i, in place of its position in the sorted domain. Values outside
|
|
89
|
+
* it draw the null colour. Null when ranks follow the domain, the default.
|
|
90
|
+
*/
|
|
91
|
+
#pinned = null;
|
|
81
92
|
#nextRank = 0;
|
|
82
93
|
/**
|
|
83
94
|
* id -> rank as a dense array, packed into the shared identity slot table so
|
|
@@ -179,6 +190,8 @@ export class ColorScaleState {
|
|
|
179
190
|
this.#midpoint = config.midpoint;
|
|
180
191
|
this.#constant = config.constant ?? 1;
|
|
181
192
|
this.#exponent = config.exponent ?? 1;
|
|
193
|
+
this.#range = config.range?.length ? [...config.range] : null;
|
|
194
|
+
this.#pinned = config.pinnedDomain?.length ? [...config.pinnedDomain] : null;
|
|
182
195
|
this.#registry = registry;
|
|
183
196
|
this.#gpuCoordinator = gpuCoordinator;
|
|
184
197
|
// Explicit type wins, fallback to inference from scheme
|
|
@@ -373,7 +386,9 @@ export class ColorScaleState {
|
|
|
373
386
|
this.#refreshRanks();
|
|
374
387
|
this.#d3Scale = this.#hasRanks()
|
|
375
388
|
? this.#buildRankedOrdinalScale()
|
|
376
|
-
:
|
|
389
|
+
: this.#range || this.#pinned
|
|
390
|
+
? this.#buildExplicitOrdinalScale()
|
|
391
|
+
: createD3Scale(this.#scheme, this.domain, { reverse: this.#reverse });
|
|
377
392
|
this.#uploadOrdinal();
|
|
378
393
|
}
|
|
379
394
|
else {
|
|
@@ -457,42 +472,58 @@ export class ColorScaleState {
|
|
|
457
472
|
if (hadIdentity !== this.hasIdentity && this.domain.length > 0)
|
|
458
473
|
this.#uploadToGPU();
|
|
459
474
|
}
|
|
460
|
-
/** True once this scale's channel carries dim ids rather than palette indices. */
|
|
461
475
|
/** The field's live id <-> value bijection, once adopted. */
|
|
462
476
|
get identity() {
|
|
463
477
|
return this.#identity ?? null;
|
|
464
478
|
}
|
|
479
|
+
/** True once this scale's channel carries dim ids rather than palette indices. */
|
|
465
480
|
get hasIdentity() {
|
|
466
481
|
return this.isOrdinal && (this.#identity?.present ?? false);
|
|
467
482
|
}
|
|
468
|
-
/**
|
|
469
|
-
* Hand a rank to every domain category that does not have one, then rebuild
|
|
470
|
-
* the dense table. Existing ranks are never touched, which is what makes a
|
|
471
|
-
* colour survive its category disappearing.
|
|
472
|
-
*/
|
|
473
483
|
/** The value -> position map, rebuilt only when the domain array changes. */
|
|
474
484
|
#categoryPositions() {
|
|
475
485
|
if (this.#categoryMap && this.#categoryMapFor === this.domain)
|
|
476
486
|
return this.#categoryMap;
|
|
477
487
|
const map = new Map();
|
|
478
|
-
|
|
488
|
+
// Pinned values index the palette directly; anything else maps to -1.
|
|
489
|
+
const positions = this.#pinned ?? this.domain;
|
|
490
|
+
positions.forEach((cat, i) => map.set(cat, i));
|
|
479
491
|
this.#categoryMap = map;
|
|
480
492
|
this.#categoryMapFor = this.domain;
|
|
481
493
|
return map;
|
|
482
494
|
}
|
|
495
|
+
/**
|
|
496
|
+
* Hand a rank to every domain category that does not have one, then rebuild
|
|
497
|
+
* the dense table. Existing ranks are never touched, which is what makes a
|
|
498
|
+
* colour survive its category disappearing.
|
|
499
|
+
*/
|
|
483
500
|
#refreshRanks() {
|
|
484
501
|
const identity = this.#identity;
|
|
485
502
|
if (!identity?.present)
|
|
486
503
|
return;
|
|
487
504
|
this.#ranksVersion = identity.version;
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
//
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
505
|
+
const pinned = this.#pinned;
|
|
506
|
+
if (pinned) {
|
|
507
|
+
// Pinned: value i takes rank i. Values that have not appeared yet keep
|
|
508
|
+
// their rank reserved, the same way a filtered-out category does. A
|
|
509
|
+
// pinned null is the one way the NULL category takes a palette colour.
|
|
510
|
+
pinned.forEach((value, rank) => {
|
|
511
|
+
const id = ColorScaleState.#idOf(identity, value);
|
|
512
|
+
if (id !== undefined && !this.#paletteRank.has(id))
|
|
513
|
+
this.#paletteRank.set(id, rank);
|
|
514
|
+
});
|
|
515
|
+
this.#nextRank = Math.max(this.#nextRank, pinned.length);
|
|
516
|
+
}
|
|
517
|
+
else {
|
|
518
|
+
for (const value of this.domain) {
|
|
519
|
+
const id = identity.ids.get(value);
|
|
520
|
+
if (id === undefined || this.#paletteRank.has(id))
|
|
521
|
+
continue;
|
|
522
|
+
// The NULL category takes no palette slot (see below).
|
|
523
|
+
if (id === identity.nullId)
|
|
524
|
+
continue;
|
|
525
|
+
this.#paletteRank.set(id, this.#nextRank++);
|
|
526
|
+
}
|
|
496
527
|
}
|
|
497
528
|
const table = new Float32Array(identity.values.length);
|
|
498
529
|
table.fill(-1);
|
|
@@ -503,9 +534,18 @@ export class ColorScaleState {
|
|
|
503
534
|
// The NULL category and the Other buckets draw the null colour: no palette
|
|
504
535
|
// slot, so they read as missing data or "the rest" and shift no category's
|
|
505
536
|
// colour. Not -1, which the shader draws transparent: a category outside
|
|
506
|
-
// this scale's domain.
|
|
507
|
-
|
|
537
|
+
// this scale's domain. With a pinned domain, so does every category the
|
|
538
|
+
// pin leaves out: drawn, but plainly not one of the named ones.
|
|
539
|
+
if (pinned) {
|
|
540
|
+
for (const value of this.domain) {
|
|
541
|
+
const id = identity.ids.get(value);
|
|
542
|
+
if (id !== undefined && table[id] === -1)
|
|
543
|
+
table[id] = NULL_COLOR_RANK;
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
if (identity.nullId !== null && !this.#paletteRank.has(identity.nullId)) {
|
|
508
547
|
table[identity.nullId] = NULL_COLOR_RANK;
|
|
548
|
+
}
|
|
509
549
|
for (const id of identity.synthetic)
|
|
510
550
|
if (id < table.length)
|
|
511
551
|
table[id] = NULL_COLOR_RANK;
|
|
@@ -519,6 +559,39 @@ export class ColorScaleState {
|
|
|
519
559
|
#hasRanks() {
|
|
520
560
|
return this.hasIdentity && this.#paletteRank.size > 0;
|
|
521
561
|
}
|
|
562
|
+
/**
|
|
563
|
+
* The dim id of a pinned value. Dim values keep the driver's type (an
|
|
564
|
+
* INTEGER column's are numbers, a BIGINT's may be bigints), while a pinned
|
|
565
|
+
* domain is whatever the author wrote, or JSON produced: `0` against `0n`,
|
|
566
|
+
* `'1'` against `1`. An exact match wins; failing that, the first value with
|
|
567
|
+
* the same string form.
|
|
568
|
+
*/
|
|
569
|
+
static #idOf(identity, value) {
|
|
570
|
+
const exact = identity.ids.get(value);
|
|
571
|
+
if (exact !== undefined)
|
|
572
|
+
return exact;
|
|
573
|
+
if (value === null)
|
|
574
|
+
return identity.nullId ?? undefined;
|
|
575
|
+
const key = String(value);
|
|
576
|
+
const i = identity.values.findIndex((v) => v !== null && String(v) === key);
|
|
577
|
+
return i < 0 ? undefined : i;
|
|
578
|
+
}
|
|
579
|
+
/** The palette's colours: the explicit range, else the scheme's. */
|
|
580
|
+
#colors() {
|
|
581
|
+
const colors = this.#range ?? getOrdinalColors(this.#scheme) ?? [];
|
|
582
|
+
return this.#reverse ? [...colors].reverse() : colors;
|
|
583
|
+
}
|
|
584
|
+
/**
|
|
585
|
+
* The CPU scale for an explicit range or pinned domain before identity has
|
|
586
|
+
* landed, indexed the way #categoryPositions indexes the GPU's palette.
|
|
587
|
+
*/
|
|
588
|
+
#buildExplicitOrdinalScale() {
|
|
589
|
+
const colors = this.#colors();
|
|
590
|
+
const positions = this.#pinned ?? this.domain;
|
|
591
|
+
return scaleOrdinal()
|
|
592
|
+
.domain(positions.map(String))
|
|
593
|
+
.range(positions.map((_, i) => colors[i % colors.length]));
|
|
594
|
+
}
|
|
522
595
|
/**
|
|
523
596
|
* An ordinal scale whose range is permuted to match the palette ranks, so
|
|
524
597
|
* `getColor(value)` returns the colour the GPU actually draws.
|
|
@@ -529,9 +602,7 @@ export class ColorScaleState {
|
|
|
529
602
|
* happening to be domain order.
|
|
530
603
|
*/
|
|
531
604
|
#buildRankedOrdinalScale() {
|
|
532
|
-
|
|
533
|
-
if (this.#reverse)
|
|
534
|
-
colors = [...colors].reverse();
|
|
605
|
+
const colors = this.#colors();
|
|
535
606
|
const identity = this.#identity;
|
|
536
607
|
const range = this.domain.map((value) => {
|
|
537
608
|
const id = identity.ids.get(value);
|
|
@@ -558,7 +629,7 @@ export class ColorScaleState {
|
|
|
558
629
|
/** Called from #uploadToGPU only, which has refreshed the ranks. */
|
|
559
630
|
#uploadOrdinal() {
|
|
560
631
|
// Bake and upload palette
|
|
561
|
-
const paletteData = bakePalette(this.#scheme, { reverse: this.#reverse });
|
|
632
|
+
const paletteData = bakePalette(this.#range ?? this.#scheme, { reverse: this.#reverse });
|
|
562
633
|
this.#gpuCoordinator.writeColorPalette(this.#paletteSlot, paletteData);
|
|
563
634
|
// Build and upload config struct
|
|
564
635
|
// paletteSize is the modulus the shader wraps a rank with, so it has to
|
|
@@ -570,7 +641,9 @@ export class ColorScaleState {
|
|
|
570
641
|
// Capped at 32, the palette's size, so a chart with more categories than
|
|
571
642
|
// that wraps as it always has. Floored at 1: this is a u32 modulus and 0
|
|
572
643
|
// is undefined behaviour in WGSL.
|
|
573
|
-
const rankSpace = this.hasIdentity
|
|
644
|
+
const rankSpace = this.hasIdentity
|
|
645
|
+
? this.#nextRank
|
|
646
|
+
: (this.#pinned?.length ?? this.domain.length);
|
|
574
647
|
const config = this.#buildColorConfig({
|
|
575
648
|
type: 0, // ORDINAL
|
|
576
649
|
paletteOffset: this.#paletteSlot * 32,
|
|
@@ -668,6 +741,40 @@ export class ColorScaleState {
|
|
|
668
741
|
this.#registry.invalidateChartsUsingScale(this);
|
|
669
742
|
}
|
|
670
743
|
}
|
|
744
|
+
/**
|
|
745
|
+
* Set explicit palette colours and pinned values, or clear them with nulls.
|
|
746
|
+
* Triggers GPU re-upload.
|
|
747
|
+
*
|
|
748
|
+
* The ranks are cleared: they were handed out under the old mapping, and a
|
|
749
|
+
* new pin means the author has just said which value takes which colour.
|
|
750
|
+
*/
|
|
751
|
+
setRange(range, pinnedDomain) {
|
|
752
|
+
const nextRange = range?.length ? [...range] : null;
|
|
753
|
+
const nextPinned = pinnedDomain?.length ? [...pinnedDomain] : null;
|
|
754
|
+
if (sameList(nextRange, this.#range) && sameList(nextPinned, this.#pinned))
|
|
755
|
+
return;
|
|
756
|
+
this.#range = nextRange;
|
|
757
|
+
this.#pinned = nextPinned;
|
|
758
|
+
this.#paletteRank.clear();
|
|
759
|
+
this.#nextRank = 0;
|
|
760
|
+
this.#categoryMap = null;
|
|
761
|
+
this._version++;
|
|
762
|
+
// CPU state, rebuilt here rather than left to the upload, which needs a
|
|
763
|
+
// coordinator: a legend reads the ranks too.
|
|
764
|
+
this.#refreshRanks();
|
|
765
|
+
if (this.domain.length > 0) {
|
|
766
|
+
this.#uploadToGPU();
|
|
767
|
+
this.#registry.invalidateChartsUsingScale(this);
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
/** The explicit palette colours, or null for the scheme's. */
|
|
771
|
+
get range() {
|
|
772
|
+
return this.#range;
|
|
773
|
+
}
|
|
774
|
+
/** The pinned values, or null when ranks follow the domain's sorted order. */
|
|
775
|
+
get pinnedDomain() {
|
|
776
|
+
return this.#pinned;
|
|
777
|
+
}
|
|
671
778
|
/**
|
|
672
779
|
* Update the normalization transform. Triggers GPU re-upload.
|
|
673
780
|
* Called when the user changes the color scale type (e.g., linear → symlog)
|
|
@@ -759,9 +866,7 @@ export class ColorScaleState {
|
|
|
759
866
|
const rank = id === undefined ? undefined : this.#paletteRank.get(id);
|
|
760
867
|
if (rank === undefined)
|
|
761
868
|
return ColorScaleState.NULL_COLOR;
|
|
762
|
-
|
|
763
|
-
if (this.#reverse)
|
|
764
|
-
colors = [...colors].reverse();
|
|
869
|
+
const colors = this.#colors();
|
|
765
870
|
// Same wrap the shader applies, so an over-32 palette agrees too.
|
|
766
871
|
return colors.length ? colors[rank % colors.length] : ColorScaleState.NULL_COLOR;
|
|
767
872
|
}
|
|
@@ -819,3 +924,10 @@ export class ColorScaleState {
|
|
|
819
924
|
`now belong to another scale. Register the holder's chart id.`);
|
|
820
925
|
}
|
|
821
926
|
}
|
|
927
|
+
function sameList(a, b) {
|
|
928
|
+
if (a === b)
|
|
929
|
+
return true;
|
|
930
|
+
if (!a || !b || a.length !== b.length)
|
|
931
|
+
return false;
|
|
932
|
+
return a.every((v, i) => v === b[i]);
|
|
933
|
+
}
|
|
@@ -6,11 +6,6 @@ export interface LinearScaleOptions {
|
|
|
6
6
|
chartsUsing(scale: unknown): string[];
|
|
7
7
|
} | null;
|
|
8
8
|
disableZoom?: boolean;
|
|
9
|
-
zoomInEffect?: boolean;
|
|
10
|
-
zoomInFactor?: number;
|
|
11
|
-
zoomInSpringConfig?: {
|
|
12
|
-
stiffness?: number;
|
|
13
|
-
};
|
|
14
9
|
}
|
|
15
10
|
export interface GpuScaleState {
|
|
16
11
|
domainMin: number;
|
|
@@ -68,11 +63,6 @@ export declare class LinearScaleState {
|
|
|
68
63
|
min: number;
|
|
69
64
|
};
|
|
70
65
|
disableZoom: boolean;
|
|
71
|
-
zoomInEffect: {
|
|
72
|
-
enabled: boolean;
|
|
73
|
-
factor: number;
|
|
74
|
-
stiffness: number;
|
|
75
|
-
};
|
|
76
66
|
/**
|
|
77
67
|
* Charts to redraw while this scale animates, asked of the registry rather
|
|
78
68
|
* than written into this object.
|
|
@@ -157,6 +147,13 @@ export declare class LinearScaleState {
|
|
|
157
147
|
* @param visualCenter - Cursor position as a visual fraction [0,1] (0=left/bottom, 1=right/top)
|
|
158
148
|
*/
|
|
159
149
|
accumulateZoom(visualCenter: number, scaleFactor: number): void;
|
|
150
|
+
/**
|
|
151
|
+
* True once a chart drawing this scale has its marks' first data in (its
|
|
152
|
+
* entrance gate opened). Until then every extent change snaps, and the
|
|
153
|
+
* axes draw no ticks: the domain is a placeholder, [0, 100], and showing
|
|
154
|
+
* it, or springing out of it, is the zoom-in on load by another route.
|
|
155
|
+
*/
|
|
156
|
+
entered: boolean;
|
|
160
157
|
/**
|
|
161
158
|
* Reset zoom to show full extent.
|
|
162
159
|
*/
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// =============================================================================
|
|
2
2
|
// LinearScaleState.svelte.ts - Refactored with SpringPair physics
|
|
3
3
|
// =============================================================================
|
|
4
|
+
import { noteTransition, transitionsObserved } from '../animation/transition-log';
|
|
4
5
|
import { scaleLinear } from 'd3-scale';
|
|
5
6
|
import { extent as d3Extent } from 'd3-array';
|
|
6
7
|
import { SpringPair, SPRING_PRESETS } from '../animation';
|
|
@@ -12,7 +13,6 @@ const SCALE_SPRING_PRESETS = {
|
|
|
12
13
|
pan: SPRING_PRESETS.pan.stiffness, // 600 - responsive dragging
|
|
13
14
|
zoom: SPRING_PRESETS.zoom.stiffness, // 400 - smooth zoom
|
|
14
15
|
reset: SPRING_PRESETS.reset.stiffness, // 150 - gentle reset animation
|
|
15
|
-
zoomIn: 200, // Initial zoom-in effect
|
|
16
16
|
externalDomain: 600 // Brush-driven domain tracking (snappy, like pan)
|
|
17
17
|
};
|
|
18
18
|
// =============================================================================
|
|
@@ -112,12 +112,6 @@ export class LinearScaleState {
|
|
|
112
112
|
center: 0,
|
|
113
113
|
hasUpdates: false
|
|
114
114
|
};
|
|
115
|
-
// --- Zoom-In Effect Config ---
|
|
116
|
-
zoomInEffect = {
|
|
117
|
-
enabled: true,
|
|
118
|
-
factor: 5,
|
|
119
|
-
stiffness: SCALE_SPRING_PRESETS.zoomIn
|
|
120
|
-
};
|
|
121
115
|
// --- Affected Charts Cache (set by GPUCoordinator) ---
|
|
122
116
|
/**
|
|
123
117
|
* Charts to redraw while this scale animates, asked of the registry rather
|
|
@@ -170,17 +164,6 @@ export class LinearScaleState {
|
|
|
170
164
|
// Store zoom limits
|
|
171
165
|
this.zoomLimits.max = zoomLimits[0];
|
|
172
166
|
this.zoomLimits.min = zoomLimits[1];
|
|
173
|
-
// Zoom-in effect options
|
|
174
|
-
if (options.zoomInEffect !== undefined) {
|
|
175
|
-
this.zoomInEffect.enabled = options.zoomInEffect;
|
|
176
|
-
}
|
|
177
|
-
if (options.zoomInFactor !== undefined) {
|
|
178
|
-
this.zoomInEffect.factor = options.zoomInFactor;
|
|
179
|
-
}
|
|
180
|
-
if (options.zoomInSpringConfig !== undefined) {
|
|
181
|
-
this.zoomInEffect.stiffness =
|
|
182
|
-
options.zoomInSpringConfig.stiffness || SCALE_SPRING_PRESETS.zoomIn;
|
|
183
|
-
}
|
|
184
167
|
// Create domain accessor object
|
|
185
168
|
this.domain = this.#createDomainAccessor();
|
|
186
169
|
}
|
|
@@ -404,6 +387,13 @@ export class LinearScaleState {
|
|
|
404
387
|
this.#setStiffness(SCALE_SPRING_PRESETS.zoom);
|
|
405
388
|
this.#ensureAnimating();
|
|
406
389
|
}
|
|
390
|
+
/**
|
|
391
|
+
* True once a chart drawing this scale has its marks' first data in (its
|
|
392
|
+
* entrance gate opened). Until then every extent change snaps, and the
|
|
393
|
+
* axes draw no ticks: the domain is a placeholder, [0, 100], and showing
|
|
394
|
+
* it, or springing out of it, is the zoom-in on load by another route.
|
|
395
|
+
*/
|
|
396
|
+
entered = $state(false);
|
|
407
397
|
/**
|
|
408
398
|
* Reset zoom to show full extent.
|
|
409
399
|
*/
|
|
@@ -422,28 +412,20 @@ export class LinearScaleState {
|
|
|
422
412
|
instant = true;
|
|
423
413
|
}
|
|
424
414
|
}
|
|
425
|
-
if (
|
|
426
|
-
|
|
427
|
-
// Also snaps when zoom-in is disabled on a not-yet-ready scale
|
|
428
|
-
// (e.g., column reassignment — no point animating from default domain).
|
|
429
|
-
this.#setInstant(min, max);
|
|
415
|
+
if (transitionsObserved()) {
|
|
416
|
+
noteTransition('domain', instant || !this.entered, () => this._affectedChartsArray, this.name);
|
|
430
417
|
}
|
|
431
|
-
|
|
432
|
-
//
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
//
|
|
440
|
-
|
|
441
|
-
this.#
|
|
442
|
-
// Initialize render values so first frame renders correctly
|
|
443
|
-
this.#renderMin = expandedMin;
|
|
444
|
-
this.#renderMax = expandedMax;
|
|
445
|
-
this.#updateGpuState();
|
|
446
|
-
this.#ensureAnimating();
|
|
418
|
+
if (instant || !this.entered) {
|
|
419
|
+
// Instant update - skip animation entirely. A scale's first domain
|
|
420
|
+
// snaps too: the viewport does not move on load. A chart's entrance
|
|
421
|
+
// is its data arriving (see the chart's entrance clock), not the
|
|
422
|
+
// camera travelling to it. So does every extent until a chart drawing
|
|
423
|
+
// this scale has its marks in (`entered`): the axes take the union of
|
|
424
|
+
// that chart's extents before it enters. Not `ready`, which several
|
|
425
|
+
// paths set before the first extent lands (time scales, field stats),
|
|
426
|
+
// and the scale then sprang from its placeholder domain. A chart that
|
|
427
|
+
// shares the scale and lands later springs it, as later data does.
|
|
428
|
+
this.#setInstant(min, max);
|
|
447
429
|
}
|
|
448
430
|
else {
|
|
449
431
|
// Normal reset
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Counts replacements of a chart's x and y scales: a defined scale id giving
|
|
3
|
+
* way to a different one, as a relabelled axis or a re-resolved scale type
|
|
4
|
+
* does. Marks rebuild their lifecycle on it.
|
|
5
|
+
*
|
|
6
|
+
* Not the ids themselves, which marks tracked before: a mark's own lifecycle
|
|
7
|
+
* registers the scale requirements that make the scales resolve, so the ids
|
|
8
|
+
* went from undefined to defined right after every mark was built, and every
|
|
9
|
+
* lifecycle was rebuilt once for nothing (its scale registration redone, and a
|
|
10
|
+
* tooltip-table DROP sent to DuckDB). The first resolution is not a change.
|
|
11
|
+
*
|
|
12
|
+
* Owned by the chart, not by ChartScales: the container recreates its
|
|
13
|
+
* ChartScales when a label or the layout changes, and a counter on that
|
|
14
|
+
* instance would restart and miss the swap. A recreated ChartScales is itself
|
|
15
|
+
* a replacement: it starts with no scale requirements (the container replays
|
|
16
|
+
* only those queued before the first one existed), so marks must rebuild to
|
|
17
|
+
* register with it. Its first resolution after that is not counted again.
|
|
18
|
+
*/
|
|
19
|
+
import type { ChartScales } from './ChartScales.svelte';
|
|
20
|
+
/** The ChartScales and last defined ids seen, and the replacements counted so far. */
|
|
21
|
+
export interface ScaleIds {
|
|
22
|
+
owner: unknown;
|
|
23
|
+
x: string | undefined;
|
|
24
|
+
y: string | undefined;
|
|
25
|
+
count: number;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Fold the current ChartScales and ids into `seen`. Counts a replacement when
|
|
29
|
+
* the ChartScales itself is a different one than before (it then forgets the
|
|
30
|
+
* ids: the new one's first resolution is part of the same replacement), or
|
|
31
|
+
* when an axis that had a defined id now has a different defined one.
|
|
32
|
+
* Undefined is neither a change nor remembered. Mutates and returns `seen`.
|
|
33
|
+
*/
|
|
34
|
+
export declare function foldScaleIds(seen: ScaleIds, owner: unknown, x: string | undefined, y: string | undefined): ScaleIds;
|
|
35
|
+
export declare class ScaleGeneration {
|
|
36
|
+
#private;
|
|
37
|
+
constructor(scales: () => ChartScales | null | undefined);
|
|
38
|
+
/**
|
|
39
|
+
* Replacements so far. Reactive: reads the chart's current ChartScales and
|
|
40
|
+
* scale ids. The derived updates `#seen` as it computes, which is safe only
|
|
41
|
+
* because folding the same input twice changes nothing: a recompute that
|
|
42
|
+
* sees unchanged ids counts nothing. Reads skipped between changes can
|
|
43
|
+
* merge two replacements into one count, which is fine: marks only need to
|
|
44
|
+
* know that it changed (a recreated scale has a new id, so a scale that
|
|
45
|
+
* goes away and returns is still seen as different).
|
|
46
|
+
*/
|
|
47
|
+
value: number;
|
|
48
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fold the current ChartScales and ids into `seen`. Counts a replacement when
|
|
3
|
+
* the ChartScales itself is a different one than before (it then forgets the
|
|
4
|
+
* ids: the new one's first resolution is part of the same replacement), or
|
|
5
|
+
* when an axis that had a defined id now has a different defined one.
|
|
6
|
+
* Undefined is neither a change nor remembered. Mutates and returns `seen`.
|
|
7
|
+
*/
|
|
8
|
+
export function foldScaleIds(seen, owner, x, y) {
|
|
9
|
+
if (owner && seen.owner && owner !== seen.owner) {
|
|
10
|
+
seen.count++;
|
|
11
|
+
seen.x = seen.y = undefined;
|
|
12
|
+
}
|
|
13
|
+
if (owner)
|
|
14
|
+
seen.owner = owner;
|
|
15
|
+
if ((x && seen.x && x !== seen.x) || (y && seen.y && y !== seen.y))
|
|
16
|
+
seen.count++;
|
|
17
|
+
if (x)
|
|
18
|
+
seen.x = x;
|
|
19
|
+
if (y)
|
|
20
|
+
seen.y = y;
|
|
21
|
+
return seen;
|
|
22
|
+
}
|
|
23
|
+
export class ScaleGeneration {
|
|
24
|
+
#scales;
|
|
25
|
+
#seen = { owner: undefined, x: undefined, y: undefined, count: 0 };
|
|
26
|
+
constructor(scales) {
|
|
27
|
+
this.#scales = scales;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Replacements so far. Reactive: reads the chart's current ChartScales and
|
|
31
|
+
* scale ids. The derived updates `#seen` as it computes, which is safe only
|
|
32
|
+
* because folding the same input twice changes nothing: a recompute that
|
|
33
|
+
* sees unchanged ids counts nothing. Reads skipped between changes can
|
|
34
|
+
* merge two replacements into one count, which is fine: marks only need to
|
|
35
|
+
* know that it changed (a recreated scale has a new id, so a scale that
|
|
36
|
+
* goes away and returns is still seen as different).
|
|
37
|
+
*/
|
|
38
|
+
value = $derived.by(() => {
|
|
39
|
+
const scales = this.#scales();
|
|
40
|
+
return foldScaleIds(this.#seen, scales, scales?.xScaleState?.id, scales?.yScaleState?.id).count;
|
|
41
|
+
});
|
|
42
|
+
}
|
|
@@ -266,7 +266,9 @@ export class ScaleRegistry {
|
|
|
266
266
|
reverse: config.reverse ?? false,
|
|
267
267
|
midpoint: config.midpoint,
|
|
268
268
|
constant: config.constant,
|
|
269
|
-
exponent: config.exponent
|
|
269
|
+
exponent: config.exponent,
|
|
270
|
+
range: config.range,
|
|
271
|
+
pinnedDomain: config.pinnedDomain
|
|
270
272
|
}, this, this.#gpuCoordinator));
|
|
271
273
|
}
|
|
272
274
|
else if (type === 'size') {
|
|
@@ -150,7 +150,21 @@ export declare class SlotTransition implements SlotSource {
|
|
|
150
150
|
* one). The space is unchanged, existing ids keep their paths, and the copy
|
|
151
151
|
* on resize is exactly right.
|
|
152
152
|
*/
|
|
153
|
-
layout(space: 'ids' | 'positions', order: ArrayLike<number> | null, idCount: number
|
|
153
|
+
layout(space: 'ids' | 'positions', order: ArrayLike<number> | null, idCount: number,
|
|
154
|
+
/**
|
|
155
|
+
* Land at once rather than travel: every id takes its new slot, leavers
|
|
156
|
+
* go and entrants appear. For a catch-up result, a change nobody just
|
|
157
|
+
* made. It settles the clock, which the marks on this axis share: their
|
|
158
|
+
* values land with it, which is what the caller wants for its chart.
|
|
159
|
+
*/
|
|
160
|
+
instant?: boolean): void;
|
|
161
|
+
/**
|
|
162
|
+
* The latest layout's number if it landed at once (an instant layout), 0
|
|
163
|
+
* otherwise. What follows from a layout, like a band grid's line opacity,
|
|
164
|
+
* lands with it, once: a number, so a consumer lands only its first write
|
|
165
|
+
* after that layout.
|
|
166
|
+
*/
|
|
167
|
+
get landedLayout(): number;
|
|
154
168
|
/**
|
|
155
169
|
* Start a transition without changing what is displayed.
|
|
156
170
|
*
|