# Legend

> Package `@umriss-ui/charts`, version 0.40.0. Demo page: <https://romanhaendler.github.io/umriss-ui/charts/legend/>. Every page in one line: [llms.txt](https://romanhaendler.github.io/umriss-ui/charts/llms.txt); every page in full: [llms-full.txt](https://romanhaendler.github.io/umriss-ui/charts/llms-full.txt).

What the colours mean (a key), and a switch for every series: a click hides it and shows it again, a double click shows only it (a solo).

```ts
import { Legend, useChart } from "@umriss-ui/charts";
```

Demo page: https://romanhaendler.github.io/umriss-ui/charts/legend/

The legend toggles by default: every entry of a named series is a button that says whether its series is shown. The hidden series are part of the chart's view, by `name` - no state of yours, and kept like the rest of the view, on [View](https://romanhaendler.github.io/umriss-ui/charts/view/). A series without a name cannot be hidden; its entry only explains.

A double click, Alt/⌥+click or Shift+Enter on an entry shows only its series; on the only one visible, all again. A click acts at once rather than wait for a second, so a double click first toggles the series and back - its result is the same either way.

A hidden series leaves the drawing, the y extent and the keys' walk; its entry stays, struck through, and keeps its colour, so showing it again moves nothing else. Nothing hides every series: what would shows all instead. A state band's entries are its states, and any of them hides or shows only the whole band.

On the hook, `hidden`, `toggleSeries(name)`, `showOnly(name)` and `showAllSeries()` do the same from your own controls.

## Examples

### Hide a series from the legend

A click on an entry hides its series and shows it again, through the chart's view - no state of yours. A double click, Alt/⌥+click or Shift+Enter shows only it, and on the only one visible all again; `showOnly` and `showAllSeries` on the hook do the same from your own buttons.

```tsx
import { Legend, Tooltip, useChart } from "@umriss-ui/charts";

/* Data from the operations world, written out here so the example runs on its own. */
/** A small LCG - the same numbers on every computer. */
function random(seed: number): () => number {
  let s = seed >>> 0;
  return () => {
    s = (Math.imul(s, 1664525) + 1013904223) >>> 0;
    return s / 4294967296;
  };
}

const at = (day: number, hours: number, minutes = 0) => new Date(2026, 2, day, hours, minutes).getTime();
const MINUTE = 60_000;

/** Tuesday, 17 March 2026, 10:30 - the moment the screens are read at. */
const NOW = at(17, 10, 30);

interface Service {
  id: string;
  name: string;
  team: string;
  /** 1 is customer-facing and pages at night; 3 waits for the morning. */
  tier: 1 | 2 | 3;
  /** The latency objective: the 95th percentile stays below this, in ms. */
  latencySlo: number;
  /** The availability promised for a month, in per cent. */
  availabilityTarget: number;
}

const SERVICES: readonly Service[] = [
  { id: "checkout", name: "Checkout", team: "Payments", tier: 1, latencySlo: 300, availabilityTarget: 99.95 },
  { id: "billing", name: "Billing", team: "Payments", tier: 1, latencySlo: 400, availabilityTarget: 99.9 },
  { id: "sign-in", name: "Sign-in", team: "Identity", tier: 1, latencySlo: 200, availabilityTarget: 99.95 },
  { id: "search", name: "Search", team: "Discovery", tier: 1, latencySlo: 250, availabilityTarget: 99.9 },
  { id: "images", name: "Image service", team: "Discovery", tier: 2, latencySlo: 500, availabilityTarget: 99.5 },
  { id: "notifications", name: "Notifications", team: "Messaging", tier: 2, latencySlo: 800, availabilityTarget: 99.5 },
  { id: "webhooks", name: "Webhooks", team: "Integrations", tier: 2, latencySlo: 1000, availabilityTarget: 99.5 },
  { id: "reports", name: "Reporting", team: "Insights", tier: 3, latencySlo: 2000, availabilityTarget: 99 },
];

interface MetricPoint {
  t: number;
  /** Median and 95th percentile latency, in ms. */
  p50: number;
  p95: number;
  /** Failed requests, in per cent. */
  errorRate: number;
  /** Requests per minute. */
  requests: number;
}

/** Requests per minute at the busiest hour, by tier. */
const VOLUME = { 1: 900, 2: 400, 3: 120 } as const;

/** A service's latency and error rate every five minutes of today, from
    midnight up to now. Busy by day, quiet at night; Checkout's 95th percentile
    climbs well past its objective from 09:40 to 10:10, and its errors with it. */
function metrics(serviceId: string): MetricPoint[] {
  const index = SERVICES.findIndex((one) => one.id === serviceId);
  const service = SERVICES[index];
  if (service === undefined) throw new Error(`No service "${serviceId}".`);
  const r = random(1000 + index * 37);
  const points: MetricPoint[] = [];
  let drift = 0;
  for (let t = at(17, 0); t <= NOW; t += 5 * MINUTE) {
    const hour = (t - at(17, 0)) / (60 * MINUTE);
    const load = 0.35 + 0.65 * Math.max(0, Math.sin((Math.PI * (hour - 5)) / 16));
    drift = 0.8 * drift + (r() - 0.5) * 0.08;
    const base = service.latencySlo * (0.45 + 0.2 * load + drift);
    const surge = serviceId === "checkout" && t >= at(17, 9, 40) && t < at(17, 10, 10) ? 1.9 : 1;
    const p95 = Math.round(base * surge * (1 + r() * 0.08));
    points.push({
      t,
      p50: Math.round(p95 * (0.42 + r() * 0.06)),
      p95,
      errorRate: Math.round((0.05 + r() * 0.12 + (surge > 1 ? 2.4 + r() : 0)) * 100) / 100,
      requests: Math.round(VOLUME[service.tier] * load * (1 + (r() - 0.5) * 0.1)),
    });
  }
  return points;
}

const SERIES = [
  { name: "Checkout", data: metrics("checkout") },
  { name: "Billing", data: metrics("billing") },
  { name: "Reporting", data: metrics("reports") },
];

export default function HideASeries() {
  /* Reporting's slow jobs would flatten the other two. */
  const { Chart, XAxis, YAxis, Line, showOnly, showAllSeries } = useChart(SERIES[0]!.data, {
    initialView: { hidden: ["Reporting"] },
  });
  return (
    <>
      <div className="demo-actions">
        <button type="button" className="demo-button" onClick={() => showOnly("Checkout")}>
          Only Checkout
        </button>
        <button type="button" className="demo-button" onClick={showAllSeries}>
          Show all
        </button>
      </div>
      <Chart height={260} ariaLabel="Latency of three services">
        <XAxis value="t" time />
        <YAxis label="ms" />
        {SERIES.map((one) => (
          <Line key={one.name} data={one.data} value="p95" name={one.name} />
        ))}
        <Legend />
        <Tooltip mode="x" />
      </Chart>
    </>
  );
}
```

### Legend above or below

A `Legend` stands above the plot by default, `placement="bottom"` puts it below; hovering an entry lifts its series.

```tsx
import { Legend, useChart } from "@umriss-ui/charts";

/* Data from the controlling world, written out here so the example runs on its own. */
/** A small LCG - the same numbers on every computer. */
function random(seed: number): () => number {
  let s = seed >>> 0;
  return () => {
    s = (Math.imul(s, 1664525) + 1013904223) >>> 0;
    return s / 4294967296;
  };
}

interface CostCentre {
  id: string;
  name: string;
  owner: string;
  /** The budget of one month. */
  monthlyBudget: number;
}

const COST_CENTRES: readonly CostCentre[] = [
  { id: "CC-1100", name: "Sales", owner: "Helen Marsh", monthlyBudget: 142_000 },
  { id: "CC-1200", name: "Marketing", owner: "Rafael Ortiz", monthlyBudget: 68_000 },
  { id: "CC-2100", name: "Engineering", owner: "Anika Sørensen", monthlyBudget: 188_000 },
  { id: "CC-2200", name: "Design", owner: "Paul Whitaker", monthlyBudget: 54_000 },
  { id: "CC-3100", name: "Customer service", owner: "Grace Obi", monthlyBudget: 61_000 },
  { id: "CC-4100", name: "Finance", owner: "Martina Vogel", monthlyBudget: 47_000 },
  { id: "CC-4200", name: "People", owner: "Daniel Frost", monthlyBudget: 39_000 },
  { id: "CC-4300", name: "IT", owner: "Kenji Arai", monthlyBudget: 83_000 },
  { id: "CC-4400", name: "Facilities", owner: "Olga Ivanova", monthlyBudget: 72_000 },
];

/** The year's months, as `"2026-01"` … `"2026-12"`. */
const MONTHS: readonly string[] = Array.from({ length: 12 }, (_, i) => `2026-${String(i + 1).padStart(2, "0")}`);

/** The months whose books are closed: they have an actual. */
const CLOSED_MONTHS = 2;

interface LedgerRow {
  costCentre: string;
  month: string;
  budget: number;
  /** `null` while the month is open. */
  actual: number | null;
  forecast: number;
}

/* Where a cost centre lands against its budget, as a factor: Marketing's
   spring campaign overspends, IT's delayed licences underspend. */
const TENDENCY: Readonly<Record<string, number>> = { "CC-1200": 1.14, "CC-4300": 0.88 };

/** Budget, actual and forecast of every cost centre in every month. */
const LEDGER: readonly LedgerRow[] = COST_CENTRES.flatMap((centre, c) => {
  const r = random(310 + c);
  const tendency = TENDENCY[centre.id] ?? 1;
  return MONTHS.map((month, m) => {
    /* December pays the bonuses, August is the quiet month. */
    const season = m === 11 ? 1.12 : m === 7 ? 0.9 : 1;
    const budget = Math.round(centre.monthlyBudget * season);
    const forecast = Math.round((budget * (tendency + (r() - 0.5) * 0.06)) / 100) * 100;
    const actual = m < CLOSED_MONTHS ? Math.round(forecast * (1 + (r() - 0.5) * 0.08)) : null;
    return { costCentre: centre.id, month, budget, actual, forecast };
  });
});

const MARKETING = LEDGER.filter((row) => row.costCentre === "CC-1200");
const MONTH_NAMES = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
/** `"2026-03"` as the axis writes it. */
const monthName = (month: string) => MONTH_NAMES[Number(month.slice(5)) - 1] ?? month;

function Marketing({ placement }: { placement?: "bottom" }) {
  const { Chart, XAxis, YAxis, Line } = useChart(MARKETING);
  return (
    <Chart height={220} ariaLabel={`Marketing's budget and forecast, legend ${placement ?? "above"}`}>
      <XAxis value="month" categoryLabel={monthName} />
      <YAxis label="€" />
      <Line value="budget" name="Budget" />
      <Line value="forecast" name="Forecast" />
      <Legend placement={placement} />
    </Chart>
  );
}

export default function LegendPlacement() {
  return (
    <div className="pair">
      <div>
        <p className="pair-caption">no placement - above, the default</p>
        <Marketing />
      </div>
      <div>
        <p className="pair-caption">placement="bottom"</p>
        <Marketing placement="bottom" />
      </div>
    </div>
  );
}
```

## Keyboard

| Key | Action |
|---|---|
| `Enter Space` | Hide the entry's series, or show it again. |
| `Shift+Enter` | Show only the entry's series; on the only one visible, all again. |

The keys of [Chart](https://romanhaendler.github.io/umriss-ui/charts/chart/#keyboard-chart) apply here.

## Accessibility

Every entry of a named series is a button whose `aria-pressed` says whether its series is shown, and the plot's summary names Shift+Enter. Where a click, a gesture or a setter would leave nothing visible and shows all instead, the chart's polite live region says so.

## API

### `LegendProps`

| Prop | Type | Default | Description |
|---|---|---|---|
| `placement` | `"top" \| "bottom"` | `"top"` | Which side of the plot area the legend stands on.<br>Shown in: [Legend above or below](https://romanhaendler.github.io/umriss-ui/charts/legend/#above-or-below), [Mix series kinds (Chart)](https://romanhaendler.github.io/umriss-ui/charts/chart/#mixed), [Mark every kind (Chart)](https://romanhaendler.github.io/umriss-ui/charts/chart/#marks-on-every-kind) and 28 more |
