@fjader/svelte 0.2.1 → 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 +32 -9
- 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 +51 -10
- package/dist/components/rendering/Lines.svelte.d.ts +9 -0
- package/dist/components/rendering/ParallelLines.svelte +28 -8
- 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/data/DataAccessor.js +1 -1
- package/dist/gpu/ColorScaleShaderLib.d.ts +14 -2
- package/dist/gpu/ColorScaleShaderLib.js +59 -7
- package/dist/gpu/GPUCoordinator.svelte.d.ts +8 -0
- package/dist/gpu/GPUCoordinator.svelte.js +68 -32
- package/dist/gpu/NanShaderLib.d.ts +30 -0
- package/dist/gpu/NanShaderLib.js +42 -0
- package/dist/gpu/ResolveShader.d.ts +3 -3
- package/dist/gpu/ResolveShader.js +6 -5
- package/dist/gpu/ScaleTemplateSystem.d.ts +19 -0
- package/dist/gpu/ScaleTemplateSystem.js +86 -7
- package/dist/gpu/SizeScaleShaderLib.d.ts +5 -5
- package/dist/gpu/SizeScaleShaderLib.js +6 -5
- package/dist/gpu/decimation/M4Shader.d.ts +13 -7
- package/dist/gpu/decimation/M4Shader.js +79 -64
- 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
|
@@ -37,7 +37,8 @@
|
|
|
37
37
|
filterGroup: FilterGroup;
|
|
38
38
|
column: string;
|
|
39
39
|
chartId: string;
|
|
40
|
-
|
|
40
|
+
/** The chart the brush skips, or null for a chart that filters its own lines. */
|
|
41
|
+
chart: object | null;
|
|
41
42
|
/** The axis's scale state now: removing another column renumbers the axes. */
|
|
42
43
|
scaleState: () => LinearScaleState | null;
|
|
43
44
|
/** The axes' pixel extent (the chart's inner height). */
|
|
@@ -47,13 +48,12 @@
|
|
|
47
48
|
}
|
|
48
49
|
|
|
49
50
|
// Keyed by column in the parent, so none of these change for an instance.
|
|
50
|
-
const { filterGroup, column, chartId,
|
|
51
|
-
$props();
|
|
51
|
+
const { filterGroup, column, chartId, chart, scaleState, pixelExtent, axes }: Props = $props();
|
|
52
52
|
|
|
53
53
|
const columnRef = sqlColumn(column);
|
|
54
54
|
|
|
55
55
|
const handle = filterGroup.register(`parallel-brush-${column}-${chartId}`, {
|
|
56
|
-
|
|
56
|
+
chart,
|
|
57
57
|
activeDims: () => {
|
|
58
58
|
const state = scaleState();
|
|
59
59
|
if (!state) return [];
|
|
@@ -31,7 +31,8 @@ interface Props {
|
|
|
31
31
|
filterGroup: FilterGroup;
|
|
32
32
|
column: string;
|
|
33
33
|
chartId: string;
|
|
34
|
-
|
|
34
|
+
/** The chart the brush skips, or null for a chart that filters its own lines. */
|
|
35
|
+
chart: object | null;
|
|
35
36
|
/** The axis's scale state now: removing another column renumbers the axes. */
|
|
36
37
|
scaleState: () => LinearScaleState | null;
|
|
37
38
|
/** The axes' pixel extent (the chart's inner height). */
|
|
@@ -19,9 +19,9 @@
|
|
|
19
19
|
interface Props {
|
|
20
20
|
/** Filter group name — selects which FilterGroup these brushes write to. */
|
|
21
21
|
selection?: string;
|
|
22
|
-
/** When true, the
|
|
23
|
-
*
|
|
24
|
-
*
|
|
22
|
+
/** When true, the brushes skip their own chart (standard crossfilter
|
|
23
|
+
* behavior). When false (default), they filter it — typical for parallel
|
|
24
|
+
* coordinates. */
|
|
25
25
|
cross?: boolean;
|
|
26
26
|
}
|
|
27
27
|
|
|
@@ -105,11 +105,10 @@
|
|
|
105
105
|
return Math.min(30, (chart.innerWidth / (ps.columns.length - 1)) * 0.35);
|
|
106
106
|
});
|
|
107
107
|
|
|
108
|
-
// Cross-filter:
|
|
109
|
-
//
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
: () => new Set<string>();
|
|
108
|
+
// Cross-filter: with cross=true the brushes skip their own chart (standard
|
|
109
|
+
// crossfilter). With cross=false, the default for parallel coordinates, the
|
|
110
|
+
// chart filters its own lines.
|
|
111
|
+
const skips = cross ? chart.config : null;
|
|
113
112
|
|
|
114
113
|
/** An axis's scale state, looked up by column: removing a column renumbers the axes. */
|
|
115
114
|
function scaleStateOf(col: string): LinearScaleState | null {
|
|
@@ -344,7 +343,7 @@
|
|
|
344
343
|
{filterGroup}
|
|
345
344
|
column={col}
|
|
346
345
|
chartId={chart.chartId}
|
|
347
|
-
{
|
|
346
|
+
chart={skips}
|
|
348
347
|
scaleState={() => scaleStateOf(col)}
|
|
349
348
|
pixelExtent={() => chart.innerHeight}
|
|
350
349
|
{axes}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
interface Props {
|
|
2
2
|
/** Filter group name — selects which FilterGroup these brushes write to. */
|
|
3
3
|
selection?: string;
|
|
4
|
-
/** When true, the
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
/** When true, the brushes skip their own chart (standard crossfilter
|
|
5
|
+
* behavior). When false (default), they filter it — typical for parallel
|
|
6
|
+
* coordinates. */
|
|
7
7
|
cross?: boolean;
|
|
8
8
|
}
|
|
9
9
|
declare const ParallelBrushes: import("svelte").Component<Props, {}, "">;
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { getContext, onDestroy } from 'svelte';
|
|
3
3
|
import type { ChartContext } from '../types';
|
|
4
|
-
import type {
|
|
5
|
-
import type { ActiveValueScale, ToggleHandle } from '../query-engine/FilterGroup.svelte';
|
|
4
|
+
import type { ToggleHandle } from '../query-engine/FilterGroup.svelte';
|
|
6
5
|
import type { FilterGroupRegistry } from '../query-engine/FilterGroupRegistry.svelte';
|
|
7
6
|
import type { BandScaleState } from '../scales/BandScaleState.svelte';
|
|
8
7
|
import BandLabelToggle from './BandLabelToggle.svelte';
|
|
@@ -16,9 +15,10 @@
|
|
|
16
15
|
* - Band axis label: toggles the axis's own source field, whatever the
|
|
17
16
|
* declared field is. Labels are a ready-made category picker.
|
|
18
17
|
*
|
|
19
|
-
* Toggles on different fields AND together. The
|
|
20
|
-
*
|
|
21
|
-
*
|
|
18
|
+
* Toggles on different fields AND together. The chart skips its own toggles
|
|
19
|
+
* (crossfilter): its marks that read the toggled field dim, the rest keep
|
|
20
|
+
* their data. Elsewhere in the group, marks that read the field dim and the
|
|
21
|
+
* rest filter.
|
|
22
22
|
*/
|
|
23
23
|
|
|
24
24
|
interface Props {
|
|
@@ -32,33 +32,18 @@
|
|
|
32
32
|
const filterGroups = getContext<FilterGroupRegistry>('filter-groups');
|
|
33
33
|
const filterGroup = filterGroups.get(selection);
|
|
34
34
|
const chart = getContext<ChartContext>('chart');
|
|
35
|
-
const scaleRegistry = getContext<ScaleRegistry>('scale-registry');
|
|
36
35
|
|
|
37
36
|
// Normalize field to array for multi-column support
|
|
38
37
|
const fields = Array.isArray(field) ? field : [field];
|
|
39
38
|
const declaredField = fields.length === 1 ? fields[0] : null;
|
|
40
39
|
|
|
41
|
-
/**
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
function scalesFor(toggleField: string): ActiveValueScale[] {
|
|
47
|
-
const out: ActiveValueScale[] = [];
|
|
48
|
-
const color = scaleRegistry?.getScaleByField(toggleField, 'color');
|
|
49
|
-
if (color) out.push(color as unknown as ActiveValueScale);
|
|
50
|
-
for (const axis of ['x', 'y'] as const) {
|
|
51
|
-
if (chart.scaleTypes?.[axis] !== 'band') continue;
|
|
52
|
-
const band = chart[`${axis}ScaleState`] as BandScaleState | null | undefined;
|
|
53
|
-
if (band && band.sourceField === toggleField) out.push(band as unknown as ActiveValueScale);
|
|
54
|
-
}
|
|
55
|
-
return out;
|
|
56
|
-
}
|
|
40
|
+
/**
|
|
41
|
+
* This chart skips its own toggles (crossfilter): its marks keyed by the
|
|
42
|
+
* toggled field dim, the rest keep their data.
|
|
43
|
+
*/
|
|
44
|
+
const skips = chart.config;
|
|
57
45
|
|
|
58
|
-
const declared = filterGroup.registerToggle(fields, {
|
|
59
|
-
excludeMarkIds,
|
|
60
|
-
scales: () => (declaredField ? scalesFor(declaredField) : [])
|
|
61
|
-
});
|
|
46
|
+
const declared = filterGroup.registerToggle(fields, { chart: skips });
|
|
62
47
|
chart.activators.add(declared.activate);
|
|
63
48
|
|
|
64
49
|
/** One BandLabelToggle per band axis field (see bandFieldsOf). */
|
|
@@ -92,8 +77,9 @@
|
|
|
92
77
|
const slot = bandState.invertScreenToData(innerPos, extent, axis === 'y');
|
|
93
78
|
if (slot == null) return null;
|
|
94
79
|
const id = bandState.idAtSlot(slot);
|
|
95
|
-
|
|
96
|
-
|
|
80
|
+
// undefined is "no category here"; null is the NULL category, a real one.
|
|
81
|
+
const value = id == null ? undefined : bandState.categoryAt(id);
|
|
82
|
+
return value === undefined ? null : { field: bandState.sourceField, value };
|
|
97
83
|
}
|
|
98
84
|
|
|
99
85
|
chart.onChartClick = (event) => {
|
|
@@ -106,7 +92,8 @@
|
|
|
106
92
|
// pointer micro-movement during click hides tooltip but data stays valid)
|
|
107
93
|
if (xInBounds && yInBounds && tm?.currentPoint) {
|
|
108
94
|
const tuple = fields.map((f) => tm.currentPoint![f]);
|
|
109
|
-
|
|
95
|
+
// A missing field is undefined; null is the NULL category, selectable.
|
|
96
|
+
if (tuple.some((v) => v === undefined)) return;
|
|
110
97
|
declared.toggle(tuple, isMulti);
|
|
111
98
|
return;
|
|
112
99
|
}
|
|
@@ -139,11 +126,5 @@
|
|
|
139
126
|
</script>
|
|
140
127
|
|
|
141
128
|
{#each bandFields as bandField (bandField)}
|
|
142
|
-
<BandLabelToggle
|
|
143
|
-
{filterGroup}
|
|
144
|
-
field={bandField}
|
|
145
|
-
{excludeMarkIds}
|
|
146
|
-
scales={() => scalesFor(bandField)}
|
|
147
|
-
handles={bandToggles}
|
|
148
|
-
/>
|
|
129
|
+
<BandLabelToggle {filterGroup} field={bandField} chart={skips} handles={bandToggles} />
|
|
149
130
|
{/each}
|
|
@@ -6,9 +6,10 @@
|
|
|
6
6
|
* - Band axis label: toggles the axis's own source field, whatever the
|
|
7
7
|
* declared field is. Labels are a ready-made category picker.
|
|
8
8
|
*
|
|
9
|
-
* Toggles on different fields AND together. The
|
|
10
|
-
*
|
|
11
|
-
*
|
|
9
|
+
* Toggles on different fields AND together. The chart skips its own toggles
|
|
10
|
+
* (crossfilter): its marks that read the toggled field dim, the rest keep
|
|
11
|
+
* their data. Elsewhere in the group, marks that read the field dim and the
|
|
12
|
+
* rest filter.
|
|
12
13
|
*/
|
|
13
14
|
interface Props {
|
|
14
15
|
/** Filter group name — selects which FilterGroup this toggle writes to. */
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
<script lang="ts">
|
|
13
13
|
import { getContext } from 'svelte';
|
|
14
14
|
import { format as d3format } from 'd3-format';
|
|
15
|
+
import { categoryLabel } from '../query-engine/categorical-identity';
|
|
15
16
|
import type { ChartContext, ContinuousScaleState } from '../types';
|
|
16
17
|
|
|
17
18
|
const commaFormat = d3format(',.4~g');
|
|
@@ -117,7 +118,7 @@
|
|
|
117
118
|
if (fmt) return fmt(value);
|
|
118
119
|
return commaFormat(value);
|
|
119
120
|
}
|
|
120
|
-
return String(value);
|
|
121
|
+
return String(categoryLabel(value));
|
|
121
122
|
}
|
|
122
123
|
|
|
123
124
|
const channelIcons: Record<string, string> = { x: '\u2192', y: '\u2191' };
|
|
@@ -440,7 +440,8 @@ export class TooltipManager {
|
|
|
440
440
|
if (index == null)
|
|
441
441
|
return null;
|
|
442
442
|
const category = this.#bandScale(axis)?.categoryAt(index);
|
|
443
|
-
|
|
443
|
+
// undefined is no category; null is the NULL category, which has a band.
|
|
444
|
+
if (category === undefined)
|
|
444
445
|
return null;
|
|
445
446
|
const pos = scale(category);
|
|
446
447
|
const bandwidth = 'bandwidth' in scale ? scale.bandwidth() : 0;
|
|
@@ -492,7 +493,8 @@ export class TooltipManager {
|
|
|
492
493
|
const { scaleTypes, bandDomains } = config;
|
|
493
494
|
// Band channel → category label
|
|
494
495
|
if ((key === 'x' || key === 'y') && scaleTypes[key] === 'band' && bandDomains?.[key]) {
|
|
495
|
-
|
|
496
|
+
const category = this.#bandScale(key)?.categoryAt(value);
|
|
497
|
+
return category === undefined ? value : category;
|
|
496
498
|
}
|
|
497
499
|
// Time offset → formatted datetime
|
|
498
500
|
if (key.startsWith(axis) && isTemporalScale(scaleTypes[axis]) && typeof value === 'number') {
|
|
@@ -577,7 +579,10 @@ export class TooltipManager {
|
|
|
577
579
|
// field name to build its filter tuples, so this is also what makes a
|
|
578
580
|
// click on a reordered band axis filter the right category.
|
|
579
581
|
if ((key === 'x' || key === 'y') && scaleTypes[key] === 'band' && bandDomains?.[key]) {
|
|
580
|
-
|
|
582
|
+
// Not `??`: the NULL category decodes to null, which must stay null
|
|
583
|
+
// (Toggle selects it) rather than fall back to the raw id.
|
|
584
|
+
const category = this.#bandScale(key)?.categoryAt(value);
|
|
585
|
+
point[fieldName] = category === undefined ? value : category;
|
|
581
586
|
continue;
|
|
582
587
|
}
|
|
583
588
|
// Resolve time offset to formatted datetime string
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { FilterGroup } from '../query-engine/FilterGroup.svelte';
|
|
2
|
+
/**
|
|
3
|
+
* Mirrors one group's clauses into another, for DerivedInterval1D: every
|
|
4
|
+
* clause but the range brush on the derived axis, which the interactor
|
|
5
|
+
* replaces with its shifted copy.
|
|
6
|
+
*
|
|
7
|
+
* `sync` runs on every change in the source group, which is every brush frame
|
|
8
|
+
* there. It relays the source's clause objects themselves, so an unchanged
|
|
9
|
+
* one is the object the target already holds, which it ignores: a brush in the
|
|
10
|
+
* source re-queries nothing in the target for the clauses it did not touch.
|
|
11
|
+
*/
|
|
12
|
+
export declare class ClauseRelay {
|
|
13
|
+
#private;
|
|
14
|
+
constructor(source: FilterGroup, target: FilterGroup);
|
|
15
|
+
/**
|
|
16
|
+
* Relay the source's clauses (all of them but the range on `axisColumn`),
|
|
17
|
+
* and remove relayed clauses the source no longer has. With `relay` false,
|
|
18
|
+
* only the removal. Call untracked: it reads the target group it writes.
|
|
19
|
+
*/
|
|
20
|
+
sync(relay: boolean, axisColumn: string): void;
|
|
21
|
+
/** Remove every relayed clause, for an interactor going away. */
|
|
22
|
+
clear(): void;
|
|
23
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mirrors one group's clauses into another, for DerivedInterval1D: every
|
|
3
|
+
* clause but the range brush on the derived axis, which the interactor
|
|
4
|
+
* replaces with its shifted copy.
|
|
5
|
+
*
|
|
6
|
+
* `sync` runs on every change in the source group, which is every brush frame
|
|
7
|
+
* there. It relays the source's clause objects themselves, so an unchanged
|
|
8
|
+
* one is the object the target already holds, which it ignores: a brush in the
|
|
9
|
+
* source re-queries nothing in the target for the clauses it did not touch.
|
|
10
|
+
*/
|
|
11
|
+
export class ClauseRelay {
|
|
12
|
+
#source;
|
|
13
|
+
#target;
|
|
14
|
+
#relayed = new Set();
|
|
15
|
+
/** Reused as the next pass's key set, swapped with #relayed after each. */
|
|
16
|
+
#current = new Set();
|
|
17
|
+
#axisColumn = '';
|
|
18
|
+
constructor(source, target) {
|
|
19
|
+
this.#source = source;
|
|
20
|
+
this.#target = target;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Relay the source's clauses (all of them but the range on `axisColumn`),
|
|
24
|
+
* and remove relayed clauses the source no longer has. With `relay` false,
|
|
25
|
+
* only the removal. Call untracked: it reads the target group it writes.
|
|
26
|
+
*/
|
|
27
|
+
sync(relay, axisColumn) {
|
|
28
|
+
this.#current.clear();
|
|
29
|
+
if (relay) {
|
|
30
|
+
this.#axisColumn = axisColumn;
|
|
31
|
+
this.#source.selection.forEach(this.#visit);
|
|
32
|
+
}
|
|
33
|
+
for (const key of this.#relayed) {
|
|
34
|
+
if (!this.#current.has(key))
|
|
35
|
+
this.#target.removeManual(key);
|
|
36
|
+
}
|
|
37
|
+
const done = this.#relayed;
|
|
38
|
+
this.#relayed = this.#current;
|
|
39
|
+
this.#current = done;
|
|
40
|
+
}
|
|
41
|
+
/** Remove every relayed clause, for an interactor going away. */
|
|
42
|
+
clear() {
|
|
43
|
+
for (const key of this.#relayed)
|
|
44
|
+
this.#target.removeManual(key);
|
|
45
|
+
this.#relayed.clear();
|
|
46
|
+
}
|
|
47
|
+
#visit = (clause, sourceId) => {
|
|
48
|
+
const { schema } = clause;
|
|
49
|
+
if (schema?.kind === 'range' && schema.column === this.#axisColumn)
|
|
50
|
+
return;
|
|
51
|
+
if (!clause.predicate)
|
|
52
|
+
return;
|
|
53
|
+
const key = `relay:${sourceId}`;
|
|
54
|
+
this.#current.add(key);
|
|
55
|
+
// The clause itself: nothing in it depends on its group, so a relayed
|
|
56
|
+
// toggle routes and dims in the target as a native one would, and an
|
|
57
|
+
// unchanged clause is the same object, which the target ignores.
|
|
58
|
+
this.#target.relayClause(key, clause);
|
|
59
|
+
};
|
|
60
|
+
}
|
|
@@ -2,12 +2,19 @@
|
|
|
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 type { ExprNode } from '@uwdata/mosaic-sql';
|
|
13
20
|
export type FilterExpr = ExprNode | string | boolean;
|
|
@@ -18,52 +25,118 @@ export interface FilterSchema {
|
|
|
18
25
|
values?: unknown[];
|
|
19
26
|
[key: string]: unknown;
|
|
20
27
|
}
|
|
21
|
-
/**
|
|
28
|
+
/**
|
|
29
|
+
* A single-field categorical selection, as a clause states it: the field's
|
|
30
|
+
* identity key and the selected values. Brushes, tuples and manual clauses have
|
|
31
|
+
* none.
|
|
32
|
+
*/
|
|
33
|
+
export interface CategoricalSelection {
|
|
34
|
+
key: string;
|
|
35
|
+
values: ReadonlySet<unknown>;
|
|
36
|
+
}
|
|
37
|
+
/** A clause stored in the selection. Nothing in it depends on its group. */
|
|
22
38
|
export interface Clause {
|
|
23
39
|
predicate: FilterExpr;
|
|
24
|
-
excludeMarkIds: Set<string>;
|
|
25
40
|
value: unknown;
|
|
26
41
|
schema?: FilterSchema;
|
|
42
|
+
/** Set when the clause selects categories of one field; written with the clause. */
|
|
43
|
+
categorical?: CategoricalSelection | null;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* What routing needs to know about a mark: the identity keys of the
|
|
47
|
+
* categorical fields its renderer reads selection activity for, its chart, and
|
|
48
|
+
* its selectionResponse. One object per mark, owned by its lifecycle and
|
|
49
|
+
* handed around by reference.
|
|
50
|
+
*
|
|
51
|
+
* A categorical selection on a field in `reads` dims the mark instead of
|
|
52
|
+
* filtering it. `chart` is the chart's ChartConfig, compared by reference: a
|
|
53
|
+
* faceted chart's cells have ids of their own, but share one config.
|
|
54
|
+
* `response` 'filter' makes every selection filter the mark, and a click
|
|
55
|
+
* control's (a discrete registration's) in its own chart too; brushes still
|
|
56
|
+
* skip their own chart.
|
|
57
|
+
*/
|
|
58
|
+
export interface MarkRoute {
|
|
59
|
+
readonly reads: readonly string[];
|
|
60
|
+
readonly chart: object | null;
|
|
61
|
+
readonly response?: 'auto' | 'filter';
|
|
27
62
|
}
|
|
63
|
+
/**
|
|
64
|
+
* What routing reads from one registration of a source: the chart it skips,
|
|
65
|
+
* if it opted into skipping one. Compared by reference with a mark's route.
|
|
66
|
+
*/
|
|
67
|
+
export interface SourceRegistration {
|
|
68
|
+
readonly chart?: object | null;
|
|
69
|
+
/**
|
|
70
|
+
* The source writes its clauses on a click (a Toggle, a legend, a band
|
|
71
|
+
* label), not under a dragged pointer. A 'filter' mark in the chart it
|
|
72
|
+
* skips is filtered by it all the same.
|
|
73
|
+
*/
|
|
74
|
+
readonly discrete?: boolean;
|
|
75
|
+
}
|
|
76
|
+
/** A mark with no route: it reads no activity and is in no chart. */
|
|
77
|
+
export declare const NO_ROUTE: MarkRoute;
|
|
28
78
|
/** A clause with its source identifier, for external consumers. */
|
|
29
79
|
export interface ClauseEntry extends Clause {
|
|
30
80
|
sourceId: string;
|
|
31
81
|
}
|
|
32
82
|
export declare class CrossfilterSelection {
|
|
33
83
|
#private;
|
|
84
|
+
/** Add a registration of `sourceId`. */
|
|
85
|
+
addRegistration(sourceId: string, registration: SourceRegistration): void;
|
|
86
|
+
/** Remove a registration of `sourceId`; the last one removes the source. */
|
|
87
|
+
removeRegistration(sourceId: string, registration: SourceRegistration): void;
|
|
88
|
+
/** The live registrations of `sourceId`, or undefined when it has none. */
|
|
89
|
+
registrationsOf(sourceId: string): ReadonlySet<SourceRegistration> | undefined;
|
|
34
90
|
/**
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
91
|
+
* Whether a clause of `sourceId` filters the mark with `route`. `key` is
|
|
92
|
+
* the clause's categorical field (its identity key), or null for a brush,
|
|
93
|
+
* a tuple or a manual clause; before the source has written a clause, its
|
|
94
|
+
* registrations' key, which preagg activation needs on hover.
|
|
95
|
+
*
|
|
96
|
+
* A plain loop over the registrations, which are one to three: no closure,
|
|
97
|
+
* no allocation, as it runs per mark per clause on every brush frame.
|
|
38
98
|
*/
|
|
39
|
-
|
|
99
|
+
filters(route: MarkRoute, sourceId: string, key: string | null): boolean;
|
|
100
|
+
/** Whether `clause`, written by `sourceId`, filters the mark with `route`. */
|
|
101
|
+
applies(sourceId: string, clause: Clause, route: MarkRoute): boolean;
|
|
102
|
+
/** Every predicate that filters the mark with `route`. */
|
|
103
|
+
predicate(route: MarkRoute): FilterExpr[];
|
|
40
104
|
/**
|
|
41
105
|
* Update or remove a clause.
|
|
42
106
|
* Pass null to remove (clear) the clause for the given source.
|
|
107
|
+
*
|
|
108
|
+
* Returns the clause it replaced or removed, so the caller can re-query the
|
|
109
|
+
* marks either one applies to.
|
|
43
110
|
*/
|
|
44
|
-
update(sourceId: string, clause: Clause | null):
|
|
111
|
+
update(sourceId: string, clause: Clause | null): Clause | undefined;
|
|
45
112
|
/** Whether a given source has an active (non-null) clause. */
|
|
46
113
|
isActive(sourceId: string): boolean;
|
|
47
114
|
/** O(1) lookup of a clause by source ID. */
|
|
48
115
|
get(sourceId: string): Clause | undefined;
|
|
116
|
+
/**
|
|
117
|
+
* Visit every active clause: the stored objects themselves, not copies, so a
|
|
118
|
+
* caller can tell an unchanged clause by reference. Reactive like `items`,
|
|
119
|
+
* without building an entry object per clause.
|
|
120
|
+
*/
|
|
121
|
+
forEach(visit: (clause: Clause, sourceId: string) => void): void;
|
|
49
122
|
/** Reactive list of all active clauses with their source IDs. */
|
|
50
123
|
get items(): ClauseEntry[];
|
|
51
124
|
/**
|
|
52
125
|
* The clause objects a preagg table bakes in, in map insertion order.
|
|
53
126
|
* Stored at build time so bakedClausesMatch() can compare by reference.
|
|
54
127
|
*/
|
|
55
|
-
bakedClauses(
|
|
128
|
+
bakedClauses(route: MarkRoute, activeSourceId: string): Clause[];
|
|
56
129
|
/**
|
|
57
130
|
* The baked predicates sorted by source ID, so the same filters always
|
|
58
131
|
* produce the same preagg SQL whatever order their clauses were added in.
|
|
59
132
|
*/
|
|
60
|
-
bakedPredicates(
|
|
133
|
+
bakedPredicates(route: MarkRoute, activeSourceId: string): FilterExpr[];
|
|
61
134
|
/**
|
|
62
135
|
* Whether the clauses baked into a preagg table are still the ones in force.
|
|
63
136
|
* Every filter change replaces its Clause object, so an unchanged filter
|
|
64
137
|
* keeps its reference. Runs every brush frame, so it allocates nothing.
|
|
65
138
|
*/
|
|
66
|
-
bakedClausesMatch(
|
|
139
|
+
bakedClausesMatch(route: MarkRoute, activeSourceId: string, then: Clause[]): boolean;
|
|
67
140
|
/** Number of active clauses. */
|
|
68
141
|
get activeCount(): number;
|
|
69
142
|
/** Remove all clauses. */
|