@fjader/svelte 0.2.3 → 0.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (167) hide show
  1. package/README.md +3 -1
  2. package/dist/animation/EntranceGate.d.ts +52 -0
  3. package/dist/animation/EntranceGate.js +93 -0
  4. package/dist/animation/TransitionClock.d.ts +61 -30
  5. package/dist/animation/TransitionClock.js +67 -29
  6. package/dist/animation/transition-log.d.ts +41 -0
  7. package/dist/animation/transition-log.js +57 -0
  8. package/dist/components/axes/AxisBottom.svelte +27 -8
  9. package/dist/components/axes/AxisBottom.svelte.d.ts +10 -0
  10. package/dist/components/axes/AxisCenter.svelte +4 -2
  11. package/dist/components/axes/AxisLeft.svelte +81 -10
  12. package/dist/components/axes/AxisLeft.svelte.d.ts +17 -0
  13. package/dist/components/axes/BandAxisTicks.svelte +4 -3
  14. package/dist/components/axes/axis-title.d.ts +30 -0
  15. package/dist/components/axes/axis-title.js +77 -0
  16. package/dist/components/charts/ChartCell.svelte +33 -6
  17. package/dist/components/charts/ChartCell.svelte.d.ts +9 -0
  18. package/dist/components/charts/ChartContainer.svelte +55 -5
  19. package/dist/components/charts/ChartContainer.svelte.d.ts +14 -1
  20. package/dist/components/charts/ChartHover.svelte.d.ts +14 -0
  21. package/dist/components/charts/ChartHover.svelte.js +22 -0
  22. package/dist/components/charts/ChartVisibility.svelte.d.ts +43 -0
  23. package/dist/components/charts/ChartVisibility.svelte.js +67 -0
  24. package/dist/components/guides/Grid.svelte +12 -1
  25. package/dist/components/guides/legend/ContinuousColorLegend.svelte +7 -1
  26. package/dist/components/guides/legend/OrdinalColorLegend.svelte +8 -5
  27. package/dist/components/marks/AreaMarkX.svelte +18 -4
  28. package/dist/components/marks/AreaMarkY.svelte +18 -4
  29. package/dist/components/marks/BarMarkX.svelte +20 -4
  30. package/dist/components/marks/BarMarkY.svelte +20 -4
  31. package/dist/components/marks/DotsMark.svelte +8 -3
  32. package/dist/components/marks/LineMark.svelte +13 -6
  33. package/dist/components/marks/ParallelCoordinatesMark.svelte +11 -2
  34. package/dist/components/marks/ParallelCoordinatesMark.svelte.d.ts +4 -0
  35. package/dist/components/marks/RectMark.svelte +8 -3
  36. package/dist/components/marks/RectXMark.svelte +9 -3
  37. package/dist/components/marks/RectYMark.svelte +9 -3
  38. package/dist/components/marks/RuleMarkX.svelte +31 -25
  39. package/dist/components/marks/RuleMarkY.svelte +31 -25
  40. package/dist/components/marks/TextMark.svelte +52 -14
  41. package/dist/components/marks/lifecycle/MarkLifecycleV2.svelte.d.ts +22 -1
  42. package/dist/components/marks/lifecycle/MarkLifecycleV2.svelte.js +579 -66
  43. package/dist/components/marks/lifecycle/domain-queries.d.ts +13 -2
  44. package/dist/components/marks/lifecycle/domain-queries.js +43 -11
  45. package/dist/components/marks/lifecycle/domain-registration.d.ts +6 -1
  46. package/dist/components/marks/lifecycle/domain-registration.js +17 -20
  47. package/dist/components/marks/lifecycle/other-rows.svelte.d.ts +112 -0
  48. package/dist/components/marks/lifecycle/other-rows.svelte.js +193 -0
  49. package/dist/components/marks/lifecycle/other-split.d.ts +80 -0
  50. package/dist/components/marks/lifecycle/other-split.js +112 -0
  51. package/dist/components/marks/lifecycle/query-builder.d.ts +53 -3
  52. package/dist/components/marks/lifecycle/query-builder.js +46 -23
  53. package/dist/components/marks/utils/MarkColor.svelte.d.ts +3 -1
  54. package/dist/components/marks/utils/MarkColor.svelte.js +33 -3
  55. package/dist/components/marks/utils/MarkSeries.svelte.d.ts +14 -1
  56. package/dist/components/marks/utils/MarkSeries.svelte.js +25 -1
  57. package/dist/components/marks/utils/MarkSize.svelte.js +4 -0
  58. package/dist/components/marks/utils/reactive-config.d.ts +1 -1
  59. package/dist/components/marks/utils/reactive-config.js +9 -0
  60. package/dist/components/marks/utils/tooltip-helpers.d.ts +4 -1
  61. package/dist/components/marks/utils/tooltip-helpers.js +2 -1
  62. package/dist/components/rendering/Areas.svelte +57 -19
  63. package/dist/components/rendering/Areas.svelte.d.ts +12 -0
  64. package/dist/components/rendering/BaseGPURenderer.svelte.d.ts +3 -1
  65. package/dist/components/rendering/BaseGPURenderer.svelte.js +4 -2
  66. package/dist/components/rendering/Dots.svelte +23 -3
  67. package/dist/components/rendering/Dots.svelte.d.ts +6 -0
  68. package/dist/components/rendering/GridLine.svelte +13 -1
  69. package/dist/components/rendering/GridLine.svelte.d.ts +5 -0
  70. package/dist/components/rendering/Lines.svelte +48 -13
  71. package/dist/components/rendering/Lines.svelte.d.ts +12 -0
  72. package/dist/components/rendering/ParallelLines.svelte +5 -5
  73. package/dist/components/rendering/Rects.svelte +164 -30
  74. package/dist/components/rendering/Rects.svelte.d.ts +23 -2
  75. package/dist/components/rendering/RuleX.svelte +1 -1
  76. package/dist/components/rendering/RuleY.svelte +1 -1
  77. package/dist/components/rendering/WebGPUCanvas.svelte +4 -2
  78. package/dist/components/rendering/activity-reads.d.ts +47 -12
  79. package/dist/components/rendering/activity-reads.js +61 -19
  80. package/dist/components/rendering/uniform-sizes.d.ts +1 -1
  81. package/dist/components/rendering/uniform-sizes.js +1 -1
  82. package/dist/config/ChartConfig.svelte.d.ts +10 -1
  83. package/dist/config/ChartConfig.svelte.js +17 -0
  84. package/dist/gpu/GPUCoordinator.svelte.d.ts +31 -1
  85. package/dist/gpu/GPUCoordinator.svelte.js +164 -7
  86. package/dist/gpu/MorphedChannels.d.ts +11 -28
  87. package/dist/gpu/MorphedChannels.js +72 -8
  88. package/dist/gpu/ScaleTemplateSystem.d.ts +20 -2
  89. package/dist/gpu/ScaleTemplateSystem.js +83 -6
  90. package/dist/index.d.ts +19 -4
  91. package/dist/index.js +22 -4
  92. package/dist/interactors/Interval1D.svelte +6 -1
  93. package/dist/interactors/Interval2D.svelte +6 -1
  94. package/dist/interactors/ParallelAxisSource.svelte +10 -1
  95. package/dist/interactors/Toggle.svelte +32 -3
  96. package/dist/interactors/Tooltip.svelte +138 -41
  97. package/dist/interactors/TooltipManager.svelte.d.ts +20 -5
  98. package/dist/interactors/TooltipManager.svelte.js +46 -12
  99. package/dist/interactors/other-tooltip.d.ts +16 -0
  100. package/dist/interactors/other-tooltip.js +22 -0
  101. package/dist/query-engine/CrossfilterSelection.svelte.d.ts +4 -2
  102. package/dist/query-engine/CrossfilterSelection.svelte.js +2 -0
  103. package/dist/query-engine/FieldActivity.svelte.d.ts +49 -1
  104. package/dist/query-engine/FieldActivity.svelte.js +175 -5
  105. package/dist/query-engine/FilterGroup.svelte.d.ts +23 -1
  106. package/dist/query-engine/FilterGroup.svelte.js +43 -3
  107. package/dist/query-engine/FilterGroupRegistry.svelte.d.ts +31 -3
  108. package/dist/query-engine/FilterGroupRegistry.svelte.js +62 -15
  109. package/dist/query-engine/MarkHandle.svelte.d.ts +28 -1
  110. package/dist/query-engine/MarkHandle.svelte.js +33 -1
  111. package/dist/query-engine/QueryEngine.d.ts +225 -5
  112. package/dist/query-engine/QueryEngine.js +697 -47
  113. package/dist/query-engine/categorical-identity.d.ts +41 -0
  114. package/dist/query-engine/categorical-identity.js +145 -65
  115. package/dist/query-engine/debug/query-log.d.ts +39 -0
  116. package/dist/query-engine/debug/query-log.js +90 -0
  117. package/dist/query-engine/duckdb.d.ts +55 -8
  118. package/dist/query-engine/duckdb.js +82 -30
  119. package/dist/query-engine/index.d.ts +7 -4
  120. package/dist/query-engine/index.js +3 -3
  121. package/dist/query-engine/preagg/BuildQueue.d.ts +33 -13
  122. package/dist/query-engine/preagg/BuildQueue.js +51 -15
  123. package/dist/query-engine/preagg/Preagg.d.ts +46 -3
  124. package/dist/query-engine/preagg/Preagg.js +159 -31
  125. package/dist/query-engine/preagg/plan.d.ts +33 -1
  126. package/dist/query-engine/preagg/plan.js +158 -14
  127. package/dist/query-engine/sliced-lane.d.ts +108 -0
  128. package/dist/query-engine/sliced-lane.js +299 -0
  129. package/dist/query-engine/source-routing.d.ts +106 -0
  130. package/dist/query-engine/source-routing.js +189 -0
  131. package/dist/query-engine/toggle-predicate.d.ts +21 -0
  132. package/dist/query-engine/toggle-predicate.js +29 -1
  133. package/dist/scales/ActivityTable.d.ts +5 -1
  134. package/dist/scales/ActivityTable.js +12 -3
  135. package/dist/scales/BandScaleState.svelte.d.ts +22 -9
  136. package/dist/scales/BandScaleState.svelte.js +39 -21
  137. package/dist/scales/ChartScales.svelte.d.ts +3 -0
  138. package/dist/scales/ChartScales.svelte.js +26 -8
  139. package/dist/scales/ColorScaleState.svelte.d.ts +18 -2
  140. package/dist/scales/ColorScaleState.svelte.js +138 -26
  141. package/dist/scales/LinearScaleState.svelte.d.ts +7 -10
  142. package/dist/scales/LinearScaleState.svelte.js +21 -39
  143. package/dist/scales/ScaleGeneration.svelte.d.ts +48 -0
  144. package/dist/scales/ScaleGeneration.svelte.js +42 -0
  145. package/dist/scales/ScaleRegistry.svelte.js +3 -1
  146. package/dist/scales/SlotTransition.d.ts +15 -1
  147. package/dist/scales/SlotTransition.js +33 -4
  148. package/dist/scales/TimeScaleState.svelte.d.ts +7 -3
  149. package/dist/scales/TimeScaleState.svelte.js +7 -7
  150. package/dist/scales/color-utils.d.ts +0 -3
  151. package/dist/scales/color-utils.js +0 -3
  152. package/dist/scales/entered.d.ts +8 -0
  153. package/dist/scales/entered.js +10 -0
  154. package/dist/spec/LazyQueryEngine.js +2 -19
  155. package/dist/spec/MarkRegistry.d.ts +3 -1
  156. package/dist/spec/MarkRegistry.js +13 -1
  157. package/dist/spec/PlotFromSpec.svelte +1 -0
  158. package/dist/spec/export.js +11 -2
  159. package/dist/spec/types.d.ts +43 -13
  160. package/dist/sql/bin.d.ts +25 -0
  161. package/dist/sql/bin.js +108 -5
  162. package/dist/theme.css +5 -4
  163. package/dist/transforms/TransformCompiler.d.ts +47 -2
  164. package/dist/transforms/TransformCompiler.js +70 -1
  165. package/dist/transforms/index.js +176 -42
  166. package/dist/types.d.ts +177 -2
  167. package/package.json +4 -3
@@ -1,16 +1,42 @@
1
1
  <script lang="ts">
2
+ import { hasEntered } from '../../scales/entered';
2
3
  import { getContext } from 'svelte';
3
4
  import type { ChartContext } from '../../types';
4
5
  import type { BandScaleState } from '../../scales/BandScaleState.svelte';
5
6
  import BandAxisTicks from './BandAxisTicks.svelte';
6
7
  import type { ContinuousScaleState } from '../../types';
8
+ import { categoryLabel } from '../../query-engine/categorical-identity';
9
+ import { clearTitleOffset, sameTicks, textWidth } from './axis-title';
7
10
 
8
11
  interface Props {
9
12
  ticksNumber?: number;
10
13
  tickFormat?: ((value: unknown) => string) | null;
14
+ /**
15
+ * Draw the scale's label as the axis title (default). `false` draws none; the
16
+ * label still names the scale, so charts sharing it still link their zoom.
17
+ */
18
+ title?: boolean;
19
+ /**
20
+ * How far left of the axis the title sits, in px (default 32), or `'auto'`:
21
+ * clear of the widest tick label, as far as the chart's left margin allows.
22
+ */
23
+ titleOffset?: number | 'auto';
24
+ /**
25
+ * Where the title goes: `'side'` (default), rotated beside the tick labels, or
26
+ * `'top'`, horizontal above the plot at the axis. `'top'` cannot meet the tick
27
+ * labels, so it needs no `titleOffset` and no room in the left margin; it takes
28
+ * the space above the plot (the chart's top margin).
29
+ */
30
+ titlePlacement?: 'side' | 'top';
11
31
  }
12
32
 
13
- let { ticksNumber = 5, tickFormat = null }: Props = $props();
33
+ let {
34
+ ticksNumber = 5,
35
+ tickFormat = null,
36
+ title = true,
37
+ titleOffset = 32,
38
+ titlePlacement = 'side'
39
+ }: Props = $props();
14
40
  const c = getContext<ChartContext>('chart');
15
41
 
16
42
  // Propagate custom format to scale state once it exists (for MarginLabel, tooltips, etc.)
@@ -20,9 +46,10 @@
20
46
  (s as ContinuousScaleState).setCustomFormat(tickFormat);
21
47
  });
22
48
 
23
- // Continuous scales (linear, time): use scaleState.getTicks()
49
+ // Continuous scales (linear, time): use scaleState.getTicks(). None until the
50
+ // scale has entered: before that its domain is a placeholder.
24
51
  const continuousTicks = $derived(
25
- c.yScaleState?.getTicks ? c.yScaleState.getTicks(ticksNumber) : []
52
+ c.yScaleState?.getTicks && hasEntered(c.yScaleState) ? c.yScaleState.getTicks(ticksNumber) : []
26
53
  );
27
54
 
28
55
  // Band scales: the categories on screen NOW, at their current fractional slots,
@@ -34,6 +61,43 @@
34
61
 
35
62
  // Unified formatter interface - all scale states have tickFormat()
36
63
  const tickFormatter = $derived(c.yScaleState?.tickFormat ? c.yScaleState.tickFormat() : String);
64
+
65
+ // The title's distance from the axis. Measured only for 'auto', from the labels
66
+ // drawn (each distinct string once, then cached). It follows the widest label
67
+ // seen on this scale and never moves back in: pan and zoom change the ticks
68
+ // all the time, and a title that followed them would jitter. Taking the
69
+ // maximum is idempotent, so re-evaluating this changes nothing.
70
+ let widestSeen = 0;
71
+ let widestScale: unknown = null;
72
+ // The ticks and formatter the widest was last taken from: a pan that only moves
73
+ // the same ticks measures (and formats) nothing.
74
+ let measuredTicks: ArrayLike<unknown> | null = null;
75
+ let measuredFormatter: unknown = null;
76
+ const titleX = $derived.by(() => {
77
+ if (titleOffset !== 'auto') return titleOffset;
78
+ const scale = c.yScaleState;
79
+ if (scale !== widestScale) {
80
+ widestScale = scale;
81
+ widestSeen = 0;
82
+ measuredTicks = null;
83
+ }
84
+ const band = c.scaleTypes?.y === 'band';
85
+ const ticks: ArrayLike<unknown> = band ? bandTicks : continuousTicks;
86
+ if (!sameTicks(measuredTicks, ticks) || (!band && measuredFormatter !== tickFormatter)) {
87
+ if (band) {
88
+ for (const tick of bandTicks) {
89
+ widestSeen = Math.max(widestSeen, textWidth(String(categoryLabel(tick.value))));
90
+ }
91
+ } else {
92
+ for (const tick of continuousTicks) {
93
+ widestSeen = Math.max(widestSeen, textWidth(String(tickFormatter(tick))));
94
+ }
95
+ }
96
+ measuredTicks = ticks;
97
+ measuredFormatter = tickFormatter;
98
+ }
99
+ return clearTitleOffset(widestSeen, c.margin.left);
100
+ });
37
101
  </script>
38
102
 
39
103
  {#if c.isReady && c.scaleTypes}
@@ -47,10 +111,6 @@
47
111
  {tickFormatter(tick)}
48
112
  </text>
49
113
  {/each}
50
-
51
- <text text-anchor="middle" transform={`translate(-32, ${c.innerHeight / 2}) rotate(-90)`}>
52
- {c.yLabel}
53
- </text>
54
114
  {:else if c.scaleTypes.y === 'band' && c.yScale}
55
115
  <BandAxisTicks
56
116
  scale={c.yScaleState as BandScaleState}
@@ -60,10 +120,21 @@
60
120
  dir={-1}
61
121
  textAnchor="end"
62
122
  />
123
+ {/if}
63
124
 
64
- <text text-anchor="middle" transform={`translate(-32, ${c.innerHeight / 2}) rotate(-90)`}>
65
- {c.yLabel}
66
- </text>
125
+ {#if title && c.yScale}
126
+ {#if titlePlacement === 'top'}
127
+ <text text-anchor="start" x={0} y={-18}>
128
+ {c.yLabel}
129
+ </text>
130
+ {:else}
131
+ <text
132
+ text-anchor="middle"
133
+ transform={`translate(${-titleX}, ${c.innerHeight / 2}) rotate(-90)`}
134
+ >
135
+ {c.yLabel}
136
+ </text>
137
+ {/if}
67
138
  {/if}
68
139
  </g>
69
140
  {/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>;
@@ -4,6 +4,7 @@
4
4
  import type { ChartContext } from '../../types';
5
5
  import { categoryLabel } from '../../query-engine/categorical-identity';
6
6
  import type { FilterGroupRegistry } from '../../query-engine/FilterGroupRegistry.svelte';
7
+ import { dimsBySelection } from '../rendering/activity-reads';
7
8
 
8
9
  /**
9
10
  * The labels on a band axis, wherever that axis is drawn.
@@ -71,8 +72,8 @@
71
72
  if (!chart || !filterGroups) return [];
72
73
  const indices: number[] = [];
73
74
  for (const entry of chart.config.entries.values()) {
74
- // A 'filter' mark never dims; its labels follow it.
75
- if (entry.selectionResponse === 'filter') continue;
75
+ // A 'filter' or 'ignore' mark never dims; its labels follow it.
76
+ if (!dimsBySelection(entry.selectionResponse)) continue;
76
77
  const index = filterGroups.get(entry.selection).index;
77
78
  if (!indices.includes(index)) indices.push(index);
78
79
  }
@@ -105,7 +106,7 @@
105
106
  </script>
106
107
 
107
108
  {#each ticks as tick (tick.key)}
108
- {@const pos = scale.tickPosition(tick.key, rangeSize, axis)}
109
+ {@const pos = scale.tickPosition(tick.key, rangeSize)}
109
110
  <g opacity={opacityOf(tick)}>
110
111
  {#if axis === 'x'}
111
112
  <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
+ }
@@ -1,4 +1,7 @@
1
1
  <script lang="ts">
2
+ import type { ScaleGeneration } from '../../scales/ScaleGeneration.svelte';
3
+ import type { ChartVisibility } from './ChartVisibility.svelte';
4
+ import type { ChartHover } from './ChartHover.svelte';
2
5
  import { setContext, getContext, onMount } from 'svelte';
3
6
  import type { Snippet } from 'svelte';
4
7
  import { ChartLayout } from '../../layout/ChartLayout.svelte';
@@ -43,6 +46,12 @@
43
46
  height?: number;
44
47
  clipPadding?: number;
45
48
  scales: ChartScales;
49
+ /** The chart's scale replacements; owned by the container, which outlives its ChartScales. */
50
+ scaleGeneration: ScaleGeneration;
51
+ /** Whether any of the chart's cells is on screen; this cell reports into it. */
52
+ chartVisibility: ChartVisibility;
53
+ /** Whether the pointer is over any of the chart's cells; this cell reports into it. */
54
+ chartHover: ChartHover;
46
55
  chartConfig: ChartConfig;
47
56
  facetingProps?: FacetingProps;
48
57
  gpuCoordinator: GPUCoordinator;
@@ -63,6 +72,9 @@
63
72
  height = 250,
64
73
  clipPadding = 0,
65
74
  scales,
75
+ scaleGeneration,
76
+ chartVisibility,
77
+ chartHover,
66
78
  chartConfig,
67
79
  facetingProps = {
68
80
  fx: null,
@@ -222,12 +234,15 @@
222
234
  };
223
235
  });
224
236
 
225
- // --- Viewport visibility callback (reserved for future use) ---
226
- const removeVisibilityCallback = gpuCoordinator.setVisibilityCallback(
227
- layout.chartId,
228
- (_visible) => {}
237
+ // --- Viewport visibility: this cell reports into the chart's ---
238
+ const removeVisibilityCallback = gpuCoordinator.setVisibilityCallback(layout.chartId, (visible) =>
239
+ chartVisibility.set(layout.chartId, visible)
229
240
  );
230
- onMount(() => removeVisibilityCallback);
241
+ onMount(() => () => {
242
+ removeVisibilityCallback();
243
+ chartVisibility.remove(layout.chartId);
244
+ chartHover.set(layout.chartId, false);
245
+ });
231
246
  onMount(() => () => hoverSettle.leave());
232
247
 
233
248
  // --- Scale readiness effect ---
@@ -358,6 +373,16 @@
358
373
  get xScaleState() {
359
374
  return scales?.xScaleState;
360
375
  },
376
+ get scaleGeneration() {
377
+ return scaleGeneration.value;
378
+ },
379
+ get visible() {
380
+ return chartVisibility.visible;
381
+ },
382
+ shownElsewhere: (scale) => chartVisibility.shownElsewhere(scale),
383
+ get hovered() {
384
+ return chartHover.hovered;
385
+ },
361
386
  get yScaleState() {
362
387
  return scales?.yScaleState;
363
388
  },
@@ -546,6 +571,7 @@
546
571
  }
547
572
 
548
573
  function handleChartPointerEnter(event: PointerEvent) {
574
+ chartHover.set(layout.chartId, true);
549
575
  activateSources();
550
576
  hoverSettle.enter();
551
577
  refreshSvgOrigin();
@@ -564,6 +590,7 @@
564
590
  }
565
591
 
566
592
  function handleChartPointerLeave() {
593
+ chartHover.set(layout.chartId, false);
567
594
  pointerInside = false;
568
595
  pointerOverSvg = false;
569
596
  hoverSettle.leave();
@@ -897,7 +924,7 @@
897
924
  if (overBrush) {
898
925
  tooltipManager?.updatePosition(null, null);
899
926
  } else {
900
- tooltipManager?.updatePointerScreen(x, y);
927
+ tooltipManager?.updatePointerClient(x + svgLeft, y + svgTop);
901
928
  tooltipManager?.updatePosition(
902
929
  scales.xScaleState?.currentDataPoint,
903
930
  scales.yScaleState?.currentDataPoint
@@ -1,3 +1,6 @@
1
+ import type { ScaleGeneration } from '../../scales/ScaleGeneration.svelte';
2
+ import type { ChartVisibility } from './ChartVisibility.svelte';
3
+ import type { ChartHover } from './ChartHover.svelte';
1
4
  import type { Snippet } from 'svelte';
2
5
  import type { Margin } from '../../types';
3
6
  import type { FieldRef } from '../../types';
@@ -27,6 +30,12 @@ interface Props {
27
30
  height?: number;
28
31
  clipPadding?: number;
29
32
  scales: ChartScales;
33
+ /** The chart's scale replacements; owned by the container, which outlives its ChartScales. */
34
+ scaleGeneration: ScaleGeneration;
35
+ /** Whether any of the chart's cells is on screen; this cell reports into it. */
36
+ chartVisibility: ChartVisibility;
37
+ /** Whether the pointer is over any of the chart's cells; this cell reports into it. */
38
+ chartHover: ChartHover;
30
39
  chartConfig: ChartConfig;
31
40
  facetingProps?: FacetingProps;
32
41
  gpuCoordinator: GPUCoordinator;
@@ -1,4 +1,7 @@
1
1
  <script lang="ts">
2
+ import { ScaleGeneration } from '../../scales/ScaleGeneration.svelte';
3
+ import { ChartVisibility } from './ChartVisibility.svelte';
4
+ import { ChartHover } from './ChartHover.svelte';
2
5
  import { setContext, getContext, onDestroy } from 'svelte';
3
6
  import type { Snippet } from 'svelte';
4
7
  import { ChartLayout } from '../../layout/ChartLayout.svelte';
@@ -6,8 +9,8 @@
6
9
  import { ChartScales } from '../../scales/ChartScales.svelte';
7
10
  import { ChartConfig } from '../../config';
8
11
  import { getGPUContext } from '../../gpu';
9
- import type { ScaleRegistry } from '../../scales/ScaleRegistry.svelte';
10
- import type { ChartContext, FieldRef } from '../../types';
12
+ import type { ScaleRegistry, AnyScaleState } from '../../scales/ScaleRegistry.svelte';
13
+ import type { ChartContext, EntranceMode, FieldRef } from '../../types';
11
14
  import type { BandScaleState } from '../../scales/BandScaleState.svelte';
12
15
  import type { DimValue } from '../../query-engine/categorical-identity';
13
16
  import { identityKeyFor } from '../../query-engine/identity-key';
@@ -78,6 +81,19 @@
78
81
  clipPadding?: number;
79
82
  useXSelection?: boolean;
80
83
  useYSelection?: boolean;
84
+ /**
85
+ * The tooltip's colours against the color-scheme the chart inherits:
86
+ * `contrast` is the opposite scheme (dark on a light page), `follow` the
87
+ * same. Unset, the nearest `data-fjader-tooltip` ancestor decides, else
88
+ * contrast. `--fjader-tooltip-bg`/`-text` set by the app override both.
89
+ */
90
+ tooltipScheme?: 'contrast' | 'follow';
91
+ /**
92
+ * How the marks enter when data first draws: 'auto' grows bars and
93
+ * areas from 0 and fades the rest, 'fade' fades everything, 'none'
94
+ * shows them at once. Reduced motion turns 'auto' into 'fade'.
95
+ */
96
+ entrance?: EntranceMode;
81
97
  }
82
98
 
83
99
  let {
@@ -94,6 +110,7 @@
94
110
  interactors = undefined,
95
111
  legend = undefined,
96
112
  legendPosition = 'top',
113
+ entrance = 'auto',
97
114
  wheelInteraction = {
98
115
  dwellTimeMs: 250,
99
116
  momentumThreshold: 150,
@@ -108,7 +125,8 @@
108
125
  margin: marginOverride = {},
109
126
  clipPadding = 0,
110
127
  useXSelection = false,
111
- useYSelection = false
128
+ useYSelection = false,
129
+ tooltipScheme = undefined
112
130
  }: Props = $props();
113
131
 
114
132
  const defaultMargin = { top: 30, right: 50, bottom: 35, left: 75 };
@@ -204,6 +222,19 @@
204
222
 
205
223
  // --- ChartScales ---
206
224
  let scales = $state<ChartScales | null>(null);
225
+ // Survives `scales` being recreated: see ScaleGeneration.
226
+ const scaleGeneration = new ScaleGeneration(() => scales);
227
+ // Whether any cell is on screen; the cells report into it.
228
+ const chartVisibility = new ChartVisibility(
229
+ gpuCoordinator
230
+ ? {
231
+ chartsUsing: (scale) => scaleRegistry.chartsUsing(scale as AnyScaleState),
232
+ isChartVisible: (chartId) => gpuCoordinator.isChartVisible(chartId)
233
+ }
234
+ : null
235
+ );
236
+ // Whether the pointer is over any cell; the cells report into it.
237
+ const chartHover = new ChartHover();
207
238
 
208
239
  // Queue for scale registrations that arrive before ChartScales is created.
209
240
  // Marks mount synchronously during render, but ChartScales is created in an
@@ -239,7 +270,8 @@
239
270
  xDisableZoom,
240
271
  yDisableZoom,
241
272
  useXSelection,
242
- useYSelection
273
+ useYSelection,
274
+ entranceGate: chartConfig.entrance
243
275
  });
244
276
 
245
277
  // Replay any scale registrations that were queued before scales existed
@@ -266,9 +298,11 @@
266
298
 
267
299
  // --- ChartConfig (reactive configuration store) ---
268
300
  const chartConfig = new ChartConfig();
301
+ onDestroy(() => chartConfig.entrance.destroy());
269
302
 
270
303
  // Register chart-level props for palette overrides
271
304
  chartConfig.registerChart({
305
+ entrance: () => entrance,
272
306
  marginTop: () => cellMargin.top,
273
307
  marginRight: () => cellMargin.right,
274
308
  marginBottom: () => cellMargin.bottom,
@@ -365,6 +399,16 @@
365
399
  get xScaleState() {
366
400
  return scales?.xScaleState;
367
401
  },
402
+ get scaleGeneration() {
403
+ return scaleGeneration.value;
404
+ },
405
+ get visible() {
406
+ return chartVisibility.visible;
407
+ },
408
+ shownElsewhere: (scale) => chartVisibility.shownElsewhere(scale),
409
+ get hovered() {
410
+ return chartHover.hovered;
411
+ },
368
412
  get yScaleState() {
369
413
  return scales?.yScaleState;
370
414
  },
@@ -452,7 +496,7 @@
452
496
  setContext('legend-position', () => legendPosition);
453
497
  </script>
454
498
 
455
- <div class="figure-container" bind:clientWidth={layout.width}>
499
+ <div class="figure-container" bind:clientWidth={layout.width} data-fjader-tooltip={tooltipScheme}>
456
500
  {#if legend && legendPosition === 'top'}
457
501
  <div
458
502
  class="legend-container legend-top"
@@ -485,6 +529,9 @@
485
529
  facetLabel={String(categoryLabel(cat))}
486
530
  margin={{ top: 20, right: 10, bottom: 35, left: 55 }}
487
531
  {scales}
532
+ {scaleGeneration}
533
+ {chartVisibility}
534
+ {chartHover}
488
535
  {chartConfig}
489
536
  {facetingProps}
490
537
  {gpuCoordinator}
@@ -507,6 +554,9 @@
507
554
  margin={cellMargin}
508
555
  {clipPadding}
509
556
  {scales}
557
+ {scaleGeneration}
558
+ {chartVisibility}
559
+ {chartHover}
510
560
  {chartConfig}
511
561
  {facetingProps}
512
562
  {gpuCoordinator}
@@ -1,6 +1,6 @@
1
1
  import type { Snippet } from 'svelte';
2
2
  import type { Margin } from '../../types';
3
- import type { FieldRef } from '../../types';
3
+ import type { EntranceMode, FieldRef } from '../../types';
4
4
  import type { DimValue } from '../../query-engine/categorical-identity';
5
5
  interface Props {
6
6
  xLabel?: string | null;
@@ -69,6 +69,19 @@ 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';
79
+ /**
80
+ * How the marks enter when data first draws: 'auto' grows bars and
81
+ * areas from 0 and fades the rest, 'fade' fades everything, 'none'
82
+ * shows them at once. Reduced motion turns 'auto' into 'fade'.
83
+ */
84
+ entrance?: EntranceMode;
72
85
  }
73
86
  declare const ChartContainer: import("svelte").Component<Props, {}, "">;
74
87
  type ChartContainer = ReturnType<typeof ChartContainer>;
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Whether the pointer is over any of a chart's cells: hover intent, for work
3
+ * worth doing only when someone is about to hover a mark, such as writing its
4
+ * tooltip table.
5
+ *
6
+ * Owned by the chart container, which in facet mode holds the marks for many
7
+ * cells, as ChartVisibility is. Cells report pointer enter and leave.
8
+ */
9
+ export declare class ChartHover {
10
+ #private;
11
+ set(cellId: string, hovered: boolean): void;
12
+ /** True while the pointer is over any cell. Reactive. */
13
+ get hovered(): boolean;
14
+ }
@@ -0,0 +1,22 @@
1
+ import { SvelteSet } from 'svelte/reactivity';
2
+ /**
3
+ * Whether the pointer is over any of a chart's cells: hover intent, for work
4
+ * worth doing only when someone is about to hover a mark, such as writing its
5
+ * tooltip table.
6
+ *
7
+ * Owned by the chart container, which in facet mode holds the marks for many
8
+ * cells, as ChartVisibility is. Cells report pointer enter and leave.
9
+ */
10
+ export class ChartHover {
11
+ #cells = new SvelteSet();
12
+ set(cellId, hovered) {
13
+ if (hovered)
14
+ this.#cells.add(cellId);
15
+ else
16
+ this.#cells.delete(cellId);
17
+ }
18
+ /** True while the pointer is over any cell. Reactive. */
19
+ get hovered() {
20
+ return this.#cells.size > 0;
21
+ }
22
+ }
@@ -0,0 +1,43 @@
1
+ /** What `shownElsewhere` asks: which charts draw a scale, and which are visible. */
2
+ export interface ChartVisibilityPeers {
3
+ chartsUsing(scale: object): readonly string[];
4
+ isChartVisible(chartId: string): boolean;
5
+ }
6
+ /**
7
+ * Whether any of a chart's cells is on screen, for its marks to stop
8
+ * re-querying while it is not (see MarkHandle.active).
9
+ *
10
+ * Owned by the chart container, which in facet mode holds the marks for many
11
+ * cells: the marks stay active while any cell is visible. Each cell reports
12
+ * what the GPU coordinator's IntersectionObserver and page visibility say
13
+ * about it; the observer has a 200 px margin, so a chart counts as visible
14
+ * just before it scrolls in. A cell that has not reported counts as visible,
15
+ * as it does for the coordinator, so nothing is held back before the first
16
+ * report.
17
+ */
18
+ export declare class ChartVisibility {
19
+ #private;
20
+ constructor(peers?: ChartVisibilityPeers | null);
21
+ /** A cell's visibility changed. */
22
+ set(cellId: string, visible: boolean): void;
23
+ /** A cell unmounted. */
24
+ remove(cellId: string): void;
25
+ /**
26
+ * True while any cell is visible, or none has reported. Reactive: it reads
27
+ * the SvelteMap. A getter, not a derived, since a chart has few cells.
28
+ */
29
+ get visible(): boolean;
30
+ /**
31
+ * Whether a chart other than this one is visible and draws `scale`, so
32
+ * that changing the scale moves something in view. A chart catching up on
33
+ * its return snaps its own parts but springs a scale like this: the chart
34
+ * being watched moves through the scale, as it would for any change.
35
+ *
36
+ * Asked when a result lands, not when it was sent: visibility changes
37
+ * during a scroll. "Visible" is the coordinator's: it includes the 200 px
38
+ * margin, so a chart in the margin, which nobody sees yet, still counts and
39
+ * keeps the spring. Untracked: a point-in-time answer, asked from inside
40
+ * effects that must not re-run when a cell scrolls.
41
+ */
42
+ shownElsewhere(scale: object): boolean;
43
+ }