|
| 1 | +import { computeBbox } from '../../lib/geo_location_utils'; |
| 2 | +import type { MapLayout, ScattermapData } from '../../types/generated/schema'; |
| 3 | + |
| 4 | +// Same shape as the user-facing `map.bounds` attribute, but with all fields required |
| 5 | +type LonLatBox = Required<NonNullable<MapLayout['bounds']>>; |
| 6 | + |
| 7 | +// Minimal shape of the fullData entries this helper reads |
| 8 | +interface FitBoundsTrace extends Pick<ScattermapData, 'subplot' | 'visible'> { |
| 9 | + // Tighten lat/lon to be more specific than default |
| 10 | + lat?: ArrayLike<number>; |
| 11 | + lon?: ArrayLike<number>; |
| 12 | + // Broaden type since this could run against multiple trace types |
| 13 | + type?: string; |
| 14 | +} |
| 15 | + |
| 16 | +/** |
| 17 | + * Compute a lon/lat bounding box from lonlat-bearing traces (`scattermap`, |
| 18 | + * `densitymap`) on the given map subplot. |
| 19 | + * |
| 20 | + * Returns null when: |
| 21 | + * - no fittable data exists on the subplot; |
| 22 | + * - a location-based trace (`choroplethmap`) is present — those carry |
| 23 | + * `locations`/`geojson`, not raw lon/lat, and need geojson bbox handling |
| 24 | + * that isn't implemented here. |
| 25 | + * |
| 26 | + * @param fullData - The full data array (post supply-defaults) |
| 27 | + * @param subplotId - e.g. `'map'`, `'map2'` |
| 28 | + */ |
| 29 | +export function getMapFitBounds(fullData: FitBoundsTrace[], subplotId: string): LonLatBox | null { |
| 30 | + const coordinates: [number, number][] = []; |
| 31 | + |
| 32 | + for (const trace of fullData) { |
| 33 | + if (trace.subplot !== subplotId || trace.visible !== true) continue; |
| 34 | + |
| 35 | + // choroplethmap traces carry locations/geojson, not raw lon/lat; bail |
| 36 | + // out rather than frame around a subset of the subplot's data. |
| 37 | + if (trace.type === 'choroplethmap') return null; |
| 38 | + |
| 39 | + const { lat, lon } = trace; |
| 40 | + if (!lon || !lat) continue; |
| 41 | + |
| 42 | + const len = Math.min(lon.length, lat.length); |
| 43 | + for (let j = 0; j < len; j++) { |
| 44 | + const lo = lon[j]; |
| 45 | + const la = lat[j]; |
| 46 | + if (Number.isFinite(lo) && Number.isFinite(la)) coordinates.push([lo, la]); |
| 47 | + } |
| 48 | + } |
| 49 | + |
| 50 | + const bbox = computeBbox({ type: 'MultiPoint', coordinates }); |
| 51 | + if (!bbox) return null; |
| 52 | + |
| 53 | + const [west, south, east, north] = bbox; |
| 54 | + return { west, east, south, north }; |
| 55 | +} |
0 commit comments