# Bubble Chart

> A customizable bubble chart component with animations and size mapping.

**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/bubble-chart
- Markdown: https://ui.ahmedbna.com/docs/charts/bubble-chart.md
- Structured JSON (props, usage, source, examples): https://ui.ahmedbna.com/r/ai/bubble-chart.json
- Install payload (source plus every file it imports): https://ui.ahmedbna.com/r/bubble-chart.json
- Install: `npx bna-ui add bubble-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/0337-bubble-chart-demo.mov

---

**Example:** A basic bubble chart with animated bubbles and grid lines

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

const sampleData = [
  { x: 10, y: 20, size: 15, label: 'A' },
  { x: 25, y: 30, size: 25, label: 'B' },
  { x: 40, y: 15, size: 30, label: 'C' },
  { x: 35, y: 45, size: 20, label: 'D' },
  { x: 60, y: 25, size: 18, label: 'E' },
  { x: 50, y: 40, size: 22, label: 'F' },
  { x: 15, y: 35, size: 28, label: 'G' },
  { x: 70, y: 50, size: 16, label: 'H' },
];

export function BubbleChartDemo() {
  return (
    <ChartContainer
      title='Performance vs Efficiency'
      description='Team performance metrics with bubble sizes representing team size'
    >
      <BubbleChart
        data={sampleData}
        config={{
          height: 300,
          showGrid: true,
          showLabels: true,
          animated: true,
          duration: 1200,
        }}
      />
    </ChartContainer>
  );
}
```

## Installation

### CLI

```bash
npx bna-ui add bubble-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/bubble-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,
  withDelay,
  withSpring,
  withTiming,
} from 'react-native-reanimated';
import Svg, { Circle, G, Line, Text as SvgText } from 'react-native-svg';

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

type AnimatedBubbleProps = {
  cx: number;
  cy: number;
  radius: number;
  fill: string;
  index: number;
  animationProgress: SharedValue<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 AnimatedBubble = React.memo(
  ({ cx, cy, radius, fill, index, animationProgress }: AnimatedBubbleProps) => {
    const bubbleAnimatedProps = useAnimatedProps(() => ({
      opacity: animationProgress.value * 0.7,
      r: withDelay(index * 100, withSpring(animationProgress.value * radius)),
    }));

    return (
      <AnimatedCircle
        cx={cx}
        cy={cy}
        fill={fill}
        animatedProps={bubbleAnimatedProps}
      />
    );
  }
);

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

interface BubbleChartDataPoint {
  x: number;
  y: number;
  size: number;
  label?: string;
  color?: string;
}

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

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

  const {
    height = 200,
    padding = 20,
    showGrid = true,
    showLabels = 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 maxX = Math.max(...data.map((d) => d.x));
  const minX = Math.min(...data.map((d) => d.x));
  const maxY = Math.max(...data.map((d) => d.y));
  const minY = Math.min(...data.map((d) => d.y));
  const maxSize = Math.max(...data.map((d) => d.size));

  const xRange = maxX - minX || 1;
  const yRange = maxY - minY || 1;

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

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

  // Convert data to screen coordinates
  const bubbles = data.map((point, index) => ({
    x: padding + ((point.x - minX) / xRange) * innerChartWidth,
    y: padding + ((maxY - point.y) / yRange) * chartHeight,
    radius: (point.size / maxSize) * 20 + 5, // Scale bubble size
    color: point.color || colors[index % colors.length],
    label: point.label,
  }));

  return (
    <View
      style={[{ width: '100%', height }, style]}
      onLayout={handleLayout}
      accessibilityRole='image'
      accessibilityLabel={`Bubble chart with ${data.length} bubbles, x from ${Math.round(minX)} to ${Math.round(maxX)}, y from ${Math.round(minY)} to ${Math.round(maxY)}`}
    >
      <Svg width={chartWidth} height={height}>
        {/* Grid lines */}
        {showGrid && (
          <G>
            {[0, 0.25, 0.5, 0.75, 1].map((ratio, index) => (
              <G key={`grid-${index}`}>
                <Line
                  x1={padding}
                  y1={padding + ratio * chartHeight}
                  x2={chartWidth - padding}
                  y2={padding + ratio * chartHeight}
                  stroke={mutedColor}
                  strokeWidth={0.5}
                  opacity={0.3}
                />
                <Line
                  x1={padding + ratio * innerChartWidth}
                  y1={padding}
                  x2={padding + ratio * innerChartWidth}
                  y2={height - padding}
                  stroke={mutedColor}
                  strokeWidth={0.5}
                  opacity={0.3}
                />
              </G>
            ))}
          </G>
        )}

        {/* Bubbles */}
        {bubbles.map((bubble, index) => {
          return (
            <G key={`bubble-${index}`}>
              <AnimatedBubble
                cx={bubble.x}
                cy={bubble.y}
                radius={bubble.radius}
                fill={bubble.color}
                index={index}
                animationProgress={animationProgress}
              />
              {showLabels && bubble.label && (
                <SvgText
                  x={bubble.x}
                  y={bubble.y}
                  textAnchor='middle'
                  fontSize={10}
                  fill='#FFFFFF'
                  fontWeight='600'
                  alignmentBaseline='middle'
                >
                  {bubble.label}
                </SvgText>
              )}
            </G>
          );
        })}

        {/* Axis labels */}
        <G>
          {/* X-axis labels */}
          {[minX, (minX + maxX) / 2, maxX].map((value, index) => (
            <SvgText
              key={`x-label-${index}`}
              x={padding + (index * innerChartWidth) / 2}
              y={height - 5}
              textAnchor='middle'
              fontSize={12}
              fill={mutedColor}
            >
              {Math.round(value)}
            </SvgText>
          ))}
          {/* Y-axis labels */}
          {[maxY, (minY + maxY) / 2, minY].map((value, index) => (
            <SvgText
              key={`y-label-${index}`}
              x={15}
              y={padding + (index * chartHeight) / 2}
              textAnchor='middle'
              fontSize={12}
              fill={mutedColor}
              alignmentBaseline='middle'
            >
              {Math.round(value)}
            </SvgText>
          ))}
        </G>
      </Svg>
    </View>
  );
};
```

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

## Usage

```tsx
import { BubbleChart } from '@/components/charts/bubble-chart';
```

```tsx
const data = [
  { x: 10, y: 20, size: 15, label: 'A', color: '#FF6B6B' },
  { x: 25, y: 30, size: 25, label: 'B', color: '#4ECDC4' },
  { x: 40, y: 15, size: 30, label: 'C', color: '#45B7D1' },
  { x: 35, y: 45, size: 20, label: 'D', color: '#96CEB4' },
];

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

## Examples

#### Basic Bubble Chart

**Example:** A basic bubble chart with animated bubbles and grid lines

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

const sampleData = [
  { x: 10, y: 20, size: 15, label: 'A' },
  { x: 25, y: 30, size: 25, label: 'B' },
  { x: 40, y: 15, size: 30, label: 'C' },
  { x: 35, y: 45, size: 20, label: 'D' },
  { x: 60, y: 25, size: 18, label: 'E' },
  { x: 50, y: 40, size: 22, label: 'F' },
  { x: 15, y: 35, size: 28, label: 'G' },
  { x: 70, y: 50, size: 16, label: 'H' },
];

export function BubbleChartDemo() {
  return (
    <ChartContainer
      title='Performance vs Efficiency'
      description='Team performance metrics with bubble sizes representing team size'
    >
      <BubbleChart
        data={sampleData}
        config={{
          height: 300,
          showGrid: true,
          showLabels: true,
          animated: true,
          duration: 1200,
        }}
      />
    </ChartContainer>
  );
}
```

#### Sample Bubble Chart

**Example:** A sample bubble chart

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

const sampleData = [
  { x: 20, y: 30, size: 45, label: 'Sales', color: '#FF6B6B' },
  { x: 35, y: 25, size: 35, label: 'Marketing', color: '#4ECDC4' },
  { x: 50, y: 40, size: 25, label: 'Dev', color: '#45B7D1' },
  { x: 65, y: 35, size: 30, label: 'Support', color: '#96CEB4' },
  { x: 40, y: 50, size: 20, label: 'HR', color: '#FFEAA7' },
  { x: 25, y: 45, size: 15, label: 'Finance', color: '#DDA0DD' },
];

export function BubbleChartSample() {
  return (
    <ChartContainer
      title='Department Analytics'
      description='Bubble chart showing department metrics'
    >
      <BubbleChart
        data={sampleData}
        config={{
          height: 320,
          showGrid: true,
          showLabels: true,
          animated: true,
          duration: 1500,
        }}
      />
    </ChartContainer>
  );
}
```

#### Styled Bubble Chart

**Example:** A customized bubble chart with custom styling

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

const styledData = [
  { x: 15, y: 25, size: 40, label: 'Q1', color: '#FF6B6B' },
  { x: 30, y: 35, size: 50, label: 'Q2', color: '#4ECDC4' },
  { x: 45, y: 30, size: 35, label: 'Q3', color: '#45B7D1' },
  { x: 60, y: 45, size: 45, label: 'Q4', color: '#96CEB4' },
  { x: 25, y: 50, size: 25, label: 'Bonus', color: '#FFEAA7' },
];

export function BubbleChartStyled() {
  const backgroundColor = useColor('card');

  return (
    <ChartContainer
      title='Quarterly Revenue'
      description='Styled bubble chart with custom colors and enhanced visuals'
    >
      <BubbleChart
        data={styledData}
        config={{
          height: 280,
          showGrid: true,
          showLabels: true,
          animated: true,
          duration: 1800,
        }}
        style={{
          backgroundColor,
          borderRadius: 12,
          padding: 8,
        }}
      />
    </ChartContainer>
  );
}
```

#### Minimal Bubble Chart

**Example:** A minimal bubble chart without labels or grid

```tsx
// components/demo/charts/bubble-chart/bubble-chart-minimal.tsx
import { BubbleChart } from '@/components/charts/bubble-chart';
import React from 'react';

const minimalData = [
  { x: 20, y: 30, size: 25 },
  { x: 40, y: 45, size: 35 },
  { x: 60, y: 25, size: 20 },
  { x: 35, y: 55, size: 30 },
  { x: 70, y: 40, size: 15 },
];

export function BubbleChartMinimal() {
  return (
    <BubbleChart
      data={minimalData}
      config={{
        height: 200,
        showGrid: false,
        showLabels: false,
        animated: true,
        duration: 1000,
        padding: 10,
      }}
    />
  );
}
```

## API Reference

### BubbleChart

A customizable bubble chart component with smooth animations.

| Prop     | Type                     | Default | Description                                   |
| -------- | ------------------------ | ------- | --------------------------------------------- |
| `data`   | `BubbleChartDataPoint[]` | -       | 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.      |

### BubbleChartDataPoint

| Prop    | Type     | Description                          |
| ------- | -------- | ------------------------------------ |
| `x`     | `number` | The x-axis value for the data point. |
| `y`     | `number` | The y-axis value for the data point. |
| `size`  | `number` | The size value for the bubble.       |
| `label` | `string` | Optional label for the data point.   |
| `color` | `string` | Optional color for the bubble.       |

### 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 bubble labels.                    |
| `animated`   | `boolean` | `true`  | Whether to animate the chart on load.             |
| `duration`   | `number`  | `800`   | Animation duration in milliseconds.               |

## Features

- **Smooth Animations**: Built-in animations using React Native Reanimated
- **Size Mapping**: Bubble sizes automatically scaled based on data values
- **Responsive Design**: Automatically adapts to container width
- **Customizable Grid**: Optional grid lines for better readability
- **Color Customization**: Support for custom colors or automatic color assignment
- **Staggered Animation**: Bubbles animate in sequence for visual appeal
- **Theme Integration**: Uses theme colors for consistent styling

## Accessibility

The BubbleChart component is built with accessibility in mind:

- The chart's outer container exposes accessibilityRole="image" with a synthesized summary label (bubble count and x/y ranges)

## Performance

The component is optimized for performance:

- Uses React Native Reanimated for smooth 60fps animations
- Efficient SVG rendering with minimal re-renders
- Gesture handling optimized for touch interactions
- Automatic cleanup of animation values
- Staggered animations to prevent performance bottlenecks
