geo.ts132 lines · main
1import { COUNTRIES } from '@/components/interfaces/Organization/BillingSettings/BillingCustomerData/BillingAddress.constants'
2import { COUNTRY_LAT_LON } from '@/components/interfaces/ProjectCreation/ProjectCreation.constants'
3
4export type CountryCountRow = { country: string | null; count: number | string }
5
6export 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
16export 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
37export 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
49export 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
58export 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
72const 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
90export const isMicroCountry = (name: string): boolean => MICRO_COUNTRIES.has(name)
91
92export const isKnownCountryCode = (code: string): code is keyof typeof COUNTRY_LAT_LON => {
93 return Object.prototype.hasOwnProperty.call(COUNTRY_LAT_LON, code)
94}
95
96export 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
102export 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
128export 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}