# Event spikes
> Counts per location as dot spikes on a globe; new data grows them in a wave from the biggest increase, led by a ring of light.
- React: `import { GeoSpikes } from "@/components/lumesec/geo-spikes"`
- 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-spikes.json
- Page: https://elements.lumesec.ai/components/location/spikes



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

Demo source:

```tsx
"use client";

import * as React from "react";

import { GeoSpikes, type SpikeDatum } from "@/components/lumesec/geo-spikes";
import { createRandom, findCity } from "@/lib/lumesec/geo-data";

/** The 24 demo cities, spread over every continent. */
const CITY_IDS = "lhr cdg fra ams mad ist jfk ord sfo lax yyz mex gru bog eze los cai jnb dxb bom sin nrt icn syd".split(" ");

/**
 * Invented counts for one refresh. Round 0 is the baseline; every later round drifts each city a little and makes
 * three cities jump, so the wave starts from the biggest increase.
 */
function demoCounts(round: number): SpikeDatum[] {
  const base = createRandom("geo-spikes");
  const drift = createRandom(round * 7919 + 13);
  const jumps = new Set<number>();
  while (round > 0 && jumps.size < 3) jumps.add(Math.floor(drift() * CITY_IDS.length));
  const result: SpikeDatum[] = [];
  CITY_IDS.forEach((id, index) => {
    const city = findCity(id);
    if (!city) return;
    let value = 42000 * Math.pow(city.weight, 2.2) * (0.7 + 0.6 * base());
    if (round > 0) value *= jumps.has(index) ? 2.2 + drift() * 2.6 : 0.82 + drift() * 0.3;
    result.push({ id, label: city.name, lat: city.lat, lon: city.lon, value: Math.round(value) });
  });
  return result;
}

export default function GeoSpikesDemo() {
  const [round, setRound] = React.useState(0);
  const data = React.useMemo(() => demoCounts(round), [round]);
  return (
    <div className="relative w-full max-w-[400px]">
      <GeoSpikes data={data} />
      <button
        type="button"
        onClick={() => setRound((value) => value + 1)}
        className="absolute top-3 right-3 z-[4] h-[30px] cursor-pointer rounded-lg border border-border bg-card px-[11px] font-[inherit] text-[12.5px] font-medium text-foreground hover:border-[color-mix(in_srgb,var(--lumesec)_50%,var(--border))]"
      >
        Refresh
      </button>
    </div>
  );
}
```

> **Your data:** Pass your counts per location as `data`. Without it the component shows demo counts for 24 cities. 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-spikes
```

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-spikes.json
```

## Usage

React:

```tsx
import { GeoSpikes, type SpikeDatum } from "@/components/lumesec/geo-spikes";

const signIns: SpikeDatum[] = [
  { id: "lon", label: "London", lat: 51.5072, lon: -0.1276, value: 18400 },
  { id: "nyc", label: "New York", lat: 40.7128, lon: -74.006, value: 15200 },
  { id: "tyo", label: "Tokyo", lat: 35.6762, lon: 139.6503, value: 9800 },
  { id: "sao", label: "São Paulo", lat: -23.5505, lon: -46.6333, value: 4100 },
];

export function Example() {
  return <GeoSpikes data={signIns} unit="sign-ins" label="Sign-ins by city" />;
}
```

## Behaviour

Counts per location as spikes of stacked dots standing on a dotted globe. Heights use a log scale by default, so one busy city does not flatten the rest.

When the data changes, spikes grow on springs in a wave that starts at the location with the biggest increase and spreads around the globe, with a ring of light on the land marking the front. Point at a spike, or Tab to it, to roll its count.

## API reference

### Props

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

| Prop            | Type                              | Default                | Description                                                                                                                                                                                                                                                       |
| --------------- | --------------------------------- | ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `data`          | `readonly SpikeDatum[]`           | `24 demo cities`       | Counts per location, `{ id, label, lat, lon, value }`. A new array is matched to the previous one by `id`: spikes grow or shrink from their previous height, new ids grow from zero and missing ids shrink away. Up to 64 locations are drawn, the largest first. |
| `value`         | `string \| null`                  | —                      | Selected spike id (controlled), or null for none. Use with `onValueChange`.                                                                                                                                                                                       |
| `defaultValue`  | `string \| null`                  | `null`                 | Selected spike when uncontrolled.                                                                                                                                                                                                                                 |
| `onValueChange` | `(value: string \| null) => void` | —                      | Called when a spike is selected or the selection is cleared.                                                                                                                                                                                                      |
| `scale`         | `"log" \| "linear"`               | `"log"`                | How values map to heights. `log` spreads the three decades below the largest value evenly over the height, so one busy place does not flatten the rest. `linear` makes height proportional to the value.                                                          |
| `maxHeight`     | `number`                          | `0.5`                  | Height of the tallest spike as a share of the globe radius, 0.1 to 0.6.                                                                                                                                                                                           |
| `format`        | `(value: number) => string`       | `formatCompact`        | Formats counts in the chip, the legend and the accessible names. The default gives `950`, `12.4K`, `3.1M`.                                                                                                                                                        |
| `unit`          | `string`                          | `"events"`             | Unit after each count.                                                                                                                                                                                                                                            |
| `defaultCenter` | `LatLon`                          | —                      | Where the globe faces on mount, `{ lat, lon }`. Without it the globe faces the value-weighted centre of the data, tilted 30° south so the spikes stand up instead of pointing at the viewer.                                                                      |
| `label`         | `string`                          | `"Events by location"` | Accessible name of the globe and of the location list.                                                                                                                                                                                                            |

### Ref

`ref` points at the root `HTMLDivElement`.

## Keyboard

| Keys                       | Action                                                             |
| -------------------------- | ------------------------------------------------------------------ |
| Tab                        | Moves from the globe to the spikes, which share one tab stop.      |
| Arrow keys (globe focused) | Turn the globe 10°; the view moves in the arrow's direction.       |
| Arrow keys (spike focused) | Move to the nearest visible spike in that direction on screen.     |
| Home / End                 | Move to the largest or the smallest spike.                         |
| Enter / Space              | Select the focused spike, or clear it when it is already selected. |
| Escape                     | Clear the selection.                                               |

## Accessibility

* The globe is a focusable `role="group"` with `aria-roledescription="globe"`, named by `label` and described by a visually hidden hint.
* Every spike is a native button with the label and the count as its name and `aria-pressed` for the selection. The buttons follow their tips and share one roving tab stop; focusing a spike on the far side turns the globe to it.
* A visually hidden list mirrors every location and value. With more than 64 locations it says that the globe shows the 64 largest.
* The canvas, the chip and the legend are `aria-hidden`; the buttons and the list carry the same content.
* Reduced motion: the spikes take their heights at once with no wave, ring, flicker or sparks, the globe neither coasts nor flies toward an update (focusing a far-side spike turns it at once), and counts and the chip swap without rolling or gliding.

## 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 `--card`, `--card-foreground`, `--foreground`, `--muted-foreground`, `--border`, `--popover`, `--popover-foreground`, `--lumesec`, `--lumesec-soft` and `--lumesec-shine`.

## Notes

* On first view every spike grows from zero in a wave from the largest value. After that, each new `data` array starts a wave at the location with the biggest increase (or the largest value when nothing grew): a ring of light runs across the land, each spike starts growing as the ring reaches it, and the counts in the chip and the legend roll at the same moment.
* When the epicentre is on the far side, nothing is selected and the globe has not been dragged for 4 seconds, the globe first turns toward it and the wave starts as it arrives.
* Places whose count rose by half or more throw a few sparks from the tip at the top of the overshoot. The sparks are drawn on a layer that reaches 32 px past the globe.
* Zero or negative values draw only the three-dot footprint. Entries with a missing id, a repeated id or a non-finite number are skipped.
* The legend shows the three largest locations. Pointing at a legend entry lights its spike. The chip sits beyond the tip, on the side the spike leans to, so it never covers the column.
* The globe stays still between updates: the animation loop stops once the springs, the ring and the globe have settled, and pauses off screen.


