# Stacked Area Chart

> A customizable stacked area chart component with smooth animations and gradient fills for visualizing multiple data series over time.

**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/stacked-area-chart
- Markdown: https://ui.ahmedbna.com/docs/charts/stacked-area-chart.md
- Structured JSON (props, usage, source, examples): https://ui.ahmedbna.com/r/ai/stacked-area-chart.json
- Install payload (source plus every file it imports): https://ui.ahmedbna.com/r/stacked-area-chart.json
- Install: `npx bna-ui add stacked-area-chart`
- npm dependencies: `react-native-reanimated`, `react-native-svg`, `react-native-worklets`
- Registry dependencies: `mode-provider`, `useColorScheme`, `colors`, `useColor`, `globals`
- Preview recording: https://demo.ahmedbna.com/0387-stacked-area-chart-demo.MOV

---

**Example:** A stacked area chart with smooth animations and gradient fills

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

const sampleData = [
  { x: 1, y: [20, 30, 25], label: 'Jan' },
  { x: 2, y: [25, 35, 30], label: 'Feb' },
  { x: 3, y: [30, 40, 35], label: 'Mar' },
  { x: 4, y: [35, 45, 40], label: 'Apr' },
  { x: 5, y: [40, 50, 45], label: 'May' },
  { x: 6, y: [45, 55, 50], label: 'Jun' },
];

const categories = ['Product A', 'Product B', 'Product C'];

export function StackedAreaChartDemo() {
  return (
    <ChartContainer
      title='Monthly Revenue by Product'
      description='Revenue breakdown showing contribution of each product line'
    >
      <StackedAreaChart
        data={sampleData}
        categories={categories}
        config={{
          height: 300,
          showLabels: true,
          showGrid: true,
          animated: true,
          duration: 1000,
        }}
      />
    </ChartContainer>
  );
}
```

## Installation

### CLI

```bash
npx bna-ui add stacked-area-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/stacked-area-chart.tsx
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, {
  Defs,
  G,
  Line,
  LinearGradient,
  Path,
  Stop,
  Text as SvgText,
} from 'react-native-svg';

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

type AnimatedAreaProps = {
  d: string;
  fill: string;
  stroke: string;
  opacityFactor: number;
  animationProgress: SharedValue<number>;
};

// Per-item hook must live in its own mounted subcomponent, not in the
// parent's Array.from(...) render loop — calling useAnimatedProps per loop
// iteration violates Rules of Hooks the moment seriesCount changes.
const AnimatedArea = React.memo(
  ({
    d,
    fill,
    stroke,
    opacityFactor,
    animationProgress,
  }: AnimatedAreaProps) => {
    const areaAnimatedProps = useAnimatedProps(() => ({
      opacity: animationProgress.value * opacityFactor,
    }));

    return (
      <AnimatedPath
        d={d}
        fill={fill}
        stroke={stroke}
        strokeWidth={1}
        animatedProps={areaAnimatedProps}
      />
    );
  }
);

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

export interface StackedAreaDataPoint {
  x: number;
  y: number[];
  label?: string;
}

type Props = {
  data: StackedAreaDataPoint[];
  colors?: string[];
  config?: ChartConfig;
  style?: ViewStyle;
  categories?: string[];
};

// Utility function to create smooth path
const createSmoothPath = (points: { x: number; y: number }[]): string => {
  if (points.length === 0) return '';

  let path = `M${points[0].x},${points[0].y}`;

  for (let i = 1; i < points.length; i++) {
    const prevPoint = points[i - 1];
    const currentPoint = points[i];
    const cpx = (prevPoint.x + currentPoint.x) / 2;
    const cpy = prevPoint.y;
    path += ` Q${cpx},${cpy} ${currentPoint.x},${currentPoint.y}`;
  }

  return path;
};

const createAreaPath = (
  topPoints: { x: number; y: number }[],
  bottomPoints: { x: number; y: number }[]
): string => {
  if (topPoints.length === 0 || bottomPoints.length === 0) return '';

  // Create the top curve
  const topPath = createSmoothPath(topPoints);

  // Create the bottom curve (reversed order for proper path closure)
  const reversedBottomPoints = [...bottomPoints].reverse();

  // Start the area path with the top curve
  let areaPath = topPath;

  // Add line to the last bottom point
  areaPath += ` L${reversedBottomPoints[0].x},${reversedBottomPoints[0].y}`;

  // Add the bottom curve
  if (reversedBottomPoints.length > 1) {
    for (let i = 1; i < reversedBottomPoints.length; i++) {
      const prevPoint = reversedBottomPoints[i - 1];
      const currentPoint = reversedBottomPoints[i];
      const cpx = (prevPoint.x + currentPoint.x) / 2;
      const cpy = prevPoint.y;
      areaPath += ` Q${cpx},${cpy} ${currentPoint.x},${currentPoint.y}`;
    }
  }

  // Close the path
  areaPath += ' Z';

  return areaPath;
};

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

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

  const chartWidth = containerWidth || config.width || 300;

  const primaryColor = useColor('primary');
  const mutedColor = useColor('mutedForeground');

  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;

  // Calculate stacked totals and max value
  const stackedData = data.map((point) => {
    const cumulative = point.y.reduce((acc, val, idx) => {
      acc.push((acc[acc.length - 1] || 0) + val);
      return acc;
    }, [] as number[]);
    return { ...point, cumulative };
  });

  const maxValue = Math.max(
    ...stackedData.map((d) => Math.max(...d.cumulative))
  );
  const seriesCount = data[0]?.y.length || 0;

  const innerChartWidth = chartWidth - padding * 2;
  const chartHeight = height - padding * 2;

  // Default colors if not provided
  const defaultColors = [
    primaryColor,
    '#8884d8',
    '#82ca9d',
    '#ffc658',
    '#ff7300',
    '#00ff00',
    '#0088fe',
  ];

  // Cycle the default palette via modulo past its length instead of
  // leaving `undefined` colors for series beyond it.
  const seriesColors = Array.from({ length: seriesCount }, (_, i) =>
    i < colors.length
      ? colors[i]
      : defaultColors[(i - colors.length) % defaultColors.length]
  );

  return (
    <View
      style={[{ width: '100%', height }, style]}
      onLayout={handleLayout}
      accessibilityRole='image'
      accessibilityLabel={`Stacked area chart with ${data.length} data points across ${seriesCount} series, maximum value ${Math.round(maxValue)}`}
    >
      <Svg width={chartWidth} height={height}>
        <Defs>
          {seriesColors.map((color, index) => (
            <LinearGradient
              key={`gradient-${index}`}
              id={`areaGradient-${index}`}
              x1='0%'
              y1='0%'
              x2='0%'
              y2='100%'
            >
              <Stop offset='0%' stopColor={color} stopOpacity='0.8' />
              <Stop offset='100%' stopColor={color} stopOpacity='0.3' />
            </LinearGradient>
          ))}
        </Defs>

        {/* Grid lines */}
        {showGrid && (
          <G>
            {[0, 0.25, 0.5, 0.75, 1].map((ratio, index) => (
              <Line
                key={`grid-${index}`}
                x1={padding}
                y1={padding + ratio * chartHeight}
                x2={chartWidth - padding}
                y2={padding + ratio * chartHeight}
                stroke={mutedColor}
                strokeWidth={0.5}
                opacity={0.3}
              />
            ))}
          </G>
        )}

        {/* Stacked areas */}
        {Array.from({ length: seriesCount }, (_, seriesIndex) => {
          const topPoints = stackedData.map((point, pointIndex) => ({
            x: padding + (pointIndex / (data.length - 1)) * innerChartWidth,
            y:
              padding +
              ((maxValue - point.cumulative[seriesIndex]) / maxValue) *
                chartHeight,
          }));

          // All areas extend from x-axis (y=0) to their cumulative value
          const bottomPoints = stackedData.map((point, pointIndex) => ({
            x: padding + (pointIndex / (data.length - 1)) * innerChartWidth,
            y: height - padding, // Always extend to x-axis (y=0 in data terms)
          }));

          const areaPath = createAreaPath(topPoints, bottomPoints);

          return (
            <AnimatedArea
              key={`area-${seriesIndex}`}
              d={areaPath}
              fill={`url(#areaGradient-${seriesIndex})`}
              stroke={seriesColors[seriesIndex]}
              opacityFactor={seriesIndex === 0 ? 1 : 0.7} // Make upper areas slightly transparent
              animationProgress={animationProgress}
            />
          );
        })}

        {/* Labels */}
        {showLabels && (
          <G>
            {data.map((point, index) => (
              <SvgText
                key={`label-${index}`}
                x={padding + (index / (data.length - 1)) * innerChartWidth}
                y={height - 5}
                textAnchor='middle'
                fontSize={12}
                fill={mutedColor}
              >
                {point.label || point.x.toString()}
              </SvgText>
            ))}
          </G>
        )}

        {/* Legend */}
        {categories.length > 0 && (
          <G>
            {categories.map((category, index) => (
              <G key={`legend-${index}`}>
                <Path
                  d={`M${padding + index * 80},${padding - 15} L${
                    padding + index * 80 + 15
                  },${padding - 15}`}
                  stroke={seriesColors[index]}
                  strokeWidth={3}
                />
                <SvgText
                  x={padding + index * 80 + 20}
                  y={padding - 10}
                  fontSize={11}
                  fill={mutedColor}
                >
                  {category}
                </SvgText>
              </G>
            ))}
          </G>
        )}
      </Svg>
    </View>
  );
};
```

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

## Usage

```tsx
import { StackedAreaChart } from '@/components/charts/stacked-area-chart';
```

```tsx
const data = [
  { x: 1, y: [20, 30, 40], label: 'Jan' },
  { x: 2, y: [25, 35, 45], label: 'Feb' },
  { x: 3, y: [30, 40, 50], label: 'Mar' },
  { x: 4, y: [35, 45, 55], label: 'Apr' },
];

const categories = ['Series 1', 'Series 2', 'Series 3'];

<StackedAreaChart
  data={data}
  categories={categories}
  config={{
    height: 300,
    showLabels: true,
    animated: true,
  }}
/>;
```

## Examples

#### Basic Stacked Area Chart

**Example:** A stacked area chart with smooth animations and gradient fills

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

const sampleData = [
  { x: 1, y: [20, 30, 25], label: 'Jan' },
  { x: 2, y: [25, 35, 30], label: 'Feb' },
  { x: 3, y: [30, 40, 35], label: 'Mar' },
  { x: 4, y: [35, 45, 40], label: 'Apr' },
  { x: 5, y: [40, 50, 45], label: 'May' },
  { x: 6, y: [45, 55, 50], label: 'Jun' },
];

const categories = ['Product A', 'Product B', 'Product C'];

export function StackedAreaChartDemo() {
  return (
    <ChartContainer
      title='Monthly Revenue by Product'
      description='Revenue breakdown showing contribution of each product line'
    >
      <StackedAreaChart
        data={sampleData}
        categories={categories}
        config={{
          height: 300,
          showLabels: true,
          showGrid: true,
          animated: true,
          duration: 1000,
        }}
      />
    </ChartContainer>
  );
}
```

#### Sample Stacked Area Chart

**Example:** A sample stacked area chart with revenue data

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

const sampleData = [
  { x: 1, y: [45, 55, 35, 25], label: 'Q1' },
  { x: 2, y: [50, 60, 40, 30], label: 'Q2' },
  { x: 3, y: [55, 65, 45, 35], label: 'Q3' },
  { x: 4, y: [60, 70, 50, 40], label: 'Q4' },
  { x: 5, y: [65, 75, 55, 45], label: 'Q1' },
  { x: 6, y: [70, 80, 60, 50], label: 'Q2' },
];

const categories = ['Direct Sales', 'Online', 'Retail', 'Partner'];

export function StackedAreaChartSample() {
  return (
    <ChartContainer
      title='Sales Channel Performance'
      description='Quarterly performance across different sales channels'
    >
      <StackedAreaChart
        data={sampleData}
        categories={categories}
        colors={['#8884d8', '#82ca9d', '#ffc658', '#ff7300']}
        config={{
          height: 280,
          showLabels: true,
          showGrid: true,
          animated: true,
          duration: 1200,
        }}
      />
    </ChartContainer>
  );
}
```

#### Styled Stacked Area Chart

**Example:** A customized stacked area chart with custom colors and styling

```tsx
// components/demo/charts/stacked-area-chart/stacked-area-chart-styled.tsx
import { StackedAreaChart } from '@/components/charts/stacked-area-chart';
import { ChartContainer } from '@/components/charts/chart-container';
import React from 'react';

const sampleData = [
  { x: 1, y: [120, 80, 60], label: 'Week 1' },
  { x: 2, y: [140, 90, 70], label: 'Week 2' },
  { x: 3, y: [160, 100, 80], label: 'Week 3' },
  { x: 4, y: [180, 110, 90], label: 'Week 4' },
  { x: 5, y: [200, 120, 100], label: 'Week 5' },
  { x: 6, y: [220, 130, 110], label: 'Week 6' },
  { x: 7, y: [240, 140, 120], label: 'Week 7' },
  { x: 8, y: [260, 150, 130], label: 'Week 8' },
];

const categories = ['Premium', 'Standard', 'Basic'];

export function StackedAreaChartStyled() {
  return (
    <ChartContainer
      title='Subscription Tiers Growth'
      description='Weekly growth in subscription tiers with custom styling'
    >
      <StackedAreaChart
        data={sampleData}
        categories={categories}
        colors={['#6366f1', '#8b5cf6', '#ec4899']}
        config={{
          height: 320,
          padding: 30,
          showLabels: true,
          showGrid: true,
          animated: true,
          duration: 1500,
        }}
        style={{
          backgroundColor: '#f8fafc',
          borderRadius: 12,
          padding: 16,
        }}
      />
    </ChartContainer>
  );
}
```

#### Large Stacked Area Chart

**Example:** A stacked area chart with large dataset

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

const generateLargeDataset = () => {
  const data = [];
  const months = [
    'Jan',
    'Feb',
    'Mar',
    'Apr',
    'May',
    'Jun',
    'Jul',
    'Aug',
    'Sep',
    'Oct',
    'Nov',
    'Dec',
  ];

  for (let i = 0; i < 12; i++) {
    data.push({
      x: i + 1,
      y: [
        Math.floor(Math.random() * 50) + 100, // Desktop
        Math.floor(Math.random() * 80) + 120, // Mobile
        Math.floor(Math.random() * 40) + 60, // Tablet
        Math.floor(Math.random() * 30) + 40, // TV
        Math.floor(Math.random() * 20) + 20, // Watch
      ],
      label: months[i],
    });
  }

  return data;
};

const sampleData = generateLargeDataset();
const categories = ['Desktop', 'Mobile', 'Tablet', 'TV', 'Watch'];

export function StackedAreaChartLarge() {
  return (
    <ChartContainer
      title='Device Usage Analytics'
      description='Monthly active users across different device types'
    >
      <StackedAreaChart
        data={sampleData}
        categories={categories}
        colors={['#0ea5e9', '#10b981', '#f59e0b', '#ef4444', '#8b5cf6']}
        config={{
          height: 350,
          padding: 25,
          showLabels: true,
          showGrid: true,
          animated: true,
          duration: 2000,
        }}
      />
    </ChartContainer>
  );
}
```

## API Reference

### StackedAreaChart

A customizable stacked area chart component with smooth animations and gradient fills. Perfect for displaying multiple data series over time or categories, showing both individual values and cumulative totals.

| Prop         | Type                     | Default | Description                                   |
| ------------ | ------------------------ | ------- | --------------------------------------------- |
| `data`       | `StackedAreaDataPoint[]` | -       | Array of data points to display on the chart. |
| `colors`     | `string[]`               | `[]`    | Array of colors for each data series.         |
| `config`     | `ChartConfig`            | `{}`    | Configuration object for chart appearance.    |
| `style`      | `ViewStyle`              | -       | Additional styles to apply to the chart.      |
| `categories` | `string[]`               | `[]`    | Array of category names for the legend.       |

### StackedAreaDataPoint

| Prop    | Type       | Description                                  |
| ------- | ---------- | -------------------------------------------- |
| `x`     | `number`   | The x-axis value for the data point.         |
| `y`     | `number[]` | Array of y-values for each series at this x. |
| `label` | `string`   | Optional label for the data point.           |

### ChartConfig

| Prop         | Type      | Default | Description                                       |
| ------------ | --------- | ------- | ------------------------------------------------- |
| `width`      | `number`  | -       | Fixed width of the chart (auto-sizes if omitted). |
| `height`     | `number`  | `200`   | Height of the chart.                              |
| `padding`    | `number`  | `20`    | Padding around the chart.                         |
| `showGrid`   | `boolean` | `true`  | Whether to show grid lines.                       |
| `showLabels` | `boolean` | `true`  | Whether to show labels for data points.           |
| `animated`   | `boolean` | `true`  | Whether to animate the chart on load.             |
| `duration`   | `number`  | `1000`  | Animation duration in milliseconds.               |

## Features

- **Stacked Areas**: Displays multiple data series as stacked areas
- **Smooth Curves**: Uses quadratic curves for smooth area transitions
- **Gradient Fills**: Beautiful gradient fills for each area
- **Smooth Animations**: Built-in animations using React Native Reanimated
- **Responsive Design**: Automatically adapts to container width
- **Custom Colors**: Support for custom color palettes
- **Grid Lines**: Optional grid lines for better readability
- **Legend Support**: Built-in legend with category names
- **Label Display**: Shows labels for data points on x-axis

## Use Cases

Stacked area charts are particularly effective for:

- **Time Series Data**: Showing how different categories contribute to a total over time
- **Revenue Analysis**: Displaying revenue streams from different sources
- **Performance Metrics**: Tracking multiple KPIs simultaneously
- **Market Share**: Visualizing market share changes over time
- **Resource Allocation**: Showing how resources are distributed across categories
- **Survey Results**: Displaying response distributions over time

## Design Considerations

The stacked area chart is ideal for:

- **Part-to-Whole Relationships**: Showing how individual parts contribute to the whole
- **Trend Analysis**: Identifying trends in both individual series and total values
- **Comparative Analysis**: Comparing the relative size of different categories
- **Cumulative Data**: Displaying cumulative values effectively

## Accessibility

The StackedAreaChart component includes several accessibility features:

- Semantic SVG structure for screen readers
- Proper contrast ratios for visual elements
- Text labels for data points and categories
- Legend with clear category identification
- Grid lines for better value estimation

## 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
- Optimized path calculations for smooth curves

## Styling

The component integrates with your theme system:

- Uses `primary` color from theme for default series color
- Uses `mutedForeground` color for labels and grid lines
- Supports custom colors for each series
- Gradient fills with opacity transitions
- Responsive layout calculations

## Animation

The chart features smooth entry animations:

- Areas animate with fade-in effect
- Configurable animation duration
- Can be disabled for instant rendering
- Uses React Native Reanimated for optimal performance
- Staggered animations for multiple series

## Data Structure

The component expects data in a specific format:

```tsx
// Each data point represents a position on the x-axis
// with multiple y-values for different series
const data = [
  { x: 1, y: [10, 20, 30], label: 'Q1' },
  { x: 2, y: [15, 25, 35], label: 'Q2' },
  { x: 3, y: [12, 22, 32], label: 'Q3' },
  { x: 4, y: [18, 28, 38], label: 'Q4' },
];
```

## Color Customization

You can customize colors for each series:

```tsx
const colors = ['#8884d8', '#82ca9d', '#ffc658', '#ff7300'];

<StackedAreaChart
  data={data}
  colors={colors}
  categories={['Series A', 'Series B', 'Series C', 'Series D']}
/>;
```

## Grid Configuration

Grid lines can be customized:

```tsx
<StackedAreaChart
  data={data}
  config={{
    showGrid: true,
    // Grid lines are drawn at 0%, 25%, 50%, 75%, and 100% of the chart height
  }}
/>
```
