@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.
Files changed (118) hide show
  1. package/dist/components/axes/BandAxisTicks.svelte +63 -24
  2. package/dist/components/axes/BandAxisTicks.svelte.d.ts +5 -5
  3. package/dist/components/charts/ChartContainer.svelte +2 -1
  4. package/dist/components/guides/legend/ContinuousColorLegend.svelte +2 -1
  5. package/dist/components/guides/legend/OrdinalColorLegend.svelte +92 -34
  6. package/dist/components/marks/AreaMarkX.svelte +19 -6
  7. package/dist/components/marks/AreaMarkY.svelte +19 -6
  8. package/dist/components/marks/BarMarkX.svelte +3 -0
  9. package/dist/components/marks/BarMarkY.svelte +3 -0
  10. package/dist/components/marks/DotsMark.svelte +3 -0
  11. package/dist/components/marks/LineMark.svelte +3 -0
  12. package/dist/components/marks/ParallelCoordinatesMark.svelte +6 -0
  13. package/dist/components/marks/ParallelCoordinatesMark.svelte.d.ts +3 -0
  14. package/dist/components/marks/RectMark.svelte +3 -0
  15. package/dist/components/marks/RectXMark.svelte +3 -0
  16. package/dist/components/marks/RectYMark.svelte +3 -0
  17. package/dist/components/marks/RuleMarkX.svelte +10 -2
  18. package/dist/components/marks/RuleMarkY.svelte +10 -2
  19. package/dist/components/marks/TextMark.svelte +68 -4
  20. package/dist/components/marks/lifecycle/MarkLifecycleV2.svelte.d.ts +15 -1
  21. package/dist/components/marks/lifecycle/MarkLifecycleV2.svelte.js +116 -27
  22. package/dist/components/marks/lifecycle/domain-queries.js +13 -11
  23. package/dist/components/marks/lifecycle/transform-scale-key.d.ts +16 -0
  24. package/dist/components/marks/lifecycle/transform-scale-key.js +27 -0
  25. package/dist/components/marks/utils/area-outline.d.ts +24 -0
  26. package/dist/components/marks/utils/area-outline.js +10 -0
  27. package/dist/components/marks/utils/reactive-config.d.ts +1 -1
  28. package/dist/components/marks/utils/reactive-config.js +3 -0
  29. package/dist/components/rendering/Areas.svelte +30 -6
  30. package/dist/components/rendering/Areas.svelte.d.ts +2 -0
  31. package/dist/components/rendering/BaseGPURenderer.svelte.js +4 -1
  32. package/dist/components/rendering/Dots.svelte +24 -3
  33. package/dist/components/rendering/Dots.svelte.d.ts +2 -0
  34. package/dist/components/rendering/GridLine.svelte +2 -1
  35. package/dist/components/rendering/Lines.svelte +49 -7
  36. package/dist/components/rendering/Lines.svelte.d.ts +9 -0
  37. package/dist/components/rendering/ParallelLines.svelte +26 -7
  38. package/dist/components/rendering/ParallelLines.svelte.d.ts +2 -0
  39. package/dist/components/rendering/Rects.svelte +20 -5
  40. package/dist/components/rendering/Rects.svelte.d.ts +2 -0
  41. package/dist/components/rendering/RuleX.svelte +20 -5
  42. package/dist/components/rendering/RuleX.svelte.d.ts +2 -0
  43. package/dist/components/rendering/RuleY.svelte +20 -5
  44. package/dist/components/rendering/RuleY.svelte.d.ts +2 -0
  45. package/dist/components/rendering/activity-reads.d.ts +93 -0
  46. package/dist/components/rendering/activity-reads.js +135 -0
  47. package/dist/components/rendering/uniform-sizes.d.ts +26 -0
  48. package/dist/components/rendering/uniform-sizes.js +28 -0
  49. package/dist/config/ChartConfig.svelte.d.ts +2 -0
  50. package/dist/gpu/ColorScaleShaderLib.d.ts +13 -1
  51. package/dist/gpu/ColorScaleShaderLib.js +56 -5
  52. package/dist/gpu/GPUCoordinator.svelte.d.ts +8 -0
  53. package/dist/gpu/GPUCoordinator.svelte.js +68 -32
  54. package/dist/gpu/ResolveShader.d.ts +2 -2
  55. package/dist/gpu/ResolveShader.js +2 -2
  56. package/dist/gpu/ScaleTemplateSystem.d.ts +19 -0
  57. package/dist/gpu/ScaleTemplateSystem.js +86 -7
  58. package/dist/gpu/SizeScaleShaderLib.d.ts +4 -4
  59. package/dist/gpu/SizeScaleShaderLib.js +4 -4
  60. package/dist/gpu/identity-slot-packing.d.ts +157 -40
  61. package/dist/gpu/identity-slot-packing.js +262 -62
  62. package/dist/gpu/tracked-scales.d.ts +14 -0
  63. package/dist/gpu/tracked-scales.js +18 -0
  64. package/dist/index.js +1 -0
  65. package/dist/interactors/BandLabelToggle.svelte +5 -9
  66. package/dist/interactors/BandLabelToggle.svelte.d.ts +3 -3
  67. package/dist/interactors/DerivedInterval1D.svelte +26 -54
  68. package/dist/interactors/Interval1D.svelte +2 -1
  69. package/dist/interactors/Interval2D.svelte +2 -1
  70. package/dist/interactors/ParallelAxisSource.svelte +4 -4
  71. package/dist/interactors/ParallelAxisSource.svelte.d.ts +2 -1
  72. package/dist/interactors/ParallelBrushes.svelte +8 -9
  73. package/dist/interactors/ParallelBrushes.svelte.d.ts +3 -3
  74. package/dist/interactors/Toggle.svelte +17 -36
  75. package/dist/interactors/Toggle.svelte.d.ts +4 -3
  76. package/dist/interactors/Tooltip.svelte +2 -1
  77. package/dist/interactors/TooltipManager.svelte.js +8 -3
  78. package/dist/interactors/relay-clauses.d.ts +23 -0
  79. package/dist/interactors/relay-clauses.js +60 -0
  80. package/dist/query-engine/CrossfilterSelection.svelte.d.ts +88 -15
  81. package/dist/query-engine/CrossfilterSelection.svelte.js +94 -20
  82. package/dist/query-engine/FieldActivity.svelte.d.ts +79 -0
  83. package/dist/query-engine/FieldActivity.svelte.js +207 -0
  84. package/dist/query-engine/FilterGroup.svelte.d.ts +53 -29
  85. package/dist/query-engine/FilterGroup.svelte.js +181 -122
  86. package/dist/query-engine/FilterGroupRegistry.svelte.d.ts +42 -0
  87. package/dist/query-engine/FilterGroupRegistry.svelte.js +103 -1
  88. package/dist/query-engine/MarkHandle.svelte.d.ts +9 -1
  89. package/dist/query-engine/MarkHandle.svelte.js +12 -2
  90. package/dist/query-engine/QueryEngine.d.ts +52 -16
  91. package/dist/query-engine/QueryEngine.js +90 -28
  92. package/dist/query-engine/categorical-identity.d.ts +99 -27
  93. package/dist/query-engine/categorical-identity.js +210 -51
  94. package/dist/query-engine/index.d.ts +2 -1
  95. package/dist/query-engine/index.js +1 -0
  96. package/dist/query-engine/preagg/Preagg.d.ts +1 -1
  97. package/dist/query-engine/preagg/Preagg.js +11 -9
  98. package/dist/query-engine/preagg/plan.d.ts +9 -5
  99. package/dist/query-engine/preagg/plan.js +13 -11
  100. package/dist/query-engine/toggle-predicate.js +12 -1
  101. package/dist/scales/ActivityTable.d.ts +99 -0
  102. package/dist/scales/ActivityTable.js +211 -0
  103. package/dist/scales/BandScaleState.svelte.d.ts +28 -7
  104. package/dist/scales/BandScaleState.svelte.js +55 -20
  105. package/dist/scales/ChartScales.svelte.d.ts +2 -2
  106. package/dist/scales/ColorScaleState.svelte.d.ts +36 -8
  107. package/dist/scales/ColorScaleState.svelte.js +118 -24
  108. package/dist/scales/DomainResolver.d.ts +2 -0
  109. package/dist/scales/DomainResolver.js +15 -8
  110. package/dist/scales/ScaleRegistry.svelte.d.ts +9 -0
  111. package/dist/scales/ScaleRegistry.svelte.js +22 -1
  112. package/dist/scales/TimeScaleState.svelte.js +7 -1
  113. package/dist/spec/index.js +1 -0
  114. package/dist/spec/types.d.ts +2 -0
  115. package/dist/transforms/index.d.ts +27 -0
  116. package/dist/transforms/index.js +97 -3
  117. package/dist/types.d.ts +30 -0
  118. package/package.json +1 -1
@@ -37,7 +37,8 @@
37
37
  filterGroup: FilterGroup;
38
38
  column: string;
39
39
  chartId: string;
40
- excludeMarkIds: () => Set<string>;
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, excludeMarkIds, scaleState, pixelExtent, axes }: Props =
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
- excludeMarkIds,
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
- excludeMarkIds: () => Set<string>;
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 parallel chart excludes itself from its own brush filters
23
- * (standard crossfilter behavior). When false (default), brushes self-filter
24
- * the chart — typical for parallel coordinates. */
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: when cross=true, exclude chart's own marks (standard crossfilter).
109
- // When cross=false (default for parallel coords), empty set — all marks self-filter.
110
- const excludeMarkIds = cross
111
- ? () => new Set(chart.config.entries.keys())
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
- {excludeMarkIds}
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 parallel chart excludes itself from its own brush filters
5
- * (standard crossfilter behavior). When false (default), brushes self-filter
6
- * the chart — typical for parallel coordinates. */
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 { ScaleRegistry } from '../scales/ScaleRegistry.svelte';
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 source chart is excluded
20
- * from its own toggles (crossfilter semantics) and dims instead via scale
21
- * active-value projection; other charts in the filter group filter.
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
- /** Marks in this chart — excluded so the source dims rather than filters. */
42
- const excludeMarkIds = () => new Set(chart.config.entries.keys());
43
-
44
- /** Scales keyed by the toggled field: its color scale and any band scale
45
- * whose source column is that field. Never a band scale for another field. */
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
- const value = id == null ? null : (bandState.categoryAt(id) ?? null);
96
- return value == null ? null : { field: bandState.sourceField, value };
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
- if (tuple.some((v) => v == null)) return;
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 source chart is excluded
10
- * from its own toggles (crossfilter semantics) and dims instead via scale
11
- * active-value projection; other charts in the filter group filter.
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
- if (category == null)
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
- return this.#bandScale(key)?.categoryAt(value) ?? value;
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
- point[fieldName] = this.#bandScale(key)?.categoryAt(value) ?? value;
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. Marks derive their filter via predicate(markId), which
6
- * returns all active predicates except those that exclude that mark.
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
- * Cross-filtering is chart-level: interactors pass excludeMarkIds (the set
9
- * of all marks in the source chart). When a brush in chart A fires, all
10
- * marks in chart A skip that clause. All other marks receive it.
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
- /** A clause stored in the selection. */
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
- * Compute the filter predicate for a given mark.
36
- * Returns all active predicates except those whose excludeMarkIds
37
- * contains the given markId.
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
- predicate(markId: string): FilterExpr[];
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): void;
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(markId: string, activeSourceId: string): Clause[];
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(markId: string, activeSourceId: string): FilterExpr[];
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(markId: string, activeSourceId: string, then: Clause[]): boolean;
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. */