# Candlestick Chart

> A customizable candlestick chart component with animations for financial data visualization.

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

---

**Example:** A basic candlestick chart with smooth animations and grid lines

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

const sampleData = [
  { date: 'Jan 1', open: 100, high: 120, low: 95, close: 110 },
  { date: 'Jan 2', open: 110, high: 125, low: 105, close: 115 },
  { date: 'Jan 3', open: 115, high: 130, low: 110, close: 125 },
  { date: 'Jan 4', open: 125, high: 140, low: 120, close: 135 },
  { date: 'Jan 5', open: 135, high: 145, low: 125, close: 128 },
  { date: 'Jan 6', open: 128, high: 135, low: 118, close: 132 },
  { date: 'Jan 7', open: 132, high: 142, low: 128, close: 138 },
  { date: 'Jan 8', open: 138, high: 148, low: 132, close: 145 },
  { date: 'Jan 9', open: 145, high: 155, low: 140, close: 150 },
  { date: 'Jan 10', open: 150, high: 160, low: 145, close: 155 },
];

export function CandlestickChartDemo() {
  return (
    <ChartContainer
      title='Stock Price Movement'
      description='Daily OHLC data showing price trends over time'
    >
      <CandlestickChart
        data={sampleData}
        config={{
          height: 220,
          showGrid: true,
          showLabels: true,
          animated: true,
          duration: 1200,
        }}
      />
    </ChartContainer>
  );
}
```

## Installation

### CLI

```bash
npx bna-ui add candlestick-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/candlestick-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);
const AnimatedLine = Animated.createAnimatedComponent(Line);

type AnimatedCandleProps = {
  x: number;
  candleWidth: number;
  highY: number;
  lowY: number;
  bodyTop: number;
  bodyHeight: number;
  color: string;
  animationProgress: SharedValue<number>;
};

// Per-item hooks must live in their own mounted subcomponent, not in the
// parent's .map() body — calling useAnimatedProps per loop iteration
// violates Rules of Hooks the moment data.length changes. Two hooks here
// (wick + body), both owned by this one subcomponent instance.
const AnimatedCandle = React.memo(
  ({
    x,
    candleWidth,
    highY,
    lowY,
    bodyTop,
    bodyHeight,
    color,
    animationProgress,
  }: AnimatedCandleProps) => {
    const wickAnimatedProps = useAnimatedProps(() => ({
      y1: highY,
      y2: lowY,
      opacity: animationProgress.value,
    }));

    const bodyAnimatedProps = useAnimatedProps(() => ({
      height: animationProgress.value * bodyHeight,
      y: bodyTop,
      opacity: animationProgress.value,
    }));

    return (
      <>
        <AnimatedLine
          x1={x + candleWidth / 2}
          x2={x + candleWidth / 2}
          stroke={color}
          strokeWidth={1}
          animatedProps={wickAnimatedProps}
        />
        <AnimatedRect
          x={x}
          width={candleWidth}
          fill={color}
          stroke={color}
          strokeWidth={1}
          animatedProps={bodyAnimatedProps}
        />
      </>
    );
  }
);

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

interface CandlestickDataPoint {
  date: string;
  open: number;
  high: number;
  low: number;
  close: number;
}

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

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

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

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

  const bullishColor = useColor('green');
  const bearishColor = useColor('red');
  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 allValues = data.flatMap((d) => [d.open, d.high, d.low, d.close]);
  const maxValue = Math.max(...allValues);
  const minValue = Math.min(...allValues);
  const valueRange = maxValue - minValue || 1;

  const innerChartWidth = chartWidth - padding * 2;
  const chartHeight = height - padding * 2;
  const candleWidth = (innerChartWidth / data.length) * 0.6;
  const candleSpacing = (innerChartWidth / data.length) * 0.4;

  return (
    <View
      style={[{ width: '100%', height }, style]}
      onLayout={handleLayout}
      accessibilityRole='image'
      accessibilityLabel={`Candlestick chart with ${data.length} candles, ranging from ${Math.round(minValue)} to ${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, index) => {
          const isBullish = item.close >= item.open;
          const color = isBullish ? bullishColor : bearishColor;

          const x =
            padding + index * (candleWidth + candleSpacing) + candleSpacing / 2;
          const highY =
            padding + ((maxValue - item.high) / valueRange) * chartHeight;
          const lowY =
            padding + ((maxValue - item.low) / valueRange) * chartHeight;
          const openY =
            padding + ((maxValue - item.open) / valueRange) * chartHeight;
          const closeY =
            padding + ((maxValue - item.close) / valueRange) * chartHeight;

          const bodyTop = Math.min(openY, closeY);
          const bodyHeight = Math.abs(closeY - openY) || 1;

          return (
            <G key={`candle-${index}`}>
              <AnimatedCandle
                x={x}
                candleWidth={candleWidth}
                highY={highY}
                lowY={lowY}
                bodyTop={bodyTop}
                bodyHeight={bodyHeight}
                color={color}
                animationProgress={animationProgress}
              />

              {showLabels &&
                index % Math.max(1, Math.floor(data.length / 5)) === 0 && (
                  <SvgText
                    x={x + candleWidth / 2}
                    y={height - 5}
                    textAnchor='middle'
                    fontSize={10}
                    fill={mutedColor}
                  >
                    {item.date}
                  </SvgText>
                )}
            </G>
          );
        })}
      </Svg>
    </View>
  );
};
```

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

## Usage

```tsx
import { CandlestickChart } from '@/components/charts/candlestick-chart';
```

```tsx
const data = [
  { date: 'Jan 1', open: 100, high: 120, low: 95, close: 110 },
  { date: 'Jan 2', open: 110, high: 125, low: 105, close: 115 },
  { date: 'Jan 3', open: 115, high: 130, low: 110, close: 125 },
  { date: 'Jan 4', open: 125, high: 140, low: 120, close: 135 },
];

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

## Examples

#### Basic Candlestick Chart

**Example:** A basic candlestick chart with smooth animations and grid lines

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

const sampleData = [
  { date: 'Jan 1', open: 100, high: 120, low: 95, close: 110 },
  { date: 'Jan 2', open: 110, high: 125, low: 105, close: 115 },
  { date: 'Jan 3', open: 115, high: 130, low: 110, close: 125 },
  { date: 'Jan 4', open: 125, high: 140, low: 120, close: 135 },
  { date: 'Jan 5', open: 135, high: 145, low: 125, close: 128 },
  { date: 'Jan 6', open: 128, high: 135, low: 118, close: 132 },
  { date: 'Jan 7', open: 132, high: 142, low: 128, close: 138 },
  { date: 'Jan 8', open: 138, high: 148, low: 132, close: 145 },
  { date: 'Jan 9', open: 145, high: 155, low: 140, close: 150 },
  { date: 'Jan 10', open: 150, high: 160, low: 145, close: 155 },
];

export function CandlestickChartDemo() {
  return (
    <ChartContainer
      title='Stock Price Movement'
      description='Daily OHLC data showing price trends over time'
    >
      <CandlestickChart
        data={sampleData}
        config={{
          height: 220,
          showGrid: true,
          showLabels: true,
          animated: true,
          duration: 1200,
        }}
      />
    </ChartContainer>
  );
}
```

#### Sample Candlestick Chart

**Example:** A candlestick chart showing weekly price movements

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

const sampleData = [
  { date: 'Week 1', open: 150, high: 165, low: 145, close: 160 },
  { date: 'Week 2', open: 160, high: 175, low: 155, close: 170 },
  { date: 'Week 3', open: 170, high: 185, low: 165, close: 180 },
  { date: 'Week 4', open: 180, high: 195, low: 175, close: 190 },
  { date: 'Week 5', open: 190, high: 205, low: 185, close: 200 },
  { date: 'Week 6', open: 200, high: 215, low: 195, close: 210 },
  { date: 'Week 7', open: 210, high: 225, low: 205, close: 220 },
  { date: 'Week 8', open: 220, high: 235, low: 215, close: 230 },
];

export function CandlestickChartSample() {
  return (
    <ChartContainer
      title='Stock Chart'
      description='Explore weekly price movements'
    >
      <CandlestickChart
        data={sampleData}
        config={{
          height: 250,
          showGrid: true,
          showLabels: true,
          animated: true,
          duration: 1500,
        }}
      />
    </ChartContainer>
  );
}
```

#### Styled Candlestick Chart

**Example:** A customized candlestick chart with custom colors

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

const styledData = [
  { date: 'Q1', open: 250, high: 280, low: 240, close: 275 },
  { date: 'Q2', open: 275, high: 300, low: 260, close: 285 },
  { date: 'Q3', open: 285, high: 320, low: 275, close: 310 },
  { date: 'Q4', open: 310, high: 340, low: 295, close: 325 },
  { date: 'Q1', open: 325, high: 350, low: 315, close: 340 },
  { date: 'Q2', open: 340, high: 365, low: 330, close: 355 },
];

export function CandlestickChartStyled() {
  return (
    <ChartContainer
      title='Quarterly Performance'
      description='Custom styled candlestick chart with quarterly data'
    >
      <CandlestickChart
        data={styledData}
        config={{
          height: 280,
          padding: 30,
          showGrid: true,
          showLabels: true,
          animated: true,
          duration: 2000,
        }}
        style={{
          backgroundColor: 'rgba(0, 0, 0, 0.02)',
          borderRadius: 12,
          padding: 16,
        }}
      />
    </ChartContainer>
  );
}
```

#### Minimal Candlestick Chart

**Example:** A minimal candlestick chart without labels

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

const minimalData = [
  { date: '10', open: 190, high: 205, low: 185, close: 200 },
  { date: '8', open: 170, high: 185, low: 165, close: 180 },
  { date: '9', open: 180, high: 195, low: 175, close: 170 },
  { date: '7', open: 160, high: 175, low: 155, close: 150 },
  { date: '6', open: 150, high: 165, low: 145, close: 160 },
  { date: '4', open: 130, high: 145, low: 125, close: 140 },
  { date: '2', open: 110, high: 125, low: 105, close: 120 },
  { date: '3', open: 120, high: 135, low: 115, close: 130 },
  { date: '5', open: 140, high: 155, low: 135, close: 150 },
  { date: '1', open: 100, high: 115, low: 95, close: 90 },
];

export function CandlestickChartMinimal() {
  return (
    <CandlestickChart
      data={minimalData}
      config={{
        height: 180,
        padding: 15,
        showGrid: false,
        showLabels: false,
        animated: true,
        duration: 1000,
      }}
    />
  );
}
```

## API Reference

### CandlestickChart

A customizable candlestick chart component for financial data visualization with smooth animations.

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

### CandlestickDataPoint

| Prop    | Type     | Description                         |
| ------- | -------- | ----------------------------------- |
| `date`  | `string` | The date/time label for the candle. |
| `open`  | `number` | The opening price for the period.   |
| `high`  | `number` | The highest price for the period.   |
| `low`   | `number` | The lowest price for the period.    |
| `close` | `number` | The closing price for the period.   |

### 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 date 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
- **Financial Data**: Specialized for OHLC (Open, High, Low, Close) data
- **Color Coding**: Automatic bullish (green) and bearish (red) candle colors
- **Responsive Design**: Automatically adapts to container width
- **Customizable Grid**: Optional grid lines for better readability
- **Smart Spacing**: Automatic candle width and spacing calculations
- **Theme Integration**: Uses theme colors for consistent styling

## Accessibility

The CandlestickChart component is built with accessibility in mind:

- The chart's outer container exposes accessibilityRole="image" with a synthesized summary label (candle count and value range)

## 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

## Financial Data Visualization

The candlestick chart is specifically designed for financial data:

- **Bullish Candles**: Green candles when close > open (price increased)
- **Bearish Candles**: Red candles when close \< open (price decreased)
- **Wicks**: Show the full price range (high and low) for each period
- **Body**: Shows the open and close prices for each period
- **Automatic Scaling**: Prices are automatically scaled to fit the chart area
