# HeartbeatBlip (/docs/charts/heartbeat-blip)

HeartbeatBlip answers "is it alive, and how busy?" at a glance. Each event in the recent window is an ECG-style spike;
in the interactive entry the trace sweeps left in real time and a new event blips in at the right edge, so the rate you
see **is** the event rate. Every spike is one real event — never a synthesized pulse on a timer — and an empty, flat
baseline is the down signal, carried by shape, not color.

```tsx
<HeartbeatBlip events={[97000, 92000, 85000, 70000, 55000, 48000]} now={100000} title="Requests" width={90} />
```

## Install

```tsx
import { HeartbeatBlip } from "@microcharts/react/heartbeat-blip";

// pass 'now' from your data layer — never Date.now() in a server render
<HeartbeatBlip events={eventTimestamps} now={serverNow} title="Requests" />
```

Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent.


## Motion, and reduced motion

The trace advances in real time so old spikes drift left and new events enter at the right — the liveness comes from
watching the blips arrive. This is the deliberate idle-loop exception, allowed because the loop parameter (elapsed time)
is the datum. It pauses off-screen (a shared observer) and, under `prefers-reduced-motion`, does not sweep at all: the
static strip simply re-renders on each data change — the same information, discretely updated. The one rule this chart
must never break: **a flat service never gets a fake pulse.** Every spike is a real event; when the window empties, the
baseline goes flat and stays flat.

## When to use it

- **Good for** — at-a-glance liveness of a service or stream, request rate in a header, or per-service liveness in a
  status table.
- **Avoid for** — exact event counts (`Seismogram`, `EventTimeline`), a continuous level (`BreathingDot`), or long-term
  trends (`Sparkline`).

## Sizing

`width` and `height` are viewBox units that also set the rendered pixel box — they default to `60 × 16`, sized to sit in
a status table row or a header. Omit them and drive the width from CSS to fill a column; the viewBox keeps the aspect
ratio. `window` sets how much time the strip covers, which changes the density of the trace rather than its box.

## Variants

```tsx
<HeartbeatBlip events={events} now={serverNow} label="count" />
<HeartbeatBlip events={[]} now={serverNow} />
```

```tsx
<HeartbeatBlip
  events={[97000, 92000, 85000]}
  now={100000}
  label="count"
  format={{ useGrouping: false }}
  locale="de-DE"
/>
```

`format`/`locale` only reach the `label="count"` numeral — the accessible summary's count is a plain integer (never run
through the formatter), and the in-chart spikes never carry text, so there's nothing else to localize.

## Edge cases

```tsx
<HeartbeatBlip events={[]} now={100000} title="Requests" />
```

```tsx
<HeartbeatBlip events={[97000, Number.NaN, 92000]} now={100000} title="Requests" />
```

```tsx
// window defaults to 60s; only the in-window events draw a spike
<HeartbeatBlip events={[95000, 10000]} now={100000} title="Requests" />
```


## Why this default

The 60-second window is the default because liveness questions are about the last minute. The `now` clock is a
required-in-spirit prop: the static entry must never call `Date.now()` (a server render and the client hydrate would
disagree and mismatch), so you pass the clock from your data layer. Flatline is never dressed up — down and no-data are
different states, and the summary distinguishes them. The boundary with `BreathingDot`: discrete arriving events are a
HeartbeatBlip; a continuous level is a BreathingDot.

## Accessibility

The accessible name is the count, the window, and the time since the last event — **"3 events in the last minute; last
3s ago."** — or **"No events in the last minute."** when the trace is flat. The trace sweeps only when motion is allowed
and on-screen; the live region announces on data change, and there is no per-spike navigation because the spikes are
transient — the summary is the record.

This chart is a single unit, so there is nothing to rove between: a click, tap, `Enter` or `Space` selects it
and fires `onSelect`, and no selection stays pinned. That is the scalar half of the shared
[interaction contract](/docs/accessibility#one-interaction-contract).

## Props

| Prop | Type | Description |
| --- | --- | --- |
| `events` (required) | `number[]` | Event timestamps (ms). |
| `window` | `number` | The visible recent window in ms (default 60000). |
| `now` | `number` | Explicit clock — defaults to the latest event (SSR-safe). |
| `label` | `"count" \| "none"` | Event-count numeral at the right. |

Plus the shared grammar — `data`, `domain`, `color`, `title`, `summary`, `format` — and the layout props (`width`, `height`, `className`, `style`) that every chart accepts. Interactive entries also share `animate` and `live`, and — wherever a chart has more than one navigable unit — `onActive`, `onSelect`, `selectedIndex` and `defaultSelectedIndex`; and — wherever the chart shows a hover value — `readout`. See [the shared grammar](/docs/quickstart#the-shared-grammar).
