Area Chart

Renders an area chart for data visualization.

Installation

pnpm dlx shadcn@latest add https://ui.vllnt.com/r/area-chart.json
bash

Code

import * as React from "react";

import { cn } from "../../lib/utils";

type Datum = {
  label?: string;
  value: number;
};

export type AreaChartProps = {
  color?: string;
  data: Datum[];
  gradientId?: string;
  height?: number;
  strokeWidth?: number;
  width?: number;
} & React.HTMLAttributes<HTMLDivElement>;

const DEFAULT_WIDTH = 340;
const DEFAULT_HEIGHT = 150;
const DEFAULT_STROKE_WIDTH = 2;

type ChartDimensions = { height: number; strokeWidth: number; width: number };

function computePoints(data: Datum[], dimensions: ChartDimensions) {
  const { height, strokeWidth, width } = dimensions;
  const values = data.map((point) => point.value);
  const minValue = Math.min(...values);
  const maxValue = Math.max(...values);
  const range = maxValue - minValue || 1;
  const safeWidth = Math.max(width - strokeWidth * 2, 0);
  const safeHeight = Math.max(height - strokeWidth * 2, 0);

  return data.map((point, index) => {
    const x =
      data.length === 1
        ? strokeWidth + safeWidth / 2
        : strokeWidth + (index / (data.length - 1)) * safeWidth;
    const ratio = (point.value - minValue) / range;
    const y = safeHeight - ratio * safeHeight + strokeWidth;
    return { x, y };
  });
}

function getPathCoordinates(data: Datum[], dimensions: ChartDimensions) {
  if (data.length === 0) return { area: "", line: "" };

  const points = computePoints(data, dimensions);
  const line = points
    .map((point, index) => `${index === 0 ? "M" : "L"}${point.x},${point.y}`)
    .join(" ");

  const area =
    points.length === 0
      ? ""
      : `M${points[0]?.x ?? 0},${dimensions.height} ${line} L${points.at(-1)?.x ?? 0},${dimensions.height}Z`;

  return { area, line };
}

export const AreaChart = React.forwardRef<HTMLDivElement, AreaChartProps>(
  (
    {
      className,
      color = "currentColor",
      data,
      gradientId = "area-chart-gradient",
      height = DEFAULT_HEIGHT,
      strokeWidth = DEFAULT_STROKE_WIDTH,
      width = DEFAULT_WIDTH,
      ...props
    },
    reference,
  ) => {
    const { area, line } = React.useMemo(
      () => getPathCoordinates(data, { height, strokeWidth, width }),
      [data, width, height, strokeWidth],
    );

    if (!line) return null;

    return (
      <div
        className={cn(
          "rounded-2xl border border-border bg-background/40 p-3",
          className,
        )}
        ref={reference}
        {...props}
      >
        <svg
          aria-label="Area chart"
          className="h-full w-full"
          height={height}
          role="img"
          viewBox={`0 0 ${width} ${height}`}
          width={width}
        >
          <defs>
            <linearGradient id={gradientId} x1="0" x2="0" y1="0" y2="1">
              <stop offset="0%" stopColor={color} stopOpacity="0.5" />
              <stop offset="100%" stopColor={color} stopOpacity="0" />
            </linearGradient>
          </defs>
          <path
            d={area}
            fill={`url(#${gradientId})`}
            stroke="none"
            vectorEffect="non-scaling-stroke"
          />
          <path
            d={line}
            fill="none"
            stroke={color}
            strokeLinecap="round"
            strokeLinejoin="round"
            strokeWidth={strokeWidth}
            vectorEffect="non-scaling-stroke"
          />
        </svg>
      </div>
    );
  },
);

AreaChart.displayName = "AreaChart";
typescript