Vector Map
vector-mapuiReact mirror of @uipkge/vector-map — see the Vue registry item for the canonical description.
Installation
$pnpm dlx shadcn@latest add https://uipkge.dev/r/react/vector-map.json$npx shadcn@latest add https://uipkge.dev/r/react/vector-map.json$yarn dlx shadcn@latest add https://uipkge.dev/r/react/vector-map.json$bunx shadcn@latest add https://uipkge.dev/r/react/vector-map.jsonNamed registry:
npx shadcn@latest add @uipkge-react/vector-mapInstalls to:components/ui/charts/vector-map/Examples
Loading interactive previews…
Loading interactive previews…
Props
| Name | Type / Values | Default | Required |
|---|---|---|---|
mode | 'countries' | 'continents' | — | optional |
regions | MapRegion[] | — | optional |
regionData | Record<string, RegionDataRecord> | — | optional |
selectedRegion | string | — | optional |
onSelectedRegionChange | (id: string) => void | — | optional |
pins | MapPin[] | — | optional |
routes | FlowRoute[] | — | optional |
height | number | string | — | optional |
interactive | boolean | — | optional |
showGraticule | boolean | — | optional |
showRegionLabels | boolean | — | optional |
fillColor | string | — | optional |
hoverColor | string | — | optional |
strokeColor | string | — | optional |
className | string | — | optional |
ariaLabel | string | — | optional |
projection | 'globe' | 'mercator' | — | optional |
Schema
Type aliases from this item's source — use them to shape the data you pass in.
MapRegioninterface MapRegion {
id: string
name: string
path?: string
}MapPininterface MapPin {
id?: string
lat?: number
lng?: number
x?: number
y?: number
label?: string
value?: string | number
color?: string
status?: string
description?: string
}FlowRouteinterface FlowRoute {
id?: string
from: { lat?: number; lng?: number; x?: number; y?: number }
to: { lat?: number; lng?: number; x?: number; y?: number }
color?: string
width?: number
curvature?: number
animated?: boolean
dashed?: boolean
duration?: number
label?: string
}RegionDataRecordinterface RegionDataRecord {
value?: number
label?: string
status?: string
color?: string
description?: string
}npm dependencies
Includes
Files installed (2)
components/ui/charts/vector-map/VectorMap.tsx9.7 kB
'use client' import * as React from 'react' import type { LineLayerSpecification } from 'mapbox-gl' import { Map, MapMarker, MapSource, MapLayer, type MapVariant } from '@/components/ui/map' import { cn } from '@/lib/utils' import { Globe } from 'lucide-react' export interface MapRegion { id: string name: string path?: string } export interface MapPin { id?: string lat?: number lng?: number x?: number y?: number label?: string value?: string | number color?: string status?: string description?: string } export interface FlowRoute { id?: string from: { lat?: number; lng?: number; x?: number; y?: number } to: { lat?: number; lng?: number; x?: number; y?: number } color?: string width?: number curvature?: number animated?: boolean dashed?: boolean duration?: number label?: string } export interface RegionDataRecord { value?: number label?: string status?: string color?: string description?: string } export interface VectorMapProps { mode?: 'countries' | 'continents' regions?: MapRegion[] regionData?: Record<string, RegionDataRecord> selectedRegion?: string onSelectedRegionChange?: (id: string) => void pins?: MapPin[] routes?: FlowRoute[] height?: number | string interactive?: boolean showGraticule?: boolean showRegionLabels?: boolean fillColor?: string hoverColor?: string strokeColor?: string className?: string ariaLabel?: string projection?: 'globe' | 'mercator' } export const WORLD_REGIONS: MapRegion[] = [ { id: 'northAmerica', name: 'North America' }, { id: 'southAmerica', name: 'South America' }, { id: 'europe', name: 'Europe' }, { id: 'asia', name: 'Asia' }, { id: 'africa', name: 'Africa' }, { id: 'australiaOceania', name: 'Oceania' }, { id: 'unitedKingdom', name: 'United Kingdom' }, { id: 'japan', name: 'Japan' }, ] export const WORLD_COUNTRIES: MapRegion[] = [ { id: 'US', name: 'United States' }, { id: 'CA', name: 'Canada' }, { id: 'GB', name: 'United Kingdom' }, { id: 'DE', name: 'Germany' }, { id: 'FR', name: 'France' }, { id: 'JP', name: 'Japan' }, { id: 'CN', name: 'China' }, { id: 'IN', name: 'India' }, { id: 'BR', name: 'Brazil' }, { id: 'AU', name: 'Australia' }, ] export function projectPoint(pt: { lat?: number; lng?: number; x?: number; y?: number }) { if (pt.x !== undefined && pt.y !== undefined) return { x: pt.x, y: pt.y } const lng = pt.lng ?? 0 const lat = pt.lat ?? 0 const x = ((lng + 180) / 360) * 1000 const y = ((90 - lat) / 180) * 500 return { x, y } } export function VectorMap({ mode = 'continents', regions = WORLD_REGIONS, regionData = {}, selectedRegion: controlledRegion, onSelectedRegionChange, pins = [], routes = [], height = 460, interactive = true, projection: initialProjection = 'globe', className, }: VectorMapProps) { const [internalRegion, setInternalRegion] = React.useState<string>('northAmerica') const [projection, setProjection] = React.useState<'globe' | 'mercator'>(initialProjection) const [hoveredPin, setHoveredPin] = React.useState<MapPin | null>(null) const activeRegion = controlledRegion !== undefined ? controlledRegion : internalRegion const activeRecord = regionData[activeRegion] || null const handleSelectRegion = (id: string) => { if (!interactive) return setInternalRegion(id) onSelectedRegionChange?.(id) } const toggleProjection = () => { setProjection((prev) => (prev === 'globe' ? 'mercator' : 'globe')) } const routesGeoJson = React.useMemo(() => { if (!routes || !routes.length) return null return { type: 'FeatureCollection', features: routes.map((r, i) => { const fromLng = r.from.lng ?? -74 const fromLat = r.from.lat ?? 40 const toLng = r.to.lng ?? 8 const toLat = r.to.lat ?? 50 return { type: 'Feature', id: i, properties: { color: r.color || 'rgba(56, 189, 248, 0.75)', }, geometry: { type: 'LineString', coordinates: [ [fromLng, fromLat], [(fromLng + toLng) / 2, (fromLat + toLat) / 2 + 5], [toLng, toLat], ], }, } }), } }, [routes]) // mapbox reads a data-driven paint value as an expression tuple; without the // annotation TS widens ['get', 'color'] to string[] and the layer rejects it. const routeLinePaint: LineLayerSpecification['paint'] = { 'line-color': ['get', 'color'], 'line-width': 1.5, 'line-dasharray': [2, 2], } return ( <div className={cn( 'border-border bg-card group relative w-full overflow-hidden rounded-xl border shadow-xs', className, )} style={{ height: typeof height === 'number' ? `${height}px` : height }} > <Map variant="dark" projection={projection} center={[10, 25]} zoom={1.6} className="size-full"> {routesGeoJson && ( <MapSource id="vector-routes-source" type="geojson" data={routesGeoJson}> <MapLayer id="vector-routes-layer" type="line" paint={routeLinePaint} /> </MapSource> )} {pins.map((pin, i) => ( <MapMarker key={i} longitude={pin.lng ?? 0} latitude={pin.lat ?? 0} anchor="center" className="cursor-pointer select-none" > <div className="relative flex size-6 items-center justify-center" onMouseEnter={() => setHoveredPin(pin)} onMouseLeave={() => setHoveredPin(null)} > <span className="absolute inline-flex size-full animate-ping rounded-full opacity-60" style={{ backgroundColor: pin.color || 'oklch(0.65 0.20 145)' }} /> <span className="ring-background relative inline-flex size-2.5 rounded-full shadow-xs ring-2" style={{ backgroundColor: pin.color || 'oklch(0.65 0.20 145)', boxShadow: `0 0 10px ${pin.color || 'oklch(0.65 0.20 145)'}`, }} /> </div> </MapMarker> ))} </Map> <div className="border-border/70 bg-card/85 absolute top-3 left-3 z-10 hidden max-w-md flex-wrap gap-1 rounded-lg border p-1.5 shadow-xs backdrop-blur-md md:flex"> {regions.map((r) => ( <button key={r.id} type="button" className={cn( 'rounded-md px-2 py-0.5 text-xs font-medium transition', activeRegion === r.id ? 'bg-primary text-primary-foreground font-semibold' : 'text-muted-foreground hover:bg-muted hover:text-foreground', )} onClick={() => handleSelectRegion(r.id)} > {r.name} </button> ))} </div> <div className="border-border/70 bg-card/85 absolute top-3 right-3 z-10 flex items-center gap-1 rounded-lg border p-1 shadow-xs backdrop-blur-md"> <button type="button" className="text-muted-foreground hover:bg-muted hover:text-foreground flex items-center gap-1.5 rounded-md px-2.5 py-1 text-xs font-medium transition" onClick={toggleProjection} > <Globe className="size-3.5" /> <span className="capitalize">{projection}</span> </button> </div> {activeRecord && ( <div className="border-border/80 bg-card/95 animate-in fade-in slide-in-from-bottom-2 absolute bottom-3 left-3 z-10 max-w-sm rounded-xl border p-3.5 shadow-lg backdrop-blur-md duration-150"> <div className="flex items-center gap-2"> <span className="size-2 rounded-full" style={{ backgroundColor: activeRecord.color || 'oklch(0.65 0.20 145)' }} /> <h4 className="text-foreground text-sm font-semibold capitalize"> {activeRegion.replace(/([A-Z])/g, ' $1')} </h4> <span className="text-muted-foreground ml-auto font-mono text-xs uppercase"> {activeRecord.status || 'Optimal'} </span> </div> {activeRecord.value !== undefined && ( <div className="border-border/60 mt-2.5 flex items-baseline justify-between border-t pt-2 font-mono text-xs"> <span className="text-muted-foreground">Active Nodes</span> <span className="text-foreground font-semibold">{activeRecord.value.toLocaleString()}</span> </div> )} {activeRecord.description && ( <p className="text-muted-foreground mt-1.5 text-xs leading-relaxed">{activeRecord.description}</p> )} </div> )} {hoveredPin && ( <div className="border-border/80 bg-card/95 animate-in fade-in slide-in-from-bottom-2 absolute right-3 bottom-3 z-10 max-w-xs rounded-xl border p-3 shadow-lg backdrop-blur-md duration-150"> <div className="flex items-center gap-2"> <span className="size-2 rounded-full" style={{ backgroundColor: hoveredPin.color || 'oklch(0.65 0.20 145)' }} /> <h5 className="text-foreground text-xs font-semibold">{hoveredPin.label || 'Hub'}</h5> </div> {hoveredPin.description && <p className="text-muted-foreground mt-1 text-xs">{hoveredPin.description}</p>} {hoveredPin.value !== undefined && ( <div className="border-border/60 mt-2 flex items-baseline justify-between border-t pt-1.5 font-mono text-xs"> <span className="text-muted-foreground">Throughput</span> <span className="text-foreground font-semibold">{hoveredPin.value}</span> </div> )} </div> )} </div> ) } export default VectorMapcomponents/ui/charts/vector-map/index.ts0.2 kB
export { VectorMap, WORLD_REGIONS, WORLD_COUNTRIES, projectPoint, type VectorMapProps, type MapRegion, type MapPin, type FlowRoute, type RegionDataRecord, } from './VectorMap' export { default } from './VectorMap'
Raw manifest:https://uipkge.dev/r/react/vector-map.json