Anycast catchment
Which point of presence serves which part of the world, as a dot map. Turn a site off and its neighbours flood the hole from their own side.
Your data
Pass your points of presence as sites, the ids in service as active and your request sources as clients. Without sites the component shows ten demo sites with demo traffic from 60 cities; your sites without clients show no request numbers. See Your data.
Playground
Change a prop and the component re-renders. Props marked remounts set an initial value, so the component starts over.
<GeoCatchment
metric="distance"
projection="equalEarth"
pitch={4}
unit="req/s"
/>Installation
pnpm dlx shadcn@latest add @lumesec/geo-catchmentFirst timeRegister the @lumesec registry once, or install by URL+
Adds @lumesec to your components.json:
pnpm dlx shadcn@latest registry add @lumesec=https://elements.lumesec.ai/r/{name}.jsonOr skip the setup and install by URL:
npx shadcn@latest add https://elements.lumesec.ai/r/geo-catchment.jsonUsage
import { GeoCatchment, type CatchmentClient, type CatchmentSite } from "@/components/lumesec/geo-catchment";
const sites: CatchmentSite[] = [
{ id: "ams", label: "Amsterdam", lat: 52.37, lon: 4.9 },
{ id: "iad", label: "Ashburn", lat: 39.04, lon: -77.49 },
{ id: "sin", label: "Singapore", lat: 1.35, lon: 103.82 },
];
const clients: CatchmentClient[] = [
{ lat: 48.86, lon: 2.35, weight: 1200 },
{ lat: 40.71, lon: -74.01, weight: 2100 },
{ lat: 35.68, lon: 139.65, weight: 900 },
];
export function Example() {
return <GeoCatchment sites={sites} clients={clients} defaultValue="ams" onActiveChange={(ids) => console.log(ids)} />;
}Behaviour
Which point of presence serves which part of the world. Every land dot belongs to its nearest active site, the borders between catchments are drawn brighter, and the selected site's area is lit in the accent colour. Point at the map to see which site serves that spot and the estimated round trip.
Turn a site off to simulate an outage: its area drops out in a wave from the site, then the neighbouring sites flood the hole, each front starting at its own side. Turn it back on and it reclaims its area from its centre. Request shares roll to the new numbers, and a dotted strip shows every site's share of the traffic.
API reference
Props
Also accepts every prop of <div> (React.ComponentProps<"div">), spread onto the root element.
sitesTypereadonly CatchmentSite[]DefaultCATCHMENT_DEMO_SITESThe points of presence: { id, label, lat, lon, stretch? }. stretch is how much longer the network path is than the great circle (default 1.5); it scales the round-trip estimates and decides the owner with metric="latency". Sites without a finite position and repeated ids are skipped. Default: ten demo sites. An empty array shows the plain map and 'No sites'.
activeTypereadonly string[]Ids of the sites in service (controlled). Use with onActiveChange.
defaultActiveTypereadonly string[]Defaultall sitesSites in service when uncontrolled.
onActiveChangeType(active: readonly string[]) => voidCalled with the new list of ids, in sites order, when a switch turns a site on or off.
valueTypestring | nullThe selected site id (controlled), or null for none. Use with onValueChange.
defaultValueTypestring | nullDefaultfirst siteThe selected site when uncontrolled.
onValueChangeType(value: string | null) => voidCalled when a marker or a list row selects a site, and with null when the selected site is pressed again.
clientsTypereadonly CatchmentClient[]Defaultdemo trafficRequest sources { lat, lon, weight }, assigned to sites like the map. weight is the request rate in unit; entries with a weight of 0 or less are skipped. Default: the 60 demo cities, each sending its demo weight × 1,000, but only while sites is also undefined. With your sites and no clients, the share, request and round-trip values show '–'.
metricType"distance" | "latency"Default"distance"distance: the nearest site in service serves a place. latency: the site with the lowest estimated round trip (great-circle distance × the site's stretch) serves it.
unitTypestringDefault"req/s"Unit of the request numbers, shown in the list header and the readout.
projectionType"equalEarth" | "equirectangular"Default"equalEarth"Map projection. Both crop the map to 56° S to 76° N; the map keeps the projection's aspect ratio.
pitchTypenumberDefault4Spacing of the land dots in CSS px, clamped to 2 to 12.
Ref
ref points at the root HTMLDivElement.
Keyboard
| Keys | Action |
|---|---|
| Tab | Moves to the map's site markers (one tab stop, on the selected site), then through the list's switches. |
| ArrowLeft / ArrowRight / ArrowUp / ArrowDown | On a marker: moves focus to the nearest marker in that direction on the map. |
| Home / End | On a marker: moves focus to the first or last site. |
| Enter / Space | On a marker: selects the site, or clears the selection when it is already selected. On a switch: turns the site on or off. |
Accessibility
- Site markers are native buttons in a group labelled "Sites on the map", with one roving tab stop.
aria-pressedmarks the selected site; each label gives the site and its share, for example "Frankfurt, serves 17%", or "Frankfurt, off". - Each list row has a native
buttonwithrole="switch"andaria-checked, labelled "<site> in service". - The list is the accessible data: each row reads the site, its share of requests and its request rate with the unit. Rolling numbers hide their outgoing text from assistive technology.
- Service changes are announced in a polite live region, naming the site that took over the most traffic: "Frankfurt off. London now serves 31%." With no site left: "Frankfurt off. No site in service."
- The canvases and the share strip are
aria-hidden. The probe (pointing at the map) is pointer only; a click on a list row also selects its site. - Reduced motion: there is no intro, catchments reassign at once with no outage wave, flood front or flash, a new selection appears without its ring of light, the probe line appears without drawing on, markers do not pop and no sparks fly, and the shares, the share strip and the switches change without rolling or sliding.
Theming
Styled with Tailwind classes on your shadcn theme tokens, so light and dark follow your theme. The accent comes from --lumesec. See Theming.
This component reads --lumesec, --lumesec-glint, --lumesec-shine, --destructive, --foreground, --muted-foreground, --muted, --card and --border.
Notes
- Each land dot belongs to the site in service with the smallest great-circle distance (or, with
metric="latency", the smallest distance × stretch). The assignment is recomputed only when the sites, the service list or the metric change. The land comes from an embedded world dataset; there are no map tiles. - The selected site's catchment is drawn in the accent colour and outlined with brighter dots. Other catchments are grey, and neighbours differ in density only, so borders read without a colour per site. A site that is off has a destructive marker with a static checker halo.
- Turning a site off drops its dots out in a wave from the site, with a short destructive flash, and leaves a hole. The neighbouring sites then flood it from their own side at one speed, each dot flashing as its new site takes it. Turning a site on lets it reclaim its catchment from its centre. A toggle during a transition carries on from what the map shows at that moment.
- On first view every site grows its catchment from its own position at once, and the fronts meet at the borders. Selecting a site spreads the accent through its catchment behind a ring of light; the old selection dissolves dot by dot.
- Pointing at the map (or tapping it) draws a dotted line to the site that serves that spot, with a chip such as "Frankfurt · ~24 ms". The round trip is an estimate: twice the great-circle distance over 204 km per ms (light in fibre), times the site's stretch. It is not a measurement.
- Shares and request rates count the
clientseach site serves; the readout under the map adds the request-weighted mean of the estimated round trips. The dotted strip below it shows every site's share in list order and grows or collapses with the shares. - Hit areas of close markers overlap, so a pointer click selects the marker nearest to the pointer. Labels sit on the side of their marker that clears the other markers.
- From a container width of 560 px the list is a 224 px column beside the map; below that it moves under the map with taller rows for touch. The root is at most 660 px wide.
- The animation runs only during transitions, the probe and sparks, and pauses while the map is off screen. The built-in text is English ("Sites", "Share", "Requests", "Round trip", "In service" and the announcements).
Related
Was this page helpful?