> ## Documentation Index
> Fetch the complete documentation index at: https://anvil.servicetitan.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Donut Charts

> Create donut and pie charts with legends using Anvil2 themes and amCharts 5.

export const LiveCode = ({children, customHeight, clickToLoad, example, fullWidth, fullHeight, hideCodeInLiveCode, screenshot, screenshotOnly, showCode: showCodeProp}) => {
  const SCREENSHOTS_BASE = "https://servicetitan.github.io/anvil2-docs-live-code/screenshots";
  const STACKBLITZ_BASE = "https://stackblitz.com/github/servicetitan/anvil2-docs-live-code/tree/main/examples";
  const [showCodeBlock, setShowCodeBlock] = useState(showCodeProp ?? false);
  const [isLocalOverride, setIsLocalOverride] = useState(false);
  useEffect(() => {
    const examplePath = `/images/live-code-screenshots-tmp/${example}.png`;
    fetch(examplePath, {
      method: "HEAD"
    }).then(r => {
      if (r.ok) setIsLocalOverride(true);
    }).catch(() => {});
  }, [example]);
  const screenshotBase = isLocalOverride ? "/images/live-code-screenshots-tmp" : SCREENSHOTS_BASE;
  if (screenshotOnly) {
    return <Frame className="flex flex-col">
        <div className="flex dark:hidden" style={{
      justifyContent: "center",
      alignItems: "center",
      width: fullWidth ? "100%" : "50%",
      minHeight: fullHeight ? "284px" : undefined,
      background: "#FFFFFF"
    }}>
          <img srcset={`${screenshotBase}/${example}.png, ${screenshotBase}/${example}-2x.png 2x`} src={`${screenshotBase}/${example}.png`} alt={example} noZoom />
        </div>
        <div className="hidden dark:flex" style={{
      justifyContent: "center",
      alignItems: "center",
      width: fullWidth ? "100%" : "50%",
      minHeight: fullHeight ? "284px" : undefined,
      background: "#141414"
    }}>
          <img srcset={`${screenshotBase}/${example}-dark.png, ${screenshotBase}/${example}-dark-2x.png 2x`} src={`${screenshotBase}/${example}-dark.png`} alt={example} noZoom />
        </div>
      </Frame>;
  }
  if (screenshot) {
    return <Frame className="flex flex-col -mb-2">
        <div className="flex dark:hidden bg-white dark:bg-codeblock border border-gray-950/10 dark:border-white/10 dark:twoslash-dark rounded-2xl overflow-hidden" style={{
      justifyContent: "center",
      alignItems: "center",
      width: fullWidth ? "100%" : "50%",
      minHeight: fullHeight ? "284px" : undefined
    }}>
          <img srcset={`${screenshotBase}/${example}.png, ${screenshotBase}/${example}-2x.png 2x`} src={`${screenshotBase}/${example}.png`} alt={example} noZoom />
        </div>

        <div className="hidden dark:flex bg-white dark:bg-codeblock border border-gray-950/10 dark:border-white/10 dark:twoslash-dark rounded-2xl overflow-hidden" style={{
      background: "#141414",
      justifyContent: "center",
      alignItems: "center",
      width: fullWidth ? "100%" : "50%",
      minHeight: fullHeight ? "284px" : undefined
    }}>
          <img srcset={`${screenshotBase}/${example}-dark.png, ${screenshotBase}/${example}-dark-2x.png 2x`} src={`${screenshotBase}/${example}-dark.png`} alt={example} noZoom />
        </div>

        <div className="flex justify-end items-center text-xs py-2 px-1 gap-4">
          {!showCodeProp ? <button className="inline-flex justify-end items-center text-gray-700 dark:text-gray-50 hover:text-blue-500 dark:hover:text-blue-300 transition-colors group self-end gap-1 cursor-pointer" onClick={() => setShowCodeBlock(!showCodeBlock)} style={{
      appearance: "none"
    }}>
              <Icon icon="code" size="12px" className="group-hover:bg-blue-500 dark:group-hover:bg-blue-300" />
              <span>{showCodeBlock ? "Hide code" : "Show code"}</span>
            </button> : null}

          <a className="inline-flex justify-end items-center hover:text-blue-500 dark:hover:text-blue-300 transition-colors group self-end gap-1" href={`${STACKBLITZ_BASE}/${example}?file=src/App.tsx`} target="_blank" rel="noreferrer">
            <Icon icon="bolt" size="12px" className="group-hover:bg-blue-500 dark:group-hover:bg-blue-300" />
            <span>StackBlitz demo</span>
          </a>
        </div>

        <div className="grid transition-[grid-template-rows] duration-300 ease-in-out overflow-auto overflow-y-hidden overflow-x-auto" style={showCodeBlock ? {
      gridTemplateRows: "1fr"
    } : {
      gridTemplateRows: "0fr"
    }}>
          <div style={{
      minHeight: 0,
      overflowX: "auto",
      overflowY: "hidden",
      marginBlockStart: "-1.25rem",
      marginBlockEnd: "-1.5rem"
    }}>
            {children}
          </div>
        </div>
      </Frame>;
  } else {
    return <div style={{
      display: "flex",
      width: fullWidth ? "100%" : "50%",
      minHeight: customHeight ? customHeight : "316px",
      resize: "vertical",
      overflow: "auto"
    }}>
        <iframe title={example} style={{
      flex: 1,
      width: fullWidth ? "100%" : "50%",
      minHeight: customHeight ? customHeight : "316px"
    }} src={`${STACKBLITZ_BASE}/${example}?embed=1&hideNavigation=1&hideExplorer=1&terminalHeight=0&file=src/App.tsx${clickToLoad ? "&ctl=1" : ""}${hideCodeInLiveCode ? "&view=preview" : ""}`} allow="accelerometer; ambient-light-sensor; camera; encrypted-media; geolocation; gyroscope; hid; microphone; midi; payment; usb; vr; xr-spatial-tracking" sandbox="allow-forms allow-modals allow-popups allow-presentation allow-same-origin allow-scripts" />
      </div>;
  }
};

Donut and pie charts use the amCharts 5 `PieChart` and `PieSeries` classes. All three Anvil2 themes apply design-token-based colors and chart styling automatically.

<Note>
  For design guidelines on donut chart spacing, direct labeling, and variable ordering, see [Donut Charts design guidelines](/docs/web/data-visualization/donut-charts).
</Note>

## Basic donut chart

A donut chart is a `PieChart` with an `innerRadius` set. The theme styles slice labels with a white semi-transparent background and pointer cursors, while this example configures the label text to show percentages inside each slice.

<LiveCode showCode example="ext-charts-donut-basic" fullWidth screenshot>
  ```tsx lines expandable theme={null}
  import { useLayoutEffect } from "react";
  import * as am5 from "@amcharts/amcharts5";
  import am5themes_Animated from "@amcharts/amcharts5/themes/Animated";
  import * as am5percent from "@amcharts/amcharts5/percent";
  import { ThemeMonochrome } from "@servicetitan/anvil2-ext-charts/am5";

  function App() {
    useLayoutEffect(() => {
      const data = [
        { category: "Lithuania", value: 501.9 },
        { category: "Czechia", value: 301.9 },
        { category: "Ireland", value: 201.1 },
        { category: "Germany", value: 165.8 },
      ];

      const root = am5.Root.new("chartdiv");
      root.setThemes([am5themes_Animated.new(root), ThemeMonochrome.new(root)]);

      const chart = root.container.children.push(
        am5percent.PieChart.new(root, {
          layout: root.horizontalLayout,
          innerRadius: am5.percent(50),
        }),
      );

      const series = chart.series.push(
        am5percent.PieSeries.new(root, {
          name: "Series",
          valueField: "value",
          categoryField: "category",
          legendValueText: "",
        }),
      );

      // Display percentage labels on slices
      series.labels.template.set(
        "text",
        "{valuePercentTotal.formatNumber('#.')}%",
      );

      series.data.setAll(data);

      return () => root.dispose();
    }, []);

    return <div id="chartdiv" style={{ minWidth: "55rem", height: "500px" }} />;
  }

  export default App;
  ```
</LiveCode>

## Adding a legend

Add a legend to display category names alongside the chart. Set the chart layout to `horizontalLayout` and position the legend vertically beside the donut.

<LiveCode showCode example="ext-charts-donut-legend" fullWidth screenshot>
  ```tsx lines expandable theme={null}
  import { useLayoutEffect } from "react";
  import * as am5 from "@amcharts/amcharts5";
  import am5themes_Animated from "@amcharts/amcharts5/themes/Animated";
  import * as am5percent from "@amcharts/amcharts5/percent";
  import { ThemeMonochrome } from "@servicetitan/anvil2-ext-charts/am5";

  function App() {
    useLayoutEffect(() => {
      const data = [
        { category: "Lithuania", value: 501.9 },
        { category: "Czechia", value: 301.9 },
        { category: "Ireland", value: 201.1 },
        { category: "Germany", value: 165.8 },
      ];

      const root = am5.Root.new("chartdiv");
      root.setThemes([am5themes_Animated.new(root), ThemeMonochrome.new(root)]);

      // Use horizontalLayout so the legend appears to the right of the chart
      const chart = root.container.children.push(
        am5percent.PieChart.new(root, {
          layout: root.horizontalLayout,
          innerRadius: am5.percent(50),
        }),
      );

      const series = chart.series.push(
        am5percent.PieSeries.new(root, {
          name: "Series",
          valueField: "value",
          categoryField: "category",
          legendValueText: "",
        }),
      );
      series.labels.template.set(
        "text",
        "{valuePercentTotal.formatNumber('#.')}%",
      );
      series.data.setAll(data);

      // Add a vertical legend to the right of the chart
      const legend = chart.children.push(
        am5.Legend.new(root, {
          centerY: am5.percent(50),
          y: am5.percent(50),
          layout: root.verticalLayout,
        }),
      );
      legend.data.setAll(series.dataItems);

      return () => root.dispose();
    }, []);

    return <div id="chartdiv" style={{ minWidth: "55rem", height: "500px" }} />;
  }

  export default App;
  ```
</LiveCode>

<Warning>
  All three Anvil2 themes set `clickTarget: "none"` on legends. This disables the default amCharts behavior where clicking a legend item toggles the corresponding series on and off. If you need clickable legends, override this setting after applying the theme: `legend.set("clickTarget", "itemContainer")`.
</Warning>

## Hover behavior

The theme registers `pointerover` and `pointerout` event handlers on all `Slice` elements. When a user hovers over a slice, all sibling slices dim to 20% opacity. This behavior applies to every pie and donut chart using the theme and cannot be disabled without overriding the theme rules.

## Pie chart (without inner radius)

The [design guidelines](/docs/web/data-visualization/donut-charts#5-donut-vs-pie) recommend donut charts over pie charts for dashboard layouts. Donut charts provide a cleaner layout and allow central labeling of key metrics. Use a pie chart only when the open center is not needed.

To create a standard pie chart instead of a donut, omit the `innerRadius` property:

```tsx theme={null}
const chart = root.container.children.push(
  am5percent.PieChart.new(root, {
    layout: root.horizontalLayout,
    // No innerRadius — renders as a full pie chart
  }),
);
```

## Using different themes

Change the theme import to apply a different color palette. The chart structure stays the same.

### Monochrome theme

Use `ThemeMonochrome` for data with a natural order or progression, or when displaying 4 or fewer variables. This is the default theme used in the examples above.

```tsx theme={null}
import { ThemeMonochrome } from "@servicetitan/anvil2-ext-charts/am5";

root.setThemes([
  am5themes_Animated.new(root),
  ThemeMonochrome.new(root),
]);
```

### Categorical theme

Use `ThemeCategorical` when displaying 5 or more distinct categories that need maximum color differentiation.

```tsx theme={null}
import { ThemeCategorical } from "@servicetitan/anvil2-ext-charts/am5";

root.setThemes([
  am5themes_Animated.new(root),
  ThemeCategorical.new(root),
]);
```

<Warning>
  The categorical palette colors do not all meet 3:1 contrast against each other. When using `ThemeCategorical`, include direct labeling on slices to meet [accessibility requirements](/docs/web/data-visualization/accessibility). The theme applies percentage labels by default, which satisfies this requirement for most cases.
</Warning>

### Semantic theme

Use `ThemeSemantic` when data represents status values. Colors follow the order: Success, Neutral, Warning, Danger.

```tsx theme={null}
import { ThemeSemantic } from "@servicetitan/anvil2-ext-charts/am5";

root.setThemes([
  am5themes_Animated.new(root),
  ThemeSemantic.new(root),
]);
```

## Tooltips

The theme applies tooltips that match the Anvil2 [Tooltip](/docs/web/components/tooltip/design) styling across all chart types.

Customize the tooltip text format on the series:

```tsx theme={null}
// The tooltip is automatically created by the theme on PieChart
// Customize the text format on the series instead:
series.slices.template.set("tooltipText", "{category}: {value}");
```
