@fjader/svelte 0.2.3 → 0.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (167) hide show
  1. package/README.md +3 -1
  2. package/dist/animation/EntranceGate.d.ts +52 -0
  3. package/dist/animation/EntranceGate.js +93 -0
  4. package/dist/animation/TransitionClock.d.ts +61 -30
  5. package/dist/animation/TransitionClock.js +67 -29
  6. package/dist/animation/transition-log.d.ts +41 -0
  7. package/dist/animation/transition-log.js +57 -0
  8. package/dist/components/axes/AxisBottom.svelte +27 -8
  9. package/dist/components/axes/AxisBottom.svelte.d.ts +10 -0
  10. package/dist/components/axes/AxisCenter.svelte +4 -2
  11. package/dist/components/axes/AxisLeft.svelte +81 -10
  12. package/dist/components/axes/AxisLeft.svelte.d.ts +17 -0
  13. package/dist/components/axes/BandAxisTicks.svelte +4 -3
  14. package/dist/components/axes/axis-title.d.ts +30 -0
  15. package/dist/components/axes/axis-title.js +77 -0
  16. package/dist/components/charts/ChartCell.svelte +33 -6
  17. package/dist/components/charts/ChartCell.svelte.d.ts +9 -0
  18. package/dist/components/charts/ChartContainer.svelte +55 -5
  19. package/dist/components/charts/ChartContainer.svelte.d.ts +14 -1
  20. package/dist/components/charts/ChartHover.svelte.d.ts +14 -0
  21. package/dist/components/charts/ChartHover.svelte.js +22 -0
  22. package/dist/components/charts/ChartVisibility.svelte.d.ts +43 -0
  23. package/dist/components/charts/ChartVisibility.svelte.js +67 -0
  24. package/dist/components/guides/Grid.svelte +12 -1
  25. package/dist/components/guides/legend/ContinuousColorLegend.svelte +7 -1
  26. package/dist/components/guides/legend/OrdinalColorLegend.svelte +8 -5
  27. package/dist/components/marks/AreaMarkX.svelte +18 -4
  28. package/dist/components/marks/AreaMarkY.svelte +18 -4
  29. package/dist/components/marks/BarMarkX.svelte +20 -4
  30. package/dist/components/marks/BarMarkY.svelte +20 -4
  31. package/dist/components/marks/DotsMark.svelte +8 -3
  32. package/dist/components/marks/LineMark.svelte +13 -6
  33. package/dist/components/marks/ParallelCoordinatesMark.svelte +11 -2
  34. package/dist/components/marks/ParallelCoordinatesMark.svelte.d.ts +4 -0
  35. package/dist/components/marks/RectMark.svelte +8 -3
  36. package/dist/components/marks/RectXMark.svelte +9 -3
  37. package/dist/components/marks/RectYMark.svelte +9 -3
  38. package/dist/components/marks/RuleMarkX.svelte +31 -25
  39. package/dist/components/marks/RuleMarkY.svelte +31 -25
  40. package/dist/components/marks/TextMark.svelte +52 -14
  41. package/dist/components/marks/lifecycle/MarkLifecycleV2.svelte.d.ts +22 -1
  42. package/dist/components/marks/lifecycle/MarkLifecycleV2.svelte.js +579 -66
  43. package/dist/components/marks/lifecycle/domain-queries.d.ts +13 -2
  44. package/dist/components/marks/lifecycle/domain-queries.js +43 -11
  45. package/dist/components/marks/lifecycle/domain-registration.d.ts +6 -1
  46. package/dist/components/marks/lifecycle/domain-registration.js +17 -20
  47. package/dist/components/marks/lifecycle/other-rows.svelte.d.ts +112 -0
  48. package/dist/components/marks/lifecycle/other-rows.svelte.js +193 -0
  49. package/dist/components/marks/lifecycle/other-split.d.ts +80 -0
  50. package/dist/components/marks/lifecycle/other-split.js +112 -0
  51. package/dist/components/marks/lifecycle/query-builder.d.ts +53 -3
  52. package/dist/components/marks/lifecycle/query-builder.js +46 -23
  53. package/dist/components/marks/utils/MarkColor.svelte.d.ts +3 -1
  54. package/dist/components/marks/utils/MarkColor.svelte.js +33 -3
  55. package/dist/components/marks/utils/MarkSeries.svelte.d.ts +14 -1
  56. package/dist/components/marks/utils/MarkSeries.svelte.js +25 -1
  57. package/dist/components/marks/utils/MarkSize.svelte.js +4 -0
  58. package/dist/components/marks/utils/reactive-config.d.ts +1 -1
  59. package/dist/components/marks/utils/reactive-config.js +9 -0
  60. package/dist/components/marks/utils/tooltip-helpers.d.ts +4 -1
  61. package/dist/components/marks/utils/tooltip-helpers.js +2 -1
  62. package/dist/components/rendering/Areas.svelte +57 -19
  63. package/dist/components/rendering/Areas.svelte.d.ts +12 -0
  64. package/dist/components/rendering/BaseGPURenderer.svelte.d.ts +3 -1
  65. package/dist/components/rendering/BaseGPURenderer.svelte.js +4 -2
  66. package/dist/components/rendering/Dots.svelte +23 -3
  67. package/dist/components/rendering/Dots.svelte.d.ts +6 -0
  68. package/dist/components/rendering/GridLine.svelte +13 -1
  69. package/dist/components/rendering/GridLine.svelte.d.ts +5 -0
  70. package/dist/components/rendering/Lines.svelte +48 -13
  71. package/dist/components/rendering/Lines.svelte.d.ts +12 -0
  72. package/dist/components/rendering/ParallelLines.svelte +5 -5
  73. package/dist/components/rendering/Rects.svelte +164 -30
  74. package/dist/components/rendering/Rects.svelte.d.ts +23 -2
  75. package/dist/components/rendering/RuleX.svelte +1 -1
  76. package/dist/components/rendering/RuleY.svelte +1 -1
  77. package/dist/components/rendering/WebGPUCanvas.svelte +4 -2
  78. package/dist/components/rendering/activity-reads.d.ts +47 -12
  79. package/dist/components/rendering/activity-reads.js +61 -19
  80. package/dist/components/rendering/uniform-sizes.d.ts +1 -1
  81. package/dist/components/rendering/uniform-sizes.js +1 -1
  82. package/dist/config/ChartConfig.svelte.d.ts +10 -1
  83. package/dist/config/ChartConfig.svelte.js +17 -0
  84. package/dist/gpu/GPUCoordinator.svelte.d.ts +31 -1
  85. package/dist/gpu/GPUCoordinator.svelte.js +164 -7
  86. package/dist/gpu/MorphedChannels.d.ts +11 -28
  87. package/dist/gpu/MorphedChannels.js +72 -8
  88. package/dist/gpu/ScaleTemplateSystem.d.ts +20 -2
  89. package/dist/gpu/ScaleTemplateSystem.js +83 -6
  90. package/dist/index.d.ts +19 -4
  91. package/dist/index.js +22 -4
  92. package/dist/interactors/Interval1D.svelte +6 -1
  93. package/dist/interactors/Interval2D.svelte +6 -1
  94. package/dist/interactors/ParallelAxisSource.svelte +10 -1
  95. package/dist/interactors/Toggle.svelte +32 -3
  96. package/dist/interactors/Tooltip.svelte +138 -41
  97. package/dist/interactors/TooltipManager.svelte.d.ts +20 -5
  98. package/dist/interactors/TooltipManager.svelte.js +46 -12
  99. package/dist/interactors/other-tooltip.d.ts +16 -0
  100. package/dist/interactors/other-tooltip.js +22 -0
  101. package/dist/query-engine/CrossfilterSelection.svelte.d.ts +4 -2
  102. package/dist/query-engine/CrossfilterSelection.svelte.js +2 -0
  103. package/dist/query-engine/FieldActivity.svelte.d.ts +49 -1
  104. package/dist/query-engine/FieldActivity.svelte.js +175 -5
  105. package/dist/query-engine/FilterGroup.svelte.d.ts +23 -1
  106. package/dist/query-engine/FilterGroup.svelte.js +43 -3
  107. package/dist/query-engine/FilterGroupRegistry.svelte.d.ts +31 -3
  108. package/dist/query-engine/FilterGroupRegistry.svelte.js +62 -15
  109. package/dist/query-engine/MarkHandle.svelte.d.ts +28 -1
  110. package/dist/query-engine/MarkHandle.svelte.js +33 -1
  111. package/dist/query-engine/QueryEngine.d.ts +225 -5
  112. package/dist/query-engine/QueryEngine.js +697 -47
  113. package/dist/query-engine/categorical-identity.d.ts +41 -0
  114. package/dist/query-engine/categorical-identity.js +145 -65
  115. package/dist/query-engine/debug/query-log.d.ts +39 -0
  116. package/dist/query-engine/debug/query-log.js +90 -0
  117. package/dist/query-engine/duckdb.d.ts +55 -8
  118. package/dist/query-engine/duckdb.js +82 -30
  119. package/dist/query-engine/index.d.ts +7 -4
  120. package/dist/query-engine/index.js +3 -3
  121. package/dist/query-engine/preagg/BuildQueue.d.ts +33 -13
  122. package/dist/query-engine/preagg/BuildQueue.js +51 -15
  123. package/dist/query-engine/preagg/Preagg.d.ts +46 -3
  124. package/dist/query-engine/preagg/Preagg.js +159 -31
  125. package/dist/query-engine/preagg/plan.d.ts +33 -1
  126. package/dist/query-engine/preagg/plan.js +158 -14
  127. package/dist/query-engine/sliced-lane.d.ts +108 -0
  128. package/dist/query-engine/sliced-lane.js +299 -0
  129. package/dist/query-engine/source-routing.d.ts +106 -0
  130. package/dist/query-engine/source-routing.js +189 -0
  131. package/dist/query-engine/toggle-predicate.d.ts +21 -0
  132. package/dist/query-engine/toggle-predicate.js +29 -1
  133. package/dist/scales/ActivityTable.d.ts +5 -1
  134. package/dist/scales/ActivityTable.js +12 -3
  135. package/dist/scales/BandScaleState.svelte.d.ts +22 -9
  136. package/dist/scales/BandScaleState.svelte.js +39 -21
  137. package/dist/scales/ChartScales.svelte.d.ts +3 -0
  138. package/dist/scales/ChartScales.svelte.js +26 -8
  139. package/dist/scales/ColorScaleState.svelte.d.ts +18 -2
  140. package/dist/scales/ColorScaleState.svelte.js +138 -26
  141. package/dist/scales/LinearScaleState.svelte.d.ts +7 -10
  142. package/dist/scales/LinearScaleState.svelte.js +21 -39
  143. package/dist/scales/ScaleGeneration.svelte.d.ts +48 -0
  144. package/dist/scales/ScaleGeneration.svelte.js +42 -0
  145. package/dist/scales/ScaleRegistry.svelte.js +3 -1
  146. package/dist/scales/SlotTransition.d.ts +15 -1
  147. package/dist/scales/SlotTransition.js +33 -4
  148. package/dist/scales/TimeScaleState.svelte.d.ts +7 -3
  149. package/dist/scales/TimeScaleState.svelte.js +7 -7
  150. package/dist/scales/color-utils.d.ts +0 -3
  151. package/dist/scales/color-utils.js +0 -3
  152. package/dist/scales/entered.d.ts +8 -0
  153. package/dist/scales/entered.js +10 -0
  154. package/dist/spec/LazyQueryEngine.js +2 -19
  155. package/dist/spec/MarkRegistry.d.ts +3 -1
  156. package/dist/spec/MarkRegistry.js +13 -1
  157. package/dist/spec/PlotFromSpec.svelte +1 -0
  158. package/dist/spec/export.js +11 -2
  159. package/dist/spec/types.d.ts +43 -13
  160. package/dist/sql/bin.d.ts +25 -0
  161. package/dist/sql/bin.js +108 -5
  162. package/dist/theme.css +5 -4
  163. package/dist/transforms/TransformCompiler.d.ts +47 -2
  164. package/dist/transforms/TransformCompiler.js +70 -1
  165. package/dist/transforms/index.js +176 -42
  166. package/dist/types.d.ts +177 -2
  167. package/package.json +4 -3
@@ -1,3 +1,4 @@
1
+ import type { OtherRowStats } from '../components/marks/lifecycle/other-rows.svelte';
1
2
  import type { QueryEngine } from '../query-engine/QueryEngine';
2
3
  import type { D3Scale, ScaleStateInstance } from '../types';
3
4
  import type { DimValue } from '../query-engine/categorical-identity';
@@ -50,6 +51,11 @@ interface TooltipMarkRegistration {
50
51
  };
51
52
  constantRadius?: number;
52
53
  displayFields?: string[] | null;
54
+ /**
55
+ * What an Other bar's point holds, for the tooltip to add its rows; read at
56
+ * display time, so a selection that changes under the pointer shows at once.
57
+ */
58
+ otherStats?: (point: DataRow) => OtherRowStats | null;
53
59
  proximityAxis: 'x' | 'y' | 'xy';
54
60
  enabled: boolean;
55
61
  }
@@ -83,11 +89,13 @@ export declare class TooltipManager {
83
89
  currentChannelFields: Record<string, string> | null;
84
90
  currentDisplayFields: string[] | null;
85
91
  visible: boolean;
86
- pointerScreenX: number;
87
- pointerScreenY: number;
92
+ pointerClientX: number;
93
+ pointerClientY: number;
88
94
  matchedRadius: number | null;
89
95
  get focusedPoint(): DataRow | null;
90
96
  get focusedMatch(): MatchedPoint | null;
97
+ /** The focused point's Other numbers, or null where it is not an Other bar. */
98
+ get focusedOtherStats(): OtherRowStats | null;
91
99
  get currentPoint(): DataRow | null;
92
100
  get currentColor(): string | null;
93
101
  get matchedScreenX(): number | null;
@@ -97,6 +105,12 @@ export declare class TooltipManager {
97
105
  * Update the cursor's data-space position. Called directly from pointer move handler.
98
106
  */
99
107
  updatePosition(xData: number | null, yData: number | null): void;
108
+ /**
109
+ * Run the hover query again at the last position, if the pointer is still
110
+ * in the chart: a mark's tooltip table was rewritten under a still pointer,
111
+ * so the row it shows is the old data's. The next frame runs it.
112
+ */
113
+ refresh(): void;
100
114
  /**
101
115
  * Cycle the focused point index by delta. Wraps around.
102
116
  * Remembers the focused category so it persists across cursor moves.
@@ -121,10 +135,11 @@ export declare class TooltipManager {
121
135
  */
122
136
  get currentMarkLOD(): MarkLODSurface | null;
123
137
  /**
124
- * Update raw screen position for tooltip positioning.
125
- * Called from chart container's pointer move handler.
138
+ * The pointer's viewport (client) position, where the tooltip is placed: it
139
+ * renders in the top layer, outside the chart's clipping ancestors. Called from
140
+ * the chart's pointer move handler.
126
141
  */
127
- updatePointerScreen(x: number, y: number): void;
142
+ updatePointerClient(x: number, y: number): void;
128
143
  /**
129
144
  * Called once per animation frame by GPUCoordinator's render loop.
130
145
  */
@@ -35,8 +35,8 @@ export class TooltipManager {
35
35
  currentChannelFields = $state(null);
36
36
  currentDisplayFields = $state(null);
37
37
  visible = $state(false);
38
- pointerScreenX = $state(0);
39
- pointerScreenY = $state(0);
38
+ pointerClientX = $state(0);
39
+ pointerClientY = $state(0);
40
40
  matchedRadius = $state(null);
41
41
  // Focused point getters — used by Tooltip and HoverIndicator
42
42
  get focusedPoint() {
@@ -45,6 +45,12 @@ export class TooltipManager {
45
45
  get focusedMatch() {
46
46
  return this.matchedPoints[this.focusedIndex] ?? null;
47
47
  }
48
+ /** The focused point's Other numbers, or null where it is not an Other bar. */
49
+ get focusedOtherStats() {
50
+ const point = this.focusedPoint;
51
+ const stats = this.currentMarkId ? this.#marks.get(this.currentMarkId)?.otherStats : null;
52
+ return point && stats ? stats(point) : null;
53
+ }
48
54
  // Backwards-compat getters
49
55
  get currentPoint() {
50
56
  return this.currentPoints[0] ?? null;
@@ -81,6 +87,15 @@ export class TooltipManager {
81
87
  this.#pendingY = yData;
82
88
  this.#hasPending = true;
83
89
  }
90
+ /**
91
+ * Run the hover query again at the last position, if the pointer is still
92
+ * in the chart: a mark's tooltip table was rewritten under a still pointer,
93
+ * so the row it shows is the old data's. The next frame runs it.
94
+ */
95
+ refresh() {
96
+ if (this.#cursorActive)
97
+ this.#hasPending = true;
98
+ }
84
99
  /**
85
100
  * Cycle the focused point index by delta. Wraps around.
86
101
  * Remembers the focused category so it persists across cursor moves.
@@ -129,12 +144,13 @@ export class TooltipManager {
129
144
  return this.#chartConfig.getMarkLOD(this.currentMarkId) ?? null;
130
145
  }
131
146
  /**
132
- * Update raw screen position for tooltip positioning.
133
- * Called from chart container's pointer move handler.
147
+ * The pointer's viewport (client) position, where the tooltip is placed: it
148
+ * renders in the top layer, outside the chart's clipping ancestors. Called from
149
+ * the chart's pointer move handler.
134
150
  */
135
- updatePointerScreen(x, y) {
136
- this.pointerScreenX = x;
137
- this.pointerScreenY = y;
151
+ updatePointerClient(x, y) {
152
+ this.pointerClientX = x;
153
+ this.pointerClientY = y;
138
154
  }
139
155
  // =========================================================================
140
156
  // QUERY SCHEDULING (driven by animation loop)
@@ -170,7 +186,16 @@ export class TooltipManager {
170
186
  if (!sql)
171
187
  continue;
172
188
  queryPromises.push(this.#queryEngine
173
- .query(sql, { callerId: 'tooltip' })
189
+ // Per mark: one shared caller id let execute-latest drop every
190
+ // lookup but the first and last when a chart has three or more
191
+ // marks. Not cached: the tooltip table is rewritten on each data
192
+ // change while a lookup's SQL can stay the same.
193
+ .query(sql, {
194
+ callerId: `tooltip:${markId}`,
195
+ cache: false,
196
+ // The tooltip table is TEMP, on the main connection.
197
+ connectionLocal: true
198
+ })
174
199
  .then((result) => {
175
200
  const rows = Array.from(result);
176
201
  return rows.length > 0 ? { markId, config, rows } : null;
@@ -484,10 +509,6 @@ export class TooltipManager {
484
509
  // =========================================================================
485
510
  // LABEL RESOLUTION
486
511
  // =========================================================================
487
- /**
488
- * Resolve band indices, time offsets, and color indices back to human-readable labels.
489
- * Renames columns from aliases (x, y, color) to original field names.
490
- */
491
512
  /** Resolve a single value: band index → label, time offset → formatted date. */
492
513
  #resolveValue(axis, key, value, config) {
493
514
  const { scaleTypes, bandDomains } = config;
@@ -518,6 +539,10 @@ export class TooltipManager {
518
539
  }
519
540
  return String(value ?? '');
520
541
  }
542
+ /**
543
+ * Resolve band indices, time offsets, and color indices back to human-readable labels.
544
+ * Renames columns from aliases (x, y, color) to original field names.
545
+ */
521
546
  #resolveLabels(row, config) {
522
547
  const { scaleTypes, channelFields, bandDomains, colorDomain, columns } = config;
523
548
  const point = {};
@@ -562,6 +587,12 @@ export class TooltipManager {
562
587
  let rangeInsertIndex = -1;
563
588
  for (let i = 0; i < entries.length; i++) {
564
589
  const [key, value] = entries[i];
590
+ // The facet cell's dim id, kept for a click (Toggle reads it to pick the
591
+ // clicked cell's Other); not displayed: `_` keys never are.
592
+ if (key === '__facet') {
593
+ point.__facetId = value;
594
+ continue;
595
+ }
565
596
  // Skip internal columns
566
597
  if (key.startsWith('_') || key.startsWith('__dplot_'))
567
598
  continue;
@@ -583,6 +614,9 @@ export class TooltipManager {
583
614
  // (Toggle selects it) rather than fall back to the raw id.
584
615
  const category = this.#bandScale(key)?.categoryAt(value);
585
616
  point[fieldName] = category === undefined ? value : category;
617
+ // The raw id beside it: a label can't name an Other bucket (several
618
+ // share one, and it is no value), so a click on Other reads this.
619
+ point[key === 'x' ? '__bandIdX' : '__bandIdY'] = value;
586
620
  continue;
587
621
  }
588
622
  // Resolve time offset to formatted datetime string
@@ -0,0 +1,16 @@
1
+ /**
2
+ * The rows an Other bar's tooltip adds: how many categories it holds, and,
3
+ * while a selection reaches into it, how much of it is selected.
4
+ */
5
+ import type { OtherRowStats } from '../components/marks/lifecycle/other-rows.svelte';
6
+ export interface OtherTooltipRow {
7
+ key: string;
8
+ value: string;
9
+ }
10
+ /**
11
+ * `format` writes a sum the way the chart writes its value axis. Rows:
12
+ * - categories: "41", or "3 of 41" while a selection is active;
13
+ * - selected: "1,234 of 5,678 (22%)", only while a selection is active.
14
+ * A share that rounds to 0 but is not 0 reads "<1%", so a sliver is not "0%".
15
+ */
16
+ export declare function otherTooltipRows(stats: OtherRowStats, format: (value: number) => string): OtherTooltipRow[];
@@ -0,0 +1,22 @@
1
+ /**
2
+ * The rows an Other bar's tooltip adds: how many categories it holds, and,
3
+ * while a selection reaches into it, how much of it is selected.
4
+ */
5
+ /**
6
+ * `format` writes a sum the way the chart writes its value axis. Rows:
7
+ * - categories: "41", or "3 of 41" while a selection is active;
8
+ * - selected: "1,234 of 5,678 (22%)", only while a selection is active.
9
+ * A share that rounds to 0 but is not 0 reads "<1%", so a sliver is not "0%".
10
+ */
11
+ export function otherTooltipRows(stats, format) {
12
+ const count = String(stats.memberCount);
13
+ if (!stats.active)
14
+ return [{ key: 'categories', value: count }];
15
+ const percent = stats.total === 0 ? 0 : (100 * stats.picked) / stats.total;
16
+ const rounded = Math.round(percent);
17
+ const share = rounded === 0 && stats.picked > 0 ? '<1%' : `${rounded}%`;
18
+ return [
19
+ { key: 'categories', value: `${stats.pickedCount} of ${count}` },
20
+ { key: 'selected', value: `${format(stats.picked)} of ${format(stats.total)} (${share})` }
21
+ ];
22
+ }
@@ -16,6 +16,7 @@
16
16
  * Both are computed when asked, from the clause, the mark's route and the
17
17
  * live registrations: nothing is cached, so nothing goes stale.
18
18
  */
19
+ import type { SelectionResponse } from '../components/rendering/activity-reads';
19
20
  import type { ExprNode } from '@uwdata/mosaic-sql';
20
21
  export type FilterExpr = ExprNode | string | boolean;
21
22
  export interface FilterSchema {
@@ -53,12 +54,13 @@ export interface Clause {
53
54
  * faceted chart's cells have ids of their own, but share one config.
54
55
  * `response` 'filter' makes every selection filter the mark, and a click
55
56
  * control's (a discrete registration's) in its own chart too; brushes still
56
- * skip their own chart.
57
+ * skip their own chart. 'ignore' makes no selection filter it, so no clause
58
+ * change re-queries it and preagg builds nothing for it.
57
59
  */
58
60
  export interface MarkRoute {
59
61
  readonly reads: readonly string[];
60
62
  readonly chart: object | null;
61
- readonly response?: 'auto' | 'filter';
63
+ readonly response?: SelectionResponse;
62
64
  }
63
65
  /**
64
66
  * What routing reads from one registration of a source: the chart it skips,
@@ -57,6 +57,8 @@ export class CrossfilterSelection {
57
57
  * no allocation, as it runs per mark per clause on every brush frame.
58
58
  */
59
59
  filters(route, sourceId, key) {
60
+ if (route.response === 'ignore')
61
+ return false;
60
62
  if (key !== null && route.reads.includes(key))
61
63
  return false;
62
64
  const chart = route.chart;
@@ -1,6 +1,21 @@
1
1
  import type { ClockLoop } from '../animation';
2
2
  import { ActivityTable } from '../scales/ActivityTable';
3
3
  import type { FieldIdentity } from './categorical-identity';
4
+ /** One mark's Other rows for one bucket: what a click on it selects. */
5
+ export interface OtherEntry {
6
+ /** The mark's filter group, or null when it is filtered by the selection (`'filter'`). */
7
+ group: number | null;
8
+ /** One per Other row: a facet cell (null without facets) and its folded ids. */
9
+ rows: {
10
+ facetId: number | null;
11
+ ids: ArrayLike<number>;
12
+ }[];
13
+ }
14
+ /** What a click on Other does to a selection: values to set, add or remove. */
15
+ export interface OtherClick {
16
+ action: 'replace' | 'add' | 'remove';
17
+ values: unknown[];
18
+ }
4
19
  /** What a FieldActivity needs from the page around it. */
5
20
  export interface FieldActivityHost {
6
21
  /** Drives the fades; null in unit tests and before a coordinator exists, and fades land at once. */
@@ -34,13 +49,28 @@ export declare const DETACHED_HOST: FieldActivityHost;
34
49
  * existing ids keep their fades while new ones start in their state.
35
50
  * `reconcile` runs from the packer and from `set`, never from a reader, so
36
51
  * nothing is resized or retargeted while a template renders.
52
+ *
53
+ * THE FREE SLOT. Each group's table holds one slot past the field's ids: 1 while
54
+ * the group selects nothing on the field, 0 while it selects anything, faded on
55
+ * the same clock as every id. The shader reads it as the last slot of the
56
+ * group's run (identityFreeActivity): an Other bar's unselected part dims by it,
57
+ * so clearing a selection fades it back in with the other bars, rather than its
58
+ * split sliding.
37
59
  */
38
60
  export declare class FieldActivity {
39
61
  #private;
40
62
  readonly key: string;
41
63
  constructor(key: string, host?: FieldActivityHost);
42
- /** How many ids the tables cover: the applied identity's, 0 without one. */
64
+ /**
65
+ * How many slots the tables cover: one per id of the applied identity, and
66
+ * one past them (FREE_SLOT); 0 without an identity.
67
+ */
43
68
  get count(): number;
69
+ /**
70
+ * Group `index`'s selected values, or null when it selects nothing on this
71
+ * field. Reactive: a reader re-runs when the selection changes.
72
+ */
73
+ selectedValues(index: number): ReadonlySet<unknown> | null;
44
74
  /** Group `index`'s fade, or null when that group never selected on this field. */
45
75
  fadeOf(index: number): ActivityTable | null;
46
76
  /**
@@ -76,4 +106,22 @@ export declare class FieldActivity {
76
106
  * live: a label is never dimmed on a guess.
77
107
  */
78
108
  opacityOfId(index: number, id: number, inactiveOpacity?: number): number;
109
+ /** Register `markId`'s Other rows under `otherId`; null removes them. */
110
+ setOther(otherId: number, markId: string, entry: OtherEntry | null): void;
111
+ /**
112
+ * What a click on Other `otherId` does to group `group`'s selection, given
113
+ * the values it holds now (`current`), and records it. Null when nothing is
114
+ * registered under the id (no result yet) or it holds no members.
115
+ *
116
+ * - The members: every registered row under the id, or only facet cell
117
+ * `facetId`'s when the click came from a cell.
118
+ * - Shift-click adds them, or takes back what this Other added before if it
119
+ * is still selected.
120
+ * - A plain click on an Other whose marks all filter by the selection (no
121
+ * group: `'filter'`) drills down, replacing the selection: such an Other is
122
+ * computed inside the selection, so a second click means "the rest of it".
123
+ * - Otherwise a plain click toggles: it takes back what this Other added if
124
+ * that is still selected, and replaces the selection with the members if not.
125
+ */
126
+ otherClick(group: number, otherId: number, facetId: number | null, current: ReadonlySet<unknown>, multi: boolean): OtherClick | null;
79
127
  }
@@ -44,6 +44,13 @@ class GroupFade {
44
44
  * existing ids keep their fades while new ones start in their state.
45
45
  * `reconcile` runs from the packer and from `set`, never from a reader, so
46
46
  * nothing is resized or retargeted while a template renders.
47
+ *
48
+ * THE FREE SLOT. Each group's table holds one slot past the field's ids: 1 while
49
+ * the group selects nothing on the field, 0 while it selects anything, faded on
50
+ * the same clock as every id. The shader reads it as the last slot of the
51
+ * group's run (identityFreeActivity): an Other bar's unselected part dims by it,
52
+ * so clearing a selection fades it back in with the other bars, rather than its
53
+ * split sliding.
47
54
  */
48
55
  export class FieldActivity {
49
56
  key;
@@ -63,9 +70,20 @@ export class FieldActivity {
63
70
  this.key = key;
64
71
  this.#host = host;
65
72
  }
66
- /** How many ids the tables cover: the applied identity's, 0 without one. */
73
+ /**
74
+ * How many slots the tables cover: one per id of the applied identity, and
75
+ * one past them (FREE_SLOT); 0 without an identity.
76
+ */
67
77
  get count() {
68
- return this.#identity ? this.#identity.values.length : 0;
78
+ return this.#identity ? this.#identity.values.length + 1 : 0;
79
+ }
80
+ /**
81
+ * Group `index`'s selected values, or null when it selects nothing on this
82
+ * field. Reactive: a reader re-runs when the selection changes.
83
+ */
84
+ selectedValues(index) {
85
+ void this.#groupsVersion;
86
+ return this.#groups[index]?.values ?? null;
69
87
  }
70
88
  /** Group `index`'s fade, or null when that group never selected on this field. */
71
89
  fadeOf(index) {
@@ -164,6 +182,113 @@ export class FieldActivity {
164
182
  }
165
183
  return values.has(identity.values[id]) ? 1 : inactiveOpacity;
166
184
  }
185
+ // -----------------------------------------------------------------
186
+ // Other buckets: what each one holds, for a click to select
187
+ // -----------------------------------------------------------------
188
+ /**
189
+ * Every Other bucket's members, by its id and the mark that drew it. Each
190
+ * mark registers its Other rows per result (`setOther`): the folded ids its
191
+ * query returned, per facet cell. A click on Other reads them, so what it
192
+ * selects is exactly what the bar summed.
193
+ */
194
+ #others = new Map();
195
+ /**
196
+ * What an Other click contributed to a group's selection, by `group:otherId`:
197
+ * how a second click on the same Other knows what to take back. Dropped with
198
+ * the id's last entry, so a reused id starts with no memory.
199
+ */
200
+ #otherMemory = new Map();
201
+ /** Register `markId`'s Other rows under `otherId`; null removes them. */
202
+ setOther(otherId, markId, entry) {
203
+ let byMark = this.#others.get(otherId);
204
+ const before = byMark?.get(markId)?.group;
205
+ if (entry) {
206
+ if (!byMark)
207
+ this.#others.set(otherId, (byMark = new Map()));
208
+ byMark.set(markId, entry);
209
+ }
210
+ else {
211
+ if (!byMark?.delete(markId))
212
+ return;
213
+ if (byMark.size === 0) {
214
+ this.#others.delete(otherId);
215
+ for (const memKey of [...this.#otherMemory.keys()]) {
216
+ if (memKey.endsWith(`:${otherId}`))
217
+ this.#otherMemory.delete(memKey);
218
+ }
219
+ }
220
+ }
221
+ // The bucket's members moved (a new result), or it came or went: whether it
222
+ // is lit may have changed in the group its marks dim under. A fade it does
223
+ // not change costs nothing (#retarget returns early).
224
+ this.#reapply(before);
225
+ if (entry && entry.group !== before)
226
+ this.#reapply(entry.group);
227
+ }
228
+ #reapply(index) {
229
+ if (index == null)
230
+ return;
231
+ const group = this.#groups[index];
232
+ if (group && group.values != null)
233
+ this.#apply(group);
234
+ }
235
+ /**
236
+ * What a click on Other `otherId` does to group `group`'s selection, given
237
+ * the values it holds now (`current`), and records it. Null when nothing is
238
+ * registered under the id (no result yet) or it holds no members.
239
+ *
240
+ * - The members: every registered row under the id, or only facet cell
241
+ * `facetId`'s when the click came from a cell.
242
+ * - Shift-click adds them, or takes back what this Other added before if it
243
+ * is still selected.
244
+ * - A plain click on an Other whose marks all filter by the selection (no
245
+ * group: `'filter'`) drills down, replacing the selection: such an Other is
246
+ * computed inside the selection, so a second click means "the rest of it".
247
+ * - Otherwise a plain click toggles: it takes back what this Other added if
248
+ * that is still selected, and replaces the selection with the members if not.
249
+ */
250
+ otherClick(group, otherId, facetId, current, multi) {
251
+ const entries = this.#others.get(otherId);
252
+ const identity = this.#host.identityOf(this.key);
253
+ if (!entries || entries.size === 0 || !identity)
254
+ return null;
255
+ const members = new Set();
256
+ let drill = true;
257
+ for (const entry of entries.values()) {
258
+ if (entry.group !== null)
259
+ drill = false;
260
+ for (const row of entry.rows) {
261
+ if (facetId !== null && row.facetId !== facetId)
262
+ continue;
263
+ for (let i = 0; i < row.ids.length; i++) {
264
+ const id = row.ids[i];
265
+ if (identity.synthetic.has(id))
266
+ continue;
267
+ const value = identity.values[id];
268
+ if (value !== undefined)
269
+ members.add(value);
270
+ }
271
+ }
272
+ }
273
+ if (members.size === 0)
274
+ return null;
275
+ const memKey = `${group}:${otherId}`;
276
+ const before = this.#otherMemory.get(memKey);
277
+ const stillSelected = !!before && before.size > 0 && [...before].every((v) => current.has(v));
278
+ if (stillSelected && (multi || !drill)) {
279
+ this.#otherMemory.delete(memKey);
280
+ return { action: 'remove', values: [...before] };
281
+ }
282
+ if (!multi) {
283
+ // A replace: what any other Other added is gone from the selection.
284
+ for (const key of [...this.#otherMemory.keys()]) {
285
+ if (key.startsWith(`${group}:`))
286
+ this.#otherMemory.delete(key);
287
+ }
288
+ }
289
+ this.#otherMemory.set(memKey, members);
290
+ return { action: multi ? 'add' : 'replace', values: [...members] };
291
+ }
167
292
  #group(index) {
168
293
  let group = this.#groups[index];
169
294
  if (!group) {
@@ -187,10 +312,13 @@ export class FieldActivity {
187
312
  group.table.resize(0);
188
313
  return;
189
314
  }
190
- const count = identity.values.length;
315
+ // Every id, and the free slot past them: clear() sets it to 1 and
316
+ // setActive() leaves it out, so it fades to 0 while anything is selected
317
+ // and back to 1 once nothing is (see FieldActivity's FREE_SLOT note).
318
+ const count = identity.values.length + 1;
191
319
  const values = group.values;
192
320
  if (values == null) {
193
- group.table.resize(count, null);
321
+ group.table.resize(count, null, true);
194
322
  group.table.clear();
195
323
  return;
196
324
  }
@@ -201,7 +329,49 @@ export class FieldActivity {
201
329
  if (id != null)
202
330
  ids.push(id);
203
331
  }
204
- group.table.resize(count, ids);
332
+ this.#lightOthers(this.#groups.indexOf(group), ids, count);
333
+ group.table.resize(count, ids, true);
205
334
  group.table.setActive(ids);
206
335
  }
336
+ /**
337
+ * Add to `ids` (the selected ids) every Other bucket that holds one of them,
338
+ * by the members its marks in group `index` registered: a bucket is lit while
339
+ * the selection reaches into it. In the same pass as the rest of the group's
340
+ * fade, so it is one retarget per update, never two. Marks filtered by the
341
+ * selection (no group) are skipped: they never dim.
342
+ */
343
+ #lightOthers(index, ids, count) {
344
+ if (this.#others.size === 0 || index < 0)
345
+ return;
346
+ let mask = this.#selectedMask;
347
+ if (mask.length < count)
348
+ mask = this.#selectedMask = new Uint8Array(count);
349
+ const selected = ids.length;
350
+ for (let i = 0; i < selected; i++)
351
+ mask[ids[i]] = 1;
352
+ for (const [otherId, entries] of this.#others) {
353
+ if (otherId >= count)
354
+ continue;
355
+ let lit = false;
356
+ for (const entry of entries.values()) {
357
+ if (entry.group !== index)
358
+ continue;
359
+ for (const row of entry.rows) {
360
+ for (let i = 0; i < row.ids.length && !lit; i++)
361
+ lit = mask[row.ids[i]] === 1;
362
+ if (lit)
363
+ break;
364
+ }
365
+ if (lit)
366
+ break;
367
+ }
368
+ if (lit)
369
+ ids.push(otherId);
370
+ }
371
+ // Unmark only what was marked: the selected ids, not the Others pushed after.
372
+ for (let i = 0; i < selected; i++)
373
+ mask[ids[i]] = 0;
374
+ }
375
+ /** Reused by #lightOthers: 1 at each selected id, 0 elsewhere between calls. */
376
+ #selectedMask = new Uint8Array(0);
207
377
  }
@@ -52,6 +52,16 @@ export interface SourceOpts extends SourceRegistration {
52
52
  * once per write and stored on the clause.
53
53
  */
54
54
  augment?: (value: unknown) => FilterSchema | null;
55
+ /**
56
+ * The source's clause was removed by someone other than this registration:
57
+ * `clearAll`, a `clearSource` from outside (a filter pill's remove button), or
58
+ * another registration under the same id clearing it. A control that keeps what
59
+ * it drew (a brush's rectangle) resets it here, because it reads no clause. Not
60
+ * called for the registration's own `clear()` or `destroy()`, which it initiated;
61
+ * and a control that clears itself from here does not loop, since the clause is
62
+ * already gone.
63
+ */
64
+ onCleared?: () => void;
55
65
  }
56
66
  export interface SourceHandle {
57
67
  /** Write the source's clause. */
@@ -81,6 +91,13 @@ export interface ToggleHandle extends Omit<SourceHandle, 'update'> {
81
91
  toggle(tuple: unknown[], multi: boolean, total?: number): void;
82
92
  /** Select exactly these tuples; empty clears. */
83
93
  set(tuples: unknown[][]): void;
94
+ /**
95
+ * A click on Other bucket `otherId` (from facet cell `facetId`, or null):
96
+ * selects, adds or takes back the categories it holds, as its field's
97
+ * registry says (FieldActivity.otherClick). A one-field toggle only: a
98
+ * bucket names no value in a tuple of several.
99
+ */
100
+ toggleOther(otherId: number, facetId: number | null, multi: boolean): void;
84
101
  }
85
102
  export declare class FilterGroup {
86
103
  #private;
@@ -136,7 +153,12 @@ export declare class FilterGroup {
136
153
  /**
137
154
  * Remove a source's clause. Re-queries only when there was a clause to
138
155
  * remove: an unmount or a clear with nothing selected changes no mark's data.
156
+ *
157
+ * The source's registrations are told (`onCleared`) so a control that drew the
158
+ * selection resets it, all of them from outside, and all but `by` when a
159
+ * registration is the one clearing (it has reset itself).
139
160
  */
140
- clearSource(sourceId: string): void;
161
+ clearSource(sourceId: string, by?: SourceOpts): void;
162
+ /** Remove every clause, and tell the controls that drew them (`onCleared`). */
141
163
  clearAll(): void;
142
164
  }