@fundar/data-chart-telling 0.0.25 → 0.0.27

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (198) hide show
  1. package/dist/charts/BaseChart.svelte +18 -10
  2. package/dist/charts/BaseChart.svelte.d.ts +4 -2
  3. package/dist/charts/bar/Chart.svelte +2 -0
  4. package/dist/charts/heatmap/Chart.svelte +5 -3
  5. package/dist/charts/heatmap/Chart.svelte.d.ts +1 -2
  6. package/dist/charts/line/Chart.svelte +2 -0
  7. package/dist/charts/pyramid/Chart.svelte +5 -3
  8. package/dist/charts/pyramid/Chart.svelte.d.ts +1 -2
  9. package/dist/charts/scatter/Chart.svelte +95 -0
  10. package/dist/charts/scatter/Chart.svelte.d.ts +28 -0
  11. package/dist/configuration/config.svelte.js +28 -0
  12. package/dist/configuration/themes/dark.js +4 -0
  13. package/dist/configuration/themes/index.d.ts +112 -0
  14. package/dist/index.d.ts +26 -16
  15. package/dist/index.js +11 -3
  16. package/dist/layout/coordinates/CoordinatesLayout.svelte +93 -0
  17. package/dist/layout/{plot/RuleLayout.svelte.d.ts → coordinates/CoordinatesLayout.svelte.d.ts} +13 -16
  18. package/dist/layout/coordinates/margins.d.ts +16 -0
  19. package/dist/layout/coordinates/margins.js +11 -0
  20. package/dist/{plots/geo → layout/coordinates}/projections/argentina.d.ts +1 -1
  21. package/dist/layout/{geo → coordinates}/resolveProjection.d.ts +1 -1
  22. package/dist/layout/coordinates/resolveProjection.js +24 -0
  23. package/dist/{plots/geo → layout/coordinates}/zoom.svelte.d.ts +1 -1
  24. package/dist/layout/facet/FacetIndicator.svelte +31 -0
  25. package/dist/layout/facet/FacetIndicator.svelte.d.ts +8 -0
  26. package/dist/layout/facet/FacetLayout.svelte +249 -24
  27. package/dist/layout/facet/FacetLayout.svelte.d.ts +7 -1
  28. package/dist/layout/geometry/GeometryLayout.svelte +231 -0
  29. package/dist/layout/geometry/GeometryLayout.svelte.d.ts +62 -0
  30. package/dist/layout/legend/ContinuousSection.svelte +1 -1
  31. package/dist/layout/legend/ContinuousSection.svelte.d.ts +1 -1
  32. package/dist/layout/legend/DiscreteSection.svelte +1 -1
  33. package/dist/layout/legend/DiscreteSection.svelte.d.ts +1 -1
  34. package/dist/layout/legend/LegendLayout.svelte +1 -1
  35. package/dist/layout/legend/LegendLayout.svelte.d.ts +1 -1
  36. package/dist/layout/legend/interaction.svelte.d.ts +9 -1
  37. package/dist/layout/legend/interaction.svelte.js +9 -1
  38. package/dist/layout/plot/BasePlotLayout.svelte +142 -155
  39. package/dist/layout/plot/BasePlotLayout.svelte.d.ts +61 -51
  40. package/dist/layout/plot/margins.d.ts +0 -16
  41. package/dist/layout/plot/margins.js +0 -11
  42. package/dist/layout/{plot/AxisLayout.svelte → scales/ScalesLayout.svelte} +23 -16
  43. package/dist/layout/{plot/AxisLayout.svelte.d.ts → scales/ScalesLayout.svelte.d.ts} +4 -5
  44. package/dist/layout/scales/buildScale.d.ts +8 -0
  45. package/dist/layout/scales/buildScale.js +43 -0
  46. package/dist/layout/styles/StylesLayout.svelte +75 -0
  47. package/dist/layout/styles/StylesLayout.svelte.d.ts +15 -0
  48. package/dist/{plots/geo → layout/styles}/TileLayer.svelte +2 -2
  49. package/dist/{plots/geo → layout/styles}/TileLayer.svelte.d.ts +1 -1
  50. package/dist/layout/timeline/Timeline.svelte +1 -1
  51. package/dist/layout/timeline/Timeline.svelte.d.ts +1 -1
  52. package/dist/layout/timeline/TimelineLayout.svelte +1 -1
  53. package/dist/layout/timeline/TimelineLayout.svelte.d.ts +1 -1
  54. package/dist/layout/tooltip/Tooltip.svelte +16 -20
  55. package/dist/layout/tooltip/Tooltip.svelte.d.ts +5 -0
  56. package/dist/layout/tooltip/TooltipLayout.svelte +7 -1
  57. package/dist/layout/tooltip/TooltipLayout.svelte.d.ts +4 -0
  58. package/dist/layout/tooltip/controller.svelte.d.ts +4 -5
  59. package/dist/layout/tooltip/controller.svelte.js +42 -16
  60. package/dist/layout/tooltip/resolveTooltipPlacement.d.ts +24 -0
  61. package/dist/layout/tooltip/resolveTooltipPlacement.js +49 -0
  62. package/dist/layout/tooltip/utils.d.ts +27 -19
  63. package/dist/layout/tooltip/utils.js +16 -15
  64. package/dist/markers/BarMarker.svelte +92 -0
  65. package/dist/markers/BarMarker.svelte.d.ts +7 -0
  66. package/dist/markers/CellMarker.svelte +21 -0
  67. package/dist/markers/CellMarker.svelte.d.ts +7 -0
  68. package/dist/markers/ContourMarker.svelte +28 -0
  69. package/dist/markers/ContourMarker.svelte.d.ts +7 -0
  70. package/dist/markers/DeltaMarker.svelte +32 -39
  71. package/dist/markers/DeltaMarker.svelte.d.ts +4 -5
  72. package/dist/markers/DotMarker.svelte +35 -6
  73. package/dist/markers/DotMarker.svelte.d.ts +11 -2
  74. package/dist/markers/GeoFeaturesMarker.svelte +19 -0
  75. package/dist/markers/GeoFeaturesMarker.svelte.d.ts +7 -0
  76. package/dist/markers/GeoHoverMarker.svelte +25 -0
  77. package/dist/markers/GeoHoverMarker.svelte.d.ts +9 -0
  78. package/dist/markers/GraticuleMarker.svelte +18 -0
  79. package/dist/markers/GraticuleMarker.svelte.d.ts +7 -0
  80. package/dist/markers/HoverMarker.svelte +34 -35
  81. package/dist/markers/HoverMarker.svelte.d.ts +15 -9
  82. package/dist/markers/LineMarker.svelte +19 -0
  83. package/dist/markers/LineMarker.svelte.d.ts +7 -0
  84. package/dist/markers/LinkMarker.svelte +42 -0
  85. package/dist/markers/LinkMarker.svelte.d.ts +7 -0
  86. package/dist/markers/RuleMarker.svelte +24 -0
  87. package/dist/markers/RuleMarker.svelte.d.ts +10 -0
  88. package/dist/markers/SphereMarker.svelte +15 -0
  89. package/dist/markers/SphereMarker.svelte.d.ts +7 -0
  90. package/dist/markers/TextMarker.svelte +1 -1
  91. package/dist/markers/TextMarker.svelte.d.ts +1 -1
  92. package/dist/markers/VectorMarker.svelte +20 -0
  93. package/dist/markers/VectorMarker.svelte.d.ts +7 -0
  94. package/dist/markers/inset/InsetMarker.svelte +38 -45
  95. package/dist/markers/inset/InsetMarker.svelte.d.ts +6 -2
  96. package/dist/plots/bar/Plot.svelte +96 -87
  97. package/dist/plots/bar/Plot.svelte.d.ts +4 -5
  98. package/dist/plots/bar/ValueLabels.svelte +70 -24
  99. package/dist/plots/bar/ValueLabels.svelte.d.ts +11 -2
  100. package/dist/plots/bar/buildBarMarkers.d.ts +30 -0
  101. package/dist/plots/bar/buildBarMarkers.js +100 -0
  102. package/dist/plots/bar/layout.svelte.d.ts +1 -2
  103. package/dist/plots/bar/layout.svelte.js +1 -1
  104. package/dist/plots/geo/Plot.svelte +113 -387
  105. package/dist/plots/geo/Plot.svelte.d.ts +5 -4
  106. package/dist/plots/geo/buildGeoMarkers.d.ts +33 -0
  107. package/dist/plots/geo/buildGeoMarkers.js +47 -0
  108. package/dist/plots/heatmap/Plot.svelte +27 -52
  109. package/dist/plots/heatmap/Plot.svelte.d.ts +4 -5
  110. package/dist/plots/heatmap/ValueLabels.svelte +5 -5
  111. package/dist/plots/heatmap/ValueLabels.svelte.d.ts +3 -3
  112. package/dist/plots/heatmap/buildCellMarkers.d.ts +21 -0
  113. package/dist/plots/heatmap/buildCellMarkers.js +25 -0
  114. package/dist/plots/line/Plot.svelte +73 -141
  115. package/dist/plots/line/Plot.svelte.d.ts +4 -4
  116. package/dist/plots/line/ValueLabels.svelte +84 -85
  117. package/dist/plots/line/ValueLabels.svelte.d.ts +10 -1
  118. package/dist/plots/line/buildLineMarkers.d.ts +15 -0
  119. package/dist/plots/line/buildLineMarkers.js +57 -0
  120. package/dist/plots/pyramid/Plot.svelte +80 -121
  121. package/dist/plots/pyramid/Plot.svelte.d.ts +4 -5
  122. package/dist/plots/pyramid/ValueLabels.svelte +71 -25
  123. package/dist/plots/pyramid/ValueLabels.svelte.d.ts +11 -2
  124. package/dist/plots/pyramid/buildPyramidBarMarkers.d.ts +19 -0
  125. package/dist/plots/pyramid/buildPyramidBarMarkers.js +32 -0
  126. package/dist/plots/scatter/Plot.svelte +242 -0
  127. package/dist/{layout/plot/GridLayout.svelte.d.ts → plots/scatter/Plot.svelte.d.ts} +9 -7
  128. package/dist/plots/scatter/ValueLabels.svelte +149 -0
  129. package/dist/plots/{bar/BarSegments.svelte.d.ts → scatter/ValueLabels.svelte.d.ts} +17 -15
  130. package/dist/plots/scatter/buildScatterMarkers.d.ts +11 -0
  131. package/dist/plots/scatter/buildScatterMarkers.js +37 -0
  132. package/dist/plots/scatter/resolveRadiusPaddedDomain.d.ts +10 -0
  133. package/dist/plots/scatter/resolveRadiusPaddedDomain.js +23 -0
  134. package/dist/plots/utils/declutter.d.ts +55 -37
  135. package/dist/plots/utils/declutter.js +79 -64
  136. package/dist/plots/utils/geoAccessors.d.ts +1 -1
  137. package/dist/plots/utils/geoAccessors.js +1 -1
  138. package/dist/plots/utils/labelOverflow.svelte.d.ts +51 -0
  139. package/dist/plots/utils/labelOverflow.svelte.js +116 -0
  140. package/dist/plots/utils/legendDisabled.d.ts +1 -1
  141. package/dist/plots/utils/segments.d.ts +17 -10
  142. package/dist/plots/utils/segments.js +26 -16
  143. package/dist/types/charts/common.d.ts +3 -50
  144. package/dist/types/charts/props.d.ts +8 -8
  145. package/dist/types/configuration/styling.d.ts +26 -2
  146. package/dist/types/{plots/scales/geo.d.ts → layout/coordinates.d.ts} +33 -1
  147. package/dist/types/layout/facet.d.ts +41 -0
  148. package/dist/types/layout/geometry.d.ts +17 -0
  149. package/dist/types/layout/legend.d.ts +65 -1
  150. package/dist/types/{plots/axis.d.ts → layout/scales.d.ts} +11 -0
  151. package/dist/types/layout/styles.d.ts +115 -0
  152. package/dist/types/layout/timeline.d.ts +32 -0
  153. package/dist/types/layout/tooltip.d.ts +20 -1
  154. package/dist/types/markers/all.d.ts +11 -0
  155. package/dist/types/markers/all.js +1 -0
  156. package/dist/types/markers/base.d.ts +45 -0
  157. package/dist/types/markers/base.js +1 -0
  158. package/dist/types/markers/common.d.ts +64 -16
  159. package/dist/types/markers/geo.d.ts +75 -100
  160. package/dist/types/markers/line.d.ts +34 -0
  161. package/dist/types/markers/line.js +1 -0
  162. package/dist/types/markers/props.d.ts +1 -1
  163. package/dist/types/plots/data/common.d.ts +1 -1
  164. package/dist/types/plots/data/scatter.d.ts +13 -0
  165. package/dist/types/plots/data/scatter.js +1 -0
  166. package/dist/types/plots/props.d.ts +46 -74
  167. package/dist/types/plots/segments/common.d.ts +6 -12
  168. package/dist/types/plots/segments/config.d.ts +1 -1
  169. package/dist/types/plots/styling.d.ts +10 -0
  170. package/dist/{plots/utils → utils}/delta.d.ts +2 -2
  171. package/dist/{plots/utils → utils}/delta.js +1 -1
  172. package/dist/utils/edgeAwareAnchor.d.ts +37 -0
  173. package/dist/utils/edgeAwareAnchor.js +65 -0
  174. package/dist/utils/grouping.d.ts +1 -1
  175. package/dist/utils/interpolate.d.ts +2 -2
  176. package/package.json +3 -1
  177. package/dist/layout/geo/GeoLayout.svelte +0 -51
  178. package/dist/layout/geo/GeoLayout.svelte.d.ts +0 -24
  179. package/dist/layout/geo/resolveProjection.js +0 -48
  180. package/dist/layout/plot/GridLayout.svelte +0 -30
  181. package/dist/layout/plot/RuleLayout.svelte +0 -111
  182. package/dist/plots/bar/BarSegments.svelte +0 -69
  183. package/dist/types/charts/legend.d.ts +0 -60
  184. package/dist/types/plots/delta.d.ts +0 -19
  185. package/dist/types/plots/styles/common.d.ts +0 -52
  186. package/dist/types/plots/styles/geo.d.ts +0 -42
  187. /package/dist/{plots/geo → layout/coordinates}/projections/argentina.js +0 -0
  188. /package/dist/{plots/geo → layout/coordinates}/projections/fit.d.ts +0 -0
  189. /package/dist/{plots/geo → layout/coordinates}/projections/fit.js +0 -0
  190. /package/dist/{plots/geo → layout/coordinates}/rotate.svelte.d.ts +0 -0
  191. /package/dist/{plots/geo → layout/coordinates}/rotate.svelte.js +0 -0
  192. /package/dist/{plots/geo → layout/coordinates}/zoom.svelte.js +0 -0
  193. /package/dist/types/{charts/legend.js → layout/coordinates.js} +0 -0
  194. /package/dist/types/{plots/axis.js → layout/facet.js} +0 -0
  195. /package/dist/types/{plots/delta.js → layout/geometry.js} +0 -0
  196. /package/dist/types/{plots/scales/geo.js → layout/scales.js} +0 -0
  197. /package/dist/types/{plots/styles/common.js → layout/styles.js} +0 -0
  198. /package/dist/types/{plots/styles/geo.js → layout/timeline.js} +0 -0
@@ -1,54 +1,42 @@
1
1
  <script lang="ts" generics="TProps extends Record<string, unknown>">
2
2
  import { SvelteMap } from 'svelte/reactivity';
3
- import { Geo, Graticule, Sphere, Dot, Vector, Contour } from 'svelteplot';
4
3
  import { resolveAccessor } from '../utils/accessors';
5
4
  import { resolveFeatureStyle } from '../utils/geoSegments';
6
- import {
7
- geometryCentroid,
8
- geometryLonLatBounds,
9
- geometryPartCentroids,
10
- filterGeometryParts,
11
- defaultFeatureId,
12
- greatCircleInterpolate,
13
- } from '../utils/geoAccessors';
5
+ import { geometryCentroid, filterGeometryParts, defaultFeatureId } from '../utils/geoAccessors';
14
6
  import { isTopology, topologyToGeoJson } from '../utils/topojson';
15
7
  import { makeColorScale } from '../../utils/color';
16
8
  import { getConfiguration } from '../../configuration/config.svelte';
17
9
  import { getLegendInteraction } from '../../layout/legend/interaction.svelte';
18
10
  import { disabledFillOverride } from '../utils/legendDisabled';
19
- import { GEO_MARGIN_ESTIMATE } from '../../layout/plot/margins';
11
+ import { GEO_MARGIN_ESTIMATE } from '../../layout/coordinates/margins';
12
+ import { buildGeoMarkers } from './buildGeoMarkers';
13
+ import { hasHoverMarker } from '../../layout/tooltip/utils';
20
14
  import BasePlotLayout from '../../layout/plot/BasePlotLayout.svelte';
21
- import GeoLayout from '../../layout/geo/GeoLayout.svelte';
22
- import { resolveProjection } from '../../layout/geo/resolveProjection';
23
- import HoverMarker from '../../markers/HoverMarker.svelte';
24
- import { createGeoZoom } from './zoom.svelte';
25
- import { createGeoRotate, type GeoRotate } from './rotate.svelte';
26
- import InsetMarker from '../../markers/inset/InsetMarker.svelte';
27
- import ContourClipMarker from '../../markers/ContourClipMarker.svelte';
28
- import type { BasePlotProps, GeoScalesConfig } from '../../types/plots/props';
15
+ import type { BasePlotProps } from '../../types/plots/props';
16
+ import type { GeoScalesConfig, CoordinatesConfig } from '../../types/layout/coordinates';
29
17
  import type { GeoFeature, GeoDataProps } from '../../types/plots/data/geo';
30
- import type { GeoStyles } from '../../types/plots/styles/geo';
18
+ import type { GeoStylesConfig } from '../../types/layout/styles';
31
19
  import type { GeoSegmentStyle } from '../../types/plots/segments/common';
32
- import type { GeoMarker, GeoHoverMarkerConfig, GeoDotMarker, GeoInsetMarker } from '../../types/markers/geo';
20
+ import type { GeoMarkersConfig } from '../../types/markers/geo';
33
21
 
34
22
  /**
35
- * A map, rendered from GeoJSON (or, optionally, TopoJSON see
36
- * `$lib/plots/utils/topojson`). Unlike the other plot kinds there's no x/y/z
37
- * axis pair: `data` IS the map. `segments` styles features by id (e.g.
38
- * `segments.argentina`), `value` + `scales.color`/`styles.colors` drive a
39
- * continuous choropleth ramp (mirrors heatmap's `z`/`colors` pattern), and
40
- * everything drawn on top — points, arrows, geodesic links, contours,
41
- * graticule/sphere — is a `markers` entry.
23
+ * A map, rendered from GeoJSON or TopoJSON. Unlike the other plot kinds
24
+ * there's no x/y/z axis pair: `data` IS the map. `segments` styles
25
+ * features by id; `value` + `scales.color`/`styles.colors` drive a
26
+ * continuous choropleth ramp. Everything drawn on top — points, arrows,
27
+ * links, contours, graticule/sphere is a `markers` entry, assembled by
28
+ * `buildGeoMarkers` and routed through `GeometryLayout`.
42
29
  */
43
30
  type TaggedFeature = GeoFeature<TProps> & { __id: string; __cx: number; __cy: number };
44
31
 
45
32
  type Props = BasePlotProps<
46
33
  GeoDataProps<TProps>,
47
34
  GeoFeature<TProps>,
48
- GeoStyles<TProps>,
35
+ GeoStylesConfig<TProps>,
49
36
  GeoSegmentStyle,
50
- GeoMarker,
51
- GeoScalesConfig
37
+ GeoMarkersConfig,
38
+ GeoScalesConfig,
39
+ CoordinatesConfig
52
40
  >;
53
41
 
54
42
  let {
@@ -63,6 +51,7 @@
63
51
  segments = {},
64
52
  markers = [],
65
53
  scales = {},
54
+ coordinates = {},
66
55
  margins,
67
56
  tooltip = undefined,
68
57
  }: Props = $props();
@@ -110,18 +99,12 @@
110
99
  );
111
100
 
112
101
  /**
113
- * Tags each feature with a plain enumerable `__id`/`__cx`/`__cy` by
114
- * spreading into a fresh object (never mutating the original Svelte 5
115
- * forbids mutating state read inside a `$derived`, and `data` may already
116
- * be a reactive proxy) required because svelteplot's `<Pointer>`
117
- * shallow-clones matched rows, so per-row metadata needed at hover-match
118
- * time must live as an own enumerable property on the object itself, not a
119
- * side `Map` keyed by identity.
120
- *
121
- * Untouched by `excludeParts` — this is the full geometry, passed to
122
- * `InsetMarker` as-is, so an inset can show a part regardless of whether
123
- * `excludeParts` also hides that same part from the main map's own
124
- * rendering below (see `features`).
102
+ * Tags each feature with a plain enumerable `__id`/`__cx`/`__cy`, so
103
+ * per-row metadata (feature id for style/hover lookups, centroid for
104
+ * positioning) lives as an own property on the object itself rather than a
105
+ * side lookup. Untouched by `excludeParts`: passed to `InsetMarker` as-is,
106
+ * so an inset can show a part the main map's own rendering (`features`,
107
+ * below) excludes.
125
108
  */
126
109
  const allFeatures = $derived.by((): TaggedFeature[] =>
127
110
  rawFeatures.map((f) => {
@@ -132,8 +115,9 @@
132
115
 
133
116
  /**
134
117
  * `allFeatures`, reduced per `excludeParts` — this is what actually
135
- * renders (`styleGroups`/`<Geo>` below) and what `hoverCandidates` derives
136
- * from. A feature reduced to no parts at all is dropped entirely.
118
+ * renders (`styleGroups`/the base `'geo'` marker below) and what
119
+ * `hoverPoints` derives from. A feature reduced to no parts at all is
120
+ * dropped entirely.
137
121
  */
138
122
  const features = $derived.by((): TaggedFeature[] => {
139
123
  if (!excludeParts) return allFeatures;
@@ -148,7 +132,7 @@
148
132
  // ── Tile-layer / projection compatibility ────────────────────────────────
149
133
  $effect(() => {
150
134
  if (!styles.tileLayer) return;
151
- const proj = scales.projection;
135
+ const proj = coordinates.projection;
152
136
  const name = typeof proj === 'string' ? proj : proj?.type;
153
137
  if (name && name !== 'mercator' && name !== 'transverse-mercator') {
154
138
  console.warn(
@@ -157,22 +141,28 @@
157
141
  }
158
142
  });
159
143
 
160
- const dataLonLatBounds = $derived.by((): [number, number, number, number] | null => {
161
- if (!styles.tileLayer) return null;
162
- let minLon = Infinity;
163
- let maxLon = -Infinity;
164
- let minLat = Infinity;
165
- let maxLat = -Infinity;
166
- for (const f of features) {
167
- const b = geometryLonLatBounds(f.geometry);
168
- if (!b) continue;
169
- if (b[0] < minLon) minLon = b[0];
170
- if (b[2] > maxLon) maxLon = b[2];
171
- if (b[1] < minLat) minLat = b[1];
172
- if (b[3] > maxLat) maxLat = b[3];
173
- }
174
- return minLon === Infinity ? null : [minLon, minLat, maxLon, maxLat];
175
- });
144
+ /**
145
+ * `coordinates.projection`, defaulted to `'mercator'` when unset and
146
+ * `styles.tileLayer` is active (the only projection family `TileLayer`
147
+ * renders correctly under) — folded in here, before `coordinates` reaches
148
+ * `BasePlotLayout`/`CoordinatesLayout`, since neither knows about
149
+ * `styles.tileLayer`.
150
+ */
151
+ const resolvedCoordinates = $derived.by((): CoordinatesConfig => ({
152
+ ...coordinates,
153
+ projection: coordinates.projection ?? (styles.tileLayer ? 'mercator' : undefined),
154
+ }));
155
+
156
+ /**
157
+ * `styles`, with `features` tacked onto `tileLayer` (never author-facing —
158
+ * `GeoTileLayerConfig` doesn't declare it) so `StylesLayout` can compute
159
+ * the tile layer's auto-fit bounds without raw geometry threaded through
160
+ * as a separate prop. `excludeParts`-reduced `features`, matching what
161
+ * actually renders.
162
+ */
163
+ const resolvedStyles = $derived.by(() =>
164
+ styles.tileLayer ? { ...styles, tileLayer: { ...styles.tileLayer, features } } : styles,
165
+ );
176
166
 
177
167
  // ── Choropleth color scale (mirrors heatmap's styles.colors/scales.z) ────
178
168
  const minColor = $derived(styles.colors?.min ?? cfg.continuous.min);
@@ -213,23 +203,8 @@
213
203
  return { ...base, ...disabledFillOverride(r.disabledStyle) };
214
204
  }
215
205
 
216
- /** Per-marker color ramp for a `dot` marker's `value` channel — independent of the base map's own choropleth ramp. */
217
- function makeDotColorScale(marker: GeoDotMarker): ((v: number) => string) | null {
218
- if (!marker.value) return null;
219
- const getValue = resolveAccessor(marker.value);
220
- const vals = marker.data.map((d) => Number(getValue(d))).filter((v) => Number.isFinite(v));
221
- if (!vals.length) return null;
222
- const domain: [number, number] = [Math.min(...vals), Math.max(...vals)];
223
- return makeColorScale(
224
- domain[0],
225
- domain[1],
226
- marker.style?.colors?.min ?? cfg.continuous.min,
227
- marker.style?.colors?.max ?? cfg.continuous.max,
228
- );
229
- }
230
-
231
206
  // ── Per-feature style resolution, grouped by distinct resolved style ─────
232
- // (one <Geo> call per group — mirrors heatmap's styleGroups: svelteplot's
207
+ // (one `'geo'` marker per group — mirrors heatmap's styleGroups: svelteplot's
233
208
  // shared numeric-scale machinery collapses a per-row fillOpacity/strokeWidth
234
209
  // *accessor* to a single value, so literal per-call constants are required.)
235
210
  function getFeatureStyle(f: TaggedFeature): GeoSegmentStyle {
@@ -252,22 +227,10 @@
252
227
  return [...groups.values()];
253
228
  });
254
229
 
255
- /**
256
- * Hover/click candidates — like `features`, but one row per disjoint
257
- * geometry part instead of per feature, so a multi-part feature's hover
258
- * point isn't stranded somewhere between its parts (see
259
- * `geometryPartCentroids`). Only used for hover matching (`BasePlotLayout`'s
260
- * `data`/`hoverPoints`) — `<Geo>` itself still renders from
261
- * `features`/`styleGroups`, one entry per feature, unaffected by this.
262
- */
263
- const hoverCandidates = $derived.by((): TaggedFeature[] =>
264
- features.flatMap((f) => geometryPartCentroids(f.geometry).map(([cx, cy]) => ({ ...f, __cx: cx, __cy: cy }))),
265
- );
266
-
267
230
  // A feature outside the active range is dimmed in place, but never
268
231
  // surfaces in hover/tooltip — it's simply absent from the candidate list.
269
232
  const hoverPoints = $derived(
270
- hoverCandidates
233
+ features
271
234
  .filter((f) => {
272
235
  const r = range;
273
236
  if (r == null || !value) return true;
@@ -280,323 +243,86 @@
280
243
  const hoverGetY = (f: TaggedFeature) => f.__cy;
281
244
 
282
245
  /**
283
- * `markers` otherwise always render after (on top of) the base map's own
284
- * `styleGroups`, regardless of array order a marker opts into drawing
285
- * underneath it instead by setting `behind: true` (see e.g.
286
- * `GeoSphereMarker.behind`).
287
- */
288
- function isBehindMarker(marker: GeoMarker): boolean {
289
- return (
290
- (marker.type === 'graticule' || marker.type === 'sphere' || marker.type === 'contour') &&
291
- marker.behind === true
292
- );
293
- }
294
-
295
- const hoverMarkerConfig = $derived(markers.find((m): m is GeoHoverMarkerConfig => m.type === 'hover'));
296
-
297
- $effect(() => {
298
- if (hoverMarkerConfig?.strategy === 'nearest-point') {
299
- console.warn(
300
- "GeoPlot: the hover marker's strategy 'nearest-point' isn't implemented yet — falling back to 'nearest-feature'.",
301
- );
302
- }
303
- });
304
-
305
- const hoverActive = $derived(tooltip != null || hoverMarkerConfig != null);
306
-
307
- // ── Projection ────────────────────────────────────────────────────────────
308
- const resolvedProjection = $derived(
309
- resolveProjection(scales.projection ?? (styles.tileLayer ? 'mercator' : undefined), hoverCandidates),
310
- );
311
-
312
- // ── Pan / zoom / rotate — owned here (not GeoLayout) since the resulting
313
- // rotated projection must reach `BasePlotLayout`'s own `<Plot projection>`
314
- // prop, and a component nested *inside* that `<Plot>` (as GeoLayout is)
315
- // has no way to reach back out to a prop on its own ancestor. GeoLayout
316
- // only renders the *result* (`zoomTransform`) as its `<g transform>`. ────
317
- let containerEl = $state<HTMLDivElement>();
318
- const zoomEnabled = $derived(styles.zoom !== false && styles.zoom != null);
319
- const zoomConfig = $derived(typeof styles.zoom === 'object' ? styles.zoom : undefined);
320
-
321
- /**
322
- * Orthographic (and other azimuthal, hemisphere-style) projections don't
323
- * gain anything from drag-to-*pan* — panning just slides the same globe a
324
- * few pixels within the frame — so when `zoom` is enabled on one, drag
325
- * instead rotates the globe (`createGeoRotate`) and wheel/pinch keeps
326
- * scaling it (`createGeoZoom`, with its own drag disabled below so the two
327
- * gestures don't fight over the same pointer events).
246
+ * `hoverPoints` keyed by feature `__id`, looked up from the exact feature
247
+ * the browser's own SVG hit-testing resolved a pointer event to (see the
248
+ * base `'geo'` marker's `onpointerenter`, built by `buildGeoMarkers`). A
249
+ * feature absent here was filtered out of `hoverPoints` and correctly
250
+ * produces no match.
328
251
  */
329
- const projectionType = $derived(
330
- typeof resolvedProjection === 'string' ? resolvedProjection : resolvedProjection?.type,
331
- );
332
- const rotateEnabled = $derived(zoomEnabled && projectionType === 'orthographic');
333
- const baseRotate = $derived.by((): GeoRotate => {
334
- const configured = typeof resolvedProjection === 'object' ? resolvedProjection?.rotate : undefined;
335
- return [configured?.[0] ?? 0, configured?.[1] ?? 0, configured?.[2] ?? 0];
336
- });
337
-
338
- const geoZoom = createGeoZoom({
339
- enabled: () => zoomEnabled,
340
- config: () => zoomConfig,
341
- node: () => containerEl ?? null,
342
- disableDrag: () => rotateEnabled,
343
- });
344
-
345
- const geoRotate = createGeoRotate({
346
- enabled: () => rotateEnabled,
347
- baseRotate: () => baseRotate,
348
- scale: () => geoZoom.transform.k,
349
- node: () => containerEl ?? null,
252
+ const hoverPointById = $derived.by(() => {
253
+ const map = new SvelteMap<string, (typeof hoverPoints)[number]>();
254
+ for (const p of hoverPoints) map.set(p.row.__id, p);
255
+ return map;
350
256
  });
351
257
 
352
- const finalProjection = $derived.by(() => {
353
- if (!rotateEnabled) return resolvedProjection;
354
- return {
355
- ...(typeof resolvedProjection === 'string' ? { type: resolvedProjection } : resolvedProjection),
356
- rotate: geoRotate.rotate,
357
- };
258
+ const hoverActive = $derived(tooltip != null || hasHoverMarker(markers));
259
+
260
+ const hover = $derived({
261
+ active: hoverActive,
262
+ points: hoverPoints,
263
+ label: (f: TaggedFeature) => f.__id,
264
+ strategy: 'punctual' as const,
265
+ // A map has no axis to draw a crosshair rule against — 'punctual' here
266
+ // only means "exact XY hover match", unlike heatmap's own use of it.
267
+ crosshair: false,
268
+ sync: tooltip?.sync ?? false,
269
+ tooltip,
270
+ exclude: isOverInset,
271
+ // A feature's true visual center can sit far from where the cursor
272
+ // actually is on a large/irregular shape (e.g. an elongated province) —
273
+ // 'contains' matches the feature the cursor is really over, via the
274
+ // browser's own SVG hit-testing on each rendered feature path, rather
275
+ // than 'nearest' centroid distance.
276
+ pointMatching: 'contains' as const,
358
277
  });
359
278
 
360
279
  /**
361
- * This plot's own hover must never fire while the cursor is actually over
362
- * an `inset` marker's panel an inset draws its own content at its own
363
- * scale/projection, so the main map's `Pointer` has no way to know it's
364
- * there and would otherwise still match/show whatever main-map feature
365
- * happens to be nearest that screen position underneath the inset.
366
- *
367
- * Checked via `e.target` (which `InsetMarker`'s own hit-test `<rect>`, or
368
- * one of its feature `<path>`s, tags with the `dct-geo-inset` ancestor —
369
- * see `InsetMarker.svelte`) rather than by comparing pixel coordinates against a
370
- * precomputed inset box: svelteplot's `<Plot>` doesn't necessarily render
371
- * at a 1:1 scale between its `width`/`height` props and actual client
372
- * pixels (its internal `viewBox` can differ), which would silently desync
373
- * any independently-computed pixel math from what's really on screen. The
374
- * browser's own hit-testing has no such problem — it already resolves
375
- * `e.target` against whatever is actually painted, accounting for any such
376
- * scaling automatically.
280
+ * The main map's own hover must never fire while the cursor is over an
281
+ * `inset` marker's panel, which draws at its own scale/projection.
282
+ * Checked via `e.target` (tagged with the `dct-geo-inset` ancestor class
283
+ * by `InsetMarker`'s own hit-test elements) rather than precomputed pixel
284
+ * bounds, since the browser's own hit-testing already accounts for any
285
+ * scale mismatch between `<Plot>`'s props and its rendered viewBox.
377
286
  */
378
287
  function isOverInset(e: PointerEvent): boolean {
379
288
  return (e.target as Element | null)?.closest?.('.dct-geo-inset') != null;
380
289
  }
381
290
  </script>
382
291
 
383
- {#snippet marker_(marker: GeoMarker, numericMargins: { top: number; right: number; bottom: number; left: number })}
384
- {#if marker.type === 'graticule'}
385
- <Graticule
386
- step={marker.step}
387
- stepX={marker.stepX}
388
- stepY={marker.stepY}
389
- stroke={marker.style?.stroke ?? 'currentColor'}
390
- strokeWidth={marker.style?.strokeWidth ?? 0.5}
391
- strokeOpacity={marker.style?.strokeOpacity ?? 0.3}
392
- strokeDasharray={marker.style?.strokeDasharray}
393
- style="vector-effect: non-scaling-stroke"
394
- />
395
- {:else if marker.type === 'sphere'}
396
- <Sphere
397
- fill={marker.style?.fill}
398
- fillOpacity={marker.style?.fillOpacity}
399
- stroke={marker.style?.stroke ?? 'currentColor'}
400
- strokeWidth={marker.style?.strokeWidth ?? 1}
401
- style="vector-effect: non-scaling-stroke"
402
- />
403
- {:else if marker.type === 'dot'}
404
- {@const getDotValue = marker.value ? resolveAccessor(marker.value) : null}
405
- {@const dotColorScale = getDotValue ? makeDotColorScale(marker) : null}
406
- <Dot
407
- data={marker.data}
408
- x={marker.x}
409
- y={marker.y}
410
- r={marker.r ?? marker.style?.dotRadius ?? 4}
411
- fill={dotColorScale && getDotValue
412
- ? (d: Record<string, unknown>) => dotColorScale(Number(getDotValue(d)))
413
- : (marker.style?.dotFill ?? cfg.palette[0])}
414
- fillOpacity={marker.style?.dotFillOpacity ?? 1}
415
- stroke={marker.style?.dotStroke}
416
- strokeWidth={marker.style?.dotStrokeWidth}
417
- />
418
- {:else if marker.type === 'vector'}
419
- <Vector
420
- data={marker.data}
421
- x={marker.x}
422
- y={marker.y}
423
- length={marker.length}
424
- rotate={marker.rotate}
425
- shape={marker.shape ?? 'arrow'}
426
- anchor={marker.anchor}
427
- fill={marker.style?.fill ?? 'currentColor'}
428
- stroke={marker.style?.stroke}
429
- strokeWidth={marker.style?.strokeWidth}
430
- />
431
- {:else if marker.type === 'link'}
432
- {@const getX1 = resolveAccessor(marker.x1)}
433
- {@const getY1 = resolveAccessor(marker.y1)}
434
- {@const getX2 = resolveAccessor(marker.x2)}
435
- {@const getY2 = resolveAccessor(marker.y2)}
436
- {@const linkFeatures = marker.data.map((d) => ({
437
- type: 'Feature' as const,
438
- properties: {},
439
- geometry: {
440
- type: 'LineString' as const,
441
- coordinates:
442
- marker.curve === 'linear'
443
- ? [
444
- [Number(getX1(d)), Number(getY1(d))],
445
- [Number(getX2(d)), Number(getY2(d))],
446
- ]
447
- : greatCircleInterpolate(Number(getX1(d)), Number(getY1(d)), Number(getX2(d)), Number(getY2(d))),
448
- },
449
- }))}
450
- <!--
451
- No `fill` prop here, deliberately: svelteplot's channel resolution
452
- (`toChannelOption`) treats *any* string value — including the
453
- literal `'none'` this used to pass — as scale-worthy unless it
454
- parses as a real CSS color, so `fill="none"` was getting run
455
- through the plot's color scale and painted an arbitrary category
456
- color instead of staying unfilled (confirmed by inspecting the
457
- rendered DOM). Omitting the prop entirely leaves svelteplot's own
458
- per-mark default in place, which is `fill: none` for an unfilled
459
- channel — the outcome this mark actually wants, without the buggy
460
- detour through the color scale.
461
- -->
462
- <Geo
463
- class="link"
464
- data={linkFeatures}
465
- stroke={marker.style?.stroke ?? 'currentColor'}
466
- strokeWidth={marker.style?.strokeWidth ?? 1}
467
- strokeOpacity={marker.style?.strokeOpacity}
468
- strokeDasharray={marker.style?.strokeDasharray}
469
- style="vector-effect: non-scaling-stroke"
470
- />
471
- {:else if marker.type === 'contour'}
472
- <!--
473
- `fill`/`stroke`: no `?? 'none'` fallback here, deliberately — same
474
- bug as the `link` marker above. Omitting the prop when
475
- `marker.fill` is unset leaves svelteplot's own per-mark default in
476
- place (`fill="none"`, per Contour's own doc comment), instead of
477
- running the literal string `'none'` through the plot's color scale
478
- (`'none'` isn't a recognized CSS color, so svelteplot doesn't treat
479
- it as a literal output value the way it does e.g. `'currentColor'`).
480
- -->
481
- <ContourClipMarker mode={marker.clip} {features}>
482
- <Contour
483
- class="contour"
484
- data={marker.data}
485
- x={marker.x}
486
- y={marker.y}
487
- value={marker.value}
488
- thresholds={marker.thresholds}
489
- fill={marker.fill}
490
- stroke={marker.stroke ?? 'currentColor'}
491
- strokeWidth={marker.style?.strokeWidth}
492
- fillOpacity={marker.style?.fillOpacity}
493
- strokeOpacity={marker.style?.strokeOpacity}
494
- blur={marker.blur}
495
- interpolate={marker.interpolate}
496
- />
497
- </ContourClipMarker>
498
- {:else if marker.type === 'component'}
499
- <marker.component {...marker.props} data={marker.data ?? features} />
500
- {:else if marker.type === 'inset'}
501
- {@const insetContentBox = {
502
- left: numericMargins.left,
503
- top: numericMargins.top,
504
- width: Math.max(0, width - numericMargins.left - numericMargins.right),
505
- height: Math.max(0, height - numericMargins.top - numericMargins.bottom),
506
- }}
507
- <InsetMarker
508
- marker={marker as GeoInsetMarker<TProps>}
509
- features={allFeatures}
510
- width={insetContentBox.width}
511
- height={insetContentBox.height}
512
- left={insetContentBox.left}
513
- top={insetContentBox.top}
514
- defaultFill={cfg.palette[0]}
515
- />
516
- {/if}
517
- {/snippet}
518
-
519
292
  <!--
520
- `x`/`y`: pins the plot's generic linear x/y scales to the full lon/lat
521
- range. Every geo-aware mark (Geo, Dot, Vector, Link, …) bypasses these
522
- entirely when a projection is active, routing through it instead except
523
- svelteplot's own `Contour` mark, whose scatter/sampler-mode grid is
524
- hardcoded to read `plot.scales.x`/`y` (see `Contour.svelte`) regardless of
525
- any active projection. Left unset, that scale would auto-fit to just the
526
- contour data's own point extent — a different frame than the projection's
527
- own (typically whole-world) fit, so the contour would sit at the wrong
528
- scale/position relative to the map under it. Pinning both to the world
529
- range only helps `contour` markers align (best exactly under an
530
- `equirectangular` projection, whose x/y truly are linear in lon/lat) and
531
- is a no-op for every other mark type.
293
+ `x`/`y`: pins the plot's generic linear scales to the full lon/lat range.
294
+ Every geo-aware mark routes through the active projection instead, except
295
+ svelteplot's `Contour`, whose grid reads `plot.scales.x`/`y` directly
296
+ this keeps a `contour` marker aligned with the map under it.
532
297
  -->
533
298
  <BasePlotLayout
534
299
  {width}
535
300
  {height}
536
- data={hoverCandidates}
301
+ data={features}
537
302
  getX={hoverGetX}
538
303
  getY={hoverGetY}
539
- {hoverPoints}
540
- label={(f) => f.__id}
541
- {tooltip}
542
- {hoverActive}
543
- hoverStrategy="punctual"
544
- hoverSync={tooltip?.sync ?? false}
304
+ {hover}
545
305
  x={{ domain: [-180, 180] }}
546
306
  y={{ domain: [90, -90] }}
547
- projection={finalProjection}
307
+ coordinates={resolvedCoordinates}
548
308
  {margins}
549
309
  marginEstimate={GEO_MARGIN_ESTIMATE}
550
- excludeHover={isOverInset}
551
- touchAction={zoomEnabled ? 'none' : undefined}
552
- bind:containerEl
553
- >
554
- {#snippet children({ matchedPoints, numericMargins })}
555
- <GeoLayout
556
- {width}
557
- {height}
558
- {numericMargins}
559
- zoomTransform={geoZoom.transform}
560
- tileLayer={styles.tileLayer}
561
- {dataLonLatBounds}
562
- >
563
- {#each markers as marker, i (marker.type + '-' + i)}
564
- {#if isBehindMarker(marker)}
565
- {@render marker_(marker, numericMargins)}
566
- {/if}
567
- {/each}
568
-
569
- {#each styleGroups as group (JSON.stringify(group.style))}
570
- <Geo
571
- data={group.data}
572
- fill={(d: TaggedFeature) => group.style.fill ?? resolveBaseFill(d)}
573
- fillOpacity={group.style.fillOpacity ?? styles.fillOpacity ?? 1}
574
- stroke={group.style.stroke ?? styles.stroke ?? cfg.axis.color}
575
- strokeWidth={group.style.strokeWidth ?? styles.strokeWidth ?? 0.5}
576
- style="vector-effect: non-scaling-stroke"
577
- onclick={styles.onFeatureClick
578
- ? (e: Event, d: TaggedFeature) => styles.onFeatureClick?.(d, e)
579
- : undefined}
580
- />
581
- {/each}
582
-
583
- {#each markers as marker, i (marker.type + '-' + i)}
584
- {#if !isBehindMarker(marker)}
585
- {@render marker_(marker, numericMargins)}
586
- {/if}
587
- {/each}
588
- </GeoLayout>
589
-
590
- {#if hoverMarkerConfig}
591
- <HoverMarker
592
- data={matchedPoints}
593
- ruleX={null}
594
- ruleY={null}
595
- showLabels={hoverMarkerConfig.showLabels ?? true}
596
- format={hoverMarkerConfig.format}
597
- dotStyle={hoverMarkerConfig.dotStyle}
598
- fontStyle={hoverMarkerConfig.fontStyle}
599
- />
600
- {/if}
601
- {/snippet}
602
- </BasePlotLayout>
310
+ styles={resolvedStyles}
311
+ markers={({ setHoverMatch, clearHoverMatch }) =>
312
+ buildGeoMarkers({
313
+ styleGroups,
314
+ resolveBaseFill,
315
+ fillOpacity: styles.fillOpacity,
316
+ stroke: styles.stroke,
317
+ strokeWidth: styles.strokeWidth,
318
+ axisColor: cfg.axis.color,
319
+ onFeatureClick: styles.onFeatureClick,
320
+ markers,
321
+ hoverActive,
322
+ hoverPointById,
323
+ isOverInset,
324
+ setHoverMatch,
325
+ clearHoverMatch,
326
+ })}
327
+ geoAllFeatures={allFeatures}
328
+ />
@@ -1,10 +1,11 @@
1
- import type { BasePlotProps, GeoScalesConfig } from '../../types/plots/props';
1
+ import type { BasePlotProps } from '../../types/plots/props';
2
+ import type { GeoScalesConfig, CoordinatesConfig } from '../../types/layout/coordinates';
2
3
  import type { GeoFeature, GeoDataProps } from '../../types/plots/data/geo';
3
- import type { GeoStyles } from '../../types/plots/styles/geo';
4
+ import type { GeoStylesConfig } from '../../types/layout/styles';
4
5
  import type { GeoSegmentStyle } from '../../types/plots/segments/common';
5
- import type { GeoMarker } from '../../types/markers/geo';
6
+ import type { GeoMarkersConfig } from '../../types/markers/geo';
6
7
  declare function $$render<TProps extends Record<string, unknown>>(): {
7
- props: BasePlotProps<GeoDataProps<TProps>, GeoFeature<TProps>, GeoStyles<TProps>, GeoSegmentStyle, GeoMarker, GeoScalesConfig>;
8
+ props: BasePlotProps<GeoDataProps<TProps>, GeoFeature<TProps>, GeoStylesConfig<TProps>, GeoSegmentStyle, GeoMarkersConfig, GeoScalesConfig, CoordinatesConfig>;
8
9
  exports: {};
9
10
  bindings: "";
10
11
  slots: {};
@@ -0,0 +1,33 @@
1
+ import type { MarkerConfig } from '../../types/markers/all';
2
+ import type { GeoMarkersConfig } from '../../types/markers/geo';
3
+ import type { GeoSegmentStyle } from '../../types/plots/segments/common';
4
+ /** One resolved-style bucket of features, as grouped by `GeoPlot`'s own `styleGroups`. */
5
+ export type GeoStyleGroup<TRow> = {
6
+ style: GeoSegmentStyle;
7
+ data: TRow[];
8
+ };
9
+ /**
10
+ * Every marker for one `GeoPlot` render, in final draw order: any authored
11
+ * marker with `behind: true`, then the base map itself (one `'geo'` marker
12
+ * per resolved feature style), then the rest of the authored markers in
13
+ * order, with any `hover` marker moved last.
14
+ */
15
+ export declare function buildGeoMarkers<TRow extends Record<string, unknown> & {
16
+ __id: string;
17
+ }>(args: {
18
+ styleGroups: GeoStyleGroup<TRow>[];
19
+ resolveBaseFill: (d: TRow) => string;
20
+ fillOpacity?: number;
21
+ stroke?: string;
22
+ strokeWidth?: number;
23
+ axisColor: string;
24
+ onFeatureClick?: (d: TRow, e: Event) => void;
25
+ markers: GeoMarkersConfig[];
26
+ hoverActive: boolean;
27
+ hoverPointById: Map<string, {
28
+ row: TRow;
29
+ }>;
30
+ isOverInset: (e: PointerEvent) => boolean;
31
+ setHoverMatch: (rows: TRow[]) => void;
32
+ clearHoverMatch: () => void;
33
+ }): MarkerConfig[];