A customizable, accessible SVG map of Uzbekistan for React — all 14 top-level administrative units (12 viloyatlar, the Republic of Karakalpakstan, and Tashkent city) and all 199 districts (tumanlar), with built-in metadata, choropleth support, tooltips, and region drill-down.
TypeScript-first. Zero runtime dependencies.
npm install @abez/uzb-mapReact 17+ is a peer dependency; nothing else is required at runtime.
import { UzbekistanMap } from '@abez/uzb-map'
export function App() {
return <UzbekistanMap locale="en" onRegionClick={(code) => console.log(code)} />
}The map is pure viewBox-based SVG — give it a container width and it scales
responsively while keeping Uzbekistan's aspect ratio.
<UzbekistanMap
data={{ 'UZ-FA': 42, 'UZ-TK': 98, 'UZ-QR': 12 }}
// colorScale defaults to a sequential blue scale; bring your own:
// colorScale={interpolateColors(['#f0fdf4', '#16a34a', '#14532d'])}
/>Keys are region codes ("UZ-FA") or, in view="districts", district codes
("UZ-FA-ALTIARIK"). The color domain is the min/max of the values you pass.
const [focusRegion, setFocusRegion] = useState<string>()
<UzbekistanMap
focusRegion={focusRegion}
onRegionClick={(code) => setFocusRegion(code)}
onDistrictClick={(code) => console.log('district', code)}
/>Setting focusRegion zooms into that region (animated, via a CSS transform —
no data re-projection) and lazily loads and reveals its districts; the other
13 regions stay visible, dimmed. Clear it (setFocusRegion(undefined)) to
zoom back out. This is a controlled prop — the component never changes it
itself, so you decide how "back" navigation works.
<UzbekistanMap view="districts" />Draws all 199 districts country-wide. District geometry is code-split into
one chunk per region (import()), so view="districts" fetches all 14
chunks and focusRegion fetches exactly one. Labels default to hidden in
this view (199 overlapping labels at that zoom is unreadable) — pass
showLabels to override.
<UzbekistanMap
theme={{ baseFill: '#f1f5f9', hoverFill: '#fbbf24', stroke: '#fff', dimOpacity: 0.3 }}
regionStyle={({ isHovered }) => (isHovered ? { strokeWidth: 1.5 } : {})}
districtStyle={{ strokeDasharray: '2 2' }}
renderLabel={({ name, centroid, scale }) =>
scale > 3 ? (
<text x={centroid[0]} y={centroid[1]} fontSize={8 / scale} textAnchor="middle">
{name}
</text>
) : null
}
renderTooltip={({ name, meta }) => (
<div style={{ background: '#000', color: '#fff', padding: 6 }}>
{name} — {meta.population?.toLocaleString()}
</div>
)}
/>regionStyle / districtStyle accept either a static SVG-props object or a
function of { code, kind, value, isFocused, isHovered }.
Children render inside the SVG, in the same panned/zoomed group as the map.
Use useProjectPoint() to convert real coordinates. Since markers sit in
that same CSS-scaled group, a marker meant to stay a constant pixel size
(rather than grow with the map) should divide by useMapScale():
import { UzbekistanMap, useProjectPoint, useMapScale } from '@abez/uzb-map'
function CapitalMarker() {
const project = useProjectPoint()
const scale = useMapScale() // 1 when zoomed out, larger during drill-down
const [x, y] = project([69.2401, 41.2995]) // Tashkent
return <circle cx={x} cy={y} r={4 / scale} fill="red" />
}
<UzbekistanMap>
<CapitalMarker />
</UzbekistanMap>Every region and district ships with names in four locales (uz, uz-Cyrl,
ru, en), plus population/area estimates:
import { getRegionMeta, getDistrictMeta, getDistrictsByRegion, REGIONS_META } from '@abez/uzb-map'
getRegionMeta('UZ-FA') // { code, names, capital, population, populationYear, areaKm2, ... }
getDistrictsByRegion('UZ-FA') // all 19 districts of Fergana RegionDistrict-level population/areaKm2 are best-effort: many of the 199
districts don't have a reliably citable area figure publicly available.
Check meta.verified before treating a district's numbers as solid — see
DATA_SOURCES.md for what's confirmed vs. estimated and where it came from.
Accessibility: every clickable unit is a keyboard-focusable (tabIndex,
role="button") <path> with a localized aria-label.
This package is also published as a shadcn registry -- directly from this GitHub repo, no separate hosting needed. Install a ready-made demo component with:
npx shadcn add lastninja294/uzb-map/uzbekistan-mapPin to a specific tag/branch/commit if you want reproducible installs:
npx shadcn add lastninja294/uzb-map/uzbekistan-map#mainAvailable items: uzbekistan-map (basic), uzbekistan-map-choropleth,
uzbekistan-map-drilldown. Each drops a small demo component into your repo
and adds @abez/uzb-map as a dependency — the geometry/metadata stay in the
npm package, only the usage example is copied.
You can also install from the pre-built registry JSON directly (useful for
tools that only accept a URL, not the owner/repo/item shorthand):
npx shadcn add https://raw.githubusercontent.com/lastninja294/uzb-map/main/public/r/uzbekistan-map.jsonGeometry is derived from the geoBoundaries
UZB ADM1/ADM2 dataset (Runfola et al. 2020, CC BY 4.0 / ODbL). Region borders
are a topological dissolve of the district geometry, so they nest exactly.
See DATA_SOURCES.md and LICENSE for full attribution and data-pipeline
notes.
npm install
npm run data:fetch # download geoBoundaries source geojson
npm run data:build # rebuild regions.topo.json + district chunks
npm run data:meta # merge data-src/work/meta/*.json -> typed metadata modules
npm run build # tsup -> dist/
npm run typecheck