@fundar/data-chart-telling 0.0.20 → 0.0.21
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 +0 -4
- package/dist/layout/geo/GeoLayout.svelte.d.ts +0 -2
- package/dist/plots/geo/Plot.svelte +5 -89
- package/dist/plots/geo/TileLayer.svelte +2 -15
- package/dist/plots/geo/TileLayer.svelte.d.ts +0 -2
- package/dist/plots/utils/geoAccessors.d.ts +0 -31
- package/dist/plots/utils/geoAccessors.js +0 -62
- package/dist/plots/utils/tiles.d.ts +0 -26
- package/dist/plots/utils/tiles.js +20 -58
- package/package.json +1 -1
|
@@ -22,7 +22,6 @@
|
|
|
22
22
|
numericMargins,
|
|
23
23
|
zoomTransform,
|
|
24
24
|
tileLayer,
|
|
25
|
-
dataLonLatBounds,
|
|
26
25
|
children,
|
|
27
26
|
}: {
|
|
28
27
|
width: number;
|
|
@@ -30,8 +29,6 @@
|
|
|
30
29
|
numericMargins: { top: number; right: number; bottom: number; left: number };
|
|
31
30
|
zoomTransform: { x: number; y: number; k: number };
|
|
32
31
|
tileLayer?: GeoTileLayerConfig;
|
|
33
|
-
/** Forwarded to `TileLayer` — see its own prop doc / `clipBoundsToData`. */
|
|
34
|
-
dataLonLatBounds?: [number, number, number, number] | null;
|
|
35
32
|
children: Snippet;
|
|
36
33
|
} = $props();
|
|
37
34
|
</script>
|
|
@@ -44,7 +41,6 @@
|
|
|
44
41
|
top={numericMargins.top}
|
|
45
42
|
width={Math.max(0, width - numericMargins.left - numericMargins.right)}
|
|
46
43
|
height={Math.max(0, height - numericMargins.top - numericMargins.bottom)}
|
|
47
|
-
{dataLonLatBounds}
|
|
48
44
|
/>
|
|
49
45
|
{/if}
|
|
50
46
|
{@render children()}
|
|
@@ -15,8 +15,6 @@ 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;
|
|
20
18
|
children: Snippet;
|
|
21
19
|
};
|
|
22
20
|
declare const GeoLayout: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
@@ -5,8 +5,6 @@
|
|
|
5
5
|
import { resolveFeatureStyle } from '../utils/geoSegments';
|
|
6
6
|
import {
|
|
7
7
|
geometryCentroid,
|
|
8
|
-
geometryLonLatBounds,
|
|
9
|
-
mercatorAspectRatio,
|
|
10
8
|
geometryPartCentroids,
|
|
11
9
|
filterGeometryParts,
|
|
12
10
|
defaultFeatureId,
|
|
@@ -146,66 +144,6 @@
|
|
|
146
144
|
});
|
|
147
145
|
});
|
|
148
146
|
|
|
149
|
-
// ── Data footprint — the rendered geometry's own lon/lat extent. Powers
|
|
150
|
-
// two independent things below: `TileLayer`'s clip-to-data (see
|
|
151
|
-
// `clipBoundsToData`'s doc comment) and `containedSize`'s contain-and-center
|
|
152
|
-
// sizing. Only computed for a `domain: 'data'` fit (see `usesDataFit`) —
|
|
153
|
-
// that's the specific case where the projection's own scale was chosen to
|
|
154
|
-
// match *this* extent, so it's also the only case this bbox is a correct
|
|
155
|
-
// stand-in for "what the projection actually fit to". Cost is a single
|
|
156
|
-
// O(total coordinate count) pass. ────
|
|
157
|
-
const usesDataFit = $derived(
|
|
158
|
-
typeof scales.projection === 'object' && scales.projection?.domain === 'data',
|
|
159
|
-
);
|
|
160
|
-
const dataLonLatBounds = $derived.by((): [number, number, number, number] | null => {
|
|
161
|
-
if (!usesDataFit) return null;
|
|
162
|
-
let minLon = Infinity;
|
|
163
|
-
let maxLon = -Infinity;
|
|
164
|
-
let minLat = Infinity;
|
|
165
|
-
let maxLat = -Infinity;
|
|
166
|
-
for (const f of features) {
|
|
167
|
-
const b = geometryLonLatBounds(f.geometry);
|
|
168
|
-
if (!b) continue;
|
|
169
|
-
if (b[0] < minLon) minLon = b[0];
|
|
170
|
-
if (b[2] > maxLon) maxLon = b[2];
|
|
171
|
-
if (b[1] < minLat) minLat = b[1];
|
|
172
|
-
if (b[3] > maxLat) maxLat = b[3];
|
|
173
|
-
}
|
|
174
|
-
return minLon === Infinity ? null : [minLon, minLat, maxLon, maxLat];
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
/**
|
|
178
|
-
* The box the map itself actually renders into — `width`/`height`
|
|
179
|
-
* shrunk (never grown) to `dataLonLatBounds`' own Mercator aspect ratio,
|
|
180
|
-
* the same way `object-fit: contain` sizes an image. Without this, a
|
|
181
|
-
* `domain: 'data'` fit already internally letterboxes the *projection*
|
|
182
|
-
* to preserve the data's true shape whenever the outer box's aspect
|
|
183
|
-
* doesn't match it (e.g. a tall/narrow country handed a short/wide facet
|
|
184
|
-
* cell) — but previously the outer SVG itself stayed stretched to the
|
|
185
|
-
* full box regardless, leaving that letterboxed margin as dead space
|
|
186
|
-
* (blank, or — before `clipBoundsToData` — filled with irrelevant tiles).
|
|
187
|
-
* Shrinking the actual rendered box to match removes that margin instead
|
|
188
|
-
* of just hiding what was in it; the template below centers this smaller
|
|
189
|
-
* box within the full `{width}×{height}` area so the map still occupies
|
|
190
|
-
* the same visual position within its facet cell.
|
|
191
|
-
*
|
|
192
|
-
* Margins (`margins`/`GEO_MARGIN_ESTIMATE`) are deliberately not netted
|
|
193
|
-
* out of this fit — they're small relative to typical facet-cell sizes,
|
|
194
|
-
* and doing this exactly would mean duplicating `BasePlotLayout`'s own
|
|
195
|
-
* private margin resolution here.
|
|
196
|
-
*/
|
|
197
|
-
const containedSize = $derived.by((): { width: number; height: number } => {
|
|
198
|
-
const full = { width, height };
|
|
199
|
-
if (!dataLonLatBounds) return full;
|
|
200
|
-
const aspect = mercatorAspectRatio(dataLonLatBounds);
|
|
201
|
-
if (!aspect) return full;
|
|
202
|
-
const boxAspect = width / height;
|
|
203
|
-
if (!(boxAspect > 0)) return full;
|
|
204
|
-
return aspect > boxAspect
|
|
205
|
-
? { width, height: width / aspect }
|
|
206
|
-
: { width: height * aspect, height };
|
|
207
|
-
});
|
|
208
|
-
|
|
209
147
|
// ── Tile-layer / projection compatibility ────────────────────────────────
|
|
210
148
|
$effect(() => {
|
|
211
149
|
if (!styles.tileLayer) return;
|
|
@@ -545,8 +483,8 @@
|
|
|
545
483
|
{@const insetContentBox = {
|
|
546
484
|
left: numericMargins.left,
|
|
547
485
|
top: numericMargins.top,
|
|
548
|
-
width: Math.max(0,
|
|
549
|
-
height: Math.max(0,
|
|
486
|
+
width: Math.max(0, width - numericMargins.left - numericMargins.right),
|
|
487
|
+
height: Math.max(0, height - numericMargins.top - numericMargins.bottom),
|
|
550
488
|
}}
|
|
551
489
|
<InsetMarker
|
|
552
490
|
marker={marker as GeoInsetMarker<TProps>}
|
|
@@ -574,10 +512,9 @@
|
|
|
574
512
|
`equirectangular` projection, whose x/y truly are linear in lon/lat) and
|
|
575
513
|
is a no-op for every other mark type.
|
|
576
514
|
-->
|
|
577
|
-
<div class="dct-geo-contain" style:width="{width}px" style:height="{height}px">
|
|
578
515
|
<BasePlotLayout
|
|
579
|
-
|
|
580
|
-
|
|
516
|
+
{width}
|
|
517
|
+
{height}
|
|
581
518
|
data={hoverCandidates}
|
|
582
519
|
getX={hoverGetX}
|
|
583
520
|
getY={hoverGetY}
|
|
@@ -597,14 +534,7 @@
|
|
|
597
534
|
bind:containerEl
|
|
598
535
|
>
|
|
599
536
|
{#snippet children({ matchedPoints, numericMargins })}
|
|
600
|
-
<GeoLayout
|
|
601
|
-
width={containedSize.width}
|
|
602
|
-
height={containedSize.height}
|
|
603
|
-
{numericMargins}
|
|
604
|
-
zoomTransform={geoZoom.transform}
|
|
605
|
-
tileLayer={styles.tileLayer}
|
|
606
|
-
{dataLonLatBounds}
|
|
607
|
-
>
|
|
537
|
+
<GeoLayout {width} {height} {numericMargins} zoomTransform={geoZoom.transform} tileLayer={styles.tileLayer}>
|
|
608
538
|
{#each markers as marker, i (marker.type + '-' + i)}
|
|
609
539
|
{#if isBehindMarker(marker)}
|
|
610
540
|
{@render marker_(marker, numericMargins)}
|
|
@@ -645,17 +575,3 @@
|
|
|
645
575
|
{/if}
|
|
646
576
|
{/snippet}
|
|
647
577
|
</BasePlotLayout>
|
|
648
|
-
</div>
|
|
649
|
-
|
|
650
|
-
<style>
|
|
651
|
-
/**
|
|
652
|
-
* Centers `BasePlotLayout`'s own (possibly smaller, `containedSize`-fit)
|
|
653
|
-
* box within the full area the facet grid actually allotted this plot —
|
|
654
|
-
* see `containedSize`'s doc comment.
|
|
655
|
-
*/
|
|
656
|
-
.dct-geo-contain {
|
|
657
|
-
display: flex;
|
|
658
|
-
align-items: center;
|
|
659
|
-
justify-content: center;
|
|
660
|
-
}
|
|
661
|
-
</style>
|
|
@@ -6,7 +6,6 @@
|
|
|
6
6
|
tileScreenBounds,
|
|
7
7
|
tileUrl,
|
|
8
8
|
toProjectionLike,
|
|
9
|
-
clipBoundsToData,
|
|
10
9
|
type StreamingProjection,
|
|
11
10
|
} from '../utils/tiles';
|
|
12
11
|
import type { GeoTileLayerConfig } from '../../types/plots/styles/geo';
|
|
@@ -27,15 +26,12 @@
|
|
|
27
26
|
top,
|
|
28
27
|
width,
|
|
29
28
|
height,
|
|
30
|
-
dataLonLatBounds,
|
|
31
29
|
}: {
|
|
32
30
|
config: GeoTileLayerConfig;
|
|
33
31
|
left: number;
|
|
34
32
|
top: number;
|
|
35
33
|
width: number;
|
|
36
34
|
height: number;
|
|
37
|
-
/** The rendered geometry's own lon/lat extent — see `clipBoundsToData`. */
|
|
38
|
-
dataLonLatBounds?: [number, number, number, number] | null;
|
|
39
35
|
} = $props();
|
|
40
36
|
|
|
41
37
|
const plot = usePlot();
|
|
@@ -48,18 +44,9 @@
|
|
|
48
44
|
const rawProjection = $derived(plot.scales.projection as unknown as StreamingProjection | undefined);
|
|
49
45
|
const projection = $derived(rawProjection ? toProjectionLike(rawProjection) : undefined);
|
|
50
46
|
|
|
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
|
-
|
|
60
47
|
const tiles = $derived.by(() => {
|
|
61
48
|
if (!projection) return [];
|
|
62
|
-
return visibleTiles(projection,
|
|
49
|
+
return visibleTiles(projection, { left, top, width, height }, tileSize, config.minZoom ?? 0, config.maxZoom ?? 19);
|
|
63
50
|
});
|
|
64
51
|
|
|
65
52
|
// One affine fit per zoom level, shared across every tile in `tiles` — see
|
|
@@ -67,7 +54,7 @@
|
|
|
67
54
|
// each tile's own corners individually.
|
|
68
55
|
const fit = $derived.by(() => {
|
|
69
56
|
if (!projection || tiles.length === 0) return null;
|
|
70
|
-
return buildMercatorFit(projection,
|
|
57
|
+
return buildMercatorFit(projection, { left, top, width, height }, tiles[0].z);
|
|
71
58
|
});
|
|
72
59
|
</script>
|
|
73
60
|
|
|
@@ -5,8 +5,6 @@ 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;
|
|
10
8
|
};
|
|
11
9
|
declare const TileLayer: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
12
10
|
type TileLayer = ReturnType<typeof TileLayer>;
|
|
@@ -8,37 +8,6 @@ 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;
|
|
23
|
-
/**
|
|
24
|
-
* The width:height aspect ratio a Mercator(-family) projection naturally
|
|
25
|
-
* produces for a `[minLon, minLat, maxLon, maxLat]` bounding box — i.e.
|
|
26
|
-
* what a `domain: 'data'`-style projection fit (see `resolveProjection.ts`)
|
|
27
|
-
* implicitly commits to before any letterboxing into a differently-shaped
|
|
28
|
-
* box. `null` for a degenerate box: no lon or lat span, or a latitude
|
|
29
|
-
* at/past ±90° where Mercator's `y` isn't finite.
|
|
30
|
-
*
|
|
31
|
-
* Longitude maps linearly to Mercator's `x`; latitude maps through the
|
|
32
|
-
* standard `ln(tan(π/4 + φ/2))` transform to `y` — the same one
|
|
33
|
-
* `tiles.ts`'s `latToTileY` uses, left unscaled by tile count here since
|
|
34
|
-
* only the *ratio* between the two axes is wanted. Both axes share the same
|
|
35
|
-
* scale by construction (Mercator is conformal), so this ratio is exactly
|
|
36
|
-
* what a real `d3-geo` fit would produce for the same bbox — used by
|
|
37
|
-
* `GeoPlot` to size itself down to its own data's footprint instead of
|
|
38
|
-
* stretching into whatever box a facet grid happens to hand it (see
|
|
39
|
-
* `Plot.svelte`'s `containedSize`).
|
|
40
|
-
*/
|
|
41
|
-
export declare function mercatorAspectRatio(bounds: [number, number, number, number]): number | null;
|
|
42
11
|
/**
|
|
43
12
|
* Splits a geometry into its disjoint parts: one `Polygon` per element of a
|
|
44
13
|
* `MultiPolygon`'s `coordinates`, one `LineString` per element of a
|
|
@@ -61,68 +61,6 @@ 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
|
-
}
|
|
96
|
-
/**
|
|
97
|
-
* The width:height aspect ratio a Mercator(-family) projection naturally
|
|
98
|
-
* produces for a `[minLon, minLat, maxLon, maxLat]` bounding box — i.e.
|
|
99
|
-
* what a `domain: 'data'`-style projection fit (see `resolveProjection.ts`)
|
|
100
|
-
* implicitly commits to before any letterboxing into a differently-shaped
|
|
101
|
-
* box. `null` for a degenerate box: no lon or lat span, or a latitude
|
|
102
|
-
* at/past ±90° where Mercator's `y` isn't finite.
|
|
103
|
-
*
|
|
104
|
-
* Longitude maps linearly to Mercator's `x`; latitude maps through the
|
|
105
|
-
* standard `ln(tan(π/4 + φ/2))` transform to `y` — the same one
|
|
106
|
-
* `tiles.ts`'s `latToTileY` uses, left unscaled by tile count here since
|
|
107
|
-
* only the *ratio* between the two axes is wanted. Both axes share the same
|
|
108
|
-
* scale by construction (Mercator is conformal), so this ratio is exactly
|
|
109
|
-
* what a real `d3-geo` fit would produce for the same bbox — used by
|
|
110
|
-
* `GeoPlot` to size itself down to its own data's footprint instead of
|
|
111
|
-
* stretching into whatever box a facet grid happens to hand it (see
|
|
112
|
-
* `Plot.svelte`'s `containedSize`).
|
|
113
|
-
*/
|
|
114
|
-
export function mercatorAspectRatio(bounds) {
|
|
115
|
-
const [minLon, minLat, maxLon, maxLat] = bounds;
|
|
116
|
-
if (minLat <= -90 || maxLat >= 90)
|
|
117
|
-
return null;
|
|
118
|
-
const toRad = (deg) => (deg * Math.PI) / 180;
|
|
119
|
-
const lonSpan = toRad(maxLon - minLon);
|
|
120
|
-
const mercatorY = (lat) => Math.log(Math.tan(Math.PI / 4 + toRad(lat) / 2));
|
|
121
|
-
const latSpan = Math.abs(mercatorY(maxLat) - mercatorY(minLat));
|
|
122
|
-
if (!(lonSpan > 0) || !(latSpan > 0) || !Number.isFinite(latSpan))
|
|
123
|
-
return null;
|
|
124
|
-
return lonSpan / latSpan;
|
|
125
|
-
}
|
|
126
64
|
/**
|
|
127
65
|
* Splits a geometry into its disjoint parts: one `Polygon` per element of a
|
|
128
66
|
* `MultiPolygon`'s `coordinates`, one `LineString` per element of a
|
|
@@ -36,32 +36,6 @@ 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;
|
|
65
39
|
/**
|
|
66
40
|
* Derives the visible XYZ tile grid from svelteplot's own already-fitted
|
|
67
41
|
* projection (rather than this package computing/duplicating a `d3-geo` fit
|
|
@@ -50,13 +50,14 @@ function estimateZoomLevel(projection, centerLon, centerLat, tileSize) {
|
|
|
50
50
|
return Math.round(Math.log2(ratio));
|
|
51
51
|
}
|
|
52
52
|
/**
|
|
53
|
-
* Clamps `bounds`' horizontal extent to at most
|
|
54
|
-
* the box's own center, before any corner of it
|
|
53
|
+
* Clamps `bounds`' horizontal extent to at most *half* a world-width (180°
|
|
54
|
+
* of longitude), centered on the box's own center, before any corner of it
|
|
55
|
+
* gets inverted.
|
|
55
56
|
*
|
|
56
57
|
* A Mercator(-family) projection wraps a single world onto a *finite* pixel
|
|
57
58
|
* span at any given scale. `visibleTiles`/`buildMercatorFit` invert `bounds`'
|
|
58
59
|
* corners and assume the result increases monotonically left-to-right — true
|
|
59
|
-
* only up to one world-width
|
|
60
|
+
* only up to one world-width; past that the inverted longitude wraps around
|
|
60
61
|
* the antimeridian and comes back the other way, silently reversing the
|
|
61
62
|
* corner ordering (`buildMercatorFit` was seen to compute a *negative*
|
|
62
63
|
* `pxPerTileX` from this). This bites specifically when a plot's actual
|
|
@@ -64,9 +65,19 @@ function estimateZoomLevel(projection, centerLon, centerLat, tileSize) {
|
|
|
64
65
|
* ratio does — e.g. a tall, narrow country's choropleth fit into a short,
|
|
65
66
|
* wide facet-grid cell: the fit shrinks scale to match the constraining
|
|
66
67
|
* (height) axis, and the *unconstrained* (width) axis's letterboxed margin
|
|
67
|
-
* can then
|
|
68
|
-
*
|
|
69
|
-
*
|
|
68
|
+
* can then imply more of the earth than actually exists.
|
|
69
|
+
*
|
|
70
|
+
* The cap is *half* a world rather than a full one specifically so the two
|
|
71
|
+
* edges of an extreme letterboxed margin can never reach all the way out to
|
|
72
|
+
* the region exactly opposite the map's own — a full-world cap's two edges
|
|
73
|
+
* meet exactly at that antipode (e.g. Argentina's own facet map, letterboxed
|
|
74
|
+
* enough to hit a full-world cap, would show Buenos Aires' antipode —
|
|
75
|
+
* Australia — prominently at both edges). Halving it keeps every visible
|
|
76
|
+
* tile within one quarter-turn of the data's own center, which for any real
|
|
77
|
+
* facet-grid cell is already a deliberately generous amount of basemap
|
|
78
|
+
* context (ocean, neighboring countries) — typical facet aspect ratios don't
|
|
79
|
+
* come close to needing the full 180° this still allows before clamping
|
|
80
|
+
* kicks in at all.
|
|
70
81
|
*/
|
|
71
82
|
function clampBoundsToWorld(projection, bounds) {
|
|
72
83
|
if (!projection.invert)
|
|
@@ -80,59 +91,10 @@ function clampBoundsToWorld(projection, bounds) {
|
|
|
80
91
|
const east = projection([180, centerLonLat[1]]);
|
|
81
92
|
if (!west || !east)
|
|
82
93
|
return bounds;
|
|
83
|
-
const
|
|
84
|
-
if (!(
|
|
85
|
-
return bounds;
|
|
86
|
-
return { ...bounds, left: centerX - worldWidth / 2, width: worldWidth };
|
|
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))
|
|
94
|
+
const maxWidth = Math.abs(east[0] - west[0]) / 2;
|
|
95
|
+
if (!(maxWidth > 0) || bounds.width <= maxWidth)
|
|
134
96
|
return bounds;
|
|
135
|
-
return {
|
|
97
|
+
return { ...bounds, left: centerX - maxWidth / 2, width: maxWidth };
|
|
136
98
|
}
|
|
137
99
|
/**
|
|
138
100
|
* Derives the visible XYZ tile grid from svelteplot's own already-fitted
|