# TreeMap Chart

> A customizable treemap chart component with hierarchical data visualization, smooth animations, 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/treemap-chart
- Markdown: https://ui.ahmedbna.com/docs/charts/treemap-chart.md
- Structured JSON (props, usage, source, examples): https://ui.ahmedbna.com/r/ai/treemap-chart.json
- Install payload (source plus every file it imports): https://ui.ahmedbna.com/r/treemap-chart.json
- Install: `npx bna-ui add treemap-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/0395-treemap-chart-demo.MP4

---

**Example:** A treemap chart with smooth animations

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

const sampleData = [
  { label: 'Sales', value: 120 },
  { label: 'Marketing', value: 98 },
  { label: 'Support', value: 86 },
  { label: 'Development', value: 140 },
  { label: 'Design', value: 75 },
  { label: 'HR', value: 65 },
];

export function TreeMapChartDemo() {
  return (
    <ChartContainer
      title='Department Performance'
      description='Quarterly performance metrics by department'
    >
      <TreeMapChart
        data={sampleData}
        config={{
          height: 300,
          showLabels: true,
          animated: true,
          duration: 1000,
        }}
      />
    </ChartContainer>
  );
}
```

## Installation

### CLI

```bash
npx bna-ui add treemap-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/treemap-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,
  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 AnimatedTreemapRectProps = {
  x: number;
  y: number;
  width: number;
  height: number;
  fill: string;
  stroke: string;
  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 the rectangle count changes.
const AnimatedTreemapRect = React.memo(
  ({
    x,
    y,
    width,
    height,
    fill,
    stroke,
    animationProgress,
  }: AnimatedTreemapRectProps) => {
    const rectAnimatedProps = useAnimatedProps(() => ({
      width: animationProgress.value * width,
      height: animationProgress.value * height,
      opacity: animationProgress.value,
    }));

    return (
      <AnimatedRect
        x={x}
        y={y}
        fill={fill}
        stroke={stroke}
        strokeWidth={1}
        rx={2}
        animatedProps={rectAnimatedProps}
      />
    );
  }
);

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

export interface TreeMapDataPoint {
  label: string;
  value: number;
  color?: string;
  children?: TreeMapDataPoint[];
}

interface TreeMapRect {
  x: number;
  y: number;
  width: number;
  height: number;
  data: TreeMapDataPoint;
  depth: number;
}

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

// Squarified treemap algorithm
const squarify = (
  data: TreeMapDataPoint[],
  x: number,
  y: number,
  width: number,
  height: number,
  depth: number = 0
): TreeMapRect[] => {
  if (data.length === 0) return [];

  const totalValue = data.reduce((sum, item) => sum + item.value, 0);
  if (totalValue <= 0) return [];

  // The squarified-treemap algorithm requires items sorted descending by
  // value before row placement, so each row is filled largest-first —
  // skipping this produces poor (long, thin) aspect ratios.
  const normalizedData = [...data]
    .sort((a, b) => b.value - a.value)
    .map((item) => ({
      ...item,
      normalizedValue: (item.value / totalValue) * width * height,
    }));

  const layoutRects: TreeMapRect[] = [];
  let remainingData = [...normalizedData];
  let currentX = x;
  let currentY = y;
  let remainingWidth = width;
  let remainingHeight = height;

  while (remainingData.length > 0) {
    const vertical = remainingWidth > remainingHeight;
    const dimension = vertical ? remainingHeight : remainingWidth;

    // Find the best row/column
    let bestRow: typeof remainingData = [];
    let bestRatio = Infinity;

    for (let i = 1; i <= remainingData.length; i++) {
      const row = remainingData.slice(0, i);
      const rowValue = row.reduce((sum, item) => sum + item.normalizedValue, 0);
      const rowDimension = rowValue / dimension;

      const worstRatio = Math.max(
        ...row.map((item) => {
          const itemDimension = item.normalizedValue / rowDimension;
          return Math.max(
            rowDimension / itemDimension,
            itemDimension / rowDimension
          );
        })
      );

      if (worstRatio < bestRatio) {
        bestRatio = worstRatio;
        bestRow = row;
      } else {
        break;
      }
    }

    // Place the row/column
    const rowValue = bestRow.reduce(
      (sum, item) => sum + item.normalizedValue,
      0
    );
    const rowDimension = rowValue / dimension;

    let offset = 0;
    bestRow.forEach((item) => {
      const itemDimension = item.normalizedValue / rowDimension;

      const rectX = vertical ? currentX : currentX + offset;
      const rectY = vertical ? currentY + offset : currentY;
      const rectWidth = vertical ? rowDimension : itemDimension;
      const rectHeight = vertical ? itemDimension : rowDimension;

      layoutRects.push({
        x: rectX,
        y: rectY,
        width: rectWidth,
        height: rectHeight,
        data: item,
        depth,
      });

      offset += itemDimension;
    });

    // Update remaining space
    remainingData = remainingData.slice(bestRow.length);

    if (vertical) {
      currentX += rowDimension;
      remainingWidth -= rowDimension;
    } else {
      currentY += rowDimension;
      remainingHeight -= rowDimension;
    }
  }

  // Items with children are containers, not leaves: subdivide their
  // allotted rect recursively instead of rendering it directly, so nested
  // data actually affects the layout instead of being silently ignored.
  const rects: TreeMapRect[] = [];
  for (const rect of layoutRects) {
    if (rect.data.children && rect.data.children.length > 0) {
      rects.push(
        ...squarify(
          rect.data.children,
          rect.x,
          rect.y,
          rect.width,
          rect.height,
          depth + 1
        )
      );
    } else {
      rects.push(rect);
    }
  }

  return rects;
};

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

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

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

  const backgroundColor = useColor('background');

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

  // squarify() is O(n²) per level — memoize rather than recomputing the
  // full layout (including any recursive children) on every render.
  const rectangles = useMemo(
    () =>
      squarify(
        data,
        padding,
        padding,
        chartWidth - padding * 2,
        height - padding * 2
      ),
    [data, padding, chartWidth, height]
  );

  if (!data.length) return null;

  // Generate color palette
  const colors = [
    '#3b82f6',
    '#ef4444',
    '#10b981',
    '#f59e0b',
    '#8b5cf6',
    '#06b6d4',
    '#f97316',
    '#84cc16',
    '#ec4899',
    '#6366f1',
  ];

  const getColor = (index: number, customColor?: string) => {
    if (customColor) return customColor;
    return colors[index % colors.length];
  };

  return (
    <View
      style={[{ width: '100%', height }, style]}
      onLayout={handleLayout}
      accessibilityRole='image'
      accessibilityLabel={`Treemap with ${data.length} top-level items`}
    >
      <Svg width={chartWidth} height={height}>
        {rectangles.map((rect, index) => {
          const color = getColor(index, rect.data.color);

          // Determine if text should be light or dark based on background
          const isLightBackground =
            color === '#f59e0b' || color === '#84cc16' || color === '#06b6d4';
          const textColor = isLightBackground ? '#000000' : '#ffffff';

          // Calculate font size based on rectangle size
          const fontSize = Math.min(rect.width / 8, rect.height / 4, 14);
          const showText = fontSize > 8 && showLabels;

          return (
            <G key={`rect-${index}`}>
              <AnimatedTreemapRect
                x={rect.x}
                y={rect.y}
                width={rect.width}
                height={rect.height}
                fill={color}
                stroke={backgroundColor}
                animationProgress={animationProgress}
              />

              {showText && (
                <G>
                  <SvgText
                    x={rect.x + rect.width / 2}
                    y={rect.y + rect.height / 2 - fontSize / 2}
                    textAnchor='middle'
                    fontSize={fontSize}
                    fontWeight='600'
                    fill={textColor}
                    opacity={animationProgress.value}
                  >
                    {rect.data.label}
                  </SvgText>

                  {rect.height > fontSize * 2.5 && (
                    <SvgText
                      x={rect.x + rect.width / 2}
                      y={rect.y + rect.height / 2 + fontSize / 2}
                      textAnchor='middle'
                      fontSize={fontSize * 0.8}
                      fill={textColor}
                      opacity={0.8}
                    >
                      {rect.data.value}
                    </SvgText>
                  )}
                </G>
              )}
            </G>
          );
        })}
      </Svg>
    </View>
  );
};
```

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

## Usage

```tsx
import { TreeMapChart } from '@/components/charts/treemap-chart';
```

```tsx
const data = [
  { label: 'Sales', value: 120 },
  { label: 'Marketing', value: 98 },
  { label: 'Support', value: 86 },
  { label: 'Development', value: 140 },
];

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

## Examples

#### Basic TreeMap Chart

**Example:** A treemap chart with smooth animations

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

const sampleData = [
  { label: 'Sales', value: 120 },
  { label: 'Marketing', value: 98 },
  { label: 'Support', value: 86 },
  { label: 'Development', value: 140 },
  { label: 'Design', value: 75 },
  { label: 'HR', value: 65 },
];

export function TreeMapChartDemo() {
  return (
    <ChartContainer
      title='Department Performance'
      description='Quarterly performance metrics by department'
    >
      <TreeMapChart
        data={sampleData}
        config={{
          height: 300,
          showLabels: true,
          animated: true,
          duration: 1000,
        }}
      />
    </ChartContainer>
  );
}
```

#### Sample TreeMap Chart

**Example:** A sample treemap chart with various data sizes

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

const sampleData = [
  { label: 'Product A', value: 250 },
  { label: 'Product B', value: 180 },
  { label: 'Product C', value: 320 },
  { label: 'Product D', value: 90 },
  { label: 'Product E', value: 150 },
  { label: 'Product F', value: 45 },
  { label: 'Product G', value: 210 },
  { label: 'Product H', value: 75 },
];

export function TreeMapChartSample() {
  return (
    <ChartContainer
      title='Product Sales Distribution'
      description='Revenue breakdown by product category'
    >
      <TreeMapChart
        data={sampleData}
        config={{
          height: 250,
          showLabels: true,
          animated: true,
          duration: 800,
        }}
      />
    </ChartContainer>
  );
}
```

#### Styled TreeMap Chart

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

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

const styledData = [
  { label: 'Mobile', value: 450, color: '#FF6B6B' },
  { label: 'Desktop', value: 320, color: '#4ECDC4' },
  { label: 'Tablet', value: 180, color: '#45B7D1' },
  { label: 'Watch', value: 90, color: '#FFA07A' },
  { label: 'TV', value: 150, color: '#98D8C8' },
  { label: 'Other', value: 60, color: '#F7DC6F' },
];

export function TreeMapChartStyled() {
  return (
    <ChartContainer
      title='Device Usage Analytics'
      description='User engagement across different device types'
    >
      <TreeMapChart
        data={styledData}
        config={{
          height: 350,
          padding: 15,
          showLabels: true,
          animated: true,
          duration: 1200,
        }}
      />
    </ChartContainer>
  );
}
```

#### Large TreeMap Chart

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

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

const largeData = [
  { label: 'North America', value: 1250 },
  { label: 'Europe', value: 980 },
  { label: 'Asia Pacific', value: 1450 },
  { label: 'South America', value: 320 },
  { label: 'Africa', value: 180 },
  { label: 'Middle East', value: 240 },
  { label: 'Oceania', value: 95 },
  { label: 'Central Asia', value: 150 },
  { label: 'Caribbean', value: 85 },
  { label: 'Eastern Europe', value: 420 },
  { label: 'Nordic', value: 280 },
  { label: 'Southeast Asia', value: 650 },
  { label: 'East Africa', value: 120 },
  { label: 'West Africa', value: 200 },
  { label: 'Central America', value: 110 },
];

export function TreeMapChartLarge() {
  return (
    <ChartContainer
      title='Global Revenue Distribution'
      description='Revenue breakdown across global regions'
    >
      <TreeMapChart
        data={largeData}
        config={{
          height: 400,
          padding: 12,
          showLabels: true,
          animated: true,
          duration: 1500,
        }}
      />
    </ChartContainer>
  );
}
```

## API Reference

### TreeMapChart

A customizable treemap chart component that uses the squarified treemap algorithm for optimal rectangle aspect ratios. Perfect for displaying hierarchical data with emphasis on proportional relationships between categories.

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

### TreeMapDataPoint

| Prop       | Type                 | Description                                    |
| ---------- | -------------------- | ---------------------------------------------- |
| `label`    | `string`             | The label for the data point.                  |
| `value`    | `number`             | The value for the data point.                  |
| `color`    | `string`             | Optional custom color for the rectangle.       |
| `children` | `TreeMapDataPoint[]` | Optional nested data for hierarchical display. |

### 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`  | `10`    | Padding around the chart.                         |
| `showLabels` | `boolean` | `true`  | Whether to show labels for rectangles.            |
| `animated`   | `boolean` | `true`  | Whether to animate the chart on load.             |
| `duration`   | `number`  | `800`   | Animation duration in milliseconds.               |

## Features

- **Squarified Algorithm**: Uses the squarified treemap algorithm for optimal rectangle aspect ratios
- **Smooth Animations**: Built-in animations using React Native Reanimated
- **Responsive Design**: Automatically adapts to container width
- **Custom Colors**: Support for individual rectangle colors with automatic color palette
- **Label Display**: Shows both category labels and values with smart text sizing
- **Theme Integration**: Uses theme colors for consistent styling
- **Hierarchical Support**: Prepared for nested data structures
- **Smart Text Rendering**: Automatically adjusts text size and color based on rectangle size

## Use Cases

TreeMap charts are particularly effective for:

- **Proportional Data**: Visualizing data where size represents importance or value
- **Portfolio Analysis**: Displaying asset allocation or investment distribution
- **Budget Visualization**: Showing spending breakdown across categories
- **Market Share**: Representing company or product market share
- **File System**: Displaying disk usage or file sizes
- **Organizational Data**: Showing department sizes or resource allocation
- **Survey Results**: Displaying response distributions with visual impact

## Algorithm

The TreeMapChart uses the **squarified treemap algorithm**, which:

- Minimizes the aspect ratio of rectangles for better readability
- Recursively subdivides the available space
- Optimizes for visual clarity by creating more square-like rectangles
- Handles varying data sizes efficiently

## Design Considerations

The TreeMapChart is designed for:

- **Proportional Visualization**: Rectangle size directly represents data values
- **Quick Comparison**: Easy to compare relative sizes at a glance
- **Space Efficiency**: Makes optimal use of available screen real estate
- **Visual Hierarchy**: Larger values are immediately apparent
- **Color Coding**: Uses distinct colors to differentiate categories

## Accessibility

The TreeMapChart component includes several accessibility features:

- Semantic SVG structure for screen readers
- Proper contrast ratios with automatic text color adjustment
- Text labels for both categories and values
- Supports dynamic text sizing based on rectangle size
- High contrast borders for visual separation

## Performance

The component is optimized for performance:

- Uses React Native Reanimated for smooth 60fps animations
- Efficient SVG rendering with minimal re-renders
- Optimized squarified algorithm implementation
- Automatic cleanup of animation values
- Responsive layout calculations

## Styling

The component integrates with your theme system:

- Uses a predefined color palette for consistent styling
- Automatic text color adjustment (light/dark) based on background
- Uses theme background color for borders
- Supports custom colors per data point
- Rounded corners with consistent border radius

## Animation

The chart features smooth entry animations:

- Rectangles animate from 0 size to full size
- Opacity fades in during animation
- Configurable animation duration
- Can be disabled for instant rendering
- Uses React Native Reanimated for optimal performance

## Text Rendering

Smart text rendering features:

- Automatic font size calculation based on rectangle dimensions
- Minimum font size threshold to prevent unreadable text
- Value display only when sufficient space is available
- Proper text centering within rectangles
- Automatic color contrast for readability

## Color System

The component uses a carefully selected color palette:

- 10 distinct colors for visual variety
- Automatic color assignment based on data index
- Support for custom colors per data point
- Automatic text color adjustment for contrast
- Consistent color cycling for large datasets
