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.
The component on the page, scaled to fit. The button saves it as a PNG.
Installation
pnpm add @react-wrapped/graphics @react-wrapped/core gsapimport "@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.
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, 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. |
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.valueshort. - With a
raritythe 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. renderInsightCardlays 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.
