@fjader/svelte 0.2.3 → 0.2.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -1
- package/dist/animation/EntranceGate.d.ts +52 -0
- package/dist/animation/EntranceGate.js +93 -0
- package/dist/animation/TransitionClock.d.ts +61 -30
- package/dist/animation/TransitionClock.js +67 -29
- package/dist/animation/transition-log.d.ts +41 -0
- package/dist/animation/transition-log.js +57 -0
- package/dist/components/axes/AxisBottom.svelte +27 -8
- package/dist/components/axes/AxisBottom.svelte.d.ts +10 -0
- package/dist/components/axes/AxisCenter.svelte +4 -2
- package/dist/components/axes/AxisLeft.svelte +81 -10
- package/dist/components/axes/AxisLeft.svelte.d.ts +17 -0
- package/dist/components/axes/BandAxisTicks.svelte +4 -3
- package/dist/components/axes/axis-title.d.ts +30 -0
- package/dist/components/axes/axis-title.js +77 -0
- package/dist/components/charts/ChartCell.svelte +33 -6
- package/dist/components/charts/ChartCell.svelte.d.ts +9 -0
- package/dist/components/charts/ChartContainer.svelte +55 -5
- package/dist/components/charts/ChartContainer.svelte.d.ts +14 -1
- package/dist/components/charts/ChartHover.svelte.d.ts +14 -0
- package/dist/components/charts/ChartHover.svelte.js +22 -0
- package/dist/components/charts/ChartVisibility.svelte.d.ts +43 -0
- package/dist/components/charts/ChartVisibility.svelte.js +67 -0
- package/dist/components/guides/Grid.svelte +12 -1
- package/dist/components/guides/legend/ContinuousColorLegend.svelte +7 -1
- package/dist/components/guides/legend/OrdinalColorLegend.svelte +8 -5
- package/dist/components/marks/AreaMarkX.svelte +18 -4
- package/dist/components/marks/AreaMarkY.svelte +18 -4
- package/dist/components/marks/BarMarkX.svelte +20 -4
- package/dist/components/marks/BarMarkY.svelte +20 -4
- package/dist/components/marks/DotsMark.svelte +8 -3
- package/dist/components/marks/LineMark.svelte +13 -6
- package/dist/components/marks/ParallelCoordinatesMark.svelte +11 -2
- package/dist/components/marks/ParallelCoordinatesMark.svelte.d.ts +4 -0
- package/dist/components/marks/RectMark.svelte +8 -3
- package/dist/components/marks/RectXMark.svelte +9 -3
- package/dist/components/marks/RectYMark.svelte +9 -3
- package/dist/components/marks/RuleMarkX.svelte +31 -25
- package/dist/components/marks/RuleMarkY.svelte +31 -25
- package/dist/components/marks/TextMark.svelte +52 -14
- package/dist/components/marks/lifecycle/MarkLifecycleV2.svelte.d.ts +22 -1
- package/dist/components/marks/lifecycle/MarkLifecycleV2.svelte.js +579 -66
- package/dist/components/marks/lifecycle/domain-queries.d.ts +13 -2
- package/dist/components/marks/lifecycle/domain-queries.js +43 -11
- package/dist/components/marks/lifecycle/domain-registration.d.ts +6 -1
- package/dist/components/marks/lifecycle/domain-registration.js +17 -20
- package/dist/components/marks/lifecycle/other-rows.svelte.d.ts +112 -0
- package/dist/components/marks/lifecycle/other-rows.svelte.js +193 -0
- package/dist/components/marks/lifecycle/other-split.d.ts +80 -0
- package/dist/components/marks/lifecycle/other-split.js +112 -0
- package/dist/components/marks/lifecycle/query-builder.d.ts +53 -3
- package/dist/components/marks/lifecycle/query-builder.js +46 -23
- package/dist/components/marks/utils/MarkColor.svelte.d.ts +3 -1
- package/dist/components/marks/utils/MarkColor.svelte.js +33 -3
- package/dist/components/marks/utils/MarkSeries.svelte.d.ts +14 -1
- package/dist/components/marks/utils/MarkSeries.svelte.js +25 -1
- package/dist/components/marks/utils/MarkSize.svelte.js +4 -0
- package/dist/components/marks/utils/reactive-config.d.ts +1 -1
- package/dist/components/marks/utils/reactive-config.js +9 -0
- package/dist/components/marks/utils/tooltip-helpers.d.ts +4 -1
- package/dist/components/marks/utils/tooltip-helpers.js +2 -1
- package/dist/components/rendering/Areas.svelte +57 -19
- package/dist/components/rendering/Areas.svelte.d.ts +12 -0
- package/dist/components/rendering/BaseGPURenderer.svelte.d.ts +3 -1
- package/dist/components/rendering/BaseGPURenderer.svelte.js +4 -2
- package/dist/components/rendering/Dots.svelte +23 -3
- package/dist/components/rendering/Dots.svelte.d.ts +6 -0
- package/dist/components/rendering/GridLine.svelte +13 -1
- package/dist/components/rendering/GridLine.svelte.d.ts +5 -0
- package/dist/components/rendering/Lines.svelte +48 -13
- package/dist/components/rendering/Lines.svelte.d.ts +12 -0
- package/dist/components/rendering/ParallelLines.svelte +5 -5
- package/dist/components/rendering/Rects.svelte +164 -30
- package/dist/components/rendering/Rects.svelte.d.ts +23 -2
- package/dist/components/rendering/RuleX.svelte +1 -1
- package/dist/components/rendering/RuleY.svelte +1 -1
- package/dist/components/rendering/WebGPUCanvas.svelte +4 -2
- package/dist/components/rendering/activity-reads.d.ts +47 -12
- package/dist/components/rendering/activity-reads.js +61 -19
- package/dist/components/rendering/uniform-sizes.d.ts +1 -1
- package/dist/components/rendering/uniform-sizes.js +1 -1
- package/dist/config/ChartConfig.svelte.d.ts +10 -1
- package/dist/config/ChartConfig.svelte.js +17 -0
- package/dist/gpu/GPUCoordinator.svelte.d.ts +31 -1
- package/dist/gpu/GPUCoordinator.svelte.js +164 -7
- package/dist/gpu/MorphedChannels.d.ts +11 -28
- package/dist/gpu/MorphedChannels.js +72 -8
- package/dist/gpu/ScaleTemplateSystem.d.ts +20 -2
- package/dist/gpu/ScaleTemplateSystem.js +83 -6
- package/dist/index.d.ts +19 -4
- package/dist/index.js +22 -4
- package/dist/interactors/Interval1D.svelte +6 -1
- package/dist/interactors/Interval2D.svelte +6 -1
- package/dist/interactors/ParallelAxisSource.svelte +10 -1
- package/dist/interactors/Toggle.svelte +32 -3
- package/dist/interactors/Tooltip.svelte +138 -41
- package/dist/interactors/TooltipManager.svelte.d.ts +20 -5
- package/dist/interactors/TooltipManager.svelte.js +46 -12
- package/dist/interactors/other-tooltip.d.ts +16 -0
- package/dist/interactors/other-tooltip.js +22 -0
- package/dist/query-engine/CrossfilterSelection.svelte.d.ts +4 -2
- package/dist/query-engine/CrossfilterSelection.svelte.js +2 -0
- package/dist/query-engine/FieldActivity.svelte.d.ts +49 -1
- package/dist/query-engine/FieldActivity.svelte.js +175 -5
- package/dist/query-engine/FilterGroup.svelte.d.ts +23 -1
- package/dist/query-engine/FilterGroup.svelte.js +43 -3
- package/dist/query-engine/FilterGroupRegistry.svelte.d.ts +31 -3
- package/dist/query-engine/FilterGroupRegistry.svelte.js +62 -15
- package/dist/query-engine/MarkHandle.svelte.d.ts +28 -1
- package/dist/query-engine/MarkHandle.svelte.js +33 -1
- package/dist/query-engine/QueryEngine.d.ts +225 -5
- package/dist/query-engine/QueryEngine.js +697 -47
- package/dist/query-engine/categorical-identity.d.ts +41 -0
- package/dist/query-engine/categorical-identity.js +145 -65
- package/dist/query-engine/debug/query-log.d.ts +39 -0
- package/dist/query-engine/debug/query-log.js +90 -0
- package/dist/query-engine/duckdb.d.ts +55 -8
- package/dist/query-engine/duckdb.js +82 -30
- package/dist/query-engine/index.d.ts +7 -4
- package/dist/query-engine/index.js +3 -3
- package/dist/query-engine/preagg/BuildQueue.d.ts +33 -13
- package/dist/query-engine/preagg/BuildQueue.js +51 -15
- package/dist/query-engine/preagg/Preagg.d.ts +46 -3
- package/dist/query-engine/preagg/Preagg.js +159 -31
- package/dist/query-engine/preagg/plan.d.ts +33 -1
- package/dist/query-engine/preagg/plan.js +158 -14
- package/dist/query-engine/sliced-lane.d.ts +108 -0
- package/dist/query-engine/sliced-lane.js +299 -0
- package/dist/query-engine/source-routing.d.ts +106 -0
- package/dist/query-engine/source-routing.js +189 -0
- package/dist/query-engine/toggle-predicate.d.ts +21 -0
- package/dist/query-engine/toggle-predicate.js +29 -1
- package/dist/scales/ActivityTable.d.ts +5 -1
- package/dist/scales/ActivityTable.js +12 -3
- package/dist/scales/BandScaleState.svelte.d.ts +22 -9
- package/dist/scales/BandScaleState.svelte.js +39 -21
- package/dist/scales/ChartScales.svelte.d.ts +3 -0
- package/dist/scales/ChartScales.svelte.js +26 -8
- package/dist/scales/ColorScaleState.svelte.d.ts +18 -2
- package/dist/scales/ColorScaleState.svelte.js +138 -26
- package/dist/scales/LinearScaleState.svelte.d.ts +7 -10
- package/dist/scales/LinearScaleState.svelte.js +21 -39
- package/dist/scales/ScaleGeneration.svelte.d.ts +48 -0
- package/dist/scales/ScaleGeneration.svelte.js +42 -0
- package/dist/scales/ScaleRegistry.svelte.js +3 -1
- package/dist/scales/SlotTransition.d.ts +15 -1
- package/dist/scales/SlotTransition.js +33 -4
- package/dist/scales/TimeScaleState.svelte.d.ts +7 -3
- package/dist/scales/TimeScaleState.svelte.js +7 -7
- package/dist/scales/color-utils.d.ts +0 -3
- package/dist/scales/color-utils.js +0 -3
- package/dist/scales/entered.d.ts +8 -0
- package/dist/scales/entered.js +10 -0
- package/dist/spec/LazyQueryEngine.js +2 -19
- package/dist/spec/MarkRegistry.d.ts +3 -1
- package/dist/spec/MarkRegistry.js +13 -1
- package/dist/spec/PlotFromSpec.svelte +1 -0
- package/dist/spec/export.js +11 -2
- package/dist/spec/types.d.ts +43 -13
- package/dist/sql/bin.d.ts +25 -0
- package/dist/sql/bin.js +108 -5
- package/dist/theme.css +5 -4
- package/dist/transforms/TransformCompiler.d.ts +47 -2
- package/dist/transforms/TransformCompiler.js +70 -1
- package/dist/transforms/index.js +176 -42
- package/dist/types.d.ts +177 -2
- package/package.json +4 -3
|
@@ -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
|
-
|
|
87
|
-
|
|
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
|
-
*
|
|
125
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
39
|
-
|
|
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
|
-
*
|
|
133
|
-
*
|
|
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
|
-
|
|
136
|
-
this.
|
|
137
|
-
this.
|
|
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
|
-
|
|
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?:
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|