component — charts
Sunburst Chart
The hierarchy a Treemap shows as rectangles, drawn as concentric rings on the quebi Chart wrapper, with themed labels and one palette step per branch. Requires the recharts package.
- chart
- sunburst
- hierarchy
- polar
- recharts
- data
- visualization
04 examples
Default
The inner ring is the top-level branches; each ring outwards is one level deeper, in the branch's own shade.
Branch colours from the config
A `config` entry named after a branch sets that branch's colour, and its descendants inherit it.
Without values
Recharts prints the value inside each segment rather than the name. `hideValues` drops them and leaves the names to the tooltip.
Half circle
A start and end angle sweep the rings over half an arc instead of a full circle.
source
sunburst-chart.tsxCopy this into your project. Resolve its dependencies from the registryDependencies in the component's API entry.
"use client"
import { type ComponentProps, useMemo } from "react"
import { SunburstChart as SunburstChartPrimitive, type SunburstData } from "recharts"
import type { NameType, ValueType } from "recharts/types/component/DefaultTooltipContent"
import {
type BaseChartProps,
type ChartColorKeys,
Chart,
ChartTooltip,
ChartTooltipContent,
EXTENDED_COLORS,
getColorValue,
} from "@/components/chart"
/**
* SunburstChart — quebi design system
*
* The same hierarchy a `Treemap` shows as nested rectangles, drawn as concentric
* rings: the inner ring is the top-level branches and every ring outwards is one
* level deeper. Each branch takes one step of the ink palette and its
* descendants inherit it, so a ring segment's color says which branch it belongs
* to. Override a branch's color with a `config` entry under its name.
*
* Recharts renders the *value* inside each segment, not the name — the name is
* on the tooltip. Pass `hideValues` for a chart that carries no numbers at all.
*
* Requires the `recharts` npm package as a peer dependency.
*/
/**
* Recharts wants one root node, not an array, and reads the root's own value as
* the full sweep of the circle. Give the root the total.
*/
export type SunburstDatum = SunburstData
/**
* Paint each top-level branch with one palette step and hand it down. Recharts
* resolves a segment's fill from the node itself before falling back to its
* parent's, so writing it on every node keeps the rings of one branch together.
*/
function paintBranches(
nodes: readonly SunburstData[],
colors: readonly (ChartColorKeys | (string & {}))[],
overrides: Record<string, string | undefined>,
inherited?: string,
): SunburstData[] {
return nodes.map((node, index) => {
const fill =
inherited ??
getColorValue(overrides[node.name] ?? colors[index % colors.length] ?? colors[0])
return {
...node,
fill,
children: node.children
? paintBranches(node.children, colors, overrides, fill)
: undefined,
}
})
}
// The recharts default is bold black text with a white halo, which is legible on
// exactly one theme. Both of these are theme tokens, so the label follows the
// surface it is drawn on.
const quebiTextOptions = {
fill: "var(--color-quebi-fg)",
stroke: "var(--color-quebi-bg)",
strokeWidth: 3,
paintOrder: "stroke fill",
fontSize: "0.75rem",
fontWeight: "500",
pointerEvents: "none",
} as const
const hiddenTextOptions = { ...quebiTextOptions, fill: "transparent", stroke: "transparent" }
export interface SunburstChartProps<TValue extends ValueType, TName extends NameType>
extends Omit<
BaseChartProps<TValue, TName>,
| "data"
| "dataKey"
| "layout"
| "type"
| "legend"
| "legendProps"
| "hideGridLines"
| "hideXAxis"
| "hideYAxis"
| "xAxisProps"
| "yAxisProps"
| "cartesianGridProps"
| "displayEdgeLabelsOnly"
| "intervalType"
| "valueFormatter"
> {
/** The root of the tree. Its `children` become the innermost ring. */
data: SunburstDatum
/** Key of the value on every node. Defaults to `"value"`. */
dataKey?: string
/** Key of the node's name, used by the tooltip. Defaults to `"name"`. */
nameKey?: string
/** Radius of the hole in the middle, in px. */
innerRadius?: number
outerRadius?: number
/** Gap between rings, in px. */
ringPadding?: number
/** Gap between neighbouring segments, in px. */
padding?: number
startAngle?: number
endAngle?: number
/** Draw the rings without the value printed inside each segment. */
hideValues?: boolean
chartProps?: Omit<ComponentProps<typeof SunburstChartPrimitive>, "data">
}
export function SunburstChart<TValue extends ValueType, TName extends NameType>({
data,
dataKey = "value",
nameKey = "name",
colors = EXTENDED_COLORS,
config,
children,
// Components
tooltip = true,
tooltipProps,
innerRadius = 40,
outerRadius,
ringPadding = 2,
padding = 2,
startAngle = 0,
endAngle = 360,
hideValues = false,
chartProps,
...props
}: SunburstChartProps<TValue, TName>) {
const overrides = useMemo(() => {
const entries: Record<string, string | undefined> = {}
for (const [key, value] of Object.entries(config)) {
entries[key] = value.color
}
return entries
}, [config])
const painted = useMemo<SunburstData>(
() => ({
...data,
children: data.children ? paintBranches(data.children, colors, overrides) : undefined,
}),
[data, colors, overrides],
)
// The Chart context wants a row list; the rings are the top-level branches.
const contextData = useMemo(
() => (painted.children ?? []).map((node) => ({ ...node })),
[painted],
)
return (
<Chart config={config} data={contextData} dataKey={nameKey} layout="radial" {...props}>
<SunburstChartPrimitive
data={painted}
dataKey={dataKey}
nameKey={nameKey}
innerRadius={innerRadius}
outerRadius={outerRadius}
ringPadding={ringPadding}
padding={padding}
startAngle={startAngle}
endAngle={endAngle}
// A sector's outline is the chart surface showing through, so the rings
// stay separated in both themes without a hairline of a third color.
stroke="var(--color-quebi-bg)"
fill={getColorValue(colors[0])}
textOptions={hideValues ? hiddenTextOptions : quebiTextOptions}
{...chartProps}
>
{tooltip && (
<ChartTooltip
content={
typeof tooltip === "boolean" ? (
<ChartTooltipContent hideLabel labelSeparator={false} nameKey={nameKey} />
) : (
tooltip
)
}
{...tooltipProps}
/>
)}
{children}
</SunburstChartPrimitive>
</Chart>
)
}