geo.ts132 lines · main
| 1 | import { COUNTRIES } from '@/components/interfaces/Organization/BillingSettings/BillingCustomerData/BillingAddress.constants' |
| 2 | import { COUNTRY_LAT_LON } from '@/components/interfaces/ProjectCreation/ProjectCreation.constants' |
| 3 | |
| 4 | export type CountryCountRow = { country: string | null; count: number | string } |
| 5 | |
| 6 | export interface MapChartTheme { |
| 7 | zeroFill: string // fill for countries with zero (or when max=0) |
| 8 | brandFill: string // base fill color (same for all, vary by opacity) |
| 9 | opacityScale: [number, number, number, number, number] // low -> high opacities |
| 10 | boundaryStroke: string |
| 11 | boundaryStrokeHover: string |
| 12 | markerFill: string |
| 13 | oceanFill: string |
| 14 | } |
| 15 | |
| 16 | export const MAP_CHART_THEME: { light: MapChartTheme; dark: MapChartTheme } = { |
| 17 | light: { |
| 18 | zeroFill: 'hsl(var(--background-surface-400))', |
| 19 | brandFill: 'hsl(var(--brand-default))', |
| 20 | opacityScale: [0.18, 0.32, 0.5, 0.68, 0.86], |
| 21 | boundaryStroke: 'hsla(var(--brand-300), 0.6)', |
| 22 | boundaryStrokeHover: 'hsl(var(--brand-500))', |
| 23 | markerFill: 'hsl(var(--brand-default))', |
| 24 | oceanFill: 'transparent', |
| 25 | }, |
| 26 | dark: { |
| 27 | zeroFill: 'hsl(var(--background-selection))', |
| 28 | brandFill: 'hsl(var(--brand-default))', |
| 29 | opacityScale: [0.18, 0.32, 0.5, 0.68, 0.86], |
| 30 | boundaryStroke: 'hsla(var(--brand-300), 0.6)', |
| 31 | boundaryStrokeHover: 'hsl(var(--brand-500))', |
| 32 | markerFill: 'hsl(var(--brand-default))', |
| 33 | oceanFill: 'transparent', |
| 34 | }, |
| 35 | } |
| 36 | |
| 37 | export const buildCountsByIso2 = (rows: CountryCountRow[]): Record<string, number> => { |
| 38 | const counts: Record<string, number> = {} |
| 39 | for (const row of rows) { |
| 40 | if (!row.country) continue |
| 41 | const code = row.country.toUpperCase() |
| 42 | const numeric = typeof row.count === 'number' ? row.count : Number(row.count) |
| 43 | if (!Number.isFinite(numeric)) continue |
| 44 | counts[code] = (counts[code] || 0) + numeric |
| 45 | } |
| 46 | return counts |
| 47 | } |
| 48 | |
| 49 | export const getFillColor = ( |
| 50 | value: number, |
| 51 | max: number, |
| 52 | theme: MapChartTheme = MAP_CHART_THEME.dark |
| 53 | ): string => { |
| 54 | if (max <= 0 || !value) return theme.zeroFill |
| 55 | return theme.brandFill |
| 56 | } |
| 57 | |
| 58 | export const getFillOpacity = ( |
| 59 | value: number, |
| 60 | max: number, |
| 61 | theme: MapChartTheme = MAP_CHART_THEME.dark |
| 62 | ): number => { |
| 63 | if (max <= 0 || !value) return 1 |
| 64 | const ratio = value / max |
| 65 | if (ratio > 0.8) return theme.opacityScale[4] |
| 66 | if (ratio > 0.6) return theme.opacityScale[3] |
| 67 | if (ratio > 0.4) return theme.opacityScale[2] |
| 68 | if (ratio > 0.2) return theme.opacityScale[1] |
| 69 | return theme.opacityScale[0] |
| 70 | } |
| 71 | |
| 72 | const MICRO_COUNTRIES = new Set([ |
| 73 | 'Singapore', |
| 74 | 'Monaco', |
| 75 | 'Andorra', |
| 76 | 'Liechtenstein', |
| 77 | 'San Marino', |
| 78 | 'Vatican', |
| 79 | 'Vatican City', |
| 80 | 'Luxembourg', |
| 81 | 'Malta', |
| 82 | 'Bahrain', |
| 83 | 'Brunei', |
| 84 | 'Qatar', |
| 85 | 'Kuwait', |
| 86 | 'Hong Kong', |
| 87 | 'Macau', |
| 88 | ]) |
| 89 | |
| 90 | export const isMicroCountry = (name: string): boolean => MICRO_COUNTRIES.has(name) |
| 91 | |
| 92 | export const isKnownCountryCode = (code: string): code is keyof typeof COUNTRY_LAT_LON => { |
| 93 | return Object.prototype.hasOwnProperty.call(COUNTRY_LAT_LON, code) |
| 94 | } |
| 95 | |
| 96 | export const computeMarkerRadius = (value: number, max: number): number => { |
| 97 | if (max <= 0) return 2 |
| 98 | return Math.max(1.5, Math.min(4, (value / max) * 4)) |
| 99 | } |
| 100 | |
| 101 | // Best-effort extraction of ISO2 code from feature properties, with name fallback |
| 102 | export const extractIso2FromFeatureProps = ( |
| 103 | props?: Record<string, unknown> |
| 104 | ): string | undefined => { |
| 105 | if (!props) return undefined |
| 106 | const candidates = [ |
| 107 | 'ISO_A2_EH', |
| 108 | 'ISO_A2', |
| 109 | 'iso_a2', |
| 110 | 'ADMIN_ISO_A2', |
| 111 | 'WB_A2', |
| 112 | 'ADM0_A3_IS', |
| 113 | 'ADM0_A3', |
| 114 | 'ISO_N3', |
| 115 | 'id', |
| 116 | ] |
| 117 | for (const key of candidates) { |
| 118 | const v = props[key] as unknown |
| 119 | if (typeof v === 'string' && v.length === 2) return v.toUpperCase() |
| 120 | } |
| 121 | const name = |
| 122 | (props['name'] as string | undefined) || (props['NAME'] as string | undefined) || undefined |
| 123 | if (!name) return undefined |
| 124 | const entry = COUNTRIES.find((c) => c.name === name) |
| 125 | return entry?.code |
| 126 | } |
| 127 | |
| 128 | export const iso2ToCountryName = (iso2: string): string => { |
| 129 | const code = iso2.toUpperCase() |
| 130 | const entry = COUNTRIES.find((c) => c.code === code) |
| 131 | return entry?.name ?? code |
| 132 | } |