@fundar/data-chart-telling 0.0.13 → 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 -237
  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 -100
  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
@@ -0,0 +1,548 @@
1
+ <script lang="ts" generics="TProps extends Record<string, unknown>">
2
+ import { SvelteMap } from 'svelte/reactivity';
3
+ import { Geo, Graticule, Sphere, Dot, Vector, Contour } from 'svelteplot';
4
+ import { resolveAccessor } from '../utils/accessors';
5
+ import { resolveFeatureStyle } from '../utils/geoSegments';
6
+ import {
7
+ geometryCentroid,
8
+ geometryPartCentroids,
9
+ filterGeometryParts,
10
+ defaultFeatureId,
11
+ greatCircleInterpolate,
12
+ } from '../utils/geoAccessors';
13
+ import { isTopology, topologyToGeoJson } from '../utils/topojson';
14
+ import { makeColorScale } from '../../utils/color';
15
+ import { getConfiguration } from '../../configuration/config.svelte';
16
+ import { GEO_MARGIN_ESTIMATE } from '../../layout/plot/margins';
17
+ import BasePlotLayout from '../../layout/plot/BasePlotLayout.svelte';
18
+ import GeoLayout from '../../layout/geo/GeoLayout.svelte';
19
+ import { resolveProjection } from '../../layout/geo/resolveProjection';
20
+ import HoverMarker from '../../markers/HoverMarker.svelte';
21
+ import { createGeoZoom } from './zoom.svelte';
22
+ import { createGeoRotate, type GeoRotate } from './rotate.svelte';
23
+ import InsetMarker from '../../markers/inset/InsetMarker.svelte';
24
+ import ContourClipMarker from '../../markers/ContourClipMarker.svelte';
25
+ import type { BasePlotProps, GeoScalesConfig } from '../../types/plots/props';
26
+ import type { GeoFeature, GeoDataProps } from '../../types/plots/data/geo';
27
+ import type { GeoStyles } from '../../types/plots/styles/geo';
28
+ import type { GeoSegmentStyle } from '../../types/plots/segments/common';
29
+ import type { GeoMarker, GeoHoverMarkerConfig, GeoDotMarker, GeoInsetMarker } from '../../types/markers/geo';
30
+
31
+ /**
32
+ * A map, rendered from GeoJSON (or, optionally, TopoJSON — see
33
+ * `$lib/plots/utils/topojson`). Unlike the other plot kinds there's no x/y/z
34
+ * axis pair: `data` IS the map. `segments` styles features by id (e.g.
35
+ * `segments.argentina`), `value` + `scales.color`/`styles.colors` drive a
36
+ * continuous choropleth ramp (mirrors heatmap's `z`/`colors` pattern), and
37
+ * everything drawn on top — points, arrows, geodesic links, contours,
38
+ * graticule/sphere — is a `markers` entry.
39
+ */
40
+ type TaggedFeature = GeoFeature<TProps> & { __id: string; __cx: number; __cy: number };
41
+
42
+ type Props = BasePlotProps<
43
+ GeoDataProps<TProps>,
44
+ GeoFeature<TProps>,
45
+ GeoStyles<TProps>,
46
+ GeoSegmentStyle,
47
+ GeoMarker,
48
+ GeoScalesConfig
49
+ >;
50
+
51
+ let {
52
+ data,
53
+ object,
54
+ featureId,
55
+ value,
56
+ excludeParts,
57
+ width,
58
+ height,
59
+ styles = {},
60
+ segments = {},
61
+ markers = [],
62
+ scales = {},
63
+ margins,
64
+ tooltip = undefined,
65
+ }: Props = $props();
66
+
67
+ const cfg = $derived(getConfiguration());
68
+
69
+ // ── Resolve data: GeoJSON is synchronous, TopoJSON is converted async ────
70
+ let topoFeatures = $state<GeoFeature<TProps>[] | null>(null);
71
+
72
+ $effect(() => {
73
+ if (!isTopology(data)) return;
74
+ if (!object) {
75
+ console.warn('GeoPlot: `data` is a TopoJSON Topology but no `object` key was given — nothing will render.');
76
+ topoFeatures = [];
77
+ return;
78
+ }
79
+ let cancelled = false;
80
+ topologyToGeoJson<TProps>(data, object)
81
+ .then((resolved) => {
82
+ if (!cancelled) topoFeatures = resolved;
83
+ })
84
+ .catch((err) => {
85
+ if (!cancelled) {
86
+ console.warn(err instanceof Error ? err.message : String(err));
87
+ topoFeatures = [];
88
+ }
89
+ });
90
+ return () => {
91
+ cancelled = true;
92
+ };
93
+ });
94
+
95
+ const rawFeatures = $derived.by((): GeoFeature<TProps>[] => {
96
+ if (isTopology(data)) return topoFeatures ?? [];
97
+ if (Array.isArray(data)) return data;
98
+ // `FeatureCollection.features` is GeoJSON's own `Feature<G, P>[]`, without
99
+ // the `Record<string, unknown>` intersection `GeoFeature` adds to satisfy
100
+ // this package's generic row-type bound — safe to assert, since every
101
+ // property `GeoFeature` promises is already present.
102
+ return data.features as GeoFeature<TProps>[];
103
+ });
104
+
105
+ const getFeatureId = $derived(
106
+ featureId ? resolveAccessor<GeoFeature<TProps>>(featureId) : defaultFeatureId<TProps>,
107
+ );
108
+
109
+ /**
110
+ * Tags each feature with a plain enumerable `__id`/`__cx`/`__cy` by
111
+ * spreading into a fresh object (never mutating the original — Svelte 5
112
+ * forbids mutating state read inside a `$derived`, and `data` may already
113
+ * be a reactive proxy) — required because svelteplot's `<Pointer>`
114
+ * shallow-clones matched rows, so per-row metadata needed at hover-match
115
+ * time must live as an own enumerable property on the object itself, not a
116
+ * side `Map` keyed by identity.
117
+ *
118
+ * Untouched by `excludeParts` — this is the full geometry, passed to
119
+ * `InsetMarker` as-is, so an inset can show a part regardless of whether
120
+ * `excludeParts` also hides that same part from the main map's own
121
+ * rendering below (see `features`).
122
+ */
123
+ const allFeatures = $derived.by((): TaggedFeature[] =>
124
+ rawFeatures.map((f) => {
125
+ const [cx, cy] = geometryCentroid(f.geometry);
126
+ return { ...f, __id: getFeatureId(f), __cx: cx, __cy: cy } as TaggedFeature;
127
+ }),
128
+ );
129
+
130
+ /**
131
+ * `allFeatures`, reduced per `excludeParts` — this is what actually
132
+ * renders (`styleGroups`/`<Geo>` below) and what `hoverCandidates` derives
133
+ * from. A feature reduced to no parts at all is dropped entirely.
134
+ */
135
+ const features = $derived.by((): TaggedFeature[] => {
136
+ if (!excludeParts) return allFeatures;
137
+ return allFeatures.flatMap((f) => {
138
+ const geometry = filterGeometryParts(f.geometry, (part) => !excludeParts(part, f));
139
+ if (!geometry) return [];
140
+ const [cx, cy] = geometryCentroid(geometry);
141
+ return [{ ...f, geometry, __cx: cx, __cy: cy } as TaggedFeature];
142
+ });
143
+ });
144
+
145
+ // ── Tile-layer / projection compatibility ────────────────────────────────
146
+ $effect(() => {
147
+ if (!styles.tileLayer) return;
148
+ const proj = scales.projection;
149
+ const name = typeof proj === 'string' ? proj : proj?.type;
150
+ if (name && name !== 'mercator' && name !== 'transverse-mercator') {
151
+ console.warn(
152
+ `GeoPlot: styles.tileLayer only renders correctly with a mercator-family projection — got "${name}". Rendering anyway.`,
153
+ );
154
+ }
155
+ });
156
+
157
+ // ── Choropleth color scale (mirrors heatmap's styles.colors/scales.z) ────
158
+ const minColor = $derived(styles.colors?.min ?? cfg.continuous.min);
159
+ const maxColor = $derived(styles.colors?.max ?? cfg.continuous.max);
160
+
161
+ const valueDomain = $derived.by((): [number, number] | null => {
162
+ if (scales.color?.domain) return scales.color.domain;
163
+ if (!value) return null;
164
+ const vals = features.map((f) => value(f)).filter((v): v is number => v != null);
165
+ return vals.length ? [Math.min(...vals), Math.max(...vals)] : null;
166
+ });
167
+
168
+ const colorScale = $derived(
169
+ valueDomain ? makeColorScale(valueDomain[0], valueDomain[1], minColor, maxColor) : null,
170
+ );
171
+
172
+ function resolveBaseFill(f: TaggedFeature): string {
173
+ if (value && colorScale) {
174
+ const v = value(f);
175
+ if (v != null) return colorScale(v);
176
+ }
177
+ return styles.fill ?? cfg.palette[0];
178
+ }
179
+
180
+ /** Per-marker color ramp for a `dot` marker's `value` channel — independent of the base map's own choropleth ramp. */
181
+ function makeDotColorScale(marker: GeoDotMarker): ((v: number) => string) | null {
182
+ if (!marker.value) return null;
183
+ const getValue = resolveAccessor(marker.value);
184
+ const vals = marker.data.map((d) => Number(getValue(d))).filter((v) => Number.isFinite(v));
185
+ if (!vals.length) return null;
186
+ const domain: [number, number] = [Math.min(...vals), Math.max(...vals)];
187
+ return makeColorScale(
188
+ domain[0],
189
+ domain[1],
190
+ marker.style?.colors?.min ?? cfg.continuous.min,
191
+ marker.style?.colors?.max ?? cfg.continuous.max,
192
+ );
193
+ }
194
+
195
+ // ── Per-feature style resolution, grouped by distinct resolved style ─────
196
+ // (one <Geo> call per group — mirrors heatmap's styleGroups: svelteplot's
197
+ // shared numeric-scale machinery collapses a per-row fillOpacity/strokeWidth
198
+ // *accessor* to a single value, so literal per-call constants are required.)
199
+ function getFeatureStyle(f: TaggedFeature): GeoSegmentStyle {
200
+ return resolveFeatureStyle<GeoSegmentStyle>(segments, f.__id);
201
+ }
202
+
203
+ type StyleGroup = { style: GeoSegmentStyle; data: TaggedFeature[] };
204
+ const styleGroups = $derived.by((): StyleGroup[] => {
205
+ const groups = new SvelteMap<string, StyleGroup>();
206
+ for (const f of features) {
207
+ const style = getFeatureStyle(f);
208
+ const key = JSON.stringify(style);
209
+ let group = groups.get(key);
210
+ if (!group) {
211
+ group = { style, data: [] };
212
+ groups.set(key, group);
213
+ }
214
+ group.data.push(f);
215
+ }
216
+ return [...groups.values()];
217
+ });
218
+
219
+ /**
220
+ * Hover/click candidates — like `features`, but one row per disjoint
221
+ * geometry part instead of per feature, so a multi-part feature's hover
222
+ * point isn't stranded somewhere between its parts (see
223
+ * `geometryPartCentroids`). Only used for hover matching (`BasePlotLayout`'s
224
+ * `data`/`hoverPoints`) — `<Geo>` itself still renders from
225
+ * `features`/`styleGroups`, one entry per feature, unaffected by this.
226
+ */
227
+ const hoverCandidates = $derived.by((): TaggedFeature[] =>
228
+ features.flatMap((f) => geometryPartCentroids(f.geometry).map(([cx, cy]) => ({ ...f, __cx: cx, __cy: cy }))),
229
+ );
230
+
231
+ const hoverPoints = $derived(hoverCandidates.map((f) => ({ x: f.__cx, y: f.__cy, row: f, label: f.__id })));
232
+ const hoverGetX = (f: TaggedFeature) => f.__cx;
233
+ const hoverGetY = (f: TaggedFeature) => f.__cy;
234
+
235
+ /**
236
+ * `markers` otherwise always render after (on top of) the base map's own
237
+ * `styleGroups`, regardless of array order — a marker opts into drawing
238
+ * underneath it instead by setting `behind: true` (see e.g.
239
+ * `GeoSphereMarker.behind`).
240
+ */
241
+ function isBehindMarker(marker: GeoMarker): boolean {
242
+ return (
243
+ (marker.type === 'graticule' || marker.type === 'sphere' || marker.type === 'contour') &&
244
+ marker.behind === true
245
+ );
246
+ }
247
+
248
+ const hoverMarkerConfig = $derived(markers.find((m): m is GeoHoverMarkerConfig => m.type === 'hover'));
249
+
250
+ $effect(() => {
251
+ if (hoverMarkerConfig?.strategy === 'nearest-point') {
252
+ console.warn(
253
+ "GeoPlot: the hover marker's strategy 'nearest-point' isn't implemented yet — falling back to 'nearest-feature'.",
254
+ );
255
+ }
256
+ });
257
+
258
+ const hoverActive = $derived(tooltip != null || hoverMarkerConfig != null);
259
+
260
+ // ── Projection ────────────────────────────────────────────────────────────
261
+ const resolvedProjection = $derived(
262
+ resolveProjection(scales.projection ?? (styles.tileLayer ? 'mercator' : undefined), hoverCandidates),
263
+ );
264
+
265
+ // ── Pan / zoom / rotate — owned here (not GeoLayout) since the resulting
266
+ // rotated projection must reach `BasePlotLayout`'s own `<Plot projection>`
267
+ // prop, and a component nested *inside* that `<Plot>` (as GeoLayout is)
268
+ // has no way to reach back out to a prop on its own ancestor. GeoLayout
269
+ // only renders the *result* (`zoomTransform`) as its `<g transform>`. ────
270
+ let containerEl = $state<HTMLDivElement>();
271
+ const zoomEnabled = $derived(styles.zoom !== false && styles.zoom != null);
272
+ const zoomConfig = $derived(typeof styles.zoom === 'object' ? styles.zoom : undefined);
273
+
274
+ /**
275
+ * Orthographic (and other azimuthal, hemisphere-style) projections don't
276
+ * gain anything from drag-to-*pan* — panning just slides the same globe a
277
+ * few pixels within the frame — so when `zoom` is enabled on one, drag
278
+ * instead rotates the globe (`createGeoRotate`) and wheel/pinch keeps
279
+ * scaling it (`createGeoZoom`, with its own drag disabled below so the two
280
+ * gestures don't fight over the same pointer events).
281
+ */
282
+ const projectionType = $derived(
283
+ typeof resolvedProjection === 'string' ? resolvedProjection : resolvedProjection?.type,
284
+ );
285
+ const rotateEnabled = $derived(zoomEnabled && projectionType === 'orthographic');
286
+ const baseRotate = $derived.by((): GeoRotate => {
287
+ const configured = typeof resolvedProjection === 'object' ? resolvedProjection?.rotate : undefined;
288
+ return [configured?.[0] ?? 0, configured?.[1] ?? 0, configured?.[2] ?? 0];
289
+ });
290
+
291
+ const geoZoom = createGeoZoom({
292
+ enabled: () => zoomEnabled,
293
+ config: () => zoomConfig,
294
+ node: () => containerEl ?? null,
295
+ disableDrag: () => rotateEnabled,
296
+ });
297
+
298
+ const geoRotate = createGeoRotate({
299
+ enabled: () => rotateEnabled,
300
+ baseRotate: () => baseRotate,
301
+ scale: () => geoZoom.transform.k,
302
+ node: () => containerEl ?? null,
303
+ });
304
+
305
+ const finalProjection = $derived.by(() => {
306
+ if (!rotateEnabled) return resolvedProjection;
307
+ return {
308
+ ...(typeof resolvedProjection === 'string' ? { type: resolvedProjection } : resolvedProjection),
309
+ rotate: geoRotate.rotate,
310
+ };
311
+ });
312
+
313
+ /**
314
+ * This plot's own hover must never fire while the cursor is actually over
315
+ * an `inset` marker's panel — an inset draws its own content at its own
316
+ * scale/projection, so the main map's `Pointer` has no way to know it's
317
+ * there and would otherwise still match/show whatever main-map feature
318
+ * happens to be nearest that screen position underneath the inset.
319
+ *
320
+ * Checked via `e.target` (which `InsetMarker`'s own hit-test `<rect>`, or
321
+ * one of its feature `<path>`s, tags with the `dct-geo-inset` ancestor —
322
+ * see `InsetMarker.svelte`) rather than by comparing pixel coordinates against a
323
+ * precomputed inset box: svelteplot's `<Plot>` doesn't necessarily render
324
+ * at a 1:1 scale between its `width`/`height` props and actual client
325
+ * pixels (its internal `viewBox` can differ), which would silently desync
326
+ * any independently-computed pixel math from what's really on screen. The
327
+ * browser's own hit-testing has no such problem — it already resolves
328
+ * `e.target` against whatever is actually painted, accounting for any such
329
+ * scaling automatically.
330
+ */
331
+ function isOverInset(e: PointerEvent): boolean {
332
+ return (e.target as Element | null)?.closest?.('.dct-geo-inset') != null;
333
+ }
334
+ </script>
335
+
336
+ {#snippet marker_(marker: GeoMarker, numericMargins: { top: number; right: number; bottom: number; left: number })}
337
+ {#if marker.type === 'graticule'}
338
+ <Graticule
339
+ step={marker.step}
340
+ stepX={marker.stepX}
341
+ stepY={marker.stepY}
342
+ stroke={marker.style?.stroke ?? 'currentColor'}
343
+ strokeWidth={marker.style?.strokeWidth ?? 0.5}
344
+ strokeOpacity={marker.style?.strokeOpacity ?? 0.3}
345
+ strokeDasharray={marker.style?.strokeDasharray}
346
+ style="vector-effect: non-scaling-stroke"
347
+ />
348
+ {:else if marker.type === 'sphere'}
349
+ <Sphere
350
+ fill={marker.style?.fill}
351
+ fillOpacity={marker.style?.fillOpacity}
352
+ stroke={marker.style?.stroke ?? 'currentColor'}
353
+ strokeWidth={marker.style?.strokeWidth ?? 1}
354
+ style="vector-effect: non-scaling-stroke"
355
+ />
356
+ {:else if marker.type === 'dot'}
357
+ {@const getDotValue = marker.value ? resolveAccessor(marker.value) : null}
358
+ {@const dotColorScale = getDotValue ? makeDotColorScale(marker) : null}
359
+ <Dot
360
+ data={marker.data}
361
+ x={marker.x}
362
+ y={marker.y}
363
+ r={marker.r ?? marker.style?.dotRadius ?? 4}
364
+ fill={dotColorScale && getDotValue
365
+ ? (d: Record<string, unknown>) => dotColorScale(Number(getDotValue(d)))
366
+ : (marker.style?.dotFill ?? cfg.palette[0])}
367
+ fillOpacity={marker.style?.dotFillOpacity ?? 1}
368
+ stroke={marker.style?.dotStroke}
369
+ strokeWidth={marker.style?.dotStrokeWidth}
370
+ />
371
+ {:else if marker.type === 'vector'}
372
+ <Vector
373
+ data={marker.data}
374
+ x={marker.x}
375
+ y={marker.y}
376
+ length={marker.length}
377
+ rotate={marker.rotate}
378
+ shape={marker.shape ?? 'arrow'}
379
+ anchor={marker.anchor}
380
+ fill={marker.style?.fill ?? 'currentColor'}
381
+ stroke={marker.style?.stroke}
382
+ strokeWidth={marker.style?.strokeWidth}
383
+ />
384
+ {:else if marker.type === 'link'}
385
+ {@const getX1 = resolveAccessor(marker.x1)}
386
+ {@const getY1 = resolveAccessor(marker.y1)}
387
+ {@const getX2 = resolveAccessor(marker.x2)}
388
+ {@const getY2 = resolveAccessor(marker.y2)}
389
+ {@const linkFeatures = marker.data.map((d) => ({
390
+ type: 'Feature' as const,
391
+ properties: {},
392
+ geometry: {
393
+ type: 'LineString' as const,
394
+ coordinates:
395
+ marker.curve === 'linear'
396
+ ? [
397
+ [Number(getX1(d)), Number(getY1(d))],
398
+ [Number(getX2(d)), Number(getY2(d))],
399
+ ]
400
+ : greatCircleInterpolate(Number(getX1(d)), Number(getY1(d)), Number(getX2(d)), Number(getY2(d))),
401
+ },
402
+ }))}
403
+ <!--
404
+ No `fill` prop here, deliberately: svelteplot's channel resolution
405
+ (`toChannelOption`) treats *any* string value — including the
406
+ literal `'none'` this used to pass — as scale-worthy unless it
407
+ parses as a real CSS color, so `fill="none"` was getting run
408
+ through the plot's color scale and painted an arbitrary category
409
+ color instead of staying unfilled (confirmed by inspecting the
410
+ rendered DOM). Omitting the prop entirely leaves svelteplot's own
411
+ per-mark default in place, which is `fill: none` for an unfilled
412
+ channel — the outcome this mark actually wants, without the buggy
413
+ detour through the color scale.
414
+ -->
415
+ <Geo
416
+ class="link"
417
+ data={linkFeatures}
418
+ stroke={marker.style?.stroke ?? 'currentColor'}
419
+ strokeWidth={marker.style?.strokeWidth ?? 1}
420
+ strokeOpacity={marker.style?.strokeOpacity}
421
+ strokeDasharray={marker.style?.strokeDasharray}
422
+ style="vector-effect: non-scaling-stroke"
423
+ />
424
+ {:else if marker.type === 'contour'}
425
+ <!--
426
+ `fill`/`stroke`: no `?? 'none'` fallback here, deliberately — same
427
+ bug as the `link` marker above. Omitting the prop when
428
+ `marker.fill` is unset leaves svelteplot's own per-mark default in
429
+ place (`fill="none"`, per Contour's own doc comment), instead of
430
+ running the literal string `'none'` through the plot's color scale
431
+ (`'none'` isn't a recognized CSS color, so svelteplot doesn't treat
432
+ it as a literal output value the way it does e.g. `'currentColor'`).
433
+ -->
434
+ <ContourClipMarker mode={marker.clip} {features}>
435
+ <Contour
436
+ class="contour"
437
+ data={marker.data}
438
+ x={marker.x}
439
+ y={marker.y}
440
+ value={marker.value}
441
+ thresholds={marker.thresholds}
442
+ fill={marker.fill}
443
+ stroke={marker.stroke ?? 'currentColor'}
444
+ strokeWidth={marker.style?.strokeWidth}
445
+ fillOpacity={marker.style?.fillOpacity}
446
+ strokeOpacity={marker.style?.strokeOpacity}
447
+ blur={marker.blur}
448
+ interpolate={marker.interpolate}
449
+ />
450
+ </ContourClipMarker>
451
+ {:else if marker.type === 'component'}
452
+ <marker.component {...marker.props} data={marker.data ?? features} />
453
+ {:else if marker.type === 'inset'}
454
+ {@const insetContentBox = {
455
+ left: numericMargins.left,
456
+ top: numericMargins.top,
457
+ width: Math.max(0, width - numericMargins.left - numericMargins.right),
458
+ height: Math.max(0, height - numericMargins.top - numericMargins.bottom),
459
+ }}
460
+ <InsetMarker
461
+ marker={marker as GeoInsetMarker<TProps>}
462
+ features={allFeatures}
463
+ width={insetContentBox.width}
464
+ height={insetContentBox.height}
465
+ left={insetContentBox.left}
466
+ top={insetContentBox.top}
467
+ defaultFill={cfg.palette[0]}
468
+ />
469
+ {/if}
470
+ {/snippet}
471
+
472
+ <!--
473
+ `x`/`y`: pins the plot's generic linear x/y scales to the full lon/lat
474
+ range. Every geo-aware mark (Geo, Dot, Vector, Link, …) bypasses these
475
+ entirely when a projection is active, routing through it instead — except
476
+ svelteplot's own `Contour` mark, whose scatter/sampler-mode grid is
477
+ hardcoded to read `plot.scales.x`/`y` (see `Contour.svelte`) regardless of
478
+ any active projection. Left unset, that scale would auto-fit to just the
479
+ contour data's own point extent — a different frame than the projection's
480
+ own (typically whole-world) fit, so the contour would sit at the wrong
481
+ scale/position relative to the map under it. Pinning both to the world
482
+ range only helps `contour` markers align (best exactly under an
483
+ `equirectangular` projection, whose x/y truly are linear in lon/lat) and
484
+ is a no-op for every other mark type.
485
+ -->
486
+ <BasePlotLayout
487
+ {width}
488
+ {height}
489
+ data={hoverCandidates}
490
+ getX={hoverGetX}
491
+ getY={hoverGetY}
492
+ {hoverPoints}
493
+ label={(f) => f.__id}
494
+ {tooltip}
495
+ {hoverActive}
496
+ hoverStrategy="punctual"
497
+ hoverSync={tooltip?.sync ?? false}
498
+ x={{ domain: [-180, 180] }}
499
+ y={{ domain: [90, -90] }}
500
+ projection={finalProjection}
501
+ {margins}
502
+ marginEstimate={GEO_MARGIN_ESTIMATE}
503
+ excludeHover={isOverInset}
504
+ touchAction={zoomEnabled ? 'none' : undefined}
505
+ bind:containerEl
506
+ >
507
+ {#snippet children({ matchedPoints, numericMargins })}
508
+ <GeoLayout {width} {height} {numericMargins} zoomTransform={geoZoom.transform} tileLayer={styles.tileLayer}>
509
+ {#each markers as marker, i (marker.type + '-' + i)}
510
+ {#if isBehindMarker(marker)}
511
+ {@render marker_(marker, numericMargins)}
512
+ {/if}
513
+ {/each}
514
+
515
+ {#each styleGroups as group (JSON.stringify(group.style))}
516
+ <Geo
517
+ data={group.data}
518
+ fill={(d: TaggedFeature) => group.style.fill ?? resolveBaseFill(d)}
519
+ fillOpacity={group.style.fillOpacity ?? styles.fillOpacity ?? 1}
520
+ stroke={group.style.stroke ?? styles.stroke ?? cfg.axis.color}
521
+ strokeWidth={group.style.strokeWidth ?? styles.strokeWidth ?? 0.5}
522
+ style="vector-effect: non-scaling-stroke"
523
+ onclick={styles.onFeatureClick
524
+ ? (e: Event, d: TaggedFeature) => styles.onFeatureClick?.(d, e)
525
+ : undefined}
526
+ />
527
+ {/each}
528
+
529
+ {#each markers as marker, i (marker.type + '-' + i)}
530
+ {#if !isBehindMarker(marker)}
531
+ {@render marker_(marker, numericMargins)}
532
+ {/if}
533
+ {/each}
534
+ </GeoLayout>
535
+
536
+ {#if hoverMarkerConfig}
537
+ <HoverMarker
538
+ data={matchedPoints}
539
+ ruleX={null}
540
+ ruleY={null}
541
+ showLabels={hoverMarkerConfig.showLabels ?? true}
542
+ format={hoverMarkerConfig.format}
543
+ dotStyle={hoverMarkerConfig.dotStyle}
544
+ fontStyle={hoverMarkerConfig.fontStyle}
545
+ />
546
+ {/if}
547
+ {/snippet}
548
+ </BasePlotLayout>
@@ -0,0 +1,29 @@
1
+ import type { BasePlotProps, GeoScalesConfig } from '../../types/plots/props';
2
+ import type { GeoFeature, GeoDataProps } from '../../types/plots/data/geo';
3
+ import type { GeoStyles } from '../../types/plots/styles/geo';
4
+ import type { GeoSegmentStyle } from '../../types/plots/segments/common';
5
+ import type { GeoMarker } from '../../types/markers/geo';
6
+ declare function $$render<TProps extends Record<string, unknown>>(): {
7
+ props: BasePlotProps<GeoDataProps<TProps>, GeoFeature<TProps>, GeoStyles<TProps>, GeoSegmentStyle, GeoMarker, GeoScalesConfig>;
8
+ exports: {};
9
+ bindings: "";
10
+ slots: {};
11
+ events: {};
12
+ };
13
+ declare class __sveltets_Render<TProps extends Record<string, unknown>> {
14
+ props(): ReturnType<typeof $$render<TProps>>['props'];
15
+ events(): ReturnType<typeof $$render<TProps>>['events'];
16
+ slots(): ReturnType<typeof $$render<TProps>>['slots'];
17
+ bindings(): "";
18
+ exports(): {};
19
+ }
20
+ interface $$IsomorphicComponent {
21
+ new <TProps extends Record<string, unknown>>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<TProps>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<TProps>['props']>, ReturnType<__sveltets_Render<TProps>['events']>, ReturnType<__sveltets_Render<TProps>['slots']>> & {
22
+ $$bindings?: ReturnType<__sveltets_Render<TProps>['bindings']>;
23
+ } & ReturnType<__sveltets_Render<TProps>['exports']>;
24
+ <TProps extends Record<string, unknown>>(internal: unknown, props: ReturnType<__sveltets_Render<TProps>['props']> & {}): ReturnType<__sveltets_Render<TProps>['exports']>;
25
+ z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
26
+ }
27
+ declare const Plot: $$IsomorphicComponent;
28
+ type Plot<TProps extends Record<string, unknown>> = InstanceType<typeof Plot<TProps>>;
29
+ export default Plot;
@@ -0,0 +1,77 @@
1
+ <script lang="ts">
2
+ import { usePlot } from 'svelteplot';
3
+ import {
4
+ visibleTiles,
5
+ buildMercatorFit,
6
+ tileScreenBounds,
7
+ tileUrl,
8
+ toProjectionLike,
9
+ type StreamingProjection,
10
+ } from '../utils/tiles';
11
+ import type { GeoTileLayerConfig } from '../../types/plots/styles/geo';
12
+
13
+ /**
14
+ * Renders an XYZ raster tile basemap under the vector geometry. Mounted as
15
+ * a child of svelteplot's `<Plot>` so `usePlot()` resolves its already-fit
16
+ * projection — this module never depends on `d3-geo` directly.
17
+ *
18
+ * v1 limitation: the visible tile grid is computed once for the initial
19
+ * view, not re-fetched as the pan/zoom `<g>` transform changes — tiles pan
20
+ * and scale visually with the map, but don't reveal new coverage when
21
+ * panned far, and pixelate past their native resolution when zoomed in.
22
+ */
23
+ let {
24
+ config,
25
+ left,
26
+ top,
27
+ width,
28
+ height,
29
+ }: {
30
+ config: GeoTileLayerConfig;
31
+ left: number;
32
+ top: number;
33
+ width: number;
34
+ height: number;
35
+ } = $props();
36
+
37
+ const plot = usePlot();
38
+
39
+ const tileSize = $derived(config.tileSize ?? 256);
40
+
41
+ // svelteplot's own `PlotScale` type doesn't reflect this, but at runtime
42
+ // `scales.projection` is `createProjection()`'s `{ stream, invert }`
43
+ // result (verified against svelteplot's own `helpers/projection.js`).
44
+ const rawProjection = $derived(plot.scales.projection as unknown as StreamingProjection | undefined);
45
+ const projection = $derived(rawProjection ? toProjectionLike(rawProjection) : undefined);
46
+
47
+ const tiles = $derived.by(() => {
48
+ if (!projection) return [];
49
+ return visibleTiles(projection, { left, top, width, height }, tileSize, config.minZoom ?? 0, config.maxZoom ?? 19);
50
+ });
51
+
52
+ // One affine fit per zoom level, shared across every tile in `tiles` — see
53
+ // `buildMercatorFit`'s doc comment for why this replaces forward-projecting
54
+ // each tile's own corners individually.
55
+ const fit = $derived.by(() => {
56
+ if (!projection || tiles.length === 0) return null;
57
+ return buildMercatorFit(projection, { left, top, width, height }, tiles[0].z);
58
+ });
59
+ </script>
60
+
61
+ {#if projection && fit}
62
+ <g class="dct-geo-tiles" opacity={config.opacity ?? 1}>
63
+ {#each tiles as tile (`${tile.z}/${tile.x}/${tile.y}`)}
64
+ {@const bounds = tileScreenBounds(fit, tile)}
65
+ {#if bounds}
66
+ <image
67
+ href={tileUrl(config.url, tile)}
68
+ x={bounds.left}
69
+ y={bounds.top}
70
+ width={bounds.width}
71
+ height={bounds.height}
72
+ preserveAspectRatio="none"
73
+ />
74
+ {/if}
75
+ {/each}
76
+ </g>
77
+ {/if}
@@ -0,0 +1,11 @@
1
+ import type { GeoTileLayerConfig } from '../../types/plots/styles/geo';
2
+ type $$ComponentProps = {
3
+ config: GeoTileLayerConfig;
4
+ left: number;
5
+ top: number;
6
+ width: number;
7
+ height: number;
8
+ };
9
+ declare const TileLayer: import("svelte").Component<$$ComponentProps, {}, "">;
10
+ type TileLayer = ReturnType<typeof TileLayer>;
11
+ export default TileLayer;
@@ -0,0 +1,8 @@
1
+ import type { GeoCustomProjectionFactory } from '../../../types/plots/scales/geo';
2
+ /**
3
+ * Argentina's territory in one unbroken frame: the mainland and the
4
+ * Antarctic/South Atlantic sector both visible, related to each other at
5
+ * true scale (azimuthal-equidistant, rotated so the split falls to the
6
+ * sides rather than through the middle of either half).
7
+ */
8
+ export declare const argentinaBicontinental: GeoCustomProjectionFactory;