feat(know-your-world): add filter tabs for size, importance, and population
Add tabbed region selector with three filter criteria: - Size: Filter by geographic size (huge → tiny) - Importance: Filter by geopolitical importance (G7/UNSC superpowers → minor territories) - Population: Filter by population (100M+ → <1M) Includes: - New category data for ~200 countries by importance and population - Utility functions for all filter criteria types - Compact tab UI with Radix tooltips - Fixed 205px panel width with proper alignment 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
fc87808b40
commit
6c3c0ac70e
|
|
@ -1,6 +1,7 @@
|
||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useState, useCallback, useMemo, useRef, useEffect } from 'react'
|
import { useState, useCallback, useMemo, useRef, useEffect } from 'react'
|
||||||
|
import * as Tooltip from '@radix-ui/react-tooltip'
|
||||||
import { useSpring } from '@react-spring/web'
|
import { useSpring } from '@react-spring/web'
|
||||||
import { css } from '@styled/css'
|
import { css } from '@styled/css'
|
||||||
import {
|
import {
|
||||||
|
|
@ -25,15 +26,30 @@ import {
|
||||||
getFilteredMapDataBySizesSync,
|
getFilteredMapDataBySizesSync,
|
||||||
REGION_SIZE_CONFIG,
|
REGION_SIZE_CONFIG,
|
||||||
ALL_REGION_SIZES,
|
ALL_REGION_SIZES,
|
||||||
|
IMPORTANCE_LEVEL_CONFIG,
|
||||||
|
ALL_IMPORTANCE_LEVELS,
|
||||||
|
POPULATION_LEVEL_CONFIG,
|
||||||
|
ALL_POPULATION_LEVELS,
|
||||||
|
FILTER_CRITERIA_CONFIG,
|
||||||
|
filterRegionsByImportance,
|
||||||
|
filterRegionsByPopulation,
|
||||||
|
filterRegionsBySizes,
|
||||||
} from '../maps'
|
} from '../maps'
|
||||||
import type { RegionSize } from '../maps'
|
import type { RegionSize, ImportanceLevel, PopulationLevel, FilterCriteria } from '../maps'
|
||||||
import {
|
import {
|
||||||
CONTINENTS,
|
CONTINENTS,
|
||||||
getContinentForCountry,
|
getContinentForCountry,
|
||||||
COUNTRY_TO_CONTINENT,
|
COUNTRY_TO_CONTINENT,
|
||||||
type ContinentId,
|
type ContinentId,
|
||||||
} from '../continents'
|
} from '../continents'
|
||||||
import { sizesToRange, rangeToSizes } from '../utils/regionSizeUtils'
|
import {
|
||||||
|
sizesToRange,
|
||||||
|
rangeToSizes,
|
||||||
|
importanceToRange,
|
||||||
|
rangeToImportance,
|
||||||
|
populationToRange,
|
||||||
|
rangeToPopulation,
|
||||||
|
} from '../utils/regionSizeUtils'
|
||||||
import { preventFlexExpansion } from '../utils/responsiveStyles'
|
import { preventFlexExpansion } from '../utils/responsiveStyles'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -46,6 +62,35 @@ const SIZE_OPTIONS: ThermometerOption<RegionSize>[] = ALL_REGION_SIZES.map((size
|
||||||
emoji: REGION_SIZE_CONFIG[size].emoji,
|
emoji: REGION_SIZE_CONFIG[size].emoji,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Importance options for the range thermometer, ordered from most to least important
|
||||||
|
*/
|
||||||
|
const IMPORTANCE_OPTIONS: ThermometerOption<ImportanceLevel>[] = ALL_IMPORTANCE_LEVELS.map(
|
||||||
|
(level) => ({
|
||||||
|
value: level,
|
||||||
|
label: IMPORTANCE_LEVEL_CONFIG[level].label,
|
||||||
|
shortLabel: IMPORTANCE_LEVEL_CONFIG[level].label,
|
||||||
|
emoji: IMPORTANCE_LEVEL_CONFIG[level].emoji,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Population options for the range thermometer, ordered from largest to smallest
|
||||||
|
*/
|
||||||
|
const POPULATION_OPTIONS: ThermometerOption<PopulationLevel>[] = ALL_POPULATION_LEVELS.map(
|
||||||
|
(level) => ({
|
||||||
|
value: level,
|
||||||
|
label: POPULATION_LEVEL_CONFIG[level].label,
|
||||||
|
shortLabel: POPULATION_LEVEL_CONFIG[level].label,
|
||||||
|
emoji: POPULATION_LEVEL_CONFIG[level].emoji,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* All filter criteria tabs
|
||||||
|
*/
|
||||||
|
const ALL_FILTER_CRITERIA: FilterCriteria[] = ['size', 'importance', 'population']
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Selection path for drill-down navigation:
|
* Selection path for drill-down navigation:
|
||||||
* - [] = World level
|
* - [] = World level
|
||||||
|
|
@ -150,6 +195,27 @@ export function DrillDownMapSelector({
|
||||||
// Track which region name is being hovered in the popover (for zoom preview)
|
// Track which region name is being hovered in the popover (for zoom preview)
|
||||||
const [previewRegionName, setPreviewRegionName] = useState<string | null>(null)
|
const [previewRegionName, setPreviewRegionName] = useState<string | null>(null)
|
||||||
|
|
||||||
|
// Filter criteria tab state
|
||||||
|
const [activeFilterCriteria, setActiveFilterCriteria] = useState<FilterCriteria>('size')
|
||||||
|
|
||||||
|
// Local state for importance and population filters (not persisted, just for display filtering)
|
||||||
|
const [includeImportance, setIncludeImportance] = useState<ImportanceLevel[]>([
|
||||||
|
'superpower',
|
||||||
|
'major',
|
||||||
|
'regional',
|
||||||
|
])
|
||||||
|
const [includePopulation, setIncludePopulation] = useState<PopulationLevel[]>([
|
||||||
|
'huge',
|
||||||
|
'large',
|
||||||
|
'medium',
|
||||||
|
])
|
||||||
|
|
||||||
|
// Preview states for importance and population thermometers
|
||||||
|
const [importanceRangePreview, setImportanceRangePreview] =
|
||||||
|
useState<RangePreviewState<ImportanceLevel> | null>(null)
|
||||||
|
const [populationRangePreview, setPopulationRangePreview] =
|
||||||
|
useState<RangePreviewState<PopulationLevel> | null>(null)
|
||||||
|
|
||||||
// Sync local path state when props change from external sources (e.g., other players)
|
// Sync local path state when props change from external sources (e.g., other players)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const expectedPath = getInitialPath()
|
const expectedPath = getInitialPath()
|
||||||
|
|
@ -290,29 +356,52 @@ export function DrillDownMapSelector({
|
||||||
return groups
|
return groups
|
||||||
}, [currentLevel])
|
}, [currentLevel])
|
||||||
|
|
||||||
// Calculate excluded regions based on includeSizes
|
// Get base regions for the current view (continent filtered but no criteria filtering)
|
||||||
// These are regions that exist but are filtered out by size settings
|
const baseRegions = useMemo(() => {
|
||||||
const excludedRegions = useMemo(() => {
|
|
||||||
// Determine which map we're looking at
|
|
||||||
const mapId = currentLevel === 2 && path[1] ? 'usa' : 'world'
|
const mapId = currentLevel === 2 && path[1] ? 'usa' : 'world'
|
||||||
const continentId: ContinentId | 'all' =
|
const continentId: ContinentId | 'all' =
|
||||||
currentLevel >= 1 && path[0] ? path[0] : selectedContinent
|
currentLevel >= 1 && path[0] ? path[0] : selectedContinent
|
||||||
|
|
||||||
// Get all regions (unfiltered by size)
|
|
||||||
const allRegionsMapData = getFilteredMapDataBySizesSync(
|
const allRegionsMapData = getFilteredMapDataBySizesSync(
|
||||||
mapId as 'world' | 'usa',
|
mapId as 'world' | 'usa',
|
||||||
continentId,
|
continentId,
|
||||||
['huge', 'large', 'medium', 'small', 'tiny'] // All sizes
|
['huge', 'large', 'medium', 'small', 'tiny'] // All sizes
|
||||||
)
|
)
|
||||||
const allRegionIds = new Set(allRegionsMapData.regions.map((r) => r.id))
|
return allRegionsMapData.regions
|
||||||
|
}, [currentLevel, path, selectedContinent])
|
||||||
|
|
||||||
// Get filtered regions (based on current includeSizes)
|
// Get the active filter levels based on current tab
|
||||||
const filteredMapData = getFilteredMapDataBySizesSync(
|
const activeFilterLevels = useMemo(() => {
|
||||||
mapId as 'world' | 'usa',
|
switch (activeFilterCriteria) {
|
||||||
continentId,
|
case 'size':
|
||||||
includeSizes
|
return includeSizes
|
||||||
)
|
case 'importance':
|
||||||
const filteredRegionIds = new Set(filteredMapData.regions.map((r) => r.id))
|
return includeImportance
|
||||||
|
case 'population':
|
||||||
|
return includePopulation
|
||||||
|
default:
|
||||||
|
return includeSizes
|
||||||
|
}
|
||||||
|
}, [activeFilterCriteria, includeSizes, includeImportance, includePopulation])
|
||||||
|
|
||||||
|
// Calculate excluded regions based on active filter criteria
|
||||||
|
const excludedRegions = useMemo(() => {
|
||||||
|
const allRegionIds = new Set(baseRegions.map((r) => r.id))
|
||||||
|
|
||||||
|
// Filter based on active criteria
|
||||||
|
let filteredRegions = baseRegions
|
||||||
|
switch (activeFilterCriteria) {
|
||||||
|
case 'size':
|
||||||
|
filteredRegions = filterRegionsBySizes(baseRegions, includeSizes, 'world')
|
||||||
|
break
|
||||||
|
case 'importance':
|
||||||
|
filteredRegions = filterRegionsByImportance(baseRegions, includeImportance)
|
||||||
|
break
|
||||||
|
case 'population':
|
||||||
|
filteredRegions = filterRegionsByPopulation(baseRegions, includePopulation)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
const filteredRegionIds = new Set(filteredRegions.map((r) => r.id))
|
||||||
|
|
||||||
// Excluded = all regions minus filtered regions
|
// Excluded = all regions minus filtered regions
|
||||||
const excluded: string[] = []
|
const excluded: string[] = []
|
||||||
|
|
@ -322,35 +411,74 @@ export function DrillDownMapSelector({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return excluded
|
return excluded
|
||||||
}, [currentLevel, path, selectedContinent, includeSizes])
|
}, [baseRegions, activeFilterCriteria, includeSizes, includeImportance, includePopulation])
|
||||||
|
|
||||||
// Compute region names by size category (for tooltip display)
|
// Compute region names by size category (for tooltip display)
|
||||||
const regionNamesBySize = useMemo(() => {
|
const regionNamesBySize = useMemo(() => {
|
||||||
const mapId = currentLevel === 2 && path[1] ? 'usa' : 'world'
|
|
||||||
const continentId: ContinentId | 'all' =
|
|
||||||
currentLevel >= 1 && path[0] ? path[0] : selectedContinent
|
|
||||||
|
|
||||||
const result: Partial<Record<RegionSize, string[]>> = {}
|
const result: Partial<Record<RegionSize, string[]>> = {}
|
||||||
for (const size of ALL_REGION_SIZES) {
|
for (const size of ALL_REGION_SIZES) {
|
||||||
const filtered = getFilteredMapDataBySizesSync(mapId as 'world' | 'usa', continentId, [size])
|
const filtered = filterRegionsBySizes(baseRegions, [size], 'world')
|
||||||
result[size] = filtered.regions.map((r) => r.name).sort((a, b) => a.localeCompare(b))
|
result[size] = filtered.map((r) => r.name).sort((a, b) => a.localeCompare(b))
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
}, [currentLevel, path, selectedContinent])
|
}, [baseRegions])
|
||||||
|
|
||||||
// Compute all selected region names (for popover display)
|
// Compute region names by importance category
|
||||||
|
const regionNamesByImportance = useMemo(() => {
|
||||||
|
const result: Partial<Record<ImportanceLevel, string[]>> = {}
|
||||||
|
for (const level of ALL_IMPORTANCE_LEVELS) {
|
||||||
|
const filtered = filterRegionsByImportance(baseRegions, [level])
|
||||||
|
result[level] = filtered.map((r) => r.name).sort((a, b) => a.localeCompare(b))
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}, [baseRegions])
|
||||||
|
|
||||||
|
// Compute region names by population category
|
||||||
|
const regionNamesByPopulation = useMemo(() => {
|
||||||
|
const result: Partial<Record<PopulationLevel, string[]>> = {}
|
||||||
|
for (const level of ALL_POPULATION_LEVELS) {
|
||||||
|
const filtered = filterRegionsByPopulation(baseRegions, [level])
|
||||||
|
result[level] = filtered.map((r) => r.name).sort((a, b) => a.localeCompare(b))
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}, [baseRegions])
|
||||||
|
|
||||||
|
// Compute region counts by category for the active filter criteria
|
||||||
|
const regionCountsByCriteria = useMemo(() => {
|
||||||
|
switch (activeFilterCriteria) {
|
||||||
|
case 'size':
|
||||||
|
return Object.fromEntries(
|
||||||
|
Object.entries(regionNamesBySize).map(([k, v]) => [k, v?.length ?? 0])
|
||||||
|
)
|
||||||
|
case 'importance':
|
||||||
|
return Object.fromEntries(
|
||||||
|
Object.entries(regionNamesByImportance).map(([k, v]) => [k, v?.length ?? 0])
|
||||||
|
)
|
||||||
|
case 'population':
|
||||||
|
return Object.fromEntries(
|
||||||
|
Object.entries(regionNamesByPopulation).map(([k, v]) => [k, v?.length ?? 0])
|
||||||
|
)
|
||||||
|
default:
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
}, [activeFilterCriteria, regionNamesBySize, regionNamesByImportance, regionNamesByPopulation])
|
||||||
|
|
||||||
|
// Compute all selected region names (for popover display) based on active filter
|
||||||
const selectedRegionNames = useMemo(() => {
|
const selectedRegionNames = useMemo(() => {
|
||||||
const mapId = currentLevel === 2 && path[1] ? 'usa' : 'world'
|
let filteredRegions = baseRegions
|
||||||
const continentId: ContinentId | 'all' =
|
switch (activeFilterCriteria) {
|
||||||
currentLevel >= 1 && path[0] ? path[0] : selectedContinent
|
case 'size':
|
||||||
|
filteredRegions = filterRegionsBySizes(baseRegions, includeSizes, 'world')
|
||||||
const filtered = getFilteredMapDataBySizesSync(
|
break
|
||||||
mapId as 'world' | 'usa',
|
case 'importance':
|
||||||
continentId,
|
filteredRegions = filterRegionsByImportance(baseRegions, includeImportance)
|
||||||
includeSizes
|
break
|
||||||
)
|
case 'population':
|
||||||
return filtered.regions.map((r) => r.name)
|
filteredRegions = filterRegionsByPopulation(baseRegions, includePopulation)
|
||||||
}, [currentLevel, path, selectedContinent, includeSizes])
|
break
|
||||||
|
}
|
||||||
|
return filteredRegions.map((r) => r.name)
|
||||||
|
}, [baseRegions, activeFilterCriteria, includeSizes, includeImportance, includePopulation])
|
||||||
|
|
||||||
// Create lookup map from region name → region data (for zoom preview)
|
// Create lookup map from region name → region data (for zoom preview)
|
||||||
const regionsByName = useMemo(() => {
|
const regionsByName = useMemo(() => {
|
||||||
|
|
@ -437,34 +565,68 @@ export function DrillDownMapSelector({
|
||||||
return region?.id ?? null
|
return region?.id ?? null
|
||||||
}, [previewRegionName, regionsByName])
|
}, [previewRegionName, regionsByName])
|
||||||
|
|
||||||
// Calculate preview regions based on hovering over the size thermometer
|
// Calculate preview regions based on hovering over the thermometer
|
||||||
// Shows what regions would be added or removed if the user clicks
|
// Shows what regions would be added or removed if the user clicks
|
||||||
const { previewAddRegions, previewRemoveRegions } = useMemo(() => {
|
const { previewAddRegions, previewRemoveRegions } = useMemo(() => {
|
||||||
if (!sizeRangePreview) {
|
// Determine which preview state to use based on active filter criteria
|
||||||
|
const activePreview =
|
||||||
|
activeFilterCriteria === 'size'
|
||||||
|
? sizeRangePreview
|
||||||
|
: activeFilterCriteria === 'importance'
|
||||||
|
? importanceRangePreview
|
||||||
|
: populationRangePreview
|
||||||
|
|
||||||
|
if (!activePreview) {
|
||||||
return { previewAddRegions: [], previewRemoveRegions: [] }
|
return { previewAddRegions: [], previewRemoveRegions: [] }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine which map we're looking at
|
// Get current included region IDs based on active criteria
|
||||||
const mapId = currentLevel === 2 && path[1] ? 'usa' : 'world'
|
let currentFiltered = baseRegions
|
||||||
const continentId: ContinentId | 'all' =
|
switch (activeFilterCriteria) {
|
||||||
currentLevel >= 1 && path[0] ? path[0] : selectedContinent
|
case 'size':
|
||||||
|
currentFiltered = filterRegionsBySizes(baseRegions, includeSizes, 'world')
|
||||||
// Get current included region IDs
|
break
|
||||||
const currentIncluded = getFilteredMapDataBySizesSync(
|
case 'importance':
|
||||||
mapId as 'world' | 'usa',
|
currentFiltered = filterRegionsByImportance(baseRegions, includeImportance)
|
||||||
continentId,
|
break
|
||||||
includeSizes
|
case 'population':
|
||||||
)
|
currentFiltered = filterRegionsByPopulation(baseRegions, includePopulation)
|
||||||
const currentIncludedIds = new Set(currentIncluded.regions.map((r) => r.id))
|
break
|
||||||
|
}
|
||||||
|
const currentIncludedIds = new Set(currentFiltered.map((r) => r.id))
|
||||||
|
|
||||||
// Get preview included region IDs (if user clicked)
|
// Get preview included region IDs (if user clicked)
|
||||||
const previewSizes = rangeToSizes(sizeRangePreview.previewMin, sizeRangePreview.previewMax)
|
let previewFiltered = baseRegions
|
||||||
const previewIncluded = getFilteredMapDataBySizesSync(
|
switch (activeFilterCriteria) {
|
||||||
mapId as 'world' | 'usa',
|
case 'size':
|
||||||
continentId,
|
if (sizeRangePreview) {
|
||||||
previewSizes
|
const previewSizes = rangeToSizes(
|
||||||
)
|
sizeRangePreview.previewMin,
|
||||||
const previewIncludedIds = new Set(previewIncluded.regions.map((r) => r.id))
|
sizeRangePreview.previewMax
|
||||||
|
)
|
||||||
|
previewFiltered = filterRegionsBySizes(baseRegions, previewSizes, 'world')
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case 'importance':
|
||||||
|
if (importanceRangePreview) {
|
||||||
|
const previewLevels = rangeToImportance(
|
||||||
|
importanceRangePreview.previewMin,
|
||||||
|
importanceRangePreview.previewMax
|
||||||
|
)
|
||||||
|
previewFiltered = filterRegionsByImportance(baseRegions, previewLevels)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case 'population':
|
||||||
|
if (populationRangePreview) {
|
||||||
|
const previewLevels = rangeToPopulation(
|
||||||
|
populationRangePreview.previewMin,
|
||||||
|
populationRangePreview.previewMax
|
||||||
|
)
|
||||||
|
previewFiltered = filterRegionsByPopulation(baseRegions, previewLevels)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
const previewIncludedIds = new Set(previewFiltered.map((r) => r.id))
|
||||||
|
|
||||||
// Regions that would be ADDED (in preview but not currently included)
|
// Regions that would be ADDED (in preview but not currently included)
|
||||||
const addRegions: string[] = []
|
const addRegions: string[] = []
|
||||||
|
|
@ -486,7 +648,16 @@ export function DrillDownMapSelector({
|
||||||
previewAddRegions: addRegions,
|
previewAddRegions: addRegions,
|
||||||
previewRemoveRegions: removeRegions,
|
previewRemoveRegions: removeRegions,
|
||||||
}
|
}
|
||||||
}, [sizeRangePreview, currentLevel, path, selectedContinent, includeSizes])
|
}, [
|
||||||
|
activeFilterCriteria,
|
||||||
|
sizeRangePreview,
|
||||||
|
importanceRangePreview,
|
||||||
|
populationRangePreview,
|
||||||
|
baseRegions,
|
||||||
|
includeSizes,
|
||||||
|
includeImportance,
|
||||||
|
includePopulation,
|
||||||
|
])
|
||||||
|
|
||||||
// Compute the label to display for the hovered region
|
// Compute the label to display for the hovered region
|
||||||
// Shows the next drill-down level name, not the individual region name
|
// Shows the next drill-down level name, not the individual region name
|
||||||
|
|
@ -1036,7 +1207,7 @@ export function DrillDownMapSelector({
|
||||||
)
|
)
|
||||||
})()}
|
})()}
|
||||||
|
|
||||||
{/* Right-side controls container - region size selector with inline list on desktop */}
|
{/* Right-side controls container - region filter selector with tabs */}
|
||||||
<div
|
<div
|
||||||
data-element="right-controls"
|
data-element="right-controls"
|
||||||
className={css({
|
className={css({
|
||||||
|
|
@ -1048,35 +1219,176 @@ export function DrillDownMapSelector({
|
||||||
transformOrigin: 'top right',
|
transformOrigin: 'top right',
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{/* Region Size Range Selector with inline list expansion on desktop */}
|
{/* Region Filter Selector with tabs */}
|
||||||
<div
|
<div
|
||||||
data-element="region-size-filters"
|
data-element="region-filters"
|
||||||
className={css({
|
className={css({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
|
alignItems: 'stretch',
|
||||||
padding: '3',
|
padding: '3',
|
||||||
bg: isDark ? 'gray.800' : 'gray.100',
|
bg: isDark ? 'gray.800' : 'gray.100',
|
||||||
rounded: 'xl',
|
rounded: 'xl',
|
||||||
shadow: 'lg',
|
shadow: 'lg',
|
||||||
maxHeight: { base: 'none', md: fillContainer ? '400px' : 'none' },
|
width: '205px',
|
||||||
|
maxHeight: { base: 'none', md: fillContainer ? '450px' : 'none' },
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<RangeThermometer
|
{/* Filter Criteria Tabs */}
|
||||||
options={SIZE_OPTIONS}
|
<Tooltip.Provider delayDuration={200}>
|
||||||
minValue={sizesToRange(includeSizes)[0]}
|
<div
|
||||||
maxValue={sizesToRange(includeSizes)[1]}
|
data-element="filter-tabs"
|
||||||
onChange={(min, max) => onRegionSizesChange(rangeToSizes(min, max))}
|
className={css({
|
||||||
orientation="vertical"
|
display: 'flex',
|
||||||
isDark={isDark}
|
gap: '1px',
|
||||||
counts={regionCountsBySize as Partial<Record<RegionSize, number>>}
|
marginBottom: '2',
|
||||||
showTotalCount
|
padding: '2px',
|
||||||
onHoverPreview={setSizeRangePreview}
|
bg: isDark ? 'gray.700' : 'gray.200',
|
||||||
regionNamesByCategory={regionNamesBySize}
|
rounded: 'md',
|
||||||
selectedRegionNames={selectedRegionNames}
|
width: '100%',
|
||||||
onRegionNameHover={setPreviewRegionName}
|
})}
|
||||||
hideCountOnMd={fillContainer && selectedRegionNames.length > 0}
|
>
|
||||||
/>
|
{ALL_FILTER_CRITERIA.map((criteria) => {
|
||||||
|
const isActive = activeFilterCriteria === criteria
|
||||||
|
const buttonContent = (
|
||||||
|
<button
|
||||||
|
data-action={`select-filter-${criteria}`}
|
||||||
|
onClick={() => setActiveFilterCriteria(criteria)}
|
||||||
|
className={css({
|
||||||
|
flex: isActive ? 1 : 'none',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
gap: '1',
|
||||||
|
padding: '1',
|
||||||
|
paddingX: isActive ? '2' : '1.5',
|
||||||
|
fontSize: 'xs',
|
||||||
|
fontWeight: isActive ? 'bold' : 'normal',
|
||||||
|
color: isActive
|
||||||
|
? isDark
|
||||||
|
? 'white'
|
||||||
|
: 'gray.900'
|
||||||
|
: isDark
|
||||||
|
? 'gray.400'
|
||||||
|
: 'gray.600',
|
||||||
|
bg: isActive ? (isDark ? 'gray.600' : 'white') : 'transparent',
|
||||||
|
rounded: 'sm',
|
||||||
|
cursor: 'pointer',
|
||||||
|
border: 'none',
|
||||||
|
transition: 'all 0.15s',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
_hover: {
|
||||||
|
bg: isActive
|
||||||
|
? isDark
|
||||||
|
? 'gray.600'
|
||||||
|
: 'white'
|
||||||
|
: isDark
|
||||||
|
? 'gray.600'
|
||||||
|
: 'gray.100',
|
||||||
|
},
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<span>{FILTER_CRITERIA_CONFIG[criteria].emoji}</span>
|
||||||
|
{isActive && <span>{FILTER_CRITERIA_CONFIG[criteria].label}</span>}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
|
||||||
|
// Wrap inactive tabs with tooltips
|
||||||
|
if (!isActive) {
|
||||||
|
return (
|
||||||
|
<Tooltip.Root key={criteria}>
|
||||||
|
<Tooltip.Trigger asChild>{buttonContent}</Tooltip.Trigger>
|
||||||
|
<Tooltip.Portal>
|
||||||
|
<Tooltip.Content
|
||||||
|
side="top"
|
||||||
|
sideOffset={5}
|
||||||
|
className={css({
|
||||||
|
bg: isDark ? 'gray.800' : 'gray.900',
|
||||||
|
color: 'white',
|
||||||
|
paddingX: '3',
|
||||||
|
paddingY: '1.5',
|
||||||
|
rounded: 'md',
|
||||||
|
fontSize: 'xs',
|
||||||
|
fontWeight: 'medium',
|
||||||
|
boxShadow: 'lg',
|
||||||
|
zIndex: 10001,
|
||||||
|
animation: 'fadeIn 0.15s ease-out',
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{FILTER_CRITERIA_CONFIG[criteria].label}
|
||||||
|
<Tooltip.Arrow
|
||||||
|
className={css({
|
||||||
|
fill: isDark ? 'gray.800' : 'gray.900',
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</Tooltip.Content>
|
||||||
|
</Tooltip.Portal>
|
||||||
|
</Tooltip.Root>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return <span key={criteria}>{buttonContent}</span>
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</Tooltip.Provider>
|
||||||
|
|
||||||
|
{/* Size Filter (when size tab is active) */}
|
||||||
|
{activeFilterCriteria === 'size' && (
|
||||||
|
<RangeThermometer
|
||||||
|
options={SIZE_OPTIONS}
|
||||||
|
minValue={sizesToRange(includeSizes)[0]}
|
||||||
|
maxValue={sizesToRange(includeSizes)[1]}
|
||||||
|
onChange={(min, max) => onRegionSizesChange(rangeToSizes(min, max))}
|
||||||
|
orientation="vertical"
|
||||||
|
isDark={isDark}
|
||||||
|
counts={regionCountsByCriteria as Partial<Record<RegionSize, number>>}
|
||||||
|
showTotalCount
|
||||||
|
onHoverPreview={setSizeRangePreview}
|
||||||
|
regionNamesByCategory={regionNamesBySize}
|
||||||
|
selectedRegionNames={selectedRegionNames}
|
||||||
|
onRegionNameHover={setPreviewRegionName}
|
||||||
|
hideCountOnMd={fillContainer && selectedRegionNames.length > 0}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Importance Filter (when importance tab is active) */}
|
||||||
|
{activeFilterCriteria === 'importance' && (
|
||||||
|
<RangeThermometer
|
||||||
|
options={IMPORTANCE_OPTIONS}
|
||||||
|
minValue={importanceToRange(includeImportance)[0]}
|
||||||
|
maxValue={importanceToRange(includeImportance)[1]}
|
||||||
|
onChange={(min, max) => setIncludeImportance(rangeToImportance(min, max))}
|
||||||
|
orientation="vertical"
|
||||||
|
isDark={isDark}
|
||||||
|
counts={regionCountsByCriteria as Partial<Record<ImportanceLevel, number>>}
|
||||||
|
showTotalCount
|
||||||
|
onHoverPreview={setImportanceRangePreview}
|
||||||
|
regionNamesByCategory={regionNamesByImportance}
|
||||||
|
selectedRegionNames={selectedRegionNames}
|
||||||
|
onRegionNameHover={setPreviewRegionName}
|
||||||
|
hideCountOnMd={fillContainer && selectedRegionNames.length > 0}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Population Filter (when population tab is active) */}
|
||||||
|
{activeFilterCriteria === 'population' && (
|
||||||
|
<RangeThermometer
|
||||||
|
options={POPULATION_OPTIONS}
|
||||||
|
minValue={populationToRange(includePopulation)[0]}
|
||||||
|
maxValue={populationToRange(includePopulation)[1]}
|
||||||
|
onChange={(min, max) => setIncludePopulation(rangeToPopulation(min, max))}
|
||||||
|
orientation="vertical"
|
||||||
|
isDark={isDark}
|
||||||
|
counts={regionCountsByCriteria as Partial<Record<PopulationLevel, number>>}
|
||||||
|
showTotalCount
|
||||||
|
onHoverPreview={setPopulationRangePreview}
|
||||||
|
regionNamesByCategory={regionNamesByPopulation}
|
||||||
|
selectedRegionNames={selectedRegionNames}
|
||||||
|
onRegionNameHover={setPreviewRegionName}
|
||||||
|
hideCountOnMd={fillContainer && selectedRegionNames.length > 0}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Inline region list - visible on larger screens only, expands below thermometer */}
|
{/* Inline region list - visible on larger screens only, expands below thermometer */}
|
||||||
{fillContainer && selectedRegionNames.length > 0 && (
|
{fillContainer && selectedRegionNames.length > 0 && (
|
||||||
|
|
@ -1089,8 +1401,8 @@ export function DrillDownMapSelector({
|
||||||
borderColor: isDark ? 'gray.700' : 'gray.300',
|
borderColor: isDark ? 'gray.700' : 'gray.300',
|
||||||
marginTop: '2',
|
marginTop: '2',
|
||||||
paddingTop: '2',
|
paddingTop: '2',
|
||||||
/* Prevent this element from expanding the parent */
|
width: '100%',
|
||||||
...preventFlexExpansion,
|
alignSelf: 'stretch',
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<RegionListPanel
|
<RegionListPanel
|
||||||
|
|
|
||||||
|
|
@ -142,7 +142,7 @@ export interface MapDifficultyConfig {
|
||||||
* Assistance level configuration - controls gameplay features separate from region filtering
|
* Assistance level configuration - controls gameplay features separate from region filtering
|
||||||
*/
|
*/
|
||||||
export interface AssistanceLevelConfig {
|
export interface AssistanceLevelConfig {
|
||||||
id: 'guided' | 'helpful' | 'standard' | 'none'
|
id: 'learning' | 'guided' | 'helpful' | 'standard' | 'none'
|
||||||
label: string
|
label: string
|
||||||
emoji: string
|
emoji: string
|
||||||
description: string
|
description: string
|
||||||
|
|
@ -818,6 +818,797 @@ const REGION_SIZE_CATEGORIES: Record<RegionSize, Set<string>> = {
|
||||||
]),
|
]),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Geopolitical importance category for filtering
|
||||||
|
* Based on international influence (G7/G20/UNSC membership, regional power, etc.)
|
||||||
|
*/
|
||||||
|
export type ImportanceLevel = 'superpower' | 'major' | 'regional' | 'standard' | 'minor'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Geopolitical importance categories for world map
|
||||||
|
* Curated based on international influence and diplomatic standing
|
||||||
|
* ISO 3166-1 alpha-2 codes (lowercase)
|
||||||
|
*/
|
||||||
|
const REGION_IMPORTANCE_CATEGORIES: Record<ImportanceLevel, Set<string>> = {
|
||||||
|
// Superpower: UNSC P5 + G7 (~9 countries)
|
||||||
|
superpower: new Set([
|
||||||
|
'us', // United States - superpower
|
||||||
|
'cn', // China - superpower
|
||||||
|
'ru', // Russia - UNSC P5
|
||||||
|
'gb', // United Kingdom - UNSC P5, G7
|
||||||
|
'fr', // France - UNSC P5, G7
|
||||||
|
'de', // Germany - G7
|
||||||
|
'jp', // Japan - G7
|
||||||
|
'it', // Italy - G7
|
||||||
|
'ca', // Canada - G7
|
||||||
|
]),
|
||||||
|
|
||||||
|
// Major: G20 members and other major economies (~15)
|
||||||
|
major: new Set([
|
||||||
|
'in', // India - G20, rising power
|
||||||
|
'br', // Brazil - G20, regional power
|
||||||
|
'au', // Australia - G20
|
||||||
|
'kr', // South Korea - G20
|
||||||
|
'mx', // Mexico - G20
|
||||||
|
'id', // Indonesia - G20
|
||||||
|
'tr', // Turkey - G20, NATO
|
||||||
|
'sa', // Saudi Arabia - G20, OPEC leader
|
||||||
|
'ar', // Argentina - G20
|
||||||
|
'za', // South Africa - G20, BRICS
|
||||||
|
'es', // Spain - EU major
|
||||||
|
'nl', // Netherlands - EU founding
|
||||||
|
'pl', // Poland - EU major
|
||||||
|
'se', // Sweden - EU
|
||||||
|
'ch', // Switzerland - financial hub
|
||||||
|
]),
|
||||||
|
|
||||||
|
// Regional: Significant regional powers and influential nations (~45)
|
||||||
|
regional: new Set([
|
||||||
|
// Europe
|
||||||
|
'at',
|
||||||
|
'be',
|
||||||
|
'dk',
|
||||||
|
'fi',
|
||||||
|
'gr',
|
||||||
|
'ie',
|
||||||
|
'no',
|
||||||
|
'pt',
|
||||||
|
'cz',
|
||||||
|
'ro',
|
||||||
|
'hu',
|
||||||
|
// Middle East
|
||||||
|
'ae',
|
||||||
|
'il',
|
||||||
|
'eg',
|
||||||
|
'ir',
|
||||||
|
'iq',
|
||||||
|
'pk',
|
||||||
|
'qa',
|
||||||
|
'kw',
|
||||||
|
// Asia
|
||||||
|
'th',
|
||||||
|
'my',
|
||||||
|
'sg',
|
||||||
|
'vn',
|
||||||
|
'ph',
|
||||||
|
'bd',
|
||||||
|
'tw',
|
||||||
|
'hk',
|
||||||
|
// Africa
|
||||||
|
'ng',
|
||||||
|
'ke',
|
||||||
|
'et',
|
||||||
|
'dz',
|
||||||
|
'ma',
|
||||||
|
'gh',
|
||||||
|
// Americas
|
||||||
|
'cl',
|
||||||
|
'co',
|
||||||
|
'pe',
|
||||||
|
've',
|
||||||
|
'cu',
|
||||||
|
// Oceania
|
||||||
|
'nz',
|
||||||
|
]),
|
||||||
|
|
||||||
|
// Standard: Most UN member states with moderate influence (~100)
|
||||||
|
standard: new Set([
|
||||||
|
// Europe
|
||||||
|
'ua',
|
||||||
|
'by',
|
||||||
|
'sk',
|
||||||
|
'bg',
|
||||||
|
'hr',
|
||||||
|
'rs',
|
||||||
|
'lt',
|
||||||
|
'lv',
|
||||||
|
'ee',
|
||||||
|
'si',
|
||||||
|
'ba',
|
||||||
|
'mk',
|
||||||
|
'al',
|
||||||
|
'me',
|
||||||
|
'xk',
|
||||||
|
'md',
|
||||||
|
'is',
|
||||||
|
'cy',
|
||||||
|
'mt',
|
||||||
|
'lu',
|
||||||
|
// Asia
|
||||||
|
'af',
|
||||||
|
'kz',
|
||||||
|
'uz',
|
||||||
|
'tm',
|
||||||
|
'kg',
|
||||||
|
'tj',
|
||||||
|
'mn',
|
||||||
|
'np',
|
||||||
|
'lk',
|
||||||
|
'mm',
|
||||||
|
'kh',
|
||||||
|
'la',
|
||||||
|
'kp',
|
||||||
|
'bt',
|
||||||
|
'bn',
|
||||||
|
// Middle East
|
||||||
|
'jo',
|
||||||
|
'lb',
|
||||||
|
'sy',
|
||||||
|
'ye',
|
||||||
|
'om',
|
||||||
|
'bh',
|
||||||
|
'az',
|
||||||
|
'ge',
|
||||||
|
'am',
|
||||||
|
'ps',
|
||||||
|
// Africa
|
||||||
|
'tz',
|
||||||
|
'ug',
|
||||||
|
'zm',
|
||||||
|
'zw',
|
||||||
|
'sd',
|
||||||
|
'ss',
|
||||||
|
'cd',
|
||||||
|
'ao',
|
||||||
|
'mz',
|
||||||
|
'mg',
|
||||||
|
'cm',
|
||||||
|
'ci',
|
||||||
|
'sn',
|
||||||
|
'ml',
|
||||||
|
'bf',
|
||||||
|
'ne',
|
||||||
|
'td',
|
||||||
|
'cf',
|
||||||
|
'cg',
|
||||||
|
'ga',
|
||||||
|
'gq',
|
||||||
|
'bj',
|
||||||
|
'tg',
|
||||||
|
'gn',
|
||||||
|
'sl',
|
||||||
|
'lr',
|
||||||
|
'gm',
|
||||||
|
'gw',
|
||||||
|
'mr',
|
||||||
|
'tn',
|
||||||
|
'ly',
|
||||||
|
'so',
|
||||||
|
'er',
|
||||||
|
'dj',
|
||||||
|
'rw',
|
||||||
|
'bi',
|
||||||
|
'mw',
|
||||||
|
'bw',
|
||||||
|
'na',
|
||||||
|
'sz',
|
||||||
|
'ls',
|
||||||
|
// Americas
|
||||||
|
'ec',
|
||||||
|
'bo',
|
||||||
|
'py',
|
||||||
|
'uy',
|
||||||
|
'gy',
|
||||||
|
'sr',
|
||||||
|
'pa',
|
||||||
|
'cr',
|
||||||
|
'ni',
|
||||||
|
'hn',
|
||||||
|
'gt',
|
||||||
|
'sv',
|
||||||
|
'bz',
|
||||||
|
'do',
|
||||||
|
'ht',
|
||||||
|
'jm',
|
||||||
|
'tt',
|
||||||
|
'bs',
|
||||||
|
// Oceania
|
||||||
|
'pg',
|
||||||
|
'fj',
|
||||||
|
]),
|
||||||
|
|
||||||
|
// Minor: Small states, territories, and dependencies (~80+)
|
||||||
|
minor: new Set([
|
||||||
|
// Caribbean
|
||||||
|
'bb',
|
||||||
|
'ag',
|
||||||
|
'dm',
|
||||||
|
'lc',
|
||||||
|
'vc',
|
||||||
|
'gd',
|
||||||
|
'kn',
|
||||||
|
'aw',
|
||||||
|
'cw',
|
||||||
|
'bq',
|
||||||
|
'sx',
|
||||||
|
'mf',
|
||||||
|
'bl',
|
||||||
|
'tc',
|
||||||
|
'vg',
|
||||||
|
'vi',
|
||||||
|
'ky',
|
||||||
|
'ai',
|
||||||
|
'ms',
|
||||||
|
'pr',
|
||||||
|
'bm',
|
||||||
|
'gp',
|
||||||
|
'mq',
|
||||||
|
'gf',
|
||||||
|
// Europe
|
||||||
|
'li',
|
||||||
|
'ad',
|
||||||
|
'mc',
|
||||||
|
'sm',
|
||||||
|
'va',
|
||||||
|
'gi',
|
||||||
|
'fo',
|
||||||
|
'ax',
|
||||||
|
'gg',
|
||||||
|
'im',
|
||||||
|
'je',
|
||||||
|
// Pacific
|
||||||
|
'ws',
|
||||||
|
'to',
|
||||||
|
'vu',
|
||||||
|
'sb',
|
||||||
|
'nc',
|
||||||
|
'pf',
|
||||||
|
'gu',
|
||||||
|
'as',
|
||||||
|
'mp',
|
||||||
|
'pw',
|
||||||
|
'fm',
|
||||||
|
'mh',
|
||||||
|
'ki',
|
||||||
|
'nr',
|
||||||
|
'tv',
|
||||||
|
'nu',
|
||||||
|
'tk',
|
||||||
|
'ck',
|
||||||
|
'wf',
|
||||||
|
'pn',
|
||||||
|
// Indian Ocean
|
||||||
|
'mv',
|
||||||
|
'sc',
|
||||||
|
'mu',
|
||||||
|
'km',
|
||||||
|
'yt',
|
||||||
|
're',
|
||||||
|
// African territories
|
||||||
|
'cv',
|
||||||
|
'st',
|
||||||
|
'sh',
|
||||||
|
'eh',
|
||||||
|
// Asian territories
|
||||||
|
'mo',
|
||||||
|
'tl',
|
||||||
|
'io',
|
||||||
|
'cx',
|
||||||
|
'cc',
|
||||||
|
'nf',
|
||||||
|
// Other
|
||||||
|
'gl',
|
||||||
|
'pm',
|
||||||
|
'hm',
|
||||||
|
'bv',
|
||||||
|
'sj',
|
||||||
|
'fk',
|
||||||
|
'gs',
|
||||||
|
'aq',
|
||||||
|
'tf',
|
||||||
|
'go',
|
||||||
|
'ju',
|
||||||
|
'um-dq',
|
||||||
|
'um-fq',
|
||||||
|
'um-hq',
|
||||||
|
'um-jq',
|
||||||
|
'um-mq',
|
||||||
|
'um-wq',
|
||||||
|
]),
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display configuration for each importance level
|
||||||
|
*/
|
||||||
|
export const IMPORTANCE_LEVEL_CONFIG: Record<
|
||||||
|
ImportanceLevel,
|
||||||
|
{ label: string; emoji: string; description: string }
|
||||||
|
> = {
|
||||||
|
superpower: {
|
||||||
|
label: 'Superpower',
|
||||||
|
emoji: '🌟',
|
||||||
|
description: 'G7 and UNSC permanent members',
|
||||||
|
},
|
||||||
|
major: {
|
||||||
|
label: 'Major',
|
||||||
|
emoji: '🏛️',
|
||||||
|
description: 'G20 members and major economies',
|
||||||
|
},
|
||||||
|
regional: {
|
||||||
|
label: 'Regional',
|
||||||
|
emoji: '🌐',
|
||||||
|
description: 'Regional powers and influential nations',
|
||||||
|
},
|
||||||
|
standard: {
|
||||||
|
label: 'Standard',
|
||||||
|
emoji: '🏳️',
|
||||||
|
description: 'Most UN member states',
|
||||||
|
},
|
||||||
|
minor: {
|
||||||
|
label: 'Minor',
|
||||||
|
emoji: '🏝️',
|
||||||
|
description: 'Small states and territories',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* All importance levels in order from most to least important
|
||||||
|
*/
|
||||||
|
export const ALL_IMPORTANCE_LEVELS: ImportanceLevel[] = [
|
||||||
|
'superpower',
|
||||||
|
'major',
|
||||||
|
'regional',
|
||||||
|
'standard',
|
||||||
|
'minor',
|
||||||
|
]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Population category for filtering
|
||||||
|
*/
|
||||||
|
export type PopulationLevel = 'huge' | 'large' | 'medium' | 'small' | 'tiny'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Population categories for world map
|
||||||
|
* Based on approximate population (2024 estimates)
|
||||||
|
* ISO 3166-1 alpha-2 codes (lowercase)
|
||||||
|
*/
|
||||||
|
const REGION_POPULATION_CATEGORIES: Record<PopulationLevel, Set<string>> = {
|
||||||
|
// Huge: 100M+ population (~13 countries)
|
||||||
|
huge: new Set([
|
||||||
|
'cn', // China - 1.4B
|
||||||
|
'in', // India - 1.4B
|
||||||
|
'us', // United States - 335M
|
||||||
|
'id', // Indonesia - 277M
|
||||||
|
'pk', // Pakistan - 235M
|
||||||
|
'br', // Brazil - 216M
|
||||||
|
'ng', // Nigeria - 224M
|
||||||
|
'bd', // Bangladesh - 173M
|
||||||
|
'ru', // Russia - 144M
|
||||||
|
'mx', // Mexico - 130M
|
||||||
|
'jp', // Japan - 125M
|
||||||
|
'et', // Ethiopia - 126M
|
||||||
|
'ph', // Philippines - 117M
|
||||||
|
]),
|
||||||
|
|
||||||
|
// Large: 30-100M population (~30 countries)
|
||||||
|
large: new Set([
|
||||||
|
'eg', // Egypt - 105M
|
||||||
|
'vn', // Vietnam - 99M
|
||||||
|
'cd', // DR Congo - 99M
|
||||||
|
'tr', // Turkey - 85M
|
||||||
|
'ir', // Iran - 87M
|
||||||
|
'de', // Germany - 84M
|
||||||
|
'th', // Thailand - 72M
|
||||||
|
'gb', // United Kingdom - 67M
|
||||||
|
'fr', // France - 68M
|
||||||
|
'it', // Italy - 59M
|
||||||
|
'za', // South Africa - 60M
|
||||||
|
'tz', // Tanzania - 65M
|
||||||
|
'mm', // Myanmar - 54M
|
||||||
|
'kr', // South Korea - 52M
|
||||||
|
'co', // Colombia - 52M
|
||||||
|
'ke', // Kenya - 54M
|
||||||
|
'es', // Spain - 48M
|
||||||
|
'ar', // Argentina - 46M
|
||||||
|
'ug', // Uganda - 48M
|
||||||
|
'dz', // Algeria - 45M
|
||||||
|
'sd', // Sudan - 46M
|
||||||
|
'ua', // Ukraine - 38M
|
||||||
|
'iq', // Iraq - 43M
|
||||||
|
'af', // Afghanistan - 41M
|
||||||
|
'pl', // Poland - 38M
|
||||||
|
'ca', // Canada - 40M
|
||||||
|
'ma', // Morocco - 37M
|
||||||
|
'sa', // Saudi Arabia - 36M
|
||||||
|
'uz', // Uzbekistan - 35M
|
||||||
|
'pe', // Peru - 34M
|
||||||
|
'my', // Malaysia - 34M
|
||||||
|
'ao', // Angola - 35M
|
||||||
|
]),
|
||||||
|
|
||||||
|
// Medium: 10-30M population (~50 countries)
|
||||||
|
medium: new Set([
|
||||||
|
've',
|
||||||
|
'np',
|
||||||
|
'gh',
|
||||||
|
'mz',
|
||||||
|
'ye',
|
||||||
|
'mg',
|
||||||
|
'kp',
|
||||||
|
'au',
|
||||||
|
'cm',
|
||||||
|
'ci',
|
||||||
|
'tw',
|
||||||
|
'ne',
|
||||||
|
'lk',
|
||||||
|
'bf',
|
||||||
|
'ml',
|
||||||
|
'sy',
|
||||||
|
'mw',
|
||||||
|
'ro',
|
||||||
|
'cl',
|
||||||
|
'kz',
|
||||||
|
'zm',
|
||||||
|
'ec',
|
||||||
|
'sn',
|
||||||
|
'td',
|
||||||
|
'nl',
|
||||||
|
'so',
|
||||||
|
'gt',
|
||||||
|
'zw',
|
||||||
|
'rw',
|
||||||
|
'gn',
|
||||||
|
'bj',
|
||||||
|
'bi',
|
||||||
|
'tn',
|
||||||
|
'be',
|
||||||
|
'bo',
|
||||||
|
'ht',
|
||||||
|
'cu',
|
||||||
|
'cz',
|
||||||
|
'jo',
|
||||||
|
'gr',
|
||||||
|
'do',
|
||||||
|
'se',
|
||||||
|
'pt',
|
||||||
|
'az',
|
||||||
|
'ae',
|
||||||
|
'hn',
|
||||||
|
'hu',
|
||||||
|
'tj',
|
||||||
|
'by',
|
||||||
|
'at',
|
||||||
|
'ch',
|
||||||
|
'pg',
|
||||||
|
'il',
|
||||||
|
'tg',
|
||||||
|
'sl',
|
||||||
|
'ss',
|
||||||
|
]),
|
||||||
|
|
||||||
|
// Small: 1-10M population (~60 countries)
|
||||||
|
small: new Set([
|
||||||
|
'hk',
|
||||||
|
'la',
|
||||||
|
'ly',
|
||||||
|
'rs',
|
||||||
|
'bg',
|
||||||
|
'pa',
|
||||||
|
'lb',
|
||||||
|
'lr',
|
||||||
|
'cf',
|
||||||
|
'ni',
|
||||||
|
'ie',
|
||||||
|
'cr',
|
||||||
|
'cg',
|
||||||
|
'ps',
|
||||||
|
'nz',
|
||||||
|
'sk',
|
||||||
|
'ge',
|
||||||
|
'hr',
|
||||||
|
'om',
|
||||||
|
'pr',
|
||||||
|
'dk',
|
||||||
|
'no',
|
||||||
|
'sg',
|
||||||
|
'er',
|
||||||
|
'fi',
|
||||||
|
'ky',
|
||||||
|
'mr',
|
||||||
|
'kw',
|
||||||
|
'bi',
|
||||||
|
'md',
|
||||||
|
'ja',
|
||||||
|
'na',
|
||||||
|
'mk',
|
||||||
|
'bw',
|
||||||
|
'lt',
|
||||||
|
'gm',
|
||||||
|
'ga',
|
||||||
|
'si',
|
||||||
|
'qa',
|
||||||
|
'xk',
|
||||||
|
'ba',
|
||||||
|
'lv',
|
||||||
|
'gw',
|
||||||
|
'ee',
|
||||||
|
'mu',
|
||||||
|
'tt',
|
||||||
|
'tl',
|
||||||
|
'cy',
|
||||||
|
'fj',
|
||||||
|
'dj',
|
||||||
|
'km',
|
||||||
|
'bt',
|
||||||
|
'gq',
|
||||||
|
'ls',
|
||||||
|
'sz',
|
||||||
|
'bh',
|
||||||
|
'mn',
|
||||||
|
'me',
|
||||||
|
'al',
|
||||||
|
'arm',
|
||||||
|
'jm',
|
||||||
|
'kg',
|
||||||
|
'tm',
|
||||||
|
]),
|
||||||
|
|
||||||
|
// Tiny: <1M population (~60+ countries/territories)
|
||||||
|
tiny: new Set([
|
||||||
|
// Caribbean
|
||||||
|
'bb',
|
||||||
|
'ag',
|
||||||
|
'dm',
|
||||||
|
'lc',
|
||||||
|
'vc',
|
||||||
|
'gd',
|
||||||
|
'kn',
|
||||||
|
'aw',
|
||||||
|
'cw',
|
||||||
|
'bq',
|
||||||
|
'sx',
|
||||||
|
'mf',
|
||||||
|
'bl',
|
||||||
|
'tc',
|
||||||
|
'vg',
|
||||||
|
'vi',
|
||||||
|
'ai',
|
||||||
|
'ms',
|
||||||
|
'bm',
|
||||||
|
'gp',
|
||||||
|
'mq',
|
||||||
|
'gf',
|
||||||
|
// Europe
|
||||||
|
'lu',
|
||||||
|
'mt',
|
||||||
|
'is',
|
||||||
|
'li',
|
||||||
|
'ad',
|
||||||
|
'mc',
|
||||||
|
'sm',
|
||||||
|
'va',
|
||||||
|
'gi',
|
||||||
|
'fo',
|
||||||
|
'ax',
|
||||||
|
'gg',
|
||||||
|
'im',
|
||||||
|
'je',
|
||||||
|
// Pacific
|
||||||
|
'ws',
|
||||||
|
'to',
|
||||||
|
'vu',
|
||||||
|
'sb',
|
||||||
|
'nc',
|
||||||
|
'pf',
|
||||||
|
'gu',
|
||||||
|
'as',
|
||||||
|
'mp',
|
||||||
|
'pw',
|
||||||
|
'fm',
|
||||||
|
'mh',
|
||||||
|
'ki',
|
||||||
|
'nr',
|
||||||
|
'tv',
|
||||||
|
'nu',
|
||||||
|
'tk',
|
||||||
|
'ck',
|
||||||
|
'wf',
|
||||||
|
'pn',
|
||||||
|
// Indian Ocean
|
||||||
|
'mv',
|
||||||
|
'sc',
|
||||||
|
're',
|
||||||
|
'yt',
|
||||||
|
// Africa
|
||||||
|
'cv',
|
||||||
|
'st',
|
||||||
|
'sh',
|
||||||
|
'eh',
|
||||||
|
// Asian/Other
|
||||||
|
'mo',
|
||||||
|
'bn',
|
||||||
|
'pm',
|
||||||
|
'io',
|
||||||
|
'cx',
|
||||||
|
'cc',
|
||||||
|
'nf',
|
||||||
|
'gl',
|
||||||
|
'hm',
|
||||||
|
'bv',
|
||||||
|
'sj',
|
||||||
|
'fk',
|
||||||
|
'gs',
|
||||||
|
'aq',
|
||||||
|
'tf',
|
||||||
|
'go',
|
||||||
|
'ju',
|
||||||
|
'um-dq',
|
||||||
|
'um-fq',
|
||||||
|
'um-hq',
|
||||||
|
'um-jq',
|
||||||
|
'um-mq',
|
||||||
|
'um-wq',
|
||||||
|
]),
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display configuration for each population level
|
||||||
|
*/
|
||||||
|
export const POPULATION_LEVEL_CONFIG: Record<
|
||||||
|
PopulationLevel,
|
||||||
|
{ label: string; emoji: string; description: string }
|
||||||
|
> = {
|
||||||
|
huge: {
|
||||||
|
label: 'Huge',
|
||||||
|
emoji: '🏙️',
|
||||||
|
description: 'Countries with 100M+ people',
|
||||||
|
},
|
||||||
|
large: {
|
||||||
|
label: 'Large',
|
||||||
|
emoji: '🌆',
|
||||||
|
description: 'Countries with 30-100M people',
|
||||||
|
},
|
||||||
|
medium: {
|
||||||
|
label: 'Medium',
|
||||||
|
emoji: '🏘️',
|
||||||
|
description: 'Countries with 10-30M people',
|
||||||
|
},
|
||||||
|
small: {
|
||||||
|
label: 'Small',
|
||||||
|
emoji: '🏡',
|
||||||
|
description: 'Countries with 1-10M people',
|
||||||
|
},
|
||||||
|
tiny: {
|
||||||
|
label: 'Tiny',
|
||||||
|
emoji: '🏝️',
|
||||||
|
description: 'Countries with <1M people',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* All population levels in order from largest to smallest
|
||||||
|
*/
|
||||||
|
export const ALL_POPULATION_LEVELS: PopulationLevel[] = ['huge', 'large', 'medium', 'small', 'tiny']
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter criteria type - which dimension to filter regions by
|
||||||
|
*/
|
||||||
|
export type FilterCriteria = 'size' | 'importance' | 'population'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display configuration for filter criteria tabs
|
||||||
|
*/
|
||||||
|
export const FILTER_CRITERIA_CONFIG: Record<
|
||||||
|
FilterCriteria,
|
||||||
|
{ label: string; emoji: string; description: string }
|
||||||
|
> = {
|
||||||
|
size: {
|
||||||
|
label: 'Size',
|
||||||
|
emoji: '📏',
|
||||||
|
description: 'Filter by geographic size',
|
||||||
|
},
|
||||||
|
importance: {
|
||||||
|
label: 'Importance',
|
||||||
|
emoji: '🏛️',
|
||||||
|
description: 'Filter by geopolitical importance',
|
||||||
|
},
|
||||||
|
population: {
|
||||||
|
label: 'Population',
|
||||||
|
emoji: '👥',
|
||||||
|
description: 'Filter by population',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the importance category for a region ID
|
||||||
|
*/
|
||||||
|
export function getRegionImportanceCategory(regionId: string): ImportanceLevel | null {
|
||||||
|
for (const [level, ids] of Object.entries(REGION_IMPORTANCE_CATEGORIES)) {
|
||||||
|
if (ids.has(regionId)) {
|
||||||
|
return level as ImportanceLevel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the population category for a region ID
|
||||||
|
*/
|
||||||
|
export function getRegionPopulationCategory(regionId: string): PopulationLevel | null {
|
||||||
|
for (const [level, ids] of Object.entries(REGION_POPULATION_CATEGORIES)) {
|
||||||
|
if (ids.has(regionId)) {
|
||||||
|
return level as PopulationLevel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a region should be included based on importance requirements
|
||||||
|
*/
|
||||||
|
export function shouldIncludeRegionByImportance(
|
||||||
|
regionId: string,
|
||||||
|
includeLevels: ImportanceLevel[]
|
||||||
|
): boolean {
|
||||||
|
const category = getRegionImportanceCategory(regionId)
|
||||||
|
if (!category) {
|
||||||
|
// If no category found, include by default (for regions not in our list)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return includeLevels.includes(category)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a region should be included based on population requirements
|
||||||
|
*/
|
||||||
|
export function shouldIncludeRegionByPopulation(
|
||||||
|
regionId: string,
|
||||||
|
includeLevels: PopulationLevel[]
|
||||||
|
): boolean {
|
||||||
|
const category = getRegionPopulationCategory(regionId)
|
||||||
|
if (!category) {
|
||||||
|
// If no category found, include by default (for regions not in our list)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return includeLevels.includes(category)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter regions by importance levels
|
||||||
|
*/
|
||||||
|
export function filterRegionsByImportance(
|
||||||
|
regions: MapRegion[],
|
||||||
|
includeLevels: ImportanceLevel[]
|
||||||
|
): MapRegion[] {
|
||||||
|
if (includeLevels.length === 0 || includeLevels.length === ALL_IMPORTANCE_LEVELS.length) {
|
||||||
|
return regions
|
||||||
|
}
|
||||||
|
return regions.filter((r) => shouldIncludeRegionByImportance(r.id, includeLevels))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter regions by population levels
|
||||||
|
*/
|
||||||
|
export function filterRegionsByPopulation(
|
||||||
|
regions: MapRegion[],
|
||||||
|
includeLevels: PopulationLevel[]
|
||||||
|
): MapRegion[] {
|
||||||
|
if (includeLevels.length === 0 || includeLevels.length === ALL_POPULATION_LEVELS.length) {
|
||||||
|
return regions
|
||||||
|
}
|
||||||
|
return regions.filter((r) => shouldIncludeRegionByPopulation(r.id, includeLevels))
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the size category for a region ID
|
* Get the size category for a region ID
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,32 @@
|
||||||
/**
|
/**
|
||||||
* Utility functions for converting between region size arrays and min/max ranges.
|
* Utility functions for converting between region filter arrays and min/max ranges.
|
||||||
* Used by the DrillDownMapSelector's RangeThermometer component.
|
* Used by the DrillDownMapSelector's RangeThermometer component.
|
||||||
|
* Supports size, importance, and population filter criteria.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { RegionSize } from '../maps'
|
import type { RegionSize, ImportanceLevel, PopulationLevel, FilterCriteria } from '../maps'
|
||||||
import { ALL_REGION_SIZES } from '../maps'
|
import { ALL_REGION_SIZES, ALL_IMPORTANCE_LEVELS, ALL_POPULATION_LEVELS } from '../maps'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generic type for all filter level types
|
||||||
|
*/
|
||||||
|
export type FilterLevel = RegionSize | ImportanceLevel | PopulationLevel
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the appropriate "ALL_*_LEVELS" array for a given filter criteria
|
||||||
|
*/
|
||||||
|
export function getAllLevelsForCriteria<T extends FilterLevel>(criteria: FilterCriteria): T[] {
|
||||||
|
switch (criteria) {
|
||||||
|
case 'size':
|
||||||
|
return ALL_REGION_SIZES as T[]
|
||||||
|
case 'importance':
|
||||||
|
return ALL_IMPORTANCE_LEVELS as T[]
|
||||||
|
case 'population':
|
||||||
|
return ALL_POPULATION_LEVELS as T[]
|
||||||
|
default:
|
||||||
|
return ALL_REGION_SIZES as T[]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert an array of sizes to min/max values for the range thermometer.
|
* Convert an array of sizes to min/max values for the range thermometer.
|
||||||
|
|
@ -75,3 +97,50 @@ export function calculatePreviewChanges(
|
||||||
|
|
||||||
return { addRegions, removeRegions }
|
return { addRegions, removeRegions }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generic: Convert an array of levels to min/max values for the range thermometer.
|
||||||
|
* Works with any filter criteria (size, importance, population).
|
||||||
|
*/
|
||||||
|
export function levelsToRange<T extends FilterLevel>(levels: T[], allLevels: T[]): [T, T] {
|
||||||
|
const sorted = [...levels].sort((a, b) => allLevels.indexOf(a) - allLevels.indexOf(b))
|
||||||
|
return [sorted[0], sorted[sorted.length - 1]]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generic: Convert min/max range values back to an array of levels.
|
||||||
|
* Works with any filter criteria (size, importance, population).
|
||||||
|
*/
|
||||||
|
export function rangeToLevels<T extends FilterLevel>(min: T, max: T, allLevels: T[]): T[] {
|
||||||
|
const minIdx = allLevels.indexOf(min)
|
||||||
|
const maxIdx = allLevels.indexOf(max)
|
||||||
|
return allLevels.slice(minIdx, maxIdx + 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Importance-specific: Convert array to range
|
||||||
|
*/
|
||||||
|
export function importanceToRange(levels: ImportanceLevel[]): [ImportanceLevel, ImportanceLevel] {
|
||||||
|
return levelsToRange(levels, ALL_IMPORTANCE_LEVELS)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Importance-specific: Convert range to array
|
||||||
|
*/
|
||||||
|
export function rangeToImportance(min: ImportanceLevel, max: ImportanceLevel): ImportanceLevel[] {
|
||||||
|
return rangeToLevels(min, max, ALL_IMPORTANCE_LEVELS)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Population-specific: Convert array to range
|
||||||
|
*/
|
||||||
|
export function populationToRange(levels: PopulationLevel[]): [PopulationLevel, PopulationLevel] {
|
||||||
|
return levelsToRange(levels, ALL_POPULATION_LEVELS)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Population-specific: Convert range to array
|
||||||
|
*/
|
||||||
|
export function rangeToPopulation(min: PopulationLevel, max: PopulationLevel): PopulationLevel[] {
|
||||||
|
return rangeToLevels(min, max, ALL_POPULATION_LEVELS)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -221,7 +221,8 @@ export function RangeThermometer<T extends string>({
|
||||||
className={css({
|
className={css({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: isVertical ? 'row' : 'column',
|
flexDirection: isVertical ? 'row' : 'column',
|
||||||
gap: '2',
|
justifyContent: 'space-between',
|
||||||
|
gap: '1',
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{/* Labels column */}
|
{/* Labels column */}
|
||||||
|
|
@ -258,9 +259,9 @@ export function RangeThermometer<T extends string>({
|
||||||
className={css({
|
className={css({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
gap: '1.5',
|
gap: '1',
|
||||||
py: '1',
|
py: '1',
|
||||||
px: '2',
|
px: '1.5',
|
||||||
rounded: 'md',
|
rounded: 'md',
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
transition: 'all 0.15s',
|
transition: 'all 0.15s',
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue