@fjader/svelte 0.2.2 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/axes/BandAxisTicks.svelte +63 -24
- package/dist/components/axes/BandAxisTicks.svelte.d.ts +5 -5
- package/dist/components/charts/ChartContainer.svelte +2 -1
- package/dist/components/guides/legend/ContinuousColorLegend.svelte +2 -1
- package/dist/components/guides/legend/OrdinalColorLegend.svelte +92 -34
- package/dist/components/marks/AreaMarkX.svelte +19 -6
- package/dist/components/marks/AreaMarkY.svelte +19 -6
- package/dist/components/marks/BarMarkX.svelte +3 -0
- package/dist/components/marks/BarMarkY.svelte +3 -0
- package/dist/components/marks/DotsMark.svelte +3 -0
- package/dist/components/marks/LineMark.svelte +3 -0
- package/dist/components/marks/ParallelCoordinatesMark.svelte +6 -0
- package/dist/components/marks/ParallelCoordinatesMark.svelte.d.ts +3 -0
- package/dist/components/marks/RectMark.svelte +3 -0
- package/dist/components/marks/RectXMark.svelte +3 -0
- package/dist/components/marks/RectYMark.svelte +3 -0
- package/dist/components/marks/RuleMarkX.svelte +10 -2
- package/dist/components/marks/RuleMarkY.svelte +10 -2
- package/dist/components/marks/TextMark.svelte +68 -4
- package/dist/components/marks/lifecycle/MarkLifecycleV2.svelte.d.ts +15 -1
- package/dist/components/marks/lifecycle/MarkLifecycleV2.svelte.js +116 -27
- package/dist/components/marks/lifecycle/domain-queries.js +13 -11
- package/dist/components/marks/lifecycle/transform-scale-key.d.ts +16 -0
- package/dist/components/marks/lifecycle/transform-scale-key.js +27 -0
- package/dist/components/marks/utils/area-outline.d.ts +24 -0
- package/dist/components/marks/utils/area-outline.js +10 -0
- package/dist/components/marks/utils/reactive-config.d.ts +1 -1
- package/dist/components/marks/utils/reactive-config.js +3 -0
- package/dist/components/rendering/Areas.svelte +30 -6
- package/dist/components/rendering/Areas.svelte.d.ts +2 -0
- package/dist/components/rendering/BaseGPURenderer.svelte.js +4 -1
- package/dist/components/rendering/Dots.svelte +24 -3
- package/dist/components/rendering/Dots.svelte.d.ts +2 -0
- package/dist/components/rendering/GridLine.svelte +2 -1
- package/dist/components/rendering/Lines.svelte +49 -7
- package/dist/components/rendering/Lines.svelte.d.ts +9 -0
- package/dist/components/rendering/ParallelLines.svelte +26 -7
- package/dist/components/rendering/ParallelLines.svelte.d.ts +2 -0
- package/dist/components/rendering/Rects.svelte +20 -5
- package/dist/components/rendering/Rects.svelte.d.ts +2 -0
- package/dist/components/rendering/RuleX.svelte +20 -5
- package/dist/components/rendering/RuleX.svelte.d.ts +2 -0
- package/dist/components/rendering/RuleY.svelte +20 -5
- package/dist/components/rendering/RuleY.svelte.d.ts +2 -0
- package/dist/components/rendering/activity-reads.d.ts +93 -0
- package/dist/components/rendering/activity-reads.js +135 -0
- package/dist/components/rendering/uniform-sizes.d.ts +26 -0
- package/dist/components/rendering/uniform-sizes.js +28 -0
- package/dist/config/ChartConfig.svelte.d.ts +2 -0
- package/dist/gpu/ColorScaleShaderLib.d.ts +13 -1
- package/dist/gpu/ColorScaleShaderLib.js +56 -5
- package/dist/gpu/GPUCoordinator.svelte.d.ts +8 -0
- package/dist/gpu/GPUCoordinator.svelte.js +68 -32
- package/dist/gpu/ResolveShader.d.ts +2 -2
- package/dist/gpu/ResolveShader.js +2 -2
- package/dist/gpu/ScaleTemplateSystem.d.ts +19 -0
- package/dist/gpu/ScaleTemplateSystem.js +86 -7
- package/dist/gpu/SizeScaleShaderLib.d.ts +4 -4
- package/dist/gpu/SizeScaleShaderLib.js +4 -4
- package/dist/gpu/identity-slot-packing.d.ts +157 -40
- package/dist/gpu/identity-slot-packing.js +262 -62
- package/dist/gpu/tracked-scales.d.ts +14 -0
- package/dist/gpu/tracked-scales.js +18 -0
- package/dist/index.js +1 -0
- package/dist/interactors/BandLabelToggle.svelte +5 -9
- package/dist/interactors/BandLabelToggle.svelte.d.ts +3 -3
- package/dist/interactors/DerivedInterval1D.svelte +26 -54
- package/dist/interactors/Interval1D.svelte +2 -1
- package/dist/interactors/Interval2D.svelte +2 -1
- package/dist/interactors/ParallelAxisSource.svelte +4 -4
- package/dist/interactors/ParallelAxisSource.svelte.d.ts +2 -1
- package/dist/interactors/ParallelBrushes.svelte +8 -9
- package/dist/interactors/ParallelBrushes.svelte.d.ts +3 -3
- package/dist/interactors/Toggle.svelte +17 -36
- package/dist/interactors/Toggle.svelte.d.ts +4 -3
- package/dist/interactors/Tooltip.svelte +2 -1
- package/dist/interactors/TooltipManager.svelte.js +8 -3
- package/dist/interactors/relay-clauses.d.ts +23 -0
- package/dist/interactors/relay-clauses.js +60 -0
- package/dist/query-engine/CrossfilterSelection.svelte.d.ts +88 -15
- package/dist/query-engine/CrossfilterSelection.svelte.js +94 -20
- package/dist/query-engine/FieldActivity.svelte.d.ts +79 -0
- package/dist/query-engine/FieldActivity.svelte.js +207 -0
- package/dist/query-engine/FilterGroup.svelte.d.ts +53 -29
- package/dist/query-engine/FilterGroup.svelte.js +181 -122
- package/dist/query-engine/FilterGroupRegistry.svelte.d.ts +42 -0
- package/dist/query-engine/FilterGroupRegistry.svelte.js +103 -1
- package/dist/query-engine/MarkHandle.svelte.d.ts +9 -1
- package/dist/query-engine/MarkHandle.svelte.js +12 -2
- package/dist/query-engine/QueryEngine.d.ts +52 -16
- package/dist/query-engine/QueryEngine.js +90 -28
- package/dist/query-engine/categorical-identity.d.ts +99 -27
- package/dist/query-engine/categorical-identity.js +210 -51
- package/dist/query-engine/index.d.ts +2 -1
- package/dist/query-engine/index.js +1 -0
- package/dist/query-engine/preagg/Preagg.d.ts +1 -1
- package/dist/query-engine/preagg/Preagg.js +11 -9
- package/dist/query-engine/preagg/plan.d.ts +9 -5
- package/dist/query-engine/preagg/plan.js +13 -11
- package/dist/query-engine/toggle-predicate.js +12 -1
- package/dist/scales/ActivityTable.d.ts +99 -0
- package/dist/scales/ActivityTable.js +211 -0
- package/dist/scales/BandScaleState.svelte.d.ts +28 -7
- package/dist/scales/BandScaleState.svelte.js +55 -20
- package/dist/scales/ChartScales.svelte.d.ts +2 -2
- package/dist/scales/ColorScaleState.svelte.d.ts +36 -8
- package/dist/scales/ColorScaleState.svelte.js +118 -24
- package/dist/scales/DomainResolver.d.ts +2 -0
- package/dist/scales/DomainResolver.js +15 -8
- package/dist/scales/ScaleRegistry.svelte.d.ts +9 -0
- package/dist/scales/ScaleRegistry.svelte.js +22 -1
- package/dist/scales/TimeScaleState.svelte.js +7 -1
- package/dist/spec/index.js +1 -0
- package/dist/spec/types.d.ts +2 -0
- package/dist/transforms/index.d.ts +27 -0
- package/dist/transforms/index.js +97 -3
- package/dist/types.d.ts +30 -0
- package/package.json +1 -1
|
@@ -2,44 +2,110 @@
|
|
|
2
2
|
* CrossfilterSelection — Svelte 5-native crossfilter predicate engine.
|
|
3
3
|
*
|
|
4
4
|
* Replaces Mosaic's Selection.intersect({ cross: true }) with a reactive
|
|
5
|
-
* $state Map
|
|
6
|
-
*
|
|
5
|
+
* $state Map of clauses, plus the registrations of the sources that write
|
|
6
|
+
* them. Marks derive their filter via predicate(route): every clause that
|
|
7
|
+
* filters the mark.
|
|
7
8
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
9
|
+
* WHICH CLAUSES FILTER A MARK. The mark decides, not the control that wrote
|
|
10
|
+
* the clause. A clause filters a mark unless:
|
|
11
|
+
* - it is a categorical selection on a field the mark reads selection
|
|
12
|
+
* activity for (the mark dims by it instead), or
|
|
13
|
+
* - the mark's chart holds a registration of the clause's source that opted
|
|
14
|
+
* into skipping its chart (crossfilter: a brush does not filter its own
|
|
15
|
+
* chart).
|
|
16
|
+
* Both are computed when asked, from the clause, the mark's route and the
|
|
17
|
+
* live registrations: nothing is cached, so nothing goes stale.
|
|
11
18
|
*/
|
|
12
19
|
import { SvelteMap } from 'svelte/reactivity';
|
|
20
|
+
/** A mark with no route: it reads no activity and is in no chart. */
|
|
21
|
+
export const NO_ROUTE = { reads: [], chart: null };
|
|
13
22
|
// ---------------------------------------------------------------------------
|
|
14
23
|
// CrossfilterSelection
|
|
15
24
|
// ---------------------------------------------------------------------------
|
|
16
25
|
export class CrossfilterSelection {
|
|
17
26
|
#clauses = new SvelteMap();
|
|
27
|
+
/** Registrations per source id. Several under one id are one source. Not reactive. */
|
|
28
|
+
#sources = new Map();
|
|
29
|
+
/** Add a registration of `sourceId`. */
|
|
30
|
+
addRegistration(sourceId, registration) {
|
|
31
|
+
let registrations = this.#sources.get(sourceId);
|
|
32
|
+
if (!registrations) {
|
|
33
|
+
registrations = new Set();
|
|
34
|
+
this.#sources.set(sourceId, registrations);
|
|
35
|
+
}
|
|
36
|
+
registrations.add(registration);
|
|
37
|
+
}
|
|
38
|
+
/** Remove a registration of `sourceId`; the last one removes the source. */
|
|
39
|
+
removeRegistration(sourceId, registration) {
|
|
40
|
+
const registrations = this.#sources.get(sourceId);
|
|
41
|
+
if (!registrations?.delete(registration))
|
|
42
|
+
return;
|
|
43
|
+
if (registrations.size === 0)
|
|
44
|
+
this.#sources.delete(sourceId);
|
|
45
|
+
}
|
|
46
|
+
/** The live registrations of `sourceId`, or undefined when it has none. */
|
|
47
|
+
registrationsOf(sourceId) {
|
|
48
|
+
return this.#sources.get(sourceId);
|
|
49
|
+
}
|
|
18
50
|
/**
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
51
|
+
* Whether a clause of `sourceId` filters the mark with `route`. `key` is
|
|
52
|
+
* the clause's categorical field (its identity key), or null for a brush,
|
|
53
|
+
* a tuple or a manual clause; before the source has written a clause, its
|
|
54
|
+
* registrations' key, which preagg activation needs on hover.
|
|
55
|
+
*
|
|
56
|
+
* A plain loop over the registrations, which are one to three: no closure,
|
|
57
|
+
* no allocation, as it runs per mark per clause on every brush frame.
|
|
22
58
|
*/
|
|
23
|
-
|
|
59
|
+
filters(route, sourceId, key) {
|
|
60
|
+
if (key !== null && route.reads.includes(key))
|
|
61
|
+
return false;
|
|
62
|
+
const chart = route.chart;
|
|
63
|
+
if (chart) {
|
|
64
|
+
// Every chart with a Toggle or legend on the same fields registers
|
|
65
|
+
// their one source, so a chart skip would let a 'filter' mark ignore
|
|
66
|
+
// those fields wherever it has such a control. A click does not move
|
|
67
|
+
// under the pointer, so filtering the own chart is safe for it.
|
|
68
|
+
const filterMark = route.response === 'filter';
|
|
69
|
+
const registrations = this.#sources.get(sourceId);
|
|
70
|
+
if (registrations) {
|
|
71
|
+
for (const registration of registrations) {
|
|
72
|
+
if (registration.chart === chart && !(filterMark && registration.discrete)) {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
/** Whether `clause`, written by `sourceId`, filters the mark with `route`. */
|
|
81
|
+
applies(sourceId, clause, route) {
|
|
82
|
+
return !!clause.predicate && this.filters(route, sourceId, clause.categorical?.key ?? null);
|
|
83
|
+
}
|
|
84
|
+
/** Every predicate that filters the mark with `route`. */
|
|
85
|
+
predicate(route) {
|
|
24
86
|
const result = [];
|
|
25
|
-
for (const clause of this.#clauses
|
|
26
|
-
if (
|
|
87
|
+
for (const [sourceId, clause] of this.#clauses) {
|
|
88
|
+
if (this.applies(sourceId, clause, route))
|
|
27
89
|
result.push(clause.predicate);
|
|
28
|
-
}
|
|
29
90
|
}
|
|
30
91
|
return result;
|
|
31
92
|
}
|
|
32
93
|
/**
|
|
33
94
|
* Update or remove a clause.
|
|
34
95
|
* Pass null to remove (clear) the clause for the given source.
|
|
96
|
+
*
|
|
97
|
+
* Returns the clause it replaced or removed, so the caller can re-query the
|
|
98
|
+
* marks either one applies to.
|
|
35
99
|
*/
|
|
36
100
|
update(sourceId, clause) {
|
|
101
|
+
const prev = this.#clauses.get(sourceId);
|
|
37
102
|
if (clause) {
|
|
38
103
|
this.#clauses.set(sourceId, clause);
|
|
39
104
|
}
|
|
40
105
|
else {
|
|
41
106
|
this.#clauses.delete(sourceId);
|
|
42
107
|
}
|
|
108
|
+
return prev;
|
|
43
109
|
}
|
|
44
110
|
/** Whether a given source has an active (non-null) clause. */
|
|
45
111
|
isActive(sourceId) {
|
|
@@ -49,6 +115,14 @@ export class CrossfilterSelection {
|
|
|
49
115
|
get(sourceId) {
|
|
50
116
|
return this.#clauses.get(sourceId);
|
|
51
117
|
}
|
|
118
|
+
/**
|
|
119
|
+
* Visit every active clause: the stored objects themselves, not copies, so a
|
|
120
|
+
* caller can tell an unchanged clause by reference. Reactive like `items`,
|
|
121
|
+
* without building an entry object per clause.
|
|
122
|
+
*/
|
|
123
|
+
forEach(visit) {
|
|
124
|
+
this.#clauses.forEach(visit);
|
|
125
|
+
}
|
|
52
126
|
/** Reactive list of all active clauses with their source IDs. */
|
|
53
127
|
get items() {
|
|
54
128
|
const result = [];
|
|
@@ -61,17 +135,17 @@ export class CrossfilterSelection {
|
|
|
61
135
|
* Whether a clause is baked into a mark's preagg table: every clause that
|
|
62
136
|
* filters the mark, except the active brush, which the table bins instead.
|
|
63
137
|
*/
|
|
64
|
-
#isBaked(sourceId, clause,
|
|
65
|
-
return sourceId !== activeSourceId &&
|
|
138
|
+
#isBaked(sourceId, clause, route, activeSourceId) {
|
|
139
|
+
return sourceId !== activeSourceId && this.applies(sourceId, clause, route);
|
|
66
140
|
}
|
|
67
141
|
/**
|
|
68
142
|
* The clause objects a preagg table bakes in, in map insertion order.
|
|
69
143
|
* Stored at build time so bakedClausesMatch() can compare by reference.
|
|
70
144
|
*/
|
|
71
|
-
bakedClauses(
|
|
145
|
+
bakedClauses(route, activeSourceId) {
|
|
72
146
|
const result = [];
|
|
73
147
|
for (const [sourceId, clause] of this.#clauses) {
|
|
74
|
-
if (this.#isBaked(sourceId, clause,
|
|
148
|
+
if (this.#isBaked(sourceId, clause, route, activeSourceId))
|
|
75
149
|
result.push(clause);
|
|
76
150
|
}
|
|
77
151
|
return result;
|
|
@@ -80,10 +154,10 @@ export class CrossfilterSelection {
|
|
|
80
154
|
* The baked predicates sorted by source ID, so the same filters always
|
|
81
155
|
* produce the same preagg SQL whatever order their clauses were added in.
|
|
82
156
|
*/
|
|
83
|
-
bakedPredicates(
|
|
157
|
+
bakedPredicates(route, activeSourceId) {
|
|
84
158
|
const entries = [];
|
|
85
159
|
for (const [sourceId, clause] of this.#clauses) {
|
|
86
|
-
if (this.#isBaked(sourceId, clause,
|
|
160
|
+
if (this.#isBaked(sourceId, clause, route, activeSourceId)) {
|
|
87
161
|
entries.push([sourceId, clause.predicate]);
|
|
88
162
|
}
|
|
89
163
|
}
|
|
@@ -95,10 +169,10 @@ export class CrossfilterSelection {
|
|
|
95
169
|
* Every filter change replaces its Clause object, so an unchanged filter
|
|
96
170
|
* keeps its reference. Runs every brush frame, so it allocates nothing.
|
|
97
171
|
*/
|
|
98
|
-
bakedClausesMatch(
|
|
172
|
+
bakedClausesMatch(route, activeSourceId, then) {
|
|
99
173
|
let i = 0;
|
|
100
174
|
for (const [sourceId, clause] of this.#clauses) {
|
|
101
|
-
if (!this.#isBaked(sourceId, clause,
|
|
175
|
+
if (!this.#isBaked(sourceId, clause, route, activeSourceId))
|
|
102
176
|
continue;
|
|
103
177
|
if (i >= then.length || clause !== then[i++])
|
|
104
178
|
return false;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type { ClockLoop } from '../animation';
|
|
2
|
+
import { ActivityTable } from '../scales/ActivityTable';
|
|
3
|
+
import type { FieldIdentity } from './categorical-identity';
|
|
4
|
+
/** What a FieldActivity needs from the page around it. */
|
|
5
|
+
export interface FieldActivityHost {
|
|
6
|
+
/** Drives the fades; null in unit tests and before a coordinator exists, and fades land at once. */
|
|
7
|
+
readonly loop: ClockLoop | null;
|
|
8
|
+
/** Fill `out` with the charts drawing any scale on `key`, for the fade's redraws. */
|
|
9
|
+
chartsFor(key: string, out: string[]): string[];
|
|
10
|
+
/** The field's live identity, or null; must never create one. */
|
|
11
|
+
identityOf(key: string): FieldIdentity | null;
|
|
12
|
+
}
|
|
13
|
+
/** A host with no page: no loop, no charts, no identity. */
|
|
14
|
+
export declare const DETACHED_HOST: FieldActivityHost;
|
|
15
|
+
/**
|
|
16
|
+
* Which categories of one field are selected, per filter group, and how far
|
|
17
|
+
* each one has faded.
|
|
18
|
+
*
|
|
19
|
+
* Activity is indexed by the field's identity id, and FieldIdentity is one per
|
|
20
|
+
* field, so it belongs to the field, not to any scale on it: the carrier colour
|
|
21
|
+
* scale and every carrier band scale read the same activity. One fade per
|
|
22
|
+
* filter group, indexed by the group's page-wide index, because a selection
|
|
23
|
+
* belongs to the group it was made in: a click in group A dims group A's marks
|
|
24
|
+
* and leaves group B's alone.
|
|
25
|
+
*
|
|
26
|
+
* The packer writes these tables into the field's activity block, and legends
|
|
27
|
+
* and band labels read them through `opacityOfValue`.
|
|
28
|
+
*
|
|
29
|
+
* IDS AND IDENTITY. Ids are only meaningful within one identity: across a
|
|
30
|
+
* reload the same id can name another category. So the tables follow the
|
|
31
|
+
* field's LIVE identity, which `reconcile` compares with the one last applied:
|
|
32
|
+
* a different object is a reload, and every fade is dropped and snapped to its
|
|
33
|
+
* selection; the same object at a higher version is a dim that grew, and the
|
|
34
|
+
* existing ids keep their fades while new ones start in their state.
|
|
35
|
+
* `reconcile` runs from the packer and from `set`, never from a reader, so
|
|
36
|
+
* nothing is resized or retargeted while a template renders.
|
|
37
|
+
*/
|
|
38
|
+
export declare class FieldActivity {
|
|
39
|
+
#private;
|
|
40
|
+
readonly key: string;
|
|
41
|
+
constructor(key: string, host?: FieldActivityHost);
|
|
42
|
+
/** How many ids the tables cover: the applied identity's, 0 without one. */
|
|
43
|
+
get count(): number;
|
|
44
|
+
/** Group `index`'s fade, or null when that group never selected on this field. */
|
|
45
|
+
fadeOf(index: number): ActivityTable | null;
|
|
46
|
+
/**
|
|
47
|
+
* Select `values` in group `index`, or nothing for null.
|
|
48
|
+
*
|
|
49
|
+
* The table is first brought up to the live identity under the selection it
|
|
50
|
+
* had, so the change from that selection to this one fades. Sizing it with
|
|
51
|
+
* the new values instead would put every id straight into its new state: a
|
|
52
|
+
* group's first click, or the first one after the dim grew, would snap.
|
|
53
|
+
*/
|
|
54
|
+
set(index: number, values: ReadonlySet<unknown> | null): void;
|
|
55
|
+
/**
|
|
56
|
+
* Follow the field's live identity: snap every fade on a reload, extend them
|
|
57
|
+
* when the dim grew. Two comparisons when nothing moved, no allocation.
|
|
58
|
+
* Returns whether it re-applied the groups.
|
|
59
|
+
*/
|
|
60
|
+
reconcile(): boolean;
|
|
61
|
+
/**
|
|
62
|
+
* How visible a category is now in group `index`: its fade when the value
|
|
63
|
+
* has an id in the current identity, and otherwise simply selected or not.
|
|
64
|
+
* Pure, so it is safe in a template: it never reconciles, and a table that
|
|
65
|
+
* belongs to an identity no longer live is not trusted.
|
|
66
|
+
*/
|
|
67
|
+
opacityOfValue(index: number, value: unknown, inactiveOpacity?: number): number;
|
|
68
|
+
/**
|
|
69
|
+
* How visible the category with dim id `id` is now in group `index`: its fade.
|
|
70
|
+
* The id form of `opacityOfValue`, and the only one that reaches a synthetic
|
|
71
|
+
* id (an Other bucket): a bucket has a label, not a value, and several may
|
|
72
|
+
* share one label. Pure, so it is safe in a template.
|
|
73
|
+
*
|
|
74
|
+
* Before the group's table covers the id, falls back to the selected values
|
|
75
|
+
* for a real id, and to fully visible for a bucket or an identity that is not
|
|
76
|
+
* live: a label is never dimmed on a guess.
|
|
77
|
+
*/
|
|
78
|
+
opacityOfId(index: number, id: number, inactiveOpacity?: number): number;
|
|
79
|
+
}
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { ActivityTable, DEFAULT_INACTIVE_OPACITY, selectionOpacity } from '../scales/ActivityTable';
|
|
2
|
+
/** A host with no page: no loop, no charts, no identity. */
|
|
3
|
+
export const DETACHED_HOST = {
|
|
4
|
+
loop: null,
|
|
5
|
+
chartsFor: (_key, out) => out,
|
|
6
|
+
identityOf: () => null
|
|
7
|
+
};
|
|
8
|
+
/** One filter group's selection on one field, and its fade. */
|
|
9
|
+
class GroupFade {
|
|
10
|
+
table;
|
|
11
|
+
/** The selected values, or null for none. State, for the by-value fallback. */
|
|
12
|
+
values = $state.raw(null);
|
|
13
|
+
/** Bumped on every frame the fade is driven, and once at rest. */
|
|
14
|
+
frame = $state(0);
|
|
15
|
+
constructor(host, key, charts) {
|
|
16
|
+
this.table = new ActivityTable(host.loop, {
|
|
17
|
+
charts: () => {
|
|
18
|
+
charts.length = 0;
|
|
19
|
+
return host.chartsFor(key, charts);
|
|
20
|
+
},
|
|
21
|
+
onFrame: () => this.frame++
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Which categories of one field are selected, per filter group, and how far
|
|
27
|
+
* each one has faded.
|
|
28
|
+
*
|
|
29
|
+
* Activity is indexed by the field's identity id, and FieldIdentity is one per
|
|
30
|
+
* field, so it belongs to the field, not to any scale on it: the carrier colour
|
|
31
|
+
* scale and every carrier band scale read the same activity. One fade per
|
|
32
|
+
* filter group, indexed by the group's page-wide index, because a selection
|
|
33
|
+
* belongs to the group it was made in: a click in group A dims group A's marks
|
|
34
|
+
* and leaves group B's alone.
|
|
35
|
+
*
|
|
36
|
+
* The packer writes these tables into the field's activity block, and legends
|
|
37
|
+
* and band labels read them through `opacityOfValue`.
|
|
38
|
+
*
|
|
39
|
+
* IDS AND IDENTITY. Ids are only meaningful within one identity: across a
|
|
40
|
+
* reload the same id can name another category. So the tables follow the
|
|
41
|
+
* field's LIVE identity, which `reconcile` compares with the one last applied:
|
|
42
|
+
* a different object is a reload, and every fade is dropped and snapped to its
|
|
43
|
+
* selection; the same object at a higher version is a dim that grew, and the
|
|
44
|
+
* existing ids keep their fades while new ones start in their state.
|
|
45
|
+
* `reconcile` runs from the packer and from `set`, never from a reader, so
|
|
46
|
+
* nothing is resized or retargeted while a template renders.
|
|
47
|
+
*/
|
|
48
|
+
export class FieldActivity {
|
|
49
|
+
key;
|
|
50
|
+
#host;
|
|
51
|
+
#groups = [];
|
|
52
|
+
/**
|
|
53
|
+
* Bumped when a group's fade is created, so a reader that found none runs
|
|
54
|
+
* again once it exists. Fades are created by `set`, never while rendering.
|
|
55
|
+
*/
|
|
56
|
+
#groupsVersion = $state(0);
|
|
57
|
+
/** The identity the tables' ids belong to, or null when they have none. */
|
|
58
|
+
#identity = null;
|
|
59
|
+
#version = -1;
|
|
60
|
+
/** Reused id list: selections are applied on every categorical click. */
|
|
61
|
+
#ids = [];
|
|
62
|
+
constructor(key, host = DETACHED_HOST) {
|
|
63
|
+
this.key = key;
|
|
64
|
+
this.#host = host;
|
|
65
|
+
}
|
|
66
|
+
/** How many ids the tables cover: the applied identity's, 0 without one. */
|
|
67
|
+
get count() {
|
|
68
|
+
return this.#identity ? this.#identity.values.length : 0;
|
|
69
|
+
}
|
|
70
|
+
/** Group `index`'s fade, or null when that group never selected on this field. */
|
|
71
|
+
fadeOf(index) {
|
|
72
|
+
return this.#groups[index]?.table ?? null;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Select `values` in group `index`, or nothing for null.
|
|
76
|
+
*
|
|
77
|
+
* The table is first brought up to the live identity under the selection it
|
|
78
|
+
* had, so the change from that selection to this one fades. Sizing it with
|
|
79
|
+
* the new values instead would put every id straight into its new state: a
|
|
80
|
+
* group's first click, or the first one after the dim grew, would snap.
|
|
81
|
+
*/
|
|
82
|
+
set(index, values) {
|
|
83
|
+
const group = this.#group(index);
|
|
84
|
+
this.reconcile();
|
|
85
|
+
if (group.table.count !== this.count)
|
|
86
|
+
this.#apply(group);
|
|
87
|
+
group.values = values;
|
|
88
|
+
this.#apply(group);
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Follow the field's live identity: snap every fade on a reload, extend them
|
|
92
|
+
* when the dim grew. Two comparisons when nothing moved, no allocation.
|
|
93
|
+
* Returns whether it re-applied the groups.
|
|
94
|
+
*/
|
|
95
|
+
reconcile() {
|
|
96
|
+
const live = this.#host.identityOf(this.key);
|
|
97
|
+
const usable = live && live.values.length > 0 ? live : null;
|
|
98
|
+
if (usable === this.#identity) {
|
|
99
|
+
if (!usable || usable.version === this.#version)
|
|
100
|
+
return false;
|
|
101
|
+
// The same identity, grown.
|
|
102
|
+
this.#version = usable.version;
|
|
103
|
+
for (const group of this.#groups)
|
|
104
|
+
this.#apply(group);
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
// Another identity, or none: the old ids mean nothing now.
|
|
108
|
+
this.#identity = usable;
|
|
109
|
+
this.#version = usable ? usable.version : -1;
|
|
110
|
+
for (const group of this.#groups) {
|
|
111
|
+
group.table.resize(0);
|
|
112
|
+
this.#apply(group);
|
|
113
|
+
}
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* How visible a category is now in group `index`: its fade when the value
|
|
118
|
+
* has an id in the current identity, and otherwise simply selected or not.
|
|
119
|
+
* Pure, so it is safe in a template: it never reconciles, and a table that
|
|
120
|
+
* belongs to an identity no longer live is not trusted.
|
|
121
|
+
*/
|
|
122
|
+
opacityOfValue(index, value, inactiveOpacity = DEFAULT_INACTIVE_OPACITY) {
|
|
123
|
+
void this.#groupsVersion;
|
|
124
|
+
const group = this.#groups[index];
|
|
125
|
+
if (!group)
|
|
126
|
+
return 1;
|
|
127
|
+
// Read first, so a reader that ran before identity landed already depends
|
|
128
|
+
// on the frame counter for the fade that follows.
|
|
129
|
+
void group.frame;
|
|
130
|
+
const identity = this.#identity;
|
|
131
|
+
if (identity && identity === this.#host.identityOf(this.key)) {
|
|
132
|
+
const id = identity.ids.get(value);
|
|
133
|
+
if (id != null)
|
|
134
|
+
return this.opacityOfId(index, id, inactiveOpacity);
|
|
135
|
+
}
|
|
136
|
+
const values = group.values;
|
|
137
|
+
return values == null || values.has(value) ? 1 : inactiveOpacity;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* How visible the category with dim id `id` is now in group `index`: its fade.
|
|
141
|
+
* The id form of `opacityOfValue`, and the only one that reaches a synthetic
|
|
142
|
+
* id (an Other bucket): a bucket has a label, not a value, and several may
|
|
143
|
+
* share one label. Pure, so it is safe in a template.
|
|
144
|
+
*
|
|
145
|
+
* Before the group's table covers the id, falls back to the selected values
|
|
146
|
+
* for a real id, and to fully visible for a bucket or an identity that is not
|
|
147
|
+
* live: a label is never dimmed on a guess.
|
|
148
|
+
*/
|
|
149
|
+
opacityOfId(index, id, inactiveOpacity = DEFAULT_INACTIVE_OPACITY) {
|
|
150
|
+
void this.#groupsVersion;
|
|
151
|
+
const group = this.#groups[index];
|
|
152
|
+
if (!group)
|
|
153
|
+
return 1;
|
|
154
|
+
void group.frame;
|
|
155
|
+
const values = group.values;
|
|
156
|
+
const identity = this.#identity;
|
|
157
|
+
if (!identity || identity !== this.#host.identityOf(this.key))
|
|
158
|
+
return 1;
|
|
159
|
+
if (id >= 0 && id < group.table.count) {
|
|
160
|
+
return selectionOpacity(group.table.activityOf(id), inactiveOpacity);
|
|
161
|
+
}
|
|
162
|
+
if (values == null || identity.synthetic.has(id) || id < 0 || id >= identity.values.length) {
|
|
163
|
+
return 1;
|
|
164
|
+
}
|
|
165
|
+
return values.has(identity.values[id]) ? 1 : inactiveOpacity;
|
|
166
|
+
}
|
|
167
|
+
#group(index) {
|
|
168
|
+
let group = this.#groups[index];
|
|
169
|
+
if (!group) {
|
|
170
|
+
// Fill any gap below it too: the packer walks the groups by index.
|
|
171
|
+
for (let i = this.#groups.length; i <= index; i++) {
|
|
172
|
+
this.#groups[i] = new GroupFade(this.#host, this.key, []);
|
|
173
|
+
}
|
|
174
|
+
group = this.#groups[index];
|
|
175
|
+
this.#groupsVersion++;
|
|
176
|
+
}
|
|
177
|
+
return group;
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Point a group's table at its selection under the applied identity. A new
|
|
181
|
+
* id starts in its state rather than fading in, which is what keeps a
|
|
182
|
+
* selection made before its ids existed from fading on arrival.
|
|
183
|
+
*/
|
|
184
|
+
#apply(group) {
|
|
185
|
+
const identity = this.#identity;
|
|
186
|
+
if (!identity) {
|
|
187
|
+
group.table.resize(0);
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
const count = identity.values.length;
|
|
191
|
+
const values = group.values;
|
|
192
|
+
if (values == null) {
|
|
193
|
+
group.table.resize(count, null);
|
|
194
|
+
group.table.clear();
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
const ids = this.#ids;
|
|
198
|
+
ids.length = 0;
|
|
199
|
+
for (const value of values) {
|
|
200
|
+
const id = identity.ids.get(value);
|
|
201
|
+
if (id != null)
|
|
202
|
+
ids.push(id);
|
|
203
|
+
}
|
|
204
|
+
group.table.resize(count, ids);
|
|
205
|
+
group.table.setActive(ids);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* FilterGroup — visual concerns layer on top of CrossfilterSelection.
|
|
3
3
|
*
|
|
4
4
|
* Manages the interactor API (register, registerToggle), schema augmentation
|
|
5
|
-
* for UI filter pills, and
|
|
6
|
-
* legends
|
|
5
|
+
* for UI filter pills, and this group's selection on each categorical field
|
|
6
|
+
* (FieldActivity), which marks, legends and band axes dim by.
|
|
7
7
|
*
|
|
8
8
|
* Owns clauses: a clause lives while at least one registration for its id
|
|
9
9
|
* exists. Components own their registrations through their own lifecycle.
|
|
@@ -12,16 +12,9 @@
|
|
|
12
12
|
* objects, no event listeners.
|
|
13
13
|
*/
|
|
14
14
|
import { CrossfilterSelection } from './CrossfilterSelection.svelte';
|
|
15
|
-
import type { FilterExpr, FilterSchema } from './CrossfilterSelection.svelte';
|
|
15
|
+
import type { Clause, FilterExpr, FilterSchema, SourceRegistration } from './CrossfilterSelection.svelte';
|
|
16
16
|
import type { ActiveDim, QueryEngine } from './QueryEngine';
|
|
17
|
-
|
|
18
|
-
* Narrow protocol for scale states that support active-value projection.
|
|
19
|
-
* Implemented by ColorScaleState and BandScaleState.
|
|
20
|
-
*/
|
|
21
|
-
export interface ActiveValueScale {
|
|
22
|
-
setActiveValues(values: Set<unknown>): void;
|
|
23
|
-
clearActiveValues(): void;
|
|
24
|
-
}
|
|
17
|
+
import { FieldActivity } from './FieldActivity.svelte';
|
|
25
18
|
export interface FilterItem {
|
|
26
19
|
sourceId: string;
|
|
27
20
|
predicate: FilterExpr;
|
|
@@ -29,10 +22,23 @@ export interface FilterItem {
|
|
|
29
22
|
schema?: FilterSchema;
|
|
30
23
|
}
|
|
31
24
|
/** One registration of a selection source: a brush, a legend, a Toggle. */
|
|
32
|
-
export interface SourceOpts {
|
|
33
|
-
/**
|
|
34
|
-
*
|
|
35
|
-
|
|
25
|
+
export interface SourceOpts extends SourceRegistration {
|
|
26
|
+
/**
|
|
27
|
+
* The chart this registration skips: its ChartConfig, or nothing. A chart
|
|
28
|
+
* holding a control for a selection is not filtered by it (crossfilter), so
|
|
29
|
+
* its marks that do not read the field keep their data; those that do, dim.
|
|
30
|
+
* Opt-in: a control that wants its own chart filtered passes none.
|
|
31
|
+
*/
|
|
32
|
+
chart?: object | null;
|
|
33
|
+
/**
|
|
34
|
+
* For a source that selects categories of one field: that field's identity
|
|
35
|
+
* key, and how to read the selected values from a write. Every clause the
|
|
36
|
+
* source writes carries both (`Clause.categorical`).
|
|
37
|
+
*/
|
|
38
|
+
categorical?: {
|
|
39
|
+
key: string;
|
|
40
|
+
values: (value: unknown) => ReadonlySet<unknown>;
|
|
41
|
+
};
|
|
36
42
|
/** How the source selects, read at hover time for preagg activation. */
|
|
37
43
|
activeDims?: () => ActiveDim[];
|
|
38
44
|
/**
|
|
@@ -41,16 +47,14 @@ export interface SourceOpts {
|
|
|
41
47
|
* while its bins are as fine as the pixels.
|
|
42
48
|
*/
|
|
43
49
|
view?: (out: Float64Array) => void;
|
|
44
|
-
/**
|
|
50
|
+
/**
|
|
51
|
+
* Schema metadata for UI filter pills, derived from the clause value. Run
|
|
52
|
+
* once per write and stored on the clause.
|
|
53
|
+
*/
|
|
45
54
|
augment?: (value: unknown) => FilterSchema | null;
|
|
46
|
-
/** The scales to dim, and the values to dim them by, taken from the clause value. */
|
|
47
|
-
projection?: {
|
|
48
|
-
scales: () => ActiveValueScale[];
|
|
49
|
-
values: (value: unknown) => Set<unknown> | null;
|
|
50
|
-
};
|
|
51
55
|
}
|
|
52
56
|
export interface SourceHandle {
|
|
53
|
-
/** Write the
|
|
57
|
+
/** Write the source's clause. */
|
|
54
58
|
update(predicate: FilterExpr, value: unknown): void;
|
|
55
59
|
clear(): void;
|
|
56
60
|
/** Prepare preagg tables for this source, ahead of its first use. */
|
|
@@ -81,8 +85,23 @@ export interface ToggleHandle extends Omit<SourceHandle, 'update'> {
|
|
|
81
85
|
export declare class FilterGroup {
|
|
82
86
|
#private;
|
|
83
87
|
readonly selection: CrossfilterSelection;
|
|
88
|
+
/**
|
|
89
|
+
* This group's page-wide index, assigned by FilterGroupRegistry and never
|
|
90
|
+
* reused: a mark stores it to read this group's half of an activity block.
|
|
91
|
+
*/
|
|
92
|
+
readonly index: number;
|
|
93
|
+
/**
|
|
94
|
+
* A group made outside a registry (unit tests) keeps its field activity to
|
|
95
|
+
* itself, with no loop: its fades land at once.
|
|
96
|
+
*/
|
|
97
|
+
constructor(index?: number, activityOf?: (key: string) => FieldActivity);
|
|
98
|
+
/** The selection state of a categorical field, by identity key: this group's fade is `index`. */
|
|
99
|
+
activity(key: string): FieldActivity;
|
|
84
100
|
setEngine(engine: QueryEngine): void;
|
|
85
|
-
/**
|
|
101
|
+
/**
|
|
102
|
+
* Active filter items with schema metadata, for UI consumption. The schema
|
|
103
|
+
* is the clause's own, computed when it was written (see `update`).
|
|
104
|
+
*/
|
|
86
105
|
get items(): FilterItem[];
|
|
87
106
|
/** Number of active filters. */
|
|
88
107
|
get activeCount(): number;
|
|
@@ -90,8 +109,7 @@ export declare class FilterGroup {
|
|
|
90
109
|
fieldSql(field: string): string | null;
|
|
91
110
|
/**
|
|
92
111
|
* Register a selection source. Several registrations under one id are one
|
|
93
|
-
* source
|
|
94
|
-
* source dims are the union of theirs.
|
|
112
|
+
* source and write one clause.
|
|
95
113
|
*/
|
|
96
114
|
register(sourceId: string, opts: SourceOpts): SourceHandle;
|
|
97
115
|
/**
|
|
@@ -103,11 +121,17 @@ export declare class FilterGroup {
|
|
|
103
121
|
* as the selection. Nothing else writes those ids (`upsertManual` prefixes
|
|
104
122
|
* its own with `manual:`).
|
|
105
123
|
*/
|
|
106
|
-
registerToggle(fields: string | string[], opts
|
|
107
|
-
|
|
108
|
-
scales?: () => ActiveValueScale[];
|
|
124
|
+
registerToggle(fields: string | string[], opts?: {
|
|
125
|
+
chart?: object | null;
|
|
109
126
|
}): ToggleHandle;
|
|
110
|
-
upsertManual(key: string, predicate: FilterExpr, value: unknown,
|
|
127
|
+
upsertManual(key: string, predicate: FilterExpr, value: unknown, schema?: FilterSchema): void;
|
|
128
|
+
/**
|
|
129
|
+
* Hold `clause` under a manual key, as given: the relay passes another
|
|
130
|
+
* group's clause on. The clause it already holds there changes nothing, so
|
|
131
|
+
* it neither re-projects nor re-queries: the relay runs on every change in
|
|
132
|
+
* its source group, on every brush frame there.
|
|
133
|
+
*/
|
|
134
|
+
relayClause(key: string, clause: Clause): void;
|
|
111
135
|
removeManual(key: string): void;
|
|
112
136
|
/**
|
|
113
137
|
* Remove a source's clause. Re-queries only when there was a clause to
|