@fjader/svelte 0.2.2 → 0.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (148) hide show
  1. package/README.md +2 -0
  2. package/dist/components/axes/AxisBottom.svelte +23 -6
  3. package/dist/components/axes/AxisBottom.svelte.d.ts +10 -0
  4. package/dist/components/axes/AxisLeft.svelte +77 -8
  5. package/dist/components/axes/AxisLeft.svelte.d.ts +17 -0
  6. package/dist/components/axes/BandAxisTicks.svelte +64 -25
  7. package/dist/components/axes/BandAxisTicks.svelte.d.ts +5 -5
  8. package/dist/components/axes/axis-title.d.ts +30 -0
  9. package/dist/components/axes/axis-title.js +77 -0
  10. package/dist/components/charts/ChartCell.svelte +1 -1
  11. package/dist/components/charts/ChartContainer.svelte +12 -3
  12. package/dist/components/charts/ChartContainer.svelte.d.ts +7 -0
  13. package/dist/components/guides/legend/ContinuousColorLegend.svelte +9 -2
  14. package/dist/components/guides/legend/OrdinalColorLegend.svelte +96 -35
  15. package/dist/components/marks/AreaMarkX.svelte +21 -6
  16. package/dist/components/marks/AreaMarkY.svelte +21 -6
  17. package/dist/components/marks/BarMarkX.svelte +16 -1
  18. package/dist/components/marks/BarMarkY.svelte +16 -1
  19. package/dist/components/marks/DotsMark.svelte +5 -0
  20. package/dist/components/marks/LineMark.svelte +8 -1
  21. package/dist/components/marks/ParallelCoordinatesMark.svelte +10 -0
  22. package/dist/components/marks/ParallelCoordinatesMark.svelte.d.ts +5 -0
  23. package/dist/components/marks/RectMark.svelte +5 -0
  24. package/dist/components/marks/RectXMark.svelte +5 -0
  25. package/dist/components/marks/RectYMark.svelte +5 -0
  26. package/dist/components/marks/RuleMarkX.svelte +12 -2
  27. package/dist/components/marks/RuleMarkY.svelte +12 -2
  28. package/dist/components/marks/TextMark.svelte +76 -13
  29. package/dist/components/marks/lifecycle/MarkLifecycleV2.svelte.d.ts +21 -2
  30. package/dist/components/marks/lifecycle/MarkLifecycleV2.svelte.js +300 -42
  31. package/dist/components/marks/lifecycle/domain-queries.d.ts +3 -1
  32. package/dist/components/marks/lifecycle/domain-queries.js +22 -15
  33. package/dist/components/marks/lifecycle/domain-registration.js +2 -6
  34. package/dist/components/marks/lifecycle/other-rows.svelte.d.ts +104 -0
  35. package/dist/components/marks/lifecycle/other-rows.svelte.js +193 -0
  36. package/dist/components/marks/lifecycle/other-split.d.ts +80 -0
  37. package/dist/components/marks/lifecycle/other-split.js +112 -0
  38. package/dist/components/marks/lifecycle/query-builder.d.ts +26 -1
  39. package/dist/components/marks/lifecycle/query-builder.js +25 -5
  40. package/dist/components/marks/lifecycle/transform-scale-key.d.ts +16 -0
  41. package/dist/components/marks/lifecycle/transform-scale-key.js +27 -0
  42. package/dist/components/marks/utils/area-outline.d.ts +24 -0
  43. package/dist/components/marks/utils/area-outline.js +10 -0
  44. package/dist/components/marks/utils/reactive-config.d.ts +1 -1
  45. package/dist/components/marks/utils/reactive-config.js +9 -0
  46. package/dist/components/marks/utils/tooltip-helpers.d.ts +4 -1
  47. package/dist/components/marks/utils/tooltip-helpers.js +2 -1
  48. package/dist/components/rendering/Areas.svelte +30 -6
  49. package/dist/components/rendering/Areas.svelte.d.ts +2 -0
  50. package/dist/components/rendering/BaseGPURenderer.svelte.d.ts +3 -1
  51. package/dist/components/rendering/BaseGPURenderer.svelte.js +8 -3
  52. package/dist/components/rendering/Dots.svelte +24 -3
  53. package/dist/components/rendering/Dots.svelte.d.ts +2 -0
  54. package/dist/components/rendering/GridLine.svelte +2 -1
  55. package/dist/components/rendering/Lines.svelte +49 -7
  56. package/dist/components/rendering/Lines.svelte.d.ts +9 -0
  57. package/dist/components/rendering/ParallelLines.svelte +26 -7
  58. package/dist/components/rendering/ParallelLines.svelte.d.ts +2 -0
  59. package/dist/components/rendering/Rects.svelte +129 -20
  60. package/dist/components/rendering/Rects.svelte.d.ts +9 -0
  61. package/dist/components/rendering/RuleX.svelte +20 -5
  62. package/dist/components/rendering/RuleX.svelte.d.ts +2 -0
  63. package/dist/components/rendering/RuleY.svelte +20 -5
  64. package/dist/components/rendering/RuleY.svelte.d.ts +2 -0
  65. package/dist/components/rendering/activity-reads.d.ts +111 -0
  66. package/dist/components/rendering/activity-reads.js +154 -0
  67. package/dist/components/rendering/uniform-sizes.d.ts +26 -0
  68. package/dist/components/rendering/uniform-sizes.js +28 -0
  69. package/dist/config/ChartConfig.svelte.d.ts +5 -1
  70. package/dist/gpu/ColorScaleShaderLib.d.ts +13 -1
  71. package/dist/gpu/ColorScaleShaderLib.js +56 -5
  72. package/dist/gpu/GPUCoordinator.svelte.d.ts +8 -0
  73. package/dist/gpu/GPUCoordinator.svelte.js +68 -32
  74. package/dist/gpu/MorphedChannels.d.ts +6 -32
  75. package/dist/gpu/MorphedChannels.js +45 -3
  76. package/dist/gpu/ResolveShader.d.ts +2 -2
  77. package/dist/gpu/ResolveShader.js +2 -2
  78. package/dist/gpu/ScaleTemplateSystem.d.ts +19 -0
  79. package/dist/gpu/ScaleTemplateSystem.js +111 -11
  80. package/dist/gpu/SizeScaleShaderLib.d.ts +4 -4
  81. package/dist/gpu/SizeScaleShaderLib.js +4 -4
  82. package/dist/gpu/identity-slot-packing.d.ts +157 -40
  83. package/dist/gpu/identity-slot-packing.js +262 -62
  84. package/dist/gpu/tracked-scales.d.ts +14 -0
  85. package/dist/gpu/tracked-scales.js +18 -0
  86. package/dist/index.d.ts +2 -0
  87. package/dist/index.js +4 -0
  88. package/dist/interactors/BandLabelToggle.svelte +5 -9
  89. package/dist/interactors/BandLabelToggle.svelte.d.ts +3 -3
  90. package/dist/interactors/DerivedInterval1D.svelte +26 -54
  91. package/dist/interactors/Interval1D.svelte +8 -2
  92. package/dist/interactors/Interval2D.svelte +8 -2
  93. package/dist/interactors/ParallelAxisSource.svelte +14 -5
  94. package/dist/interactors/ParallelAxisSource.svelte.d.ts +2 -1
  95. package/dist/interactors/ParallelBrushes.svelte +8 -9
  96. package/dist/interactors/ParallelBrushes.svelte.d.ts +3 -3
  97. package/dist/interactors/Toggle.svelte +48 -38
  98. package/dist/interactors/Toggle.svelte.d.ts +4 -3
  99. package/dist/interactors/Tooltip.svelte +140 -42
  100. package/dist/interactors/TooltipManager.svelte.d.ts +20 -5
  101. package/dist/interactors/TooltipManager.svelte.js +40 -10
  102. package/dist/interactors/other-tooltip.d.ts +16 -0
  103. package/dist/interactors/other-tooltip.js +22 -0
  104. package/dist/interactors/relay-clauses.d.ts +23 -0
  105. package/dist/interactors/relay-clauses.js +60 -0
  106. package/dist/query-engine/CrossfilterSelection.svelte.d.ts +88 -15
  107. package/dist/query-engine/CrossfilterSelection.svelte.js +94 -20
  108. package/dist/query-engine/FieldActivity.svelte.d.ts +128 -0
  109. package/dist/query-engine/FieldActivity.svelte.js +378 -0
  110. package/dist/query-engine/FilterGroup.svelte.d.ts +76 -30
  111. package/dist/query-engine/FilterGroup.svelte.js +223 -124
  112. package/dist/query-engine/FilterGroupRegistry.svelte.d.ts +73 -3
  113. package/dist/query-engine/FilterGroupRegistry.svelte.js +161 -12
  114. package/dist/query-engine/MarkHandle.svelte.d.ts +9 -1
  115. package/dist/query-engine/MarkHandle.svelte.js +12 -2
  116. package/dist/query-engine/QueryEngine.d.ts +52 -16
  117. package/dist/query-engine/QueryEngine.js +116 -34
  118. package/dist/query-engine/categorical-identity.d.ts +114 -27
  119. package/dist/query-engine/categorical-identity.js +225 -51
  120. package/dist/query-engine/index.d.ts +2 -1
  121. package/dist/query-engine/index.js +1 -0
  122. package/dist/query-engine/preagg/Preagg.d.ts +1 -1
  123. package/dist/query-engine/preagg/Preagg.js +11 -9
  124. package/dist/query-engine/preagg/plan.d.ts +14 -5
  125. package/dist/query-engine/preagg/plan.js +44 -18
  126. package/dist/query-engine/toggle-predicate.d.ts +21 -0
  127. package/dist/query-engine/toggle-predicate.js +41 -2
  128. package/dist/scales/ActivityTable.d.ts +103 -0
  129. package/dist/scales/ActivityTable.js +220 -0
  130. package/dist/scales/BandScaleState.svelte.d.ts +41 -15
  131. package/dist/scales/BandScaleState.svelte.js +78 -36
  132. package/dist/scales/ChartScales.svelte.d.ts +2 -2
  133. package/dist/scales/ColorScaleState.svelte.d.ts +36 -8
  134. package/dist/scales/ColorScaleState.svelte.js +118 -24
  135. package/dist/scales/DomainResolver.d.ts +2 -0
  136. package/dist/scales/DomainResolver.js +15 -8
  137. package/dist/scales/ScaleRegistry.svelte.d.ts +9 -0
  138. package/dist/scales/ScaleRegistry.svelte.js +22 -1
  139. package/dist/scales/TimeScaleState.svelte.js +7 -1
  140. package/dist/spec/index.js +1 -0
  141. package/dist/spec/types.d.ts +4 -0
  142. package/dist/theme.css +5 -4
  143. package/dist/transforms/TransformCompiler.d.ts +37 -2
  144. package/dist/transforms/TransformCompiler.js +44 -1
  145. package/dist/transforms/index.d.ts +27 -0
  146. package/dist/transforms/index.js +127 -19
  147. package/dist/types.d.ts +71 -0
  148. package/package.json +3 -2
@@ -47,7 +47,6 @@ export class BandScaleState {
47
47
  // =========================================================================
48
48
  // SELECTION STATE
49
49
  // =========================================================================
50
- #activeValues = $state(null); // null = all active, Set = only these active
51
50
  // =========================================================================
52
51
  // POINTER STATE
53
52
  // =========================================================================
@@ -104,21 +103,6 @@ export class BandScaleState {
104
103
  setReady() {
105
104
  this.ready = true;
106
105
  }
107
- // =========================================================================
108
- // ACTIVE VALUES (selection feedback)
109
- // =========================================================================
110
- get activeValues() {
111
- return this.#activeValues;
112
- }
113
- setActiveValues(values) {
114
- this.#activeValues = values instanceof Set ? values : new Set(values);
115
- }
116
- clearActiveValues() {
117
- this.#activeValues = null;
118
- }
119
- isActive(value) {
120
- return this.#activeValues == null || this.#activeValues.has(value);
121
- }
122
106
  /**
123
107
  * Marks the scale dead. Clears nothing, deliberately.
124
108
  *
@@ -186,6 +170,15 @@ export class BandScaleState {
186
170
  get sourceField() {
187
171
  return this.#sourceField ?? this.name;
188
172
  }
173
+ /**
174
+ * The identity key of the dim this scale adopted, or null before it has one.
175
+ * Selection activity is kept per identity key, so this is what the packer
176
+ * points the scale's block at.
177
+ */
178
+ get identityKey() {
179
+ // Only while the channel carries ids, as for colour.
180
+ return this.hasIdentity ? this.#resolver.identity.key || null : null;
181
+ }
189
182
  // =========================================================================
190
183
  // FREEZE/DEBOUNCE CONTROL
191
184
  // =========================================================================
@@ -334,6 +327,53 @@ export class BandScaleState {
334
327
  setOtherId(id) {
335
328
  this.#slots.otherId = id;
336
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
+ }
334
+ /**
335
+ * This axis's Other bucket, shared by every mark on it that has one.
336
+ *
337
+ * One id per band scale, not per mark or per chart: marks share a band scale
338
+ * only when they agree on what Other is — the scale's key carries the table
339
+ * and the category-reducing transform (its `transformKey`), N included — so
340
+ * everything drawn on one axis lines up in one Other band. A bar and its
341
+ * value labels, or two charts with the identical topN, share it.
342
+ *
343
+ * Reference-counted by mark id. The first holder acquires; holders arriving
344
+ * while that is in flight share it; the last to release gives it back.
345
+ */
346
+ retainOther(holder, acquire) {
347
+ this.#otherHolders.add(holder);
348
+ if (!this.#other) {
349
+ const pending = acquire();
350
+ this.#other = pending;
351
+ pending.then((other) => {
352
+ if (this.#other === pending)
353
+ this.setOtherId(other.id);
354
+ }, () => {
355
+ if (this.#other === pending)
356
+ this.#other = null;
357
+ });
358
+ }
359
+ return this.#other;
360
+ }
361
+ /**
362
+ * Drop `holder`'s claim on the Other bucket. The last one out hands the id to
363
+ * `release` — once the acquire has landed, if it is still in flight.
364
+ */
365
+ releaseOther(holder, release) {
366
+ if (!this.#otherHolders.delete(holder) || this.#otherHolders.size > 0)
367
+ return;
368
+ const pending = this.#other;
369
+ this.#other = null;
370
+ this.setOtherId(null);
371
+ pending?.then(release, () => { });
372
+ }
373
+ /** The marks holding this axis's Other bucket. */
374
+ #otherHolders = new Set();
375
+ /** The bucket they share, while any holds it. */
376
+ #other = null;
337
377
  // -------------------------------------------------------------------------
338
378
  // IDENTITY <-> DISPLAY, FOR CPU CONSUMERS
339
379
  // -------------------------------------------------------------------------
@@ -585,8 +625,10 @@ export class BandScaleState {
585
625
  /**
586
626
  * Get D3 band scale configured for rendering
587
627
  */
588
- getBaseScale(axis, innerWidthOrHeight) {
589
- this.#sharedScale.range(axis === 'x' ? [0, innerWidthOrHeight] : [innerWidthOrHeight, 0]);
628
+ getBaseScale(_axis, innerWidthOrHeight) {
629
+ // The same on both axes, not reversed on y: slot 0 is at the top, as in
630
+ // the shader (slotPosition).
631
+ this.#sharedScale.range([0, innerWidthOrHeight]);
590
632
  if (this.#paddingInner !== null && this.#paddingOuter !== null) {
591
633
  this.#sharedScale.paddingInner(this.#paddingInner).paddingOuter(this.#paddingOuter);
592
634
  }
@@ -709,6 +751,9 @@ export class BandScaleState {
709
751
  * so it fades out over the half slot before that centre reaches the axis end
710
752
  * (the animated count), and is gone from there; the same at the start. An
711
753
  * entrant's label appears as its band crosses in.
754
+ *
755
+ * A selection's dimming is not here: it belongs to the filter groups of the
756
+ * marks the axis labels, so the axis multiplies it in (BandAxisTicks).
712
757
  */
713
758
  tickOpacity(key) {
714
759
  void this.#slotFrame;
@@ -720,27 +765,25 @@ export class BandScaleState {
720
765
  /**
721
766
  * Where a tick's label sits now, in pixels along the axis: the centre of its
722
767
  * category's current slot, so it travels with the bar. Slot 0 is at the start
723
- * of the range on x and at the bottom on y, as in the shader and d3's
724
- * reversed range. Per frame, O(1).
768
+ * of the range on both axes: the left on x, the top on y, as in the shader
769
+ * and d3's range. Per frame, O(1).
725
770
  */
726
- tickPosition(key, rangeSize, axis) {
727
- return this.slotPosition(this.tickSlot(key), rangeSize, axis);
771
+ tickPosition(key, rangeSize) {
772
+ return this.slotPosition(this.tickSlot(key), rangeSize);
728
773
  }
729
774
  /**
730
775
  * The same, for a caller that already has the slot.
731
776
  *
732
777
  * A consumer that has to test the slot first — an SVG label, which must not
733
778
  * draw a category the axis is not displaying — would otherwise ask for it
734
- * twice per item per frame. The axis flip lives here rather than being
735
- * re-derived by such a caller.
779
+ * twice per item per frame.
780
+ *
781
+ * The same on both axes. A y band axis reads top-down, like a list: slot 0
782
+ * at the top, so a ranking and an explicit domain need no reversing, and
783
+ * the exit one past the last slot is the end of the list on either axis.
736
784
  */
737
- slotPosition(slot, rangeSize, axis) {
738
- const centre = this.slotCenter(slot, rangeSize);
739
- return axis === 'y' ? rangeSize - centre : centre;
740
- }
741
- /** Whether a tick's category is outside the current selection. */
742
- isTickDimmed(tick) {
743
- return tick.value != null && !this.isActive(tick.value);
785
+ slotPosition(slot, rangeSize) {
786
+ return this.slotCenter(slot, rangeSize);
744
787
  }
745
788
  /**
746
789
  * Centre of a (possibly fractional) slot, measured from the start of the
@@ -819,7 +862,9 @@ export class BandScaleState {
819
862
  * Returns the 0-based index of the band under the cursor, or null if
820
863
  * no bands exist.
821
864
  */
822
- invertScreenToData(screenVal, rangeSize, isYAxis = false) {
865
+ invertScreenToData(screenVal, rangeSize,
866
+ // Band slots run from the start of the range on both axes (slotPosition).
867
+ _isYAxis = false) {
823
868
  const config = this.getConfig();
824
869
  const n = config.categoryCount;
825
870
  if (n === 0)
@@ -828,10 +873,7 @@ export class BandScaleState {
828
873
  if (stepSize === 0)
829
874
  return null;
830
875
  const offset = config.paddingOuter * stepSize;
831
- // GPU band scale renders Y categories bottom-to-top (matching D3),
832
- // so flip screen coordinate for Y axis inversion.
833
- const pos = isYAxis ? rangeSize - screenVal : screenVal;
834
- const bandIndex = Math.floor((pos - offset) / stepSize);
876
+ const bandIndex = Math.floor((screenVal - offset) / stepSize);
835
877
  // Clamped to a WHOLE slot. Mid-transition the count is fractional, so a
836
878
  // bare `n - 1` hands back a fraction for a pointer past the last band —
837
879
  // and a fractional slot indexes nothing, so idAtSlot and the domain both
@@ -65,8 +65,8 @@ export declare class ChartScales {
65
65
  };
66
66
  get xScaleState(): ScaleStateInstance | null;
67
67
  get yScaleState(): ScaleStateInstance | null;
68
- baseXScale: import("d3-scale").ScaleContinuousNumeric<number, number, never> | import("d3-scale").ScaleBand<import("../query-engine/categorical-identity").DimValue>;
69
- baseYScale: import("d3-scale").ScaleContinuousNumeric<number, number, never> | import("d3-scale").ScaleBand<import("../query-engine/categorical-identity").DimValue>;
68
+ baseXScale: import("d3-scale").ScaleBand<import("../query-engine/categorical-identity").DimValue> | import("d3-scale").ScaleContinuousNumeric<number, number, never>;
69
+ baseYScale: import("d3-scale").ScaleBand<import("../query-engine/categorical-identity").DimValue> | import("d3-scale").ScaleContinuousNumeric<number, number, never>;
70
70
  get xScale(): D3Scale | null;
71
71
  get yScale(): D3Scale | null;
72
72
  get xTargetScale(): D3Scale | null;
@@ -25,7 +25,21 @@ export declare class ColorScaleState {
25
25
  domain: (string | number)[];
26
26
  ready: boolean;
27
27
  _version: number;
28
+ /**
29
+ * Read by the packer every frame. The rank table covers the ids the field had
30
+ * when it was built, and the field's identity grows in place: another table
31
+ * appends categories, and a chart acquiring an Other bucket appends its id,
32
+ * any chart, since this scale is shared per field. An id past the table's end
33
+ * reads as unranked and draws transparent, so a grown identity rebuilds it
34
+ * here, once per growth.
35
+ */
28
36
  get slots(): SlotSource;
37
+ /**
38
+ * Reactive: changes whenever the palette ranks are rebuilt, which is also when
39
+ * identity is adopted. A reader of `identityKey` or `getColor` in a derived
40
+ * reads this to re-run then.
41
+ */
42
+ get ranksRevision(): number;
29
43
  /**
30
44
  * Called by the packer when this scale's block moves, and told its own base
31
45
  * rather than having it written into its config from outside.
@@ -51,9 +65,14 @@ export declare class ColorScaleState {
51
65
  setSlotBase(base: number): void;
52
66
  constructor(config: ColorScaleConfig, registry: ScaleRegistry, gpuCoordinator: GPUCoordinator | null);
53
67
  get field(): string;
68
+ /**
69
+ * The identity key of the dim this scale adopted, or null before it has one.
70
+ * Selection activity is kept per identity key, so this is what the packer
71
+ * points the scale's block at.
72
+ */
73
+ get identityKey(): string | null;
54
74
  get scheme(): string;
55
75
  get scaleType(): ColorScaleType;
56
- get activeValues(): Set<string | number> | null;
57
76
  get isOrdinal(): boolean;
58
77
  get isContinuous(): boolean;
59
78
  /**
@@ -73,9 +92,14 @@ export declare class ColorScaleState {
73
92
  */
74
93
  get d3Scale(): D3ColorScale | null;
75
94
  /**
76
- * Unique scale ID for ViewManager registration.
95
+ * The id the coordinator tracks this scale by, as it does every scale.
96
+ *
97
+ * Tracking is what puts the scale in front of the identity slot packer
98
+ * (see tracked-scales.ts). Without an id, its rank table and selection
99
+ * activity were never packed: the shader read the channel's dim id as a
100
+ * palette index, and selection dimming had nothing to read.
77
101
  */
78
- get scaleId(): string;
102
+ get id(): string;
79
103
  /**
80
104
  * Converts raw data into GPU-ready values.
81
105
  * - Ordinal: string -> 0-based index (-1 = null/unknown, 0+ = categories)
@@ -95,13 +119,15 @@ export declare class ColorScaleState {
95
119
  */
96
120
  setIdentity(identity: FieldIdentity): void;
97
121
  /** True once this scale's channel carries dim ids rather than palette indices. */
122
+ /** The field's live id <-> value bijection, once adopted. */
123
+ get identity(): FieldIdentity | null;
98
124
  get hasIdentity(): boolean;
99
125
  /**
100
126
  * Source value for a channel id — the decode side of the encode the query
101
127
  * performs, for tooltips and legends. Undefined without identity, where the
102
128
  * channel carries a palette index and the caller should read the domain.
103
129
  */
104
- categoryAt(id: number): string | number | undefined;
130
+ categoryAt(id: number): string | number | null | undefined;
105
131
  /** Update the domain. Triggers GPU re-upload. */
106
132
  setDomain(newDomain: (string | number)[]): void;
107
133
  /**
@@ -139,14 +165,16 @@ export declare class ColorScaleState {
139
165
  * Returns NULL_COLOR for values that are unmappable under the current
140
166
  * transform (e.g. non-positive values under log), mirroring the GPU shader.
141
167
  */
142
- getColor(value: string | number): string;
168
+ getColor(value: string | number | null): string;
143
169
  /**
144
170
  * Get color as RGBA array (0-1 range).
145
171
  */
146
172
  getColorRGBA(value: string | number): RGBA;
147
- setActiveValues(values: Iterable<string | number>): void;
148
- clearActiveValues(): void;
149
- isActive(value: string | number): boolean;
173
+ /**
174
+ * Charts drawing this scale. Asked of the registry, as BandScaleState does,
175
+ * so it is current when charts come and go.
176
+ */
177
+ get _affectedChartsArray(): string[];
150
178
  /**
151
179
  * Marks the scale dead. Clears nothing, deliberately — see
152
180
  * BandScaleState.destroy for the full argument.
@@ -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
- #scaleId = crypto.randomUUID(); // Unique ID for ViewManager registration
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, base);
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
- * Unique scale ID for ViewManager registration.
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 scaleId() {
214
- return this.#scaleId;
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.hasIdentity ? this.#slotBase : undefined
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
- // ACTIVE VALUES (selection feedback)
679
- // ============================================================
680
- setActiveValues(values) {
681
- this.#activeValues = values instanceof Set ? values : new Set(values);
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 == null` test below, which drops
192
- // an id past the end, a negative one and a non-integer one alike, because
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 (the dim has no NULL values and
200
- // synthetic rows carry a label), which is why the two are built together
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 == null)
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
- if (item == null)
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;