# Insight cards

An insight card is one thing a year turned out to be: the number, the chart that shows it, and the line that says what it means. `InsightCard` is the card as a React component, and `renderInsightCard` is the same card as a PNG. Make one per finding, so a viewer has more than one picture worth posting.

> Live preview: try it at https://bettertyped.github.io/react-wrapped/docs/graphics/insight-cards/
>
> The component on the page, scaled to fit. The button saves it as a PNG.

## Installation

```bash
pnpm add @react-wrapped/graphics @react-wrapped/core gsap
```

```tsx
import "@react-wrapped/core/styles.css";
import "@react-wrapped/graphics/styles.css";
```

## Usage

`InsightCard` always draws at 1200 by 630. To show it on a page, scale it into a box of that shape. To save it, call `renderInsightCard` with the same spec and palette.

`insight-card.tsx`

```tsx
import { type CSSProperties, useLayoutEffect, useRef, useState } from "react";

import { resolvePalette, type WrappedConfig } from "@react-wrapped/core";
import { InsightCard, type InsightCardSpec, renderInsightCard } from "@react-wrapped/graphics";

// The card is always drawn 1200 by 630. On a page it is scaled down to the room it has.
const BOX: CSSProperties = { position: "relative", aspectRatio: "1200 / 630", overflow: "hidden", borderRadius: 14 };

export function InsightCardOnThePage({
  spec,
  config,
  fileName,
}: {
  spec: InsightCardSpec;
  config: WrappedConfig;
  fileName: string;
}) {
  const box = useRef<HTMLDivElement>(null);
  const [scale, setScale] = useState(0.5);
  const [busy, setBusy] = useState(false);
  const palette = resolvePalette(config);

  useLayoutEffect(() => {
    const element = box.current;
    if (!element) return;
    const fit = () => setScale(element.clientWidth / 1200);
    fit();
    const observer = new ResizeObserver(fit);
    observer.observe(element);
    return () => observer.disconnect();
  }, []);

  const download = async () => {
    setBusy(true);
    try {
      // The same card, captured off screen at twice its size.
      const png = await renderInsightCard(spec, palette);
      const url = URL.createObjectURL(png);
      const link = document.createElement("a");
      link.href = url;
      link.download = fileName;
      link.click();
      setTimeout(() => URL.revokeObjectURL(url), 1000);
    } finally {
      setBusy(false);
    }
  };

  return (
    <div>
      <div ref={box} style={BOX}>
        <div style={{ transform: `scale(${scale})`, transformOrigin: "0 0" }}>
          <InsightCard spec={spec} palette={palette} />
        </div>
      </div>
      <div role="group" aria-label="Insight card">
        <button type="button" disabled={busy} onClick={() => void download()}>
          {busy ? "Drawing the PNG" : "Download the PNG"}
        </button>
      </div>
    </div>
  );
}
```

## The spec

| Key | Type | What it is |
| --- | --- | --- |
| `handle` | `string` | Written as @handle above the chart. |
| `caption` | `string` | The line after the handle: what this Wrapped is called and the period. |
| `name` | `string` | What the card is called: an achievement's name, or the part of the year it shows. |
| `rarity?` | `"common" \| "rare" \| "epic" \| "legendary"` | For an achievement. Its word goes under the name and its colour lights the chart's point. |
| `stat` | `{ value: string; label: string }` | The headline number and what it counts, read as one sentence. |
| `insight` | `string` | One line saying what the chart shows. |
| `chart` | `SceneChart` | Any of the [charts](https://bettertyped.github.io/react-wrapped/docs/charts/), as data. Use the one the video plays for the same finding. |
| `brand?` | `string` | A quiet line under the chart: a site address or a product name. |

```ts
import type { InsightCardSpec } from "@react-wrapped/graphics";

export const fridays: InsightCardSpec = {
  handle: "ada",
  caption: "Orbit Wrapped, Oct 2025 to Oct 2026",
  name: "Friday Shipper",
  rarity: "rare",
  stat: { value: "31%", label: "of your work landed on a Friday" },
  insight: "Contributions by weekday. The lit column is Friday.",
  chart: {
    type: "bars",
    bars: [
      { label: "M", value: 312 },
      { label: "T", value: 341 },
      { label: "W", value: 330 },
      { label: "T", value: 352 },
      { label: "F", value: 398, peak: true },
      { label: "S", value: 84 },
      { label: "S", value: 61 },
    ],
  },
};
```

## Good to know

- The number is sized to its length: "31%" is set bigger than "12,480". Keep `stat.value` short.
- With a `rarity` the chart's point takes that rarity's colour. A common one, or a card with no rarity, lights it in the card's own ink.
- `renderInsightCard` lays the card out off screen and captures it at twice its size. Captures run one at a time, so asking for ten at once does not freeze the page.
- The chart on a card is drawn finished. Entrances belong to the video.

## Related

- [Graphics](https://bettertyped.github.io/react-wrapped/docs/graphics/)
- [Charts](https://bettertyped.github.io/react-wrapped/docs/charts/)
- [Graphics gallery](https://bettertyped.github.io/react-wrapped/docs/graphics/gallery/)
- [Share card](https://bettertyped.github.io/react-wrapped/docs/graphics/share-card/)
- [Fact scene](https://bettertyped.github.io/react-wrapped/docs/scenes/fact/)

Source: https://bettertyped.github.io/react-wrapped/docs/graphics/insight-cards/
