diff --git a/README.md b/README.md
index 0d053a9..3d5a917 100644
--- a/README.md
+++ b/README.md
@@ -13,6 +13,7 @@ A lightweight React + D3 chart library for building responsive, accessible data
- **ScatterPlot** — correlation scatter chart
- **Histogram** — frequency distribution chart
- **BarChart** — categorical bar chart
+- **PieChart** — pie / donut chart for part-to-whole comparisons
## Installation
@@ -193,6 +194,54 @@ const data = [
---
+### PieChart
+
+Renders a pie or donut chart for part-to-whole comparisons. Labels are automatically hidden on slices narrower than ~20° to prevent overlap.
+
+```jsx
+import { PieChart } from 'quick-charts'
+
+const data = [
+ { label: 'Apples', value: 32 },
+ { label: 'Bananas', value: 21 },
+ { label: 'Cherries', value: 18 },
+ { label: 'Dates', value: 14 },
+]
+
+
+
d.value}
+ labelAccessor={d => d.label}
+ />
+
+
+{/* Donut variant */}
+
+
d.value}
+ labelAccessor={d => d.label}
+ innerRadius={0.55}
+ colors={['#e74c3c', '#3498db', '#2ecc71', '#f39c12']}
+ />
+
+```
+
+
+
+| Prop | Type | Default | Description |
+|------|------|---------|-------------|
+| `data` | `Array` | — | Array of data objects |
+| `valueAccessor` | `Function` | `d => d.value` | Returns a numeric value from each datum — determines slice size |
+| `labelAccessor` | `Function` | `d => d.label` | Returns a label string from each datum — used for color mapping and slice text |
+| `colors` | `String[]` | `d3.schemeSet2` | Array of color strings for the slices |
+| `innerRadius` | `Number` | `0` | Donut hole size as a fraction of the outer radius (0 = full pie, 0.5 = half donut) |
+| `padAngle` | `Number` | `0.02` | Gap between slices in radians |
+| `showLabels` | `Boolean` | `true` | Show label text inside each slice when `labelAccessor` is provided |
+
+---
+
## Full Example
```jsx
diff --git a/docs/piechart.png b/docs/piechart.png
new file mode 100644
index 0000000..d63a3d9
Binary files /dev/null and b/docs/piechart.png differ
diff --git a/src/Charts/PieChart.js b/src/Charts/PieChart.js
new file mode 100644
index 0000000..c50323e
--- /dev/null
+++ b/src/Charts/PieChart.js
@@ -0,0 +1,106 @@
+import React from "react"
+import PropTypes from "prop-types"
+import * as d3 from "d3"
+
+import Chart from "../Components/Chart"
+import { useChartDimensions, accessorPropsType } from "../Utils/utils"
+
+const defaultMargin = { marginTop: 20, marginRight: 20, marginBottom: 20, marginLeft: 20 }
+
+const PieChart = ({
+ data, valueAccessor, labelAccessor,
+ colors, innerRadius, padAngle, showLabels,
+}) => {
+ const [ref, dimensions] = useChartDimensions(defaultMargin)
+
+ if (!data || data.length === 0) return null
+
+ const outerRadius = Math.min(dimensions.boundedWidth, dimensions.boundedHeight) / 2
+ const cx = dimensions.boundedWidth / 2
+ const cy = dimensions.boundedHeight / 2
+
+ const colorScale = d3.scaleOrdinal(
+ Array.isArray(colors) ? colors : d3.schemeSet2
+ )
+
+ const resolvedInnerRadius = typeof innerRadius === 'number'
+ ? outerRadius * Math.min(Math.max(innerRadius, 0), 0.95)
+ : 0
+
+ const pieGenerator = d3.pie()
+ .value(valueAccessor)
+ .padAngle(padAngle !== undefined ? padAngle : 0.02)
+ .sort(null)
+
+ const arcGenerator = d3.arc()
+ .innerRadius(resolvedInnerRadius)
+ .outerRadius(outerRadius)
+
+ // Labels sit at 80% of the outer radius (or midpoint for donuts)
+ const labelRadius = resolvedInnerRadius > 0
+ ? (resolvedInnerRadius + outerRadius) / 2
+ : outerRadius * 0.65
+
+ const labelArc = d3.arc()
+ .innerRadius(labelRadius)
+ .outerRadius(labelRadius)
+
+ const arcs = pieGenerator(data)
+ const displayLabels = showLabels !== false && typeof labelAccessor === 'function'
+ // Hide labels on slices narrower than ~20° to prevent overlap
+ const MIN_LABEL_ANGLE = 0.35
+
+ return (
+
+
+
+ {arcs.map((arc, i) => {
+ const label = labelAccessor ? labelAccessor(data[i]) : String(i)
+ const sliceAngle = arc.endAngle - arc.startAngle
+ return (
+
+
+ {displayLabels && sliceAngle >= MIN_LABEL_ANGLE && (
+
+ {label}
+
+ )}
+
+ )
+ })}
+
+
+
+ )
+}
+
+PieChart.propTypes = {
+ data: PropTypes.array,
+ /** Returns a numeric value from each datum — determines slice size. */
+ valueAccessor: accessorPropsType,
+ /** Returns a label string from each datum — used for color mapping and optional text. */
+ labelAccessor: accessorPropsType,
+ /** Array of color strings for the slices. Defaults to d3.schemeSet2. */
+ colors: PropTypes.arrayOf(PropTypes.string),
+ /** Donut hole size as a fraction of the outer radius (0 = full pie, 0.5 = half donut). Default: 0. */
+ innerRadius: PropTypes.number,
+ /** Gap between slices in radians. Default: 0.02. */
+ padAngle: PropTypes.number,
+ /** Show label text inside each slice. Default: true when labelAccessor is provided. */
+ showLabels: PropTypes.bool,
+}
+
+PieChart.defaultProps = {
+ valueAccessor: d => d.value,
+ labelAccessor: d => d.label,
+}
+
+export default PieChart
diff --git a/src/Charts/index.js b/src/Charts/index.js
index 37fc85d..d7f762c 100644
--- a/src/Charts/index.js
+++ b/src/Charts/index.js
@@ -1,5 +1,6 @@
export {default as BarChart} from './BarChart';
export {default as Histogram} from './Histogram';
+export {default as PieChart} from './PieChart';
export {default as ScatterPlot} from './ScatterPlot';
export {default as Timeline} from './Timeline';
diff --git a/src/__tests__/PieChart.test.js b/src/__tests__/PieChart.test.js
new file mode 100644
index 0000000..6ce22f3
--- /dev/null
+++ b/src/__tests__/PieChart.test.js
@@ -0,0 +1,94 @@
+import React from 'react'
+import { render } from '@testing-library/react'
+import PieChart from '../Charts/PieChart'
+
+const makeData = () => [
+ { label: 'Apples', value: 30 },
+ { label: 'Bananas', value: 20 },
+ { label: 'Cherries', value: 15 },
+ { label: 'Dates', value: 10 },
+ { label: 'Elderberries', value: 25 },
+]
+
+describe('PieChart', () => {
+ it('renders without crashing', () => {
+ const { container } = render(
+ d.value}
+ labelAccessor={d => d.label}
+ />
+ )
+ expect(container.firstChild).toBeInTheDocument()
+ })
+
+ it('wraps content in a div with class "PieChart"', () => {
+ const { container } = render(
+ d.value} />
+ )
+ expect(container.querySelector('.PieChart')).toBeInTheDocument()
+ })
+
+ it('renders an SVG chart', () => {
+ const { container } = render(
+ d.value} />
+ )
+ expect(container.querySelector('svg.Chart')).toBeInTheDocument()
+ })
+
+ it('renders one slice per data point', () => {
+ const data = makeData()
+ const { container } = render(
+ d.value} />
+ )
+ expect(container.querySelectorAll('.PieChart__slice')).toHaveLength(data.length)
+ })
+
+ it('renders nothing for empty data', () => {
+ const { container } = render(
+ d.value} />
+ )
+ expect(container.firstChild).toBeNull()
+ })
+
+ it('renders labels when labelAccessor is provided', () => {
+ const data = makeData()
+ const { container } = render(
+ d.value}
+ labelAccessor={d => d.label}
+ />
+ )
+ expect(container.querySelectorAll('.PieChart__label')).toHaveLength(data.length)
+ })
+
+ it('hides labels when showLabels is false', () => {
+ const { container } = render(
+ d.value}
+ labelAccessor={d => d.label}
+ showLabels={false}
+ />
+ )
+ expect(container.querySelector('.PieChart__label')).toBeNull()
+ })
+
+ it('uses default accessors when none are supplied', () => {
+ const defaultData = makeData().map(d => ({ label: d.label, value: d.value }))
+ expect(() => render()).not.toThrow()
+ })
+
+ it('renders a donut when innerRadius is provided', () => {
+ const { container } = render(
+ d.value}
+ innerRadius={0.5}
+ />
+ )
+ // All slices should still render
+ expect(container.querySelectorAll('.PieChart__slice')).toHaveLength(makeData().length)
+ })
+})