# Marker map
> A flat map of land dots with markers. Values light halos of dots sized by area, which spring to new values; labels place themselves.
- React: `import { GeoMarkerMap } from "@/components/lumesec/geo-marker-map"`
- Collection: Location (https://elements.lumesec.ai/components/location)
- Data: takes your data (`data`); shows demo data until you pass it
- Registry item: https://elements.lumesec.ai/r/geo-marker-map.json
- Page: https://elements.lumesec.ai/components/location/marker-map



Live preview: https://elements.lumesec.ai/view/geo-marker-map

Demo source:

```tsx
"use client";

import * as React from "react";

import { GeoMarkerMap, type GeoMarkerMapGrid, type MapMarker } from "@/components/lumesec/geo-marker-map";
import { DEMO_CITIES, formatCompact, formatNumber } from "@/lib/lumesec/geo-data";
import { RollText } from "@/lib/lumesec/roll-text";
import { cn } from "@/lib/utils";

/** Invented requests per minute at 14 edge locations; São Paulo is degraded. */
const START: readonly (readonly [id: string, value: number, extra?: Partial<MapMarker>])[] = [
  ["lhr", 48200, { pulse: true }],
  ["jfk", 52600],
  ["nrt", 41200, { pulse: true }],
  ["sfo", 36400],
  ["sin", 33900],
  ["fra", 31800],
  ["bom", 27300],
  ["gru", 21400, { tone: "warning", pulse: true }],
  ["ord", 18900],
  ["dxb", 14800],
  ["syd", 12600],
  ["mex", 9800],
  ["los", 7600],
  ["jnb", 6400],
];

const INITIAL: MapMarker[] = START.flatMap(([id, value, extra]) => {
  const city = DEMO_CITIES.find((entry) => entry.id === id);
  return city ? [{ id, label: city.name, lat: city.lat, lon: city.lon, value, ...extra }] : [];
});

const BUTTON =
  "h-[30px] cursor-pointer rounded-lg border border-border bg-card px-[11px] font-[inherit] text-[12.5px] font-medium text-foreground outline-none hover:border-[color-mix(in_srgb,var(--lumesec)_50%,var(--border))] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-solid focus-visible:outline-lumesec";

export default function GeoMarkerMapDemo() {
  const [markers, setMarkers] = React.useState<MapMarker[]>(INITIAL);
  const [grid, setGrid] = React.useState<GeoMarkerMapGrid>("honeycomb");
  const [selected, setSelected] = React.useState<string | null>(null);

  const total = markers.reduce((sum, marker) => sum + (marker.value ?? 0), 0);
  const current = markers.find((marker) => marker.id === selected) ?? null;
  const shown = current ? (current.value ?? 0) : total;

  // derived state: which way the readout rolls
  const [readout, setReadout] = React.useState({ value: shown, direction: 1 });
  if (readout.value !== shown) setReadout({ value: shown, direction: shown >= readout.value ? 1 : -1 });

  function shuffle() {
    setMarkers((list) =>
      list.map((marker) => {
        const factor = 0.3 + Math.random() * 1.6;
        return { ...marker, value: Math.round(clampValue((marker.value ?? 0) * factor) / 100) * 100 };
      }),
    );
  }

  return (
    <div className="w-full max-w-[660px] rounded-[14px] border border-border bg-card px-5 pt-4 pb-[18px] text-foreground shadow-[0_14px_34px_-20px_rgb(0_0_0/0.4)]">
      <div className="mb-3 flex items-baseline justify-between gap-3">
        <div className="min-w-0">
          <div className="text-[13px] leading-5 font-medium">Edge requests</div>
          <div className="text-[12px] leading-4 text-muted-foreground">Per minute, by location</div>
        </div>
        <div className="flex items-baseline gap-2 text-right" aria-live="polite">
          <span className="truncate text-[12px] text-muted-foreground">{current ? current.label : "All locations"}</span>
          <RollText
            text={formatNumber(shown)}
            direction={readout.direction}
            className="font-mono text-[18px] leading-6 font-medium tabular-nums"
          />
        </div>
      </div>
      <GeoMarkerMap markers={markers} grid={grid} value={selected} onValueChange={setSelected} format={formatCompact} label="Requests per minute by edge location" />
      <div className="mt-3 flex flex-wrap items-center justify-between gap-2">
        <button type="button" onClick={shuffle} className={BUTTON}>
          Shuffle values
        </button>
        <div role="group" aria-label="Grid" className="flex gap-1">
          {(["honeycomb", "square"] as const).map((option) => (
            <button
              key={option}
              type="button"
              aria-pressed={grid === option}
              onClick={() => setGrid(option)}
              className={cn(
                BUTTON,
                "aria-pressed:border-[color-mix(in_srgb,var(--lumesec)_55%,var(--border))] aria-pressed:bg-lumesec/8 aria-[pressed=false]:text-muted-foreground",
              )}
            >
              {option === "honeycomb" ? "Honeycomb" : "Square"}
            </button>
          ))}
        </div>
      </div>
    </div>
  );
}

/** Keeps shuffled values in a believable range. */
function clampValue(value: number) {
  return Math.min(90000, Math.max(1200, value));
}
```

> **Your data:** Pass your places as `markers`. Without it the map shows 14 demo cities with invented request counts. See [Your data](/docs/data).

## Playground

Change a prop and the component re-renders. Props marked remounts set an initial value, so the component starts over.

## Installation

```bash
npx shadcn@latest add @lumesec/geo-marker-map
```

First time with the @lumesec registry? Register it once, or install by URL:

```bash
npx shadcn@latest registry add @lumesec=https://elements.lumesec.ai/r/{name}.json
npx shadcn@latest add https://elements.lumesec.ai/r/geo-marker-map.json
```

## Usage

React:

```tsx
import { GeoMarkerMap, type MapMarker } from "@/components/lumesec/geo-marker-map";

const markers: MapMarker[] = [
  { id: "lhr", label: "London", lat: 51.507, lon: -0.128, value: 4200, pulse: true },
  { id: "jfk", label: "New York", lat: 40.713, lon: -74.006, value: 5100 },
  { id: "nrt", label: "Tokyo", lat: 35.676, lon: 139.65, value: 3100 },
  { id: "gru", label: "São Paulo", lat: -23.551, lon: -46.633, value: 1800, tone: "warning" },
];

export function Example() {
  return <GeoMarkerMap markers={markers} label="Requests by city" className="max-w-[660px]" />;
}
```

## Behaviour

A flat map of land dots with markers. Markers with a value grow a halo of lit dots sized by that value, so the busiest places read at a glance without circles drawn over the map.

Halos grow and shrink behind an uneven front when values change, markers can pulse as rings of light, and labels place themselves so they do not overlap.

## API reference

### Props

Also accepts every prop of `<div>` (`React.ComponentProps<"div">`), spread onto the root element.

| Prop            | Type                                              | Default          | Description                                                                                                                                                                                                                                                                                                                                                              |
| --------------- | ------------------------------------------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `markers`       | `readonly MapMarker[]`                            | `14 demo cities` | The places: `{ id, label?, lat, lon, value?, tone?, size?, pulse? }`. `tone` is `accent`, `info`, `success`, `warning`, `destructive` or `muted`; `size` is `sm`, `md` or `lg` (a 6, 8 or 10 px dot); `pulse` sends a ring of light through the land dots every 2.4 s. Halos spring to new values when it changes. An empty array shows the plain land with `emptyText`. |
| `value`         | `string \| null`                                  | —                | The selected marker id (controlled), or `null` for none. Use with `onValueChange`.                                                                                                                                                                                                                                                                                       |
| `defaultValue`  | `string \| null`                                  | `null`           | The marker selected at first when uncontrolled.                                                                                                                                                                                                                                                                                                                          |
| `onValueChange` | `(value: string \| null) => void`                 | —                | Called when the user selects a marker or clears the selection (Escape, a second press on the selected marker, a click on the map).                                                                                                                                                                                                                                       |
| `projection`    | `"equalEarth" \| "equirectangular" \| "mercator"` | `"equalEarth"`   | Map projection. Equal Earth keeps areas true, so halos compare fairly at every latitude.                                                                                                                                                                                                                                                                                 |
| `grid`          | `"honeycomb" \| "square"`                         | `"honeycomb"`    | Dot arrangement of the land.                                                                                                                                                                                                                                                                                                                                             |
| `pitch`         | `number`                                          | `4`              | Dot spacing in px, clamped to 3 to 8.                                                                                                                                                                                                                                                                                                                                    |
| `latRange`      | `readonly [number, number]`                       | `[-58, 84]`      | Latitude crop as `[south, north]` in degrees. Markers outside it are skipped.                                                                                                                                                                                                                                                                                            |
| `maxHalo`       | `number`                                          | `28`             | Halo radius in px for the largest value on a map 660 px wide. The radius scales with the map width, from 60% to 150%.                                                                                                                                                                                                                                                    |
| `labels`        | `"all" \| "selected" \| "none"`                   | `"all"`          | Which label chips show: the largest values that have room, only the selected one, or none. Hovered and focused markers always show theirs. Without the prop, maps narrower than 480 px use `selected`.                                                                                                                                                                   |
| `reveal`        | `"view" \| "mount" \| "none"`                     | `"view"`         | When the land prints itself: the first time a third of the map is on screen, right after mount, or never (drawn at once).                                                                                                                                                                                                                                                |
| `revealFrom`    | `string \| LatLon`                                | `first marker`   | Marker id or `{ lat, lon }` point the reveal grows from.                                                                                                                                                                                                                                                                                                                 |
| `format`        | `(value: number) => string`                       | `formatCompact`  | Formats values for the label chips and the markers' accessible names. The default gives `48.2K`.                                                                                                                                                                                                                                                                         |
| `label`         | `string`                                          | `"Map"`          | Accessible name of the map.                                                                                                                                                                                                                                                                                                                                              |
| `emptyText`     | `string`                                          | `"No locations"` | Text shown when `markers` is an empty array.                                                                                                                                                                                                                                                                                                                             |

### Ref

`ref` points at the root `HTMLDivElement`.

## Keyboard

| Keys                                         | Action                                                                                                                |
| -------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- |
| Tab                                          | Moves focus into the map. One marker holds the tab stop: the last one focused, else the selected one, else the first. |
| ArrowLeft / ArrowRight / ArrowUp / ArrowDown | Move focus to the nearest marker in that direction on the map.                                                        |
| Home / End                                   | Move focus to the first / last marker in `markers` order.                                                             |
| Enter / Space                                | Select the focused marker, or clear the selection when it is already selected.                                        |
| Escape                                       | Clear the selection.                                                                                                  |

## Accessibility

* The root has `role="group"`, `aria-roledescription="map"` and the `label` as its `aria-label`.
* Each marker is a native `<button>` with `aria-pressed` and an `aria-label` of its label (or id) and formatted value, for example "London, 48.2K".
* The markers share one roving tab stop, so the map is a single Tab stop however many markers it has.
* A marker focused from the keyboard shows its label chip and a 2 px accent focus ring.
* The canvases and the label chips are `aria-hidden`; the chips repeat what the buttons already say.
* Reduced motion: the land is drawn at once, halos appear at their size without the growth front, pulsing markers show a still ring of brighter dots, a selection changes without the ring, pop and sparks, and chip values change without rolling.

## Theming

Styled with Tailwind classes on your shadcn theme tokens, so light and dark follow your theme. The accent comes from `--lumesec`. See [Theming](/docs/theming).

This component reads `--lumesec`, `--lumesec-glint`, `--lumesec-shine`, `--lumesec-info`, `--lumesec-success`, `--lumesec-warning`, `--destructive`, `--foreground`, `--muted-foreground`, `--muted`, `--card` and `--border`.

## Notes

* Halo area is proportional to the value: the radius is `maxHalo · √(value / largest value)`, scaled with the map width. Zero, negative and missing values have no halo.
* Halos light land dots only, so a coastal marker lights the land around it. Some coastal cities fall on water cells of the 0.5° land mask and show a partial halo.
* Where halos overlap, each dot takes the brighter halo; nothing stacks.
* With `labels="all"`, the largest values get chips, at most one per 120 px of map width. A chip that would cover another marker stays hidden.
* Changing `projection`, `grid`, `pitch` or `latRange` after the first reveal prints the new lattice again from the selected marker, or from `revealFrom`.
* A click on the map outside the markers clears the selection.
* The root fills its container's width and keeps the projection's aspect ratio, so the server markup reserves the height and already places the markers.
* Marker ids must be unique; a repeated id keeps its first marker.
* Pulsing markers keep the animation running while the map is on screen. Without them it stops once everything has settled.


