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'
|
||||
|
||||
import { useState, useCallback, useMemo, useRef, useEffect } from 'react'
|
||||
import * as Tooltip from '@radix-ui/react-tooltip'
|
||||
import { useSpring } from '@react-spring/web'
|
||||
import { css } from '@styled/css'
|
||||
import {
|
||||
|
|
@ -25,15 +26,30 @@ import {
|
|||
getFilteredMapDataBySizesSync,
|
||||
REGION_SIZE_CONFIG,
|
||||
ALL_REGION_SIZES,
|
||||
IMPORTANCE_LEVEL_CONFIG,
|
||||
ALL_IMPORTANCE_LEVELS,
|
||||
POPULATION_LEVEL_CONFIG,
|
||||
ALL_POPULATION_LEVELS,
|
||||
FILTER_CRITERIA_CONFIG,
|
||||
filterRegionsByImportance,
|
||||
filterRegionsByPopulation,
|
||||
filterRegionsBySizes,
|
||||
} from '../maps'
|
||||
import type { RegionSize } from '../maps'
|
||||
import type { RegionSize, ImportanceLevel, PopulationLevel, FilterCriteria } from '../maps'
|
||||
import {
|
||||
CONTINENTS,
|
||||
getContinentForCountry,
|
||||
COUNTRY_TO_CONTINENT,
|
||||
type ContinentId,
|
||||
} from '../continents'
|
||||
import { sizesToRange, rangeToSizes } from '../utils/regionSizeUtils'
|
||||
import {
|
||||
sizesToRange,
|
||||
rangeToSizes,
|
||||
importanceToRange,
|
||||
rangeToImportance,
|
||||
populationToRange,
|
||||
rangeToPopulation,
|
||||
} from '../utils/regionSizeUtils'
|
||||
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,
|
||||
}))
|
||||
|
||||
/**
|
||||
* 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:
|
||||
* - [] = World level
|
||||
|
|
@ -150,6 +195,27 @@ export function DrillDownMapSelector({
|
|||
// Track which region name is being hovered in the popover (for zoom preview)
|
||||
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)
|
||||
useEffect(() => {
|
||||
const expectedPath = getInitialPath()
|
||||
|
|
@ -290,29 +356,52 @@ export function DrillDownMapSelector({
|
|||
return groups
|
||||
}, [currentLevel])
|
||||
|
||||
// Calculate excluded regions based on includeSizes
|
||||
// These are regions that exist but are filtered out by size settings
|
||||
const excludedRegions = useMemo(() => {
|
||||
// Determine which map we're looking at
|
||||
// Get base regions for the current view (continent filtered but no criteria filtering)
|
||||
const baseRegions = useMemo(() => {
|
||||
const mapId = currentLevel === 2 && path[1] ? 'usa' : 'world'
|
||||
const continentId: ContinentId | 'all' =
|
||||
currentLevel >= 1 && path[0] ? path[0] : selectedContinent
|
||||
|
||||
// Get all regions (unfiltered by size)
|
||||
const allRegionsMapData = getFilteredMapDataBySizesSync(
|
||||
mapId as 'world' | 'usa',
|
||||
continentId,
|
||||
['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)
|
||||
const filteredMapData = getFilteredMapDataBySizesSync(
|
||||
mapId as 'world' | 'usa',
|
||||
continentId,
|
||||
includeSizes
|
||||
)
|
||||
const filteredRegionIds = new Set(filteredMapData.regions.map((r) => r.id))
|
||||
// Get the active filter levels based on current tab
|
||||
const activeFilterLevels = useMemo(() => {
|
||||
switch (activeFilterCriteria) {
|
||||
case 'size':
|
||||
return includeSizes
|
||||
case 'importance':
|
||||
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
|
||||
const excluded: string[] = []
|
||||
|
|
@ -322,35 +411,74 @@ export function DrillDownMapSelector({
|
|||
}
|
||||
}
|
||||
return excluded
|
||||
}, [currentLevel, path, selectedContinent, includeSizes])
|
||||
}, [baseRegions, activeFilterCriteria, includeSizes, includeImportance, includePopulation])
|
||||
|
||||
// Compute region names by size category (for tooltip display)
|
||||
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[]>> = {}
|
||||
for (const size of ALL_REGION_SIZES) {
|
||||
const filtered = getFilteredMapDataBySizesSync(mapId as 'world' | 'usa', continentId, [size])
|
||||
result[size] = filtered.regions.map((r) => r.name).sort((a, b) => a.localeCompare(b))
|
||||
const filtered = filterRegionsBySizes(baseRegions, [size], 'world')
|
||||
result[size] = filtered.map((r) => r.name).sort((a, b) => a.localeCompare(b))
|
||||
}
|
||||
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 mapId = currentLevel === 2 && path[1] ? 'usa' : 'world'
|
||||
const continentId: ContinentId | 'all' =
|
||||
currentLevel >= 1 && path[0] ? path[0] : selectedContinent
|
||||
|
||||
const filtered = getFilteredMapDataBySizesSync(
|
||||
mapId as 'world' | 'usa',
|
||||
continentId,
|
||||
includeSizes
|
||||
)
|
||||
return filtered.regions.map((r) => r.name)
|
||||
}, [currentLevel, path, selectedContinent, includeSizes])
|
||||
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
|
||||
}
|
||||
return filteredRegions.map((r) => r.name)
|
||||
}, [baseRegions, activeFilterCriteria, includeSizes, includeImportance, includePopulation])
|
||||
|
||||
// Create lookup map from region name → region data (for zoom preview)
|
||||
const regionsByName = useMemo(() => {
|
||||
|
|
@ -437,34 +565,68 @@ export function DrillDownMapSelector({
|
|||
return region?.id ?? null
|
||||
}, [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
|
||||
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: [] }
|
||||
}
|
||||
|
||||
// Determine which map we're looking at
|
||||
const mapId = currentLevel === 2 && path[1] ? 'usa' : 'world'
|
||||
const continentId: ContinentId | 'all' =
|
||||
currentLevel >= 1 && path[0] ? path[0] : selectedContinent
|
||||
|
||||
// Get current included region IDs
|
||||
const currentIncluded = getFilteredMapDataBySizesSync(
|
||||
mapId as 'world' | 'usa',
|
||||
continentId,
|
||||
includeSizes
|
||||
)
|
||||
const currentIncludedIds = new Set(currentIncluded.regions.map((r) => r.id))
|
||||
// Get current included region IDs based on active criteria
|
||||
let currentFiltered = baseRegions
|
||||
switch (activeFilterCriteria) {
|
||||
case 'size':
|
||||
currentFiltered = filterRegionsBySizes(baseRegions, includeSizes, 'world')
|
||||
break
|
||||
case 'importance':
|
||||
currentFiltered = filterRegionsByImportance(baseRegions, includeImportance)
|
||||
break
|
||||
case 'population':
|
||||
currentFiltered = filterRegionsByPopulation(baseRegions, includePopulation)
|
||||
break
|
||||
}
|
||||
const currentIncludedIds = new Set(currentFiltered.map((r) => r.id))
|
||||
|
||||
// Get preview included region IDs (if user clicked)
|
||||
const previewSizes = rangeToSizes(sizeRangePreview.previewMin, sizeRangePreview.previewMax)
|
||||
const previewIncluded = getFilteredMapDataBySizesSync(
|
||||
mapId as 'world' | 'usa',
|
||||
continentId,
|
||||
previewSizes
|
||||
)
|
||||
const previewIncludedIds = new Set(previewIncluded.regions.map((r) => r.id))
|
||||
let previewFiltered = baseRegions
|
||||
switch (activeFilterCriteria) {
|
||||
case 'size':
|
||||
if (sizeRangePreview) {
|
||||
const previewSizes = rangeToSizes(
|
||||
sizeRangePreview.previewMin,
|
||||
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)
|
||||
const addRegions: string[] = []
|
||||
|
|
@ -486,7 +648,16 @@ export function DrillDownMapSelector({
|
|||
previewAddRegions: addRegions,
|
||||
previewRemoveRegions: removeRegions,
|
||||
}
|
||||
}, [sizeRangePreview, currentLevel, path, selectedContinent, includeSizes])
|
||||
}, [
|
||||
activeFilterCriteria,
|
||||
sizeRangePreview,
|
||||
importanceRangePreview,
|
||||
populationRangePreview,
|
||||
baseRegions,
|
||||
includeSizes,
|
||||
includeImportance,
|
||||
includePopulation,
|
||||
])
|
||||
|
||||
// Compute the label to display for the hovered region
|
||||
// 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
|
||||
data-element="right-controls"
|
||||
className={css({
|
||||
|
|
@ -1048,35 +1219,176 @@ export function DrillDownMapSelector({
|
|||
transformOrigin: 'top right',
|
||||
})}
|
||||
>
|
||||
{/* Region Size Range Selector with inline list expansion on desktop */}
|
||||
{/* Region Filter Selector with tabs */}
|
||||
<div
|
||||
data-element="region-size-filters"
|
||||
data-element="region-filters"
|
||||
className={css({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'stretch',
|
||||
padding: '3',
|
||||
bg: isDark ? 'gray.800' : 'gray.100',
|
||||
rounded: 'xl',
|
||||
shadow: 'lg',
|
||||
maxHeight: { base: 'none', md: fillContainer ? '400px' : 'none' },
|
||||
width: '205px',
|
||||
maxHeight: { base: 'none', md: fillContainer ? '450px' : 'none' },
|
||||
overflow: 'hidden',
|
||||
})}
|
||||
>
|
||||
<RangeThermometer
|
||||
options={SIZE_OPTIONS}
|
||||
minValue={sizesToRange(includeSizes)[0]}
|
||||
maxValue={sizesToRange(includeSizes)[1]}
|
||||
onChange={(min, max) => onRegionSizesChange(rangeToSizes(min, max))}
|
||||
orientation="vertical"
|
||||
isDark={isDark}
|
||||
counts={regionCountsBySize as Partial<Record<RegionSize, number>>}
|
||||
showTotalCount
|
||||
onHoverPreview={setSizeRangePreview}
|
||||
regionNamesByCategory={regionNamesBySize}
|
||||
selectedRegionNames={selectedRegionNames}
|
||||
onRegionNameHover={setPreviewRegionName}
|
||||
hideCountOnMd={fillContainer && selectedRegionNames.length > 0}
|
||||
/>
|
||||
{/* Filter Criteria Tabs */}
|
||||
<Tooltip.Provider delayDuration={200}>
|
||||
<div
|
||||
data-element="filter-tabs"
|
||||
className={css({
|
||||
display: 'flex',
|
||||
gap: '1px',
|
||||
marginBottom: '2',
|
||||
padding: '2px',
|
||||
bg: isDark ? 'gray.700' : 'gray.200',
|
||||
rounded: 'md',
|
||||
width: '100%',
|
||||
})}
|
||||
>
|
||||
{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 */}
|
||||
{fillContainer && selectedRegionNames.length > 0 && (
|
||||
|
|
@ -1089,8 +1401,8 @@ export function DrillDownMapSelector({
|
|||
borderColor: isDark ? 'gray.700' : 'gray.300',
|
||||
marginTop: '2',
|
||||
paddingTop: '2',
|
||||
/* Prevent this element from expanding the parent */
|
||||
...preventFlexExpansion,
|
||||
width: '100%',
|
||||
alignSelf: 'stretch',
|
||||
})}
|
||||
>
|
||||
<RegionListPanel
|
||||
|
|
|
|||
|
|
@ -142,7 +142,7 @@ export interface MapDifficultyConfig {
|
|||
* Assistance level configuration - controls gameplay features separate from region filtering
|
||||
*/
|
||||
export interface AssistanceLevelConfig {
|
||||
id: 'guided' | 'helpful' | 'standard' | 'none'
|
||||
id: 'learning' | 'guided' | 'helpful' | 'standard' | 'none'
|
||||
label: string
|
||||
emoji: 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
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
* Supports size, importance, and population filter criteria.
|
||||
*/
|
||||
|
||||
import type { RegionSize } from '../maps'
|
||||
import { ALL_REGION_SIZES } from '../maps'
|
||||
import type { RegionSize, ImportanceLevel, PopulationLevel, FilterCriteria } 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.
|
||||
|
|
@ -75,3 +97,50 @@ export function calculatePreviewChanges(
|
|||
|
||||
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({
|
||||
display: 'flex',
|
||||
flexDirection: isVertical ? 'row' : 'column',
|
||||
gap: '2',
|
||||
justifyContent: 'space-between',
|
||||
gap: '1',
|
||||
})}
|
||||
>
|
||||
{/* Labels column */}
|
||||
|
|
@ -258,9 +259,9 @@ export function RangeThermometer<T extends string>({
|
|||
className={css({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '1.5',
|
||||
gap: '1',
|
||||
py: '1',
|
||||
px: '2',
|
||||
px: '1.5',
|
||||
rounded: 'md',
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.15s',
|
||||
|
|
|
|||
Loading…
Reference in New Issue