@fundar/data-chart-telling 0.0.24 → 0.0.26

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 (170) hide show
  1. package/dist/charts/bar/Chart.svelte +2 -0
  2. package/dist/charts/heatmap/Chart.svelte +3 -1
  3. package/dist/charts/heatmap/Chart.svelte.d.ts +1 -2
  4. package/dist/charts/line/Chart.svelte +2 -0
  5. package/dist/charts/pyramid/Chart.svelte +3 -1
  6. package/dist/charts/pyramid/Chart.svelte.d.ts +1 -2
  7. package/dist/charts/scatter/Chart.svelte +95 -0
  8. package/dist/charts/scatter/Chart.svelte.d.ts +28 -0
  9. package/dist/configuration/config.svelte.js +13 -1
  10. package/dist/configuration/themes/index.d.ts +76 -0
  11. package/dist/index.d.ts +20 -14
  12. package/dist/index.js +9 -3
  13. package/dist/layout/coordinates/CoordinatesLayout.svelte +95 -0
  14. package/dist/layout/{plot/RuleLayout.svelte.d.ts → coordinates/CoordinatesLayout.svelte.d.ts} +13 -16
  15. package/dist/layout/coordinates/margins.d.ts +16 -0
  16. package/dist/layout/coordinates/margins.js +11 -0
  17. package/dist/{plots/geo → layout/coordinates}/projections/argentina.d.ts +1 -1
  18. package/dist/layout/{geo → coordinates}/resolveProjection.d.ts +1 -1
  19. package/dist/layout/coordinates/resolveProjection.js +24 -0
  20. package/dist/{plots/geo → layout/coordinates}/zoom.svelte.d.ts +1 -1
  21. package/dist/layout/geometry/GeometryLayout.svelte +231 -0
  22. package/dist/layout/geometry/GeometryLayout.svelte.d.ts +62 -0
  23. package/dist/layout/plot/BasePlotLayout.svelte +142 -155
  24. package/dist/layout/plot/BasePlotLayout.svelte.d.ts +61 -51
  25. package/dist/layout/plot/margins.d.ts +0 -16
  26. package/dist/layout/plot/margins.js +0 -11
  27. package/dist/layout/{plot/AxisLayout.svelte → scales/ScalesLayout.svelte} +23 -16
  28. package/dist/layout/{plot/AxisLayout.svelte.d.ts → scales/ScalesLayout.svelte.d.ts} +4 -5
  29. package/dist/layout/scales/buildScale.d.ts +8 -0
  30. package/dist/layout/scales/buildScale.js +43 -0
  31. package/dist/layout/styles/StylesLayout.svelte +75 -0
  32. package/dist/layout/styles/StylesLayout.svelte.d.ts +15 -0
  33. package/dist/{plots/geo → layout/styles}/TileLayer.svelte +2 -2
  34. package/dist/{plots/geo → layout/styles}/TileLayer.svelte.d.ts +1 -1
  35. package/dist/layout/tooltip/Tooltip.svelte +16 -20
  36. package/dist/layout/tooltip/Tooltip.svelte.d.ts +5 -0
  37. package/dist/layout/tooltip/TooltipLayout.svelte +7 -1
  38. package/dist/layout/tooltip/TooltipLayout.svelte.d.ts +4 -0
  39. package/dist/layout/tooltip/controller.svelte.d.ts +4 -5
  40. package/dist/layout/tooltip/controller.svelte.js +28 -16
  41. package/dist/layout/tooltip/resolveTooltipPlacement.d.ts +24 -0
  42. package/dist/layout/tooltip/resolveTooltipPlacement.js +49 -0
  43. package/dist/layout/tooltip/utils.d.ts +27 -19
  44. package/dist/layout/tooltip/utils.js +16 -15
  45. package/dist/markers/BarMarker.svelte +92 -0
  46. package/dist/markers/BarMarker.svelte.d.ts +7 -0
  47. package/dist/markers/CellMarker.svelte +21 -0
  48. package/dist/markers/CellMarker.svelte.d.ts +7 -0
  49. package/dist/markers/ContourMarker.svelte +28 -0
  50. package/dist/markers/ContourMarker.svelte.d.ts +7 -0
  51. package/dist/markers/DeltaMarker.svelte +32 -39
  52. package/dist/markers/DeltaMarker.svelte.d.ts +4 -5
  53. package/dist/markers/DotMarker.svelte +35 -6
  54. package/dist/markers/DotMarker.svelte.d.ts +11 -2
  55. package/dist/markers/GeoFeaturesMarker.svelte +19 -0
  56. package/dist/markers/GeoFeaturesMarker.svelte.d.ts +7 -0
  57. package/dist/markers/GeoHoverMarker.svelte +25 -0
  58. package/dist/markers/GeoHoverMarker.svelte.d.ts +9 -0
  59. package/dist/markers/GraticuleMarker.svelte +18 -0
  60. package/dist/markers/GraticuleMarker.svelte.d.ts +7 -0
  61. package/dist/markers/HoverMarker.svelte +32 -33
  62. package/dist/markers/HoverMarker.svelte.d.ts +15 -9
  63. package/dist/markers/LineMarker.svelte +19 -0
  64. package/dist/markers/LineMarker.svelte.d.ts +7 -0
  65. package/dist/markers/LinkMarker.svelte +42 -0
  66. package/dist/markers/LinkMarker.svelte.d.ts +7 -0
  67. package/dist/markers/RuleMarker.svelte +24 -0
  68. package/dist/markers/RuleMarker.svelte.d.ts +10 -0
  69. package/dist/markers/SphereMarker.svelte +15 -0
  70. package/dist/markers/SphereMarker.svelte.d.ts +7 -0
  71. package/dist/markers/TextMarker.svelte +1 -1
  72. package/dist/markers/TextMarker.svelte.d.ts +1 -1
  73. package/dist/markers/VectorMarker.svelte +20 -0
  74. package/dist/markers/VectorMarker.svelte.d.ts +7 -0
  75. package/dist/markers/inset/InsetMarker.svelte +38 -45
  76. package/dist/markers/inset/InsetMarker.svelte.d.ts +6 -2
  77. package/dist/plots/bar/Plot.svelte +54 -84
  78. package/dist/plots/bar/Plot.svelte.d.ts +4 -5
  79. package/dist/plots/bar/ValueLabels.svelte +1 -1
  80. package/dist/plots/bar/ValueLabels.svelte.d.ts +1 -1
  81. package/dist/plots/bar/buildBarMarkers.d.ts +30 -0
  82. package/dist/plots/bar/buildBarMarkers.js +100 -0
  83. package/dist/plots/bar/layout.svelte.d.ts +1 -2
  84. package/dist/plots/bar/layout.svelte.js +1 -1
  85. package/dist/plots/geo/Plot.svelte +113 -387
  86. package/dist/plots/geo/Plot.svelte.d.ts +5 -4
  87. package/dist/plots/geo/buildGeoMarkers.d.ts +33 -0
  88. package/dist/plots/geo/buildGeoMarkers.js +47 -0
  89. package/dist/plots/heatmap/Plot.svelte +24 -49
  90. package/dist/plots/heatmap/Plot.svelte.d.ts +4 -5
  91. package/dist/plots/heatmap/ValueLabels.svelte +2 -2
  92. package/dist/plots/heatmap/ValueLabels.svelte.d.ts +2 -2
  93. package/dist/plots/heatmap/buildCellMarkers.d.ts +21 -0
  94. package/dist/plots/heatmap/buildCellMarkers.js +25 -0
  95. package/dist/plots/line/Plot.svelte +68 -141
  96. package/dist/plots/line/Plot.svelte.d.ts +4 -4
  97. package/dist/plots/line/ValueLabels.svelte +94 -49
  98. package/dist/plots/line/ValueLabels.svelte.d.ts +6 -1
  99. package/dist/plots/line/buildLineMarkers.d.ts +15 -0
  100. package/dist/plots/line/buildLineMarkers.js +57 -0
  101. package/dist/plots/pyramid/Plot.svelte +41 -117
  102. package/dist/plots/pyramid/Plot.svelte.d.ts +4 -5
  103. package/dist/plots/pyramid/ValueLabels.svelte +1 -1
  104. package/dist/plots/pyramid/ValueLabels.svelte.d.ts +1 -1
  105. package/dist/plots/pyramid/buildPyramidBarMarkers.d.ts +19 -0
  106. package/dist/plots/pyramid/buildPyramidBarMarkers.js +32 -0
  107. package/dist/plots/scatter/Plot.svelte +206 -0
  108. package/dist/plots/{bar/BarSegments.svelte.d.ts → scatter/Plot.svelte.d.ts} +9 -20
  109. package/dist/plots/scatter/ValueLabels.svelte +106 -0
  110. package/dist/{layout/plot/GridLayout.svelte.d.ts → plots/scatter/ValueLabels.svelte.d.ts} +11 -5
  111. package/dist/plots/scatter/buildScatterMarkers.d.ts +11 -0
  112. package/dist/plots/scatter/buildScatterMarkers.js +37 -0
  113. package/dist/plots/scatter/resolveRadiusPaddedDomain.d.ts +10 -0
  114. package/dist/plots/scatter/resolveRadiusPaddedDomain.js +23 -0
  115. package/dist/plots/utils/declutter.d.ts +82 -33
  116. package/dist/plots/utils/declutter.js +182 -40
  117. package/dist/plots/utils/geoAccessors.d.ts +1 -1
  118. package/dist/plots/utils/geoAccessors.js +1 -1
  119. package/dist/plots/utils/segments.d.ts +17 -10
  120. package/dist/plots/utils/segments.js +26 -16
  121. package/dist/types/charts/common.d.ts +1 -1
  122. package/dist/types/charts/props.d.ts +5 -6
  123. package/dist/types/configuration/styling.d.ts +24 -2
  124. package/dist/types/{plots/scales/geo.d.ts → layout/coordinates.d.ts} +33 -1
  125. package/dist/types/layout/geometry.d.ts +17 -0
  126. package/dist/types/{plots/axis.d.ts → layout/scales.d.ts} +11 -0
  127. package/dist/types/layout/styles.d.ts +126 -0
  128. package/dist/types/layout/tooltip.d.ts +20 -1
  129. package/dist/types/markers/all.d.ts +11 -0
  130. package/dist/types/markers/base.d.ts +45 -0
  131. package/dist/types/markers/base.js +1 -0
  132. package/dist/types/markers/common.d.ts +64 -16
  133. package/dist/types/markers/geo.d.ts +75 -100
  134. package/dist/types/markers/line.d.ts +34 -0
  135. package/dist/types/markers/line.js +1 -0
  136. package/dist/types/markers/props.d.ts +1 -1
  137. package/dist/types/plots/data/common.d.ts +1 -1
  138. package/dist/types/plots/data/scatter.d.ts +13 -0
  139. package/dist/types/plots/data/scatter.js +1 -0
  140. package/dist/types/plots/props.d.ts +46 -74
  141. package/dist/types/plots/segments/common.d.ts +6 -12
  142. package/dist/types/plots/segments/config.d.ts +1 -1
  143. package/dist/types/plots/styling.d.ts +10 -0
  144. package/dist/{plots/utils → utils}/delta.d.ts +2 -2
  145. package/dist/{plots/utils → utils}/delta.js +1 -1
  146. package/dist/utils/edgeAwareAnchor.d.ts +37 -0
  147. package/dist/utils/edgeAwareAnchor.js +65 -0
  148. package/dist/utils/grouping.d.ts +1 -1
  149. package/dist/utils/interpolate.d.ts +1 -1
  150. package/package.json +3 -1
  151. package/dist/layout/geo/GeoLayout.svelte +0 -51
  152. package/dist/layout/geo/GeoLayout.svelte.d.ts +0 -24
  153. package/dist/layout/geo/resolveProjection.js +0 -48
  154. package/dist/layout/plot/GridLayout.svelte +0 -30
  155. package/dist/layout/plot/RuleLayout.svelte +0 -111
  156. package/dist/plots/bar/BarSegments.svelte +0 -69
  157. package/dist/types/plots/delta.d.ts +0 -19
  158. package/dist/types/plots/styles/common.d.ts +0 -52
  159. package/dist/types/plots/styles/geo.d.ts +0 -42
  160. /package/dist/{plots/geo → layout/coordinates}/projections/argentina.js +0 -0
  161. /package/dist/{plots/geo → layout/coordinates}/projections/fit.d.ts +0 -0
  162. /package/dist/{plots/geo → layout/coordinates}/projections/fit.js +0 -0
  163. /package/dist/{plots/geo → layout/coordinates}/rotate.svelte.d.ts +0 -0
  164. /package/dist/{plots/geo → layout/coordinates}/rotate.svelte.js +0 -0
  165. /package/dist/{plots/geo → layout/coordinates}/zoom.svelte.js +0 -0
  166. /package/dist/types/{plots/axis.js → layout/coordinates.js} +0 -0
  167. /package/dist/types/{plots/delta.js → layout/geometry.js} +0 -0
  168. /package/dist/types/{plots/scales/geo.js → layout/scales.js} +0 -0
  169. /package/dist/types/{plots/styles/common.js → layout/styles.js} +0 -0
  170. /package/dist/types/{plots/styles/geo.js → markers/all.js} +0 -0
@@ -4,31 +4,11 @@
4
4
  import { resolveAccessor } from '../utils/accessors';
5
5
  import { getConfiguration } from '../../configuration/config.svelte';
6
6
  import { paletteColor } from '../../utils/color';
7
- import { declutter1D, mirrorRanks } from '../utils/declutter';
7
+ import { declutter1D, laneAssignment } from '../utils/declutter';
8
8
  import type { Series } from '../../types/plots/data/common';
9
- import type { ValuesStyle } from '../../types/plots/styles/common';
9
+ import type { ValuesStyle } from '../../types/layout/styles';
10
10
  import type { ValueAnchor } from '../../types/plots/constants';
11
11
 
12
- // Elbow-routed leader line geometry (pixels) — a run's top/bottom pair
13
- // share the shortest lane, its next-in pair the next lane out, and so on
14
- // (see `mirrorRanks`'s doc comment for why sharing a lane is safe). Each
15
- // lane is `LANE_STEP` further out than the last; `ELBOW_GAP` is the
16
- // shortest lane (rank 0, right off the data point); `LABEL_GAP` is the
17
- // small clearance between the outermost lane and the label text it feeds
18
- // into.
19
- const ELBOW_GAP = 10;
20
- const LANE_STEP = 7;
21
- const LABEL_GAP = 6;
22
-
23
- // Default connector look — deliberately *not* each series' own colour or
24
- // stroke weight: a leader line only needs to be traceable back to its
25
- // label, and matching the data line's own styling reads as if it were
26
- // more data, which is confusing right where several lines already
27
- // converge. Neutral, thin, and solid keeps it legible as a leader — the
28
- // colour and weight are still fully overridable via `values.strokeStyle`.
29
- const DEFAULT_CONNECTOR_COLOR = '#94a3b8';
30
- const DEFAULT_CONNECTOR_WIDTH = 0.5;
31
-
32
12
  /**
33
13
  * End-point value labels for `LinePlot` — one label per series at its last
34
14
  * data point, with automatic collision resolution whenever several series
@@ -40,12 +20,33 @@
40
20
  let {
41
21
  lastPoints,
42
22
  values,
23
+ onOverflow,
43
24
  }: {
44
25
  lastPoints: { series: Series<TData>; lastRow: TData; seriesIndex: number }[];
45
26
  values: ValuesStyle | undefined;
27
+ /** Reports how far the rendered labels overflow the plot's own content box, in pixels, on each side (0 = no overflow). */
28
+ onOverflow?: (overflow: { left: number; right: number }) => void;
46
29
  } = $props();
47
30
 
48
31
  const cfg = $derived(getConfiguration());
32
+
33
+ // Elbow-routed leader line geometry (pixels), configurable project-wide
34
+ // via `setConfiguration({ line: { valueLabels: {...} } })` — each lane a
35
+ // run's members get spread across (see `laneAssignment`'s doc comment for
36
+ // how lanes are picked without crossing) is `laneStep` further out than
37
+ // the last; `elbowGap` is the shortest lane (rank 0, right off the data
38
+ // point); `labelGap` is the small clearance between the outermost lane and
39
+ // the label text it feeds into. `connectorColor`/`connectorWidth` are the
40
+ // leader line's own default look — deliberately *not* each series' own
41
+ // colour or stroke weight (matching the data line reads as more data, not
42
+ // as a leader, right where several lines are already converging) — both
43
+ // still overridable per-plot via `values.strokeStyle`.
44
+ const elbowGap = $derived(cfg.line.valueLabels.elbowGap);
45
+ const laneStep = $derived(cfg.line.valueLabels.laneStep);
46
+ const labelGap = $derived(cfg.line.valueLabels.labelGap);
47
+ const defaultConnectorColor = $derived(cfg.line.valueLabels.connectorColor);
48
+ const defaultConnectorWidth = $derived(cfg.line.valueLabels.connectorWidth);
49
+
49
50
  const font = $derived(values?.fontStyle);
50
51
  const connector = $derived(values?.strokeStyle);
51
52
  const fmtVal = $derived(values?.format ?? ((v: number, name: string) => `${name} - ${v}`));
@@ -61,9 +62,45 @@
61
62
  valAnchor === 'start' ? ('start' as const) : valAnchor === 'end' ? ('end' as const) : ('middle' as const),
62
63
  );
63
64
  const defaultLA = $derived(valAnchor === 'outside' ? ('bottom' as const) : ('middle' as const));
65
+ const fontSize = $derived(typeof font?.fontSize === 'number' ? font.fontSize : 12);
64
66
 
65
67
  const plot = usePlot();
66
68
 
69
+ // ── Overflow measurement ─────────────────────────────────────────────────
70
+ // Measures how far the rendered labels extend past the plot's own content
71
+ // box and reports it to the caller (`Plot.svelte`), which grows its own
72
+ // local margin state to fit — real `getBBox()` measurement, same as the
73
+ // approach this used to register directly into svelteplot's *shared*
74
+ // margin-auto-sizing context. Reporting through a plain callback instead
75
+ // keeps this entirely local to one `<Plot>` instance: nothing here is
76
+ // shared across instances, which is what made the old approach spike
77
+ // CPU/memory into unresponsiveness when many instances mounted/unmounted
78
+ // together (e.g. a multi-story docs page → single story).
79
+ let groupEl: SVGGElement | undefined = $state();
80
+
81
+ $effect(() => {
82
+ // getBBox() below is a plain DOM read, not itself tracked — depend on
83
+ // whatever actually changes the rendered label layout so this re-runs.
84
+ void lastPoints;
85
+ void values;
86
+ const el = groupEl;
87
+ if (!el || !onOverflow) return;
88
+ const xRange = plot.scales.x?.fn?.range?.()?.map(Number);
89
+ if (!xRange) return;
90
+ const plotLeft = Math.min(xRange[0], xRange[1]);
91
+ const plotRight = Math.max(xRange[0], xRange[1]);
92
+ let box: { x: number; width: number };
93
+ try {
94
+ box = el.getBBox();
95
+ } catch {
96
+ return;
97
+ }
98
+ onOverflow({
99
+ left: Math.ceil(Math.max(0, plotLeft - box.x)),
100
+ right: Math.ceil(Math.max(0, box.x + box.width - plotRight)),
101
+ });
102
+ });
103
+
67
104
  /**
68
105
  * Pixel-space collision fix-up for end-point labels: resolves each label's
69
106
  * true pixel Y through the plot's own live y-scale, then runs the shared
@@ -74,45 +111,49 @@
74
111
  * which would extend the y-scale's own domain and provoke an infinite
75
112
  * autoscale ↔ declutter feedback loop.
76
113
  *
77
- * `inGroup`/`rank`/`depth` come from `mirrorRanks`, fed by `declutter1D`'s
78
- * own `connected` record rather than re-inferred from positions — see
79
- * both functions' doc comments for why that's the only reliable source:
80
- * position-based heuristics either miss a series that only had to move
81
- * because a neighbour got pushed into it, or wrongly bridge two unrelated
82
- * clusters that each happened to need decluttering on their own. The
83
- * elbow connector below turns `rank` into a lane index — mirrored
84
- * top/bottom pairs share a lane, so a run only ever needs `ceil(depth / 2)`
85
- * distinct ones. A series with nothing nearby is absent from the map, and
86
- * its label renders with no extra offset.
114
+ * `inGroup`/`rank`/`lanesUsed` come from `laneAssignment`, fed by
115
+ * `declutter1D`'s own `connected` record rather than re-inferred from
116
+ * positions — see both functions' doc comments for why that's the only
117
+ * reliable source: position-based heuristics either miss a series that
118
+ * only had to move because a neighbour got pushed into it, or wrongly
119
+ * bridge two unrelated clusters that each happened to need decluttering
120
+ * on their own. The elbow connector below turns `rank` into a lane index.
121
+ * A series with nothing nearby is absent from the map, and its label
122
+ * renders with no extra offset.
87
123
  */
88
124
  const declutteredOffsets = $derived.by(() => {
89
- const map = new SvelteMap<string, { extraDy: number; inGroup: boolean; rank: number; depth: number }>();
125
+ const map = new SvelteMap<string, { extraDy: number; inGroup: boolean; rank: number; lanesUsed: number }>();
90
126
  if (lastPoints.length < 2) return map;
91
127
  const yScale = plot.scales.y?.fn;
92
128
  if (!yScale) return map;
93
- const fontSize = font?.fontSize ?? 12;
94
129
  const minGap = Math.max(16, fontSize * 1.4);
95
130
  const items = lastPoints.map(({ series: s, lastRow }) => ({
96
131
  name: s.name,
97
132
  pos: Number(yScale(Number(resolveAccessor(s.y)(lastRow)))),
98
133
  }));
99
134
  const sorted = [...items].sort((a, b) => a.pos - b.pos);
100
- const { positions: spread, connected } = declutter1D(items, minGap);
135
+ const range = yScale.range?.().map(Number);
136
+ const bounds: [number, number] | undefined = range
137
+ ? [Math.min(range[0], range[1]), Math.max(range[0], range[1])]
138
+ : undefined;
139
+ const { positions: spread, connected } = declutter1D(items, minGap, bounds);
101
140
  const spreadByName = new Map(spread.map((d) => [d.name, d.pos]));
102
- const ranks = mirrorRanks(connected);
141
+ const originals = sorted.map((item) => item.pos);
142
+ const finals = sorted.map((item) => spreadByName.get(item.name) ?? item.pos);
143
+ const ranks = laneAssignment(originals, finals, connected);
103
144
  sorted.forEach((item, i) => {
104
- const final = spreadByName.get(item.name) ?? item.pos;
105
145
  map.set(item.name, {
106
- extraDy: final - item.pos,
107
- inGroup: ranks[i].depth > 1,
146
+ extraDy: finals[i] - originals[i],
147
+ inGroup: ranks[i].runSize > 1,
108
148
  rank: ranks[i].rank,
109
- depth: ranks[i].depth,
149
+ lanesUsed: ranks[i].lanesUsed,
110
150
  });
111
151
  });
112
152
  return map;
113
153
  });
114
154
  </script>
115
155
 
156
+ <g bind:this={groupEl}>
116
157
  {#each lastPoints as { series: s, lastRow, seriesIndex } (s.name)}
117
158
  {@const xFn = resolveAccessor(s.x)}
118
159
  {@const yFn = resolveAccessor(s.y)}
@@ -121,20 +162,23 @@
121
162
  {@const extraDy = offset?.inGroup ? offset.extraDy : 0}
122
163
  {@const baseDx = font?.dx ?? defaultDx}
123
164
  {@const dir = Math.sign(baseDx) || 1}
124
- {@const numLanes = offset?.inGroup ? Math.ceil(offset.depth / 2) : 0}
165
+ {@const numLanes = offset?.inGroup ? offset.lanesUsed : 0}
125
166
  {@const labelDx = offset?.inGroup
126
- ? dir * (ELBOW_GAP + (numLanes - 1) * LANE_STEP + LABEL_GAP)
167
+ ? dir * (elbowGap + (numLanes - 1) * laneStep + labelGap)
127
168
  : baseDx}
169
+ {@const text = fmtVal(Number(yFn(lastRow)), s.name)}
170
+ {@const preferredTA = resolvedTextAnchor ?? defaultTA}
171
+ {@const preferredLA = font?.lineAnchor ?? defaultLA}
128
172
  {#if offset?.inGroup && plot.scales.x?.fn && plot.scales.y?.fn}
129
173
  {@const px = Number(plot.scales.x.fn(xFn(lastRow)))}
130
174
  {@const py = Number(plot.scales.y.fn(Number(yFn(lastRow))))}
131
- {@const laneX = px + dir * (ELBOW_GAP + offset.rank * LANE_STEP)}
175
+ {@const laneX = px + dir * (elbowGap + offset.rank * laneStep)}
132
176
  {@const targetY = py + extraDy}
133
177
  <path
134
178
  d={`M ${px} ${py} H ${laneX} V ${targetY} H ${px + labelDx}`}
135
179
  fill="none"
136
- stroke={connector?.stroke ?? DEFAULT_CONNECTOR_COLOR}
137
- stroke-width={connector?.strokeWidth ?? DEFAULT_CONNECTOR_WIDTH}
180
+ stroke={connector?.stroke ?? defaultConnectorColor}
181
+ stroke-width={connector?.strokeWidth ?? defaultConnectorWidth}
138
182
  stroke-opacity={connector?.strokeOpacity ?? 1}
139
183
  stroke-dasharray={connector?.strokeDasharray}
140
184
  stroke-linecap={connector?.strokeLinecap ?? 'square'}
@@ -145,17 +189,17 @@
145
189
  data={[lastRow]}
146
190
  x={xFn}
147
191
  y={yFn}
148
- text={() => fmtVal(Number(yFn(lastRow)), s.name)}
192
+ text={() => text}
149
193
  dx={labelDx}
150
194
  dy={(font?.dy ?? defaultDy) + extraDy}
151
- textAnchor={resolvedTextAnchor ?? defaultTA}
152
- lineAnchor={font?.lineAnchor ?? defaultLA}
195
+ textAnchor={preferredTA}
196
+ lineAnchor={preferredLA}
153
197
  lineHeight={font?.lineHeight}
154
198
  rotate={font?.rotate}
155
199
  class={font?.class}
156
200
  textClass={font?.textClass}
157
201
  fill={font?.fill ?? seriesColor}
158
- fontSize={font?.fontSize ?? 12}
202
+ {fontSize}
159
203
  fontWeight={font?.fontWeight}
160
204
  fontStyle={font?.fontStyle}
161
205
  stroke={font?.stroke}
@@ -163,3 +207,4 @@
163
207
  paintOrder={font?.paintOrder}
164
208
  />
165
209
  {/each}
210
+ </g>
@@ -1,5 +1,5 @@
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
3
  declare function $$render<TData extends Record<string, unknown>>(): {
4
4
  props: {
5
5
  lastPoints: {
@@ -8,6 +8,11 @@ declare function $$render<TData extends Record<string, unknown>>(): {
8
8
  seriesIndex: number;
9
9
  }[];
10
10
  values: ValuesStyle | undefined;
11
+ /** Reports how far the rendered labels overflow the plot's own content box, in pixels, on each side (0 = no overflow). */
12
+ onOverflow?: (overflow: {
13
+ left: number;
14
+ right: number;
15
+ }) => void;
11
16
  };
12
17
  exports: {};
13
18
  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,22 @@
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 { buildGroupedSeries, validateSegments, buildScopedMarkerGroups } from '../utils/segments';
11
10
  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
11
  import ValueLabels from './ValueLabels.svelte';
17
12
  import { hasHoverMarker, resolveHoverStrategy, resolveHoverSync } from '../../layout/tooltip/utils';
18
13
  import type { BasePlotProps } from '../../types/plots/props';
19
- import type { AxisValue } from '../../types/plots/axis';
14
+ import type { AxisValue, AxisBasedScalesConfig } from '../../types/layout/scales';
20
15
  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';
16
+ import type { BasePlotStylesConfig } from '../../types/layout/styles';
23
17
  import type { ValueAnchor } from '../../types/plots/constants';
24
18
  import type { BarSegmentStyle } from '../../types/plots/segments/common';
25
- import type { Marker } from '../../types/markers/common';
19
+ import type { AxisBasedMarkersConfig } from '../../types/markers/common';
26
20
 
27
21
  /**
28
22
  * A population-pyramid-style diverging bar chart: each series is mirrored
@@ -43,9 +37,9 @@
43
37
  type Props = BasePlotProps<
44
38
  SeriesProps<TData> | DataProps<TData>,
45
39
  TData,
46
- BasePlotStyles,
40
+ BasePlotStylesConfig,
47
41
  BarSegmentStyle,
48
- Marker,
42
+ AxisBasedMarkersConfig,
49
43
  AxisBasedScalesConfig<TData>
50
44
  >;
51
45
 
@@ -166,17 +160,6 @@
166
160
  return vals.length ? Math.max(...vals) : 1;
167
161
  });
168
162
 
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
163
  // The single, fully-resolved scales object BasePlotLayout reads for
181
164
  // everything (ticks, grid, sort, and the real svelteplot scale) — avoids
182
165
  // keeping a second, separately-defaulted copy of `scales.x`/`scales.y`
@@ -192,111 +175,52 @@
192
175
  y: { type: 'band', padding: 0.2, domain: yDomain, ...(scales.y ?? {}) },
193
176
  });
194
177
 
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));
178
+ const hover = $derived({
179
+ active: tooltip != null || hasHoverMarker(markers),
180
+ points: hoverPoints,
181
+ label: labelFor,
182
+ strategy: resolveHoverStrategy(tooltip?.strategy, markers, 'y'),
183
+ sync: resolveHoverSync(tooltip?.sync, markers),
184
+ tooltip,
185
+ });
186
+
187
+ const pyramidMarkers = $derived(
188
+ buildPyramidBarMarkers({
189
+ groupedSeries,
190
+ valueFor: signedValue,
191
+ colorFor: (s) => (s.side === 'left' ? leftColor : rightColor),
192
+ disabledFillFor: (name) => disabledFillOverride(legendInteraction?.disabledSeries.get(name)),
193
+ }),
194
+ );
195
+
196
+ const scopedGroups = $derived(
197
+ buildScopedMarkerGroups<TData, InternalSeries, BarSegmentStyle>({
198
+ groupedSeries,
199
+ segments,
200
+ colorFor: (series) => (series.side === 'left' ? leftColor : rightColor),
201
+ segmentColorFor: (style, defaultColor) => style?.fill ?? defaultColor,
202
+ }),
203
+ );
198
204
  </script>
199
205
 
200
206
  <BasePlotLayout
201
207
  {width}
202
208
  {height}
209
+ {styles}
203
210
  data={flat}
204
211
  {getX}
205
212
  {getY}
206
- {hoverPoints}
207
- label={labelFor}
208
- {tooltip}
209
- {hoverActive}
210
- {hoverStrategy}
211
- {hoverSync}
213
+ {hover}
212
214
  scales={resolvedScales}
213
215
  {margins}
214
216
  xTickRotate={resolvedScales?.x?.tickRotate}
217
+ markers={[...pyramidMarkers, ...markers]}
218
+ seriesData={resolvedSeries}
219
+ {scopedGroups}
215
220
  >
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}>
221
+ {#if showVals}
220
222
  {#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}
223
+ <ValueLabels {group} {valAnchor} {fmtVal} values={styles.values} axisColor={cfg.axis.color} />
299
224
  {/each}
300
- </RuleLayout>
301
- {/snippet}
225
+ {/if}
302
226
  </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: {};
@@ -2,7 +2,7 @@
2
2
  import { Text } from 'svelteplot';
3
3
  import { resolveAccessor } from '../utils/accessors';
4
4
  import type { Series } from '../../types/plots/data/common';
5
- import type { ValuesStyle } from '../../types/plots/styles/common';
5
+ import type { ValuesStyle } from '../../types/layout/styles';
6
6
  import type { ValueAnchor } from '../../types/plots/constants';
7
7
  import type { BarSegmentStyle } from '../../types/plots/segments/common';
8
8
  import type { VisualGroup } from '../../types/plots/segments/config';
@@ -1,5 +1,5 @@
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
3
  import type { ValueAnchor } from '../../types/plots/constants';
4
4
  import type { BarSegmentStyle } from '../../types/plots/segments/common';
5
5
  import type { VisualGroup } from '../../types/plots/segments/config';
@@ -0,0 +1,19 @@
1
+ import type { Series } from '../../types/plots/data/common';
2
+ import type { BarSegmentStyle } from '../../types/plots/segments/common';
3
+ import type { VisualGroup } from '../../types/plots/segments/config';
4
+ import type { BarMarkerConfig } from '../../types/markers/common';
5
+ /**
6
+ * Every `'bar'` marker for one PyramidPlot render — reuses `BarMarkerConfig`
7
+ * directly, always horizontal (category on y, signed magnitude on x) and
8
+ * with no insets. Emits no `role: 'hitArea'` markers, since PyramidPlot's
9
+ * hover matches nearest points rather than hit-testing category bands.
10
+ */
11
+ export declare function buildPyramidBarMarkers<TData extends Record<string, unknown>, S extends Series<TData>>(args: {
12
+ groupedSeries: VisualGroup<S, BarSegmentStyle>[];
13
+ valueFor: (series: S, d: TData) => number;
14
+ colorFor: (series: S) => string;
15
+ disabledFillFor: (seriesName: string) => {
16
+ fill?: string;
17
+ fillOpacity?: number;
18
+ } | undefined;
19
+ }): BarMarkerConfig[];
@@ -0,0 +1,32 @@
1
+ import { resolveAccessor } from '../utils/accessors';
2
+ /**
3
+ * Every `'bar'` marker for one PyramidPlot render — reuses `BarMarkerConfig`
4
+ * directly, always horizontal (category on y, signed magnitude on x) and
5
+ * with no insets. Emits no `role: 'hitArea'` markers, since PyramidPlot's
6
+ * hover matches nearest points rather than hit-testing category bands.
7
+ */
8
+ export function buildPyramidBarMarkers(args) {
9
+ const { groupedSeries, valueFor, colorFor, disabledFillFor } = args;
10
+ const markers = [];
11
+ for (const group of groupedSeries) {
12
+ const getCategory = resolveAccessor(group.series.x);
13
+ const defaultColor = colorFor(group.series);
14
+ const disabledFill = disabledFillFor(group.series.name);
15
+ for (const seg of group.visualSegments) {
16
+ markers.push({
17
+ type: 'bar',
18
+ role: 'segment',
19
+ isHorizontal: true,
20
+ data: seg.data,
21
+ y: getCategory,
22
+ x1: 0,
23
+ x2: (d) => valueFor(group.series, d),
24
+ style: {
25
+ fill: disabledFill?.fill ?? seg.style.fill ?? defaultColor,
26
+ fillOpacity: disabledFill?.fillOpacity ?? seg.style.fillOpacity ?? 1,
27
+ },
28
+ });
29
+ }
30
+ }
31
+ return markers;
32
+ }