Skip to content

Repository files navigation

@abez/uzb-map

Live demo →

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.

Install

npm install @abez/uzb-map

React 17+ is a peer dependency; nothing else is required at runtime.

Quick start

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.

Choropleth

<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.

Drill-down

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.

Districts view

<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.

Styling

<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 }.

Custom markers

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>

Locales & metadata

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 Region

District-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.

shadcn registry

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-map

Pin to a specific tag/branch/commit if you want reproducible installs:

npx shadcn add lastninja294/uzb-map/uzbekistan-map#main

Available 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.json

Data & attribution

Geometry 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.

Development

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

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages