@fundar/data-chart-telling 0.0.18 → 0.0.19
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/dist/layout/geo/GeoLayout.svelte +4 -0
- package/dist/layout/geo/GeoLayout.svelte.d.ts +2 -0
- package/dist/plots/geo/Plot.svelte +31 -1
- package/dist/plots/geo/TileLayer.svelte +15 -2
- package/dist/plots/geo/TileLayer.svelte.d.ts +2 -0
- package/dist/plots/utils/geoAccessors.d.ts +12 -0
- package/dist/plots/utils/geoAccessors.js +32 -0
- package/dist/plots/utils/tiles.d.ts +26 -0
- package/dist/plots/utils/tiles.js +49 -0
- package/package.json +1 -1
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
numericMargins,
|
|
23
23
|
zoomTransform,
|
|
24
24
|
tileLayer,
|
|
25
|
+
dataLonLatBounds,
|
|
25
26
|
children,
|
|
26
27
|
}: {
|
|
27
28
|
width: number;
|
|
@@ -29,6 +30,8 @@
|
|
|
29
30
|
numericMargins: { top: number; right: number; bottom: number; left: number };
|
|
30
31
|
zoomTransform: { x: number; y: number; k: number };
|
|
31
32
|
tileLayer?: GeoTileLayerConfig;
|
|
33
|
+
/** Forwarded to `TileLayer` — see its own prop doc / `clipBoundsToData`. */
|
|
34
|
+
dataLonLatBounds?: [number, number, number, number] | null;
|
|
32
35
|
children: Snippet;
|
|
33
36
|
} = $props();
|
|
34
37
|
</script>
|
|
@@ -41,6 +44,7 @@
|
|
|
41
44
|
top={numericMargins.top}
|
|
42
45
|
width={Math.max(0, width - numericMargins.left - numericMargins.right)}
|
|
43
46
|
height={Math.max(0, height - numericMargins.top - numericMargins.bottom)}
|
|
47
|
+
{dataLonLatBounds}
|
|
44
48
|
/>
|
|
45
49
|
{/if}
|
|
46
50
|
{@render children()}
|
|
@@ -15,6 +15,8 @@ type $$ComponentProps = {
|
|
|
15
15
|
k: number;
|
|
16
16
|
};
|
|
17
17
|
tileLayer?: GeoTileLayerConfig;
|
|
18
|
+
/** Forwarded to `TileLayer` — see its own prop doc / `clipBoundsToData`. */
|
|
19
|
+
dataLonLatBounds?: [number, number, number, number] | null;
|
|
18
20
|
children: Snippet;
|
|
19
21
|
};
|
|
20
22
|
declare const GeoLayout: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import { resolveFeatureStyle } from '../utils/geoSegments';
|
|
6
6
|
import {
|
|
7
7
|
geometryCentroid,
|
|
8
|
+
geometryLonLatBounds,
|
|
8
9
|
geometryPartCentroids,
|
|
9
10
|
filterGeometryParts,
|
|
10
11
|
defaultFeatureId,
|
|
@@ -144,6 +145,28 @@
|
|
|
144
145
|
});
|
|
145
146
|
});
|
|
146
147
|
|
|
148
|
+
// ── Tile-layer clipping — the rendered geometry's own lon/lat extent, so
|
|
149
|
+
// `TileLayer` can clip its raster grid to where the map actually is
|
|
150
|
+
// instead of a `domain: 'data'` fit's full (possibly letterboxed) box —
|
|
151
|
+
// see `clipBoundsToData`'s doc comment. Only computed when a tile layer is
|
|
152
|
+
// actually configured, since it's an O(total coordinate count) pass. ────
|
|
153
|
+
const dataLonLatBounds = $derived.by((): [number, number, number, number] | null => {
|
|
154
|
+
if (!styles.tileLayer) return null;
|
|
155
|
+
let minLon = Infinity;
|
|
156
|
+
let maxLon = -Infinity;
|
|
157
|
+
let minLat = Infinity;
|
|
158
|
+
let maxLat = -Infinity;
|
|
159
|
+
for (const f of features) {
|
|
160
|
+
const b = geometryLonLatBounds(f.geometry);
|
|
161
|
+
if (!b) continue;
|
|
162
|
+
if (b[0] < minLon) minLon = b[0];
|
|
163
|
+
if (b[2] > maxLon) maxLon = b[2];
|
|
164
|
+
if (b[1] < minLat) minLat = b[1];
|
|
165
|
+
if (b[3] > maxLat) maxLat = b[3];
|
|
166
|
+
}
|
|
167
|
+
return minLon === Infinity ? null : [minLon, minLat, maxLon, maxLat];
|
|
168
|
+
});
|
|
169
|
+
|
|
147
170
|
// ── Tile-layer / projection compatibility ────────────────────────────────
|
|
148
171
|
$effect(() => {
|
|
149
172
|
if (!styles.tileLayer) return;
|
|
@@ -534,7 +557,14 @@
|
|
|
534
557
|
bind:containerEl
|
|
535
558
|
>
|
|
536
559
|
{#snippet children({ matchedPoints, numericMargins })}
|
|
537
|
-
<GeoLayout
|
|
560
|
+
<GeoLayout
|
|
561
|
+
{width}
|
|
562
|
+
{height}
|
|
563
|
+
{numericMargins}
|
|
564
|
+
zoomTransform={geoZoom.transform}
|
|
565
|
+
tileLayer={styles.tileLayer}
|
|
566
|
+
{dataLonLatBounds}
|
|
567
|
+
>
|
|
538
568
|
{#each markers as marker, i (marker.type + '-' + i)}
|
|
539
569
|
{#if isBehindMarker(marker)}
|
|
540
570
|
{@render marker_(marker, numericMargins)}
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
tileScreenBounds,
|
|
7
7
|
tileUrl,
|
|
8
8
|
toProjectionLike,
|
|
9
|
+
clipBoundsToData,
|
|
9
10
|
type StreamingProjection,
|
|
10
11
|
} from '../utils/tiles';
|
|
11
12
|
import type { GeoTileLayerConfig } from '../../types/plots/styles/geo';
|
|
@@ -26,12 +27,15 @@
|
|
|
26
27
|
top,
|
|
27
28
|
width,
|
|
28
29
|
height,
|
|
30
|
+
dataLonLatBounds,
|
|
29
31
|
}: {
|
|
30
32
|
config: GeoTileLayerConfig;
|
|
31
33
|
left: number;
|
|
32
34
|
top: number;
|
|
33
35
|
width: number;
|
|
34
36
|
height: number;
|
|
37
|
+
/** The rendered geometry's own lon/lat extent — see `clipBoundsToData`. */
|
|
38
|
+
dataLonLatBounds?: [number, number, number, number] | null;
|
|
35
39
|
} = $props();
|
|
36
40
|
|
|
37
41
|
const plot = usePlot();
|
|
@@ -44,9 +48,18 @@
|
|
|
44
48
|
const rawProjection = $derived(plot.scales.projection as unknown as StreamingProjection | undefined);
|
|
45
49
|
const projection = $derived(rawProjection ? toProjectionLike(rawProjection) : undefined);
|
|
46
50
|
|
|
51
|
+
// Clipped to where the map's own geometry actually is — see
|
|
52
|
+
// `clipBoundsToData`'s doc comment for why the raw `{left,top,width,height}`
|
|
53
|
+
// box alone isn't safe to hand to `visibleTiles`/`buildMercatorFit`.
|
|
54
|
+
const clippedBounds = $derived.by(() => {
|
|
55
|
+
const full = { left, top, width, height };
|
|
56
|
+
if (!projection) return full;
|
|
57
|
+
return clipBoundsToData(projection, full, dataLonLatBounds);
|
|
58
|
+
});
|
|
59
|
+
|
|
47
60
|
const tiles = $derived.by(() => {
|
|
48
61
|
if (!projection) return [];
|
|
49
|
-
return visibleTiles(projection,
|
|
62
|
+
return visibleTiles(projection, clippedBounds, tileSize, config.minZoom ?? 0, config.maxZoom ?? 19);
|
|
50
63
|
});
|
|
51
64
|
|
|
52
65
|
// One affine fit per zoom level, shared across every tile in `tiles` — see
|
|
@@ -54,7 +67,7 @@
|
|
|
54
67
|
// each tile's own corners individually.
|
|
55
68
|
const fit = $derived.by(() => {
|
|
56
69
|
if (!projection || tiles.length === 0) return null;
|
|
57
|
-
return buildMercatorFit(projection,
|
|
70
|
+
return buildMercatorFit(projection, clippedBounds, tiles[0].z);
|
|
58
71
|
});
|
|
59
72
|
</script>
|
|
60
73
|
|
|
@@ -5,6 +5,8 @@ type $$ComponentProps = {
|
|
|
5
5
|
top: number;
|
|
6
6
|
width: number;
|
|
7
7
|
height: number;
|
|
8
|
+
/** The rendered geometry's own lon/lat extent — see `clipBoundsToData`. */
|
|
9
|
+
dataLonLatBounds?: [number, number, number, number] | null;
|
|
8
10
|
};
|
|
9
11
|
declare const TileLayer: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
10
12
|
type TileLayer = ReturnType<typeof TileLayer>;
|
|
@@ -8,6 +8,18 @@ import type { GeoFeature } from '../../types/plots/data/geo';
|
|
|
8
8
|
* multi-part geometry whose pieces are geographically spread out.
|
|
9
9
|
*/
|
|
10
10
|
export declare function geometryCentroid(geometry: GeoJSON.Geometry): [number, number];
|
|
11
|
+
/**
|
|
12
|
+
* A geometry's flattened lon/lat bounding box — `[minLon, minLat, maxLon,
|
|
13
|
+
* maxLat]`, or `null` for a geometry with no coordinates at all. Same
|
|
14
|
+
* bbox-not-geodesic caveat as `geometryCentroid` (no antimeridian handling),
|
|
15
|
+
* acceptable for the same reason: real feature sets fed through this
|
|
16
|
+
* package (Argentina's provinces, etc.) don't straddle it. Used to derive
|
|
17
|
+
* `GeoPlot`'s rendered data's own screen-space extent (see `Plot.svelte`'s
|
|
18
|
+
* `dataLonLatBounds`), so `TileLayer` can clip its raster grid to where the
|
|
19
|
+
* map's own geometry actually is instead of a projection's full letterboxed
|
|
20
|
+
* fit box (see `clipBoundsToData`).
|
|
21
|
+
*/
|
|
22
|
+
export declare function geometryLonLatBounds(geometry: GeoJSON.Geometry): [number, number, number, number] | null;
|
|
11
23
|
/**
|
|
12
24
|
* Splits a geometry into its disjoint parts: one `Polygon` per element of a
|
|
13
25
|
* `MultiPolygon`'s `coordinates`, one `LineString` per element of a
|
|
@@ -61,6 +61,38 @@ export function geometryCentroid(geometry) {
|
|
|
61
61
|
flattenCoords(geometry, coords);
|
|
62
62
|
return boundsCenter(coords);
|
|
63
63
|
}
|
|
64
|
+
/**
|
|
65
|
+
* A geometry's flattened lon/lat bounding box — `[minLon, minLat, maxLon,
|
|
66
|
+
* maxLat]`, or `null` for a geometry with no coordinates at all. Same
|
|
67
|
+
* bbox-not-geodesic caveat as `geometryCentroid` (no antimeridian handling),
|
|
68
|
+
* acceptable for the same reason: real feature sets fed through this
|
|
69
|
+
* package (Argentina's provinces, etc.) don't straddle it. Used to derive
|
|
70
|
+
* `GeoPlot`'s rendered data's own screen-space extent (see `Plot.svelte`'s
|
|
71
|
+
* `dataLonLatBounds`), so `TileLayer` can clip its raster grid to where the
|
|
72
|
+
* map's own geometry actually is instead of a projection's full letterboxed
|
|
73
|
+
* fit box (see `clipBoundsToData`).
|
|
74
|
+
*/
|
|
75
|
+
export function geometryLonLatBounds(geometry) {
|
|
76
|
+
const coords = [];
|
|
77
|
+
flattenCoords(geometry, coords);
|
|
78
|
+
if (coords.length === 0)
|
|
79
|
+
return null;
|
|
80
|
+
let minLon = Infinity;
|
|
81
|
+
let maxLon = -Infinity;
|
|
82
|
+
let minLat = Infinity;
|
|
83
|
+
let maxLat = -Infinity;
|
|
84
|
+
for (const [lon, lat] of coords) {
|
|
85
|
+
if (lon < minLon)
|
|
86
|
+
minLon = lon;
|
|
87
|
+
if (lon > maxLon)
|
|
88
|
+
maxLon = lon;
|
|
89
|
+
if (lat < minLat)
|
|
90
|
+
minLat = lat;
|
|
91
|
+
if (lat > maxLat)
|
|
92
|
+
maxLat = lat;
|
|
93
|
+
}
|
|
94
|
+
return [minLon, minLat, maxLon, maxLat];
|
|
95
|
+
}
|
|
64
96
|
/**
|
|
65
97
|
* Splits a geometry into its disjoint parts: one `Polygon` per element of a
|
|
66
98
|
* `MultiPolygon`'s `coordinates`, one `LineString` per element of a
|
|
@@ -36,6 +36,32 @@ export type StreamingProjection = {
|
|
|
36
36
|
* projected output.
|
|
37
37
|
*/
|
|
38
38
|
export declare function toProjectionLike(projection: StreamingProjection): ProjectionLike;
|
|
39
|
+
/**
|
|
40
|
+
* Clips `bounds` down to the screen-space box its actual geo data occupies
|
|
41
|
+
* — `dataLonLatBounds`' four extremes, forward-projected — intersected with
|
|
42
|
+
* `bounds` itself.
|
|
43
|
+
*
|
|
44
|
+
* A `domain: 'data'`-fitted Mercator projection scales to the *constraining*
|
|
45
|
+
* axis of its box (see `clampBoundsToWorld`'s doc comment): a tall/narrow
|
|
46
|
+
* region (Argentina) fit into a short/wide facet cell only fills a fraction
|
|
47
|
+
* of the box's width, and the letterboxed margin on either side is real,
|
|
48
|
+
* inverts to real (if occasionally distant) lon/lat, and so gets real tiles
|
|
49
|
+
* drawn under it — `clampBoundsToWorld` only stops that margin from
|
|
50
|
+
* wrapping *more* than one world-width once inverted, it doesn't stop a
|
|
51
|
+
* *single* world-width's worth of legitimate-but-irrelevant coverage from
|
|
52
|
+
* rendering there. For Argentina specifically that margin is wide enough to
|
|
53
|
+
* reach Argentina's own antipodal region — Australia — which is what a
|
|
54
|
+
* viewer actually sees. Clipping to the data's own footprint removes the
|
|
55
|
+
* letterboxed margin's tiles entirely rather than trying to pick a
|
|
56
|
+
* "more correct" set of tiles to show there.
|
|
57
|
+
*
|
|
58
|
+
* Only valid for a Mercator-family projection, same as the rest of this
|
|
59
|
+
* module: `x` is assumed to depend only on longitude and `y` only on
|
|
60
|
+
* latitude, so each axis's screen extent can be found by projecting just
|
|
61
|
+
* two opposite corners of the lon/lat bbox rather than tracing its full
|
|
62
|
+
* outline.
|
|
63
|
+
*/
|
|
64
|
+
export declare function clipBoundsToData(projection: ProjectionLike, bounds: ScreenBounds, dataLonLatBounds: [number, number, number, number] | null | undefined): ScreenBounds;
|
|
39
65
|
/**
|
|
40
66
|
* Derives the visible XYZ tile grid from svelteplot's own already-fitted
|
|
41
67
|
* projection (rather than this package computing/duplicating a `d3-geo` fit
|
|
@@ -85,6 +85,55 @@ function clampBoundsToWorld(projection, bounds) {
|
|
|
85
85
|
return bounds;
|
|
86
86
|
return { ...bounds, left: centerX - worldWidth / 2, width: worldWidth };
|
|
87
87
|
}
|
|
88
|
+
/**
|
|
89
|
+
* Clips `bounds` down to the screen-space box its actual geo data occupies
|
|
90
|
+
* — `dataLonLatBounds`' four extremes, forward-projected — intersected with
|
|
91
|
+
* `bounds` itself.
|
|
92
|
+
*
|
|
93
|
+
* A `domain: 'data'`-fitted Mercator projection scales to the *constraining*
|
|
94
|
+
* axis of its box (see `clampBoundsToWorld`'s doc comment): a tall/narrow
|
|
95
|
+
* region (Argentina) fit into a short/wide facet cell only fills a fraction
|
|
96
|
+
* of the box's width, and the letterboxed margin on either side is real,
|
|
97
|
+
* inverts to real (if occasionally distant) lon/lat, and so gets real tiles
|
|
98
|
+
* drawn under it — `clampBoundsToWorld` only stops that margin from
|
|
99
|
+
* wrapping *more* than one world-width once inverted, it doesn't stop a
|
|
100
|
+
* *single* world-width's worth of legitimate-but-irrelevant coverage from
|
|
101
|
+
* rendering there. For Argentina specifically that margin is wide enough to
|
|
102
|
+
* reach Argentina's own antipodal region — Australia — which is what a
|
|
103
|
+
* viewer actually sees. Clipping to the data's own footprint removes the
|
|
104
|
+
* letterboxed margin's tiles entirely rather than trying to pick a
|
|
105
|
+
* "more correct" set of tiles to show there.
|
|
106
|
+
*
|
|
107
|
+
* Only valid for a Mercator-family projection, same as the rest of this
|
|
108
|
+
* module: `x` is assumed to depend only on longitude and `y` only on
|
|
109
|
+
* latitude, so each axis's screen extent can be found by projecting just
|
|
110
|
+
* two opposite corners of the lon/lat bbox rather than tracing its full
|
|
111
|
+
* outline.
|
|
112
|
+
*/
|
|
113
|
+
export function clipBoundsToData(projection, bounds, dataLonLatBounds) {
|
|
114
|
+
if (!dataLonLatBounds)
|
|
115
|
+
return bounds;
|
|
116
|
+
const [minLon, minLat, maxLon, maxLat] = dataLonLatBounds;
|
|
117
|
+
const midLon = (minLon + maxLon) / 2;
|
|
118
|
+
const midLat = (minLat + maxLat) / 2;
|
|
119
|
+
const west = projection([minLon, midLat]);
|
|
120
|
+
const east = projection([maxLon, midLat]);
|
|
121
|
+
const south = projection([midLon, minLat]);
|
|
122
|
+
const north = projection([midLon, maxLat]);
|
|
123
|
+
if (!west || !east || !south || !north)
|
|
124
|
+
return bounds;
|
|
125
|
+
const dataLeft = Math.min(west[0], east[0]);
|
|
126
|
+
const dataRight = Math.max(west[0], east[0]);
|
|
127
|
+
const dataTop = Math.min(north[1], south[1]);
|
|
128
|
+
const dataBottom = Math.max(north[1], south[1]);
|
|
129
|
+
const left = Math.max(bounds.left, dataLeft);
|
|
130
|
+
const top = Math.max(bounds.top, dataTop);
|
|
131
|
+
const right = Math.min(bounds.left + bounds.width, dataRight);
|
|
132
|
+
const bottom = Math.min(bounds.top + bounds.height, dataBottom);
|
|
133
|
+
if (!(right > left) || !(bottom > top))
|
|
134
|
+
return bounds;
|
|
135
|
+
return { left, top, width: right - left, height: bottom - top };
|
|
136
|
+
}
|
|
88
137
|
/**
|
|
89
138
|
* Derives the visible XYZ tile grid from svelteplot's own already-fitted
|
|
90
139
|
* projection (rather than this package computing/duplicating a `d3-geo` fit
|