> ## Documentation Index
> Fetch the complete documentation index at: https://prefab.prefect.io/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Charts

> The shared data, series, and formatting model behind every Prefab chart.

export const ComponentPreview = ({json, height, resizable, bare, hideJson, playground, children}) => {
  const hostRef = React.useRef(null);
  const handleRef = React.useRef(null);
  const cardRef = React.useRef(null);
  const containerRef = React.useRef(null);
  const [previewWidth, setPreviewWidth] = React.useState(null);
  const [isDragging, setIsDragging] = React.useState(false);
  const [playgroundHref, setPlaygroundHref] = React.useState(null);
  React.useEffect(function () {
    if (!playground) return;
    var isLocal = location.hostname === "localhost" || location.hostname === "127.0.0.1";
    var path = isLocal ? "/playground" : "/docs/playground";
    var url = new URL(path, window.location.href);
    url.hash = "code=" + playground;
    setPlaygroundHref(url.href);
  }, [playground]);
  var jsonStr = typeof json === "string" ? json : JSON.stringify(json, null, 2);
  React.useEffect(function () {
    if (cardRef.current) {
      var card = cardRef.current.closest(".group");
      if (card) card.classList.remove("group");
    }
    var host = hostRef.current;
    if (!host || !json) return;
    function mount() {
      if (!window.__prefab || !host) return;
      var dark = document.documentElement.classList.contains("dark");
      handleRef.current = window.__prefab.mountPreview(host, jsonStr, {
        dark: dark
      });
      if (bare && host.shadowRoot) {
        var m = host.shadowRoot.querySelector("[data-prefab-mount]");
        if (m) m.style.background = "transparent";
      }
    }
    if (window.__prefab) {
      mount();
    } else {
      if (!window.__prefabLoading) {
        if (!window.__prefabReady) {
          var s = document.createElement("script");
          s.src = "/renderer.js";
          document.head.appendChild(s);
        }
        window.__prefabLoading = new Promise(function (resolve) {
          function check() {
            if (window.__prefabReady) {
              window.__prefabReady.then(resolve);
            } else {
              setTimeout(check, 10);
            }
          }
          check();
        });
      }
      window.__prefabLoading.then(mount);
    }
    var observer = new MutationObserver(function () {
      var dark = document.documentElement.classList.contains("dark");
      if (handleRef.current) handleRef.current.setDark(dark);
    });
    observer.observe(document.documentElement, {
      attributes: true,
      attributeFilter: ["class"]
    });
    return function () {
      observer.disconnect();
      if (handleRef.current) {
        handleRef.current.unmount();
        handleRef.current = null;
      }
    };
  }, [json]);
  function startResize(e) {
    e.preventDefault();
    var startX = e.clientX;
    var startW = hostRef.current ? hostRef.current.offsetWidth : 0;
    var maxW = containerRef.current ? containerRef.current.offsetWidth - 10 : startW;
    setIsDragging(true);
    function onMove(ev) {
      var w = Math.max(260, Math.min(startW + (ev.clientX - startX), maxW));
      setPreviewWidth(w);
    }
    function onUp() {
      document.removeEventListener("mousemove", onMove);
      document.removeEventListener("mouseup", onUp);
      setIsDragging(false);
    }
    document.addEventListener("mousemove", onMove);
    document.addEventListener("mouseup", onUp);
  }
  if (bare) {
    return <div ref={hostRef} />;
  }
  if (resizable) {
    return <Card>
        <div ref={cardRef} />
        <div ref={containerRef} style={{
      display: "flex",
      position: "relative",
      overflow: "hidden"
    }}>
          <div ref={hostRef} style={{
      flex: previewWidth ? "none" : 1,
      width: previewWidth ? previewWidth + "px" : undefined,
      minWidth: 0
    }} />
          <div onMouseDown={startResize} style={{
      width: "14px",
      flexShrink: 0,
      cursor: "col-resize",
      background: isDragging ? "var(--border, #e5e7eb)" : "color-mix(in srgb, var(--border, #e5e7eb) 40%, transparent)",
      display: "flex",
      alignItems: "center",
      justifyContent: "center",
      transition: "background 0.15s",
      userSelect: "none"
    }}>
            <svg width="6" height="24" style={{
      opacity: 0.5
    }}>
              <circle cx="2" cy="4" r="1.2" fill="currentColor" />
              <circle cx="2" cy="9" r="1.2" fill="currentColor" />
              <circle cx="2" cy="14" r="1.2" fill="currentColor" />
              <circle cx="2" cy="19" r="1.2" fill="currentColor" />
              <circle cx="5" cy="4" r="1.2" fill="currentColor" />
              <circle cx="5" cy="9" r="1.2" fill="currentColor" />
              <circle cx="5" cy="14" r="1.2" fill="currentColor" />
              <circle cx="5" cy="19" r="1.2" fill="currentColor" />
            </svg>
          </div>
          <div style={{
      flex: previewWidth ? 1 : "0 0 24px",
      minWidth: "24px",
      background: "repeating-linear-gradient(-45deg, transparent, transparent 3px, var(--border, #e5e7eb) 3px, var(--border, #e5e7eb) 4px)",
      opacity: 0.4
    }} />
          {previewWidth && <div style={{
      position: "absolute",
      bottom: "8px",
      right: "8px",
      background: "var(--background, white)",
      border: "1px solid var(--border, #e5e7eb)",
      borderRadius: "4px",
      padding: "1px 6px",
      fontSize: "11px",
      fontFamily: "monospace",
      color: "var(--muted-foreground, #6b7280)",
      pointerEvents: "none"
    }}>
              {previewWidth}px
            </div>}
        </div>
        {playgroundHref && <div style={{
      display: "flex",
      justifyContent: "flex-end",
      padding: "4px 0 8px"
    }}>
            <a href={playgroundHref} target="_blank" rel="noopener noreferrer" style={{
      fontSize: "11px",
      opacity: 0.4,
      textDecoration: "none",
      textDecorationLine: "none",
      borderBottom: "none",
      boxShadow: "none",
      display: "inline-flex",
      alignItems: "center",
      gap: "3px",
      transition: "opacity 0.15s",
      fontWeight: 400
    }} onMouseOver={function (e) {
      e.currentTarget.style.opacity = "0.6";
    }} onMouseOut={function (e) {
      e.currentTarget.style.opacity = "0.4";
    }}>
              Edit in Playground
              <svg xmlns="http://www.w3.org/2000/svg" width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M15 3h6v6" /><path d="M10 14 21 3" /><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" /></svg>
            </a>
          </div>}
        <div style={{
      marginBottom: "-2rem"
    }}>{children}</div>
      </Card>;
  }
  return <Card>
      <div ref={cardRef} />
      <div ref={hostRef} />
      {playgroundHref && <div style={{
    display: "flex",
    justifyContent: "flex-end",
    padding: "4px 0 8px"
  }}>
          <a href={playgroundHref} target="_blank" rel="noopener noreferrer" style={{
    fontSize: "11px",
    opacity: 0.4,
    textDecoration: "none",
    textDecorationLine: "none",
    borderBottom: "none",
    boxShadow: "none",
    display: "inline-flex",
    alignItems: "center",
    gap: "3px",
    transition: "opacity 0.15s",
    fontWeight: 400
  }} onMouseOver={function (e) {
    e.currentTarget.style.opacity = "0.6";
  }} onMouseOut={function (e) {
    e.currentTarget.style.opacity = "0.4";
  }}>
            Edit in Playground
            <svg xmlns="http://www.w3.org/2000/svg" width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M15 3h6v6" /><path d="M10 14 21 3" /><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" /></svg>
          </a>
        </div>}
      <div style={{
    marginBottom: "-2rem"
  }}>{children}</div>
    </Card>;
};

Every Prefab chart — bar, line, area, scatter, radar, and the rest — is built from the same three pieces: a list of **data** rows, one or more **series** that each pick a field to plot, and an **axis** field that labels the categories. Learn this model once and every chart page reads as the same idea with a different shape.

## Data and series

`data` is a list of dictionaries, one per row. Each row holds a category label plus one value per series. A `ChartSeries` maps a single `data_key` to a drawn series, so plotting two metrics side by side means two `ChartSeries` reading two keys from the same rows.

<ComponentPreview json={{"view":{"type":"BarChart","data":[{"month":"Jan","desktop":186,"mobile":80},{"month":"Feb","desktop":305,"mobile":200},{"month":"Mar","desktop":237,"mobile":120}],"series":[{"dataKey":"desktop","label":"Desktop"},{"dataKey":"mobile","label":"Mobile"}],"xAxis":"month","height":300,"stacked":false,"horizontal":false,"barRadius":4,"showLegend":true,"showTooltip":true,"animate":true,"showGrid":true,"showYAxis":true,"valueFormat":"auto"}}} playground="ZnJvbSBwcmVmYWJfdWkuY29tcG9uZW50cy5jaGFydHMgaW1wb3J0IEJhckNoYXJ0LCBDaGFydFNlcmllcwoKZGF0YSA9IFsKICAgIHsibW9udGgiOiAiSmFuIiwgImRlc2t0b3AiOiAxODYsICJtb2JpbGUiOiA4MH0sCiAgICB7Im1vbnRoIjogIkZlYiIsICJkZXNrdG9wIjogMzA1LCAibW9iaWxlIjogMjAwfSwKICAgIHsibW9udGgiOiAiTWFyIiwgImRlc2t0b3AiOiAyMzcsICJtb2JpbGUiOiAxMjB9LApdCgpCYXJDaGFydCgKICAgIGRhdGE9ZGF0YSwKICAgIHNlcmllcz1bCiAgICAgICAgQ2hhcnRTZXJpZXMoZGF0YV9rZXk9ImRlc2t0b3AiLCBsYWJlbD0iRGVza3RvcCIpLAogICAgICAgIENoYXJ0U2VyaWVzKGRhdGFfa2V5PSJtb2JpbGUiLCBsYWJlbD0iTW9iaWxlIiksCiAgICBdLAogICAgeF9heGlzPSJtb250aCIsCikK">
  <CodeGroup>
    ```python Python icon="python" theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
    from prefab_ui.components.charts import BarChart, ChartSeries

    data = [
        {"month": "Jan", "desktop": 186, "mobile": 80},
        {"month": "Feb", "desktop": 305, "mobile": 200},
        {"month": "Mar", "desktop": 237, "mobile": 120},
    ]

    BarChart(
        data=data,
        series=[
            ChartSeries(data_key="desktop", label="Desktop"),
            ChartSeries(data_key="mobile", label="Mobile"),
        ],
        x_axis="month",
    )
    ```

    ```json Protocol icon="brackets-curly" theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
    {
      "view": {
        "type": "BarChart",
        "data": [
          {"month": "Jan", "desktop": 186, "mobile": 80},
          {"month": "Feb", "desktop": 305, "mobile": 200},
          {"month": "Mar", "desktop": 237, "mobile": 120}
        ],
        "series": [
          {"dataKey": "desktop", "label": "Desktop"},
          {"dataKey": "mobile", "label": "Mobile"}
        ],
        "xAxis": "month",
        "height": 300,
        "stacked": false,
        "horizontal": false,
        "barRadius": 4,
        "showLegend": true,
        "showTooltip": true,
        "animate": true,
        "showGrid": true,
        "showYAxis": true,
        "valueFormat": "auto"
      }
    }
    ```
  </CodeGroup>
</ComponentPreview>

The field that names each category is the **axis**. Cartesian charts (bar, line, area, scatter) call it `x_axis`; radar charts call it `axis_key`. It points at the row field that labels each point — `"month"` above — while every `ChartSeries` reads a value field. Pie, radial, and sparkline charts take a single series and skip the explicit axis.

`data` can also be a live state key instead of a literal list. Pass an `Rx` or a `{{ key }}` string to bind the chart to data that a server action loads at runtime, and the chart re-renders whenever that key changes.

## ChartSeries

A `ChartSeries` describes one drawn series. `data_key` is the only required field — it names the row field to plot. `label` sets the legend and tooltip text, and `color` overrides the automatic palette with any CSS color or theme variable.

```python theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
from prefab_ui.components.charts import ChartSeries

ChartSeries(data_key="revenue", label="Revenue", color="var(--chart-1)")
```

Series colors default to the theme's chart palette in order, so most charts need only `data_key` and `label`. Reach for `color` when a specific series carries meaning — a "danger" red, a brand color, a muted gray for a baseline.

## Formatting values

`value_format` formats the numbers a chart renders on its value axis and in tooltips. It accepts the same pipe strings as [expression pipes](/concepts/expressions#formatting-with-pipes) and data table columns: `currency`, `currency:EUR`, `percent:1`, `compact:0`, and the rest. The raw data stays untouched; only the display is formatted.

```python theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
BarChart(data=data, series=series, x_axis="month", value_format="currency")
```

This keeps formatting logic out of your data: feed the chart raw numbers like `42000000` and let `value_format="compact"` render `42M` at display time.

## Display toggles

A common set of booleans controls chart chrome across the cartesian charts. Each defaults to a sensible value, so you configure them only to move away from the default:

* `show_legend` — the series legend, off by default until you have more than one series worth naming.
* `show_tooltip` — the hover tooltip, on by default.
* `show_grid` — background gridlines, on by default. Turn them off for a compact, glanceable read where the shape matters more than exact values.
* `show_y_axis` — the value axis and its tick labels, on by default. Hide it for sparkline-like contexts.

Each individual chart page documents the props specific to its shape — `stacked` and `curve` for area, `horizontal` for bar, `inner_radius` for radial — on top of this shared foundation.
