@fundar/data-chart-telling 0.0.14 → 0.0.15

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 (141) hide show
  1. package/README.md +121 -17
  2. package/dist/charts/{Chart.svelte.d.ts → BaseChart.svelte.d.ts} +3 -3
  3. package/dist/charts/bar/Chart.svelte +5 -5
  4. package/dist/charts/bar/Chart.svelte.d.ts +1 -2
  5. package/dist/charts/heatmap/Chart.svelte +8 -7
  6. package/dist/charts/heatmap/Chart.svelte.d.ts +2 -3
  7. package/dist/charts/line/Chart.svelte +5 -5
  8. package/dist/charts/line/Chart.svelte.d.ts +1 -2
  9. package/dist/charts/pyramid/Chart.svelte +7 -6
  10. package/dist/charts/pyramid/Chart.svelte.d.ts +2 -3
  11. package/dist/configuration/config.svelte.js +1 -1
  12. package/dist/configuration/themes/index.d.ts +56 -40
  13. package/dist/index.d.ts +26 -12
  14. package/dist/index.js +12 -7
  15. package/dist/layout/facet/FacetLayout.svelte +1 -1
  16. package/dist/layout/facet/FacetLayout.svelte.d.ts +1 -1
  17. package/dist/layout/geo/GeoLayout.svelte +47 -0
  18. package/dist/layout/geo/GeoLayout.svelte.d.ts +22 -0
  19. package/dist/layout/geo/resolveProjection.d.ts +10 -0
  20. package/dist/layout/geo/resolveProjection.js +48 -0
  21. package/dist/layout/legend/DiscreteSection.svelte +1 -1
  22. package/dist/layout/plot/AxisLayout.svelte +95 -0
  23. package/dist/layout/plot/AxisLayout.svelte.d.ts +39 -0
  24. package/dist/layout/plot/BasePlotLayout.svelte +302 -0
  25. package/dist/layout/plot/BasePlotLayout.svelte.d.ts +95 -0
  26. package/dist/layout/plot/GridLayout.svelte +30 -0
  27. package/dist/layout/plot/GridLayout.svelte.d.ts +27 -0
  28. package/dist/layout/plot/RuleLayout.svelte +111 -0
  29. package/dist/layout/plot/RuleLayout.svelte.d.ts +40 -0
  30. package/dist/layout/plot/margins.d.ts +34 -0
  31. package/dist/layout/plot/margins.js +26 -0
  32. package/dist/layout/timeline/TimelineLayout.svelte +1 -1
  33. package/dist/layout/timeline/TimelineLayout.svelte.d.ts +1 -1
  34. package/dist/layout/tooltip/Tooltip.svelte +15 -0
  35. package/dist/layout/tooltip/Tooltip.svelte.d.ts +7 -0
  36. package/dist/layout/tooltip/TooltipLayout.svelte +45 -0
  37. package/dist/layout/tooltip/TooltipLayout.svelte.d.ts +41 -0
  38. package/dist/layout/tooltip/controller.svelte.d.ts +4 -3
  39. package/dist/layout/tooltip/controller.svelte.js +5 -6
  40. package/dist/layout/tooltip/hover.svelte.d.ts +1 -1
  41. package/dist/layout/tooltip/hover.svelte.js +1 -1
  42. package/dist/layout/tooltip/utils.d.ts +2 -2
  43. package/dist/markers/ContourClipMarker.svelte +77 -0
  44. package/dist/markers/ContourClipMarker.svelte.d.ts +10 -0
  45. package/dist/{plots/markers → markers}/DeltaMarker.svelte +4 -4
  46. package/dist/{plots/markers → markers}/DeltaMarker.svelte.d.ts +3 -3
  47. package/dist/{plots/markers → markers}/DotMarker.svelte +3 -3
  48. package/dist/{plots/markers → markers}/DotMarker.svelte.d.ts +3 -3
  49. package/dist/markers/HoverMarker.svelte +136 -0
  50. package/dist/{plots/markers → markers}/HoverMarker.svelte.d.ts +4 -4
  51. package/dist/{plots/markers → markers}/PeakMarker.svelte +2 -2
  52. package/dist/{plots/markers → markers}/PeakMarker.svelte.d.ts +2 -2
  53. package/dist/{plots/markers → markers}/TextMarker.svelte +11 -4
  54. package/dist/{plots/markers → markers}/TextMarker.svelte.d.ts +3 -3
  55. package/dist/markers/inset/InsetMarker.svelte +170 -0
  56. package/dist/markers/inset/InsetMarker.svelte.d.ts +51 -0
  57. package/dist/markers/inset/insetLayout.d.ts +25 -0
  58. package/dist/markers/inset/insetLayout.js +39 -0
  59. package/dist/markers/inset/insetProjection.d.ts +8 -0
  60. package/dist/markers/inset/insetProjection.js +39 -0
  61. package/dist/plots/bar/BarSegments.svelte +66 -0
  62. package/dist/plots/bar/BarSegments.svelte.d.ts +36 -0
  63. package/dist/plots/bar/Plot.svelte +132 -76
  64. package/dist/plots/bar/Plot.svelte.d.ts +7 -3
  65. package/dist/plots/bar/ValueLabels.svelte +89 -0
  66. package/dist/plots/bar/ValueLabels.svelte.d.ts +39 -0
  67. package/dist/plots/bar/hoverPoints.d.ts +15 -0
  68. package/dist/plots/bar/hoverPoints.js +35 -0
  69. package/dist/plots/bar/layout.svelte.d.ts +37 -0
  70. package/dist/plots/bar/layout.svelte.js +132 -0
  71. package/dist/plots/geo/Plot.svelte +548 -0
  72. package/dist/plots/geo/Plot.svelte.d.ts +29 -0
  73. package/dist/plots/geo/TileLayer.svelte +77 -0
  74. package/dist/plots/geo/TileLayer.svelte.d.ts +11 -0
  75. package/dist/plots/geo/projections/argentina.d.ts +8 -0
  76. package/dist/plots/geo/projections/argentina.js +35 -0
  77. package/dist/plots/geo/projections/fit.d.ts +8 -0
  78. package/dist/plots/geo/projections/fit.js +14 -0
  79. package/dist/plots/geo/rotate.svelte.d.ts +26 -0
  80. package/dist/plots/geo/rotate.svelte.js +79 -0
  81. package/dist/plots/geo/zoom.svelte.d.ts +32 -0
  82. package/dist/plots/geo/zoom.svelte.js +50 -0
  83. package/dist/plots/heatmap/Plot.svelte +129 -47
  84. package/dist/plots/heatmap/Plot.svelte.d.ts +7 -3
  85. package/dist/plots/line/Plot.svelte +44 -16
  86. package/dist/plots/line/Plot.svelte.d.ts +7 -3
  87. package/dist/plots/pyramid/Plot.svelte +70 -25
  88. package/dist/plots/pyramid/Plot.svelte.d.ts +7 -3
  89. package/dist/plots/utils/delta.d.ts +1 -1
  90. package/dist/plots/utils/geoAccessors.d.ts +64 -0
  91. package/dist/plots/utils/geoAccessors.js +188 -0
  92. package/dist/plots/utils/geoSegments.d.ts +14 -0
  93. package/dist/plots/utils/geoSegments.js +17 -0
  94. package/dist/plots/utils/segments.d.ts +14 -5
  95. package/dist/plots/utils/segments.js +25 -13
  96. package/dist/plots/utils/tiles.d.ts +89 -0
  97. package/dist/plots/utils/tiles.js +159 -0
  98. package/dist/plots/utils/topojson.d.ts +9 -0
  99. package/dist/plots/utils/topojson.js +30 -0
  100. package/dist/types/charts/common.d.ts +2 -1
  101. package/dist/types/charts/legend.d.ts +1 -1
  102. package/dist/types/charts/props.d.ts +9 -6
  103. package/dist/types/configuration/styling.d.ts +11 -5
  104. package/dist/types/layout/tooltip.d.ts +14 -21
  105. package/dist/types/{plots/markers.d.ts → markers/common.d.ts} +2 -18
  106. package/dist/types/markers/geo.d.ts +237 -0
  107. package/dist/types/markers/props.d.ts +11 -0
  108. package/dist/types/plots/axis.d.ts +55 -0
  109. package/dist/types/plots/axis.js +1 -0
  110. package/dist/types/plots/constants.d.ts +30 -0
  111. package/dist/types/plots/constants.js +1 -0
  112. package/dist/types/plots/data/common.d.ts +40 -0
  113. package/dist/types/plots/data/common.js +1 -0
  114. package/dist/types/plots/data/geo.d.ts +74 -0
  115. package/dist/types/plots/data/geo.js +1 -0
  116. package/dist/types/plots/delta.d.ts +2 -2
  117. package/dist/types/plots/props.d.ts +105 -16
  118. package/dist/types/plots/scales/geo.d.ts +44 -0
  119. package/dist/types/plots/scales/geo.js +1 -0
  120. package/dist/types/plots/segments/common.d.ts +25 -0
  121. package/dist/types/plots/segments/common.js +1 -0
  122. package/dist/types/plots/segments/config.d.ts +51 -0
  123. package/dist/types/plots/segments/config.js +1 -0
  124. package/dist/types/plots/styles/common.d.ts +37 -0
  125. package/dist/types/plots/styles/common.js +1 -0
  126. package/dist/types/plots/styles/geo.d.ts +42 -0
  127. package/dist/types/plots/styles/geo.js +1 -0
  128. package/dist/types/plots/styling.d.ts +45 -0
  129. package/dist/types/plots/styling.js +1 -0
  130. package/dist/utils/grouping.d.ts +2 -1
  131. package/dist/utils/interpolate.d.ts +2 -1
  132. package/package.json +27 -13
  133. package/dist/layout/plot/PlotLayout.svelte +0 -294
  134. package/dist/layout/plot/PlotLayout.svelte.d.ts +0 -70
  135. package/dist/plots/markers/HoverMarker.svelte +0 -98
  136. package/dist/types/plots/common.d.ts +0 -97
  137. package/dist/types/plots/styles.d.ts +0 -187
  138. /package/dist/charts/{Chart.svelte → BaseChart.svelte} +0 -0
  139. /package/dist/types/{plots → markers}/common.js +0 -0
  140. /package/dist/types/{plots/markers.js → markers/geo.js} +0 -0
  141. /package/dist/types/{plots/styles.js → markers/props.js} +0 -0
@@ -1,7 +1,11 @@
1
- import type { PlotProps } from '../../types/plots/props';
2
- import type { LineSegmentStyle } from '../../types/plots/styles';
1
+ import type { BasePlotProps } from '../../types/plots/props';
2
+ 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 { LineSegmentStyle } from '../../types/plots/segments/common';
6
+ import type { Marker } from '../../types/markers/common';
3
7
  declare function $$render<TData extends Record<string, unknown>>(): {
4
- props: PlotProps<TData, LineSegmentStyle>;
8
+ props: BasePlotProps<SeriesProps<TData> | DataProps<TData>, TData, BasePlotStyles, LineSegmentStyle, Marker, AxisBasedScalesConfig<TData>>;
5
9
  exports: {};
6
10
  bindings: "";
7
11
  slots: {};
@@ -6,12 +6,20 @@
6
6
  import { paletteColor } from '../../utils/color';
7
7
  import { buildSeries } from '../../utils/grouping';
8
8
  import { buildGroupedSeries, validateSegments, resolveSegmentsForSeries, matchesSegmentX, getDataForSegments } from '../utils/segments';
9
- import PlotLayout from '../../layout/plot/PlotLayout.svelte';
10
- import HoverMarker from '../markers/HoverMarker.svelte';
11
- import type { PlotProps } from '../../types/plots/props';
12
- import type { Series, AxisValue } from '../../types/plots/common';
13
- import type { BarSegmentStyle, ValueAnchor } from '../../types/plots/styles';
14
- import type { Marker } from '../../types/plots/markers';
9
+ import BasePlotLayout from '../../layout/plot/BasePlotLayout.svelte';
10
+ import AxisLayout from '../../layout/plot/AxisLayout.svelte';
11
+ import GridLayout from '../../layout/plot/GridLayout.svelte';
12
+ import RuleLayout from '../../layout/plot/RuleLayout.svelte';
13
+ import HoverMarker from '../../markers/HoverMarker.svelte';
14
+ import { hasHoverMarker, resolveHoverStrategy, resolveHoverSync } from '../../layout/tooltip/utils';
15
+ import type { BasePlotProps } from '../../types/plots/props';
16
+ import type { AxisValue } from '../../types/plots/axis';
17
+ import type { Series, SeriesProps, DataProps } from '../../types/plots/data/common';
18
+ import type { AxisBasedScalesConfig } from '../../types/plots/props';
19
+ import type { BasePlotStyles } from '../../types/plots/styles/common';
20
+ import type { ValueAnchor } from '../../types/plots/constants';
21
+ import type { BarSegmentStyle } from '../../types/plots/segments/common';
22
+ import type { Marker } from '../../types/markers/common';
15
23
 
16
24
  /**
17
25
  * A population-pyramid-style diverging bar chart: each series is mirrored
@@ -29,6 +37,15 @@
29
37
  type PyramidSide = 'left' | 'right';
30
38
  type InternalSeries = Series<TData> & { side: PyramidSide };
31
39
 
40
+ type Props = BasePlotProps<
41
+ SeriesProps<TData> | DataProps<TData>,
42
+ TData,
43
+ BasePlotStyles,
44
+ BarSegmentStyle,
45
+ Marker,
46
+ AxisBasedScalesConfig<TData>
47
+ >;
48
+
32
49
  let {
33
50
  width,
34
51
  height,
@@ -43,7 +60,7 @@
43
60
  segments = {},
44
61
  markers = [],
45
62
  tooltip = undefined,
46
- }: PlotProps<TData, BarSegmentStyle> = $props();
63
+ }: Props = $props();
47
64
 
48
65
  $effect(() => { validateSegments(segments); });
49
66
 
@@ -53,6 +70,10 @@
53
70
  const showVals = $derived(styles.values?.show ?? false);
54
71
  const fmtVal = $derived(styles.values?.format ?? ((v: number) => `${v}`));
55
72
  const valAnchor = $derived<ValueAnchor>(styles.values?.anchor ?? 'outside');
73
+ // 'outside' is a per-point escape hatch resolved by HoverMarker; pyramid's
74
+ // own value labels already have their own `anchor: 'outside'` concept
75
+ // above, so treat it as unset here.
76
+ const resolvedTextAnchor = $derived(styles.values?.textAnchor === 'outside' ? undefined : styles.values?.textAnchor);
56
77
 
57
78
  // Sort series names alphabetically; even index → left, odd → right.
58
79
  // scales.z.reverse flips the assignment so the second name goes left.
@@ -68,7 +89,9 @@
68
89
  });
69
90
 
70
91
  const groupedSeries = $derived(
71
- buildGroupedSeries<InternalSeries, BarSegmentStyle>(resolvedSeries, segments),
92
+ // connect: false — bars are discrete; there's no visual gap between
93
+ // adjacent categories to bridge (see buildGroupedSeries's own doc).
94
+ buildGroupedSeries<InternalSeries, BarSegmentStyle>(resolvedSeries, segments, false),
72
95
  );
73
96
 
74
97
  function signedValue(s: InternalSeries, d: TData): number {
@@ -149,31 +172,47 @@
149
172
  }
150
173
  return map;
151
174
  });
175
+
176
+ // The single, fully-resolved scales object BasePlotLayout reads for
177
+ // everything (ticks, grid, sort, and the real svelteplot scale) — avoids
178
+ // keeping a second, separately-defaulted copy of `scales.x`/`scales.y`
179
+ // (our own diverging-domain + band-scale defaults) out of sync with the
180
+ // one actually forwarded.
181
+ const resolvedScales = $derived<AxisBasedScalesConfig<TData>>({
182
+ ...scales,
183
+ x: {
184
+ domain: [-maxMagnitude, maxMagnitude],
185
+ tickFormat: (v: unknown) => fmtVal(Math.abs(Number(v)), ''),
186
+ ...(scales.x ?? {}),
187
+ },
188
+ y: { type: 'band', padding: 0.2, domain: yDomain, ...(scales.y ?? {}) },
189
+ });
190
+
191
+ const hoverActive = $derived(tooltip != null || hasHoverMarker(markers));
192
+ const hoverStrategy = $derived(resolveHoverStrategy(tooltip?.strategy, markers, 'y'));
193
+ const hoverSync = $derived(resolveHoverSync(tooltip?.sync, markers));
152
194
  </script>
153
195
 
154
- <PlotLayout
196
+ <BasePlotLayout
155
197
  {width}
156
198
  {height}
157
- x={{
158
- domain: [-maxMagnitude, maxMagnitude],
159
- tickFormat: (v: unknown) => fmtVal(Math.abs(Number(v)), ''),
160
- ...(scales.x ?? {}),
161
- }}
162
- y={{ type: 'band', padding: 0.2, domain: yDomain, ...(scales.y ?? {}) }}
163
199
  data={flat}
164
200
  {getX}
165
201
  {getY}
166
- matchY={getY}
167
202
  {hoverPoints}
168
203
  label={labelFor}
169
204
  {tooltip}
170
- {markers}
171
- seriesData={resolvedSeries}
172
- {scales}
205
+ {hoverActive}
206
+ {hoverStrategy}
207
+ {hoverSync}
208
+ scales={resolvedScales}
173
209
  {margins}
174
- defaultStrategy="y"
210
+ xTickRotate={resolvedScales?.x?.tickRotate}
175
211
  >
176
- {#snippet children({ matchedPoints, ruleX, ruleY, impliesRuleX, impliesRuleY })}
212
+ {#snippet children({ matchedPoints, ruleX, ruleY, impliesRuleX, impliesRuleY, numericMargins })}
213
+ <AxisLayout data={flat} {getX} {getY} {width} {height} {numericMargins} scales={resolvedScales} />
214
+ <GridLayout scales={resolvedScales} />
215
+ <RuleLayout {markers} seriesData={resolvedSeries} {matchedPoints} {ruleX} {ruleY} {impliesRuleX} {impliesRuleY}>
177
216
  {#each groupedSeries as group (group.series.name)}
178
217
  {@const defaultColor = group.series.side === 'left' ? leftColor : rightColor}
179
218
  {@const getCategory = resolveAccessor(group.series.x)}
@@ -210,14 +249,19 @@
210
249
  text={(d: TData) => fmtVal(Math.abs(Number(resolveAccessor(group.series.y)(d))), group.series.name)}
211
250
  dx={styles.values?.dx ?? defaultDx}
212
251
  dy={styles.values?.dy ?? 0}
213
- textAnchor={styles.values?.textAnchor ?? defaultTextAnchor}
252
+ textAnchor={resolvedTextAnchor ?? defaultTextAnchor}
214
253
  lineAnchor={styles.values?.lineAnchor ?? 'middle'}
215
254
  lineHeight={styles.values?.lineHeight}
216
255
  rotate={styles.values?.rotate}
217
256
  class={styles.values?.class}
218
257
  textClass={styles.values?.textClass}
219
- fill={cfg.axis.color}
220
- fontSize={12}
258
+ fill={styles.values?.fill ?? cfg.axis.color}
259
+ fontSize={styles.values?.fontSize ?? 12}
260
+ fontWeight={styles.values?.fontWeight}
261
+ fontStyle={styles.values?.fontStyle}
262
+ stroke={styles.values?.stroke}
263
+ strokeWidth={styles.values?.strokeWidth}
264
+ paintOrder={styles.values?.paintOrder}
221
265
  />
222
266
  {/if}
223
267
 
@@ -282,5 +326,6 @@
282
326
  {/if}
283
327
  {/each}
284
328
  {/each}
329
+ </RuleLayout>
285
330
  {/snippet}
286
- </PlotLayout>
331
+ </BasePlotLayout>
@@ -1,7 +1,11 @@
1
- import type { PlotProps } from '../../types/plots/props';
2
- import type { BarSegmentStyle } from '../../types/plots/styles';
1
+ import type { BasePlotProps } from '../../types/plots/props';
2
+ 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';
3
7
  declare function $$render<TData extends Record<string, unknown>>(): {
4
- props: PlotProps<TData, BarSegmentStyle>;
8
+ props: BasePlotProps<SeriesProps<TData> | DataProps<TData>, TData, BasePlotStyles, BarSegmentStyle, Marker, AxisBasedScalesConfig<TData>>;
5
9
  exports: {};
6
10
  bindings: "";
7
11
  slots: {};
@@ -1,4 +1,4 @@
1
- import type { Series } from '../../types/plots/common';
1
+ import type { Series } from '../../types/plots/data/common';
2
2
  import type { DeltaConfig } from '../../types/plots/delta';
3
3
  export type ResolvedDelta = {
4
4
  cfg: DeltaConfig;
@@ -0,0 +1,64 @@
1
+ import type { GeoFeature } from '../../types/plots/data/geo';
2
+ /**
3
+ * Bounding-box center of a geometry's flattened coordinates — a cheap
4
+ * approximation used for default label placement, not a true
5
+ * geographic/area centroid (no polygon-hole or geodesic-area weighting).
6
+ * For hover-matching specifically, see `geometryPartCentroids` instead —
7
+ * this single combined point can land far from every actual part of a
8
+ * multi-part geometry whose pieces are geographically spread out.
9
+ */
10
+ export declare function geometryCentroid(geometry: GeoJSON.Geometry): [number, number];
11
+ /**
12
+ * Splits a geometry into its disjoint parts: one `Polygon` per element of a
13
+ * `MultiPolygon`'s `coordinates`, one `LineString` per element of a
14
+ * `MultiLineString`'s, `GeometryCollection` recursed and flattened, and any
15
+ * other geometry type returned unchanged as its own single-element array.
16
+ * The shared building block behind `geometryPartCentroids` (one centroid per
17
+ * part) and `filterGeometryParts` (keep/drop whole parts).
18
+ */
19
+ export declare function splitGeometryParts(geometry: GeoJSON.Geometry): GeoJSON.Geometry[];
20
+ /**
21
+ * Like `geometryCentroid`, but returns one point per disjoint part instead
22
+ * of a single bounding-box center across all of them. A `MultiPolygon`
23
+ * whose parts are geographically far apart — e.g. Argentina's "Tierra del
24
+ * Fuego, Antártida e Islas del Atlántico Sur" province, whose `MultiPolygon`
25
+ * spans the mainland island, the Antarctic sector, and the South Atlantic
26
+ * islands in one feature — would otherwise get a single hover point
27
+ * stranded somewhere between all of them, matching hovers over *none* of
28
+ * the actually-rendered shapes. Multiple points attributed to the same
29
+ * feature is fine for hover-matching (nearest point wins, whichever part
30
+ * that is); it's only label placement (`geometryCentroid`) that needs a
31
+ * single point.
32
+ */
33
+ export declare function geometryPartCentroids(geometry: GeoJSON.Geometry): [number, number][];
34
+ /**
35
+ * Keeps or drops each disjoint part of a geometry (see `splitGeometryParts`)
36
+ * based on a per-part predicate, reassembling a geometry of the same type
37
+ * from whatever survives — `null` if nothing does. Used to let a single
38
+ * feature's geometry be split between the main map and an `inset` marker
39
+ * (see `GeoDataProps.excludeParts`/`GeoInsetMarker.featurePartsFilter`)
40
+ * without preprocessing the source data: e.g. a province whose `MultiPolygon`
41
+ * spans both a mainland part and a far-flung part can have the main map
42
+ * exclude the latter while an inset shows only it.
43
+ */
44
+ export declare function filterGeometryParts(geometry: GeoJSON.Geometry, keep: (part: {
45
+ centroid: [number, number];
46
+ geometry: GeoJSON.Geometry;
47
+ }) => boolean): GeoJSON.Geometry | null;
48
+ /** Default `featureId`: `feature.id`, else `properties.id`, else `properties.name`. */
49
+ export declare function defaultFeatureId<TProps extends Record<string, unknown>>(f: GeoFeature<TProps>): string;
50
+ /**
51
+ * Densifies a great-circle arc between two lon/lat points into `steps + 1`
52
+ * points via spherical linear interpolation (slerp) — used to draw a `link`
53
+ * marker's geodesic curve through the `Geo` mark (see `Plot.svelte`).
54
+ *
55
+ * Not delegated to svelteplot's own `Link` mark: its 0.14.2 projection
56
+ * integration (`Mark.svelte`'s paired-channel handling) reads a single
57
+ * `row.x`/`row.y` pair regardless of channel suffix, so a mark bound to
58
+ * `x1`/`y1`/`x2`/`y2` (never plain `x`/`y`) resolves every point as invalid
59
+ * whenever the plot has an active `projection` scale — which GeoPlot always
60
+ * does. `Geo` has no such limitation (geo marks are explicitly excluded from
61
+ * that code path), so a manually-interpolated `LineString` feature sidesteps
62
+ * the bug entirely while still producing the same geodesic visual.
63
+ */
64
+ export declare function greatCircleInterpolate(lon1: number, lat1: number, lon2: number, lat2: number, steps?: number): [number, number][];
@@ -0,0 +1,188 @@
1
+ function flattenCoords(geom, out) {
2
+ switch (geom.type) {
3
+ case 'Point':
4
+ out.push(geom.coordinates);
5
+ break;
6
+ case 'MultiPoint':
7
+ case 'LineString':
8
+ for (const c of geom.coordinates)
9
+ out.push(c);
10
+ break;
11
+ case 'MultiLineString':
12
+ case 'Polygon':
13
+ for (const ring of geom.coordinates) {
14
+ for (const c of ring)
15
+ out.push(c);
16
+ }
17
+ break;
18
+ case 'MultiPolygon':
19
+ for (const poly of geom.coordinates) {
20
+ for (const ring of poly) {
21
+ for (const c of ring)
22
+ out.push(c);
23
+ }
24
+ }
25
+ break;
26
+ case 'GeometryCollection':
27
+ for (const g of geom.geometries)
28
+ flattenCoords(g, out);
29
+ break;
30
+ }
31
+ }
32
+ function boundsCenter(coords) {
33
+ if (coords.length === 0)
34
+ return [0, 0];
35
+ let minX = Infinity;
36
+ let maxX = -Infinity;
37
+ let minY = Infinity;
38
+ let maxY = -Infinity;
39
+ for (const [x, y] of coords) {
40
+ if (x < minX)
41
+ minX = x;
42
+ if (x > maxX)
43
+ maxX = x;
44
+ if (y < minY)
45
+ minY = y;
46
+ if (y > maxY)
47
+ maxY = y;
48
+ }
49
+ return [(minX + maxX) / 2, (minY + maxY) / 2];
50
+ }
51
+ /**
52
+ * Bounding-box center of a geometry's flattened coordinates — a cheap
53
+ * approximation used for default label placement, not a true
54
+ * geographic/area centroid (no polygon-hole or geodesic-area weighting).
55
+ * For hover-matching specifically, see `geometryPartCentroids` instead —
56
+ * this single combined point can land far from every actual part of a
57
+ * multi-part geometry whose pieces are geographically spread out.
58
+ */
59
+ export function geometryCentroid(geometry) {
60
+ const coords = [];
61
+ flattenCoords(geometry, coords);
62
+ return boundsCenter(coords);
63
+ }
64
+ /**
65
+ * Splits a geometry into its disjoint parts: one `Polygon` per element of a
66
+ * `MultiPolygon`'s `coordinates`, one `LineString` per element of a
67
+ * `MultiLineString`'s, `GeometryCollection` recursed and flattened, and any
68
+ * other geometry type returned unchanged as its own single-element array.
69
+ * The shared building block behind `geometryPartCentroids` (one centroid per
70
+ * part) and `filterGeometryParts` (keep/drop whole parts).
71
+ */
72
+ export function splitGeometryParts(geometry) {
73
+ switch (geometry.type) {
74
+ case 'MultiPolygon':
75
+ return geometry.coordinates.map((polygon) => ({ type: 'Polygon', coordinates: polygon }));
76
+ case 'MultiLineString':
77
+ return geometry.coordinates.map((line) => ({ type: 'LineString', coordinates: line }));
78
+ case 'GeometryCollection':
79
+ return geometry.geometries.flatMap((g) => splitGeometryParts(g));
80
+ default:
81
+ return [geometry];
82
+ }
83
+ }
84
+ /**
85
+ * Like `geometryCentroid`, but returns one point per disjoint part instead
86
+ * of a single bounding-box center across all of them. A `MultiPolygon`
87
+ * whose parts are geographically far apart — e.g. Argentina's "Tierra del
88
+ * Fuego, Antártida e Islas del Atlántico Sur" province, whose `MultiPolygon`
89
+ * spans the mainland island, the Antarctic sector, and the South Atlantic
90
+ * islands in one feature — would otherwise get a single hover point
91
+ * stranded somewhere between all of them, matching hovers over *none* of
92
+ * the actually-rendered shapes. Multiple points attributed to the same
93
+ * feature is fine for hover-matching (nearest point wins, whichever part
94
+ * that is); it's only label placement (`geometryCentroid`) that needs a
95
+ * single point.
96
+ */
97
+ export function geometryPartCentroids(geometry) {
98
+ return splitGeometryParts(geometry).map((part) => geometryCentroid(part));
99
+ }
100
+ /**
101
+ * Keeps or drops each disjoint part of a geometry (see `splitGeometryParts`)
102
+ * based on a per-part predicate, reassembling a geometry of the same type
103
+ * from whatever survives — `null` if nothing does. Used to let a single
104
+ * feature's geometry be split between the main map and an `inset` marker
105
+ * (see `GeoDataProps.excludeParts`/`GeoInsetMarker.featurePartsFilter`)
106
+ * without preprocessing the source data: e.g. a province whose `MultiPolygon`
107
+ * spans both a mainland part and a far-flung part can have the main map
108
+ * exclude the latter while an inset shows only it.
109
+ */
110
+ export function filterGeometryParts(geometry, keep) {
111
+ const parts = splitGeometryParts(geometry);
112
+ const keptParts = parts.filter((part) => keep({ centroid: geometryCentroid(part), geometry: part }));
113
+ if (keptParts.length === parts.length)
114
+ return geometry;
115
+ if (keptParts.length === 0)
116
+ return null;
117
+ if (parts.length === 1)
118
+ return keptParts[0];
119
+ switch (geometry.type) {
120
+ case 'MultiPolygon':
121
+ return { type: 'MultiPolygon', coordinates: keptParts.map((part) => part.coordinates) };
122
+ case 'MultiLineString':
123
+ return {
124
+ type: 'MultiLineString',
125
+ coordinates: keptParts.map((part) => part.coordinates),
126
+ };
127
+ case 'GeometryCollection':
128
+ return { type: 'GeometryCollection', geometries: keptParts };
129
+ default:
130
+ return geometry;
131
+ }
132
+ }
133
+ /** Default `featureId`: `feature.id`, else `properties.id`, else `properties.name`. */
134
+ export function defaultFeatureId(f) {
135
+ const props = f.properties;
136
+ return String(f.id ?? props?.id ?? props?.name ?? '');
137
+ }
138
+ function toRad(deg) {
139
+ return (deg * Math.PI) / 180;
140
+ }
141
+ function toDeg(rad) {
142
+ return (rad * 180) / Math.PI;
143
+ }
144
+ /**
145
+ * Densifies a great-circle arc between two lon/lat points into `steps + 1`
146
+ * points via spherical linear interpolation (slerp) — used to draw a `link`
147
+ * marker's geodesic curve through the `Geo` mark (see `Plot.svelte`).
148
+ *
149
+ * Not delegated to svelteplot's own `Link` mark: its 0.14.2 projection
150
+ * integration (`Mark.svelte`'s paired-channel handling) reads a single
151
+ * `row.x`/`row.y` pair regardless of channel suffix, so a mark bound to
152
+ * `x1`/`y1`/`x2`/`y2` (never plain `x`/`y`) resolves every point as invalid
153
+ * whenever the plot has an active `projection` scale — which GeoPlot always
154
+ * does. `Geo` has no such limitation (geo marks are explicitly excluded from
155
+ * that code path), so a manually-interpolated `LineString` feature sidesteps
156
+ * the bug entirely while still producing the same geodesic visual.
157
+ */
158
+ export function greatCircleInterpolate(lon1, lat1, lon2, lat2, steps = 64) {
159
+ const phi1 = toRad(lat1);
160
+ const lambda1 = toRad(lon1);
161
+ const phi2 = toRad(lat2);
162
+ const lambda2 = toRad(lon2);
163
+ const sinPhi1 = Math.sin(phi1);
164
+ const cosPhi1 = Math.cos(phi1);
165
+ const sinPhi2 = Math.sin(phi2);
166
+ const cosPhi2 = Math.cos(phi2);
167
+ const d = 2 *
168
+ Math.asin(Math.sqrt(Math.sin((phi2 - phi1) / 2) ** 2 + cosPhi1 * cosPhi2 * Math.sin((lambda2 - lambda1) / 2) ** 2));
169
+ if (!Number.isFinite(d) || d === 0) {
170
+ return [
171
+ [lon1, lat1],
172
+ [lon2, lat2],
173
+ ];
174
+ }
175
+ const points = [];
176
+ for (let i = 0; i <= steps; i++) {
177
+ const f = i / steps;
178
+ const a = Math.sin((1 - f) * d) / Math.sin(d);
179
+ const b = Math.sin(f * d) / Math.sin(d);
180
+ const x = a * cosPhi1 * Math.cos(lambda1) + b * cosPhi2 * Math.cos(lambda2);
181
+ const y = a * cosPhi1 * Math.sin(lambda1) + b * cosPhi2 * Math.sin(lambda2);
182
+ const z = a * sinPhi1 + b * sinPhi2;
183
+ const phi = Math.atan2(z, Math.sqrt(x * x + y * y));
184
+ const lambda = Math.atan2(y, x);
185
+ points.push([toDeg(lambda), toDeg(phi)]);
186
+ }
187
+ return points;
188
+ }
@@ -0,0 +1,14 @@
1
+ import type { Segment } from '../../types/plots/segments/config';
2
+ import type { SegmentsConfig } from '../../types/plots/props';
3
+ /** Feature-specific segments listed after 'default' so they override it on merge (see `resolveFeatureStyle`). */
4
+ export declare function resolveSegmentsForFeature<TStyle>(segments: SegmentsConfig<TStyle>, featureId: string): Segment<TStyle>[];
5
+ /**
6
+ * A geo feature is atomic — `Segment.areas` (x/y range restriction) doesn't
7
+ * apply, so every entry under a key is treated as a flat style patch applied
8
+ * in order: `'default'` entries apply first, feature-specific entries
9
+ * override. This intentionally does NOT reuse `segments.ts`'s
10
+ * `validateSegments` — its "only the first catch-all wins" warning assumes
11
+ * area-based semantics that don't hold here, since every geo segment entry
12
+ * merges rather than being dropped.
13
+ */
14
+ export declare function resolveFeatureStyle<TStyle extends object>(segments: SegmentsConfig<TStyle>, featureId: string): TStyle;
@@ -0,0 +1,17 @@
1
+ const DEFAULT_KEY = 'default';
2
+ /** Feature-specific segments listed after 'default' so they override it on merge (see `resolveFeatureStyle`). */
3
+ export function resolveSegmentsForFeature(segments, featureId) {
4
+ return [...(segments[DEFAULT_KEY] ?? []), ...(segments[featureId] ?? [])];
5
+ }
6
+ /**
7
+ * A geo feature is atomic — `Segment.areas` (x/y range restriction) doesn't
8
+ * apply, so every entry under a key is treated as a flat style patch applied
9
+ * in order: `'default'` entries apply first, feature-specific entries
10
+ * override. This intentionally does NOT reuse `segments.ts`'s
11
+ * `validateSegments` — its "only the first catch-all wins" warning assumes
12
+ * area-based semantics that don't hold here, since every geo segment entry
13
+ * merges rather than being dropped.
14
+ */
15
+ export function resolveFeatureStyle(segments, featureId) {
16
+ return resolveSegmentsForFeature(segments, featureId).reduce((acc, seg) => ({ ...acc, ...(seg.style ?? {}) }), {});
17
+ }
@@ -1,11 +1,14 @@
1
- import type { AxisValue, Series } from '../../types/plots/common';
2
- import type { Segment, Segments, LineSegmentStyle, VisualGroup } from '../../types/plots/styles';
1
+ import type { AxisValue } from '../../types/plots/axis';
2
+ import type { Series } from '../../types/plots/data/common';
3
+ import type { LineSegmentStyle } from '../../types/plots/segments/common';
4
+ import type { Segment, VisualGroup } from '../../types/plots/segments/config';
5
+ import type { SegmentsConfig } from '../../types/plots/props';
3
6
  /**
4
7
  * Returns the flat {@link Segment} list for one series.
5
8
  * Series-specific segments are added before `'default'` segments so that
6
9
  * series-specific catch-alls win over default catch-alls on tie-breaks.
7
10
  */
8
- export declare function resolveSegmentsForSeries<TStyle>(segments: Segments<TStyle>, seriesName: string): Segment<TStyle>[];
11
+ export declare function resolveSegmentsForSeries<TStyle>(segments: SegmentsConfig<TStyle>, seriesName: string): Segment<TStyle>[];
9
12
  /** Returns true when `xVal` falls inside any of a segment's x ranges. */
10
13
  export declare function matchesSegmentX<TStyle>(xVal: AxisValue, seg: Segment<TStyle>): boolean;
11
14
  /**
@@ -22,10 +25,16 @@ export declare function getDataForSegments<TData, TStyle>(data: TData[], xAccess
22
25
  * (no `areas`) in the same series list. Since catch-alls are evaluated last
23
26
  * and in order, only the first one can ever match; later ones are dead code.
24
27
  */
25
- export declare function validateSegments<TStyle>(segments: Segments<TStyle>): void;
28
+ export declare function validateSegments<TStyle>(segments: SegmentsConfig<TStyle>): void;
26
29
  /**
27
30
  * Builds the full list of visual groups — one per series — ready to render.
28
31
  * Each group's `visualSegments` is derived by resolving the flat segment list
29
32
  * for that series from all groups in `segments`.
33
+ *
34
+ * `connect` bridges adjacent visual segments so a *continuous* mark (line)
35
+ * has no visual gap at their boundary — the default, since `buildGroupedSeries`
36
+ * itself defaults `TStyle` to {@link LineSegmentStyle}. Discrete marks (bar,
37
+ * pyramid) have no gap to bridge; pass `connect: false` so boundary rows
38
+ * aren't duplicated as an extra, separately-styled mark.
30
39
  */
31
- export declare function buildGroupedSeries<S extends Series<any>, TStyle = LineSegmentStyle>(seriesList: S[], segments: Segments<TStyle>): VisualGroup<S, TStyle>[];
40
+ export declare function buildGroupedSeries<S extends Series<any>, TStyle = LineSegmentStyle>(seriesList: S[], segments: SegmentsConfig<TStyle>, connect?: boolean): VisualGroup<S, TStyle>[];
@@ -90,7 +90,7 @@ function mergeStyles(base, override) {
90
90
  return result;
91
91
  }
92
92
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
93
- function resolveSeriesSegments(series, segs) {
93
+ function resolveSeriesSegments(series, segs, connect) {
94
94
  if (!segs.length) {
95
95
  return [{ data: series.data, style: {} }];
96
96
  }
@@ -147,14 +147,18 @@ function resolveSeriesSegments(series, segs) {
147
147
  for (let r = 0; r < runs.length; r++) {
148
148
  const run = runs[r];
149
149
  if (run.segIdx === -1) {
150
- // Unmatched run: insert bridge points so the line connects to adjacent
151
- // user segments without a gap.
152
- const bridged = [];
153
- if (r > 0)
154
- bridged.push(runs[r - 1].data[runs[r - 1].data.length - 1]);
155
- bridged.push(...run.data);
156
- if (r < runs.length - 1)
157
- bridged.push(runs[r + 1].data[0]);
150
+ // Unmatched run: for a continuous mark (line), insert bridge points so
151
+ // it connects to adjacent user segments without a visual gap. Discrete
152
+ // marks (bar, pyramid) have no such gap to bridge — bridging would just
153
+ // duplicate the neighboring runs' boundary rows as an extra, wrongly
154
+ // styled mark at that same category.
155
+ const bridged = [...run.data];
156
+ if (connect) {
157
+ if (r > 0)
158
+ bridged.unshift(runs[r - 1].data[runs[r - 1].data.length - 1]);
159
+ if (r < runs.length - 1)
160
+ bridged.push(runs[r + 1].data[0]);
161
+ }
158
162
  defaultVisual.push({ data: bridged, style: {} });
159
163
  }
160
164
  else {
@@ -170,9 +174,11 @@ function resolveSeriesSegments(series, segs) {
170
174
  // When the next run is also a user segment (no unmatched data between them),
171
175
  // insert a line-only connector so the lines join without a gap. Using a
172
176
  // separate mark (rather than appending a bridge point to the current run)
173
- // avoids rendering an extra dot at the shared boundary point.
177
+ // avoids rendering an extra dot at the shared boundary point. Skipped
178
+ // entirely for discrete marks (see above) — it would duplicate both
179
+ // runs' boundary rows as an extra mark.
174
180
  const nextRun = runs[r + 1];
175
- if (nextRun && nextRun.segIdx !== -1) {
181
+ if (connect && nextRun && nextRun.segIdx !== -1) {
176
182
  connectors.push({
177
183
  data: [run.data[run.data.length - 1], nextRun.data[0]],
178
184
  style: connectorStyle,
@@ -190,10 +196,16 @@ function resolveSeriesSegments(series, segs) {
190
196
  * Builds the full list of visual groups — one per series — ready to render.
191
197
  * Each group's `visualSegments` is derived by resolving the flat segment list
192
198
  * for that series from all groups in `segments`.
199
+ *
200
+ * `connect` bridges adjacent visual segments so a *continuous* mark (line)
201
+ * has no visual gap at their boundary — the default, since `buildGroupedSeries`
202
+ * itself defaults `TStyle` to {@link LineSegmentStyle}. Discrete marks (bar,
203
+ * pyramid) have no gap to bridge; pass `connect: false` so boundary rows
204
+ * aren't duplicated as an extra, separately-styled mark.
193
205
  */
194
- export function buildGroupedSeries(seriesList, segments) {
206
+ export function buildGroupedSeries(seriesList, segments, connect = true) {
195
207
  return seriesList.map((s) => ({
196
208
  series: s,
197
- visualSegments: resolveSeriesSegments(s, resolveSegmentsForSeries(segments, s.name)),
209
+ visualSegments: resolveSeriesSegments(s, resolveSegmentsForSeries(segments, s.name), connect),
198
210
  }));
199
211
  }