# Stacked Bar Chart

> A customizable stacked bar chart component with smooth animations, support for both horizontal and vertical layouts, 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/stacked-bar-chart
- Markdown: https://ui.ahmedbna.com/docs/charts/stacked-bar-chart.md
- Structured JSON (props, usage, source, examples): https://ui.ahmedbna.com/r/ai/stacked-bar-chart.json
- Install payload (source plus every file it imports): https://ui.ahmedbna.com/r/stacked-bar-chart.json
- Install: `npx bna-ui add stacked-bar-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/0391-stacked-bar-chart-demo.MOV

---

**Example:** A stacked bar chart with smooth animations

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

const sampleData = [
  { label: 'Q1', values: [120, 98, 86] },
  { label: 'Q2', values: [140, 110, 95] },
  { label: 'Q3', values: [160, 130, 105] },
  { label: 'Q4', values: [180, 150, 115] },
];

const categories = ['Sales', 'Marketing', 'Support'];

export function StackedBarChartDemo() {
  return (
    <ChartContainer
      title='Quarterly Performance'
      description='Revenue breakdown by department across quarters'
    >
      <StackedBarChart
        data={sampleData}
        categories={categories}
        config={{
          height: 300,
          showLabels: true,
          showGrid: true,
          animated: true,
          duration: 1000,
        }}
      />
    </ChartContainer>
  );
}
```

## Installation

### CLI

```bash
npx bna-ui add stacked-bar-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-bar-chart.tsx
// components/charts/stacked-bar-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, { G, Line, Rect, Text as SvgText } from 'react-native-svg';

// Animated SVG Components
const AnimatedRect = Animated.createAnimatedComponent(Rect);

type AnimatedHorizontalSegmentProps = {
  x: number;
  y: number;
  barHeight: number;
  segmentWidth: number;
  fill: string;
  animationProgress: SharedValue<number>;
};

// Per-item hooks must live in their own mounted subcomponent, not in the
// parent's nested item×value .map() body — calling useAnimatedProps per
// loop iteration violates Rules of Hooks the moment data changes. Two
// subcomponents here (horizontal vs vertical branch) since the two modes
// animate different SVG attributes.
const AnimatedHorizontalSegment = React.memo(
  ({
    x,
    y,
    barHeight,
    segmentWidth,
    fill,
    animationProgress,
  }: AnimatedHorizontalSegmentProps) => {
    const segmentAnimatedProps = useAnimatedProps(() => ({
      width: animationProgress.value * segmentWidth,
    }));

    return (
      <AnimatedRect
        x={x}
        y={y}
        height={barHeight}
        fill={fill}
        rx={2}
        animatedProps={segmentAnimatedProps}
      />
    );
  }
);

type AnimatedVerticalSegmentProps = {
  x: number;
  barWidth: number;
  segmentHeight: number;
  bottomY: number;
  fill: string;
  animationProgress: SharedValue<number>;
};

const AnimatedVerticalSegment = React.memo(
  ({
    x,
    barWidth,
    segmentHeight,
    bottomY,
    fill,
    animationProgress,
  }: AnimatedVerticalSegmentProps) => {
    const segmentAnimatedProps = useAnimatedProps(() => ({
      height: animationProgress.value * segmentHeight,
      y: bottomY - animationProgress.value * segmentHeight,
    }));

    return (
      <AnimatedRect
        x={x}
        width={barWidth}
        fill={fill}
        rx={2}
        animatedProps={segmentAnimatedProps}
      />
    );
  }
);

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

export interface StackedBarDataPoint {
  label: string;
  values: number[];
}

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

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

  const {
    height = 200,
    padding = 20,
    showLabels = true,
    showGrid = true,
    animated = true,
    duration = 800,
  } = 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;

  const maxValue = Math.max(
    ...data.map((d) => d.values.reduce((sum, val) => sum + val, 0))
  );
  const seriesCount = data[0]?.values.length || 0;

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

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

  // 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]
  );

  if (horizontal) {
    // Horizontal stacked bars
    const barHeight = (chartHeight / data.length) * 0.8;
    const barSpacing = (chartHeight / data.length) * 0.2;

    return (
      <View
        style={[{ width: '100%', height }, style]}
        onLayout={handleLayout}
        accessibilityRole='image'
        accessibilityLabel={`Horizontal stacked bar chart with ${data.length} bars across ${seriesCount} series, maximum total ${Math.round(maxValue)}`}
      >
        <Svg width={chartWidth} height={height}>
          {/* Grid lines */}
          {showGrid && (
            <G>
              {[0, 0.25, 0.5, 0.75, 1].map((ratio, index) => (
                <Line
                  key={`grid-${index}`}
                  x1={padding + ratio * innerChartWidth}
                  y1={padding}
                  x2={padding + ratio * innerChartWidth}
                  y2={height - padding}
                  stroke={mutedColor}
                  strokeWidth={0.5}
                  opacity={0.3}
                />
              ))}
            </G>
          )}

          {data.map((item, itemIndex) => {
            let cumulativeWidth = 0;
            const y =
              padding + itemIndex * (barHeight + barSpacing) + barSpacing / 2;

            return (
              <G key={`bar-group-${itemIndex}`}>
                {item.values.map((value, valueIndex) => {
                  const segmentWidth = (value / maxValue) * innerChartWidth;
                  const x = padding + cumulativeWidth;

                  cumulativeWidth += segmentWidth;

                  return (
                    <AnimatedHorizontalSegment
                      key={`segment-${itemIndex}-${valueIndex}`}
                      x={x}
                      y={y}
                      barHeight={barHeight}
                      segmentWidth={segmentWidth}
                      fill={seriesColors[valueIndex]}
                      animationProgress={animationProgress}
                    />
                  );
                })}

                {/* Bar labels */}
                {showLabels && (
                  <SvgText
                    x={padding - 10}
                    y={y + barHeight / 2 + 4}
                    textAnchor='end'
                    fontSize={12}
                    fill={mutedColor}
                  >
                    {item.label}
                  </SvgText>
                )}
              </G>
            );
          })}

          {/* Legend */}
          {categories.length > 0 && (
            <G>
              {categories.map((category, index) => (
                <G key={`legend-${index}`}>
                  <Rect
                    x={padding + index * 80}
                    y={height - padding + 10}
                    width={12}
                    height={8}
                    fill={seriesColors[index]}
                    rx={2}
                  />
                  <SvgText
                    x={padding + index * 80 + 18}
                    y={height - padding + 18}
                    fontSize={11}
                    fill={mutedColor}
                  >
                    {category}
                  </SvgText>
                </G>
              ))}
            </G>
          )}
        </Svg>
      </View>
    );
  }

  // Vertical stacked bars
  const barWidth = (innerChartWidth / data.length) * 0.8;
  const barSpacing = (innerChartWidth / data.length) * 0.2;

  return (
    <View
      style={[{ width: '100%', height }, style]}
      onLayout={handleLayout}
      accessibilityRole='image'
      accessibilityLabel={`Stacked bar chart with ${data.length} bars across ${seriesCount} series, maximum total ${Math.round(maxValue)}`}
    >
      <Svg width={chartWidth} height={height}>
        {/* 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>
        )}

        {data.map((item, itemIndex) => {
          let cumulativeHeight = 0;
          const x =
            padding + itemIndex * (barWidth + barSpacing) + barSpacing / 2;
          const totalValue = item.values.reduce((sum, val) => sum + val, 0);

          return (
            <G key={`bar-group-${itemIndex}`}>
              {item.values.map((value, valueIndex) => {
                const segmentHeight = (value / maxValue) * chartHeight;
                const bottomY = height - padding - cumulativeHeight;

                cumulativeHeight += segmentHeight;

                return (
                  <AnimatedVerticalSegment
                    key={`segment-${itemIndex}-${valueIndex}`}
                    x={x}
                    barWidth={barWidth}
                    segmentHeight={segmentHeight}
                    bottomY={bottomY}
                    fill={seriesColors[valueIndex]}
                    animationProgress={animationProgress}
                  />
                );
              })}

              {/* Bar labels */}
              {showLabels && (
                <SvgText
                  x={x + barWidth / 2}
                  y={height - 5}
                  textAnchor='middle'
                  fontSize={12}
                  fill={mutedColor}
                >
                  {item.label}
                </SvgText>
              )}
            </G>
          );
        })}

        {/* Legend */}
        {categories.length > 0 && (
          <G>
            {categories.map((category, index) => (
              <G key={`legend-${index}`}>
                <Rect
                  x={padding + index * 80}
                  y={padding - 25}
                  width={12}
                  height={8}
                  fill={seriesColors[index]}
                  rx={2}
                />
                <SvgText
                  x={padding + index * 80 + 18}
                  y={padding - 17}
                  fontSize={11}
                  fill={mutedColor}
                >
                  {category}
                </SvgText>
              </G>
            ))}
          </G>
        )}
      </Svg>
    </View>
  );
};
```

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

## Usage

```tsx
import { StackedBarChart } from '@/components/charts/stacked-bar-chart';
```

```tsx
const data = [
  { label: 'Q1', values: [120, 98, 86] },
  { label: 'Q2', values: [140, 110, 95] },
  { label: 'Q3', values: [160, 130, 105] },
  { label: 'Q4', values: [180, 150, 115] },
];

const categories = ['Sales', 'Marketing', 'Support'];

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

## Examples

#### Basic Stacked Bar Chart

**Example:** A stacked bar chart with smooth animations

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

const sampleData = [
  { label: 'Q1', values: [120, 98, 86] },
  { label: 'Q2', values: [140, 110, 95] },
  { label: 'Q3', values: [160, 130, 105] },
  { label: 'Q4', values: [180, 150, 115] },
];

const categories = ['Sales', 'Marketing', 'Support'];

export function StackedBarChartDemo() {
  return (
    <ChartContainer
      title='Quarterly Performance'
      description='Revenue breakdown by department across quarters'
    >
      <StackedBarChart
        data={sampleData}
        categories={categories}
        config={{
          height: 300,
          showLabels: true,
          showGrid: true,
          animated: true,
          duration: 1000,
        }}
      />
    </ChartContainer>
  );
}
```

#### Horizontal Stacked Bar Chart

**Example:** A horizontal stacked bar chart

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

const sampleData = [
  { label: 'Product A', values: [45, 30, 25] },
  { label: 'Product B', values: [60, 40, 35] },
  { label: 'Product C', values: [55, 35, 30] },
  { label: 'Product D', values: [70, 45, 40] },
  { label: 'Product E', values: [50, 32, 28] },
];

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

export function StackedBarChartHorizontal() {
  return (
    <ChartContainer
      title='Product Sales by Channel'
      description='Sales distribution across different channels'
    >
      <StackedBarChart
        data={sampleData}
        categories={categories}
        horizontal={true}
        config={{
          height: 350,
          showLabels: true,
          showGrid: true,
          animated: true,
          duration: 1200,
        }}
      />
    </ChartContainer>
  );
}
```

#### Styled Stacked Bar Chart

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

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

const sampleData = [
  { label: 'Mobile', values: [85, 45, 30, 20] },
  { label: 'Desktop', values: [120, 80, 50, 35] },
  { label: 'Tablet', values: [65, 35, 25, 15] },
  { label: 'Smart TV', values: [40, 20, 15, 10] },
];

const categories = ['Chrome', 'Safari', 'Firefox', 'Edge'];

// Custom colors for different browsers
const customColors = [
  '#4285F4', // Chrome blue
  '#FF9500', // Safari orange
  '#FF6611', // Firefox orange
  '#0078D4', // Edge blue
];

export function StackedBarChartStyled() {
  return (
    <ChartContainer
      title='Browser Usage by Device'
      description='Browser market share across different device types'
    >
      <StackedBarChart
        data={sampleData}
        categories={categories}
        colors={customColors}
        config={{
          height: 320,
          showLabels: true,
          showGrid: true,
          animated: true,
          duration: 1500,
          padding: 30,
        }}
      />
    </ChartContainer>
  );
}
```

#### Large Dataset Stacked Bar Chart

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

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

const sampleData = [
  { label: 'Jan', values: [220, 180, 140, 100, 80] },
  { label: 'Feb', values: [240, 190, 150, 110, 85] },
  { label: 'Mar', values: [260, 200, 160, 120, 90] },
  { label: 'Apr', values: [280, 210, 170, 130, 95] },
  { label: 'May', values: [300, 220, 180, 140, 100] },
  { label: 'Jun', values: [320, 230, 190, 150, 105] },
  { label: 'Jul', values: [340, 240, 200, 160, 110] },
  { label: 'Aug', values: [360, 250, 210, 170, 115] },
  { label: 'Sep', values: [380, 260, 220, 180, 120] },
  { label: 'Oct', values: [400, 270, 230, 190, 125] },
  { label: 'Nov', values: [420, 280, 240, 200, 130] },
  { label: 'Dec', values: [440, 290, 250, 210, 135] },
];

const categories = ['Enterprise', 'Professional', 'Standard', 'Basic', 'Free'];

export function StackedBarChartLarge() {
  return (
    <ChartContainer
      title='Annual Subscription Revenue'
      description='Monthly recurring revenue breakdown by subscription tier'
    >
      <StackedBarChart
        data={sampleData}
        categories={categories}
        config={{
          height: 400,
          showLabels: true,
          showGrid: true,
          animated: true,
          duration: 2000,
          padding: 25,
        }}
      />
    </ChartContainer>
  );
}
```

## API Reference

### StackedBarChart

A customizable stacked bar chart component with smooth animations and flexible styling. Perfect for displaying multiple data series stacked on top of each other, supporting both vertical and horizontal orientations.

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

### StackedBarDataPoint

| Prop     | Type       | Description                                |
| -------- | ---------- | ------------------------------------------ |
| `label`  | `string`   | The label for the data point.              |
| `values` | `number[]` | Array of values for each stack in the bar. |

### 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.                         |
| `showLabels` | `boolean` | `true`  | Whether to show labels for bars.                  |
| `showGrid`   | `boolean` | `true`  | Whether to show grid lines.                       |
| `animated`   | `boolean` | `true`  | Whether to animate the chart on load.             |
| `duration`   | `number`  | `800`   | Animation duration in milliseconds.               |

## Features

- **Dual Orientation**: Supports both vertical and horizontal bar layouts
- **Smooth Animations**: Built-in animations using React Native Reanimated
- **Responsive Design**: Automatically adapts to container width
- **Custom Colors**: Support for custom color schemes per data series
- **Legend Support**: Built-in legend with category labels
- **Grid Lines**: Optional grid lines for better value reading
- **Theme Integration**: Uses theme colors for consistent styling
- **Rounded Corners**: Aesthetic rounded bar corners

## Use Cases

Stacked bar charts are particularly effective for:

- **Multi-Category Comparison**: Comparing multiple data series across categories
- **Part-to-Whole Analysis**: Showing how individual components contribute to totals
- **Time Series Data**: Displaying data evolution over time periods
- **Budget Breakdown**: Visualizing spending across different categories and subcategories
- **Performance Metrics**: Showing multiple KPIs stacked for comparison
- **Survey Results**: Displaying response distributions across multiple questions

## Design Considerations

The StackedBarChart component offers flexibility for different use cases:

- **Vertical Layout**: Better for time series data and when you have short category labels
- **Horizontal Layout**: Ideal for long category names and when you need more space for labels
- **Color Coordination**: Uses a default color palette but supports custom colors
- **Legend Positioning**: Automatically positions legend based on orientation

## Accessibility

The StackedBarChart component includes several accessibility features:

- Semantic SVG structure for screen readers
- Proper contrast ratios for visual elements
- Text labels for both categories and values
- Legend support for data series identification
- Supports dynamic text sizing
- 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
- Optimized for large datasets

## Styling

The component integrates with your theme system:

- Uses `primary` color from theme for default bar colors
- Uses `mutedForeground` color for labels and text
- Supports custom colors per data series
- Rounded corners with consistent border radius
- Grid lines with subtle opacity

## Animation

The chart features smooth entry animations:

- Bars animate from 0 size to full size
- Stacks animate sequentially for visual appeal
- Configurable animation duration
- Can be disabled for instant rendering
- Uses React Native Reanimated for optimal performance

## Data Structure

The component expects data in a specific format:

```tsx
// Each data point contains multiple values for stacking
const data = [
  { label: 'Category A', values: [10, 20, 30] }, // Stack of 3 values
  { label: 'Category B', values: [15, 25, 35] }, // Stack of 3 values
];

// Categories define what each stack represents
const categories = ['Series 1', 'Series 2', 'Series 3'];
```

## Layout Modes

### Vertical Layout (Default)

- Bars extend upward from the bottom
- Labels positioned below bars
- Legend positioned at the top
- Best for timeline data and short labels

### Horizontal Layout

- Bars extend rightward from the left
- Labels positioned to the left of bars
- Legend positioned at the bottom
- Best for long category names and mobile screens

## Color Management

The component provides flexible color options:

- **Default Colors**: Uses a predefined palette with theme integration
- **Custom Colors**: Pass an array of colors matching your data series
- **Theme Colors**: Automatically uses primary theme color as the base
- **Consistent Mapping**: Same color always represents the same data series
