@fundar/data-chart-telling 1.17.0 → 1.18.0

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 (205) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/dist/configuration/config.svelte.js +3 -2
  3. package/dist/index.d.ts +7 -2
  4. package/dist/index.js +2 -0
  5. package/dist/layout/geometry/GeometryLayout.svelte +188 -54
  6. package/dist/layout/hover/HoverDefaults.d.ts +52 -0
  7. package/dist/layout/hover/HoverDefaults.js +96 -0
  8. package/dist/layout/hover/HoverLayers.d.ts +35 -0
  9. package/dist/layout/hover/HoverLayers.js +122 -0
  10. package/dist/layout/hover/HoverSettings.d.ts +1 -3
  11. package/dist/layout/hover/HoverSettings.js +3 -2
  12. package/dist/layout/hover/HoverShapes.d.ts +44 -0
  13. package/dist/layout/hover/HoverShapes.js +103 -0
  14. package/dist/layout/hover/PlotHover.svelte.d.ts +18 -0
  15. package/dist/layout/hover/PlotHover.svelte.js +54 -1
  16. package/dist/layout/legend/LegendState.svelte.d.ts +2 -0
  17. package/dist/layout/legend/LegendState.svelte.js +5 -0
  18. package/dist/layout/plot/Core.svelte +2 -2
  19. package/dist/layout/plot/PublicPlotContext.d.ts +6 -0
  20. package/dist/layout/plot/PublicPlotContext.js +8 -0
  21. package/dist/layout/styles/ValueLabels.d.ts +12 -4
  22. package/dist/layout/styles/ValueLabels.js +22 -13
  23. package/dist/layout/tooltip/TooltipState.svelte.d.ts +8 -9
  24. package/dist/layout/tooltip/TooltipState.svelte.js +28 -29
  25. package/dist/markers/MarkerKinds.d.ts +5 -0
  26. package/dist/markers/MarkerKinds.js +1 -0
  27. package/dist/markers/band/BandMarker.svelte +74 -0
  28. package/dist/markers/band/BandMarker.svelte.d.ts +7 -0
  29. package/dist/markers/band/Bands.d.ts +16 -0
  30. package/dist/markers/band/Bands.js +48 -0
  31. package/dist/markers/bar/BarMarker.svelte +23 -66
  32. package/dist/markers/hover/HoverMarker.svelte +7 -265
  33. package/dist/markers/hover/HoverMarker.svelte.d.ts +4 -27
  34. package/dist/markers/utils/PointPixel.d.ts +37 -0
  35. package/dist/markers/utils/PointPixel.js +44 -0
  36. package/dist/phases/Pipeline.d.ts +7 -7
  37. package/dist/phases/Pipeline.js +12 -9
  38. package/dist/phases/context/ContextPhase.d.ts +22 -0
  39. package/dist/phases/context/ContextPhase.js +23 -0
  40. package/dist/phases/context/RowContext.d.ts +35 -0
  41. package/dist/phases/context/RowContext.js +95 -0
  42. package/dist/phases/data/DataPhase.d.ts +4 -4
  43. package/dist/phases/data/DataPhase.js +7 -7
  44. package/dist/phases/hover/HoverPhase.d.ts +6 -7
  45. package/dist/phases/hover/HoverPhase.js +28 -23
  46. package/dist/phases/mapping/MappingPhase.d.ts +4 -4
  47. package/dist/phases/mapping/MappingPhase.js +2 -2
  48. package/dist/phases/markers/MarkerStyles.d.ts +8 -0
  49. package/dist/phases/markers/MarkerStyles.js +15 -0
  50. package/dist/phases/markers/MarkersPhase.d.ts +4 -4
  51. package/dist/phases/markers/MarkersPhase.js +4 -4
  52. package/dist/phases/scales/ScalesPhase.d.ts +4 -4
  53. package/dist/phases/scales/ScalesPhase.js +4 -4
  54. package/dist/phases/sharedScales/SharedScalesPhase.d.ts +2 -2
  55. package/dist/phases/sharedScales/SharedScalesPhase.js +2 -2
  56. package/dist/phases/statistics/MarkerGrouping.d.ts +0 -9
  57. package/dist/phases/statistics/MarkerGrouping.js +0 -16
  58. package/dist/phases/styles/StylesPhase.d.ts +3 -3
  59. package/dist/phases/styles/StylesPhase.js +2 -2
  60. package/dist/phases/tooltip/TooltipPhase.d.ts +4 -4
  61. package/dist/phases/tooltip/TooltipPhase.js +10 -7
  62. package/dist/phases/transforms/TransformsPhase.d.ts +3 -3
  63. package/dist/phases/transforms/TransformsPhase.js +2 -2
  64. package/dist/plots/correlation/heatmap/defaults.d.ts +2 -2
  65. package/dist/plots/correlation/heatmap/defaults.js +21 -6
  66. package/dist/plots/correlation/heatmap/markers.d.ts +2 -2
  67. package/dist/plots/correlation/heatmap/markers.js +4 -3
  68. package/dist/plots/correlation/scatter/defaults.d.ts +2 -2
  69. package/dist/plots/correlation/scatter/defaults.js +9 -3
  70. package/dist/plots/correlation/scatter/markers.d.ts +2 -2
  71. package/dist/plots/correlation/scatter/markers.js +4 -3
  72. package/dist/plots/distribution/box/BoxSummaries.d.ts +17 -8
  73. package/dist/plots/distribution/box/BoxSummaries.js +90 -40
  74. package/dist/plots/distribution/box/defaults.d.ts +2 -2
  75. package/dist/plots/distribution/box/defaults.js +9 -6
  76. package/dist/plots/distribution/box/markers.d.ts +2 -2
  77. package/dist/plots/distribution/box/markers.js +3 -3
  78. package/dist/plots/distribution/density/defaults.d.ts +2 -2
  79. package/dist/plots/distribution/density/defaults.js +17 -1
  80. package/dist/plots/distribution/density/markers.d.ts +2 -2
  81. package/dist/plots/distribution/density/markers.js +4 -4
  82. package/dist/plots/distribution/histogram/defaults.d.ts +2 -2
  83. package/dist/plots/distribution/histogram/defaults.js +20 -25
  84. package/dist/plots/distribution/histogram/markers.d.ts +2 -2
  85. package/dist/plots/distribution/histogram/markers.js +5 -4
  86. package/dist/plots/distribution/pyramid/defaults.d.ts +2 -2
  87. package/dist/plots/distribution/pyramid/defaults.js +4 -1
  88. package/dist/plots/distribution/pyramid/markers.d.ts +2 -2
  89. package/dist/plots/distribution/pyramid/markers.js +4 -3
  90. package/dist/plots/distribution/violin/defaults.d.ts +2 -2
  91. package/dist/plots/distribution/violin/defaults.js +9 -7
  92. package/dist/plots/distribution/violin/markers.d.ts +2 -2
  93. package/dist/plots/distribution/violin/markers.js +3 -3
  94. package/dist/plots/evolution/area/defaults.d.ts +2 -2
  95. package/dist/plots/evolution/area/defaults.js +1 -1
  96. package/dist/plots/evolution/area/markers.d.ts +2 -2
  97. package/dist/plots/evolution/area/markers.js +4 -4
  98. package/dist/plots/evolution/line/defaults.d.ts +2 -2
  99. package/dist/plots/evolution/line/defaults.js +1 -1
  100. package/dist/plots/evolution/line/markers.d.ts +2 -2
  101. package/dist/plots/evolution/line/markers.js +3 -3
  102. package/dist/plots/evolution/slope/defaults.d.ts +2 -2
  103. package/dist/plots/evolution/slope/defaults.js +8 -4
  104. package/dist/plots/evolution/slope/markers.d.ts +2 -2
  105. package/dist/plots/evolution/slope/markers.js +3 -3
  106. package/dist/plots/evolution/sparkline/defaults.d.ts +2 -2
  107. package/dist/plots/evolution/sparkline/defaults.js +5 -2
  108. package/dist/plots/evolution/sparkline/markers.d.ts +2 -2
  109. package/dist/plots/evolution/sparkline/markers.js +3 -3
  110. package/dist/plots/map/geo/Core.svelte +3 -3
  111. package/dist/plots/map/geo/defaults.d.ts +2 -2
  112. package/dist/plots/map/geo/phases/data.d.ts +1 -1
  113. package/dist/plots/map/geo/phases/data.js +1 -1
  114. package/dist/plots/map/geo/phases/hover.d.ts +2 -2
  115. package/dist/plots/map/geo/phases/hover.js +3 -3
  116. package/dist/plots/map/geo/phases/markers.d.ts +2 -2
  117. package/dist/plots/map/geo/phases/markers.js +3 -3
  118. package/dist/plots/map/geo/phases/scales.d.ts +2 -2
  119. package/dist/plots/map/geo/phases/scales.js +2 -2
  120. package/dist/plots/part-of-a-whole/treemap/defaults.d.ts +2 -2
  121. package/dist/plots/part-of-a-whole/treemap/defaults.js +37 -21
  122. package/dist/plots/ranking/bar/defaults.d.ts +2 -2
  123. package/dist/plots/ranking/bar/defaults.js +13 -7
  124. package/dist/plots/ranking/bar/markers.d.ts +3 -3
  125. package/dist/plots/ranking/bar/markers.js +8 -7
  126. package/dist/plots/ranking/bullet/defaults.d.ts +2 -2
  127. package/dist/plots/ranking/bullet/defaults.js +4 -4
  128. package/dist/plots/ranking/bullet/markers.d.ts +3 -3
  129. package/dist/plots/ranking/bullet/markers.js +9 -9
  130. package/dist/plots/ranking/cleveland/ClevelandMarkers.d.ts +3 -3
  131. package/dist/plots/ranking/cleveland/ClevelandMarkers.js +11 -10
  132. package/dist/plots/ranking/cleveland/defaults.d.ts +2 -2
  133. package/dist/plots/ranking/cleveland/defaults.js +7 -4
  134. package/dist/plots/ranking/dumbbell/defaults.d.ts +2 -2
  135. package/dist/plots/ranking/forest/defaults.d.ts +2 -2
  136. package/dist/plots/ranking/lollipop/defaults.d.ts +2 -2
  137. package/dist/types/configuration/styling.d.ts +4 -0
  138. package/dist/types/layout/context.d.ts +21 -0
  139. package/dist/types/layout/context.js +1 -0
  140. package/dist/types/layout/hover.d.ts +3 -0
  141. package/dist/types/layout/styles/values.d.ts +8 -2
  142. package/dist/types/layout/tooltip.d.ts +5 -10
  143. package/dist/types/markers/axis/axisBasedMarkersConfig.d.ts +2 -1
  144. package/dist/types/markers/axis/band.d.ts +24 -0
  145. package/dist/types/markers/axis/band.js +1 -0
  146. package/dist/types/markers/axis/bar.d.ts +2 -2
  147. package/dist/types/markers/axis/hover.d.ts +60 -15
  148. package/dist/types/markers/channels/commonMarkerConfig.d.ts +6 -2
  149. package/dist/types/markers/common.d.ts +1 -0
  150. package/dist/types/markers/common.js +1 -0
  151. package/dist/types/phases/context/phase.d.ts +7 -0
  152. package/dist/types/phases/context/phase.js +1 -0
  153. package/dist/types/phases/data/phase.d.ts +2 -2
  154. package/dist/types/phases/hover/phase.d.ts +10 -7
  155. package/dist/types/phases/index.d.ts +2 -1
  156. package/dist/types/phases/kind.d.ts +11 -8
  157. package/dist/types/phases/mapping/phase.d.ts +2 -2
  158. package/dist/types/phases/markers/phase.d.ts +3 -3
  159. package/dist/types/phases/scales/phase.d.ts +4 -4
  160. package/dist/types/phases/stage.d.ts +3 -3
  161. package/dist/types/phases/state.d.ts +1 -1
  162. package/dist/types/phases/styles/phase.d.ts +3 -2
  163. package/dist/types/phases/transforms/phase.d.ts +2 -2
  164. package/dist/types/plots/constants.d.ts +3 -5
  165. package/dist/types/plots/correlation/heatmap/state.d.ts +2 -2
  166. package/dist/types/plots/correlation/scatter/state.d.ts +2 -2
  167. package/dist/types/plots/distribution/box/props.d.ts +23 -1
  168. package/dist/types/plots/distribution/box/state.d.ts +6 -17
  169. package/dist/types/plots/distribution/density/state.d.ts +2 -2
  170. package/dist/types/plots/distribution/histogram/props.d.ts +14 -1
  171. package/dist/types/plots/distribution/histogram/state.d.ts +2 -2
  172. package/dist/types/plots/distribution/pyramid/state.d.ts +2 -2
  173. package/dist/types/plots/distribution/violin/props.d.ts +2 -1
  174. package/dist/types/plots/distribution/violin/state.d.ts +2 -2
  175. package/dist/types/plots/evolution/area/state.d.ts +2 -2
  176. package/dist/types/plots/evolution/line/state.d.ts +2 -2
  177. package/dist/types/plots/evolution/slope/state.d.ts +2 -2
  178. package/dist/types/plots/evolution/sparkline/state.d.ts +2 -2
  179. package/dist/types/plots/map/geo/state.d.ts +1 -1
  180. package/dist/types/plots/part-of-a-whole/treemap/props.d.ts +14 -4
  181. package/dist/types/plots/part-of-a-whole/treemap/state.d.ts +2 -2
  182. package/dist/types/plots/props.d.ts +3 -0
  183. package/dist/types/plots/ranking/bar/state.d.ts +2 -2
  184. package/dist/types/plots/ranking/bullet/state.d.ts +2 -2
  185. package/dist/types/plots/ranking/cleveland/state.d.ts +2 -2
  186. package/dist/types/plots/ranking/dumbbell/state.d.ts +2 -2
  187. package/dist/types/plots/ranking/forest/state.d.ts +2 -2
  188. package/dist/types/plots/ranking/lollipop/state.d.ts +2 -2
  189. package/docs/markers.md +34 -2
  190. package/docs/plots/evolution/line.md +1 -1
  191. package/docs/plots/evolution/slope.md +1 -1
  192. package/docs/plots/part-of-a-whole/treemap.md +36 -2
  193. package/docs/plots/ranking/bar.md +1 -0
  194. package/docs/plots/ranking/cleveland.md +1 -1
  195. package/docs/state.md +33 -0
  196. package/docs/styles-and-segments.md +2 -2
  197. package/docs/theming.md +2 -1
  198. package/docs/tooltip-and-hover.md +81 -3
  199. package/examples/LineDates.svelte +1 -1
  200. package/examples/ResponsiveLine.svelte +3 -1
  201. package/examples/TreemapHover.svelte +25 -0
  202. package/examples/ValueLabels.svelte +1 -1
  203. package/llms-full.txt +8 -6
  204. package/llms.txt +2 -1
  205. package/package.json +1 -1
@@ -1,283 +1,25 @@
1
1
  <script lang="ts">
2
2
  import { usePlot } from '../../layout/plot/usePlot';
3
- import DotMarker from '../dot/DotMarker.svelte';
4
- import TextMarker from '../text/TextMarker.svelte';
5
- import RuleMarker from '../rule/RuleMarker.svelte';
6
- import BarMarker from '../bar/BarMarker.svelte';
7
- import { getConfiguration } from '../../configuration/config.svelte';
8
- import { NumberFormat } from '../../utils/NumberFormat';
9
- import { BandNudge } from '../utils/BandNudge';
10
- import type { BandSlice } from '../../types/layout/hover';
11
- import type { HoverMarkerConfig } from '../../types/markers/common';
3
+ import { PointPixel } from '../utils/PointPixel';
12
4
  import type { HoverDisplayPoint } from '../../types/layout/hover';
13
5
 
14
6
  /**
15
- * In-SVG hover highlight, rendered *inside* a `<Plot>`. Because it positions
16
- * itself through the host plot's scales, it places the same coordinate
17
- * correctly in every facet - that is what makes cross-facet sync work without
18
- * any pixel math. Draws optional x/y crosshair rules plus a dot + value label
19
- * for each matched point.
20
- *
21
- * Takes the raw `marker` config plus the couple of things it can't know on
22
- * its own - `activePoint` (the specific matched point actually under the
23
- * cursor, for the crosshair position; see `controller.svelte.ts`'s own doc
24
- * comment) and whether the active hover strategy implies a crosshair on
25
- * each axis by default - the same "component owns its own rendering"
26
- * pattern `BarMarker`/`RuleMarker` follow. `activePoint`/
27
- * `impliesCrosshairX`/`impliesCrosshairY` are all optional and default to
28
- * "no crosshair".
29
- *
30
- * `areaX`/`areaY` are a separate, always-explicit opt-in (no implied
31
- * default) for a thickened area highlight in place of the thin crosshair -
32
- * see `HoverMarkerConfig`'s own doc comment. Rendered via `BarMarker`'s
33
- * `role: 'highlight'`, so it renders nothing on an axis without a band
34
- * scale (e.g. a line/scatter plot's continuous axis).
7
+ * Where the hover is, rendered *inside* a `<Plot>`: it reports the pixel position of the point under the pointer,
8
+ * through the host plot's scales, so a synced plot's tooltip can sit on it. It draws nothing itself - the
9
+ * `hover` marker's `back` and `front` markers are drawn by `GeometryLayout`, as every other marker.
35
10
  */
36
11
  interface Props {
37
- data?: HoverDisplayPoint[];
38
- /** Narrowed to just the fields this component reads - `HoverMarkerConfig` also carries routing/scoping fields (`series`/`scope`/`strategy`/`sync`) it has no use for. */
39
- marker: Pick<
40
- HoverMarkerConfig,
41
- | 'ruleX'
42
- | 'ruleY'
43
- | 'areaX'
44
- | 'areaY'
45
- | 'showLabels'
46
- | 'format'
47
- | 'rule'
48
- | 'area'
49
- | 'dot'
50
- | 'text'
51
- | 'decimals'
52
- | 'prefix'
53
- | 'suffix'
54
- | 'signDisplay'
55
- >;
56
12
  activePoint?: HoverDisplayPoint | null;
57
- impliesCrosshairX?: boolean;
58
- impliesCrosshairY?: boolean;
59
- /** Reports `activePoint`'s real pixel position within this facet's own box, or `null` when nothing's matched. */
13
+ /** Reports `activePoint`'s pixel position within this facet's own box, or `null` when nothing's matched. */
60
14
  onActivePointPixel?: (pos: { x: number; y: number } | null) => void;
61
15
  }
62
16
 
63
- let {
64
- data = [],
65
- marker,
66
- activePoint = null,
67
- impliesCrosshairX = false,
68
- impliesCrosshairY = false,
69
- onActivePointPixel
70
- }: Props = $props();
71
-
72
- const cfg = $derived(getConfiguration());
17
+ let { activePoint = null, onActivePointPixel }: Props = $props();
73
18
  const plot = usePlot().svelteplot;
74
19
 
75
- /** Scaled pixel position of `value`, centered within its band for a band scale. */
76
- function scaledPixel(
77
- scale: { type?: string; fn?: (v: never) => unknown } | undefined,
78
- value: unknown
79
- ): number | null {
80
- if (!scale?.fn || value == null) return null;
81
- const raw = Number(scale.fn(value as never));
82
- if (!Number.isFinite(raw)) return null;
83
- if (scale.type === 'band') {
84
- const bandwidth = (scale.fn as { bandwidth?: () => number }).bandwidth?.() ?? 0;
85
- return raw + bandwidth / 2;
86
- }
87
- return raw;
88
- }
89
-
90
- const nudgeOf = (point: { areaInset?: BandSlice } | null | undefined) =>
91
- BandNudge.of(plot.scales, point?.areaInset);
92
-
93
- const activePixel = $derived.by((): { x: number; y: number } | null => {
94
- if (!activePoint) return null;
95
- const x = scaledPixel(plot.scales.x, activePoint.x);
96
- const y = scaledPixel(plot.scales.y, activePoint.y);
97
- if (x == null || y == null) return null;
98
- const nudge = nudgeOf(activePoint);
99
- return { x: x + nudge.dx, y: y + nudge.dy };
100
- });
20
+ const activePixel = $derived(activePoint ? PointPixel.of(plot.scales, activePoint) : null);
101
21
 
102
22
  $effect(() => {
103
23
  onActivePointPixel?.(activePixel);
104
24
  });
105
-
106
- const showCrosshairX = $derived(
107
- marker.ruleX !== false && (impliesCrosshairX || marker.ruleX === true)
108
- );
109
- const showCrosshairY = $derived(
110
- marker.ruleY !== false && (impliesCrosshairY || marker.ruleY === true)
111
- );
112
- const ruleX = $derived(showCrosshairX ? (activePoint?.x ?? null) : null);
113
- const ruleY = $derived(showCrosshairY ? (activePoint?.y ?? null) : null);
114
- // areaX/areaY have no implied default - always off unless explicitly set.
115
- const areaX = $derived(marker.areaX === true ? (activePoint?.x ?? null) : null);
116
- const areaY = $derived(marker.areaY === true ? (activePoint?.y ?? null) : null);
117
- /**
118
- * The part of the band an area covers: the active point's own slice (a grouped bar's series), or the whole
119
- * band while the hover matches more than one point in that category, as its dots do.
120
- */
121
- const areaInsetOf = (axis: 'x' | 'y'): BandSlice | undefined => {
122
- const at = activePoint?.[axis];
123
- const key = (value: unknown) => (value instanceof Date ? +value : value);
124
- const shared = data.filter((point) => key(point[axis]) === key(at)).length > 1;
125
- return shared ? undefined : activePoint?.areaInset;
126
- };
127
- const areaInsetX = $derived(areaX != null ? areaInsetOf('x') : undefined);
128
- const areaInsetY = $derived(areaY != null ? areaInsetOf('y') : undefined);
129
- const showLabels = $derived(marker.showLabels ?? true);
130
- const format = $derived(marker.format);
131
- /** The hovered value as the marker's display fields ask; `undefined` while nothing does, so the plot's own label stays. */
132
- const numberOf = $derived(NumberFormat.maybe(NumberFormat.displayOf(marker), cfg));
133
- const number = $derived(numberOf ?? NumberFormat.of(undefined, cfg));
134
- const labelOf = (d: HoverDisplayPoint): string => {
135
- if (format) return format(d, number);
136
- const value = Number(d.label);
137
- return numberOf && d.label !== undefined && d.label !== '' && Number.isFinite(value)
138
- ? numberOf(value)
139
- : (d.label ?? '');
140
- };
141
- const ruleStyle = $derived(marker.rule);
142
- const areaStyle = $derived({ ...cfg.hover.area, ...marker.area });
143
- const dotStyle = $derived(marker.dot);
144
- const fontStyle = $derived(marker.text);
145
-
146
- /** Explicit style wins; otherwise fall back to the point's own resolved color. */
147
- const resolveColor = (explicit: string | undefined, point: HoverDisplayPoint) =>
148
- explicit ?? point.color;
149
-
150
- // Horizontal gap between the dot's edge and an 'outside'-anchored label -
151
- // scales with the dot's own resolved radius so the label clears it.
152
- const dotRadius = $derived(dotStyle?.dotRadius ?? cfg.hover.dot.dotRadius ?? 5);
153
-
154
- /**
155
- * Resolves `text.textAnchor: 'outside'` into a per-point anchor +
156
- * offset, mirroring the value-label `'outside'` anchor's own left/right
157
- * split (see pyramid/bar's own `ValueAnchor` handling): a positive `x`
158
- * anchors `'start'` and nudges right, a negative `x` anchors `'end'` and
159
- * nudges left - so a diverging mark's label sits on the side of the dot
160
- * away from zero, on whichever side that dot itself landed. Only
161
- * meaningful when the point's `x` is numeric (e.g. pyramid's magnitude
162
- * axis); anything else falls through to the plain `text` style unchanged.
163
- *
164
- * Also overrides `cfg.hover.font`'s default `dy: -18` (meant for a label
165
- * floating *above* the dot) back to vertically centered, and clears the
166
- * dot's own radius horizontally instead of the flat default nudge, so the
167
- * label doesn't collide with the dot it's labeling.
168
- */
169
- function resolveFontStyle(point: HoverDisplayPoint) {
170
- if (fontStyle?.textAnchor !== 'outside' || typeof point.x !== 'number') {
171
- return { ...cfg.hover.font, ...fontStyle, fill: resolveColor(fontStyle?.fill, point) };
172
- }
173
- const outward = point.x >= 0;
174
- const gap = dotRadius + 5;
175
- return {
176
- ...cfg.hover.font,
177
- ...fontStyle,
178
- fill: resolveColor(fontStyle?.fill, point),
179
- textAnchor: outward ? ('start' as const) : ('end' as const),
180
- lineAnchor: fontStyle?.lineAnchor ?? ('middle' as const),
181
- dx: fontStyle?.dx ?? (outward ? gap : -gap),
182
- dy: fontStyle?.dy ?? 0
183
- };
184
- }
185
25
  </script>
186
-
187
- {#if areaX != null}
188
- <BarMarker
189
- marker={{
190
- type: 'bar',
191
- role: 'highlight',
192
- isHorizontal: false,
193
- data: [],
194
- category: areaX,
195
- styles: {
196
- ...areaStyle,
197
- insetLeft: areaInsetX?.start,
198
- insetRight: areaInsetX?.end
199
- }
200
- }}
201
- />
202
- {/if}
203
-
204
- {#if areaY != null}
205
- <BarMarker
206
- marker={{
207
- type: 'bar',
208
- role: 'highlight',
209
- isHorizontal: true,
210
- data: [],
211
- category: areaY,
212
- styles: {
213
- ...areaStyle,
214
- insetTop: areaInsetY?.start,
215
- insetBottom: areaInsetY?.end
216
- }
217
- }}
218
- />
219
- {/if}
220
-
221
- {#if ruleX != null}
222
- <g transform="translate({nudgeOf(activePoint).dx}, {nudgeOf(activePoint).dy})">
223
- <RuleMarker
224
- marker={{
225
- type: 'rule',
226
- axis: 'x',
227
- value: ruleX,
228
- styles: { ...cfg.hover.rule, ...ruleStyle }
229
- }}
230
- />
231
- </g>
232
- {/if}
233
-
234
- {#if ruleY != null}
235
- <g transform="translate({nudgeOf(activePoint).dx}, {nudgeOf(activePoint).dy})">
236
- <RuleMarker
237
- marker={{
238
- type: 'rule',
239
- axis: 'y',
240
- value: ruleY,
241
- styles: { ...cfg.hover.rule, ...ruleStyle }
242
- }}
243
- />
244
- </g>
245
- {/if}
246
-
247
- {#if data.length > 0}
248
- {#each data as point (`${point.series ?? ''}_${point.x}_${point.y}`)}
249
- <!-- A grouped bar's point sits in its series' slice of the band (`areaInset`), not at the band's middle. -->
250
- <g transform="translate({nudgeOf(point).dx}, {nudgeOf(point).dy})">
251
- <DotMarker
252
- marker={{
253
- type: 'dot',
254
- data: [point],
255
- x: (d) => d.x,
256
- y: (d) => d.y,
257
- styles: {
258
- ...cfg.hover.dot,
259
- ...dotStyle,
260
- dotFill: resolveColor(dotStyle?.dotFill, point),
261
- dotStroke: resolveColor(dotStyle?.dotStroke, point)
262
- }
263
- }}
264
- />
265
- </g>
266
- {/each}
267
- {#if showLabels}
268
- {#each data as point (`${point.series ?? ''}_${point.x}_${point.y}`)}
269
- <g transform="translate({nudgeOf(point).dx}, {nudgeOf(point).dy})">
270
- <TextMarker
271
- marker={{
272
- type: 'text',
273
- data: [point],
274
- x: (d: HoverDisplayPoint) => d.x,
275
- y: (d: HoverDisplayPoint) => d.y,
276
- text: labelOf,
277
- styles: resolveFontStyle(point)
278
- }}
279
- />
280
- </g>
281
- {/each}
282
- {/if}
283
- {/if}
@@ -1,35 +1,12 @@
1
- import type { HoverMarkerConfig } from '../../types/markers/common';
2
1
  import type { HoverDisplayPoint } from '../../types/layout/hover';
3
2
  /**
4
- * In-SVG hover highlight, rendered *inside* a `<Plot>`. Because it positions
5
- * itself through the host plot's scales, it places the same coordinate
6
- * correctly in every facet - that is what makes cross-facet sync work without
7
- * any pixel math. Draws optional x/y crosshair rules plus a dot + value label
8
- * for each matched point.
9
- *
10
- * Takes the raw `marker` config plus the couple of things it can't know on
11
- * its own - `activePoint` (the specific matched point actually under the
12
- * cursor, for the crosshair position; see `controller.svelte.ts`'s own doc
13
- * comment) and whether the active hover strategy implies a crosshair on
14
- * each axis by default - the same "component owns its own rendering"
15
- * pattern `BarMarker`/`RuleMarker` follow. `activePoint`/
16
- * `impliesCrosshairX`/`impliesCrosshairY` are all optional and default to
17
- * "no crosshair".
18
- *
19
- * `areaX`/`areaY` are a separate, always-explicit opt-in (no implied
20
- * default) for a thickened area highlight in place of the thin crosshair -
21
- * see `HoverMarkerConfig`'s own doc comment. Rendered via `BarMarker`'s
22
- * `role: 'highlight'`, so it renders nothing on an axis without a band
23
- * scale (e.g. a line/scatter plot's continuous axis).
3
+ * Where the hover is, rendered *inside* a `<Plot>`: it reports the pixel position of the point under the pointer,
4
+ * through the host plot's scales, so a synced plot's tooltip can sit on it. It draws nothing itself - the
5
+ * `hover` marker's `back` and `front` markers are drawn by `GeometryLayout`, as every other marker.
24
6
  */
25
7
  interface Props {
26
- data?: HoverDisplayPoint[];
27
- /** Narrowed to just the fields this component reads - `HoverMarkerConfig` also carries routing/scoping fields (`series`/`scope`/`strategy`/`sync`) it has no use for. */
28
- marker: Pick<HoverMarkerConfig, 'ruleX' | 'ruleY' | 'areaX' | 'areaY' | 'showLabels' | 'format' | 'rule' | 'area' | 'dot' | 'text' | 'decimals' | 'prefix' | 'suffix' | 'signDisplay'>;
29
8
  activePoint?: HoverDisplayPoint | null;
30
- impliesCrosshairX?: boolean;
31
- impliesCrosshairY?: boolean;
32
- /** Reports `activePoint`'s real pixel position within this facet's own box, or `null` when nothing's matched. */
9
+ /** Reports `activePoint`'s pixel position within this facet's own box, or `null` when nothing's matched. */
33
10
  onActivePointPixel?: (pos: {
34
11
  x: number;
35
12
  y: number;
@@ -0,0 +1,37 @@
1
+ import type { BandSlice } from '../../types/layout/hover';
2
+ type Scale = {
3
+ type?: string;
4
+ fn?: (value: never) => unknown;
5
+ } | undefined;
6
+ /** Where a point is drawn, in pixels of its plot: its `x` and `y` through the plot's scales. */
7
+ export declare class PointPixel {
8
+ #private;
9
+ /**
10
+ * `point`'s pixel position - the middle of its band on a band scale, of its slice when it has one (a grouped
11
+ * bar's series) - or `null` when a scale cannot place it.
12
+ */
13
+ static of(scales: {
14
+ x?: Scale;
15
+ y?: Scale;
16
+ }, point: {
17
+ x: unknown;
18
+ y: unknown;
19
+ areaInset?: BandSlice;
20
+ }): {
21
+ x: number;
22
+ y: number;
23
+ } | null;
24
+ /** The index of the one of `points` nearest `cursor`, or `-1` when none can be placed. */
25
+ static nearest(scales: {
26
+ x?: Scale;
27
+ y?: Scale;
28
+ }, points: readonly {
29
+ x: unknown;
30
+ y: unknown;
31
+ areaInset?: BandSlice;
32
+ }[], cursor: {
33
+ x: number;
34
+ y: number;
35
+ }): number;
36
+ }
37
+ export {};
@@ -0,0 +1,44 @@
1
+ import { BandNudge } from './BandNudge';
2
+ /** Where a point is drawn, in pixels of its plot: its `x` and `y` through the plot's scales. */
3
+ export class PointPixel {
4
+ /**
5
+ * `point`'s pixel position - the middle of its band on a band scale, of its slice when it has one (a grouped
6
+ * bar's series) - or `null` when a scale cannot place it.
7
+ */
8
+ static of(scales, point) {
9
+ const x = PointPixel.#along(scales.x, point.x);
10
+ const y = PointPixel.#along(scales.y, point.y);
11
+ if (x == null || y == null)
12
+ return null;
13
+ const nudge = BandNudge.of(scales, point.areaInset);
14
+ return { x: x + nudge.dx, y: y + nudge.dy };
15
+ }
16
+ /** The index of the one of `points` nearest `cursor`, or `-1` when none can be placed. */
17
+ static nearest(scales, points, cursor) {
18
+ let best = -1;
19
+ let bestDistance = Infinity;
20
+ points.forEach((point, index) => {
21
+ const pixel = PointPixel.of(scales, point);
22
+ if (!pixel)
23
+ return;
24
+ const distance = Math.hypot(pixel.x - cursor.x, pixel.y - cursor.y);
25
+ if (distance < bestDistance) {
26
+ best = index;
27
+ bestDistance = distance;
28
+ }
29
+ });
30
+ return best;
31
+ }
32
+ /** `value`'s pixel along one scale, in the middle of its band on a band scale. */
33
+ static #along(scale, value) {
34
+ if (!scale?.fn || value == null)
35
+ return null;
36
+ const raw = Number(scale.fn(value));
37
+ if (!Number.isFinite(raw))
38
+ return null;
39
+ if (scale.type !== 'band')
40
+ return raw;
41
+ const bandwidth = scale.fn.bandwidth?.() ?? 0;
42
+ return raw + bandwidth / 2;
43
+ }
44
+ }
@@ -1,4 +1,4 @@
1
- import type { AxisPlotState, AxisPlotCtx, AxisPlotKind, AxisPlotDefaults, DataRunState, HoverStageOptions, PhaseStage, PlotPhase } from '../types/phases';
1
+ import type { AxisPlotState, AxisPlotEnv, AxisPlotKind, AxisPlotDefaults, DataRunState, HoverStageOptions, PhaseStage, PlotPhase } from '../types/phases';
2
2
  import type { PlotStylesConfig } from '../types/configuration/styling';
3
3
  import type { FacetConfig } from '../types/layout/facet';
4
4
  import type { LegendState } from '../layout/legend/LegendState.svelte';
@@ -55,7 +55,7 @@ export type PipelineOptions<TState> = {
55
55
  * Stages declared out of phase order still run in it - with a warning, once,
56
56
  * since the declared order would otherwise read as the one that runs.
57
57
  */
58
- export declare class Pipeline<TState, TCtx = unknown> {
58
+ export declare class Pipeline<TState, TEnv = unknown> {
59
59
  #private;
60
60
  /**
61
61
  * The phases that work on the plot's rows - everything before its scales,
@@ -65,25 +65,25 @@ export declare class Pipeline<TState, TCtx = unknown> {
65
65
  static readonly DATA_PHASES: readonly PlotPhase[];
66
66
  /** Every {@link PlotPhase}, in the order they run: the data phases, then each panel's own. */
67
67
  static readonly ORDER: readonly PlotPhase[];
68
- readonly stages: readonly PhaseStage<TState, TCtx>[];
69
- constructor(stages: PhaseStage<TState, TCtx>[], options?: PipelineOptions<TState>);
68
+ readonly stages: readonly PhaseStage<TState, TEnv>[];
69
+ constructor(stages: PhaseStage<TState, TEnv>[], options?: PipelineOptions<TState>);
70
70
  /**
71
71
  * An axis-based plot kind's pipeline: the shared stages, each set up with
72
72
  * the kind's own options - its marks and value labels among them - started
73
73
  * from a core's props. Built once per kind object.
74
74
  */
75
- static ofAxisKind<TState extends AxisPlotState<any, any, any, any>, TCtx extends AxisPlotCtx>(kind: AxisPlotKind<TState, TCtx> & Pick<AxisPlotDefaults<any, any>, 'timeline'>): Pipeline<TState, TCtx>;
75
+ static ofAxisKind<TState extends AxisPlotState<any, any, any, any>, TEnv extends AxisPlotEnv>(kind: AxisPlotKind<TState, TEnv> & Pick<AxisPlotDefaults<any, any>, 'timeline'>): Pipeline<TState, TEnv>;
76
76
  /** A plot's pipeline from what its kind gives: its axis-kind defaults, or a pipeline of its own. */
77
77
  static of(defaults: AxisPlotDefaults<any, any> | Pipeline<any, any>): Pipeline<any, any>;
78
78
  /** The stage of `phase`, if the pipeline has one. */
79
- stage(phase: PlotPhase): PhaseStage<TState, TCtx> | undefined;
79
+ stage(phase: PlotPhase): PhaseStage<TState, TEnv> | undefined;
80
80
  /**
81
81
  * A core's run: every stage over the state its `input` (an axis core's
82
82
  * props) seeds - or, given `panelState` (one facet panel's part of
83
83
  * {@link runData}), only the stages after the data phases, over that state
84
84
  * with the input's own props (its box, margins and scales) on top.
85
85
  */
86
- run: (input: unknown, ctx: TCtx, panelState?: object) => TState;
86
+ run: (input: unknown, env: TEnv, panelState?: object) => TState;
87
87
  /** Whether the pipeline has data phases to run once over every facet panel ({@link runData}). */
88
88
  get facets(): boolean;
89
89
  /**
@@ -2,6 +2,7 @@ import { FacetPhase } from './facet/FacetPhase';
2
2
  import { DataPhase } from './data/DataPhase';
3
3
  import { MappingPhase } from './mapping/MappingPhase';
4
4
  import { TransformsPhase } from './transforms/TransformsPhase';
5
+ import { ContextPhase } from './context/ContextPhase';
5
6
  import { StatisticsPhase } from './statistics/StatisticsPhase';
6
7
  import { ScalesPhase } from './scales/ScalesPhase';
7
8
  import { SharedScalesPhase } from './sharedScales/SharedScalesPhase';
@@ -33,6 +34,7 @@ export class Pipeline {
33
34
  'data',
34
35
  'mapping',
35
36
  'transforms',
37
+ 'context',
36
38
  'statistics',
37
39
  'sharedScales'
38
40
  ];
@@ -79,6 +81,7 @@ export class Pipeline {
79
81
  new DataPhase(kind.data),
80
82
  new MappingPhase(kind.mapping),
81
83
  new TransformsPhase(kind.transforms, kind.data?.segments?.connect),
84
+ new ContextPhase(kind.context),
82
85
  new StatisticsPhase(kind.statistics),
83
86
  new SharedScalesPhase(kind.sharedScales, scales),
84
87
  scales,
@@ -112,12 +115,12 @@ export class Pipeline {
112
115
  * {@link runData}), only the stages after the data phases, over that state
113
116
  * with the input's own props (its box, margins and scales) on top.
114
117
  */
115
- run = (input, ctx, panelState) => {
118
+ run = (input, env, panelState) => {
116
119
  const seed = this.#seed(input);
117
120
  if (!panelState)
118
- return Pipeline.#through(this.stages, seed, ctx);
121
+ return Pipeline.#through(this.stages, seed, env);
119
122
  this.#panel ??= this.stages.filter((stage) => !Pipeline.DATA_PHASES.includes(stage.phase));
120
- return Pipeline.#through(this.#panel, { ...panelState, ...seed }, ctx);
123
+ return Pipeline.#through(this.#panel, { ...panelState, ...seed }, env);
121
124
  };
122
125
  /** Whether the pipeline has data phases to run once over every facet panel ({@link runData}). */
123
126
  get facets() {
@@ -131,7 +134,7 @@ export class Pipeline {
131
134
  runData(props, rows, facet, legendInteraction, cfg) {
132
135
  this.#data ??= this.stages.filter((stage) => Pipeline.DATA_PHASES.includes(stage.phase));
133
136
  const seed = { ...this.#seed(props, rows), facet };
134
- return Pipeline.#through(this.#data, seed, Pipeline.#ctxFor(cfg, legendInteraction));
137
+ return Pipeline.#through(this.#data, seed, Pipeline.#envFor(cfg, legendInteraction));
135
138
  }
136
139
  /**
137
140
  * The plot's hover strategy when neither its `tooltip` nor a hover marker
@@ -143,7 +146,7 @@ export class Pipeline {
143
146
  if (fixed !== undefined)
144
147
  return fixed;
145
148
  const upToData = this.stages.filter((s) => s.phase === 'data');
146
- const state = Pipeline.#through(upToData, this.#seed(props), Pipeline.#ctxFor(cfg, null));
149
+ const state = Pipeline.#through(upToData, this.#seed(props), Pipeline.#envFor(cfg, null));
147
150
  return HoverSettings.strategyOf(state.axes);
148
151
  }
149
152
  /** The plot's default `timeline.filterMode`. */
@@ -162,7 +165,7 @@ export class Pipeline {
162
165
  return undefined;
163
166
  const upToMapping = this.stages.filter((s) => s.phase === 'data' || s.phase === 'mapping');
164
167
  // Without a legend: the channels read the same whatever it turns off.
165
- const state = Pipeline.#through(upToMapping, this.#seed(props), Pipeline.#ctxFor(cfg, null));
168
+ const state = Pipeline.#through(upToMapping, this.#seed(props), Pipeline.#envFor(cfg, null));
166
169
  const channel = (ref) => {
167
170
  const name = ref === 'value' || ref === 'category' ? state.axes?.[ref] : ref;
168
171
  if (name === undefined)
@@ -186,11 +189,11 @@ export class Pipeline {
186
189
  return this.#options.seed ? this.#options.seed(input, rows) : input;
187
190
  }
188
191
  /** `stages` over `state`, in order. */
189
- static #through(stages, state, ctx) {
190
- return stages.reduce((acc, stage) => stage.run(acc, ctx), state);
192
+ static #through(stages, state, env) {
193
+ return stages.reduce((acc, stage) => stage.run(acc, env), state);
191
194
  }
192
195
  /** The context a plot-level run reads, outside any core: the configuration and the legend. */
193
- static #ctxFor(cfg, legendInteraction) {
196
+ static #envFor(cfg, legendInteraction) {
194
197
  const interaction = new PlotInteraction(() => cfg, { legend: legendInteraction });
195
198
  return { cfg, interaction };
196
199
  }
@@ -0,0 +1,22 @@
1
+ import type { ContextStageOptions } from '../../types/phases/context/phase';
2
+ import type { PhaseStage, ProcessedData } from '../../types/phases';
3
+ import type { PlotAxes } from '../data/PlotAxes';
4
+ import type { Series } from '../../types/plots/data/common';
5
+ /** What the "context" phase reads. */
6
+ type ContextStageState = {
7
+ processedData?: ProcessedData;
8
+ resolvedSeries?: Series<any>[];
9
+ axes?: PlotAxes;
10
+ };
11
+ /**
12
+ * The "context" phase every axis-based plot shares: right after the transforms, it works out once what the plot
13
+ * knows of each processed row - the shared {@link RowContext}, with what the kind adds - and keeps it on the row,
14
+ * for the tooltip, the hover, the value labels and the author's own markers to read.
15
+ */
16
+ export declare class ContextPhase<TState extends ContextStageState, TEnv = unknown> implements PhaseStage<TState, TEnv> {
17
+ #private;
18
+ readonly phase: "context";
19
+ constructor(kind?: ContextStageOptions<TState, TEnv>);
20
+ run: (state: TState, env: TEnv) => TState;
21
+ }
22
+ export {};
@@ -0,0 +1,23 @@
1
+ import { RowContext } from './RowContext';
2
+ /**
3
+ * The "context" phase every axis-based plot shares: right after the transforms, it works out once what the plot
4
+ * knows of each processed row - the shared {@link RowContext}, with what the kind adds - and keeps it on the row,
5
+ * for the tooltip, the hover, the value labels and the author's own markers to read.
6
+ */
7
+ export class ContextPhase {
8
+ phase = 'context';
9
+ #kind;
10
+ constructor(kind) {
11
+ this.#kind = kind;
12
+ }
13
+ run = (state, env) => {
14
+ const rows = state.processedData?.data;
15
+ if (!rows?.length)
16
+ return state;
17
+ const shared = RowContext.shared(state);
18
+ const own = this.#kind?.(state, env);
19
+ for (const row of rows)
20
+ RowContext.tag(row, { ...shared(row), ...own?.(row) });
21
+ return state;
22
+ };
23
+ }
@@ -0,0 +1,35 @@
1
+ import { PlotAxes } from '../data/PlotAxes';
2
+ import type { PlotRowContext } from '../../types/layout/context';
3
+ import type { Series } from '../../types/plots/data/common';
4
+ import type { ProcessedData, ProcessedRow } from '../../types/phases';
5
+ /** What the shared context is worked out from. */
6
+ type ContextSource = {
7
+ processedData?: ProcessedData;
8
+ resolvedSeries?: Series<any>[];
9
+ axes?: PlotAxes;
10
+ };
11
+ /**
12
+ * The context of the rows a plot draws ({@link PlotRowContext}): worked out once per row by the "context" phase,
13
+ * kept on the row itself, and read back from it - or from any copy of it - with {@link of}.
14
+ */
15
+ export declare class RowContext {
16
+ #private;
17
+ /**
18
+ * What the plot knows of `row`: its series, where it sits, the value it shows and whatever its kind works out
19
+ * for it, with the row itself. A row no plot drew reads as its series alone.
20
+ */
21
+ static of<TContext extends PlotRowContext = PlotRowContext>(row: unknown): TContext;
22
+ /** Keeps `context` on `row`, for {@link of} to read back. */
23
+ static tag(row: ProcessedRow, context: PlotRowContext): void;
24
+ /** Whether `row` carries a context already. */
25
+ static has(row: unknown): boolean;
26
+ /** A reader of the rows of `state`: the context each carries, else the shared one, for a row no phase tagged. */
27
+ static reader(state: ContextSource): (row: ProcessedRow) => PlotRowContext;
28
+ /**
29
+ * The context every plot gives a processed row: its series; its `x` and `y` - each its series' own value
30
+ * (a stacked row's own, not the running total), else where the transforms put it (a density's estimate, a
31
+ * bin's centre); and its value on the plot's value axis, when that is a number.
32
+ */
33
+ static shared(state: ContextSource): (row: ProcessedRow) => PlotRowContext;
34
+ }
35
+ export {};