@fjader/svelte 0.2.2 → 0.2.3
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/dist/components/axes/BandAxisTicks.svelte +63 -24
- package/dist/components/axes/BandAxisTicks.svelte.d.ts +5 -5
- package/dist/components/charts/ChartContainer.svelte +2 -1
- package/dist/components/guides/legend/ContinuousColorLegend.svelte +2 -1
- package/dist/components/guides/legend/OrdinalColorLegend.svelte +92 -34
- package/dist/components/marks/AreaMarkX.svelte +19 -6
- package/dist/components/marks/AreaMarkY.svelte +19 -6
- package/dist/components/marks/BarMarkX.svelte +3 -0
- package/dist/components/marks/BarMarkY.svelte +3 -0
- package/dist/components/marks/DotsMark.svelte +3 -0
- package/dist/components/marks/LineMark.svelte +3 -0
- package/dist/components/marks/ParallelCoordinatesMark.svelte +6 -0
- package/dist/components/marks/ParallelCoordinatesMark.svelte.d.ts +3 -0
- package/dist/components/marks/RectMark.svelte +3 -0
- package/dist/components/marks/RectXMark.svelte +3 -0
- package/dist/components/marks/RectYMark.svelte +3 -0
- package/dist/components/marks/RuleMarkX.svelte +10 -2
- package/dist/components/marks/RuleMarkY.svelte +10 -2
- package/dist/components/marks/TextMark.svelte +68 -4
- package/dist/components/marks/lifecycle/MarkLifecycleV2.svelte.d.ts +15 -1
- package/dist/components/marks/lifecycle/MarkLifecycleV2.svelte.js +116 -27
- package/dist/components/marks/lifecycle/domain-queries.js +13 -11
- package/dist/components/marks/lifecycle/transform-scale-key.d.ts +16 -0
- package/dist/components/marks/lifecycle/transform-scale-key.js +27 -0
- package/dist/components/marks/utils/area-outline.d.ts +24 -0
- package/dist/components/marks/utils/area-outline.js +10 -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 +30 -6
- package/dist/components/rendering/Areas.svelte.d.ts +2 -0
- package/dist/components/rendering/BaseGPURenderer.svelte.js +4 -1
- package/dist/components/rendering/Dots.svelte +24 -3
- package/dist/components/rendering/Dots.svelte.d.ts +2 -0
- package/dist/components/rendering/GridLine.svelte +2 -1
- package/dist/components/rendering/Lines.svelte +49 -7
- package/dist/components/rendering/Lines.svelte.d.ts +9 -0
- package/dist/components/rendering/ParallelLines.svelte +26 -7
- package/dist/components/rendering/ParallelLines.svelte.d.ts +2 -0
- package/dist/components/rendering/Rects.svelte +20 -5
- package/dist/components/rendering/Rects.svelte.d.ts +2 -0
- package/dist/components/rendering/RuleX.svelte +20 -5
- package/dist/components/rendering/RuleX.svelte.d.ts +2 -0
- package/dist/components/rendering/RuleY.svelte +20 -5
- package/dist/components/rendering/RuleY.svelte.d.ts +2 -0
- package/dist/components/rendering/activity-reads.d.ts +93 -0
- package/dist/components/rendering/activity-reads.js +135 -0
- package/dist/components/rendering/uniform-sizes.d.ts +26 -0
- package/dist/components/rendering/uniform-sizes.js +28 -0
- package/dist/config/ChartConfig.svelte.d.ts +2 -0
- package/dist/gpu/ColorScaleShaderLib.d.ts +13 -1
- package/dist/gpu/ColorScaleShaderLib.js +56 -5
- package/dist/gpu/GPUCoordinator.svelte.d.ts +8 -0
- package/dist/gpu/GPUCoordinator.svelte.js +68 -32
- package/dist/gpu/ResolveShader.d.ts +2 -2
- package/dist/gpu/ResolveShader.js +2 -2
- package/dist/gpu/ScaleTemplateSystem.d.ts +19 -0
- package/dist/gpu/ScaleTemplateSystem.js +86 -7
- package/dist/gpu/SizeScaleShaderLib.d.ts +4 -4
- package/dist/gpu/SizeScaleShaderLib.js +4 -4
- package/dist/gpu/identity-slot-packing.d.ts +157 -40
- package/dist/gpu/identity-slot-packing.js +262 -62
- package/dist/gpu/tracked-scales.d.ts +14 -0
- package/dist/gpu/tracked-scales.js +18 -0
- package/dist/index.js +1 -0
- package/dist/interactors/BandLabelToggle.svelte +5 -9
- package/dist/interactors/BandLabelToggle.svelte.d.ts +3 -3
- package/dist/interactors/DerivedInterval1D.svelte +26 -54
- package/dist/interactors/Interval1D.svelte +2 -1
- package/dist/interactors/Interval2D.svelte +2 -1
- package/dist/interactors/ParallelAxisSource.svelte +4 -4
- package/dist/interactors/ParallelAxisSource.svelte.d.ts +2 -1
- package/dist/interactors/ParallelBrushes.svelte +8 -9
- package/dist/interactors/ParallelBrushes.svelte.d.ts +3 -3
- package/dist/interactors/Toggle.svelte +17 -36
- package/dist/interactors/Toggle.svelte.d.ts +4 -3
- package/dist/interactors/Tooltip.svelte +2 -1
- package/dist/interactors/TooltipManager.svelte.js +8 -3
- package/dist/interactors/relay-clauses.d.ts +23 -0
- package/dist/interactors/relay-clauses.js +60 -0
- package/dist/query-engine/CrossfilterSelection.svelte.d.ts +88 -15
- package/dist/query-engine/CrossfilterSelection.svelte.js +94 -20
- package/dist/query-engine/FieldActivity.svelte.d.ts +79 -0
- package/dist/query-engine/FieldActivity.svelte.js +207 -0
- package/dist/query-engine/FilterGroup.svelte.d.ts +53 -29
- package/dist/query-engine/FilterGroup.svelte.js +181 -122
- package/dist/query-engine/FilterGroupRegistry.svelte.d.ts +42 -0
- package/dist/query-engine/FilterGroupRegistry.svelte.js +103 -1
- package/dist/query-engine/MarkHandle.svelte.d.ts +9 -1
- package/dist/query-engine/MarkHandle.svelte.js +12 -2
- package/dist/query-engine/QueryEngine.d.ts +52 -16
- package/dist/query-engine/QueryEngine.js +90 -28
- package/dist/query-engine/categorical-identity.d.ts +99 -27
- package/dist/query-engine/categorical-identity.js +210 -51
- package/dist/query-engine/index.d.ts +2 -1
- package/dist/query-engine/index.js +1 -0
- package/dist/query-engine/preagg/Preagg.d.ts +1 -1
- package/dist/query-engine/preagg/Preagg.js +11 -9
- package/dist/query-engine/preagg/plan.d.ts +9 -5
- package/dist/query-engine/preagg/plan.js +13 -11
- package/dist/query-engine/toggle-predicate.js +12 -1
- package/dist/scales/ActivityTable.d.ts +99 -0
- package/dist/scales/ActivityTable.js +211 -0
- package/dist/scales/BandScaleState.svelte.d.ts +28 -7
- package/dist/scales/BandScaleState.svelte.js +55 -20
- package/dist/scales/ChartScales.svelte.d.ts +2 -2
- package/dist/scales/ColorScaleState.svelte.d.ts +36 -8
- package/dist/scales/ColorScaleState.svelte.js +118 -24
- package/dist/scales/DomainResolver.d.ts +2 -0
- package/dist/scales/DomainResolver.js +15 -8
- package/dist/scales/ScaleRegistry.svelte.d.ts +9 -0
- package/dist/scales/ScaleRegistry.svelte.js +22 -1
- package/dist/scales/TimeScaleState.svelte.js +7 -1
- package/dist/spec/index.js +1 -0
- package/dist/spec/types.d.ts +2 -0
- package/dist/transforms/index.d.ts +27 -0
- package/dist/transforms/index.js +97 -3
- package/dist/types.d.ts +30 -0
- package/package.json +1 -1
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { scaleOrdinal } from 'd3-scale';
|
|
3
3
|
import { createD3Scale, getOrdinalColors, bakeGradient, bakeInterpolator, bakePalette, getSchemeType, parseColorToRGBA, resolveColorScaleTypes, getResolvedInterpolator, transformTypeToGPU, applyTransform, colorScaleTicks } from './color-utils';
|
|
4
4
|
import { COLOR_RAMP_RESOLUTION } from '../gpu/GPUCoordinator.svelte';
|
|
5
|
-
import { COLOR_CONFIG_BYTES, COLOR_CONFIG_SLOT_BASE_OFFSET, NO_SLOT_TABLE } from '../gpu/ColorScaleShaderLib';
|
|
5
|
+
import { COLOR_CONFIG_BYTES, COLOR_CONFIG_SLOT_BASE_OFFSET, NO_SLOT_TABLE, NULL_COLOR_RANK } from '../gpu/ColorScaleShaderLib';
|
|
6
6
|
/**
|
|
7
7
|
* Reactive state for a GPU-backed color scale.
|
|
8
8
|
*
|
|
@@ -14,7 +14,7 @@ export class ColorScaleState {
|
|
|
14
14
|
#field;
|
|
15
15
|
#registry;
|
|
16
16
|
#gpuCoordinator;
|
|
17
|
-
#
|
|
17
|
+
#id = crypto.randomUUID();
|
|
18
18
|
// GPU slot indices (allocated from registry)
|
|
19
19
|
#configSlot = -1;
|
|
20
20
|
#rampSlot = -1; // Used by continuous scales
|
|
@@ -31,7 +31,6 @@ export class ColorScaleState {
|
|
|
31
31
|
// Reactive state
|
|
32
32
|
domain = $state([]);
|
|
33
33
|
ready = $state(false);
|
|
34
|
-
#activeValues = $state(null); // null = all active, Set = only these active
|
|
35
34
|
// Bumped on any visual-config change (transform type, constant, exponent, scheme).
|
|
36
35
|
// Legend components read this to know when to re-render the gradient.
|
|
37
36
|
_version = $state(0);
|
|
@@ -90,9 +89,37 @@ export class ColorScaleState {
|
|
|
90
89
|
* a change.
|
|
91
90
|
*/
|
|
92
91
|
#slots = { count: 0, from: new Float32Array(0), to: new Float32Array(0), t: 1, version: 0 };
|
|
92
|
+
/**
|
|
93
|
+
* Read by the packer every frame. The rank table covers the ids the field had
|
|
94
|
+
* when it was built, and the field's identity grows in place: another table
|
|
95
|
+
* appends categories, and a chart acquiring an Other bucket appends its id,
|
|
96
|
+
* any chart, since this scale is shared per field. An id past the table's end
|
|
97
|
+
* reads as unranked and draws transparent, so a grown identity rebuilds it
|
|
98
|
+
* here, once per growth.
|
|
99
|
+
*/
|
|
93
100
|
get slots() {
|
|
101
|
+
const identity = this.#identity;
|
|
102
|
+
if (identity?.present && identity.version !== this.#ranksVersion)
|
|
103
|
+
this.#refreshRanks();
|
|
94
104
|
return this.#slots;
|
|
95
105
|
}
|
|
106
|
+
/** The identity version the rank table was last built from. */
|
|
107
|
+
#ranksVersion = -1;
|
|
108
|
+
/**
|
|
109
|
+
* Bumped whenever the ranks change, so a reader of getColor (a legend's
|
|
110
|
+
* swatches) re-reads once identity lands. The ranks are a plain Map, and the
|
|
111
|
+
* domain usually arrives first: without this a legend kept the colours it
|
|
112
|
+
* computed before there were ranks.
|
|
113
|
+
*/
|
|
114
|
+
#ranksRevision = $state(0);
|
|
115
|
+
/**
|
|
116
|
+
* Reactive: changes whenever the palette ranks are rebuilt, which is also when
|
|
117
|
+
* identity is adopted. A reader of `identityKey` or `getColor` in a derived
|
|
118
|
+
* reads this to re-run then.
|
|
119
|
+
*/
|
|
120
|
+
get ranksRevision() {
|
|
121
|
+
return this.#ranksRevision;
|
|
122
|
+
}
|
|
96
123
|
/**
|
|
97
124
|
* Where this scale's block sits in the shared identity slot table. Assigned
|
|
98
125
|
* by the coordinator's packer.
|
|
@@ -132,9 +159,18 @@ export class ColorScaleState {
|
|
|
132
159
|
// Before the scale has a config slot there is nothing to patch; whichever
|
|
133
160
|
// upload path claims one writes the base with the rest of the struct.
|
|
134
161
|
if (this.#configSlot >= 0) {
|
|
135
|
-
this.#gpuCoordinator?.writeColorConfigSlotBase(this.#configSlot,
|
|
162
|
+
this.#gpuCoordinator?.writeColorConfigSlotBase(this.#configSlot, this.#configSlotBase);
|
|
136
163
|
}
|
|
137
164
|
}
|
|
165
|
+
/**
|
|
166
|
+
* The slot base the config carries: this scale's block while its channel
|
|
167
|
+
* carries ids, NO_SLOT_TABLE while it carries palette indices. The one rule
|
|
168
|
+
* for both writers (setSlotBase's patch and the full upload), so which wrote
|
|
169
|
+
* last cannot change what the GPU reads.
|
|
170
|
+
*/
|
|
171
|
+
get #configSlotBase() {
|
|
172
|
+
return this.hasIdentity && this.#slotBase >= 0 ? this.#slotBase : NO_SLOT_TABLE;
|
|
173
|
+
}
|
|
138
174
|
// ViewManager integration - tracks tables this scale has registered columns for
|
|
139
175
|
constructor(config, registry, gpuCoordinator) {
|
|
140
176
|
this.#field = config.field;
|
|
@@ -170,15 +206,22 @@ export class ColorScaleState {
|
|
|
170
206
|
get field() {
|
|
171
207
|
return this.#field;
|
|
172
208
|
}
|
|
209
|
+
/**
|
|
210
|
+
* The identity key of the dim this scale adopted, or null before it has one.
|
|
211
|
+
* Selection activity is kept per identity key, so this is what the packer
|
|
212
|
+
* points the scale's block at.
|
|
213
|
+
*/
|
|
214
|
+
get identityKey() {
|
|
215
|
+
// Only while the channel carries ids: a key without them would route a
|
|
216
|
+
// mark to dim by activity the shader cannot read.
|
|
217
|
+
return this.hasIdentity ? this.#identity.key || null : null;
|
|
218
|
+
}
|
|
173
219
|
get scheme() {
|
|
174
220
|
return this.#scheme;
|
|
175
221
|
}
|
|
176
222
|
get scaleType() {
|
|
177
223
|
return this.#scaleType;
|
|
178
224
|
}
|
|
179
|
-
get activeValues() {
|
|
180
|
-
return this.#activeValues;
|
|
181
|
-
}
|
|
182
225
|
get isOrdinal() {
|
|
183
226
|
return this.#renderType === 'ordinal';
|
|
184
227
|
}
|
|
@@ -208,10 +251,15 @@ export class ColorScaleState {
|
|
|
208
251
|
return this.#d3Scale;
|
|
209
252
|
}
|
|
210
253
|
/**
|
|
211
|
-
*
|
|
254
|
+
* The id the coordinator tracks this scale by, as it does every scale.
|
|
255
|
+
*
|
|
256
|
+
* Tracking is what puts the scale in front of the identity slot packer
|
|
257
|
+
* (see tracked-scales.ts). Without an id, its rank table and selection
|
|
258
|
+
* activity were never packed: the shader read the channel's dim id as a
|
|
259
|
+
* palette index, and selection dimming had nothing to read.
|
|
212
260
|
*/
|
|
213
|
-
get
|
|
214
|
-
return this.#
|
|
261
|
+
get id() {
|
|
262
|
+
return this.#id;
|
|
215
263
|
}
|
|
216
264
|
// ============================================================
|
|
217
265
|
// SLOT ALLOCATION
|
|
@@ -300,6 +348,10 @@ export class ColorScaleState {
|
|
|
300
348
|
return [lo, hi];
|
|
301
349
|
}
|
|
302
350
|
#uploadToGPU() {
|
|
351
|
+
// A destroyed scale's slots may already belong to another scale: a late
|
|
352
|
+
// upload (identity landing after the chart unmounted) would overwrite them.
|
|
353
|
+
if (this.#destroyed)
|
|
354
|
+
return;
|
|
303
355
|
if (!this.#gpuCoordinator) {
|
|
304
356
|
console.warn('[ColorScaleState] No GPU coordinator, skipping upload');
|
|
305
357
|
return;
|
|
@@ -388,15 +440,28 @@ export class ColorScaleState {
|
|
|
388
440
|
*/
|
|
389
441
|
setIdentity(identity) {
|
|
390
442
|
this.#warnIfDestroyed('setIdentity');
|
|
443
|
+
const hadIdentity = this.hasIdentity;
|
|
391
444
|
const previous = this.#identity;
|
|
392
445
|
this.#identity = identity;
|
|
393
446
|
if (previous && previous !== identity && !previous.present) {
|
|
394
447
|
this.#paletteRank.clear();
|
|
395
448
|
this.#nextRank = 0;
|
|
396
449
|
}
|
|
450
|
+
// Identity landing after the config was uploaded, or going (a reload
|
|
451
|
+
// empties it). The domain fetch and the identity columns are fetched in
|
|
452
|
+
// parallel, so either can arrive first; the config's slot base, its palette
|
|
453
|
+
// size and the CPU scale all depend on whether the channel carries ids,
|
|
454
|
+
// so a change of that uploads again. The ranks are refreshed here either
|
|
455
|
+
// way: they are CPU state, needed without a coordinator too.
|
|
397
456
|
this.#refreshRanks();
|
|
457
|
+
if (hadIdentity !== this.hasIdentity && this.domain.length > 0)
|
|
458
|
+
this.#uploadToGPU();
|
|
398
459
|
}
|
|
399
460
|
/** True once this scale's channel carries dim ids rather than palette indices. */
|
|
461
|
+
/** The field's live id <-> value bijection, once adopted. */
|
|
462
|
+
get identity() {
|
|
463
|
+
return this.#identity ?? null;
|
|
464
|
+
}
|
|
400
465
|
get hasIdentity() {
|
|
401
466
|
return this.isOrdinal && (this.#identity?.present ?? false);
|
|
402
467
|
}
|
|
@@ -419,10 +484,14 @@ export class ColorScaleState {
|
|
|
419
484
|
const identity = this.#identity;
|
|
420
485
|
if (!identity?.present)
|
|
421
486
|
return;
|
|
487
|
+
this.#ranksVersion = identity.version;
|
|
422
488
|
for (const value of this.domain) {
|
|
423
489
|
const id = identity.ids.get(value);
|
|
424
490
|
if (id === undefined || this.#paletteRank.has(id))
|
|
425
491
|
continue;
|
|
492
|
+
// The NULL category takes no palette slot (see below).
|
|
493
|
+
if (id === identity.nullId)
|
|
494
|
+
continue;
|
|
426
495
|
this.#paletteRank.set(id, this.#nextRank++);
|
|
427
496
|
}
|
|
428
497
|
const table = new Float32Array(identity.values.length);
|
|
@@ -431,6 +500,16 @@ export class ColorScaleState {
|
|
|
431
500
|
if (id >= 0 && id < table.length)
|
|
432
501
|
table[id] = rank;
|
|
433
502
|
}
|
|
503
|
+
// The NULL category and the Other buckets draw the null colour: no palette
|
|
504
|
+
// slot, so they read as missing data or "the rest" and shift no category's
|
|
505
|
+
// colour. Not -1, which the shader draws transparent: a category outside
|
|
506
|
+
// this scale's domain.
|
|
507
|
+
if (identity.nullId !== null)
|
|
508
|
+
table[identity.nullId] = NULL_COLOR_RANK;
|
|
509
|
+
for (const id of identity.synthetic)
|
|
510
|
+
if (id < table.length)
|
|
511
|
+
table[id] = NULL_COLOR_RANK;
|
|
512
|
+
this.#ranksRevision++;
|
|
434
513
|
const slots = this.#slots;
|
|
435
514
|
slots.from = table;
|
|
436
515
|
slots.to = table;
|
|
@@ -476,9 +555,8 @@ export class ColorScaleState {
|
|
|
476
555
|
return undefined;
|
|
477
556
|
return this.#identity.values[id];
|
|
478
557
|
}
|
|
558
|
+
/** Called from #uploadToGPU only, which has refreshed the ranks. */
|
|
479
559
|
#uploadOrdinal() {
|
|
480
|
-
// New categories in the domain need ranks before the palette is sized.
|
|
481
|
-
this.#refreshRanks();
|
|
482
560
|
// Bake and upload palette
|
|
483
561
|
const paletteData = bakePalette(this.#scheme, { reverse: this.#reverse });
|
|
484
562
|
this.#gpuCoordinator.writeColorPalette(this.#paletteSlot, paletteData);
|
|
@@ -497,7 +575,7 @@ export class ColorScaleState {
|
|
|
497
575
|
type: 0, // ORDINAL
|
|
498
576
|
paletteOffset: this.#paletteSlot * 32,
|
|
499
577
|
paletteSize: Math.min(Math.max(rankSpace, 1), 32),
|
|
500
|
-
slotBase: this
|
|
578
|
+
slotBase: this.#configSlotBase
|
|
501
579
|
});
|
|
502
580
|
this.#gpuCoordinator.writeColorConfig(this.#configSlot, config);
|
|
503
581
|
}
|
|
@@ -660,6 +738,14 @@ export class ColorScaleState {
|
|
|
660
738
|
* transform (e.g. non-positive values under log), mirroring the GPU shader.
|
|
661
739
|
*/
|
|
662
740
|
getColor(value) {
|
|
741
|
+
// With palette ranks, the colour is exactly the shader's: value -> id ->
|
|
742
|
+
// rank -> palette, and the null colour for the NULL category and the Other
|
|
743
|
+
// buckets. Not through the d3 scale, whose domain is stringified: a null
|
|
744
|
+
// (or a number) would miss it, and d3 would hand the value the next
|
|
745
|
+
// palette colour.
|
|
746
|
+
void this.#ranksRevision;
|
|
747
|
+
if (this.#hasRanks())
|
|
748
|
+
return this.#rankedColor(value);
|
|
663
749
|
if (!this.#d3Scale)
|
|
664
750
|
return '#000000';
|
|
665
751
|
if (this.#transformType === 'log' && typeof value === 'number' && value <= 0) {
|
|
@@ -667,6 +753,18 @@ export class ColorScaleState {
|
|
|
667
753
|
}
|
|
668
754
|
return this.#d3Scale(value);
|
|
669
755
|
}
|
|
756
|
+
#rankedColor(value) {
|
|
757
|
+
const identity = this.#identity;
|
|
758
|
+
const id = identity.ids.get(value);
|
|
759
|
+
const rank = id === undefined ? undefined : this.#paletteRank.get(id);
|
|
760
|
+
if (rank === undefined)
|
|
761
|
+
return ColorScaleState.NULL_COLOR;
|
|
762
|
+
let colors = getOrdinalColors(this.#scheme) ?? [];
|
|
763
|
+
if (this.#reverse)
|
|
764
|
+
colors = [...colors].reverse();
|
|
765
|
+
// Same wrap the shader applies, so an over-32 palette agrees too.
|
|
766
|
+
return colors.length ? colors[rank % colors.length] : ColorScaleState.NULL_COLOR;
|
|
767
|
+
}
|
|
670
768
|
/**
|
|
671
769
|
* Get color as RGBA array (0-1 range).
|
|
672
770
|
*/
|
|
@@ -674,18 +772,14 @@ export class ColorScaleState {
|
|
|
674
772
|
const cssColor = this.getColor(value);
|
|
675
773
|
return parseColorToRGBA(cssColor);
|
|
676
774
|
}
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
clearActiveValues() {
|
|
684
|
-
this.#activeValues = null;
|
|
685
|
-
}
|
|
686
|
-
isActive(value) {
|
|
687
|
-
return this.#activeValues == null || this.#activeValues.has(value);
|
|
775
|
+
/**
|
|
776
|
+
* Charts drawing this scale. Asked of the registry, as BandScaleState does,
|
|
777
|
+
* so it is current when charts come and go.
|
|
778
|
+
*/
|
|
779
|
+
get _affectedChartsArray() {
|
|
780
|
+
return this.#registry?.chartsUsing(this) ?? ColorScaleState.#noCharts;
|
|
688
781
|
}
|
|
782
|
+
static #noCharts = [];
|
|
689
783
|
// ============================================================
|
|
690
784
|
// CLEANUP
|
|
691
785
|
// ============================================================
|
|
@@ -141,6 +141,8 @@ export declare class DomainResolver {
|
|
|
141
141
|
channelValueFor(value: DimValue, displayed: readonly DimValue[]): number | null;
|
|
142
142
|
/** True once this field's dim exists and holds at least one value. */
|
|
143
143
|
get hasIdentity(): boolean;
|
|
144
|
+
/** The dim adopted last; a different object after a reload. */
|
|
145
|
+
get identity(): FieldIdentity | null;
|
|
144
146
|
/** How many ids the dim holds — the width of a slot table over it. */
|
|
145
147
|
get idCount(): number;
|
|
146
148
|
/** How many marks have registered. */
|
|
@@ -188,17 +188,18 @@ export class DomainResolver {
|
|
|
188
188
|
// #extents encodes at the boundary, which is the whole point of the two
|
|
189
189
|
// registration methods, so its input was already ids that had passed the
|
|
190
190
|
// same check, and the list it was given had just been deduplicated. Its
|
|
191
|
-
// range check is subsumed by the `value
|
|
192
|
-
// an id past the end, a negative one and a non-integer one alike,
|
|
193
|
-
// all three index the value array to undefined.
|
|
191
|
+
// range check is subsumed by the `value === undefined` test below, which
|
|
192
|
+
// drops an id past the end, a negative one and a non-integer one alike,
|
|
193
|
+
// because all three index the value array to undefined. Not `== null`:
|
|
194
|
+
// the NULL category's value IS null, and it is a category like any other.
|
|
194
195
|
//
|
|
195
196
|
// An id with no value is dropped from the IDS, before either projection.
|
|
196
197
|
// Filtering only the values would shorten the value list without
|
|
197
198
|
// shortening the permutation, and the two would disagree with the band
|
|
198
199
|
// index space d3 and the shader's categoryCount use — by one, with
|
|
199
|
-
// nothing failing. Unreachable today (
|
|
200
|
-
//
|
|
201
|
-
// rather than checked against each other.
|
|
200
|
+
// nothing failing. Unreachable today (every dim row has a value: a real
|
|
201
|
+
// one, a synthetic row's label, or the NULL category's null), which is
|
|
202
|
+
// why the two are built together rather than checked against each other.
|
|
202
203
|
const seen = new Set();
|
|
203
204
|
const orderedIds = [];
|
|
204
205
|
const resolved = [];
|
|
@@ -214,7 +215,7 @@ export class DomainResolver {
|
|
|
214
215
|
continue;
|
|
215
216
|
}
|
|
216
217
|
const value = values[category];
|
|
217
|
-
if (value
|
|
218
|
+
if (value === undefined)
|
|
218
219
|
continue;
|
|
219
220
|
orderedIds.push(category);
|
|
220
221
|
resolved.push(value);
|
|
@@ -293,7 +294,9 @@ export class DomainResolver {
|
|
|
293
294
|
const out = [];
|
|
294
295
|
const seen = new Set();
|
|
295
296
|
for (const item of items) {
|
|
296
|
-
|
|
297
|
+
// A value of null is the NULL category, which `ids` maps to its id; an
|
|
298
|
+
// encoded null is no id at all.
|
|
299
|
+
if (item === undefined || (encoded && item === null))
|
|
297
300
|
continue;
|
|
298
301
|
const id = encoded ? item : identity.ids.get(item);
|
|
299
302
|
if (id === undefined || !Number.isInteger(id) || id < 0 || id >= limit)
|
|
@@ -371,6 +374,10 @@ export class DomainResolver {
|
|
|
371
374
|
get hasIdentity() {
|
|
372
375
|
return this.#identity?.present ?? false;
|
|
373
376
|
}
|
|
377
|
+
/** The dim adopted last; a different object after a reload. */
|
|
378
|
+
get identity() {
|
|
379
|
+
return this.#identity;
|
|
380
|
+
}
|
|
374
381
|
/** How many ids the dim holds — the width of a slot table over it. */
|
|
375
382
|
get idCount() {
|
|
376
383
|
return this.#identity?.values.length ?? 0;
|
|
@@ -73,6 +73,15 @@ export declare class ScaleRegistry {
|
|
|
73
73
|
* a scale this registry has never seen.
|
|
74
74
|
*/
|
|
75
75
|
chartsUsing(scale: AnyScaleState): string[];
|
|
76
|
+
/**
|
|
77
|
+
* Fill `out` with every chart drawing a scale on the categorical field
|
|
78
|
+
* `key` (by identity key), each once: the charts a selection fade on that
|
|
79
|
+
* field redraws. A superset of the charts whose marks read the field's
|
|
80
|
+
* activity, since every such mark draws through a scale on it; the extra
|
|
81
|
+
* charts (other groups, marks holding the scale without reading it) redraw
|
|
82
|
+
* for the 0.25 s of a fade. Read on every frame a fade runs.
|
|
83
|
+
*/
|
|
84
|
+
chartsDrawingField(key: string, out: string[]): string[];
|
|
76
85
|
deregisterChart(scale: AnyScaleState, chartId: string): void;
|
|
77
86
|
removeChartUsage(chartId: string): void;
|
|
78
87
|
_removeScaleFromStorage(scaleInstance: AnyScaleState): void;
|
|
@@ -9,6 +9,7 @@ import { BandScaleState } from './BandScaleState.svelte';
|
|
|
9
9
|
import { ColorScaleState } from './ColorScaleState.svelte';
|
|
10
10
|
import { SizeScaleState } from './SizeScaleState.svelte';
|
|
11
11
|
import { scaleOrdinal } from 'd3-scale';
|
|
12
|
+
import { MAX_COLOR_SLOTS } from '../gpu/ColorScaleShaderLib';
|
|
12
13
|
import { isTransformType } from './color-utils';
|
|
13
14
|
// Helper to stringify sorted
|
|
14
15
|
const stableStringify = (obj) => {
|
|
@@ -50,7 +51,8 @@ export class ScaleRegistry {
|
|
|
50
51
|
#nextColorSlot = 0;
|
|
51
52
|
#nextRampSlot = 0;
|
|
52
53
|
#nextPaletteSlot = 0;
|
|
53
|
-
|
|
54
|
+
// The shader's array length and the coordinator's buffer size, stated once.
|
|
55
|
+
#maxColorSlots = MAX_COLOR_SLOTS;
|
|
54
56
|
#maxRampSlots = 64;
|
|
55
57
|
#maxPaletteSlots = 64;
|
|
56
58
|
// Size scale slot management
|
|
@@ -348,6 +350,25 @@ export class ScaleRegistry {
|
|
|
348
350
|
chartsUsing(scale) {
|
|
349
351
|
return this.#chartsFor.get(scale) ?? NO_CHARTS;
|
|
350
352
|
}
|
|
353
|
+
/**
|
|
354
|
+
* Fill `out` with every chart drawing a scale on the categorical field
|
|
355
|
+
* `key` (by identity key), each once: the charts a selection fade on that
|
|
356
|
+
* field redraws. A superset of the charts whose marks read the field's
|
|
357
|
+
* activity, since every such mark draws through a scale on it; the extra
|
|
358
|
+
* charts (other groups, marks holding the scale without reading it) redraw
|
|
359
|
+
* for the 0.25 s of a fade. Read on every frame a fade runs.
|
|
360
|
+
*/
|
|
361
|
+
chartsDrawingField(key, out) {
|
|
362
|
+
for (const [scale, charts] of this.#chartsFor) {
|
|
363
|
+
if (scale.identityKey !== key)
|
|
364
|
+
continue;
|
|
365
|
+
for (let i = 0; i < charts.length; i++) {
|
|
366
|
+
if (!out.includes(charts[i]))
|
|
367
|
+
out.push(charts[i]);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
return out;
|
|
371
|
+
}
|
|
351
372
|
/** Re-derive one scale's redraw list. The only writer. */
|
|
352
373
|
#publishCharts(scale, chartIds) {
|
|
353
374
|
this.#chartsFor.set(scale, Array.from(chartIds));
|
|
@@ -20,7 +20,13 @@ export class TimeScaleState extends LinearScaleState {
|
|
|
20
20
|
return 'time';
|
|
21
21
|
}
|
|
22
22
|
// --- Time Metadata ---
|
|
23
|
-
|
|
23
|
+
/**
|
|
24
|
+
* Origin Date (raw global min datetime). State, because SQL is built from
|
|
25
|
+
* it: a mark's offset select and a brush's predicate are $derived, and one
|
|
26
|
+
* computed before the first datetime extent landed must rebuild when it
|
|
27
|
+
* does, or it keeps comparing the raw column with offsets.
|
|
28
|
+
*/
|
|
29
|
+
#t0 = $state.raw(null);
|
|
24
30
|
#unit = 1; // Seconds per offset unit (1 = seconds)
|
|
25
31
|
// --- Datetime Extent Tracking ---
|
|
26
32
|
#datetimeExtents = new Map(); // Map<markId, [minDate, maxDate]>
|
package/dist/spec/index.js
CHANGED
|
@@ -47,6 +47,7 @@ export async function render(target, spec, options = {}) {
|
|
|
47
47
|
// 4. Build context map — injected into Svelte mount() so child components
|
|
48
48
|
// find these via getContext() calls (same keys as initFjader())
|
|
49
49
|
const { filterGroups } = createQueryServices(queryEngine);
|
|
50
|
+
gpuCoordinator.setFilterGroups(filterGroups);
|
|
50
51
|
const context = new Map();
|
|
51
52
|
context.set('query-engine', queryEngine);
|
|
52
53
|
context.set('filter-groups', filterGroups);
|
package/dist/spec/types.d.ts
CHANGED
|
@@ -133,6 +133,8 @@ export type MarkSpec = DotMarkSpec | BarXMarkSpec | BarYMarkSpec | LineMarkSpec
|
|
|
133
133
|
interface BaseMarkSpec {
|
|
134
134
|
table: string;
|
|
135
135
|
selection?: string;
|
|
136
|
+
/** How the mark responds to selections in its group; see BaseMarkProps. Default 'auto'. */
|
|
137
|
+
selectionResponse?: 'auto' | 'filter';
|
|
136
138
|
scheme?: ColorScheme;
|
|
137
139
|
limit?: number;
|
|
138
140
|
transforms?: TransformSpec[];
|
|
@@ -45,6 +45,33 @@ export declare function topN(n: number | (() => number), options?: TopNOptions):
|
|
|
45
45
|
* the identity index columns in the same result.
|
|
46
46
|
*/
|
|
47
47
|
export declare const RANK_COLUMN = "__dplot_rank";
|
|
48
|
+
/**
|
|
49
|
+
* The Other row's members: the ids of the categories folded into it, and each
|
|
50
|
+
* one's measure, as two aligned lists ordered by id. NULL on every other row.
|
|
51
|
+
*
|
|
52
|
+
* Returned by the query rather than worked out on the client, because only the
|
|
53
|
+
* query knows exactly what Other summed: under every filter the chart receives,
|
|
54
|
+
* NULL (`-1`) included when it was folded. Clicking Other selects these, and
|
|
55
|
+
* its selected share is computed from them.
|
|
56
|
+
*
|
|
57
|
+
* `__dplot_`-prefixed, like the rank, so tooltips never show them; and the SQL
|
|
58
|
+
* tooltip table leaves out every `OTHER_MEMBER_PREFIX` column, so a hover never
|
|
59
|
+
* ships them back.
|
|
60
|
+
*/
|
|
61
|
+
export declare const OTHER_MEMBER_PREFIX = "__dplot_other_";
|
|
62
|
+
export declare const OTHER_IDS_COLUMN = "__dplot_other_ids";
|
|
63
|
+
export declare const OTHER_VALUES_COLUMN = "__dplot_other_values";
|
|
64
|
+
/**
|
|
65
|
+
* `sql` without Other's member lists, for tables queried on every hover. A
|
|
66
|
+
* COLUMNS filter rather than EXCLUDE, which errors on a column that is absent,
|
|
67
|
+
* as it is on every mark without an Other bucket.
|
|
68
|
+
*
|
|
69
|
+
* The `lambda c:` form, not `c ->`: DuckDB deprecated the arrow lambda in 1.3,
|
|
70
|
+
* and every SQL tooltip table goes through this, so an upgrade dropping it would
|
|
71
|
+
* break tooltips on every mark. Both DuckDB-WASM 1.30 (DuckDB 1.3.2) and the
|
|
72
|
+
* DuckDB the tests run accept it.
|
|
73
|
+
*/
|
|
74
|
+
export declare function withoutOtherMembers(sql: string): string;
|
|
48
75
|
/**
|
|
49
76
|
* Most padded rows `dense()` will produce before refusing.
|
|
50
77
|
*
|
package/dist/transforms/index.js
CHANGED
|
@@ -135,6 +135,37 @@ function resolveTopNColumns(aliases, options, config) {
|
|
|
135
135
|
* the identity index columns in the same result.
|
|
136
136
|
*/
|
|
137
137
|
export const RANK_COLUMN = '__dplot_rank';
|
|
138
|
+
/**
|
|
139
|
+
* The Other row's members: the ids of the categories folded into it, and each
|
|
140
|
+
* one's measure, as two aligned lists ordered by id. NULL on every other row.
|
|
141
|
+
*
|
|
142
|
+
* Returned by the query rather than worked out on the client, because only the
|
|
143
|
+
* query knows exactly what Other summed: under every filter the chart receives,
|
|
144
|
+
* NULL (`-1`) included when it was folded. Clicking Other selects these, and
|
|
145
|
+
* its selected share is computed from them.
|
|
146
|
+
*
|
|
147
|
+
* `__dplot_`-prefixed, like the rank, so tooltips never show them; and the SQL
|
|
148
|
+
* tooltip table leaves out every `OTHER_MEMBER_PREFIX` column, so a hover never
|
|
149
|
+
* ships them back.
|
|
150
|
+
*/
|
|
151
|
+
export const OTHER_MEMBER_PREFIX = '__dplot_other_';
|
|
152
|
+
export const OTHER_IDS_COLUMN = `${OTHER_MEMBER_PREFIX}ids`;
|
|
153
|
+
export const OTHER_VALUES_COLUMN = `${OTHER_MEMBER_PREFIX}values`;
|
|
154
|
+
/**
|
|
155
|
+
* `sql` without Other's member lists, for tables queried on every hover. A
|
|
156
|
+
* COLUMNS filter rather than EXCLUDE, which errors on a column that is absent,
|
|
157
|
+
* as it is on every mark without an Other bucket.
|
|
158
|
+
*
|
|
159
|
+
* The `lambda c:` form, not `c ->`: DuckDB deprecated the arrow lambda in 1.3,
|
|
160
|
+
* and every SQL tooltip table goes through this, so an upgrade dropping it would
|
|
161
|
+
* break tooltips on every mark. Both DuckDB-WASM 1.30 (DuckDB 1.3.2) and the
|
|
162
|
+
* DuckDB the tests run accept it.
|
|
163
|
+
*/
|
|
164
|
+
export function withoutOtherMembers(sql) {
|
|
165
|
+
return `SELECT COLUMNS(lambda c: NOT starts_with(c, '${OTHER_MEMBER_PREFIX}')) FROM (${sql})`;
|
|
166
|
+
}
|
|
167
|
+
/** The original band id, carried through bucketing, which rewrites the band column. */
|
|
168
|
+
const BAND_SOURCE_COLUMN = '__dplot_band_src';
|
|
138
169
|
/**
|
|
139
170
|
* Give every category in the dim a row, at zero past the cutoff.
|
|
140
171
|
*
|
|
@@ -189,6 +220,12 @@ function densify(inner, aliases, plan) {
|
|
|
189
220
|
// missing (category, series) pair carries that series' real id.
|
|
190
221
|
select[alias] = rawSql(`${crossed}."id"`);
|
|
191
222
|
}
|
|
223
|
+
else if (alias.startsWith(OTHER_MEMBER_PREFIX)) {
|
|
224
|
+
// Other's member lists: NULL on a padded row, as on every row but
|
|
225
|
+
// Other's. Never COALESCEd to 0 like a measure, which is a type error on
|
|
226
|
+
// a list.
|
|
227
|
+
select[alias] = rawSql(`a.${column(alias)}`);
|
|
228
|
+
}
|
|
192
229
|
else if (plan.identity[alias]?.dim === dim) {
|
|
193
230
|
// Same dim as the band, so this channel IS the category — the ordinary
|
|
194
231
|
// "colour the bars by their own category" chart. Its value for a row
|
|
@@ -235,13 +272,13 @@ function densify(inner, aliases, plan) {
|
|
|
235
272
|
// a missing category. Each key's ids get -1 appended: a row whose series is
|
|
236
273
|
// NULL carries -1, and a join over the dim's ids alone would drop it.
|
|
237
274
|
const joins = cross
|
|
238
|
-
.map(([, keyDim], i) => ` CROSS JOIN (SELECT "id" FROM ${
|
|
275
|
+
.map(([key, keyDim], i) => ` CROSS JOIN (SELECT "id" FROM ${padIds(plan.identity[key], keyDim)} UNION ALL SELECT -1) AS k${i}`)
|
|
239
276
|
.join('');
|
|
240
277
|
const on = [
|
|
241
278
|
`a.${column(band)} = d."id"`,
|
|
242
279
|
...cross.map(([key], i) => `a.${column(key)} = k${i}."id"`)
|
|
243
280
|
].join(' AND ');
|
|
244
|
-
return Query.from(rawSql(`${
|
|
281
|
+
return Query.from(rawSql(`${padIds(plan.identity[band], dim)} AS d${joins} LEFT JOIN (${inner}) AS a ON ${on}`)).select(select);
|
|
245
282
|
}
|
|
246
283
|
/**
|
|
247
284
|
* Most padded rows `dense()` will produce before refusing.
|
|
@@ -254,6 +291,23 @@ function densify(inner, aliases, plan) {
|
|
|
254
291
|
* before any of this existed.
|
|
255
292
|
*/
|
|
256
293
|
export const DENSE_ROW_LIMIT = 10_000;
|
|
294
|
+
/**
|
|
295
|
+
* The ids a channel pads from: the ids its mark's own table holds, plus every
|
|
296
|
+
* Other bucket of the dim, when the table's ids are known; the whole dim before.
|
|
297
|
+
*
|
|
298
|
+
* The dim spans every table registering the field, so padding from all of it
|
|
299
|
+
* gave a chart a zero row, and a slot on a sorted axis, for every category only
|
|
300
|
+
* another table has. Other buckets stay in: an Other row must find its padding
|
|
301
|
+
* row, and another mark's bucket is kept off the axis by its NaN rank, as
|
|
302
|
+
* before.
|
|
303
|
+
*/
|
|
304
|
+
function padIds(id, dim) {
|
|
305
|
+
const present = id?.present;
|
|
306
|
+
if (!present)
|
|
307
|
+
return `(SELECT "id" FROM ${quoteTable(dim)})`;
|
|
308
|
+
return (`(SELECT "id" FROM ${quoteTable(present)} UNION ALL ` +
|
|
309
|
+
`SELECT "id" FROM ${quoteTable(dim)} WHERE "label" IS NOT NULL)`);
|
|
310
|
+
}
|
|
257
311
|
/**
|
|
258
312
|
* Whether `dense()` applies to a query with these aliases, and how.
|
|
259
313
|
*
|
|
@@ -680,7 +734,37 @@ function collapseIntoOther(ranked, aliases, o) {
|
|
|
680
734
|
}
|
|
681
735
|
}
|
|
682
736
|
bucket[RANK_COLUMN] = sqlTag `CASE WHEN ${kept} THEN ${rank} ELSE ${n + 1} END`;
|
|
737
|
+
// The band column is rewritten to the Other id above, so the category each
|
|
738
|
+
// folded row came from is carried beside it for the member lists.
|
|
739
|
+
bucket[BAND_SOURCE_COLUMN] = column(band);
|
|
683
740
|
const bucketed = Query.from({ __topn_ranked: ranked }).select(bucket);
|
|
741
|
+
// PER MEMBER first: one row per group key and folded category. A grouped mark
|
|
742
|
+
// already has one row per (category, series), so this changes nothing there;
|
|
743
|
+
// an ungrouped one (raw rows, no groupKeys) would otherwise list one entry per
|
|
744
|
+
// RAW ROW past the cutoff — ids repeated, and a list that grows with the table
|
|
745
|
+
// rather than the field. Every aggregate below (SUM, MAX, MIN) gives the same
|
|
746
|
+
// answer over these rows as over the raw ones, so Other's values are unchanged.
|
|
747
|
+
const src = column(BAND_SOURCE_COLUMN);
|
|
748
|
+
const member = {};
|
|
749
|
+
const memberKeys = [src];
|
|
750
|
+
for (const alias of aliases) {
|
|
751
|
+
const col = column(alias);
|
|
752
|
+
if (isKey(alias)) {
|
|
753
|
+
member[alias] = col;
|
|
754
|
+
memberKeys.push(col);
|
|
755
|
+
}
|
|
756
|
+
else if (alias === by) {
|
|
757
|
+
member[alias] = sqlTag `SUM(${col})`;
|
|
758
|
+
}
|
|
759
|
+
else {
|
|
760
|
+
member[alias] = sqlTag `MAX(${col})`;
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
member[RANK_COLUMN] = sqlTag `MIN(${rank})`;
|
|
764
|
+
member[BAND_SOURCE_COLUMN] = src;
|
|
765
|
+
const members = Query.from({ __topn_bucketed: bucketed })
|
|
766
|
+
.select(member)
|
|
767
|
+
.groupby(...memberKeys);
|
|
684
768
|
const collapse = {};
|
|
685
769
|
const keys = [];
|
|
686
770
|
for (const alias of aliases) {
|
|
@@ -697,7 +781,17 @@ function collapseIntoOther(ranked, aliases, o) {
|
|
|
697
781
|
}
|
|
698
782
|
}
|
|
699
783
|
collapse[RANK_COLUMN] = sqlTag `MIN(${rank})`;
|
|
700
|
-
|
|
784
|
+
// Other's members, from the rows the bucket step moved past the cutoff (their
|
|
785
|
+
// rank is n + 1 there): one entry per folded category, from the step above. A
|
|
786
|
+
// kept row's group has none, so its lists are NULL. Cast so they arrive as
|
|
787
|
+
// Int32Array and Float64Array children: ids are REAL in the channel, and a
|
|
788
|
+
// COUNT measure is BIGINT, which would arrive as BigInt64Array.
|
|
789
|
+
const past = sqlTag `${rank} > ${n}`;
|
|
790
|
+
collapse[OTHER_IDS_COLUMN] =
|
|
791
|
+
sqlTag `list(CAST(${src} AS INTEGER) ORDER BY ${src}) FILTER (WHERE ${past})`;
|
|
792
|
+
collapse[OTHER_VALUES_COLUMN] =
|
|
793
|
+
sqlTag `list(CAST(${column(by)} AS DOUBLE) ORDER BY ${src}) FILTER (WHERE ${past})`;
|
|
794
|
+
return Query.from({ __topn_members: members })
|
|
701
795
|
.select(collapse)
|
|
702
796
|
.groupby(...keys);
|
|
703
797
|
}
|