@wescld/dotted-map
v1.0.0
Published
Interactive dotted globe and flat map component for React — pure Canvas 2D, zero WebGL
Maintainers
Readme

Features
- Globe + Flat map — switch between 3D orthographic globe and 2D equirectangular projection
- Pure Canvas 2D — no WebGL, no Three.js. The 3D globe is rendered with manual orthographic projection math
- Smart clustering — two-phase clustering (geo-grid + screen-space merge) with animated transitions
- Custom markers — render any React component as a marker via
renderMarker - Dark/Light mode — built-in theme support with full customization
- Drag, zoom, auto-rotate — smooth interactions with configurable zoom range
- Tiny footprint — zero runtime dependencies, React as peer dep
- SSR-safe — canvas rendering only runs in the browser
Install
npm install @wescld/dotted-mapQuick start
import { DottedMap } from "@wescld/dotted-map";
const markers = [
{ id: "ny", latitude: 40.71, longitude: -74.01, data: { name: "New York" } },
{ id: "ld", latitude: 51.51, longitude: -0.13, data: { name: "London" } },
{ id: "tk", latitude: 35.68, longitude: 139.69, data: { name: "Tokyo" } },
];
function App() {
return (
<DottedMap
markers={markers}
defaultViewMode="globe"
darkMode
onMarkerClick={(cluster) => {
console.log(cluster.markers);
}}
/>
);
}Custom markers
Use renderMarker to render any React element at each marker position:
<DottedMap
markers={markers}
renderMarker={(marker, position, isActive) => (
<div
style={{
width: 24,
height: 24,
borderRadius: "50%",
background: isActive ? "#22c55e" : "#f97316",
display: "flex",
alignItems: "center",
justifyContent: "center",
color: "white",
fontSize: 10,
fontWeight: 700,
}}
>
{marker.data?.name?.[0]}
</div>
)}
/>Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| markers | MarkerData[] | [] | Array of markers to display |
| defaultViewMode | "globe" \| "flat" | "flat" | Initial view mode (uncontrolled) |
| viewMode | "globe" \| "flat" | — | Controlled view mode |
| onViewModeChange | (mode) => void | — | Called when view mode changes |
| activeMarkerIds | Set<string> \| string[] | — | IDs of markers to highlight as active |
| onMarkerClick | (cluster) => void | — | Called when a marker/cluster is clicked |
| renderMarker | (marker, pos, isActive) => ReactNode | — | Custom marker renderer |
| theme | DottedMapTheme | — | Theme overrides |
| darkMode | boolean | false | Enable dark mode |
| className | string | — | CSS class for the container |
| style | CSSProperties | — | Inline styles for the container |
| initialRotation | [lng, lat] | [-40, 0] | Initial globe rotation |
| autoRotate | boolean | false | Enable auto-rotation on the globe |
| zoomRange | [min, max] | [0.8, 5] | Min/max zoom levels |
| clusterCellDegrees | number | 15 | Geo-grid cell size for clustering |
MarkerData
interface MarkerData {
id: string;
latitude: number;
longitude: number;
data?: Record<string, unknown>;
}DottedMapTheme
interface DottedMapTheme {
dotColor?: string;
globeFill?: string;
outlineColor?: string;
clusterColor?: string;
clusterTextColor?: string;
clusterBorderColor?: string;
activeGlow?: string;
activeBadgeColor?: string;
}Icons
The package exports GlobeIcon and FlatMapIcon components for building view mode toggles:
import { GlobeIcon, FlatMapIcon } from "@wescld/dotted-map";
<button onClick={() => setMode("flat")}><FlatMapIcon /></button>
<button onClick={() => setMode("globe")}><GlobeIcon /></button>How it works
The 3D globe is not WebGL — it's an orthographic projection computed on a 2D canvas. Each of the 4,577 pre-generated land points is projected frame-by-frame using sin/cos math, with back-face culling to hide points on the far side.
Clustering runs in two phases:
- Geo-grid — markers are grouped by latitude/longitude cells
- Screen-space merge — nearby clusters within 50px are merged after projection
Transitions between zoom levels and view modes are animated with proximity-matched lerping for smooth cluster morphing.
Demo
cd packages/react-dotted-map
npm run demo
# opens on http://localhost:3333License
MIT
