@fundar/data-chart-telling 0.0.14 → 0.0.16
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 → BaseChart.svelte} +2 -0
- 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 +2 -1
- package/dist/configuration/themes/index.d.ts +60 -40
- package/dist/index.d.ts +27 -13
- 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/ContinuousSection.svelte +158 -17
- package/dist/layout/legend/DiscreteSection.svelte +56 -4
- package/dist/layout/legend/interaction.svelte.d.ts +7 -0
- package/dist/layout/legend/interaction.svelte.js +24 -0
- 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 +69 -0
- package/dist/plots/bar/BarSegments.svelte.d.ts +40 -0
- package/dist/plots/bar/Plot.svelte +140 -74
- 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 +577 -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 +171 -58
- package/dist/plots/heatmap/Plot.svelte.d.ts +7 -3
- package/dist/plots/line/Plot.svelte +67 -30
- package/dist/plots/line/Plot.svelte.d.ts +7 -3
- package/dist/plots/pyramid/Plot.svelte +88 -35
- 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/legendDisabled.d.ts +15 -0
- package/dist/plots/utils/legendDisabled.js +41 -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 +32 -1
- package/dist/types/charts/props.d.ts +9 -6
- package/dist/types/configuration/styling.d.ts +13 -5
- package/dist/types/layout/legend.d.ts +26 -0
- 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/markers/props.js +1 -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/types/{plots/common.js → layout/legend.js} +0 -0
- /package/dist/types/{plots/markers.js → markers/common.js} +0 -0
- /package/dist/types/{plots/styles.js → markers/geo.js} +0 -0
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
function flattenCoords(geom, out) {
|
|
2
|
+
switch (geom.type) {
|
|
3
|
+
case 'Point':
|
|
4
|
+
out.push(geom.coordinates);
|
|
5
|
+
break;
|
|
6
|
+
case 'MultiPoint':
|
|
7
|
+
case 'LineString':
|
|
8
|
+
for (const c of geom.coordinates)
|
|
9
|
+
out.push(c);
|
|
10
|
+
break;
|
|
11
|
+
case 'MultiLineString':
|
|
12
|
+
case 'Polygon':
|
|
13
|
+
for (const ring of geom.coordinates) {
|
|
14
|
+
for (const c of ring)
|
|
15
|
+
out.push(c);
|
|
16
|
+
}
|
|
17
|
+
break;
|
|
18
|
+
case 'MultiPolygon':
|
|
19
|
+
for (const poly of geom.coordinates) {
|
|
20
|
+
for (const ring of poly) {
|
|
21
|
+
for (const c of ring)
|
|
22
|
+
out.push(c);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
break;
|
|
26
|
+
case 'GeometryCollection':
|
|
27
|
+
for (const g of geom.geometries)
|
|
28
|
+
flattenCoords(g, out);
|
|
29
|
+
break;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function boundsCenter(coords) {
|
|
33
|
+
if (coords.length === 0)
|
|
34
|
+
return [0, 0];
|
|
35
|
+
let minX = Infinity;
|
|
36
|
+
let maxX = -Infinity;
|
|
37
|
+
let minY = Infinity;
|
|
38
|
+
let maxY = -Infinity;
|
|
39
|
+
for (const [x, y] of coords) {
|
|
40
|
+
if (x < minX)
|
|
41
|
+
minX = x;
|
|
42
|
+
if (x > maxX)
|
|
43
|
+
maxX = x;
|
|
44
|
+
if (y < minY)
|
|
45
|
+
minY = y;
|
|
46
|
+
if (y > maxY)
|
|
47
|
+
maxY = y;
|
|
48
|
+
}
|
|
49
|
+
return [(minX + maxX) / 2, (minY + maxY) / 2];
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Bounding-box center of a geometry's flattened coordinates — a cheap
|
|
53
|
+
* approximation used for default label placement, not a true
|
|
54
|
+
* geographic/area centroid (no polygon-hole or geodesic-area weighting).
|
|
55
|
+
* For hover-matching specifically, see `geometryPartCentroids` instead —
|
|
56
|
+
* this single combined point can land far from every actual part of a
|
|
57
|
+
* multi-part geometry whose pieces are geographically spread out.
|
|
58
|
+
*/
|
|
59
|
+
export function geometryCentroid(geometry) {
|
|
60
|
+
const coords = [];
|
|
61
|
+
flattenCoords(geometry, coords);
|
|
62
|
+
return boundsCenter(coords);
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Splits a geometry into its disjoint parts: one `Polygon` per element of a
|
|
66
|
+
* `MultiPolygon`'s `coordinates`, one `LineString` per element of a
|
|
67
|
+
* `MultiLineString`'s, `GeometryCollection` recursed and flattened, and any
|
|
68
|
+
* other geometry type returned unchanged as its own single-element array.
|
|
69
|
+
* The shared building block behind `geometryPartCentroids` (one centroid per
|
|
70
|
+
* part) and `filterGeometryParts` (keep/drop whole parts).
|
|
71
|
+
*/
|
|
72
|
+
export function splitGeometryParts(geometry) {
|
|
73
|
+
switch (geometry.type) {
|
|
74
|
+
case 'MultiPolygon':
|
|
75
|
+
return geometry.coordinates.map((polygon) => ({ type: 'Polygon', coordinates: polygon }));
|
|
76
|
+
case 'MultiLineString':
|
|
77
|
+
return geometry.coordinates.map((line) => ({ type: 'LineString', coordinates: line }));
|
|
78
|
+
case 'GeometryCollection':
|
|
79
|
+
return geometry.geometries.flatMap((g) => splitGeometryParts(g));
|
|
80
|
+
default:
|
|
81
|
+
return [geometry];
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Like `geometryCentroid`, but returns one point per disjoint part instead
|
|
86
|
+
* of a single bounding-box center across all of them. A `MultiPolygon`
|
|
87
|
+
* whose parts are geographically far apart — e.g. Argentina's "Tierra del
|
|
88
|
+
* Fuego, Antártida e Islas del Atlántico Sur" province, whose `MultiPolygon`
|
|
89
|
+
* spans the mainland island, the Antarctic sector, and the South Atlantic
|
|
90
|
+
* islands in one feature — would otherwise get a single hover point
|
|
91
|
+
* stranded somewhere between all of them, matching hovers over *none* of
|
|
92
|
+
* the actually-rendered shapes. Multiple points attributed to the same
|
|
93
|
+
* feature is fine for hover-matching (nearest point wins, whichever part
|
|
94
|
+
* that is); it's only label placement (`geometryCentroid`) that needs a
|
|
95
|
+
* single point.
|
|
96
|
+
*/
|
|
97
|
+
export function geometryPartCentroids(geometry) {
|
|
98
|
+
return splitGeometryParts(geometry).map((part) => geometryCentroid(part));
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Keeps or drops each disjoint part of a geometry (see `splitGeometryParts`)
|
|
102
|
+
* based on a per-part predicate, reassembling a geometry of the same type
|
|
103
|
+
* from whatever survives — `null` if nothing does. Used to let a single
|
|
104
|
+
* feature's geometry be split between the main map and an `inset` marker
|
|
105
|
+
* (see `GeoDataProps.excludeParts`/`GeoInsetMarker.featurePartsFilter`)
|
|
106
|
+
* without preprocessing the source data: e.g. a province whose `MultiPolygon`
|
|
107
|
+
* spans both a mainland part and a far-flung part can have the main map
|
|
108
|
+
* exclude the latter while an inset shows only it.
|
|
109
|
+
*/
|
|
110
|
+
export function filterGeometryParts(geometry, keep) {
|
|
111
|
+
const parts = splitGeometryParts(geometry);
|
|
112
|
+
const keptParts = parts.filter((part) => keep({ centroid: geometryCentroid(part), geometry: part }));
|
|
113
|
+
if (keptParts.length === parts.length)
|
|
114
|
+
return geometry;
|
|
115
|
+
if (keptParts.length === 0)
|
|
116
|
+
return null;
|
|
117
|
+
if (parts.length === 1)
|
|
118
|
+
return keptParts[0];
|
|
119
|
+
switch (geometry.type) {
|
|
120
|
+
case 'MultiPolygon':
|
|
121
|
+
return { type: 'MultiPolygon', coordinates: keptParts.map((part) => part.coordinates) };
|
|
122
|
+
case 'MultiLineString':
|
|
123
|
+
return {
|
|
124
|
+
type: 'MultiLineString',
|
|
125
|
+
coordinates: keptParts.map((part) => part.coordinates),
|
|
126
|
+
};
|
|
127
|
+
case 'GeometryCollection':
|
|
128
|
+
return { type: 'GeometryCollection', geometries: keptParts };
|
|
129
|
+
default:
|
|
130
|
+
return geometry;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
/** Default `featureId`: `feature.id`, else `properties.id`, else `properties.name`. */
|
|
134
|
+
export function defaultFeatureId(f) {
|
|
135
|
+
const props = f.properties;
|
|
136
|
+
return String(f.id ?? props?.id ?? props?.name ?? '');
|
|
137
|
+
}
|
|
138
|
+
function toRad(deg) {
|
|
139
|
+
return (deg * Math.PI) / 180;
|
|
140
|
+
}
|
|
141
|
+
function toDeg(rad) {
|
|
142
|
+
return (rad * 180) / Math.PI;
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Densifies a great-circle arc between two lon/lat points into `steps + 1`
|
|
146
|
+
* points via spherical linear interpolation (slerp) — used to draw a `link`
|
|
147
|
+
* marker's geodesic curve through the `Geo` mark (see `Plot.svelte`).
|
|
148
|
+
*
|
|
149
|
+
* Not delegated to svelteplot's own `Link` mark: its 0.14.2 projection
|
|
150
|
+
* integration (`Mark.svelte`'s paired-channel handling) reads a single
|
|
151
|
+
* `row.x`/`row.y` pair regardless of channel suffix, so a mark bound to
|
|
152
|
+
* `x1`/`y1`/`x2`/`y2` (never plain `x`/`y`) resolves every point as invalid
|
|
153
|
+
* whenever the plot has an active `projection` scale — which GeoPlot always
|
|
154
|
+
* does. `Geo` has no such limitation (geo marks are explicitly excluded from
|
|
155
|
+
* that code path), so a manually-interpolated `LineString` feature sidesteps
|
|
156
|
+
* the bug entirely while still producing the same geodesic visual.
|
|
157
|
+
*/
|
|
158
|
+
export function greatCircleInterpolate(lon1, lat1, lon2, lat2, steps = 64) {
|
|
159
|
+
const phi1 = toRad(lat1);
|
|
160
|
+
const lambda1 = toRad(lon1);
|
|
161
|
+
const phi2 = toRad(lat2);
|
|
162
|
+
const lambda2 = toRad(lon2);
|
|
163
|
+
const sinPhi1 = Math.sin(phi1);
|
|
164
|
+
const cosPhi1 = Math.cos(phi1);
|
|
165
|
+
const sinPhi2 = Math.sin(phi2);
|
|
166
|
+
const cosPhi2 = Math.cos(phi2);
|
|
167
|
+
const d = 2 *
|
|
168
|
+
Math.asin(Math.sqrt(Math.sin((phi2 - phi1) / 2) ** 2 + cosPhi1 * cosPhi2 * Math.sin((lambda2 - lambda1) / 2) ** 2));
|
|
169
|
+
if (!Number.isFinite(d) || d === 0) {
|
|
170
|
+
return [
|
|
171
|
+
[lon1, lat1],
|
|
172
|
+
[lon2, lat2],
|
|
173
|
+
];
|
|
174
|
+
}
|
|
175
|
+
const points = [];
|
|
176
|
+
for (let i = 0; i <= steps; i++) {
|
|
177
|
+
const f = i / steps;
|
|
178
|
+
const a = Math.sin((1 - f) * d) / Math.sin(d);
|
|
179
|
+
const b = Math.sin(f * d) / Math.sin(d);
|
|
180
|
+
const x = a * cosPhi1 * Math.cos(lambda1) + b * cosPhi2 * Math.cos(lambda2);
|
|
181
|
+
const y = a * cosPhi1 * Math.sin(lambda1) + b * cosPhi2 * Math.sin(lambda2);
|
|
182
|
+
const z = a * sinPhi1 + b * sinPhi2;
|
|
183
|
+
const phi = Math.atan2(z, Math.sqrt(x * x + y * y));
|
|
184
|
+
const lambda = Math.atan2(y, x);
|
|
185
|
+
points.push([toDeg(lambda), toDeg(phi)]);
|
|
186
|
+
}
|
|
187
|
+
return points;
|
|
188
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Segment } from '../../types/plots/segments/config';
|
|
2
|
+
import type { SegmentsConfig } from '../../types/plots/props';
|
|
3
|
+
/** Feature-specific segments listed after 'default' so they override it on merge (see `resolveFeatureStyle`). */
|
|
4
|
+
export declare function resolveSegmentsForFeature<TStyle>(segments: SegmentsConfig<TStyle>, featureId: string): Segment<TStyle>[];
|
|
5
|
+
/**
|
|
6
|
+
* A geo feature is atomic — `Segment.areas` (x/y range restriction) doesn't
|
|
7
|
+
* apply, so every entry under a key is treated as a flat style patch applied
|
|
8
|
+
* in order: `'default'` entries apply first, feature-specific entries
|
|
9
|
+
* override. This intentionally does NOT reuse `segments.ts`'s
|
|
10
|
+
* `validateSegments` — its "only the first catch-all wins" warning assumes
|
|
11
|
+
* area-based semantics that don't hold here, since every geo segment entry
|
|
12
|
+
* merges rather than being dropped.
|
|
13
|
+
*/
|
|
14
|
+
export declare function resolveFeatureStyle<TStyle extends object>(segments: SegmentsConfig<TStyle>, featureId: string): TStyle;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
const DEFAULT_KEY = 'default';
|
|
2
|
+
/** Feature-specific segments listed after 'default' so they override it on merge (see `resolveFeatureStyle`). */
|
|
3
|
+
export function resolveSegmentsForFeature(segments, featureId) {
|
|
4
|
+
return [...(segments[DEFAULT_KEY] ?? []), ...(segments[featureId] ?? [])];
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* A geo feature is atomic — `Segment.areas` (x/y range restriction) doesn't
|
|
8
|
+
* apply, so every entry under a key is treated as a flat style patch applied
|
|
9
|
+
* in order: `'default'` entries apply first, feature-specific entries
|
|
10
|
+
* override. This intentionally does NOT reuse `segments.ts`'s
|
|
11
|
+
* `validateSegments` — its "only the first catch-all wins" warning assumes
|
|
12
|
+
* area-based semantics that don't hold here, since every geo segment entry
|
|
13
|
+
* merges rather than being dropped.
|
|
14
|
+
*/
|
|
15
|
+
export function resolveFeatureStyle(segments, featureId) {
|
|
16
|
+
return resolveSegmentsForFeature(segments, featureId).reduce((acc, seg) => ({ ...acc, ...(seg.style ?? {}) }), {});
|
|
17
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { LegendDisabledStyle } from '../../types/charts/legend';
|
|
2
|
+
import type { StrokeStyle, DotStyle } from '../../types/plots/styling';
|
|
3
|
+
/**
|
|
4
|
+
* The three per-mark override layers a legend "disabled" state can apply,
|
|
5
|
+
* derived once from a {@link LegendDisabledStyle}. Each is `undefined` when
|
|
6
|
+
* there's no active override, so call sites can slot the result in as just
|
|
7
|
+
* one more `??` layer ahead of their existing segment-style/default-color
|
|
8
|
+
* chain — no branching needed at the call site.
|
|
9
|
+
*/
|
|
10
|
+
export declare function disabledStrokeOverride(disabled: LegendDisabledStyle | undefined): StrokeStyle | undefined;
|
|
11
|
+
export declare function disabledDotsOverride(disabled: LegendDisabledStyle | undefined): DotStyle | undefined;
|
|
12
|
+
export declare function disabledFillOverride(disabled: LegendDisabledStyle | undefined): {
|
|
13
|
+
fill?: string;
|
|
14
|
+
fillOpacity?: number;
|
|
15
|
+
} | undefined;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The three per-mark override layers a legend "disabled" state can apply,
|
|
3
|
+
* derived once from a {@link LegendDisabledStyle}. Each is `undefined` when
|
|
4
|
+
* there's no active override, so call sites can slot the result in as just
|
|
5
|
+
* one more `??` layer ahead of their existing segment-style/default-color
|
|
6
|
+
* chain — no branching needed at the call site.
|
|
7
|
+
*/
|
|
8
|
+
export function disabledStrokeOverride(disabled) {
|
|
9
|
+
if (!disabled)
|
|
10
|
+
return undefined;
|
|
11
|
+
return {
|
|
12
|
+
stroke: disabled.stroke?.stroke,
|
|
13
|
+
strokeWidth: disabled.stroke?.strokeWidth,
|
|
14
|
+
strokeOpacity: disabled.stroke?.strokeOpacity ?? disabled.opacity,
|
|
15
|
+
strokeDasharray: disabled.stroke?.strokeDasharray,
|
|
16
|
+
strokeLinecap: disabled.stroke?.strokeLinecap,
|
|
17
|
+
strokeLinejoin: disabled.stroke?.strokeLinejoin,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
export function disabledDotsOverride(disabled) {
|
|
21
|
+
if (!disabled)
|
|
22
|
+
return undefined;
|
|
23
|
+
return {
|
|
24
|
+
dotRadius: disabled.dots?.dotRadius,
|
|
25
|
+
dotFill: disabled.dots?.dotFill,
|
|
26
|
+
dotFillOpacity: disabled.dots?.dotFillOpacity ?? disabled.opacity,
|
|
27
|
+
dotSymbol: disabled.dots?.dotSymbol,
|
|
28
|
+
dotStroke: disabled.dots?.dotStroke,
|
|
29
|
+
dotStrokeWidth: disabled.dots?.dotStrokeWidth,
|
|
30
|
+
dotStrokeOpacity: disabled.dots?.dotStrokeOpacity ?? disabled.opacity,
|
|
31
|
+
dotStrokeDasharray: disabled.dots?.dotStrokeDasharray,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
export function disabledFillOverride(disabled) {
|
|
35
|
+
if (!disabled)
|
|
36
|
+
return undefined;
|
|
37
|
+
return {
|
|
38
|
+
fill: disabled.fill,
|
|
39
|
+
fillOpacity: disabled.fillOpacity ?? disabled.opacity,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
@@ -1,11 +1,14 @@
|
|
|
1
|
-
import type { AxisValue
|
|
2
|
-
import type {
|
|
1
|
+
import type { AxisValue } from '../../types/plots/axis';
|
|
2
|
+
import type { Series } from '../../types/plots/data/common';
|
|
3
|
+
import type { LineSegmentStyle } from '../../types/plots/segments/common';
|
|
4
|
+
import type { Segment, VisualGroup } from '../../types/plots/segments/config';
|
|
5
|
+
import type { SegmentsConfig } from '../../types/plots/props';
|
|
3
6
|
/**
|
|
4
7
|
* Returns the flat {@link Segment} list for one series.
|
|
5
8
|
* Series-specific segments are added before `'default'` segments so that
|
|
6
9
|
* series-specific catch-alls win over default catch-alls on tie-breaks.
|
|
7
10
|
*/
|
|
8
|
-
export declare function resolveSegmentsForSeries<TStyle>(segments:
|
|
11
|
+
export declare function resolveSegmentsForSeries<TStyle>(segments: SegmentsConfig<TStyle>, seriesName: string): Segment<TStyle>[];
|
|
9
12
|
/** Returns true when `xVal` falls inside any of a segment's x ranges. */
|
|
10
13
|
export declare function matchesSegmentX<TStyle>(xVal: AxisValue, seg: Segment<TStyle>): boolean;
|
|
11
14
|
/**
|
|
@@ -22,10 +25,16 @@ export declare function getDataForSegments<TData, TStyle>(data: TData[], xAccess
|
|
|
22
25
|
* (no `areas`) in the same series list. Since catch-alls are evaluated last
|
|
23
26
|
* and in order, only the first one can ever match; later ones are dead code.
|
|
24
27
|
*/
|
|
25
|
-
export declare function validateSegments<TStyle>(segments:
|
|
28
|
+
export declare function validateSegments<TStyle>(segments: SegmentsConfig<TStyle>): void;
|
|
26
29
|
/**
|
|
27
30
|
* Builds the full list of visual groups — one per series — ready to render.
|
|
28
31
|
* Each group's `visualSegments` is derived by resolving the flat segment list
|
|
29
32
|
* for that series from all groups in `segments`.
|
|
33
|
+
*
|
|
34
|
+
* `connect` bridges adjacent visual segments so a *continuous* mark (line)
|
|
35
|
+
* has no visual gap at their boundary — the default, since `buildGroupedSeries`
|
|
36
|
+
* itself defaults `TStyle` to {@link LineSegmentStyle}. Discrete marks (bar,
|
|
37
|
+
* pyramid) have no gap to bridge; pass `connect: false` so boundary rows
|
|
38
|
+
* aren't duplicated as an extra, separately-styled mark.
|
|
30
39
|
*/
|
|
31
|
-
export declare function buildGroupedSeries<S extends Series<any>, TStyle = LineSegmentStyle>(seriesList: S[], segments:
|
|
40
|
+
export declare function buildGroupedSeries<S extends Series<any>, TStyle = LineSegmentStyle>(seriesList: S[], segments: SegmentsConfig<TStyle>, connect?: boolean): VisualGroup<S, TStyle>[];
|
|
@@ -90,7 +90,7 @@ function mergeStyles(base, override) {
|
|
|
90
90
|
return result;
|
|
91
91
|
}
|
|
92
92
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
93
|
-
function resolveSeriesSegments(series, segs) {
|
|
93
|
+
function resolveSeriesSegments(series, segs, connect) {
|
|
94
94
|
if (!segs.length) {
|
|
95
95
|
return [{ data: series.data, style: {} }];
|
|
96
96
|
}
|
|
@@ -147,14 +147,18 @@ function resolveSeriesSegments(series, segs) {
|
|
|
147
147
|
for (let r = 0; r < runs.length; r++) {
|
|
148
148
|
const run = runs[r];
|
|
149
149
|
if (run.segIdx === -1) {
|
|
150
|
-
// Unmatched run:
|
|
151
|
-
// user segments without a gap.
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
bridged
|
|
156
|
-
if (
|
|
157
|
-
|
|
150
|
+
// Unmatched run: for a continuous mark (line), insert bridge points so
|
|
151
|
+
// it connects to adjacent user segments without a visual gap. Discrete
|
|
152
|
+
// marks (bar, pyramid) have no such gap to bridge — bridging would just
|
|
153
|
+
// duplicate the neighboring runs' boundary rows as an extra, wrongly
|
|
154
|
+
// styled mark at that same category.
|
|
155
|
+
const bridged = [...run.data];
|
|
156
|
+
if (connect) {
|
|
157
|
+
if (r > 0)
|
|
158
|
+
bridged.unshift(runs[r - 1].data[runs[r - 1].data.length - 1]);
|
|
159
|
+
if (r < runs.length - 1)
|
|
160
|
+
bridged.push(runs[r + 1].data[0]);
|
|
161
|
+
}
|
|
158
162
|
defaultVisual.push({ data: bridged, style: {} });
|
|
159
163
|
}
|
|
160
164
|
else {
|
|
@@ -170,9 +174,11 @@ function resolveSeriesSegments(series, segs) {
|
|
|
170
174
|
// When the next run is also a user segment (no unmatched data between them),
|
|
171
175
|
// insert a line-only connector so the lines join without a gap. Using a
|
|
172
176
|
// separate mark (rather than appending a bridge point to the current run)
|
|
173
|
-
// avoids rendering an extra dot at the shared boundary point.
|
|
177
|
+
// avoids rendering an extra dot at the shared boundary point. Skipped
|
|
178
|
+
// entirely for discrete marks (see above) — it would duplicate both
|
|
179
|
+
// runs' boundary rows as an extra mark.
|
|
174
180
|
const nextRun = runs[r + 1];
|
|
175
|
-
if (nextRun && nextRun.segIdx !== -1) {
|
|
181
|
+
if (connect && nextRun && nextRun.segIdx !== -1) {
|
|
176
182
|
connectors.push({
|
|
177
183
|
data: [run.data[run.data.length - 1], nextRun.data[0]],
|
|
178
184
|
style: connectorStyle,
|
|
@@ -190,10 +196,16 @@ function resolveSeriesSegments(series, segs) {
|
|
|
190
196
|
* Builds the full list of visual groups — one per series — ready to render.
|
|
191
197
|
* Each group's `visualSegments` is derived by resolving the flat segment list
|
|
192
198
|
* for that series from all groups in `segments`.
|
|
199
|
+
*
|
|
200
|
+
* `connect` bridges adjacent visual segments so a *continuous* mark (line)
|
|
201
|
+
* has no visual gap at their boundary — the default, since `buildGroupedSeries`
|
|
202
|
+
* itself defaults `TStyle` to {@link LineSegmentStyle}. Discrete marks (bar,
|
|
203
|
+
* pyramid) have no gap to bridge; pass `connect: false` so boundary rows
|
|
204
|
+
* aren't duplicated as an extra, separately-styled mark.
|
|
193
205
|
*/
|
|
194
|
-
export function buildGroupedSeries(seriesList, segments) {
|
|
206
|
+
export function buildGroupedSeries(seriesList, segments, connect = true) {
|
|
195
207
|
return seriesList.map((s) => ({
|
|
196
208
|
series: s,
|
|
197
|
-
visualSegments: resolveSeriesSegments(s, resolveSegmentsForSeries(segments, s.name)),
|
|
209
|
+
visualSegments: resolveSeriesSegments(s, resolveSegmentsForSeries(segments, s.name), connect),
|
|
198
210
|
}));
|
|
199
211
|
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
export type TileCoord = {
|
|
2
|
+
x: number;
|
|
3
|
+
y: number;
|
|
4
|
+
z: number;
|
|
5
|
+
};
|
|
6
|
+
export type ScreenBounds = {
|
|
7
|
+
left: number;
|
|
8
|
+
top: number;
|
|
9
|
+
width: number;
|
|
10
|
+
height: number;
|
|
11
|
+
};
|
|
12
|
+
/** The subset of a `d3-geo`-style projection this module needs. */
|
|
13
|
+
export type ProjectionLike = {
|
|
14
|
+
(point: [number, number]): [number, number] | null;
|
|
15
|
+
invert?(point: [number, number]): [number, number] | null;
|
|
16
|
+
};
|
|
17
|
+
/** The minimal geometry-stream shape a projection's `.stream()` accepts — kept local instead of importing `d3-geo`'s own `GeoStream` type, so this module stays dependency-free. */
|
|
18
|
+
type MinimalStream = {
|
|
19
|
+
point: (x: number, y: number, z?: number) => void;
|
|
20
|
+
lineStart: () => void;
|
|
21
|
+
lineEnd: () => void;
|
|
22
|
+
polygonStart: () => void;
|
|
23
|
+
polygonEnd: () => void;
|
|
24
|
+
sphere?: () => void;
|
|
25
|
+
};
|
|
26
|
+
/** What `toProjectionLike` needs from a svelteplot-resolved projection. */
|
|
27
|
+
export type StreamingProjection = {
|
|
28
|
+
stream: (s: MinimalStream) => MinimalStream;
|
|
29
|
+
invert?: (point: [number, number]) => [number, number] | null;
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* Adapts a svelteplot-resolved projection (which exposes `.stream()`/
|
|
33
|
+
* `.invert()`, but — unlike a raw `d3-geo` projection instance — isn't
|
|
34
|
+
* itself callable as `(point) => [x, y]`) into a `ProjectionLike`, by
|
|
35
|
+
* feeding a single point through its stream pipeline and capturing the
|
|
36
|
+
* projected output.
|
|
37
|
+
*/
|
|
38
|
+
export declare function toProjectionLike(projection: StreamingProjection): ProjectionLike;
|
|
39
|
+
/**
|
|
40
|
+
* Derives the visible XYZ tile grid from svelteplot's own already-fitted
|
|
41
|
+
* projection (rather than this package computing/duplicating a `d3-geo` fit
|
|
42
|
+
* itself) — see `TileLayer.svelte`. Only correct for Mercator-family
|
|
43
|
+
* projections, where a screen-space rectangle maps back to a lon/lat bbox
|
|
44
|
+
* whose tile coverage is well-defined; callers are responsible for the
|
|
45
|
+
* projection-compatibility check (see `Plot.svelte`'s validation effect).
|
|
46
|
+
*/
|
|
47
|
+
export declare function visibleTiles(projection: ProjectionLike, bounds: ScreenBounds, tileSize?: number, minZoom?: number, maxZoom?: number): TileCoord[];
|
|
48
|
+
/** An affine tile-space → screen-space mapping (see `buildMercatorFit`), reused across every tile in a grid instead of forward-projecting each one individually. */
|
|
49
|
+
export type MercatorFit = {
|
|
50
|
+
z: number;
|
|
51
|
+
screenLeft: number;
|
|
52
|
+
screenTop: number;
|
|
53
|
+
tileXAtLeft: number;
|
|
54
|
+
tileYAtTop: number;
|
|
55
|
+
pxPerTileX: number;
|
|
56
|
+
pxPerTileY: number;
|
|
57
|
+
};
|
|
58
|
+
/**
|
|
59
|
+
* Builds an affine tile-space → screen-space mapping from two reliable
|
|
60
|
+
* anchor points — `bounds`' own top-left/bottom-right corners, inverted
|
|
61
|
+
* back to lon/lat — rather than forward-projecting each tile's own corners
|
|
62
|
+
* individually (what this replaced). That per-tile approach broke under a
|
|
63
|
+
* projection whose scale/translate were fit to a *custom* domain rather
|
|
64
|
+
* than set via its own `.scale()`/`.translate()`: `d3-geo`'s cylindrical
|
|
65
|
+
* projections (mercator included) auto-install an internal `clipExtent`
|
|
66
|
+
* sized to whatever scale/translate they were constructed or last
|
|
67
|
+
* `.scale()`/`.translate()`-called with (guarding against the y → ±Infinity
|
|
68
|
+
* blowup near the poles); svelteplot's own domain-fitting wraps the
|
|
69
|
+
* *unmodified* projection instance in an external transform instead of
|
|
70
|
+
* calling those setters, so that internal clip keeps silently rejecting
|
|
71
|
+
* forward-projected points outside its stale bounds — nulling out most of
|
|
72
|
+
* a tile grid even for tiles nowhere near a pole. `.invert()` isn't routed
|
|
73
|
+
* through that clip, so anchoring to two known-good inverted corners
|
|
74
|
+
* sidesteps it entirely — sound because Mercator's tile space (longitude,
|
|
75
|
+
* and the standard log-tan latitude transform used by `latToTileY`) is
|
|
76
|
+
* affine in screen space by construction, so one linear fit applies
|
|
77
|
+
* uniformly to every tile in the grid.
|
|
78
|
+
*/
|
|
79
|
+
export declare function buildMercatorFit(projection: ProjectionLike, bounds: ScreenBounds, z: number): MercatorFit | null;
|
|
80
|
+
/** Places a tile on screen via a `MercatorFit` — `null` only if `tile` is from a different zoom level than the fit was built for. */
|
|
81
|
+
export declare function tileScreenBounds(fit: MercatorFit, tile: TileCoord): ScreenBounds | null;
|
|
82
|
+
/**
|
|
83
|
+
* Fills `{z}/{x}/{y}` placeholders in a tile URL template. Also supports
|
|
84
|
+
* `{-y}` for TMS-scheme sources (e.g. IGN's tile services), whose row index
|
|
85
|
+
* counts from the south instead of the north — the standard XYZ-to-TMS
|
|
86
|
+
* conversion `2^z - 1 - y`.
|
|
87
|
+
*/
|
|
88
|
+
export declare function tileUrl(template: string, tile: TileCoord): string;
|
|
89
|
+
export {};
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Adapts a svelteplot-resolved projection (which exposes `.stream()`/
|
|
3
|
+
* `.invert()`, but — unlike a raw `d3-geo` projection instance — isn't
|
|
4
|
+
* itself callable as `(point) => [x, y]`) into a `ProjectionLike`, by
|
|
5
|
+
* feeding a single point through its stream pipeline and capturing the
|
|
6
|
+
* projected output.
|
|
7
|
+
*/
|
|
8
|
+
export function toProjectionLike(projection) {
|
|
9
|
+
const fn = ((point) => {
|
|
10
|
+
let result = null;
|
|
11
|
+
const sink = {
|
|
12
|
+
point: (x, y) => {
|
|
13
|
+
result = [x, y];
|
|
14
|
+
},
|
|
15
|
+
lineStart() { },
|
|
16
|
+
lineEnd() { },
|
|
17
|
+
polygonStart() { },
|
|
18
|
+
polygonEnd() { },
|
|
19
|
+
};
|
|
20
|
+
projection.stream(sink).point(point[0], point[1]);
|
|
21
|
+
return result;
|
|
22
|
+
});
|
|
23
|
+
if (projection.invert)
|
|
24
|
+
fn.invert = (point) => projection.invert(point);
|
|
25
|
+
return fn;
|
|
26
|
+
}
|
|
27
|
+
function lonToTileX(lon, z) {
|
|
28
|
+
return ((lon + 180) / 360) * 2 ** z;
|
|
29
|
+
}
|
|
30
|
+
function latToTileY(lat, z) {
|
|
31
|
+
const rad = (lat * Math.PI) / 180;
|
|
32
|
+
return ((1 - Math.log(Math.tan(rad) + 1 / Math.cos(rad)) / Math.PI) / 2) * 2 ** z;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Picks an integer XYZ zoom level whose tile pixel density roughly matches
|
|
36
|
+
* the projection's *current* pixels-per-degree at the map center — so tiles
|
|
37
|
+
* neither look blurry (too coarse) nor waste bandwidth (too fine).
|
|
38
|
+
*/
|
|
39
|
+
function estimateZoomLevel(projection, centerLon, centerLat, tileSize) {
|
|
40
|
+
const eps = 0.01;
|
|
41
|
+
const p0 = projection([centerLon, centerLat]);
|
|
42
|
+
const p1 = projection([centerLon + eps, centerLat]);
|
|
43
|
+
if (!p0 || !p1)
|
|
44
|
+
return null;
|
|
45
|
+
const pixelsPerDegree = Math.abs(p1[0] - p0[0]) / eps;
|
|
46
|
+
const worldPixelsAtZ0 = tileSize / 360;
|
|
47
|
+
const ratio = pixelsPerDegree / worldPixelsAtZ0;
|
|
48
|
+
if (!Number.isFinite(ratio) || ratio <= 0)
|
|
49
|
+
return null;
|
|
50
|
+
return Math.round(Math.log2(ratio));
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Derives the visible XYZ tile grid from svelteplot's own already-fitted
|
|
54
|
+
* projection (rather than this package computing/duplicating a `d3-geo` fit
|
|
55
|
+
* itself) — see `TileLayer.svelte`. Only correct for Mercator-family
|
|
56
|
+
* projections, where a screen-space rectangle maps back to a lon/lat bbox
|
|
57
|
+
* whose tile coverage is well-defined; callers are responsible for the
|
|
58
|
+
* projection-compatibility check (see `Plot.svelte`'s validation effect).
|
|
59
|
+
*/
|
|
60
|
+
export function visibleTiles(projection, bounds, tileSize = 256, minZoom = 0, maxZoom = 19) {
|
|
61
|
+
if (!projection.invert)
|
|
62
|
+
return [];
|
|
63
|
+
const corners = [
|
|
64
|
+
[bounds.left, bounds.top],
|
|
65
|
+
[bounds.left + bounds.width, bounds.top],
|
|
66
|
+
[bounds.left, bounds.top + bounds.height],
|
|
67
|
+
[bounds.left + bounds.width, bounds.top + bounds.height],
|
|
68
|
+
];
|
|
69
|
+
const lonLats = corners.map((c) => projection.invert(c)).filter((p) => p != null);
|
|
70
|
+
if (lonLats.length === 0)
|
|
71
|
+
return [];
|
|
72
|
+
const centerScreen = [bounds.left + bounds.width / 2, bounds.top + bounds.height / 2];
|
|
73
|
+
const center = projection.invert(centerScreen);
|
|
74
|
+
const z = center
|
|
75
|
+
? Math.max(minZoom, Math.min(maxZoom, estimateZoomLevel(projection, center[0], center[1], tileSize) ?? minZoom))
|
|
76
|
+
: minZoom;
|
|
77
|
+
const lons = lonLats.map((p) => p[0]);
|
|
78
|
+
const lats = lonLats.map((p) => p[1]);
|
|
79
|
+
const minTileX = Math.floor(lonToTileX(Math.min(...lons), z));
|
|
80
|
+
const maxTileX = Math.floor(lonToTileX(Math.max(...lons), z));
|
|
81
|
+
const minTileY = Math.floor(latToTileY(Math.max(...lats), z));
|
|
82
|
+
const maxTileY = Math.floor(latToTileY(Math.min(...lats), z));
|
|
83
|
+
const maxTile = 2 ** z - 1;
|
|
84
|
+
const tiles = [];
|
|
85
|
+
for (let x = Math.max(0, minTileX); x <= Math.min(maxTile, maxTileX); x++) {
|
|
86
|
+
for (let y = Math.max(0, minTileY); y <= Math.min(maxTile, maxTileY); y++) {
|
|
87
|
+
tiles.push({ x, y, z });
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return tiles;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Builds an affine tile-space → screen-space mapping from two reliable
|
|
94
|
+
* anchor points — `bounds`' own top-left/bottom-right corners, inverted
|
|
95
|
+
* back to lon/lat — rather than forward-projecting each tile's own corners
|
|
96
|
+
* individually (what this replaced). That per-tile approach broke under a
|
|
97
|
+
* projection whose scale/translate were fit to a *custom* domain rather
|
|
98
|
+
* than set via its own `.scale()`/`.translate()`: `d3-geo`'s cylindrical
|
|
99
|
+
* projections (mercator included) auto-install an internal `clipExtent`
|
|
100
|
+
* sized to whatever scale/translate they were constructed or last
|
|
101
|
+
* `.scale()`/`.translate()`-called with (guarding against the y → ±Infinity
|
|
102
|
+
* blowup near the poles); svelteplot's own domain-fitting wraps the
|
|
103
|
+
* *unmodified* projection instance in an external transform instead of
|
|
104
|
+
* calling those setters, so that internal clip keeps silently rejecting
|
|
105
|
+
* forward-projected points outside its stale bounds — nulling out most of
|
|
106
|
+
* a tile grid even for tiles nowhere near a pole. `.invert()` isn't routed
|
|
107
|
+
* through that clip, so anchoring to two known-good inverted corners
|
|
108
|
+
* sidesteps it entirely — sound because Mercator's tile space (longitude,
|
|
109
|
+
* and the standard log-tan latitude transform used by `latToTileY`) is
|
|
110
|
+
* affine in screen space by construction, so one linear fit applies
|
|
111
|
+
* uniformly to every tile in the grid.
|
|
112
|
+
*/
|
|
113
|
+
export function buildMercatorFit(projection, bounds, z) {
|
|
114
|
+
if (!projection.invert)
|
|
115
|
+
return null;
|
|
116
|
+
const topLeft = projection.invert([bounds.left, bounds.top]);
|
|
117
|
+
const bottomRight = projection.invert([bounds.left + bounds.width, bounds.top + bounds.height]);
|
|
118
|
+
if (!topLeft || !bottomRight)
|
|
119
|
+
return null;
|
|
120
|
+
const tileXAtLeft = lonToTileX(topLeft[0], z);
|
|
121
|
+
const tileXAtRight = lonToTileX(bottomRight[0], z);
|
|
122
|
+
const tileYAtTop = latToTileY(topLeft[1], z);
|
|
123
|
+
const tileYAtBottom = latToTileY(bottomRight[1], z);
|
|
124
|
+
if (tileXAtRight === tileXAtLeft || tileYAtBottom === tileYAtTop)
|
|
125
|
+
return null;
|
|
126
|
+
return {
|
|
127
|
+
z,
|
|
128
|
+
screenLeft: bounds.left,
|
|
129
|
+
screenTop: bounds.top,
|
|
130
|
+
tileXAtLeft,
|
|
131
|
+
tileYAtTop,
|
|
132
|
+
pxPerTileX: bounds.width / (tileXAtRight - tileXAtLeft),
|
|
133
|
+
pxPerTileY: bounds.height / (tileYAtBottom - tileYAtTop),
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
/** Places a tile on screen via a `MercatorFit` — `null` only if `tile` is from a different zoom level than the fit was built for. */
|
|
137
|
+
export function tileScreenBounds(fit, tile) {
|
|
138
|
+
if (tile.z !== fit.z)
|
|
139
|
+
return null;
|
|
140
|
+
return {
|
|
141
|
+
left: fit.screenLeft + (tile.x - fit.tileXAtLeft) * fit.pxPerTileX,
|
|
142
|
+
top: fit.screenTop + (tile.y - fit.tileYAtTop) * fit.pxPerTileY,
|
|
143
|
+
width: fit.pxPerTileX,
|
|
144
|
+
height: fit.pxPerTileY,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Fills `{z}/{x}/{y}` placeholders in a tile URL template. Also supports
|
|
149
|
+
* `{-y}` for TMS-scheme sources (e.g. IGN's tile services), whose row index
|
|
150
|
+
* counts from the south instead of the north — the standard XYZ-to-TMS
|
|
151
|
+
* conversion `2^z - 1 - y`.
|
|
152
|
+
*/
|
|
153
|
+
export function tileUrl(template, tile) {
|
|
154
|
+
return template
|
|
155
|
+
.replace('{z}', String(tile.z))
|
|
156
|
+
.replace('{x}', String(tile.x))
|
|
157
|
+
.replace('{-y}', String(2 ** tile.z - 1 - tile.y))
|
|
158
|
+
.replace('{y}', String(tile.y));
|
|
159
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { GeoFeature, Topology } from '../../types/plots/data/geo';
|
|
2
|
+
export declare function isTopology(data: unknown): data is Topology;
|
|
3
|
+
/**
|
|
4
|
+
* Converts a TopoJSON `Topology` to GeoJSON via `topojson-client`'s
|
|
5
|
+
* `feature()`, loaded with a dynamic import so the package works perfectly
|
|
6
|
+
* for consumers who never install `topojson-client` — it's an optional peer
|
|
7
|
+
* dependency (see package.json), never statically imported.
|
|
8
|
+
*/
|
|
9
|
+
export declare function topologyToGeoJson<TProps extends Record<string, unknown>>(topology: Topology, object: string): Promise<GeoFeature<TProps>[]>;
|