# Heatmap Chart

> A customizable heatmap chart component with smooth animations and flexible color scaling for visualizing matrix data.

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

---

**Example:** A heatmap chart with smooth animations and color scaling

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

const sampleData = [
  { row: 'Mon', col: 'Morning', value: 45 },
  { row: 'Mon', col: 'Afternoon', value: 62 },
  { row: 'Mon', col: 'Evening', value: 38 },
  { row: 'Tue', col: 'Morning', value: 52 },
  { row: 'Tue', col: 'Afternoon', value: 71 },
  { row: 'Tue', col: 'Evening', value: 43 },
  { row: 'Wed', col: 'Morning', value: 39 },
  { row: 'Wed', col: 'Afternoon', value: 85 },
  { row: 'Wed', col: 'Evening', value: 57 },
  { row: 'Thu', col: 'Morning', value: 68 },
  { row: 'Thu', col: 'Afternoon', value: 92 },
  { row: 'Thu', col: 'Evening', value: 61 },
  { row: 'Fri', col: 'Morning', value: 73 },
  { row: 'Fri', col: 'Afternoon', value: 88 },
  { row: 'Fri', col: 'Evening', value: 79 },
];

export function HeatmapChartDemo() {
  return (
    <ChartContainer
      title='Weekly Activity Heatmap'
      description='Activity levels throughout the week by time of day'
    >
      <HeatmapChart
        data={sampleData}
        config={{
          height: 300,
          showLabels: true,
          animated: true,
          duration: 1000,
          colorScale: ['#e0f2fe', '#0369a1', '#1e3a8a'],
        }}
      />
    </ChartContainer>
  );
}
```

## Installation

### CLI

```bash
npx bna-ui add heatmap-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/heatmap-chart.tsx
import { useColor } from '@/hooks/useColor';
import React, { useEffect, useMemo, useState } from 'react';
import { LayoutChangeEvent, View, ViewStyle } from 'react-native';
import Animated, {
  SharedValue,
  useAnimatedProps,
  useSharedValue,
  withDelay,
  withTiming,
} from 'react-native-reanimated';
import Svg, { G, Rect, Text as SvgText } from 'react-native-svg';

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

type AnimatedCellProps = {
  x: number;
  y: number;
  width: number;
  height: number;
  fill: string;
  delay: number;
  animationProgress: SharedValue<number>;
};

// Per-item hook must live in its own mounted subcomponent, not in the
// parent's nested rows×cols .map() body — calling useAnimatedProps per
// loop iteration violates Rules of Hooks the moment data changes, and this
// is the worst multiplier in the chart set (one mount per row×col cell).
const AnimatedCell = React.memo(
  ({
    x,
    y,
    width,
    height,
    fill,
    delay,
    animationProgress,
  }: AnimatedCellProps) => {
    const cellAnimatedProps = useAnimatedProps(() => ({
      opacity: withDelay(
        delay,
        withTiming(animationProgress.value, { duration: 300 })
      ),
    }));

    return (
      <AnimatedRect
        x={x}
        y={y}
        width={width}
        height={height}
        fill={fill}
        rx={4}
        animatedProps={cellAnimatedProps}
      />
    );
  }
);

// Utility functions
const interpolateColor = (
  color1: string,
  color2: string,
  factor: number
): string => {
  // Simple color interpolation between two hex colors
  const hex1 = color1.replace('#', '');
  const hex2 = color2.replace('#', '');

  const r1 = parseInt(hex1.substr(0, 2), 16);
  const g1 = parseInt(hex1.substr(2, 2), 16);
  const b1 = parseInt(hex1.substr(4, 2), 16);

  const r2 = parseInt(hex2.substr(0, 2), 16);
  const g2 = parseInt(hex2.substr(2, 2), 16);
  const b2 = parseInt(hex2.substr(4, 2), 16);

  const r = Math.round(r1 + (r2 - r1) * factor);
  const g = Math.round(g1 + (g2 - g1) * factor);
  const b = Math.round(b1 + (b2 - b1) * factor);

  return `#${r.toString(16).padStart(2, '0')}${g
    .toString(16)
    .padStart(2, '0')}${b.toString(16).padStart(2, '0')}`;
};

const getHeatmapColor = (
  value: number,
  minValue: number,
  maxValue: number,
  colorScale: string[]
): string => {
  if (maxValue === minValue) return colorScale[0];

  const normalizedValue = (value - minValue) / (maxValue - minValue);
  const segmentSize = 1 / (colorScale.length - 1);
  const segmentIndex = Math.floor(normalizedValue / segmentSize);
  const segmentProgress = (normalizedValue % segmentSize) / segmentSize;

  if (segmentIndex >= colorScale.length - 1) {
    return colorScale[colorScale.length - 1];
  }

  return interpolateColor(
    colorScale[segmentIndex],
    colorScale[segmentIndex + 1],
    segmentProgress
  );
};

interface ChartConfig {
  width?: number;
  height?: number;
  padding?: number;
  showLabels?: boolean;
  animated?: boolean;
  duration?: number;
  colorScale?: string[];
}

interface HeatmapDataPoint {
  row: string | number;
  col: string | number;
  value: number;
  label?: string;
}

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

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

  const {
    height = 200,
    padding = 20,
    showLabels = true,
    animated = true,
    duration = 1000,
    colorScale = ['#e0f2fe', '#0369a1', '#1e3a8a'], // Light blue to dark blue
  } = config;

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

  const mutedColor = useColor('mutedForeground');
  const textColor = useColor('foreground');

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

  // The grid rebuild is O(rows×cols) — the worst-case multiplier in the
  // chart set — so it's memoized rather than recomputed on every render
  // regardless of whether the inputs changed.
  const layout = useMemo(() => {
    const uniqueRows = [...new Set(data.map((d) => d.row))].sort();
    const uniqueCols = [...new Set(data.map((d) => d.col))].sort();
    const numRows = uniqueRows.length;
    const numCols = uniqueCols.length;

    const values = data.map((d) => d.value);
    const minValue = Math.min(...values);
    const maxValue = Math.max(...values);

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

    const cellSpacing = 2;
    const cellWidth = (innerChartWidth - (numCols - 1) * cellSpacing) / numCols;
    const cellHeight = (chartHeight - (numRows - 1) * cellSpacing) / numRows;

    const dataMap = new Map<string, HeatmapDataPoint>();
    data.forEach((point) => {
      dataMap.set(`${point.row}-${point.col}`, point);
    });

    const cells = uniqueRows.flatMap((row, rowIndex) =>
      uniqueCols.map((col, colIndex) => {
        const point = dataMap.get(`${row}-${col}`);
        const value = point?.value || 0;
        return {
          key: `${row}-${col}`,
          row,
          col,
          value,
          hasPoint: !!point,
          x: padding + colIndex * (cellWidth + cellSpacing),
          y: padding + rowIndex * (cellHeight + cellSpacing),
          color: getHeatmapColor(value, minValue, maxValue, colorScale),
          delay: (rowIndex * numCols + colIndex) * 50,
        };
      })
    );

    return {
      uniqueRows,
      uniqueCols,
      numRows,
      numCols,
      minValue,
      maxValue,
      cellWidth,
      cellHeight,
      cellSpacing,
      cells,
    };
  }, [data, chartWidth, height, padding, colorScale]);

  if (!data.length) return null;

  const {
    uniqueRows,
    uniqueCols,
    numRows,
    numCols,
    minValue,
    maxValue,
    cellWidth,
    cellHeight,
    cellSpacing,
    cells,
  } = layout;

  return (
    <View
      style={[{ width: '100%', height }, style]}
      onLayout={handleLayout}
      accessibilityRole='image'
      accessibilityLabel={`Heatmap with ${numRows} rows and ${numCols} columns, values from ${Math.round(minValue)} to ${Math.round(maxValue)}`}
    >
      <Svg width={chartWidth} height={height}>
        {cells.map((cell) => (
          <G key={`cell-${cell.key}`}>
            <AnimatedCell
              x={cell.x}
              y={cell.y}
              width={cellWidth}
              height={cellHeight}
              fill={cell.color}
              delay={cell.delay}
              animationProgress={animationProgress}
            />

            {showLabels && cellWidth > 30 && cellHeight > 20 && (
              <SvgText
                x={cell.x + cellWidth / 2}
                y={cell.y + cellHeight / 2 + 4}
                textAnchor='middle'
                fontSize={Math.min(10, cellWidth / 4)}
                fill={
                  cell.value > (minValue + maxValue) / 2 ? '#ffffff' : textColor
                }
                fontWeight='500'
              >
                {cell.hasPoint ? cell.value.toString() : ''}
              </SvgText>
            )}
          </G>
        ))}

        {/* Row labels */}
        {showLabels && (
          <G>
            {uniqueRows.map((row, rowIndex) => (
              <SvgText
                key={`row-label-${row}`}
                x={padding - 8}
                y={
                  padding +
                  rowIndex * (cellHeight + cellSpacing) +
                  cellHeight / 2 +
                  4
                }
                textAnchor='end'
                fontSize={12}
                fill={mutedColor}
              >
                {row}
              </SvgText>
            ))}
          </G>
        )}

        {/* Column labels */}
        {showLabels && (
          <G>
            {uniqueCols.map((col, colIndex) => (
              <SvgText
                key={`col-label-${col}`}
                x={
                  padding + colIndex * (cellWidth + cellSpacing) + cellWidth / 2
                }
                y={height - 5}
                textAnchor='middle'
                fontSize={12}
                fill={mutedColor}
              >
                {col}
              </SvgText>
            ))}
          </G>
        )}
      </Svg>
    </View>
  );
};
```

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

## Usage

```tsx
import { HeatmapChart } from '@/components/charts/heatmap-chart';
```

```tsx
const data = [
  { row: 'Mon', col: 'Morning', value: 45 },
  { row: 'Mon', col: 'Afternoon', value: 62 },
  { row: 'Mon', col: 'Evening', value: 38 },
  { row: 'Tue', col: 'Morning', value: 52 },
  { row: 'Tue', col: 'Afternoon', value: 71 },
  { row: 'Tue', col: 'Evening', value: 43 },
];

<HeatmapChart
  data={data}
  config={{
    height: 300,
    showLabels: true,
    animated: true,
    colorScale: ['#e0f2fe', '#0369a1', '#1e3a8a'],
  }}
/>;
```

## Examples

#### Basic Heatmap Chart

**Example:** A heatmap chart with smooth animations and color scaling

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

const sampleData = [
  { row: 'Mon', col: 'Morning', value: 45 },
  { row: 'Mon', col: 'Afternoon', value: 62 },
  { row: 'Mon', col: 'Evening', value: 38 },
  { row: 'Tue', col: 'Morning', value: 52 },
  { row: 'Tue', col: 'Afternoon', value: 71 },
  { row: 'Tue', col: 'Evening', value: 43 },
  { row: 'Wed', col: 'Morning', value: 39 },
  { row: 'Wed', col: 'Afternoon', value: 85 },
  { row: 'Wed', col: 'Evening', value: 57 },
  { row: 'Thu', col: 'Morning', value: 68 },
  { row: 'Thu', col: 'Afternoon', value: 92 },
  { row: 'Thu', col: 'Evening', value: 61 },
  { row: 'Fri', col: 'Morning', value: 73 },
  { row: 'Fri', col: 'Afternoon', value: 88 },
  { row: 'Fri', col: 'Evening', value: 79 },
];

export function HeatmapChartDemo() {
  return (
    <ChartContainer
      title='Weekly Activity Heatmap'
      description='Activity levels throughout the week by time of day'
    >
      <HeatmapChart
        data={sampleData}
        config={{
          height: 300,
          showLabels: true,
          animated: true,
          duration: 1000,
          colorScale: ['#e0f2fe', '#0369a1', '#1e3a8a'],
        }}
      />
    </ChartContainer>
  );
}
```

#### Sample Heatmap Chart

**Example:** A sample heatmap chart with different data

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

const sampleData = [
  { row: 'Q1', col: 'Sales', value: 85 },
  { row: 'Q1', col: 'Marketing', value: 72 },
  { row: 'Q1', col: 'Support', value: 90 },
  { row: 'Q1', col: 'Development', value: 78 },
  { row: 'Q2', col: 'Sales', value: 92 },
  { row: 'Q2', col: 'Marketing', value: 65 },
  { row: 'Q2', col: 'Support', value: 88 },
  { row: 'Q2', col: 'Development', value: 95 },
  { row: 'Q3', col: 'Sales', value: 78 },
  { row: 'Q3', col: 'Marketing', value: 83 },
  { row: 'Q3', col: 'Support', value: 91 },
  { row: 'Q3', col: 'Development', value: 87 },
  { row: 'Q4', col: 'Sales', value: 96 },
  { row: 'Q4', col: 'Marketing', value: 89 },
  { row: 'Q4', col: 'Support', value: 94 },
  { row: 'Q4', col: 'Development', value: 92 },
];

export function HeatmapChartSample() {
  return (
    <ChartContainer
      title='Quarterly Performance Matrix'
      description='Performance scores by department and quarter'
    >
      <HeatmapChart
        data={sampleData}
        config={{
          height: 280,
          showLabels: true,
          animated: true,
          duration: 800,
          colorScale: ['#fef3c7', '#f59e0b', '#d97706'],
        }}
      />
    </ChartContainer>
  );
}
```

#### Styled Heatmap Chart

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

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

const sampleData = [
  { row: 'Low', col: 'Low', value: 10 },
  { row: 'Low', col: 'Medium', value: 25 },
  { row: 'Low', col: 'High', value: 40 },
  { row: 'Medium', col: 'Low', value: 30 },
  { row: 'Medium', col: 'Medium', value: 55 },
  { row: 'Medium', col: 'High', value: 70 },
  { row: 'High', col: 'Low', value: 50 },
  { row: 'High', col: 'Medium', value: 75 },
  { row: 'High', col: 'High', value: 95 },
];

export function HeatmapChartStyled() {
  const isDark = useColor('background') === '#000000';

  const colorScale = isDark
    ? ['#0f172a', '#1e293b', '#334155', '#64748b', '#94a3b8']
    : ['#f8fafc', '#e2e8f0', '#cbd5e1', '#94a3b8', '#64748b'];

  return (
    <ChartContainer
      title='Risk Assessment Matrix'
      description='Risk levels across different probability and impact combinations'
    >
      <HeatmapChart
        data={sampleData}
        config={{
          height: 250,
          showLabels: true,
          animated: true,
          duration: 1200,
          colorScale,
          padding: 30,
        }}
      />
    </ChartContainer>
  );
}
```

#### Large Heatmap Chart

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

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

// Generate large dataset
const generateLargeDataset = () => {
  const data = [];
  const months = [
    'Jan',
    'Feb',
    'Mar',
    'Apr',
    'May',
    'Jun',
    'Jul',
    'Aug',
    'Sep',
    'Oct',
    'Nov',
    'Dec',
  ];
  const hours = Array.from({ length: 24 }, (_, i) =>
    i.toString().padStart(2, '0')
  );

  for (const month of months) {
    for (const hour of hours) {
      // Generate realistic activity data (higher during work hours)
      const isWorkHour = parseInt(hour) >= 8 && parseInt(hour) <= 18;
      const baseValue = isWorkHour ? 40 : 10;
      const randomVariation = Math.random() * 30;
      const value = Math.round(baseValue + randomVariation);

      data.push({
        row: month,
        col: `${hour}:00`,
        value,
      });
    }
  }

  return data;
};

const largeDataset = generateLargeDataset();

export function HeatmapChartLarge() {
  return (
    <ChartContainer
      title='Annual Activity Heatmap'
      description='User activity patterns throughout the year by hour of day'
    >
      <HeatmapChart
        data={largeDataset}
        config={{
          height: 400,
          showLabels: false, // Disabled for large datasets
          animated: true,
          duration: 1500,
          colorScale: ['#ecfdf5', '#10b981', '#065f46'],
          padding: 40,
        }}
      />
    </ChartContainer>
  );
}
```

## API Reference

### HeatmapChart

A customizable heatmap chart component with smooth animations and flexible color scaling. Perfect for visualizing matrix data, correlation matrices, activity patterns, and any two-dimensional data with intensity values.

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

### HeatmapDataPoint

| Prop    | Type               | Description                               |
| ------- | ------------------ | ----------------------------------------- |
| `row`   | `string \| number` | The row identifier for the data point.    |
| `col`   | `string \| number` | The column identifier for the data point. |
| `value` | `number`           | The intensity value for the data point.   |
| `label` | `string`           | Optional custom 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.                         |
| `showLabels` | `boolean`  | `true`                              | Whether to show labels for cells and axes.        |
| `animated`   | `boolean`  | `true`                              | Whether to animate the chart on load.             |
| `duration`   | `number`   | `1000`                              | Animation duration in milliseconds.               |
| `colorScale` | `string[]` | `['#e0f2fe', '#0369a1', '#1e3a8a']` | Array of hex colors for the gradient scale.       |

## Features

- **Matrix Visualization**: Displays data in a grid format with color-coded intensity
- **Smooth Animations**: Built-in staggered animations using React Native Reanimated
- **Responsive Design**: Automatically adapts to container width
- **Custom Color Scales**: Support for multi-color gradients
- **Value Display**: Shows actual values within cells when space permits
- **Axis Labels**: Displays row and column labels for context
- **Theme Integration**: Uses theme colors for consistent styling
- **Rounded Corners**: Aesthetic rounded cell corners

## Use Cases

Heatmap charts are particularly effective for:

- **Activity Patterns**: Visualizing user activity across time periods
- **Correlation Analysis**: Displaying correlation matrices between variables
- **Performance Metrics**: Showing performance across different dimensions
- **Geographic Data**: Visualizing data intensity across regions
- **Quality Metrics**: Displaying quality scores across products/services
- **Risk Assessment**: Showing risk levels across different categories
- **Resource Utilization**: Visualizing usage patterns across time and resources

## Design Considerations

The HeatmapChart component is designed for:

- **Data Density**: Efficiently displays large amounts of matrix data
- **Pattern Recognition**: Color coding helps identify patterns and outliers
- **Comparative Analysis**: Easy to compare values across rows and columns
- **Scalability**: Handles varying grid sizes automatically
- **Accessibility**: High contrast colors and value labels for clarity

## Color Scaling

The heatmap uses intelligent color interpolation:

- **Gradient Generation**: Smoothly interpolates between multiple colors
- **Value Normalization**: Automatically scales colors based on data range
- **Custom Palettes**: Supports any number of colors in the scale
- **Contrast Optimization**: Automatically adjusts text color for readability

## Animation

The chart features sophisticated entry animations:

- **Staggered Reveal**: Cells animate in sequence for visual appeal
- **Opacity Transitions**: Smooth fade-in effects
- **Configurable Timing**: Adjustable animation duration
- **Performance Optimized**: Uses React Native Reanimated for 60fps animations

## Accessibility

The HeatmapChart component includes several accessibility features:

- **Semantic Structure**: Proper SVG structure for screen readers
- **Value Labels**: Numeric values displayed within cells
- **High Contrast**: Automatic text color adjustment for readability
- **Descriptive Labels**: Row and column labels provide context
- **Keyboard Navigation**: Supports focus management

## Performance

The component is optimized for performance:

- **Efficient Rendering**: Uses SVG for crisp graphics at any scale
- **Animation Optimization**: Leverages React Native Reanimated
- **Memory Management**: Efficient data structures and cleanup
- **Responsive Layout**: Minimal re-renders on size changes

## Styling

The component integrates with your theme system:

- **Theme Colors**: Uses `mutedForeground` and `foreground` from theme
- **Custom Color Scales**: Override default colors with custom palettes
- **Consistent Spacing**: Maintains consistent cell spacing and padding
- **Rounded Aesthetics**: Configurable border radius for cells

## Data Requirements

The heatmap requires properly structured data:

- **Complete Coverage**: All row/column combinations should be provided
- **Numeric Values**: Values must be numeric for proper color scaling
- **Consistent Types**: Row and column identifiers should be consistent
- **Sorted Data**: Data is automatically sorted but pre-sorting improves performance

## Best Practices

For optimal results with HeatmapChart:

- **Meaningful Labels**: Use descriptive row and column labels
- **Appropriate Color Scales**: Choose colors that represent your data semantically
- **Reasonable Grid Size**: Consider screen size when determining grid dimensions
- **Value Ranges**: Ensure your data has sufficient range for meaningful color variation
- **Loading States**: Consider showing loading indicators for large datasets
