# Pie Chart

> A customizable pie chart component with smooth animations and flexible styling.

**BNA UI** — a React Native / Expo component library.
These components render through `react-native`, not the DOM: there are no HTML
elements, no Tailwind classes and no Radix primitives. Source is copied into your
project and imported through `@/components/ui/*`, `@/components/charts/*`,
`@/hooks/*` and `@/theme/*`. Colours come from the `useColor` hook rather than
hardcoded hex; sizing tokens (`HEIGHT`, `FONT_SIZE`, `BORDER_RADIUS`, `CORNERS`)
come from `@/theme/globals`.

- Docs: https://ui.ahmedbna.com/docs/charts/pie-chart
- Markdown: https://ui.ahmedbna.com/docs/charts/pie-chart.md
- Structured JSON (props, usage, source, examples): https://ui.ahmedbna.com/r/ai/pie-chart.json
- Install payload (source plus every file it imports): https://ui.ahmedbna.com/r/pie-chart.json
- Install: `npx bna-ui add pie-chart`
- npm dependencies: `react-native-reanimated`, `react-native-svg`, `react-native-worklets`
- Registry dependencies: `mode-provider`, `useColorScheme`, `colors`, `useColor`, `globals`, `text`
- Preview recording: https://demo.ahmedbna.com/0363-pie-chart-demo.MOV

---

**Example:** A pie chart with smooth animations

```tsx
// components/demo/charts/pie-chart/pie-chart-demo.tsx
import { PieChart } from '@/components/charts/pie-chart';
import { ChartContainer } from '@/components/charts/chart-container';
import React from 'react';

const sampleData = [
  { label: 'Sales', value: 120 },
  { label: 'Marketing', value: 98 },
  { label: 'Support', value: 86 },
  { label: 'Development', value: 140 },
  { label: 'Design', value: 75 },
  { label: 'HR', value: 65 },
];

export function PieChartDemo() {
  return (
    <ChartContainer
      title='Department Performance'
      description='Quarterly performance metrics by department'
    >
      <PieChart
        data={sampleData}
        config={{
          height: 300,
          showLabels: true,
          animated: true,
          duration: 1000,
        }}
      />
    </ChartContainer>
  );
}
```

## Installation

### CLI

```bash
npx bna-ui add pie-chart
```

### Manual

**1.** Install the required dependencies.

```bash
npm install react-native-svg react-native-reanimated react-native-worklets
```

**2.** Copy and paste the following code into your project.

```tsx
// components/charts/pie-chart.tsx
// components/charts/pie-chart.tsx

import { Text } from '@/components/ui/text';
import { useColor } from '@/hooks/useColor';
import React, { useEffect, useState } from 'react';
import { LayoutChangeEvent, View, ViewStyle } from 'react-native';
import Animated, {
  SharedValue,
  useAnimatedProps,
  useSharedValue,
  withTiming,
} from 'react-native-reanimated';
import Svg, { Circle, G, Path, Text as SvgText } from 'react-native-svg';

// Animated SVG Components
const AnimatedPath = Animated.createAnimatedComponent(Path);
const AnimatedCircle = Animated.createAnimatedComponent(Circle);

type AnimatedSliceProps = {
  d: string;
  fill: string;
  animationProgress: SharedValue<number>;
  // A single 100%-share slice makes the arc's start/end points coincide,
  // which SVG's arc command can't draw — render a plain circle instead.
  fullCircle?: { cx: number; cy: number; r: number };
};

// Per-item hook must live in its own mounted subcomponent, not in the
// parent's .map() body — calling useAnimatedProps per loop iteration
// violates Rules of Hooks the moment data.length changes.
const AnimatedSlice = React.memo(
  ({ d, fill, animationProgress, fullCircle }: AnimatedSliceProps) => {
    const sliceAnimatedProps = useAnimatedProps(() => ({
      opacity: animationProgress.value,
    }));

    if (fullCircle) {
      return (
        <AnimatedCircle
          cx={fullCircle.cx}
          cy={fullCircle.cy}
          r={fullCircle.r}
          fill={fill}
          animatedProps={sliceAnimatedProps}
        />
      );
    }

    return (
      <AnimatedPath d={d} fill={fill} animatedProps={sliceAnimatedProps} />
    );
  }
);

interface ChartConfig {
  width?: number;
  height?: number;
  showLabels?: boolean;
  animated?: boolean;
  duration?: number;
}

interface ChartDataPoint {
  label: string;
  value: number;
  color?: string;
}

type Props = {
  data: ChartDataPoint[];
  config?: ChartConfig;
  style?: ViewStyle;
};

export const PieChart = ({ data, config = {}, style }: Props) => {
  const [containerWidth, setContainerWidth] = useState(300);

  const {
    height = 200,
    showLabels = true,
    animated = true,
    duration = 1000,
  } = config;

  // Use measured width or fallback to config width or default
  const chartWidth = containerWidth || config.width || 300;

  const primaryColor = useColor('primary');

  const animationProgress = useSharedValue(0);

  const handleLayout = (event: LayoutChangeEvent) => {
    const { width: measuredWidth } = event.nativeEvent.layout;
    if (measuredWidth > 0) {
      setContainerWidth(measuredWidth);
    }
  };

  useEffect(() => {
    if (animated) {
      animationProgress.value = withTiming(1, { duration });
    } else {
      animationProgress.value = 1;
    }
  }, [data, animated, duration]);

  if (!data.length) return null;

  const total = data.reduce((sum, item) => sum + item.value, 0);
  if (total === 0) return null;

  const radius = Math.min(chartWidth, height) / 2 - 20;
  const centerX = chartWidth / 2;
  const centerY = height / 2;

  let currentAngle = -Math.PI / 2; // Start from top

  const colors = [
    primaryColor,
    useColor('blue'),
    useColor('green'),
    useColor('orange'),
    useColor('purple'),
    useColor('pink'),
  ];

  return (
    <View
      style={[{ width: '100%' }, style]}
      onLayout={handleLayout}
      accessibilityRole='image'
      accessibilityLabel={`Pie chart with ${data.length} slices, total ${Math.round(total)}`}
    >
      <Svg width={chartWidth} height={height}>
        {data.map((item, index) => {
          const sliceAngle = (item.value / total) * 2 * Math.PI;
          const startAngle = currentAngle;
          const endAngle = currentAngle + sliceAngle;

          const largeArcFlag = sliceAngle > Math.PI ? 1 : 0;

          const x1 = centerX + radius * Math.cos(startAngle);
          const y1 = centerY + radius * Math.sin(startAngle);
          const x2 = centerX + radius * Math.cos(endAngle);
          const y2 = centerY + radius * Math.sin(endAngle);

          const pathData = [
            `M ${centerX} ${centerY}`,
            `L ${x1} ${y1}`,
            `A ${radius} ${radius} 0 ${largeArcFlag} 1 ${x2} ${y2}`,
            'Z',
          ].join(' ');

          // Label position
          const labelAngle = startAngle + sliceAngle / 2;
          const labelRadius = radius * 0.7;
          const labelX = centerX + labelRadius * Math.cos(labelAngle);
          const labelY = centerY + labelRadius * Math.sin(labelAngle);

          currentAngle = endAngle;

          // A single slice spanning the full circle (only one item, or every
          // other item has a value of 0) has coincident arc start/end points.
          const isFullCircle = sliceAngle >= 2 * Math.PI - 1e-6;

          return (
            <G key={`slice-${index}`}>
              <AnimatedSlice
                d={pathData}
                fill={item.color || colors[index % colors.length]}
                animationProgress={animationProgress}
                fullCircle={
                  isFullCircle
                    ? { cx: centerX, cy: centerY, r: radius }
                    : undefined
                }
              />

              {showLabels && (
                <SvgText
                  x={labelX}
                  y={labelY}
                  textAnchor='middle'
                  fontSize={12}
                  fill='#FFFFFF'
                  fontWeight='600'
                >
                  {Math.round((item.value / total) * 100)}%
                </SvgText>
              )}
            </G>
          );
        })}
      </Svg>

      {/* Legend */}
      <View style={{ marginTop: 10 }}>
        {data.map((item, index) => (
          <View
            key={`legend-${index}`}
            style={{
              flexDirection: 'row',
              alignItems: 'center',
              marginBottom: 5,
            }}
          >
            <View
              style={{
                width: 12,
                height: 12,
                borderRadius: 6,
                backgroundColor: item.color || colors[index % colors.length],
                marginRight: 8,
              }}
            />
            <Text variant='caption'>
              {item.label}: {item.value}
            </Text>
          </View>
        ))}
      </View>
    </View>
  );
};
```

**3.** Update the import paths to match your project setup.

## Usage

```tsx
import { PieChart } from '@/components/charts/pie-chart';
```

```tsx
const data = [
  { label: 'Sales', value: 120 },
  { label: 'Marketing', value: 98 },
  { label: 'Support', value: 86 },
  { label: 'Development', value: 140 },
];

<PieChart
  data={data}
  config={{
    height: 200,
    showLabels: true,
    animated: true,
  }}
/>;
```

## Examples

#### Basic Pie Chart

**Example:** A pie chart with smooth animations

```tsx
// components/demo/charts/pie-chart/pie-chart-demo.tsx
import { PieChart } from '@/components/charts/pie-chart';
import { ChartContainer } from '@/components/charts/chart-container';
import React from 'react';

const sampleData = [
  { label: 'Sales', value: 120 },
  { label: 'Marketing', value: 98 },
  { label: 'Support', value: 86 },
  { label: 'Development', value: 140 },
  { label: 'Design', value: 75 },
  { label: 'HR', value: 65 },
];

export function PieChartDemo() {
  return (
    <ChartContainer
      title='Department Performance'
      description='Quarterly performance metrics by department'
    >
      <PieChart
        data={sampleData}
        config={{
          height: 300,
          showLabels: true,
          animated: true,
          duration: 1000,
        }}
      />
    </ChartContainer>
  );
}
```

#### Sample Pie Chart

**Example:** A sample pie chart

```tsx
// components/demo/charts/pie-chart/pie-chart-sample.tsx
import { PieChart } from '@/components/charts/pie-chart';
import { ChartContainer } from '@/components/charts/chart-container';
import React from 'react';

const sampleData = [
  { label: 'Mobile', value: 45 },
  { label: 'Desktop', value: 35 },
  { label: 'Tablet', value: 15 },
  { label: 'Other', value: 5 },
];

export function PieChartSample() {
  return (
    <ChartContainer
      title='Traffic Sources'
      description='Website traffic distribution by device type'
    >
      <PieChart
        data={sampleData}
        config={{
          height: 250,
          showLabels: true,
          animated: true,
          duration: 800,
        }}
      />
    </ChartContainer>
  );
}
```

#### Styled Pie Chart

**Example:** A customized pie chart with custom colors and styling

```tsx
// components/demo/charts/pie-chart/pie-chart-styled.tsx
import { PieChart } from '@/components/charts/pie-chart';
import { ChartContainer } from '@/components/charts/chart-container';
import { useColor } from '@/hooks/useColor';
import React from 'react';

export function PieChartStyled() {
  const primaryColor = useColor('primary');
  const successColor = useColor('green');
  const warningColor = useColor('orange');
  const errorColor = useColor('red');

  const styledData = [
    { label: 'Completed', value: 65, color: successColor },
    { label: 'In Progress', value: 20, color: primaryColor },
    { label: 'Pending', value: 10, color: warningColor },
    { label: 'Failed', value: 5, color: errorColor },
  ];

  return (
    <ChartContainer
      title='Project Status'
      description='Current project completion status overview'
    >
      <PieChart
        data={styledData}
        config={{
          height: 280,
          showLabels: true,
          animated: true,
          duration: 1200,
        }}
      />
    </ChartContainer>
  );
}
```

#### Large Pie Chart

**Example:** A pie chart with large dataset

```tsx
// components/demo/charts/pie-chart/pie-chart-large.tsx
import { PieChart } from '@/components/charts/pie-chart';
import { ChartContainer } from '@/components/charts/chart-container';
import React from 'react';

const largeData = [
  { label: 'North America', value: 35 },
  { label: 'Europe', value: 28 },
  { label: 'Asia Pacific', value: 22 },
  { label: 'Latin America', value: 8 },
  { label: 'Middle East', value: 4 },
  { label: 'Africa', value: 3 },
];

export function PieChartLarge() {
  return (
    <ChartContainer
      title='Global Revenue Distribution'
      description='Revenue breakdown by geographical regions'
    >
      <PieChart
        data={largeData}
        config={{
          height: 350,
          showLabels: true,
          animated: true,
          duration: 1500,
        }}
      />
    </ChartContainer>
  );
}
```

## API Reference

### PieChart

A customizable pie chart component with smooth animations and flexible styling. Perfect for displaying proportional data with emphasis on parts of a whole.

| Prop     | Type               | Default | Description                                   |
| -------- | ------------------ | ------- | --------------------------------------------- |
| `data`   | `ChartDataPoint[]` | -       | Array of data points to display on the chart. |
| `config` | `ChartConfig`      | `{}`    | Configuration object for chart appearance.    |
| `style`  | `ViewStyle`        | -       | Additional styles to apply to the chart.      |

### ChartDataPoint

| Prop    | Type     | Description                          |
| ------- | -------- | ------------------------------------ |
| `label` | `string` | The label for the data point.        |
| `value` | `number` | The value for the data point.        |
| `color` | `string` | Optional custom color for the slice. |

### ChartConfig

| Prop         | Type      | Default | Description                                       |
| ------------ | --------- | ------- | ------------------------------------------------- |
| `width`      | `number`  | -       | Fixed width of the chart (auto-sizes if omitted). |
| `height`     | `number`  | `200`   | Height of the chart.                              |
| `showLabels` | `boolean` | `true`  | Whether to show percentage labels on slices.      |
| `animated`   | `boolean` | `true`  | Whether to animate the chart on load.             |
| `duration`   | `number`  | `1000`  | Animation duration in milliseconds.               |

## Features

- **Circular Layout**: Displays data as slices of a circle for proportional visualization
- **Smooth Animations**: Built-in animations using React Native Reanimated
- **Responsive Design**: Automatically adapts to container width
- **Custom Colors**: Support for individual slice colors
- **Percentage Labels**: Shows percentage values on each slice
- **Legend Display**: Displays legend with colors and values below the chart
- **Theme Integration**: Uses theme colors for consistent styling
- **Auto-sizing**: Automatically calculates optimal size based on container

## Use Cases

Pie charts are particularly effective for:

- **Market Share**: Displaying market share distribution
- **Budget Breakdown**: Showing expense categories as percentages
- **Survey Results**: Visualizing response distributions
- **Demographics**: Displaying population segments
- **Resource Allocation**: Showing how resources are distributed
- **Progress Tracking**: Displaying completion vs remaining work

## Design Considerations

The circular layout of the PieChart makes it ideal for:

- **Proportional Data**: Best for showing parts of a whole
- **Limited Categories**: Works best with 2-8 categories
- **Percentage Focus**: Emphasizes relative proportions over absolute values
- **Quick Comparison**: Easy to see largest and smallest segments

## Accessibility

The PieChart component includes several accessibility features:

- Semantic SVG structure for screen readers
- Proper contrast ratios for visual elements
- Text labels for both percentages and categories
- Legend with colors and values
- Keyboard navigation support

## Performance

The component is optimized for performance:

- Uses React Native Reanimated for smooth 60fps animations
- Efficient SVG rendering with minimal re-renders
- Automatic cleanup of animation values
- Responsive layout calculations

## Styling

The component integrates with your theme system:

- Uses theme colors (`primary`, `blue`, `green`, `orange`, `purple`, `pink`) for default slice colors
- Uses white text for percentage labels on slices
- Supports custom colors per data point
- Consistent legend styling with theme colors

## Animation

The chart features smooth entry animations:

- Slices animate with opacity fade-in effect
- Configurable animation duration
- Can be disabled for instant rendering
- Uses React Native Reanimated for optimal performance

## Mathematical Calculations

The component automatically handles:

- **Percentage Calculations**: Converts values to percentages
- **Angle Calculations**: Converts percentages to slice angles
- **Arc Path Generation**: Creates proper SVG arc paths
- **Label Positioning**: Calculates optimal label positions within slices
- **Legend Generation**: Creates legend items with colors and values

## Best Practices

When using pie charts:

- **Limit Categories**: Keep to 2-8 categories for clarity
- **Order by Size**: Consider ordering slices by size for better readability
- **Use Contrasting Colors**: Ensure sufficient contrast between adjacent slices
- **Provide Legend**: Always include a legend for color identification
- **Consider Alternatives**: For many categories, consider using a bar chart instead
