Skip to content
design
Components

Chart container

Give a composed Recharts chart responsive sizing, token styling and a shared series registry.

On this page
  • Illustrative amount
Day
Illustrative amount
Mon
2
Tue
4
Wed
3
Thu
6
Give a composed Recharts chart responsive sizing, token styling and a shared series registry.
Code
import { ChartContainer, type ChartRegistry } from "@bosunkit/components/chart-container";
import { ChartTooltip } from "@bosunkit/components/chart-tooltip";
import { ChartLegend } from "@bosunkit/components/chart-legend";
import { DataTable, type Column } from "@bosunkit/components/data-table";
import * as Recharts from "recharts";
import type { ReactElement } from "react";

export const description: string =
  "A custom Recharts area uses the same registry, token colors and formatted tooltip as the standard charts.";
const values: Readonly<Record<number, string>> = {
  0: "0",
  1: "1",
  2: "2",
  3: "3",
  4: "4",
  5: "5",
  6: "6",
  7: "7",
  8: "8",
};
const formatValue: (value: number) => string = (value: number): string => values[value] ?? "";
const formatCategory: (value: string | number) => string = (value: string | number): string =>
  typeof value === "string" ? value : "";
const config: ChartRegistry = { amount: { label: "Illustrative amount", formatValue, color: "03" } };
const data: { day: string; amount: number | null }[] = [
  { day: "Mon", amount: 2 },
  { day: "Tue", amount: 4 },
  { day: "Wed", amount: 3 },
  { day: "Thu", amount: 6 },
];
const columns: readonly Column[] = [
  { key: "day", label: "Day" },
  {
    key: "amount",
    label: "Illustrative amount",
    align: "end",
    render: (row: Record<string, unknown>): string => {
      const { amount }: Record<string, unknown> = row;
      return typeof amount === "number" ? formatValue(amount) : "Not counted";
    },
  },
];

export default function ComposedArea(): ReactElement {
  return (
    <div>
      <ChartContainer
        config={config}
        label="Illustrative daily amounts"
        formatCategory={formatCategory}
        missingValueText="Not counted"
      >
        <Recharts.AreaChart data={data} accessibilityLayer aria-label="Illustrative daily amounts">
          <Recharts.CartesianGrid vertical={false} />
          <Recharts.XAxis dataKey="day" tickFormatter={formatCategory} axisLine={false} tickLine={false} />
          <Recharts.YAxis domain={[0, 8]} tickFormatter={formatValue} axisLine={false} tickLine={false} width="auto" />
          <Recharts.Area
            dataKey="amount"
            type="linear"
            stroke="var(--color-amount)"
            fill="var(--color-amount)"
            fillOpacity={0.2}
            isAnimationActive={false}
            connectNulls={false}
          />
          <ChartTooltip />
        </Recharts.AreaChart>
      </ChartContainer>
      <ChartLegend config={config} label="Amount series" />
      <section aria-label="Illustrative source values">
        <DataTable columns={columns} rows={data} />
      </section>
    </div>
  );
}

The chart container supplies a definite plot height, provisional server-rendering dimensions and the shared registry. Series colors are inline custom properties that reference the operational palette, so the enclosing Day, Dusk, Night or Bright theme resolves them through CSS.

Registry entries may select palette="analytical" for the blue comparison palette. The operational palette remains the default. The reference examples use pale blue, primary blue, deep blue and neutral gray from these tokens; labels retain category meaning when neighboring shades look alike.

01One series definitionNames, palette slots and patterns come from the same registry used by the drawing.
02Caller-formatted valuesThe caller supplies display text and quantity formatters; missing values have an explicit description.
03Accessible inspectionNames and patterns supplement color, and analytical plots expose their observations in a source table.

A registry entry may use emphasis="context" for a subdued comparison trace. bands supplies inspection metadata for custom ranged areas; raw composition remains responsible for drawing the interval.

Use

A custom root imports Recharts directly. Use qualified imports such as Recharts.AreaChart when kit chart names also appear in the file. Raw marks use var(--color-key), caller formatters, gap-preserving drawing and disabled interpolation. Their SVG carries an accessible name and an enabled accessibility layer. Custom composition also supplies an accessible source table. The private theme adapter matches the pinned experimental Recharts theme; consumers do not depend on its shape.

callout adds a persistent summary bubble inside the drawing. Supply a formatted value, a description naming the quantity and period, and a corner position. The caller computes the summary from its selected data. Keep essential source information in the surrounding panel and place the bubble where it does not obscure observations. The callout is also available on LineChart, BarChart, AreaChart, ScatterChart, PolarChart and PieChart.

Examples

Composition

  • Illustrative amount
Day
Illustrative amount
Mon
2
Tue
4
Wed
3
Thu
6
A custom Recharts area uses the same registry, token colors and formatted tooltip as the standard charts.
Code
import { ChartContainer, type ChartRegistry } from "@bosunkit/components/chart-container";
import { ChartTooltip } from "@bosunkit/components/chart-tooltip";
import { ChartLegend } from "@bosunkit/components/chart-legend";
import { DataTable, type Column } from "@bosunkit/components/data-table";
import * as Recharts from "recharts";
import type { ReactElement } from "react";

export const description: string =
  "A custom Recharts area uses the same registry, token colors and formatted tooltip as the standard charts.";
const values: Readonly<Record<number, string>> = {
  0: "0",
  1: "1",
  2: "2",
  3: "3",
  4: "4",
  5: "5",
  6: "6",
  7: "7",
  8: "8",
};
const formatValue: (value: number) => string = (value: number): string => values[value] ?? "";
const formatCategory: (value: string | number) => string = (value: string | number): string =>
  typeof value === "string" ? value : "";
const config: ChartRegistry = { amount: { label: "Illustrative amount", formatValue, color: "03" } };
const data: { day: string; amount: number | null }[] = [
  { day: "Mon", amount: 2 },
  { day: "Tue", amount: 4 },
  { day: "Wed", amount: 3 },
  { day: "Thu", amount: 6 },
];
const columns: readonly Column[] = [
  { key: "day", label: "Day" },
  {
    key: "amount",
    label: "Illustrative amount",
    align: "end",
    render: (row: Record<string, unknown>): string => {
      const { amount }: Record<string, unknown> = row;
      return typeof amount === "number" ? formatValue(amount) : "Not counted";
    },
  },
];

export default function ComposedArea(): ReactElement {
  return (
    <div>
      <ChartContainer
        config={config}
        label="Illustrative daily amounts"
        formatCategory={formatCategory}
        missingValueText="Not counted"
      >
        <Recharts.AreaChart data={data} accessibilityLayer aria-label="Illustrative daily amounts">
          <Recharts.CartesianGrid vertical={false} />
          <Recharts.XAxis dataKey="day" tickFormatter={formatCategory} axisLine={false} tickLine={false} />
          <Recharts.YAxis domain={[0, 8]} tickFormatter={formatValue} axisLine={false} tickLine={false} width="auto" />
          <Recharts.Area
            dataKey="amount"
            type="linear"
            stroke="var(--color-amount)"
            fill="var(--color-amount)"
            fillOpacity={0.2}
            isAnimationActive={false}
            connectNulls={false}
          />
          <ChartTooltip />
        </Recharts.AreaChart>
      </ChartContainer>
      <ChartLegend config={config} label="Amount series" />
      <section aria-label="Illustrative source values">
        <DataTable columns={columns} rows={data} />
      </section>
    </div>
  );
}

Cost versus latency and request volume

A custom Recharts scatter composition uses the shared container, tooltip and legend. Each model has one registry entry. Request count maps to a bounded circle area, preserving visible markers at low volume. The source table exposes each model's cost, P95 latency and count without requiring hover.

Cost vs latency

Each circle is a model, sized by requests.

Illustrative model cost versus P95 latency uses blue circles whose areas scale with request volume over a bounded marker range, with every value also in a table.
Code
import { ChartContainer, type ChartColor, type ChartRegistry } from "@bosunkit/components/chart-container";
import { ChartPanel } from "@bosunkit/components/chart-panel";
import { ChartLegend } from "@bosunkit/components/chart-legend";
import { ChartTooltip } from "@bosunkit/components/chart-tooltip";
import { Collapsible, CollapsibleTrigger, CollapsiblePanel } from "@bosunkit/components/collapsible";
import { DataTable, type Column } from "@bosunkit/components/data-table";
import * as Recharts from "recharts";
import type { ReactElement } from "react";

export const description: string =
  "Illustrative model cost versus P95 latency uses blue circles whose areas scale with request volume over a bounded marker range, with every value also in a table.";
interface Model {
  readonly color: ChartColor;
  readonly key: string;
  readonly label: string;
  readonly latency: number;
  readonly cost: number;
  readonly requests: number;
  readonly requestText: string;
}
const models: readonly Model[] = [
  { key: "sonnet", color: "04", label: "Sonnet", latency: 1900, cost: 2.1, requests: 712000, requestText: "712,000" },
  { key: "haiku", color: "05", label: "Haiku", latency: 540, cost: 0.62, requests: 388000, requestText: "388,000" },
  {
    key: "llama",
    color: "07",
    label: "Llama 70B",
    latency: 2800,
    cost: 0.94,
    requests: 102000,
    requestText: "102,000",
  },
  {
    key: "mistral",
    color: "06",
    label: "Mistral Large",
    latency: 2200,
    cost: 1.7,
    requests: 41000,
    requestText: "41,000",
  },
  { key: "embed", color: "09", label: "Embed", latency: 120, cost: 0.08, requests: 37000, requestText: "37,000" },
];
const costs: Readonly<Record<number, string>> = {
  0: "$0",
  0.08: "$0.08",
  0.5: "$0.50",
  0.62: "$0.62",
  0.94: "$0.94",
  1: "$1",
  1.5: "$1.50",
  1.7: "$1.70",
  2: "$2",
  2.1: "$2.10",
  2.5: "$2.50",
};
const latencies: Readonly<Record<number, string>> = {
  0: "0 s",
  120: "120 ms",
  500: "0.5 s",
  540: "540 ms",
  1000: "1 s",
  1500: "1.5 s",
  1900: "1.9 s",
  2000: "2 s",
  2200: "2.2 s",
  2500: "2.5 s",
  2800: "2.8 s",
  3000: "3 s",
};
const formatCost: (value: number) => string = (value: number): string => costs[value] ?? "";
const formatLatency: (value: string | number) => string = (value: string | number): string =>
  typeof value === "number" ? (latencies[value] ?? "") : "";
const config: ChartRegistry = Object.fromEntries(
  models.map((model: Model) => [
    model.key,
    {
      label: model.label,
      formatValue: formatCost,
      palette: "analytical" as const,
      color: model.color,
      pattern: "solid" as const,
    },
  ]),
);
const columns: readonly Column[] = [
  { key: "model", label: "Model" },
  { key: "latency", label: "P95 latency", align: "end" },
  { key: "cost", label: "USD / 1,000 requests", align: "end" },
  { key: "requests", label: "Requests", align: "end" },
];
const rows: readonly Record<string, unknown>[] = models.map((model: Model) => ({
  model: model.label,
  latency: formatLatency(model.latency),
  cost: formatCost(model.cost),
  requests: model.requestText,
}));
export default function CostLatency(): ReactElement {
  return (
    <div>
      <ChartPanel
        appearance="analytical"
        title="Cost vs latency"
        description="Each circle is a model, sized by requests."
      >
        <ChartContainer
          config={config}
          label="Illustrative model cost, P95 latency and request volume"
          formatCategory={formatLatency}
          missingValueText="Not measured"
        >
          <Recharts.ScatterChart accessibilityLayer aria-label="Model cost versus P95 latency">
            <Recharts.CartesianGrid />
            <Recharts.XAxis
              type="number"
              dataKey="latency"
              domain={[0, 3000]}
              ticks={[0, 1000, 2000, 3000]}
              tickFormatter={formatLatency}
              axisLine={false}
              tickLine={false}
            />
            <Recharts.YAxis
              type="number"
              domain={[0, 2.5]}
              ticks={[0, 1, 2]}
              tickFormatter={formatCost}
              width="auto"
              axisLine={false}
              tickLine={false}
            />
            <ChartTooltip />
            {models.map((model: Model): ReactElement => (
              <Recharts.Scatter
                key={model.key}
                name={model.key}
                dataKey={model.key}
                data={[{ latency: model.latency, [model.key]: model.cost }]}
                fill={`var(--color-${model.key})`}
                isAnimationActive={false}
                shape={(properties: unknown): ReactElement => {
                  const point = properties as { readonly cx?: number; readonly cy?: number };
                  if (point.cx === undefined || point.cy === undefined) return <g />;
                  const scale: number = Math.sqrt(9 + (72 * model.requests) / 712000);
                  const end: boolean = model.latency > 2500;
                  return (
                    <g>
                      <circle
                        cx={point.cx}
                        cy={point.cy}
                        r={`calc(var(--bk-chart-dot-radius) * ${String(scale)})`}
                        fill={`var(--color-${model.key})`}
                      />
                      <text
                        x={point.cx + (end ? -1 : 1) * (8 + scale * 3)}
                        y={point.cy}
                        dominantBaseline="middle"
                        textAnchor={end ? "end" : "start"}
                        fill="var(--on-flat-active-color)"
                        fontSize="var(--bk-type-caption-size)"
                      >
                        {model.label}
                      </text>
                    </g>
                  );
                }}
              />
            ))}
          </Recharts.ScatterChart>
        </ChartContainer>
      </ChartPanel>
      <Collapsible>
        <CollapsibleTrigger>View model cost and request volume</CollapsibleTrigger>
        <CollapsiblePanel motion={false}>
          <p>
            Illustrative comparison · Sep 27 to Oct 4, 2026 · Cost in USD per 1,000 requests · Circle areas scale with
            request count over a bounded marker range.
          </p>
          <ChartLegend config={config} label="Models" mark="point" />
          <DataTable columns={columns} rows={rows} />
        </CollapsiblePanel>
      </Collapsible>
    </div>
  );
}

Props

PropTypeDefaultDescription
calloutChartCallout | undefined–A persistent summary inside the plot; all text comes from the caller.
configrequiredChartRegistry–The registry shared by marks, tooltip and legend, with at most nine CSS-identifier keys.
seriesreadonly string[] | undefinedObject.keys(config)The visible series in display order; all registry keys by default.
bandsreadonly ChartBandDefinition[] | undefined[]Intervals exposed by ChartTooltip; raw composition draws their bounds with ranged Recharts areas.
labelrequiredstring–Names the chart group, as caller-translated display text.
formatCategoryrequired(value: string | number) => string–Writes the category or timestamp in the tooltip; include the time reference for times.
missingValueTextrequiredstring–Display text for a missing observation. Missing is never rendered as zero.
heightstring | undefined"var(--bk-chart-height)"The plot height as a CSS length; the chart height token by default.
initialDimension{ readonly width: number; readonly height: number } | undefined{ width: 480, height: 288 }Provisional dimensions for SSR before the responsive container measures its parent.
motionMotionProp | undefined–Entrance and exit motion; off by default. Marks and observations never interpolate.
childrenrequiredReactNode–A Recharts chart root; give the SVG an accessible name and keep its accessibility layer enabled.
classNamestring | undefined–Classes added to the container.
testIdstring | undefined–Becomes data-testid on the container.
refRef<HTMLElement> | undefined–The container element.
Do
  • Illustrative amount
Day
Illustrative amount
Mon
2
Tue
4
Wed
3
Thu
6
Name the observations and keep the comparison basis visible.
Don't

Keep a gap and describe the missing measurement in the source table.

A missing observation must not appear to be zero.
  • Data visualization: scales, missing measurements and comparable evidence.
  • Chart panel: titles, controls and source information.
  • Sparkline: compact trends with operational integrity and forecast information.