@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.
- package/README.md +121 -17
- package/dist/charts/{Chart.svelte.d.ts → BaseChart.svelte.d.ts} +3 -3
- package/dist/charts/bar/Chart.svelte +5 -5
- package/dist/charts/bar/Chart.svelte.d.ts +1 -2
- package/dist/charts/heatmap/Chart.svelte +8 -7
- package/dist/charts/heatmap/Chart.svelte.d.ts +2 -3
- package/dist/charts/line/Chart.svelte +5 -5
- package/dist/charts/line/Chart.svelte.d.ts +1 -2
- package/dist/charts/pyramid/Chart.svelte +7 -6
- package/dist/charts/pyramid/Chart.svelte.d.ts +2 -3
- package/dist/configuration/config.svelte.js +1 -1
- package/dist/configuration/themes/index.d.ts +56 -40
- package/dist/index.d.ts +26 -12
- package/dist/index.js +12 -7
- package/dist/layout/facet/FacetLayout.svelte +1 -1
- package/dist/layout/facet/FacetLayout.svelte.d.ts +1 -1
- package/dist/layout/geo/GeoLayout.svelte +47 -0
- package/dist/layout/geo/GeoLayout.svelte.d.ts +22 -0
- package/dist/layout/geo/resolveProjection.d.ts +10 -0
- package/dist/layout/geo/resolveProjection.js +48 -0
- package/dist/layout/legend/DiscreteSection.svelte +1 -1
- package/dist/layout/plot/AxisLayout.svelte +95 -0
- package/dist/layout/plot/AxisLayout.svelte.d.ts +39 -0
- package/dist/layout/plot/BasePlotLayout.svelte +302 -0
- package/dist/layout/plot/BasePlotLayout.svelte.d.ts +95 -0
- package/dist/layout/plot/GridLayout.svelte +30 -0
- package/dist/layout/plot/GridLayout.svelte.d.ts +27 -0
- package/dist/layout/plot/RuleLayout.svelte +111 -0
- package/dist/layout/plot/RuleLayout.svelte.d.ts +40 -0
- package/dist/layout/plot/margins.d.ts +34 -0
- package/dist/layout/plot/margins.js +26 -0
- package/dist/layout/timeline/TimelineLayout.svelte +1 -1
- package/dist/layout/timeline/TimelineLayout.svelte.d.ts +1 -1
- package/dist/layout/tooltip/Tooltip.svelte +15 -0
- package/dist/layout/tooltip/Tooltip.svelte.d.ts +7 -0
- package/dist/layout/tooltip/TooltipLayout.svelte +45 -0
- package/dist/layout/tooltip/TooltipLayout.svelte.d.ts +41 -0
- package/dist/layout/tooltip/controller.svelte.d.ts +4 -3
- package/dist/layout/tooltip/controller.svelte.js +5 -6
- package/dist/layout/tooltip/hover.svelte.d.ts +1 -1
- package/dist/layout/tooltip/hover.svelte.js +1 -1
- package/dist/layout/tooltip/utils.d.ts +2 -2
- package/dist/markers/ContourClipMarker.svelte +77 -0
- package/dist/markers/ContourClipMarker.svelte.d.ts +10 -0
- package/dist/{plots/markers → markers}/DeltaMarker.svelte +4 -4
- package/dist/{plots/markers → markers}/DeltaMarker.svelte.d.ts +3 -3
- package/dist/{plots/markers → markers}/DotMarker.svelte +3 -3
- package/dist/{plots/markers → markers}/DotMarker.svelte.d.ts +3 -3
- package/dist/markers/HoverMarker.svelte +136 -0
- package/dist/{plots/markers → markers}/HoverMarker.svelte.d.ts +4 -4
- package/dist/{plots/markers → markers}/PeakMarker.svelte +2 -2
- package/dist/{plots/markers → markers}/PeakMarker.svelte.d.ts +2 -2
- package/dist/{plots/markers → markers}/TextMarker.svelte +11 -4
- package/dist/{plots/markers → markers}/TextMarker.svelte.d.ts +3 -3
- package/dist/markers/inset/InsetMarker.svelte +170 -0
- package/dist/markers/inset/InsetMarker.svelte.d.ts +51 -0
- package/dist/markers/inset/insetLayout.d.ts +25 -0
- package/dist/markers/inset/insetLayout.js +39 -0
- package/dist/markers/inset/insetProjection.d.ts +8 -0
- package/dist/markers/inset/insetProjection.js +39 -0
- package/dist/plots/bar/BarSegments.svelte +66 -0
- package/dist/plots/bar/BarSegments.svelte.d.ts +36 -0
- package/dist/plots/bar/Plot.svelte +132 -76
- package/dist/plots/bar/Plot.svelte.d.ts +7 -3
- package/dist/plots/bar/ValueLabels.svelte +89 -0
- package/dist/plots/bar/ValueLabels.svelte.d.ts +39 -0
- package/dist/plots/bar/hoverPoints.d.ts +15 -0
- package/dist/plots/bar/hoverPoints.js +35 -0
- package/dist/plots/bar/layout.svelte.d.ts +37 -0
- package/dist/plots/bar/layout.svelte.js +132 -0
- package/dist/plots/geo/Plot.svelte +548 -0
- package/dist/plots/geo/Plot.svelte.d.ts +29 -0
- package/dist/plots/geo/TileLayer.svelte +77 -0
- package/dist/plots/geo/TileLayer.svelte.d.ts +11 -0
- package/dist/plots/geo/projections/argentina.d.ts +8 -0
- package/dist/plots/geo/projections/argentina.js +35 -0
- package/dist/plots/geo/projections/fit.d.ts +8 -0
- package/dist/plots/geo/projections/fit.js +14 -0
- package/dist/plots/geo/rotate.svelte.d.ts +26 -0
- package/dist/plots/geo/rotate.svelte.js +79 -0
- package/dist/plots/geo/zoom.svelte.d.ts +32 -0
- package/dist/plots/geo/zoom.svelte.js +50 -0
- package/dist/plots/heatmap/Plot.svelte +129 -47
- package/dist/plots/heatmap/Plot.svelte.d.ts +7 -3
- package/dist/plots/line/Plot.svelte +44 -16
- package/dist/plots/line/Plot.svelte.d.ts +7 -3
- package/dist/plots/pyramid/Plot.svelte +70 -25
- package/dist/plots/pyramid/Plot.svelte.d.ts +7 -3
- package/dist/plots/utils/delta.d.ts +1 -1
- package/dist/plots/utils/geoAccessors.d.ts +64 -0
- package/dist/plots/utils/geoAccessors.js +188 -0
- package/dist/plots/utils/geoSegments.d.ts +14 -0
- package/dist/plots/utils/geoSegments.js +17 -0
- package/dist/plots/utils/segments.d.ts +14 -5
- package/dist/plots/utils/segments.js +25 -13
- package/dist/plots/utils/tiles.d.ts +89 -0
- package/dist/plots/utils/tiles.js +159 -0
- package/dist/plots/utils/topojson.d.ts +9 -0
- package/dist/plots/utils/topojson.js +30 -0
- package/dist/types/charts/common.d.ts +2 -1
- package/dist/types/charts/legend.d.ts +1 -1
- package/dist/types/charts/props.d.ts +9 -6
- package/dist/types/configuration/styling.d.ts +11 -5
- package/dist/types/layout/tooltip.d.ts +14 -21
- package/dist/types/{plots/markers.d.ts → markers/common.d.ts} +2 -18
- package/dist/types/markers/geo.d.ts +237 -0
- package/dist/types/markers/props.d.ts +11 -0
- package/dist/types/plots/axis.d.ts +55 -0
- package/dist/types/plots/axis.js +1 -0
- package/dist/types/plots/constants.d.ts +30 -0
- package/dist/types/plots/constants.js +1 -0
- package/dist/types/plots/data/common.d.ts +40 -0
- package/dist/types/plots/data/common.js +1 -0
- package/dist/types/plots/data/geo.d.ts +74 -0
- package/dist/types/plots/data/geo.js +1 -0
- package/dist/types/plots/delta.d.ts +2 -2
- package/dist/types/plots/props.d.ts +105 -16
- package/dist/types/plots/scales/geo.d.ts +44 -0
- package/dist/types/plots/scales/geo.js +1 -0
- package/dist/types/plots/segments/common.d.ts +25 -0
- package/dist/types/plots/segments/common.js +1 -0
- package/dist/types/plots/segments/config.d.ts +51 -0
- package/dist/types/plots/segments/config.js +1 -0
- package/dist/types/plots/styles/common.d.ts +37 -0
- package/dist/types/plots/styles/common.js +1 -0
- package/dist/types/plots/styles/geo.d.ts +42 -0
- package/dist/types/plots/styles/geo.js +1 -0
- package/dist/types/plots/styling.d.ts +45 -0
- package/dist/types/plots/styling.js +1 -0
- package/dist/utils/grouping.d.ts +2 -1
- package/dist/utils/interpolate.d.ts +2 -1
- package/package.json +27 -13
- package/dist/layout/plot/PlotLayout.svelte +0 -294
- package/dist/layout/plot/PlotLayout.svelte.d.ts +0 -70
- package/dist/plots/markers/HoverMarker.svelte +0 -98
- package/dist/types/plots/common.d.ts +0 -97
- package/dist/types/plots/styles.d.ts +0 -187
- /package/dist/charts/{Chart.svelte → BaseChart.svelte} +0 -0
- /package/dist/types/{plots → markers}/common.js +0 -0
- /package/dist/types/{plots/markers.js → markers/geo.js} +0 -0
- /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;
|