@fjader/svelte 0.2.3 → 0.2.4

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 (87) hide show
  1. package/README.md +2 -0
  2. package/dist/components/axes/AxisBottom.svelte +23 -6
  3. package/dist/components/axes/AxisBottom.svelte.d.ts +10 -0
  4. package/dist/components/axes/AxisLeft.svelte +77 -8
  5. package/dist/components/axes/AxisLeft.svelte.d.ts +17 -0
  6. package/dist/components/axes/BandAxisTicks.svelte +1 -1
  7. package/dist/components/axes/axis-title.d.ts +30 -0
  8. package/dist/components/axes/axis-title.js +77 -0
  9. package/dist/components/charts/ChartCell.svelte +1 -1
  10. package/dist/components/charts/ChartContainer.svelte +10 -2
  11. package/dist/components/charts/ChartContainer.svelte.d.ts +7 -0
  12. package/dist/components/guides/legend/ContinuousColorLegend.svelte +7 -1
  13. package/dist/components/guides/legend/OrdinalColorLegend.svelte +8 -5
  14. package/dist/components/marks/AreaMarkX.svelte +2 -0
  15. package/dist/components/marks/AreaMarkY.svelte +2 -0
  16. package/dist/components/marks/BarMarkX.svelte +13 -1
  17. package/dist/components/marks/BarMarkY.svelte +13 -1
  18. package/dist/components/marks/DotsMark.svelte +2 -0
  19. package/dist/components/marks/LineMark.svelte +5 -1
  20. package/dist/components/marks/ParallelCoordinatesMark.svelte +4 -0
  21. package/dist/components/marks/ParallelCoordinatesMark.svelte.d.ts +2 -0
  22. package/dist/components/marks/RectMark.svelte +2 -0
  23. package/dist/components/marks/RectXMark.svelte +2 -0
  24. package/dist/components/marks/RectYMark.svelte +2 -0
  25. package/dist/components/marks/RuleMarkX.svelte +2 -0
  26. package/dist/components/marks/RuleMarkY.svelte +2 -0
  27. package/dist/components/marks/TextMark.svelte +8 -9
  28. package/dist/components/marks/lifecycle/MarkLifecycleV2.svelte.d.ts +6 -1
  29. package/dist/components/marks/lifecycle/MarkLifecycleV2.svelte.js +187 -18
  30. package/dist/components/marks/lifecycle/domain-queries.d.ts +3 -1
  31. package/dist/components/marks/lifecycle/domain-queries.js +11 -6
  32. package/dist/components/marks/lifecycle/domain-registration.js +2 -6
  33. package/dist/components/marks/lifecycle/other-rows.svelte.d.ts +104 -0
  34. package/dist/components/marks/lifecycle/other-rows.svelte.js +193 -0
  35. package/dist/components/marks/lifecycle/other-split.d.ts +80 -0
  36. package/dist/components/marks/lifecycle/other-split.js +112 -0
  37. package/dist/components/marks/lifecycle/query-builder.d.ts +26 -1
  38. package/dist/components/marks/lifecycle/query-builder.js +25 -5
  39. package/dist/components/marks/utils/reactive-config.d.ts +1 -1
  40. package/dist/components/marks/utils/reactive-config.js +6 -0
  41. package/dist/components/marks/utils/tooltip-helpers.d.ts +4 -1
  42. package/dist/components/marks/utils/tooltip-helpers.js +2 -1
  43. package/dist/components/rendering/BaseGPURenderer.svelte.d.ts +3 -1
  44. package/dist/components/rendering/BaseGPURenderer.svelte.js +4 -2
  45. package/dist/components/rendering/Rects.svelte +117 -23
  46. package/dist/components/rendering/Rects.svelte.d.ts +7 -0
  47. package/dist/components/rendering/activity-reads.d.ts +18 -0
  48. package/dist/components/rendering/activity-reads.js +19 -0
  49. package/dist/config/ChartConfig.svelte.d.ts +3 -1
  50. package/dist/gpu/MorphedChannels.d.ts +6 -32
  51. package/dist/gpu/MorphedChannels.js +45 -3
  52. package/dist/gpu/ScaleTemplateSystem.js +25 -4
  53. package/dist/index.d.ts +2 -0
  54. package/dist/index.js +3 -0
  55. package/dist/interactors/Interval1D.svelte +6 -1
  56. package/dist/interactors/Interval2D.svelte +6 -1
  57. package/dist/interactors/ParallelAxisSource.svelte +10 -1
  58. package/dist/interactors/Toggle.svelte +32 -3
  59. package/dist/interactors/Tooltip.svelte +138 -41
  60. package/dist/interactors/TooltipManager.svelte.d.ts +20 -5
  61. package/dist/interactors/TooltipManager.svelte.js +32 -7
  62. package/dist/interactors/other-tooltip.d.ts +16 -0
  63. package/dist/interactors/other-tooltip.js +22 -0
  64. package/dist/query-engine/FieldActivity.svelte.d.ts +50 -1
  65. package/dist/query-engine/FieldActivity.svelte.js +176 -5
  66. package/dist/query-engine/FilterGroup.svelte.d.ts +23 -1
  67. package/dist/query-engine/FilterGroup.svelte.js +43 -3
  68. package/dist/query-engine/FilterGroupRegistry.svelte.d.ts +31 -3
  69. package/dist/query-engine/FilterGroupRegistry.svelte.js +62 -15
  70. package/dist/query-engine/QueryEngine.js +26 -6
  71. package/dist/query-engine/categorical-identity.d.ts +15 -0
  72. package/dist/query-engine/categorical-identity.js +15 -0
  73. package/dist/query-engine/preagg/plan.d.ts +5 -0
  74. package/dist/query-engine/preagg/plan.js +31 -7
  75. package/dist/query-engine/toggle-predicate.d.ts +21 -0
  76. package/dist/query-engine/toggle-predicate.js +29 -1
  77. package/dist/scales/ActivityTable.d.ts +5 -1
  78. package/dist/scales/ActivityTable.js +12 -3
  79. package/dist/scales/BandScaleState.svelte.d.ts +13 -8
  80. package/dist/scales/BandScaleState.svelte.js +23 -16
  81. package/dist/spec/types.d.ts +2 -0
  82. package/dist/theme.css +5 -4
  83. package/dist/transforms/TransformCompiler.d.ts +37 -2
  84. package/dist/transforms/TransformCompiler.js +44 -1
  85. package/dist/transforms/index.js +32 -18
  86. package/dist/types.d.ts +41 -0
  87. package/package.json +3 -2
package/README.md CHANGED
@@ -16,6 +16,8 @@ pip install fjader # Python: marimo and Jupyter
16
16
  pip install "fjader[streamlit]" # Python: Streamlit
17
17
  ```
18
18
 
19
+ `@uwdata/mosaic-sql` is a peer dependency (0.34 or 0.35), so your app and fjader share one copy. Two copies would not recognise each other's expressions: an aggregate such as `sum(...)` written with one would be misread by the other.
20
+
19
21
  fjader needs Chrome 149 or later, or another Chromium browser on that version. It relies on two recent features other browsers don't ship yet: WebGPU's `immediate_address_space`, through which every chart passes its scales to the shaders (so every zoom and pan uses it), and the `Temporal` API, used for calendar offsets. There is no fallback renderer.
20
22
 
21
23
  Three things to know before using it in a deployed app:
@@ -8,9 +8,24 @@
8
8
  interface Props {
9
9
  ticksNumber?: number;
10
10
  tickFormat?: ((value: unknown) => string) | null;
11
+ /**
12
+ * Draw the scale's label as the axis title (default). `false` draws none; the
13
+ * label still names the scale, so charts sharing it still link their zoom.
14
+ */
15
+ title?: boolean;
16
+ /**
17
+ * Where along the axis the title sits: `'center'` (default), or at the plot's
18
+ * `'start'` (left) or `'end'` (right) edge.
19
+ */
20
+ titleAnchor?: 'start' | 'center' | 'end';
11
21
  }
12
22
 
13
- let { ticksNumber = 5, tickFormat = null }: Props = $props();
23
+ let {
24
+ ticksNumber = 5,
25
+ tickFormat = null,
26
+ title = true,
27
+ titleAnchor = 'center'
28
+ }: Props = $props();
14
29
  const c = getContext<ChartContext>('chart');
15
30
 
16
31
  // Propagate custom format to scale state once it exists (for MarginLabel, tooltips, etc.)
@@ -47,10 +62,6 @@
47
62
  {tickFormatter(tick)}
48
63
  </text>
49
64
  {/each}
50
-
51
- <text text-anchor="middle" x={c.innerWidth / 2} y={28}>
52
- {c.xLabel}
53
- </text>
54
65
  {:else if c.scaleTypes.x === 'band' && c.xScale}
55
66
  <BandAxisTicks
56
67
  scale={c.xScaleState as BandScaleState}
@@ -60,8 +71,14 @@
60
71
  tickLength={5}
61
72
  labelGap={16}
62
73
  />
74
+ {/if}
63
75
 
64
- <text text-anchor="middle" x={c.innerWidth / 2} y={28}>
76
+ {#if title && c.xScale}
77
+ <text
78
+ text-anchor={titleAnchor === 'start' ? 'start' : titleAnchor === 'end' ? 'end' : 'middle'}
79
+ x={titleAnchor === 'start' ? 0 : titleAnchor === 'end' ? c.innerWidth : c.innerWidth / 2}
80
+ y={28}
81
+ >
65
82
  {c.xLabel}
66
83
  </text>
67
84
  {/if}
@@ -1,6 +1,16 @@
1
1
  interface Props {
2
2
  ticksNumber?: number;
3
3
  tickFormat?: ((value: unknown) => string) | null;
4
+ /**
5
+ * Draw the scale's label as the axis title (default). `false` draws none; the
6
+ * label still names the scale, so charts sharing it still link their zoom.
7
+ */
8
+ title?: boolean;
9
+ /**
10
+ * Where along the axis the title sits: `'center'` (default), or at the plot's
11
+ * `'start'` (left) or `'end'` (right) edge.
12
+ */
13
+ titleAnchor?: 'start' | 'center' | 'end';
4
14
  }
5
15
  declare const AxisBottom: import("svelte").Component<Props, {}, "">;
6
16
  type AxisBottom = ReturnType<typeof AxisBottom>;
@@ -4,13 +4,38 @@
4
4
  import type { BandScaleState } from '../../scales/BandScaleState.svelte';
5
5
  import BandAxisTicks from './BandAxisTicks.svelte';
6
6
  import type { ContinuousScaleState } from '../../types';
7
+ import { categoryLabel } from '../../query-engine/categorical-identity';
8
+ import { clearTitleOffset, sameTicks, textWidth } from './axis-title';
7
9
 
8
10
  interface Props {
9
11
  ticksNumber?: number;
10
12
  tickFormat?: ((value: unknown) => string) | null;
13
+ /**
14
+ * Draw the scale's label as the axis title (default). `false` draws none; the
15
+ * label still names the scale, so charts sharing it still link their zoom.
16
+ */
17
+ title?: boolean;
18
+ /**
19
+ * How far left of the axis the title sits, in px (default 32), or `'auto'`:
20
+ * clear of the widest tick label, as far as the chart's left margin allows.
21
+ */
22
+ titleOffset?: number | 'auto';
23
+ /**
24
+ * Where the title goes: `'side'` (default), rotated beside the tick labels, or
25
+ * `'top'`, horizontal above the plot at the axis. `'top'` cannot meet the tick
26
+ * labels, so it needs no `titleOffset` and no room in the left margin; it takes
27
+ * the space above the plot (the chart's top margin).
28
+ */
29
+ titlePlacement?: 'side' | 'top';
11
30
  }
12
31
 
13
- let { ticksNumber = 5, tickFormat = null }: Props = $props();
32
+ let {
33
+ ticksNumber = 5,
34
+ tickFormat = null,
35
+ title = true,
36
+ titleOffset = 32,
37
+ titlePlacement = 'side'
38
+ }: Props = $props();
14
39
  const c = getContext<ChartContext>('chart');
15
40
 
16
41
  // Propagate custom format to scale state once it exists (for MarginLabel, tooltips, etc.)
@@ -34,6 +59,43 @@
34
59
 
35
60
  // Unified formatter interface - all scale states have tickFormat()
36
61
  const tickFormatter = $derived(c.yScaleState?.tickFormat ? c.yScaleState.tickFormat() : String);
62
+
63
+ // The title's distance from the axis. Measured only for 'auto', from the labels
64
+ // drawn (each distinct string once, then cached). It follows the widest label
65
+ // seen on this scale and never moves back in: pan and zoom change the ticks
66
+ // all the time, and a title that followed them would jitter. Taking the
67
+ // maximum is idempotent, so re-evaluating this changes nothing.
68
+ let widestSeen = 0;
69
+ let widestScale: unknown = null;
70
+ // The ticks and formatter the widest was last taken from: a pan that only moves
71
+ // the same ticks measures (and formats) nothing.
72
+ let measuredTicks: ArrayLike<unknown> | null = null;
73
+ let measuredFormatter: unknown = null;
74
+ const titleX = $derived.by(() => {
75
+ if (titleOffset !== 'auto') return titleOffset;
76
+ const scale = c.yScaleState;
77
+ if (scale !== widestScale) {
78
+ widestScale = scale;
79
+ widestSeen = 0;
80
+ measuredTicks = null;
81
+ }
82
+ const band = c.scaleTypes?.y === 'band';
83
+ const ticks: ArrayLike<unknown> = band ? bandTicks : continuousTicks;
84
+ if (!sameTicks(measuredTicks, ticks) || (!band && measuredFormatter !== tickFormatter)) {
85
+ if (band) {
86
+ for (const tick of bandTicks) {
87
+ widestSeen = Math.max(widestSeen, textWidth(String(categoryLabel(tick.value))));
88
+ }
89
+ } else {
90
+ for (const tick of continuousTicks) {
91
+ widestSeen = Math.max(widestSeen, textWidth(String(tickFormatter(tick))));
92
+ }
93
+ }
94
+ measuredTicks = ticks;
95
+ measuredFormatter = tickFormatter;
96
+ }
97
+ return clearTitleOffset(widestSeen, c.margin.left);
98
+ });
37
99
  </script>
38
100
 
39
101
  {#if c.isReady && c.scaleTypes}
@@ -47,10 +109,6 @@
47
109
  {tickFormatter(tick)}
48
110
  </text>
49
111
  {/each}
50
-
51
- <text text-anchor="middle" transform={`translate(-32, ${c.innerHeight / 2}) rotate(-90)`}>
52
- {c.yLabel}
53
- </text>
54
112
  {:else if c.scaleTypes.y === 'band' && c.yScale}
55
113
  <BandAxisTicks
56
114
  scale={c.yScaleState as BandScaleState}
@@ -60,10 +118,21 @@
60
118
  dir={-1}
61
119
  textAnchor="end"
62
120
  />
121
+ {/if}
63
122
 
64
- <text text-anchor="middle" transform={`translate(-32, ${c.innerHeight / 2}) rotate(-90)`}>
65
- {c.yLabel}
66
- </text>
123
+ {#if title && c.yScale}
124
+ {#if titlePlacement === 'top'}
125
+ <text text-anchor="start" x={0} y={-18}>
126
+ {c.yLabel}
127
+ </text>
128
+ {:else}
129
+ <text
130
+ text-anchor="middle"
131
+ transform={`translate(${-titleX}, ${c.innerHeight / 2}) rotate(-90)`}
132
+ >
133
+ {c.yLabel}
134
+ </text>
135
+ {/if}
67
136
  {/if}
68
137
  </g>
69
138
  {/if}
@@ -1,6 +1,23 @@
1
1
  interface Props {
2
2
  ticksNumber?: number;
3
3
  tickFormat?: ((value: unknown) => string) | null;
4
+ /**
5
+ * Draw the scale's label as the axis title (default). `false` draws none; the
6
+ * label still names the scale, so charts sharing it still link their zoom.
7
+ */
8
+ title?: boolean;
9
+ /**
10
+ * How far left of the axis the title sits, in px (default 32), or `'auto'`:
11
+ * clear of the widest tick label, as far as the chart's left margin allows.
12
+ */
13
+ titleOffset?: number | 'auto';
14
+ /**
15
+ * Where the title goes: `'side'` (default), rotated beside the tick labels, or
16
+ * `'top'`, horizontal above the plot at the axis. `'top'` cannot meet the tick
17
+ * labels, so it needs no `titleOffset` and no room in the left margin; it takes
18
+ * the space above the plot (the chart's top margin).
19
+ */
20
+ titlePlacement?: 'side' | 'top';
4
21
  }
5
22
  declare const AxisLeft: import("svelte").Component<Props, {}, "">;
6
23
  type AxisLeft = ReturnType<typeof AxisLeft>;
@@ -105,7 +105,7 @@
105
105
  </script>
106
106
 
107
107
  {#each ticks as tick (tick.key)}
108
- {@const pos = scale.tickPosition(tick.key, rangeSize, axis)}
108
+ {@const pos = scale.tickPosition(tick.key, rangeSize)}
109
109
  <g opacity={opacityOf(tick)}>
110
110
  {#if axis === 'x'}
111
111
  <line x1={pos} x2={pos} y1={base} y2={tickEnd} />
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Where an axis title sits relative to its tick labels.
3
+ *
4
+ * The left axis puts its tick labels right-anchored 9px from the axis line and
5
+ * its title, rotated, to the left of them. A fixed offset is right for short
6
+ * labels and runs into wide ones (`40,000`, a band name). `'auto'` places the
7
+ * title clear of the widest label, within the margin the chart gave it.
8
+ */
9
+ /** The title's offset when nothing says otherwise, and the least `'auto'` uses. */
10
+ export declare const DEFAULT_TITLE_OFFSET = 32;
11
+ /**
12
+ * The width of a tick label in px. Measured on a canvas, once per distinct
13
+ * string (a band axis has the same labels frame after frame); 6px a character
14
+ * where there is no document. An estimate by design: the page's own font can
15
+ * differ a little, and CLEARANCE covers that.
16
+ */
17
+ export declare function textWidth(text: string): number;
18
+ /**
19
+ * How far left of the axis line the title's baseline goes, clear of a label
20
+ * `widest` px wide: never less than the default, and never so far that its body
21
+ * leaves the margin. A margin too narrow for the labels leaves them overlapping,
22
+ * and only a wider margin fixes that.
23
+ */
24
+ export declare function clearTitleOffset(widest: number, marginLeft: number): number;
25
+ /**
26
+ * Whether two tick lists are the same ticks: equal values, or band ticks of the
27
+ * same key. A loop with no allocation, to skip re-measuring labels when a pan
28
+ * moved the ticks but did not change them.
29
+ */
30
+ export declare function sameTicks(a: ArrayLike<unknown> | null, b: ArrayLike<unknown>): boolean;
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Where an axis title sits relative to its tick labels.
3
+ *
4
+ * The left axis puts its tick labels right-anchored 9px from the axis line and
5
+ * its title, rotated, to the left of them. A fixed offset is right for short
6
+ * labels and runs into wide ones (`40,000`, a band name). `'auto'` places the
7
+ * title clear of the widest label, within the margin the chart gave it.
8
+ */
9
+ /** Where the tick labels end, from the axis line: AxisLeft's text x and BandAxisTicks' labelGap. */
10
+ const TICK_GAP = 9;
11
+ /** Between the widest label and the title's baseline; the title's descenders reach into it. */
12
+ const CLEARANCE = 12;
13
+ /** A rotated title's glyphs sit on the far side of its baseline, away from the axis. */
14
+ const TITLE_BODY = 10;
15
+ /** The title's offset when nothing says otherwise, and the least `'auto'` uses. */
16
+ export const DEFAULT_TITLE_OFFSET = 32;
17
+ const FONT = '10px system-ui, sans-serif';
18
+ const FALLBACK_CHAR_WIDTH = 6;
19
+ const CACHE_LIMIT = 2000;
20
+ let context;
21
+ const widths = new Map();
22
+ /**
23
+ * The width of a tick label in px. Measured on a canvas, once per distinct
24
+ * string (a band axis has the same labels frame after frame); 6px a character
25
+ * where there is no document. An estimate by design: the page's own font can
26
+ * differ a little, and CLEARANCE covers that.
27
+ */
28
+ export function textWidth(text) {
29
+ const known = widths.get(text);
30
+ if (known !== undefined)
31
+ return known;
32
+ if (context === undefined) {
33
+ context =
34
+ typeof document === 'undefined' ? null : document.createElement('canvas').getContext('2d');
35
+ if (context)
36
+ context.font = FONT;
37
+ }
38
+ const width = context ? context.measureText(text).width : text.length * FALLBACK_CHAR_WIDTH;
39
+ if (widths.size >= CACHE_LIMIT)
40
+ widths.clear();
41
+ widths.set(text, width);
42
+ return width;
43
+ }
44
+ /**
45
+ * How far left of the axis line the title's baseline goes, clear of a label
46
+ * `widest` px wide: never less than the default, and never so far that its body
47
+ * leaves the margin. A margin too narrow for the labels leaves them overlapping,
48
+ * and only a wider margin fixes that.
49
+ */
50
+ export function clearTitleOffset(widest, marginLeft) {
51
+ const wanted = Math.max(TICK_GAP + widest + CLEARANCE, DEFAULT_TITLE_OFFSET);
52
+ return Math.max(0, Math.min(wanted, marginLeft - TITLE_BODY));
53
+ }
54
+ /**
55
+ * Whether two tick lists are the same ticks: equal values, or band ticks of the
56
+ * same key. A loop with no allocation, to skip re-measuring labels when a pan
57
+ * moved the ticks but did not change them.
58
+ */
59
+ export function sameTicks(a, b) {
60
+ if (!a || a.length !== b.length)
61
+ return false;
62
+ for (let i = 0; i < b.length; i++) {
63
+ const x = a[i];
64
+ const y = b[i];
65
+ if (x === y)
66
+ continue;
67
+ if (typeof x === 'object' &&
68
+ typeof y === 'object' &&
69
+ x !== null &&
70
+ y !== null &&
71
+ x.key !== undefined &&
72
+ x.key === y.key)
73
+ continue;
74
+ return false;
75
+ }
76
+ return true;
77
+ }
@@ -897,7 +897,7 @@
897
897
  if (overBrush) {
898
898
  tooltipManager?.updatePosition(null, null);
899
899
  } else {
900
- tooltipManager?.updatePointerScreen(x, y);
900
+ tooltipManager?.updatePointerClient(x + svgLeft, y + svgTop);
901
901
  tooltipManager?.updatePosition(
902
902
  scales.xScaleState?.currentDataPoint,
903
903
  scales.yScaleState?.currentDataPoint
@@ -78,6 +78,13 @@
78
78
  clipPadding?: number;
79
79
  useXSelection?: boolean;
80
80
  useYSelection?: boolean;
81
+ /**
82
+ * The tooltip's colours against the color-scheme the chart inherits:
83
+ * `contrast` is the opposite scheme (dark on a light page), `follow` the
84
+ * same. Unset, the nearest `data-fjader-tooltip` ancestor decides, else
85
+ * contrast. `--fjader-tooltip-bg`/`-text` set by the app override both.
86
+ */
87
+ tooltipScheme?: 'contrast' | 'follow';
81
88
  }
82
89
 
83
90
  let {
@@ -108,7 +115,8 @@
108
115
  margin: marginOverride = {},
109
116
  clipPadding = 0,
110
117
  useXSelection = false,
111
- useYSelection = false
118
+ useYSelection = false,
119
+ tooltipScheme = undefined
112
120
  }: Props = $props();
113
121
 
114
122
  const defaultMargin = { top: 30, right: 50, bottom: 35, left: 75 };
@@ -452,7 +460,7 @@
452
460
  setContext('legend-position', () => legendPosition);
453
461
  </script>
454
462
 
455
- <div class="figure-container" bind:clientWidth={layout.width}>
463
+ <div class="figure-container" bind:clientWidth={layout.width} data-fjader-tooltip={tooltipScheme}>
456
464
  {#if legend && legendPosition === 'top'}
457
465
  <div
458
466
  class="legend-container legend-top"
@@ -69,6 +69,13 @@ interface Props {
69
69
  clipPadding?: number;
70
70
  useXSelection?: boolean;
71
71
  useYSelection?: boolean;
72
+ /**
73
+ * The tooltip's colours against the color-scheme the chart inherits:
74
+ * `contrast` is the opposite scheme (dark on a light page), `follow` the
75
+ * same. Unset, the nearest `data-fjader-tooltip` ancestor decides, else
76
+ * contrast. `--fjader-tooltip-bg`/`-text` set by the app override both.
77
+ */
78
+ tooltipScheme?: 'contrast' | 'follow';
72
79
  }
73
80
  declare const ChartContainer: import("svelte").Component<Props, {}, "">;
74
81
  type ChartContainer = ReturnType<typeof ChartContainer>;
@@ -194,7 +194,13 @@
194
194
  column: scaleState.field,
195
195
  label: scaleState.field,
196
196
  values: value ? [(value as number[])[0], (value as number[])[1]] : []
197
- })
197
+ }),
198
+ // The clause was removed from outside (clearAll, a filter pill): take the
199
+ // brush down too, which only this legend holds.
200
+ onCleared: () => {
201
+ brushT0 = null;
202
+ brushT1 = null;
203
+ }
198
204
  })
199
205
  : null;
200
206
 
@@ -65,7 +65,7 @@
65
65
  id: number | null;
66
66
  value: DimValue;
67
67
  color: string;
68
- /** An Other bucket: shown, not clickable (it names no value). */
68
+ /** An Other bucket: a click selects what it holds (see toggleOther). */
69
69
  bucket: boolean;
70
70
  }
71
71
 
@@ -128,8 +128,11 @@
128
128
  onDestroy(() => toggle?.destroy());
129
129
 
130
130
  // Shift/meta-click adds or removes; selecting every value clears instead.
131
- function handleClick(value: DimValue, event: MouseEvent) {
132
- toggle?.toggle([value], event.shiftKey || event.metaKey, scaleState.domain.length);
131
+ function handleClick(item: Item, event: MouseEvent) {
132
+ const multi = event.shiftKey || event.metaKey;
133
+ // An Other bucket names no value: the click selects what it holds.
134
+ if (item.bucket && item.id !== null) toggle?.toggleOther(item.id, null, multi);
135
+ else toggle?.toggle([item.value], multi, scaleState.domain.length);
133
136
  }
134
137
  </script>
135
138
 
@@ -149,11 +152,11 @@
149
152
  >
150
153
  {#each items as item (item.id ?? item.value)}
151
154
  {@const { value, color } = item}
152
- {#if isInteractive && !item.bucket}
155
+ {#if isInteractive}
153
156
  <button
154
157
  class="legend-item legend-item-interactive"
155
158
  style:opacity={opacityOf(item)}
156
- onclick={(e) => handleClick(value, e)}
159
+ onclick={(e) => handleClick(item, e)}
157
160
  >
158
161
  <span
159
162
  class="legend-swatch"
@@ -48,6 +48,7 @@
48
48
  table,
49
49
  selection = undefined,
50
50
  selectionResponse = undefined,
51
+ nulls = undefined,
51
52
  y,
52
53
  x,
53
54
  x1: x1Prop,
@@ -101,6 +102,7 @@
101
102
  transforms: () => transforms,
102
103
  selection: () => selection,
103
104
  selectionResponse: () => selectionResponse,
105
+ nulls: () => nulls,
104
106
  limit: () => limit,
105
107
  preferredXScaleType: () => preferredXScaleType,
106
108
  preferredYScaleType: () => preferredYScaleType,
@@ -48,6 +48,7 @@
48
48
  table,
49
49
  selection = undefined,
50
50
  selectionResponse = undefined,
51
+ nulls = undefined,
51
52
  x,
52
53
  y,
53
54
  y1: y1Prop,
@@ -101,6 +102,7 @@
101
102
  transforms: () => transforms,
102
103
  selection: () => selection,
103
104
  selectionResponse: () => selectionResponse,
105
+ nulls: () => nulls,
104
106
  limit: () => limit,
105
107
  preferredXScaleType: () => preferredXScaleType,
106
108
  preferredYScaleType: () => preferredYScaleType,
@@ -41,6 +41,8 @@
41
41
  table,
42
42
  selection = undefined,
43
43
  selectionResponse = undefined,
44
+ nulls = undefined,
45
+ contributeDomain = undefined,
44
46
  x,
45
47
  x1: x1Prop,
46
48
  x2: x2Prop,
@@ -83,6 +85,8 @@
83
85
  transforms: () => transforms,
84
86
  selection: () => selection,
85
87
  selectionResponse: () => selectionResponse,
88
+ nulls: () => nulls,
89
+ contributeDomain: () => contributeDomain,
86
90
  limit: () => limit,
87
91
  padding: () => padding,
88
92
  paddingInner: () => paddingInner,
@@ -280,7 +284,14 @@
280
284
  colorColumn: 'fill',
281
285
  bandAxes: { y: true },
282
286
  markColor,
283
- displayFields: resolvedTip.displayFields
287
+ displayFields: resolvedTip.displayFields,
288
+ otherStats: (point) => {
289
+ const id = point.__bandIdY;
290
+ const facet = point.__facetId;
291
+ return typeof id === 'number'
292
+ ? setup!.otherRows.statsAt(id, typeof facet === 'number' ? facet : null)
293
+ : null;
294
+ }
284
295
  })
285
296
  );
286
297
  });
@@ -303,6 +314,7 @@
303
314
  {y1Data}
304
315
  colorValueData={markColor.data}
305
316
  alphaData={opacityData}
317
+ share={setup?.otherRows.split ?? null}
306
318
  colorMode={markColor.colorMode}
307
319
  colorScaleIndex={markColor.colorScaleIndex}
308
320
  groupIndex={setup?.groupIndex ?? 0}
@@ -40,6 +40,8 @@
40
40
  table,
41
41
  selection = undefined,
42
42
  selectionResponse = undefined,
43
+ nulls = undefined,
44
+ contributeDomain = undefined,
43
45
  x,
44
46
  y,
45
47
  y1: y1Prop,
@@ -82,6 +84,8 @@
82
84
  transforms: () => transforms,
83
85
  selection: () => selection,
84
86
  selectionResponse: () => selectionResponse,
87
+ nulls: () => nulls,
88
+ contributeDomain: () => contributeDomain,
85
89
  limit: () => limit,
86
90
  padding: () => padding,
87
91
  paddingInner: () => paddingInner,
@@ -281,7 +285,14 @@
281
285
  colorColumn: 'fill',
282
286
  bandAxes: { x: true },
283
287
  markColor,
284
- displayFields: resolvedTip.displayFields
288
+ displayFields: resolvedTip.displayFields,
289
+ otherStats: (point) => {
290
+ const id = point.__bandIdX;
291
+ const facet = point.__facetId;
292
+ return typeof id === 'number'
293
+ ? setup!.otherRows.statsAt(id, typeof facet === 'number' ? facet : null)
294
+ : null;
295
+ }
285
296
  })
286
297
  );
287
298
  });
@@ -304,6 +315,7 @@
304
315
  {y1Data}
305
316
  colorValueData={markColor.data}
306
317
  alphaData={opacityData}
318
+ share={setup?.otherRows.split ?? null}
307
319
  colorMode={markColor.colorMode}
308
320
  colorScaleIndex={markColor.colorScaleIndex}
309
321
  groupIndex={setup?.groupIndex ?? 0}
@@ -43,6 +43,7 @@
43
43
  table,
44
44
  selection = undefined,
45
45
  selectionResponse = undefined,
46
+ nulls = undefined,
46
47
  x,
47
48
  y,
48
49
  fill = null,
@@ -82,6 +83,7 @@
82
83
  transforms: () => transforms,
83
84
  selection: () => selection,
84
85
  selectionResponse: () => selectionResponse,
86
+ nulls: () => nulls,
85
87
  limit: () => limit,
86
88
  preferredXScaleType: () => preferredXScaleType,
87
89
  preferredYScaleType: () => preferredYScaleType,
@@ -71,6 +71,7 @@
71
71
  table,
72
72
  selection = undefined,
73
73
  selectionResponse = undefined,
74
+ nulls = undefined,
74
75
  x,
75
76
  y,
76
77
  stroke = null,
@@ -124,6 +125,7 @@
124
125
  transforms: () => transforms,
125
126
  selection: () => selection,
126
127
  selectionResponse: () => selectionResponse,
128
+ nulls: () => nulls,
127
129
  limit: () => limit,
128
130
  preferredXScaleType: () => preferredXScaleType,
129
131
  preferredYScaleType: () => preferredYScaleType,
@@ -146,7 +148,9 @@
146
148
  inferMarkScaleType(entry.y, entry.table, entry.preferredYScaleType)
147
149
  );
148
150
 
149
- // Series grouping: z overrides stroke for grouping
151
+ // Series grouping: z overrides stroke for grouping. A selection dims a line
152
+ // by its stroke colour only; dimming by a separate z field is not supported
153
+ // yet (see activityReads in activity-reads.ts).
150
154
  const seriesChannelKey = entry.z != null ? 'z' : 'stroke';
151
155
  const markSeries = new MarkSeries(
152
156
  () => entry.z ?? entry.stroke,
@@ -12,6 +12,7 @@
12
12
 
13
13
  <script lang="ts">
14
14
  import type { SelectionResponse } from '../rendering/activity-reads';
15
+ import type { NullsResponse } from '../../types';
15
16
  import { getContext, onDestroy, untrack } from 'svelte';
16
17
  import { MarkLifecycleV2 } from './lifecycle/MarkLifecycleV2.svelte';
17
18
  import { ParallelLines } from '../rendering';
@@ -34,6 +35,7 @@
34
35
  selection?: string;
35
36
  /** How this mark responds to selections in its group (see BaseMarkProps). */
36
37
  selectionResponse?: SelectionResponse;
38
+ nulls?: NullsResponse;
37
39
  transforms?: Transform[];
38
40
  }
39
41
 
@@ -47,6 +49,7 @@
47
49
  curve = 'linear',
48
50
  selection = undefined,
49
51
  selectionResponse = undefined,
52
+ nulls = undefined,
50
53
  transforms = []
51
54
  }: Props = $props();
52
55
 
@@ -64,6 +67,7 @@
64
67
  scheme: () => scheme,
65
68
  selection: () => selection,
66
69
  selectionResponse: () => selectionResponse,
70
+ nulls: () => nulls,
67
71
  transforms: () => transforms
68
72
  });
69
73
 
@@ -10,6 +10,7 @@ export declare const parallelParity: {
10
10
  reverseScales: boolean;
11
11
  };
12
12
  import type { SelectionResponse } from '../rendering/activity-reads';
13
+ import type { NullsResponse } from '../../types';
13
14
  import type { FieldRef, ColorScheme, CurveType, Transform } from '../../types';
14
15
  interface Props {
15
16
  table: string;
@@ -22,6 +23,7 @@ interface Props {
22
23
  selection?: string;
23
24
  /** How this mark responds to selections in its group (see BaseMarkProps). */
24
25
  selectionResponse?: SelectionResponse;
26
+ nulls?: NullsResponse;
25
27
  transforms?: Transform[];
26
28
  }
27
29
  declare const ParallelCoordinatesMark: import("svelte").Component<Props, {}, "">;