@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
@@ -15,11 +15,34 @@
15
15
  * already >= minGap and the average of two such gaps is too.
16
16
  *
17
17
  * Also records, per adjacent pair, whether either pass actually had to push
18
- * — the ground truth for "did these two really interact," which `mirrorRanks`
19
- * needs and can't reliably reconstruct from positions alone after the fact
20
- * (see its own doc comment).
18
+ * — the ground truth for "did these two really interact," which
19
+ * `laneAssignment` needs and can't reliably reconstruct from positions alone
20
+ * after the fact (see its own doc comment).
21
+ *
22
+ * `bounds`, if given, keeps every *run* (a maximal stretch of connected
23
+ * items — lone, unconnected items are left at their true position
24
+ * regardless) inside `[lo, hi]`: nothing pushed apart by this function is
25
+ * ever allowed to end up off the edge of whatever pixel range the caller
26
+ * considers in-bounds (typically the plot's own content box). A run that
27
+ * already fits is only shifted, never resized; a run too big to fit even
28
+ * at its natural spacing is shrunk just enough to fit, uniformly, so it's
29
+ * still evenly spaced — just tighter than `minGap` calls for. See
30
+ * `fitRunsWithinBounds` for the actual shift/shrink math.
31
+ *
32
+ * `bounds` also biases *which* pass a run's average leans on, via
33
+ * {@link runBiasWeights}: a run sitting close to one edge has little slack on
34
+ * that side, so a plain 50/50 blend would still ask the up-pass and
35
+ * down-pass to pull the block symmetrically toward *both* edges before
36
+ * `fitRunsWithinBounds` shifts/shrinks it back — the up-pass reaching for
37
+ * room that was never really there, needlessly widening how far members end
38
+ * up travelling from their original positions (`laneAssignment`'s own lane
39
+ * count grows with exactly that travel distance — see its doc comment) — so
40
+ * a run near an edge leans toward whichever pass already grows away from it
41
+ * instead — the block still expands to clear `minGap` everywhere, just
42
+ * mostly in the one direction that was actually available, the way a person
43
+ * sliding books apart on a shelf pinned against one wall would.
21
44
  */
22
- export function declutter1D(items, minGap) {
45
+ export function declutter1D(items, minGap, bounds) {
23
46
  const sorted = [...items].sort((a, b) => a.pos - b.pos);
24
47
  const n = sorted.length;
25
48
  const connected = new Array(Math.max(0, n - 1)).fill(false);
@@ -39,55 +62,174 @@ export function declutter1D(items, minGap) {
39
62
  connected[i] = true;
40
63
  }
41
64
  }
42
- const positions = sorted.map((d, i) => ({ ...d, pos: (down[i] + up[i]) / 2 }));
65
+ const weights = bounds ? runBiasWeights(down, up, connected, bounds) : sorted.map(() => 0.5);
66
+ const averaged = sorted.map((_, i) => down[i] * weights[i] + up[i] * (1 - weights[i]));
67
+ const fitted = bounds ? fitRunsWithinBounds(averaged, connected, bounds) : averaged;
68
+ const positions = sorted.map((d, i) => ({ ...d, pos: fitted[i] }));
43
69
  return { positions, connected };
44
70
  }
71
+ /**
72
+ * Per-run blend weight (applied to the down-pass; `1 - weight` goes to the
73
+ * up-pass) favouring whichever pass already grows away from the nearer edge
74
+ * — see {@link declutter1D}'s doc comment for why. `0.5` (the down/up
75
+ * midpoint, i.e. no bias) when a run's naturally-centred span has roughly
76
+ * equal room on both sides; sliding toward `1` (all down-pass) as room above
77
+ * vanishes, toward `0` (all up-pass) as room below vanishes. Every member of
78
+ * a run gets the *same* weight — the point is the block moving as one
79
+ * (mostly) one-directional piece, not each member picking its own bias.
80
+ */
81
+ function runBiasWeights(down, up, connected, [lo, hi]) {
82
+ const weights = down.map(() => 0.5);
83
+ let i = 0;
84
+ while (i < down.length) {
85
+ let j = i;
86
+ while (j < connected.length && connected[j])
87
+ j++;
88
+ if (j > i) {
89
+ const center = ((down[i] + up[i]) / 2 + (down[j] + up[j]) / 2) / 2;
90
+ const roomAbove = Math.max(0, center - lo);
91
+ const roomBelow = Math.max(0, hi - center);
92
+ const total = roomAbove + roomBelow;
93
+ const weight = total > 0 ? roomBelow / total : 0.5;
94
+ for (let k = i; k <= j; k++)
95
+ weights[k] = weight;
96
+ }
97
+ i = j + 1;
98
+ }
99
+ return weights;
100
+ }
101
+ /**
102
+ * Shifts, and shrinks only if it must, each run of `connected` positions so
103
+ * it lands entirely inside `[lo, hi]`. A run's own relative spacing is
104
+ * preserved as-is (just translated) whenever it already fits in `hi - lo`;
105
+ * only a run wider than the available space gets uniformly scaled down
106
+ * around its centre first. Lone, unconnected positions pass through
107
+ * untouched — they were never pushed by `declutter1D` in the first place,
108
+ * so clamping one to the bounds would move a label away from the data point
109
+ * it's honestly reporting, for no collision-avoidance reason at all.
110
+ */
111
+ function fitRunsWithinBounds(positions, connected, [lo, hi]) {
112
+ const out = [...positions];
113
+ const available = hi - lo;
114
+ let i = 0;
115
+ while (i < out.length) {
116
+ let j = i;
117
+ while (j < connected.length && connected[j])
118
+ j++;
119
+ if (j > i) {
120
+ const runMin = out[i];
121
+ const runMax = out[j];
122
+ if (runMin < lo || runMax > hi) {
123
+ const naturalSpan = runMax - runMin;
124
+ const scale = naturalSpan > 0 ? Math.min(1, available / naturalSpan) : 1;
125
+ const idealCenter = (runMin + runMax) / 2;
126
+ const newSpan = naturalSpan * scale;
127
+ const newCenter = Math.min(Math.max(idealCenter, lo + newSpan / 2), hi - newSpan / 2);
128
+ for (let k = i; k <= j; k++) {
129
+ out[k] = newCenter + (out[k] - idealCenter) * scale;
130
+ }
131
+ }
132
+ }
133
+ i = j + 1;
134
+ }
135
+ return out;
136
+ }
45
137
  /**
46
138
  * Groups `declutter1D`'s own `connected` flags into runs (maximal stretches
47
- * of consecutive connected pairs), then assigns each run member a lane by
48
- * *mirrored* position within it: the run's top and bottom member share lane
49
- * 0, the next-in-from-top and next-in-from-bottom share lane 1, and so on
50
- * toward the run's centre (an odd-sized run's exact middle member gets the
51
- * innermost lane alone). `depth` is the run's own size, the same for every
52
- * member; the number of *distinct* lanes it actually needs is
53
- * `ceil(depth / 2)`.
139
+ * of consecutive connected pairs), then gives every run member its own,
140
+ * unique lane no two members ever share one, deliberately (see below) —
141
+ * ordered so that a leader line drawn through lane `rank` never crosses, or
142
+ * even draws visually flush against, one drawn through any other lane of
143
+ * the same run.
144
+ *
145
+ * Every member's leader line is the same three-segment elbow: a horizontal
146
+ * stub from the data point out to its lane, a vertical run up/down that
147
+ * lane to the label's height, then a final horizontal run in to the shared
148
+ * label column. Two such elbows, `i` (in the tighter of the two lanes) and
149
+ * `j` (further out), are safe from crossing *specifically* when neither of
150
+ * these can happen:
151
+ * - `j`'s own stub (at `j`'s original height) reaches out past `i`'s lane
152
+ * while `i`'s vertical run passes through that height — `j`'s stub would
153
+ * cut across `i`'s vertical segment.
154
+ * - `i`'s final run (at `i`'s label height) reaches past `i`'s own lane out
155
+ * to the shared column, and in doing so crosses `j`'s lane while `j`'s
156
+ * vertical run passes through *that* height — `i`'s own final segment
157
+ * would cut across `j`'s vertical segment.
54
158
  *
55
- * Deliberately doesn't re-derive connectivity from before/after positions
56
- * an early version tried that (some mix of "both moved" and "final gap is
57
- * near minGap") and it's unreliable both ways: a real one-sided interaction
58
- * (an item that only moved because its neighbour got pushed into it) can
59
- * settle with more slack than minGap, so a gap-threshold check misses it;
60
- * and "both moved" over-connects, since two unrelated, far-apart clusters
61
- * that each independently needed to declutter still each have moved members
62
- * sitting next to each other in sort order, which a moved-only check can't
63
- * tell apart from a real interaction. `declutter1D`'s own `connected` array
64
- * has neither problem, because it's not inferred it's the actual record
65
- * of which pushes happened.
159
+ * Whether `i` can safely take the tighter lane depends on both members'
160
+ * *actual* pixel geometry not simply on which one moved further so each
161
+ * pair is checked directly per {@link saferNearer} rather than inferred from
162
+ * a single sort key. A pair with a clean, provable answer either way (the
163
+ * overwhelmingly common case for `declutter1D`'s own output) is ordered by
164
+ * that answer; a pair with no clean answer (both directions equally
165
+ * "unsafe," or, symmetrically, both fine) falls back to the same *travel
166
+ * distance* heuristic used before this per-pair check existed furthest
167
+ * `|final - original|` first which is what decides ties and keeps the
168
+ * "furthest traveller gets the tightest lane, so it starts bending
169
+ * immediately rather than reaching across everyone else's leader lines
170
+ * first" intent for every pair the geometry itself doesn't already settle.
66
171
  *
67
- * Meant to drive a leader line's lane: `rank` 0 bends right off the data
68
- * point (a short first stub); higher ranks travel further before bending
69
- * (right up to the shared label column, for a run's own centre member).
70
- * Two members sharing a lane never collide: by how `declutter1D` centers
71
- * its result, a run's top half only ever ends up at or above its original
72
- * spot and its bottom half only ever at or below, so paired members bend
73
- * away from each other same lane, opposite direction, no shared space.
74
- * This also roughly halves the lane depth (and so the horizontal room) a
75
- * run needs, versus giving every member its own lane.
172
+ * No sharing, ever, is the other half of this: two members can only have
173
+ * non-overlapping travel spans (and so pass the old, now-removed,
174
+ * share-a-lane check) by sitting far apart along the axis but "far apart"
175
+ * is exactly the case with the most room to spare, so it never needed the
176
+ * lane it would save by sharing, while any pair close enough to actually be
177
+ * short on room has spans close enough together that sharing draws their
178
+ * two leader lines only a couple pixels apart visually indistinguishable
179
+ * from one continuous line touching the next label down. A run of `n`
180
+ * members uses exactly `n` lanes; letting the lane count grow is the price
181
+ * of a fan that never has two leader lines touching *or* crossing, and
182
+ * `ValueLabels`' own auto-margin measurement (see its doc comment) already
183
+ * grows `margins.right` to fit however many lanes that turns out to be, so
184
+ * nothing has to be guessed up front.
185
+ *
186
+ * `rank` is the lane index (0-indexed) to drive a leader line's routing:
187
+ * `rank` 0 bends right off the data point (a short first stub); higher
188
+ * ranks travel further before bending (right up to the shared label
189
+ * column). `lanesUsed` is the run's own total lane count — always equal to
190
+ * `runSize` — shared by every member; `runSize` is the run's member count,
191
+ * for callers that just need to know whether a given member belongs to a
192
+ * real run at all.
76
193
  */
77
- export function mirrorRanks(connected) {
194
+ export function laneAssignment(originals, finals, connected) {
78
195
  const n = connected.length + 1;
79
- const out = Array.from({ length: n }, () => ({ rank: 0, depth: 1 }));
196
+ const out = Array.from({ length: n }, () => ({ rank: 0, lanesUsed: 1, runSize: 1 }));
197
+ const span = (k) => {
198
+ const a = originals[k];
199
+ const b = finals[k];
200
+ return a < b ? [a, b] : [b, a];
201
+ };
202
+ const distance = (k) => Math.abs(finals[k] - originals[k]);
203
+ /** Whether lane-`i`-nearer-than-`j` is provably crossing-free — see this function's doc comment. */
204
+ function saferNearer(i, j) {
205
+ const [loI, hiI] = span(i);
206
+ const [loJ, hiJ] = span(j);
207
+ const jStubCutsI = originals[j] >= loI && originals[j] <= hiI;
208
+ const iFinalCutsJ = finals[i] >= loJ && finals[i] <= hiJ;
209
+ return !jStubCutsI && !iFinalCutsJ;
210
+ }
80
211
  let i = 0;
81
212
  while (i < n) {
82
213
  let j = i;
83
214
  while (j < connected.length && connected[j])
84
215
  j++;
85
- const runLength = j - i + 1;
86
- if (runLength > 1) {
87
- for (let k = i; k <= j; k++) {
88
- const posInRun = k - i;
89
- out[k] = { rank: Math.min(posInRun, runLength - 1 - posInRun), depth: runLength };
90
- }
216
+ const runSize = j - i + 1;
217
+ if (runSize > 1) {
218
+ // Members of this run, ordered so each is provably safe to place
219
+ // nearer than the next whenever that's decidable, furthest-travelled
220
+ // first otherwise (ties keep their original i.e. top-to-bottom
221
+ // order, since `sort` is stable). Position in this order *is* the
222
+ // rank: no sharing.
223
+ const order = Array.from({ length: runSize }, (_, k) => i + k).sort((a, b) => {
224
+ const aNearer = saferNearer(a, b);
225
+ const bNearer = saferNearer(b, a);
226
+ if (aNearer !== bNearer)
227
+ return aNearer ? -1 : 1;
228
+ return distance(b) - distance(a);
229
+ });
230
+ order.forEach((k, rank) => {
231
+ out[k] = { rank, lanesUsed: runSize, runSize };
232
+ });
91
233
  }
92
234
  i = j + 1;
93
235
  }
@@ -36,7 +36,7 @@ export declare function geometryPartCentroids(geometry: GeoJSON.Geometry): [numb
36
36
  * based on a per-part predicate, reassembling a geometry of the same type
37
37
  * from whatever survives — `null` if nothing does. Used to let a single
38
38
  * feature's geometry be split between the main map and an `inset` marker
39
- * (see `GeoDataProps.excludeParts`/`GeoInsetMarker.featurePartsFilter`)
39
+ * (see `GeoDataProps.excludeParts`/`GeoInsetMarkerConfig.featurePartsFilter`)
40
40
  * without preprocessing the source data: e.g. a province whose `MultiPolygon`
41
41
  * spans both a mainland part and a far-flung part can have the main map
42
42
  * exclude the latter while an inset shows only it.
@@ -102,7 +102,7 @@ export function geometryPartCentroids(geometry) {
102
102
  * based on a per-part predicate, reassembling a geometry of the same type
103
103
  * from whatever survives — `null` if nothing does. Used to let a single
104
104
  * feature's geometry be split between the main map and an `inset` marker
105
- * (see `GeoDataProps.excludeParts`/`GeoInsetMarker.featurePartsFilter`)
105
+ * (see `GeoDataProps.excludeParts`/`GeoInsetMarkerConfig.featurePartsFilter`)
106
106
  * without preprocessing the source data: e.g. a province whose `MultiPolygon`
107
107
  * spans both a mainland part and a far-flung part can have the main map
108
108
  * exclude the latter while an inset shows only it.
@@ -1,8 +1,9 @@
1
- import type { AxisValue } from '../../types/plots/axis';
1
+ import type { AxisValue } from '../../types/layout/scales';
2
2
  import type { Series } from '../../types/plots/data/common';
3
3
  import type { LineSegmentStyle } from '../../types/plots/segments/common';
4
4
  import type { Segment, VisualGroup } from '../../types/plots/segments/config';
5
5
  import type { SegmentsConfig } from '../../types/plots/props';
6
+ import type { ScopedMarkerGroup } from '../../types/layout/geometry';
6
7
  /**
7
8
  * Returns the flat {@link Segment} list for one series.
8
9
  * Series-specific segments are added before `'default'` segments so that
@@ -11,15 +12,6 @@ import type { SegmentsConfig } from '../../types/plots/props';
11
12
  export declare function resolveSegmentsForSeries<TStyle>(segments: SegmentsConfig<TStyle>, seriesName: string): Segment<TStyle>[];
12
13
  /** Returns true when `xVal` falls inside any of a segment's x ranges. */
13
14
  export declare function matchesSegmentX<TStyle>(xVal: AxisValue, seg: Segment<TStyle>): boolean;
14
- /**
15
- * For each segment in `segs`, returns the subset of `data` rows whose x value
16
- * (from `xAccessor`) falls inside any of the segment's x ranges. Rows are
17
- * not deduplicated across segments — a row that matches multiple segments
18
- * appears in each resulting array.
19
- *
20
- * Used by component and hover markers with the `'segment'` strategy.
21
- */
22
- export declare function getDataForSegments<TData, TStyle>(data: TData[], xAccessor: (d: TData) => AxisValue, segs: Segment<TStyle>[]): TData[][];
23
15
  /**
24
16
  * Warns about obvious configuration mistakes: multiple catch-all segments
25
17
  * (no `areas`) in the same series list. Since catch-alls are evaluated last
@@ -38,3 +30,18 @@ export declare function validateSegments<TStyle>(segments: SegmentsConfig<TStyle
38
30
  * aren't duplicated as an extra, separately-styled mark.
39
31
  */
40
32
  export declare function buildGroupedSeries<S extends Series<any>, TStyle = LineSegmentStyle>(seriesList: S[], segments: SegmentsConfig<TStyle>, connect?: boolean): VisualGroup<S, TStyle>[];
33
+ /**
34
+ * Builds the `ScopedMarkerGroup[]` a `Plot.svelte` hands to `GeometryLayout`
35
+ * for rendering `series`-scoped `hover`/`component` markers — one group per
36
+ * series, carrying its resolved segments' match predicates and colors.
37
+ * `colorFor` resolves a series' own default color; `segmentColorFor` reads
38
+ * whichever field counts as "color" out of that plot kind's segment style
39
+ * shape (e.g. `fill` vs `stroke.stroke`), since that's the only part of this
40
+ * that isn't shared across plot kinds.
41
+ */
42
+ export declare function buildScopedMarkerGroups<TData extends Record<string, unknown>, S extends Series<any>, TStyle>(args: {
43
+ groupedSeries: VisualGroup<S, TStyle>[];
44
+ segments: SegmentsConfig<TStyle>;
45
+ colorFor: (series: S, seriesIndex: number) => string;
46
+ segmentColorFor: (style: TStyle | undefined, defaultColor: string) => string;
47
+ }): ScopedMarkerGroup<TData>[];
@@ -35,22 +35,6 @@ function areaMatchesXY(xVal, yVal, area) {
35
35
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
36
36
  (area.y.to === undefined || yVal <= area.y.to);
37
37
  }
38
- /**
39
- * For each segment in `segs`, returns the subset of `data` rows whose x value
40
- * (from `xAccessor`) falls inside any of the segment's x ranges. Rows are
41
- * not deduplicated across segments — a row that matches multiple segments
42
- * appears in each resulting array.
43
- *
44
- * Used by component and hover markers with the `'segment'` strategy.
45
- */
46
- export function getDataForSegments(data, xAccessor, segs) {
47
- return segs.map((seg) => {
48
- const areas = seg.areas;
49
- if (!areas || areas.length === 0)
50
- return data;
51
- return data.filter((d) => areas.some((area) => areaMatchesX(xAccessor(d), area)));
52
- });
53
- }
54
38
  /**
55
39
  * Warns about obvious configuration mistakes: multiple catch-all segments
56
40
  * (no `areas`) in the same series list. Since catch-alls are evaluated last
@@ -209,3 +193,29 @@ export function buildGroupedSeries(seriesList, segments, connect = true) {
209
193
  visualSegments: resolveSeriesSegments(s, resolveSegmentsForSeries(segments, s.name), connect),
210
194
  }));
211
195
  }
196
+ /**
197
+ * Builds the `ScopedMarkerGroup[]` a `Plot.svelte` hands to `GeometryLayout`
198
+ * for rendering `series`-scoped `hover`/`component` markers — one group per
199
+ * series, carrying its resolved segments' match predicates and colors.
200
+ * `colorFor` resolves a series' own default color; `segmentColorFor` reads
201
+ * whichever field counts as "color" out of that plot kind's segment style
202
+ * shape (e.g. `fill` vs `stroke.stroke`), since that's the only part of this
203
+ * that isn't shared across plot kinds.
204
+ */
205
+ export function buildScopedMarkerGroups(args) {
206
+ const { groupedSeries, segments, colorFor, segmentColorFor } = args;
207
+ return groupedSeries.map((group, seriesIndex) => {
208
+ const defaultColor = colorFor(group.series, seriesIndex);
209
+ const xFor = resolveAccessor(group.series.x);
210
+ const segs = resolveSegmentsForSeries(segments, group.series.name);
211
+ return {
212
+ name: group.series.name,
213
+ data: group.series.data,
214
+ color: defaultColor,
215
+ segments: segs.map((seg) => ({
216
+ match: (row) => matchesSegmentX(xFor(row), seg),
217
+ color: segmentColorFor(seg.style, defaultColor),
218
+ })),
219
+ };
220
+ });
221
+ }
@@ -1,5 +1,5 @@
1
1
  import type { Snippet } from 'svelte';
2
- import type { AxisValue } from '../plots/axis';
2
+ import type { AxisValue } from '../layout/scales';
3
3
  import type { Accessor } from '../plots/data/common';
4
4
  import type { EasingName, EasingFn } from './interpolate';
5
5
  export type TimeValue = number;
@@ -1,10 +1,9 @@
1
1
  import type { Snippet } from 'svelte';
2
- import type { AxisValue } from '../plots/axis';
2
+ import type { AxisValue, AxisBasedScalesConfig } from '../layout/scales';
3
3
  import type { Accessor } from '../plots/data/common';
4
- import type { AxisBasedScalesConfig } from '../plots/props';
5
- import type { BasePlotStyles } from '../plots/styles/common';
4
+ import type { BasePlotStylesConfig } from '../layout/styles';
6
5
  import type { SegmentsConfig, MarginConfig } from '../plots/props';
7
- import type { Marker } from '../markers/common';
6
+ import type { AxisBasedMarkersConfig } from '../markers/common';
8
7
  import type { TooltipProp } from '../layout/tooltip';
9
8
  import type { LegendSection } from './legend';
10
9
  import type { FacetConfig, TimelineConfig } from './common';
@@ -30,9 +29,9 @@ export type ChartProps<TRow extends Record<string, unknown>, TSegmentStyle exten
30
29
  x: Accessor<TRow, AxisValue>;
31
30
  y: Accessor<TRow, AxisValue>;
32
31
  z?: Accessor<TRow, unknown>;
33
- styles?: BasePlotStyles;
32
+ styles?: BasePlotStylesConfig;
34
33
  segments?: SegmentsConfig<TSegmentStyle>;
35
- markers?: Marker[];
34
+ markers?: AxisBasedMarkersConfig[];
36
35
  scales?: AxisBasedScalesConfig<TRow>;
37
36
  margins?: MarginConfig;
38
37
  facet?: FacetConfig<TRow>;
@@ -1,4 +1,4 @@
1
- import type { StrokeStyle, DotStyle, FontStyle } from '../plots/styling';
1
+ import type { StrokeStyle, DotStyle, FontStyle, AreaStyle } from '../plots/styling';
2
2
  export type TextStyle = {
3
3
  /** font-family */
4
4
  font: string;
@@ -83,6 +83,27 @@ export type ChartConfig = {
83
83
  line: {
84
84
  strokeWidth: number;
85
85
  dotRadius: number;
86
+ /**
87
+ * End-of-line value-label declutter geometry and connector styling —
88
+ * `LinePlot`-specific, since only line charts spread colliding
89
+ * end-point labels apart with elbow-routed leader lines.
90
+ */
91
+ valueLabels: {
92
+ /** Shortest lane's distance (px) from the data point before a leader line bends. */
93
+ elbowGap: number;
94
+ /** Extra distance (px) each subsequent lane sits out from the last. */
95
+ laneStep: number;
96
+ /** Clearance (px) between the outermost lane and the label text it feeds into. */
97
+ labelGap: number;
98
+ /** Default leader-line colour — overridable per-plot via `styles.values.strokeStyle`. */
99
+ connectorColor: string;
100
+ /** Default leader-line width — overridable per-plot via `styles.values.strokeStyle`. */
101
+ connectorWidth: number;
102
+ };
103
+ };
104
+ /** Default scatter point geometry. */
105
+ scatter: {
106
+ dotRadius: number;
86
107
  };
87
108
  /** Timeline gutter styling. */
88
109
  timeline: {
@@ -90,11 +111,12 @@ export type ChartConfig = {
90
111
  activeColor: string;
91
112
  thickness: number;
92
113
  };
93
- /** In-SVG hover highlight (crosshair rule, dot, and value label). */
114
+ /** In-SVG hover highlight: crosshair rule, dot + value label, and geo feature restyle. */
94
115
  hover: {
95
116
  rule: StrokeStyle;
96
117
  dot: DotStyle;
97
118
  font: FontStyle;
119
+ area: AreaStyle;
98
120
  };
99
121
  };
100
122
  /** Deep-partial input accepted by `setConfiguration`. */
@@ -1,10 +1,10 @@
1
+ import type { GeoProjection } from 'd3-geo';
1
2
  /**
2
3
  * The projection names svelteplot's `<Plot projection>` accepts as a string
3
4
  * (svelteplot itself doesn't export this as a type — defined here so authors
4
5
  * get autocomplete/type-checking over the full supported set).
5
6
  */
6
7
  export type GeoProjectionName = 'albers-usa' | 'albers' | 'azimuthal-equal-area' | 'azimuthal-equidistant' | 'conic-conformal' | 'conic-equal-area' | 'conic-equidistant' | 'equal-earth' | 'equirectangular' | 'gnomonic' | 'identity' | 'reflect-y' | 'mercator' | 'orthographic' | 'stereographic' | 'transverse-mercator';
7
- import type { GeoProjection } from 'd3-geo';
8
8
  /**
9
9
  * What a custom projection factory (e.g. `argentinaBicontinental` under
10
10
  * `$lib`'s geo projections) must return — just enough of d3's `GeoProjection`
@@ -42,3 +42,35 @@ export type GeoProjectionConfig = GeoProjectionName | {
42
42
  */
43
43
  domain?: GeoJSON.GeoJsonObject | GeoJSON.FeatureCollection | GeoJSON.Feature | 'data';
44
44
  };
45
+ /**
46
+ * `scales` prop for GeoPlot — the geo analog of `AxisBasedScalesConfig`.
47
+ * A map has no x/y/z axis pair, so this only configures the choropleth
48
+ * colour domain (the coordinate system itself is `coordinates.projection`).
49
+ */
50
+ export type GeoScalesConfig = {
51
+ /** Continuous choropleth value domain — mirrors heatmap's `scales.z.domain`. */
52
+ color?: {
53
+ domain?: [number, number];
54
+ };
55
+ };
56
+ export type GeoZoomConfig = {
57
+ min?: number;
58
+ max?: number;
59
+ initial?: {
60
+ x?: number;
61
+ y?: number;
62
+ k?: number;
63
+ };
64
+ };
65
+ /**
66
+ * `coordinates` prop for GeoPlot — the only plot kind with a coordinate
67
+ * system other than implicit cartesian. Bundles the projection (a `GoG`
68
+ * Coordinates concern) and pan/zoom (navigating within that coordinate
69
+ * system), resolved together by `CoordinatesLayout`.
70
+ */
71
+ export type CoordinatesConfig = {
72
+ /** Which map projection to draw with. Defaults to `'equal-earth'`. */
73
+ projection?: GeoProjectionConfig;
74
+ /** `true`/config enables drag-pan + scroll-zoom. Omitted/`false` = static map. */
75
+ zoom?: boolean | GeoZoomConfig;
76
+ };
@@ -0,0 +1,17 @@
1
+ /** One resolved segment for scoped-marker rendering — see {@link ScopedMarkerGroup}. */
2
+ export type ScopedMarkerSegment<TData> = {
3
+ match: (row: TData) => boolean;
4
+ color: string;
5
+ };
6
+ /**
7
+ * One series' resolved context for rendering its `series`-scoped `'hover'`/
8
+ * `'component'` markers — built by each `Plot.svelte`, consumed by
9
+ * `GeometryLayout`. Keeps `GeometryLayout` itself agnostic of each plot
10
+ * kind's segment style shape (e.g. `fill` vs `stroke.stroke`).
11
+ */
12
+ export type ScopedMarkerGroup<TData> = {
13
+ name: string;
14
+ data: TData[];
15
+ color: string;
16
+ segments: ScopedMarkerSegment<TData>[];
17
+ };
@@ -53,3 +53,14 @@ export type AxisScale<TData = Record<string, unknown>> = {
53
53
  seriesLayout?: 'overlap' | 'grouped' | 'stacked';
54
54
  seriesPadding?: number;
55
55
  };
56
+ /**
57
+ * Per-axis scale overrides, passed as a single `scales` prop to every
58
+ * axis-based plot kind (`GeoPlot` uses `GeoScalesConfig` instead — a map has
59
+ * no x/y/z axis pair). `TData` threads through to {@link AxisScale.sort} on
60
+ * each axis.
61
+ */
62
+ export type AxisBasedScalesConfig<TData = Record<string, unknown>> = {
63
+ x?: AxisScale<TData>;
64
+ y?: AxisScale<TData>;
65
+ z?: AxisScale<TData>;
66
+ };