Charts

Pie Chart

Pie and donut chart built on the quebi chart wrapper and recharts, with teal-led quebi segment colors and an optional centered total label. Requires the recharts package.

chartpiedonutrechartsdatavisualization

Pie

A pie chart with five segments colored from the teal-led quebi palette.

Donut

The donut variant with a hollow center and a tooltip on hover.

Donut with total label

A donut chart showing the summed total in the center.

Source

Copy this into your project. Resolve its dependencies from the registryDependencies in the component's API entry.

"use client"

import type { ComponentProps } from "react"
import { Cell, Pie, PieChart as PieChartPrimitive } from "recharts"
import type { NameType, ValueType } from "recharts/types/component/DefaultTooltipContent"
import {
  type BaseChartProps,
  Chart,
  ChartTooltip,
  ChartTooltipContent,
  DEFAULT_COLORS,
  getColorValue,
} from "@/components/chart"

/**
 * PieChart — quebi design system
 *
 * A pie / donut chart built on the quebi `Chart` wrapper and recharts. Segment
 * colors come from the teal-led quebi palette (teal, violet, sky, amber, pink)
 * and can be overridden per-segment via the `config` prop. Supports an optional
 * centered total label in the `donut` variant.
 *
 * Requires the `recharts` npm package as a peer dependency.
 */

function sumNumericArray(arr: number[]): number {
  return arr.reduce((sum, num) => sum + num, 0)
}

function calculateDefaultLabel(data: Record<string, unknown>[], valueKey: string): number {
  return sumNumericArray(data.map((dataPoint) => dataPoint[valueKey] as number))
}

function parseLabelInput(
  labelInput: string | undefined,
  valueFormatter: (value: number) => string,
  data: Record<string, unknown>[],
  valueKey: string,
): string {
  return labelInput || valueFormatter(calculateDefaultLabel(data, valueKey))
}

interface PieChartProps<TValue extends ValueType, TName extends NameType>
  extends Omit<
    BaseChartProps<TValue, TName>,
    | "hideGridLines"
    | "hideXAxis"
    | "hideYAxis"
    | "xAxisProps"
    | "yAxisProps"
    | "displayEdgeLabelsOnly"
    | "legend"
    | "legendProps"
  > {
  variant?: "pie" | "donut"
  nameKey?: string

  chartProps?: Omit<ComponentProps<typeof PieChartPrimitive>, "data" | "stackOffset">

  label?: string
  showLabel?: boolean
  pieProps?: Omit<ComponentProps<typeof Pie>, "data" | "dataKey" | "name">
}

const PieChart = <TValue extends ValueType, TName extends NameType>({
  data = [],
  dataKey,
  colors = DEFAULT_COLORS,
  config,
  children,
  label,
  showLabel,

  // Components
  tooltip = true,
  tooltipProps,

  variant = "pie",
  nameKey,

  chartProps,

  valueFormatter = (value: number) => value.toString(),
  pieProps,
  ...props
}: PieChartProps<TValue, TName>) => {
  const parsedLabelInput = parseLabelInput(label, valueFormatter, data, dataKey)

  return (
    <Chart config={config} data={data} layout="radial" dataKey={dataKey} {...props}>
      {({ onLegendSelect }) => (
        <PieChartPrimitive
          data={data}
          onClick={() => {
            onLegendSelect(null)
          }}
          margin={{
            bottom: 0,
            left: 0,
            right: 0,
            top: 0,
          }}
          {...chartProps}
        >
          {showLabel && variant === "donut" && (
            <text
              className="fill-white font-semibold"
              x="50%"
              data-slot="label"
              y="50%"
              textAnchor="middle"
              dominantBaseline="middle"
            >
              {parsedLabelInput}
            </text>
          )}

          {!children ? (
            <Pie
              name={nameKey}
              dataKey={dataKey}
              data={data}
              cx={pieProps?.cx ?? "50%"}
              cy={pieProps?.cy ?? "50%"}
              startAngle={pieProps?.startAngle ?? 90}
              endAngle={pieProps?.endAngle ?? -270}
              strokeLinejoin="round"
              innerRadius={variant === "donut" ? "50%" : "0%"}
              isAnimationActive
              {...pieProps}
            >
              {data.map((_, index) => (
                <Cell
                  // biome-ignore lint/suspicious/noArrayIndexKey: stable data array for pie chart segments
                  key={`cell-${index}`}
                  fill={getColorValue(
                    config?.[(data[index]?.code || data[index]?.name) as string]?.color ??
                      colors[index % colors.length],
                  )}
                />
              ))}
            </Pie>
          ) : (
            children
          )}

          {tooltip && (
            <ChartTooltip
              content={
                typeof tooltip === "boolean" ? (
                  <ChartTooltipContent hideLabel labelSeparator={false} accessibilityLayer />
                ) : (
                  tooltip
                )
              }
              {...tooltipProps}
            />
          )}
        </PieChartPrimitive>
      )}
    </Chart>
  )
}

export type { PieChartProps }
export { PieChart }