@fundar/data-chart-telling 0.0.25 → 0.0.27

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 (198) hide show
  1. package/dist/charts/BaseChart.svelte +18 -10
  2. package/dist/charts/BaseChart.svelte.d.ts +4 -2
  3. package/dist/charts/bar/Chart.svelte +2 -0
  4. package/dist/charts/heatmap/Chart.svelte +5 -3
  5. package/dist/charts/heatmap/Chart.svelte.d.ts +1 -2
  6. package/dist/charts/line/Chart.svelte +2 -0
  7. package/dist/charts/pyramid/Chart.svelte +5 -3
  8. package/dist/charts/pyramid/Chart.svelte.d.ts +1 -2
  9. package/dist/charts/scatter/Chart.svelte +95 -0
  10. package/dist/charts/scatter/Chart.svelte.d.ts +28 -0
  11. package/dist/configuration/config.svelte.js +28 -0
  12. package/dist/configuration/themes/dark.js +4 -0
  13. package/dist/configuration/themes/index.d.ts +112 -0
  14. package/dist/index.d.ts +26 -16
  15. package/dist/index.js +11 -3
  16. package/dist/layout/coordinates/CoordinatesLayout.svelte +93 -0
  17. package/dist/layout/{plot/RuleLayout.svelte.d.ts → coordinates/CoordinatesLayout.svelte.d.ts} +13 -16
  18. package/dist/layout/coordinates/margins.d.ts +16 -0
  19. package/dist/layout/coordinates/margins.js +11 -0
  20. package/dist/{plots/geo → layout/coordinates}/projections/argentina.d.ts +1 -1
  21. package/dist/layout/{geo → coordinates}/resolveProjection.d.ts +1 -1
  22. package/dist/layout/coordinates/resolveProjection.js +24 -0
  23. package/dist/{plots/geo → layout/coordinates}/zoom.svelte.d.ts +1 -1
  24. package/dist/layout/facet/FacetIndicator.svelte +31 -0
  25. package/dist/layout/facet/FacetIndicator.svelte.d.ts +8 -0
  26. package/dist/layout/facet/FacetLayout.svelte +249 -24
  27. package/dist/layout/facet/FacetLayout.svelte.d.ts +7 -1
  28. package/dist/layout/geometry/GeometryLayout.svelte +231 -0
  29. package/dist/layout/geometry/GeometryLayout.svelte.d.ts +62 -0
  30. package/dist/layout/legend/ContinuousSection.svelte +1 -1
  31. package/dist/layout/legend/ContinuousSection.svelte.d.ts +1 -1
  32. package/dist/layout/legend/DiscreteSection.svelte +1 -1
  33. package/dist/layout/legend/DiscreteSection.svelte.d.ts +1 -1
  34. package/dist/layout/legend/LegendLayout.svelte +1 -1
  35. package/dist/layout/legend/LegendLayout.svelte.d.ts +1 -1
  36. package/dist/layout/legend/interaction.svelte.d.ts +9 -1
  37. package/dist/layout/legend/interaction.svelte.js +9 -1
  38. package/dist/layout/plot/BasePlotLayout.svelte +142 -155
  39. package/dist/layout/plot/BasePlotLayout.svelte.d.ts +61 -51
  40. package/dist/layout/plot/margins.d.ts +0 -16
  41. package/dist/layout/plot/margins.js +0 -11
  42. package/dist/layout/{plot/AxisLayout.svelte → scales/ScalesLayout.svelte} +23 -16
  43. package/dist/layout/{plot/AxisLayout.svelte.d.ts → scales/ScalesLayout.svelte.d.ts} +4 -5
  44. package/dist/layout/scales/buildScale.d.ts +8 -0
  45. package/dist/layout/scales/buildScale.js +43 -0
  46. package/dist/layout/styles/StylesLayout.svelte +75 -0
  47. package/dist/layout/styles/StylesLayout.svelte.d.ts +15 -0
  48. package/dist/{plots/geo → layout/styles}/TileLayer.svelte +2 -2
  49. package/dist/{plots/geo → layout/styles}/TileLayer.svelte.d.ts +1 -1
  50. package/dist/layout/timeline/Timeline.svelte +1 -1
  51. package/dist/layout/timeline/Timeline.svelte.d.ts +1 -1
  52. package/dist/layout/timeline/TimelineLayout.svelte +1 -1
  53. package/dist/layout/timeline/TimelineLayout.svelte.d.ts +1 -1
  54. package/dist/layout/tooltip/Tooltip.svelte +16 -20
  55. package/dist/layout/tooltip/Tooltip.svelte.d.ts +5 -0
  56. package/dist/layout/tooltip/TooltipLayout.svelte +7 -1
  57. package/dist/layout/tooltip/TooltipLayout.svelte.d.ts +4 -0
  58. package/dist/layout/tooltip/controller.svelte.d.ts +4 -5
  59. package/dist/layout/tooltip/controller.svelte.js +42 -16
  60. package/dist/layout/tooltip/resolveTooltipPlacement.d.ts +24 -0
  61. package/dist/layout/tooltip/resolveTooltipPlacement.js +49 -0
  62. package/dist/layout/tooltip/utils.d.ts +27 -19
  63. package/dist/layout/tooltip/utils.js +16 -15
  64. package/dist/markers/BarMarker.svelte +92 -0
  65. package/dist/markers/BarMarker.svelte.d.ts +7 -0
  66. package/dist/markers/CellMarker.svelte +21 -0
  67. package/dist/markers/CellMarker.svelte.d.ts +7 -0
  68. package/dist/markers/ContourMarker.svelte +28 -0
  69. package/dist/markers/ContourMarker.svelte.d.ts +7 -0
  70. package/dist/markers/DeltaMarker.svelte +32 -39
  71. package/dist/markers/DeltaMarker.svelte.d.ts +4 -5
  72. package/dist/markers/DotMarker.svelte +35 -6
  73. package/dist/markers/DotMarker.svelte.d.ts +11 -2
  74. package/dist/markers/GeoFeaturesMarker.svelte +19 -0
  75. package/dist/markers/GeoFeaturesMarker.svelte.d.ts +7 -0
  76. package/dist/markers/GeoHoverMarker.svelte +25 -0
  77. package/dist/markers/GeoHoverMarker.svelte.d.ts +9 -0
  78. package/dist/markers/GraticuleMarker.svelte +18 -0
  79. package/dist/markers/GraticuleMarker.svelte.d.ts +7 -0
  80. package/dist/markers/HoverMarker.svelte +34 -35
  81. package/dist/markers/HoverMarker.svelte.d.ts +15 -9
  82. package/dist/markers/LineMarker.svelte +19 -0
  83. package/dist/markers/LineMarker.svelte.d.ts +7 -0
  84. package/dist/markers/LinkMarker.svelte +42 -0
  85. package/dist/markers/LinkMarker.svelte.d.ts +7 -0
  86. package/dist/markers/RuleMarker.svelte +24 -0
  87. package/dist/markers/RuleMarker.svelte.d.ts +10 -0
  88. package/dist/markers/SphereMarker.svelte +15 -0
  89. package/dist/markers/SphereMarker.svelte.d.ts +7 -0
  90. package/dist/markers/TextMarker.svelte +1 -1
  91. package/dist/markers/TextMarker.svelte.d.ts +1 -1
  92. package/dist/markers/VectorMarker.svelte +20 -0
  93. package/dist/markers/VectorMarker.svelte.d.ts +7 -0
  94. package/dist/markers/inset/InsetMarker.svelte +38 -45
  95. package/dist/markers/inset/InsetMarker.svelte.d.ts +6 -2
  96. package/dist/plots/bar/Plot.svelte +96 -87
  97. package/dist/plots/bar/Plot.svelte.d.ts +4 -5
  98. package/dist/plots/bar/ValueLabels.svelte +70 -24
  99. package/dist/plots/bar/ValueLabels.svelte.d.ts +11 -2
  100. package/dist/plots/bar/buildBarMarkers.d.ts +30 -0
  101. package/dist/plots/bar/buildBarMarkers.js +100 -0
  102. package/dist/plots/bar/layout.svelte.d.ts +1 -2
  103. package/dist/plots/bar/layout.svelte.js +1 -1
  104. package/dist/plots/geo/Plot.svelte +113 -387
  105. package/dist/plots/geo/Plot.svelte.d.ts +5 -4
  106. package/dist/plots/geo/buildGeoMarkers.d.ts +33 -0
  107. package/dist/plots/geo/buildGeoMarkers.js +47 -0
  108. package/dist/plots/heatmap/Plot.svelte +27 -52
  109. package/dist/plots/heatmap/Plot.svelte.d.ts +4 -5
  110. package/dist/plots/heatmap/ValueLabels.svelte +5 -5
  111. package/dist/plots/heatmap/ValueLabels.svelte.d.ts +3 -3
  112. package/dist/plots/heatmap/buildCellMarkers.d.ts +21 -0
  113. package/dist/plots/heatmap/buildCellMarkers.js +25 -0
  114. package/dist/plots/line/Plot.svelte +73 -141
  115. package/dist/plots/line/Plot.svelte.d.ts +4 -4
  116. package/dist/plots/line/ValueLabels.svelte +84 -85
  117. package/dist/plots/line/ValueLabels.svelte.d.ts +10 -1
  118. package/dist/plots/line/buildLineMarkers.d.ts +15 -0
  119. package/dist/plots/line/buildLineMarkers.js +57 -0
  120. package/dist/plots/pyramid/Plot.svelte +80 -121
  121. package/dist/plots/pyramid/Plot.svelte.d.ts +4 -5
  122. package/dist/plots/pyramid/ValueLabels.svelte +71 -25
  123. package/dist/plots/pyramid/ValueLabels.svelte.d.ts +11 -2
  124. package/dist/plots/pyramid/buildPyramidBarMarkers.d.ts +19 -0
  125. package/dist/plots/pyramid/buildPyramidBarMarkers.js +32 -0
  126. package/dist/plots/scatter/Plot.svelte +242 -0
  127. package/dist/{layout/plot/GridLayout.svelte.d.ts → plots/scatter/Plot.svelte.d.ts} +9 -7
  128. package/dist/plots/scatter/ValueLabels.svelte +149 -0
  129. package/dist/plots/{bar/BarSegments.svelte.d.ts → scatter/ValueLabels.svelte.d.ts} +17 -15
  130. package/dist/plots/scatter/buildScatterMarkers.d.ts +11 -0
  131. package/dist/plots/scatter/buildScatterMarkers.js +37 -0
  132. package/dist/plots/scatter/resolveRadiusPaddedDomain.d.ts +10 -0
  133. package/dist/plots/scatter/resolveRadiusPaddedDomain.js +23 -0
  134. package/dist/plots/utils/declutter.d.ts +55 -37
  135. package/dist/plots/utils/declutter.js +79 -64
  136. package/dist/plots/utils/geoAccessors.d.ts +1 -1
  137. package/dist/plots/utils/geoAccessors.js +1 -1
  138. package/dist/plots/utils/labelOverflow.svelte.d.ts +51 -0
  139. package/dist/plots/utils/labelOverflow.svelte.js +116 -0
  140. package/dist/plots/utils/legendDisabled.d.ts +1 -1
  141. package/dist/plots/utils/segments.d.ts +17 -10
  142. package/dist/plots/utils/segments.js +26 -16
  143. package/dist/types/charts/common.d.ts +3 -50
  144. package/dist/types/charts/props.d.ts +8 -8
  145. package/dist/types/configuration/styling.d.ts +26 -2
  146. package/dist/types/{plots/scales/geo.d.ts → layout/coordinates.d.ts} +33 -1
  147. package/dist/types/layout/facet.d.ts +41 -0
  148. package/dist/types/layout/geometry.d.ts +17 -0
  149. package/dist/types/layout/legend.d.ts +65 -1
  150. package/dist/types/{plots/axis.d.ts → layout/scales.d.ts} +11 -0
  151. package/dist/types/layout/styles.d.ts +115 -0
  152. package/dist/types/layout/timeline.d.ts +32 -0
  153. package/dist/types/layout/tooltip.d.ts +20 -1
  154. package/dist/types/markers/all.d.ts +11 -0
  155. package/dist/types/markers/all.js +1 -0
  156. package/dist/types/markers/base.d.ts +45 -0
  157. package/dist/types/markers/base.js +1 -0
  158. package/dist/types/markers/common.d.ts +64 -16
  159. package/dist/types/markers/geo.d.ts +75 -100
  160. package/dist/types/markers/line.d.ts +34 -0
  161. package/dist/types/markers/line.js +1 -0
  162. package/dist/types/markers/props.d.ts +1 -1
  163. package/dist/types/plots/data/common.d.ts +1 -1
  164. package/dist/types/plots/data/scatter.d.ts +13 -0
  165. package/dist/types/plots/data/scatter.js +1 -0
  166. package/dist/types/plots/props.d.ts +46 -74
  167. package/dist/types/plots/segments/common.d.ts +6 -12
  168. package/dist/types/plots/segments/config.d.ts +1 -1
  169. package/dist/types/plots/styling.d.ts +10 -0
  170. package/dist/{plots/utils → utils}/delta.d.ts +2 -2
  171. package/dist/{plots/utils → utils}/delta.js +1 -1
  172. package/dist/utils/edgeAwareAnchor.d.ts +37 -0
  173. package/dist/utils/edgeAwareAnchor.js +65 -0
  174. package/dist/utils/grouping.d.ts +1 -1
  175. package/dist/utils/interpolate.d.ts +2 -2
  176. package/package.json +3 -1
  177. package/dist/layout/geo/GeoLayout.svelte +0 -51
  178. package/dist/layout/geo/GeoLayout.svelte.d.ts +0 -24
  179. package/dist/layout/geo/resolveProjection.js +0 -48
  180. package/dist/layout/plot/GridLayout.svelte +0 -30
  181. package/dist/layout/plot/RuleLayout.svelte +0 -111
  182. package/dist/plots/bar/BarSegments.svelte +0 -69
  183. package/dist/types/charts/legend.d.ts +0 -60
  184. package/dist/types/plots/delta.d.ts +0 -19
  185. package/dist/types/plots/styles/common.d.ts +0 -52
  186. package/dist/types/plots/styles/geo.d.ts +0 -42
  187. /package/dist/{plots/geo → layout/coordinates}/projections/argentina.js +0 -0
  188. /package/dist/{plots/geo → layout/coordinates}/projections/fit.d.ts +0 -0
  189. /package/dist/{plots/geo → layout/coordinates}/projections/fit.js +0 -0
  190. /package/dist/{plots/geo → layout/coordinates}/rotate.svelte.d.ts +0 -0
  191. /package/dist/{plots/geo → layout/coordinates}/rotate.svelte.js +0 -0
  192. /package/dist/{plots/geo → layout/coordinates}/zoom.svelte.js +0 -0
  193. /package/dist/types/{charts/legend.js → layout/coordinates.js} +0 -0
  194. /package/dist/types/{plots/axis.js → layout/facet.js} +0 -0
  195. /package/dist/types/{plots/delta.js → layout/geometry.js} +0 -0
  196. /package/dist/types/{plots/scales/geo.js → layout/scales.js} +0 -0
  197. /package/dist/types/{plots/styles/common.js → layout/styles.js} +0 -0
  198. /package/dist/types/{plots/styles/geo.js → layout/timeline.js} +0 -0
@@ -1,30 +1,17 @@
1
1
  <script lang="ts" generics="TData extends Record<string, unknown>">
2
- import { getContext } from 'svelte';
3
- import { get } from 'svelte/store';
4
2
  import { SvelteMap } from 'svelte/reactivity';
5
- import type { Writable } from 'svelte/store';
6
3
  import { Text, usePlot } from 'svelteplot';
7
4
  import { resolveAccessor } from '../utils/accessors';
8
5
  import { getConfiguration } from '../../configuration/config.svelte';
9
6
  import { paletteColor } from '../../utils/color';
10
7
  import { declutter1D, laneAssignment } from '../utils/declutter';
8
+ import { disabledFillOverride, disabledStrokeOverride } from '../utils/legendDisabled';
9
+ import { watchLabelOverflow } from '../utils/labelOverflow.svelte';
11
10
  import type { Series } from '../../types/plots/data/common';
12
- import type { ValuesStyle } from '../../types/plots/styles/common';
11
+ import type { ValuesStyle } from '../../types/layout/styles';
13
12
  import type { ValueAnchor } from '../../types/plots/constants';
14
-
15
- // svelteplot's own type for this context isn't part of its public export
16
- // surface, but the context itself (keyed by this fixed string, set by its
17
- // `<Plot>`) is — every axis mark already reads/writes it this way to
18
- // auto-size margins around what it actually renders. Mirroring that here
19
- // is what lets `margins.right: 'auto'` (the default) grow to fit these
20
- // labels on its own, instead of the caller having to guess a fixed
21
- // `margins.right` up front.
22
- type AutoMarginStores = {
23
- autoMarginTop: Writable<Map<string, number>>;
24
- autoMarginLeft: Writable<Map<string, number>>;
25
- autoMarginRight: Writable<Map<string, number>>;
26
- autoMarginBottom: Writable<Map<string, number>>;
27
- };
13
+ import type { LegendDisabledStyle } from '../../types/layout/legend';
14
+ import type { MarginOverflow } from '../utils/labelOverflow.svelte';
28
15
 
29
16
  /**
30
17
  * End-point value labels for `LinePlot` — one label per series at its last
@@ -37,9 +24,20 @@
37
24
  let {
38
25
  lastPoints,
39
26
  values,
27
+ onOverflow,
28
+ disabledFor,
29
+ onHoverSeries,
30
+ onHoverEnd,
40
31
  }: {
41
32
  lastPoints: { series: Series<TData>; lastRow: TData; seriesIndex: number }[];
42
33
  values: ValuesStyle | undefined;
34
+ /** Reports how far the rendered labels overflow the plot's own content box, in pixels, on each side (0 = no overflow). */
35
+ onOverflow?: (overflow: MarginOverflow) => void;
36
+ /** Resolved legend/hover-isolate dimming style for a series, if any. */
37
+ disabledFor?: (name: string) => LegendDisabledStyle | undefined;
38
+ /** Fired when the pointer enters a label — only wired when hover-isolate is enabled. */
39
+ onHoverSeries?: (name: string) => void;
40
+ onHoverEnd?: () => void;
43
41
  } = $props();
44
42
 
45
43
  const cfg = $derived(getConfiguration());
@@ -63,7 +61,7 @@
63
61
 
64
62
  const font = $derived(values?.fontStyle);
65
63
  const connector = $derived(values?.strokeStyle);
66
- const fmtVal = $derived(values?.format ?? ((v: number, name: string) => `${name} - ${v}`));
64
+ const formatValue = $derived(values?.format ?? ((v: number, name: string) => `${name} - ${v}`));
67
65
  const valAnchor = $derived<ValueAnchor>(values?.anchor ?? 'start');
68
66
  // 'outside' is a per-point escape hatch resolved by HoverMarker; line's own
69
67
  // value labels already have their own `anchor: 'outside'` concept above,
@@ -76,54 +74,40 @@
76
74
  valAnchor === 'start' ? ('start' as const) : valAnchor === 'end' ? ('end' as const) : ('middle' as const),
77
75
  );
78
76
  const defaultLA = $derived(valAnchor === 'outside' ? ('bottom' as const) : ('middle' as const));
77
+ const fontSize = $derived(typeof font?.fontSize === 'number' ? font.fontSize : 12);
79
78
 
80
79
  const plot = usePlot();
81
80
 
82
- // ── Auto margin ──────────────────────────────────────────────────────────
83
- // Registers how far these labels (+ their leader lines) actually extend
84
- // past the plot's own content box, on whichever side they grow toward, so
85
- // `margins.{left,right}: 'auto'` (the default) sizes itself to fit them
86
- // the same mechanism svelteplot's own axis labels use, rather than a
87
- // fixed margin the caller has to guess and hard-code up front.
88
- const autoMargins = getContext<AutoMarginStores | undefined>('svelteplot/autoMargins');
89
- const marginId = `line-value-labels-${Math.random().toString(36).slice(2)}`;
81
+ // ── Overflow measurement ─────────────────────────────────────────────────
82
+ // Measures how far the rendered labels extend past the plot's own content
83
+ // box and reports it to the caller (`Plot.svelte`), which grows its own
84
+ // local margin state to fit see `labelOverflow.svelte.ts` for the actual
85
+ // `getBBox()` measurement. Reporting through a plain callback instead of
86
+ // registering directly into svelteplot's *shared* margin-auto-sizing
87
+ // context keeps this entirely local to one `<Plot>` instance: nothing here
88
+ // is shared across instances, which is what made that old approach spike
89
+ // CPU/memory into unresponsiveness when many instances mounted/unmounted
90
+ // together (e.g. a multi-story docs page → single story).
90
91
  let groupEl: SVGGElement | undefined = $state();
91
92
 
92
- $effect(() => {
93
- // Depend on whatever actually changes the rendered label layout —
94
- // getBBox() below is a plain DOM read, not itself tracked, so without
95
- // this the effect would only ever measure once.
96
- void lastPoints;
97
- void values;
98
- const el = groupEl;
99
- if (!autoMargins || !el) return;
100
- const xRange = plot.scales.x?.fn?.range?.()?.map(Number);
101
- if (!xRange) return;
102
- const plotLeft = Math.min(xRange[0], xRange[1]);
103
- const plotRight = Math.max(xRange[0], xRange[1]);
104
- // getBBox() is in the <svg>'s own user-space coordinates — the same
105
- // pixel space `plot.scales.*.fn()` already produces everywhere else in
106
- // this file, so no viewport/DOM conversion is needed here.
107
- let box: { x: number; width: number };
108
- try {
109
- box = el.getBBox();
110
- } catch {
111
- return;
112
- }
113
- const overflowRight = Math.ceil(Math.max(0, box.x + box.width - plotRight));
114
- const overflowLeft = Math.ceil(Math.max(0, plotLeft - box.x));
115
- const rightMargins = get(autoMargins.autoMarginRight);
116
- const leftMargins = get(autoMargins.autoMarginLeft);
117
- if (rightMargins.get(marginId) !== overflowRight) rightMargins.set(marginId, overflowRight);
118
- if (leftMargins.get(marginId) !== overflowLeft) leftMargins.set(marginId, overflowLeft);
119
- });
120
-
121
- $effect(() => {
122
- return () => {
123
- if (!autoMargins) return;
124
- get(autoMargins.autoMarginRight).delete(marginId);
125
- get(autoMargins.autoMarginLeft).delete(marginId);
126
- };
93
+ watchLabelOverflow({
94
+ el: () => groupEl,
95
+ bounds: () => {
96
+ // getBBox() inside isn't itself tracked depend on whatever actually
97
+ // changes the rendered label layout so this re-measures.
98
+ void lastPoints;
99
+ void values;
100
+ const xRange = plot.scales.x?.fn?.range?.()?.map(Number);
101
+ const yRange = plot.scales.y?.fn?.range?.()?.map(Number);
102
+ if (!xRange || !yRange) return undefined;
103
+ return {
104
+ left: Math.min(xRange[0], xRange[1]),
105
+ right: Math.max(xRange[0], xRange[1]),
106
+ top: Math.min(yRange[0], yRange[1]),
107
+ bottom: Math.max(yRange[0], yRange[1]),
108
+ };
109
+ },
110
+ onOverflow: (overflow) => onOverflow?.(overflow),
127
111
  });
128
112
 
129
113
  /**
@@ -151,7 +135,6 @@
151
135
  if (lastPoints.length < 2) return map;
152
136
  const yScale = plot.scales.y?.fn;
153
137
  if (!yScale) return map;
154
- const fontSize = font?.fontSize ?? 12;
155
138
  const minGap = Math.max(16, fontSize * 1.4);
156
139
  const items = lastPoints.map(({ series: s, lastRow }) => ({
157
140
  name: s.name,
@@ -192,6 +175,12 @@
192
175
  {@const labelDx = offset?.inGroup
193
176
  ? dir * (elbowGap + (numLanes - 1) * laneStep + labelGap)
194
177
  : baseDx}
178
+ {@const text = formatValue(Number(yFn(lastRow)), s.name)}
179
+ {@const preferredTA = resolvedTextAnchor ?? defaultTA}
180
+ {@const preferredLA = font?.lineAnchor ?? defaultLA}
181
+ {@const disabled = disabledFor?.(s.name)}
182
+ {@const disabledStroke = disabledStrokeOverride(disabled)}
183
+ {@const disabledFill = disabledFillOverride(disabled)}
195
184
  {#if offset?.inGroup && plot.scales.x?.fn && plot.scales.y?.fn}
196
185
  {@const px = Number(plot.scales.x.fn(xFn(lastRow)))}
197
186
  {@const py = Number(plot.scales.y.fn(Number(yFn(lastRow))))}
@@ -202,32 +191,42 @@
202
191
  fill="none"
203
192
  stroke={connector?.stroke ?? defaultConnectorColor}
204
193
  stroke-width={connector?.strokeWidth ?? defaultConnectorWidth}
205
- stroke-opacity={connector?.strokeOpacity ?? 1}
194
+ stroke-opacity={connector?.strokeOpacity ?? disabledStroke?.strokeOpacity ?? 1}
206
195
  stroke-dasharray={connector?.strokeDasharray}
207
196
  stroke-linecap={connector?.strokeLinecap ?? 'square'}
208
197
  stroke-linejoin={connector?.strokeLinejoin ?? 'miter'}
209
198
  />
210
199
  {/if}
211
- <Text
212
- data={[lastRow]}
213
- x={xFn}
214
- y={yFn}
215
- text={() => fmtVal(Number(yFn(lastRow)), s.name)}
216
- dx={labelDx}
217
- dy={(font?.dy ?? defaultDy) + extraDy}
218
- textAnchor={resolvedTextAnchor ?? defaultTA}
219
- lineAnchor={font?.lineAnchor ?? defaultLA}
220
- lineHeight={font?.lineHeight}
221
- rotate={font?.rotate}
222
- class={font?.class}
223
- textClass={font?.textClass}
224
- fill={font?.fill ?? seriesColor}
225
- fontSize={font?.fontSize ?? 12}
226
- fontWeight={font?.fontWeight}
227
- fontStyle={font?.fontStyle}
228
- stroke={font?.stroke}
229
- strokeWidth={font?.strokeWidth}
230
- paintOrder={font?.paintOrder}
231
- />
200
+ <!-- svelteplot's Text mark never wires pointer handlers to its DOM node,
201
+ despite declaring them in its types — a wrapping <g> catches the
202
+ boundary crossing instead (pointerenter/leave fire on every ancestor). -->
203
+ <g
204
+ role="presentation"
205
+ onpointerenter={onHoverSeries && (() => onHoverSeries(s.name))}
206
+ onpointerleave={onHoverEnd}
207
+ >
208
+ <Text
209
+ data={[lastRow]}
210
+ x={xFn}
211
+ y={yFn}
212
+ text={() => text}
213
+ dx={labelDx}
214
+ dy={(font?.dy ?? defaultDy) + extraDy}
215
+ textAnchor={preferredTA}
216
+ lineAnchor={preferredLA}
217
+ lineHeight={font?.lineHeight}
218
+ rotate={font?.rotate}
219
+ class={font?.class}
220
+ textClass={font?.textClass}
221
+ fill={font?.fill ?? seriesColor}
222
+ fillOpacity={disabledFill?.fillOpacity}
223
+ {fontSize}
224
+ fontWeight={font?.fontWeight}
225
+ fontStyle={font?.fontStyle}
226
+ stroke={font?.stroke}
227
+ strokeWidth={font?.strokeWidth}
228
+ paintOrder={font?.paintOrder}
229
+ />
230
+ </g>
232
231
  {/each}
233
232
  </g>
@@ -1,5 +1,7 @@
1
1
  import type { Series } from '../../types/plots/data/common';
2
- import type { ValuesStyle } from '../../types/plots/styles/common';
2
+ import type { ValuesStyle } from '../../types/layout/styles';
3
+ import type { LegendDisabledStyle } from '../../types/layout/legend';
4
+ import type { MarginOverflow } from '../utils/labelOverflow.svelte';
3
5
  declare function $$render<TData extends Record<string, unknown>>(): {
4
6
  props: {
5
7
  lastPoints: {
@@ -8,6 +10,13 @@ declare function $$render<TData extends Record<string, unknown>>(): {
8
10
  seriesIndex: number;
9
11
  }[];
10
12
  values: ValuesStyle | undefined;
13
+ /** Reports how far the rendered labels overflow the plot's own content box, in pixels, on each side (0 = no overflow). */
14
+ onOverflow?: (overflow: MarginOverflow) => void;
15
+ /** Resolved legend/hover-isolate dimming style for a series, if any. */
16
+ disabledFor?: (name: string) => LegendDisabledStyle | undefined;
17
+ /** Fired when the pointer enters a label — only wired when hover-isolate is enabled. */
18
+ onHoverSeries?: (name: string) => void;
19
+ onHoverEnd?: () => void;
11
20
  };
12
21
  exports: {};
13
22
  bindings: "";
@@ -0,0 +1,15 @@
1
+ import type { Series } from '../../types/plots/data/common';
2
+ import type { LineSegmentStyle } from '../../types/plots/segments/common';
3
+ import type { VisualGroup } from '../../types/plots/segments/config';
4
+ import type { StrokeStyle, DotStyle } from '../../types/plots/styling';
5
+ import type { LineMarkerConfig, DotMarkerConfig } from '../../types/markers/common';
6
+ /**
7
+ * Every `'line'` (+ `'dot'`, where a segment's style asks for them) marker
8
+ * for one LinePlot render: one `'line'` marker per visual segment, plus a
9
+ * `'dot'` marker right after it when that segment sets `style.dots`.
10
+ */
11
+ export declare function buildLineMarkers<TData extends Record<string, unknown>>(args: {
12
+ groupedSeries: VisualGroup<Series<TData>, LineSegmentStyle>[];
13
+ disabledStrokeFor: (seriesName: string) => StrokeStyle | undefined;
14
+ disabledDotsFor: (seriesName: string) => DotStyle | undefined;
15
+ }): (LineMarkerConfig | DotMarkerConfig)[];
@@ -0,0 +1,57 @@
1
+ import { resolveAccessor } from '../utils/accessors';
2
+ import { paletteColor } from '../../utils/color';
3
+ import { getConfiguration } from '../../configuration/config.svelte';
4
+ /**
5
+ * Every `'line'` (+ `'dot'`, where a segment's style asks for them) marker
6
+ * for one LinePlot render: one `'line'` marker per visual segment, plus a
7
+ * `'dot'` marker right after it when that segment sets `style.dots`.
8
+ */
9
+ export function buildLineMarkers(args) {
10
+ const { groupedSeries, disabledStrokeFor, disabledDotsFor } = args;
11
+ const cfg = getConfiguration();
12
+ const markers = [];
13
+ groupedSeries.forEach((group, seriesIndex) => {
14
+ const defaultColor = paletteColor(seriesIndex, cfg.palette);
15
+ const disabledStroke = disabledStrokeFor(group.series.name);
16
+ const disabledDots = disabledDotsFor(group.series.name);
17
+ const xFn = resolveAccessor(group.series.x);
18
+ const yFn = resolveAccessor(group.series.y);
19
+ for (const seg of group.visualSegments) {
20
+ const strokeStyle = seg.style.stroke;
21
+ const dotStyle = seg.style.dots;
22
+ markers.push({
23
+ type: 'line',
24
+ data: seg.data,
25
+ x: xFn,
26
+ y: yFn,
27
+ style: {
28
+ stroke: disabledStroke?.stroke ?? strokeStyle?.stroke ?? defaultColor,
29
+ strokeWidth: disabledStroke?.strokeWidth ?? strokeStyle?.strokeWidth ?? cfg.line.strokeWidth,
30
+ strokeOpacity: disabledStroke?.strokeOpacity ?? strokeStyle?.strokeOpacity ?? 1,
31
+ strokeDasharray: disabledStroke?.strokeDasharray ?? strokeStyle?.strokeDasharray,
32
+ strokeLinejoin: disabledStroke?.strokeLinejoin ?? strokeStyle?.strokeLinejoin,
33
+ strokeLinecap: disabledStroke?.strokeLinecap ?? strokeStyle?.strokeLinecap,
34
+ },
35
+ });
36
+ if (!dotStyle)
37
+ continue;
38
+ markers.push({
39
+ type: 'dot',
40
+ data: seg.data,
41
+ x: xFn,
42
+ y: yFn,
43
+ style: {
44
+ dotRadius: disabledDots?.dotRadius ?? dotStyle.dotRadius ?? cfg.line.dotRadius,
45
+ dotFill: disabledDots?.dotFill ?? dotStyle.dotFill ?? strokeStyle?.stroke ?? defaultColor,
46
+ dotSymbol: disabledDots?.dotSymbol ?? dotStyle.dotSymbol ?? 'circle',
47
+ dotStroke: disabledDots?.dotStroke ?? dotStyle.dotStroke ?? strokeStyle?.stroke ?? defaultColor,
48
+ dotStrokeWidth: disabledDots?.dotStrokeWidth ?? dotStyle.dotStrokeWidth ?? 1,
49
+ dotFillOpacity: disabledDots?.dotFillOpacity ?? dotStyle.dotFillOpacity ?? strokeStyle?.strokeOpacity ?? 1,
50
+ dotStrokeOpacity: disabledDots?.dotStrokeOpacity ?? dotStyle.dotStrokeOpacity ?? strokeStyle?.strokeOpacity ?? 1,
51
+ dotStrokeDasharray: disabledDots?.dotStrokeDasharray ?? dotStyle.dotStrokeDasharray,
52
+ },
53
+ });
54
+ }
55
+ });
56
+ return markers;
57
+ }
@@ -1,28 +1,24 @@
1
1
  <script lang="ts" generics="TData extends Record<string, unknown>">
2
- import { SvelteMap } from 'svelte/reactivity';
3
- import { BarX } from 'svelteplot';
4
2
  import { resolveAccessor } from '../utils/accessors';
5
3
  import { getConfiguration } from '../../configuration/config.svelte';
6
4
  import { paletteColor } from '../../utils/color';
7
5
  import { buildSeries } from '../../utils/grouping';
8
6
  import { getLegendInteraction } from '../../layout/legend/interaction.svelte';
9
7
  import { disabledFillOverride } from '../utils/legendDisabled';
10
- import { buildGroupedSeries, validateSegments, resolveSegmentsForSeries, matchesSegmentX, getDataForSegments } from '../utils/segments';
8
+ import { buildPyramidBarMarkers } from './buildPyramidBarMarkers';
9
+ import { createLabelMarginTracker } from '../utils/labelOverflow.svelte';
10
+ import { buildGroupedSeries, validateSegments, buildScopedMarkerGroups } from '../utils/segments';
11
11
  import BasePlotLayout from '../../layout/plot/BasePlotLayout.svelte';
12
- import AxisLayout from '../../layout/plot/AxisLayout.svelte';
13
- import GridLayout from '../../layout/plot/GridLayout.svelte';
14
- import RuleLayout from '../../layout/plot/RuleLayout.svelte';
15
- import HoverMarker from '../../markers/HoverMarker.svelte';
16
12
  import ValueLabels from './ValueLabels.svelte';
17
13
  import { hasHoverMarker, resolveHoverStrategy, resolveHoverSync } from '../../layout/tooltip/utils';
18
14
  import type { BasePlotProps } from '../../types/plots/props';
19
- import type { AxisValue } from '../../types/plots/axis';
15
+ import type { AxisValue, AxisBasedScalesConfig } from '../../types/layout/scales';
20
16
  import type { Series, SeriesProps, DataProps } from '../../types/plots/data/common';
21
- import type { AxisBasedScalesConfig } from '../../types/plots/props';
22
- import type { BasePlotStyles } from '../../types/plots/styles/common';
17
+ import type { BasePlotStylesConfig } from '../../types/layout/styles';
23
18
  import type { ValueAnchor } from '../../types/plots/constants';
24
19
  import type { BarSegmentStyle } from '../../types/plots/segments/common';
25
- import type { Marker } from '../../types/markers/common';
20
+ import type { AxisBasedMarkersConfig } from '../../types/markers/common';
21
+ import type { LegendDisabledStyle } from '../../types/layout/legend';
26
22
 
27
23
  /**
28
24
  * A population-pyramid-style diverging bar chart: each series is mirrored
@@ -43,9 +39,9 @@
43
39
  type Props = BasePlotProps<
44
40
  SeriesProps<TData> | DataProps<TData>,
45
41
  TData,
46
- BasePlotStyles,
42
+ BasePlotStylesConfig,
47
43
  BarSegmentStyle,
48
- Marker,
44
+ AxisBasedMarkersConfig,
49
45
  AxisBasedScalesConfig<TData>
50
46
  >;
51
47
 
@@ -69,10 +65,27 @@
69
65
 
70
66
  const cfg = $derived(getConfiguration());
71
67
  const legendInteraction = $derived(getLegendInteraction());
68
+ const hoverIsolate = $derived(styles.values?.hoverIsolate ?? false);
69
+
70
+ // A value label hovered while `styles.values.hoverIsolate` is on dims
71
+ // every other series exactly like a legend toggle would — local to this
72
+ // one `<Plot>` instance (not the shared HoverStore) so the gesture can't
73
+ // leak into unrelated tooltip/crosshair behavior.
74
+ let hoverIsolatedSeries = $state<string | null>(null);
75
+
76
+ function effectiveDisabledFor(name: string): LegendDisabledStyle | undefined {
77
+ return (
78
+ legendInteraction?.disabledSeries.get(name) ??
79
+ (hoverIsolate && hoverIsolatedSeries && hoverIsolatedSeries !== name
80
+ ? { opacity: cfg.legend.disabledOpacity }
81
+ : undefined)
82
+ );
83
+ }
84
+
72
85
  const leftColor = $derived(styles.colors?.left ?? paletteColor(0, cfg.palette));
73
86
  const rightColor = $derived(styles.colors?.right ?? paletteColor(1, cfg.palette));
74
87
  const showVals = $derived(styles.values?.show ?? false);
75
- const fmtVal = $derived(styles.values?.format ?? ((v: number) => `${v}`));
88
+ const formatValue = $derived(styles.values?.format ?? ((v: number) => `${v}`));
76
89
  const valAnchor = $derived<ValueAnchor>(styles.values?.anchor ?? 'outside');
77
90
 
78
91
  // Sort series names alphabetically; even index → left, odd → right.
@@ -123,7 +136,7 @@
123
136
  __side: s.side,
124
137
  __x: s.side === 'left' ? -v : v,
125
138
  __y: getCategory(d) as AxisValue,
126
- __label: fmtVal(v, s.name),
139
+ __label: formatValue(v, s.name),
127
140
  __series: s.name,
128
141
  } as TaggedRow;
129
142
  });
@@ -166,17 +179,6 @@
166
179
  return vals.length ? Math.max(...vals) : 1;
167
180
  });
168
181
 
169
- const seriesMarkers = $derived.by(() => {
170
- const map = new SvelteMap<string, Marker[]>();
171
- for (const marker of markers) {
172
- if ((marker.type === 'component' || marker.type === 'hover') && marker.series) {
173
- if (!map.has(marker.series)) map.set(marker.series, []);
174
- map.get(marker.series)!.push(marker);
175
- }
176
- }
177
- return map;
178
- });
179
-
180
182
  // The single, fully-resolved scales object BasePlotLayout reads for
181
183
  // everything (ticks, grid, sort, and the real svelteplot scale) — avoids
182
184
  // keeping a second, separately-defaulted copy of `scales.x`/`scales.y`
@@ -186,117 +188,74 @@
186
188
  ...scales,
187
189
  x: {
188
190
  domain: [-maxMagnitude, maxMagnitude],
189
- tickFormat: (v: unknown) => fmtVal(Math.abs(Number(v)), ''),
191
+ tickFormat: (v: unknown) => formatValue(Math.abs(Number(v)), ''),
190
192
  ...(scales.x ?? {}),
191
193
  },
192
194
  y: { type: 'band', padding: 0.2, domain: yDomain, ...(scales.y ?? {}) },
193
195
  });
194
196
 
195
- const hoverActive = $derived(tooltip != null || hasHoverMarker(markers));
196
- const hoverStrategy = $derived(resolveHoverStrategy(tooltip?.strategy, markers, 'y'));
197
- const hoverSync = $derived(resolveHoverSync(tooltip?.sync, markers));
197
+ const hover = $derived({
198
+ active: tooltip != null || hasHoverMarker(markers),
199
+ points: hoverPoints,
200
+ label: labelFor,
201
+ strategy: resolveHoverStrategy(tooltip?.strategy, markers, 'y'),
202
+ sync: resolveHoverSync(tooltip?.sync, markers),
203
+ tooltip,
204
+ });
205
+
206
+ const pyramidMarkers = $derived(
207
+ buildPyramidBarMarkers({
208
+ groupedSeries,
209
+ valueFor: signedValue,
210
+ colorFor: (s) => (s.side === 'left' ? leftColor : rightColor),
211
+ disabledFillFor: (name) => disabledFillOverride(effectiveDisabledFor(name)),
212
+ }),
213
+ );
214
+
215
+ const scopedGroups = $derived(
216
+ buildScopedMarkerGroups<TData, InternalSeries, BarSegmentStyle>({
217
+ groupedSeries,
218
+ segments,
219
+ colorFor: (series) => (series.side === 'left' ? leftColor : rightColor),
220
+ segmentColorFor: (style, defaultColor) => style?.fill ?? defaultColor,
221
+ }),
222
+ );
223
+
224
+ // A pyramid bar's value label can grow past the plot's own content box —
225
+ // reserving real space there is what each label's own overflow
226
+ // measurement feeds into here, via a tracker local to this one `<Plot>`
227
+ // instance. See `labelOverflow.svelte.ts`.
228
+ const labelMargin = createLabelMarginTracker(() => margins);
198
229
  </script>
199
230
 
200
231
  <BasePlotLayout
201
232
  {width}
202
233
  {height}
234
+ {styles}
203
235
  data={flat}
204
236
  {getX}
205
237
  {getY}
206
- {hoverPoints}
207
- label={labelFor}
208
- {tooltip}
209
- {hoverActive}
210
- {hoverStrategy}
211
- {hoverSync}
238
+ {hover}
212
239
  scales={resolvedScales}
213
- {margins}
240
+ margins={labelMargin.resolvedMargins}
214
241
  xTickRotate={resolvedScales?.x?.tickRotate}
242
+ markers={[...pyramidMarkers, ...markers]}
243
+ seriesData={resolvedSeries}
244
+ {scopedGroups}
215
245
  >
216
- {#snippet children({ matchedPoints, ruleX, ruleY, impliesRuleX, impliesRuleY, numericMargins })}
217
- <AxisLayout data={flat} {getX} {getY} {width} {height} {numericMargins} scales={resolvedScales} />
218
- <GridLayout scales={resolvedScales} />
219
- <RuleLayout {markers} seriesData={resolvedSeries} {matchedPoints} {ruleX} {ruleY} {impliesRuleX} {impliesRuleY}>
246
+ {#if showVals}
220
247
  {#each groupedSeries as group (group.series.name)}
221
- {@const defaultColor = group.series.side === 'left' ? leftColor : rightColor}
222
- {@const getCategory = resolveAccessor(group.series.x)}
223
- {@const disabledFill = disabledFillOverride(legendInteraction?.disabledSeries.get(group.series.name))}
224
- {#each group.visualSegments as seg, i (`${group.series.name}-${i}`)}
225
- <BarX
226
- data={seg.data}
227
- x1={0}
228
- x2={(d: TData) => signedValue(group.series, d)}
229
- y={getCategory}
230
- fill={disabledFill?.fill ?? seg.style.fill ?? defaultColor}
231
- fillOpacity={disabledFill?.fillOpacity ?? seg.style.fillOpacity ?? 1}
232
- />
233
- {/each}
234
-
235
- {#if showVals}
236
- <ValueLabels {group} {valAnchor} {fmtVal} values={styles.values} axisColor={cfg.axis.color} />
237
- {/if}
238
-
239
- {#each seriesMarkers.get(group.series.name) ?? [] as marker, i (`series-${group.series.name}-${marker.type}-${i}`)}
240
- {#if marker.type === 'component'}
241
- {#if marker.scope === 'segment'}
242
- {@const segs = resolveSegmentsForSeries(segments, group.series.name)}
243
- {@const segmentDataArrays = getDataForSegments(group.series.data, getCategory, segs)}
244
- {#each segmentDataArrays as segmentData, segIdx (`${group.series.name}-marker-segment-${segIdx}`)}
245
- {#if segmentData.length > 0}
246
- {@const segmentColor = segs[segIdx]?.style?.fill ?? defaultColor}
247
- <marker.component
248
- {...marker.props}
249
- series={group.series.name}
250
- data={segmentData}
251
- seriesColor={segmentColor}
252
- />
253
- {/if}
254
- {/each}
255
- {:else}
256
- {@const segs = resolveSegmentsForSeries(segments, group.series.name)}
257
- {@const seriesColor = segs[0]?.style?.fill ?? defaultColor}
258
- <marker.component
259
- {...marker.props}
260
- series={group.series.name}
261
- data={group.series.data}
262
- seriesColor={seriesColor}
263
- />
264
- {/if}
265
- {:else if marker.type === 'hover'}
266
- {@const seriesPoints = matchedPoints.filter((p) => p.series === group.series.name)}
267
- {#if marker.scope === 'segment'}
268
- {@const segs = resolveSegmentsForSeries(segments, group.series.name)}
269
- {#each segs as seg, segIdx (`${group.series.name}-hover-segment-${segIdx}`)}
270
- {@const segPoints = seriesPoints.filter((p) => matchesSegmentX(p.x, seg))}
271
- {#if segPoints.length > 0}
272
- <HoverMarker
273
- data={segPoints}
274
- ruleX={marker.ruleX !== false && (impliesRuleX || marker.ruleX === true) ? ruleX : null}
275
- ruleY={marker.ruleY !== false && (impliesRuleY || marker.ruleY === true) ? ruleY : null}
276
- showLabels={marker.showLabels}
277
- ruleStyle={marker.ruleStyle}
278
- dotStyle={marker.dotStyle}
279
- fontStyle={marker.fontStyle}
280
- seriesColor={seg.style?.fill ?? defaultColor}
281
- />
282
- {/if}
283
- {/each}
284
- {:else}
285
- {@const segs = resolveSegmentsForSeries(segments, group.series.name)}
286
- <HoverMarker
287
- data={seriesPoints}
288
- ruleX={marker.ruleX !== false && (impliesRuleX || marker.ruleX === true) ? ruleX : null}
289
- ruleY={marker.ruleY !== false && (impliesRuleY || marker.ruleY === true) ? ruleY : null}
290
- showLabels={marker.showLabels}
291
- ruleStyle={marker.ruleStyle}
292
- dotStyle={marker.dotStyle}
293
- fontStyle={marker.fontStyle}
294
- seriesColor={segs[0]?.style?.fill ?? defaultColor}
295
- />
296
- {/if}
297
- {/if}
298
- {/each}
248
+ <ValueLabels
249
+ {group}
250
+ {valAnchor}
251
+ {formatValue}
252
+ values={styles.values}
253
+ axisColor={cfg.axis.color}
254
+ disabled={effectiveDisabledFor(group.series.name)}
255
+ onHoverSeries={hoverIsolate ? () => { hoverIsolatedSeries = group.series.name; } : undefined}
256
+ onHoverEnd={hoverIsolate ? () => { hoverIsolatedSeries = null; } : undefined}
257
+ onOverflow={labelMargin.report}
258
+ />
299
259
  {/each}
300
- </RuleLayout>
301
- {/snippet}
260
+ {/if}
302
261
  </BasePlotLayout>
@@ -1,11 +1,10 @@
1
1
  import type { BasePlotProps } from '../../types/plots/props';
2
+ import type { AxisBasedScalesConfig } from '../../types/layout/scales';
2
3
  import type { SeriesProps, DataProps } from '../../types/plots/data/common';
3
- import type { AxisBasedScalesConfig } from '../../types/plots/props';
4
- import type { BasePlotStyles } from '../../types/plots/styles/common';
5
- import type { BarSegmentStyle } from '../../types/plots/segments/common';
6
- import type { Marker } from '../../types/markers/common';
4
+ import type { BasePlotStylesConfig } from '../../types/layout/styles';
5
+ import type { AxisBasedMarkersConfig } from '../../types/markers/common';
7
6
  declare function $$render<TData extends Record<string, unknown>>(): {
8
- props: BasePlotProps<SeriesProps<TData> | DataProps<TData>, TData, BasePlotStyles, BarSegmentStyle, Marker, AxisBasedScalesConfig<TData>>;
7
+ props: BasePlotProps<SeriesProps<TData> | DataProps<TData>, TData, BasePlotStylesConfig, import("../..").AreaStyle, AxisBasedMarkersConfig, AxisBasedScalesConfig<TData>>;
9
8
  exports: {};
10
9
  bindings: "";
11
10
  slots: {};