deepmoon

Charts

Recharts wrappers themed with --chart-1 through --chart-5 instead of the library defaults.

Line

Bar

Area

Pie

Installation

$ npx shadcn@latest add https://deepmoon.dev/r/charts.json

npm dependencies: recharts

Props

ChartProps
PropTypeRequired
dataRecord<string, string | number>[]Yes
indexstringYes
seriesChartSeries[]Yes
heightnumberNo
classNamestringNo
PieChartProps
PropTypeRequired
dataPieChartDatum[]Yes
heightnumberNo
classNamestringNo

Code

charts.tsx
"use client";

import * as React from "react";
import type { TooltipContentProps } from "recharts";
import {
  ResponsiveContainer,
  LineChart as RLineChart,
  Line,
  BarChart as RBarChart,
  Bar,
  AreaChart as RAreaChart,
  Area,
  PieChart as RPieChart,
  Pie,
  Cell,
  XAxis,
  YAxis,
  CartesianGrid,
  Tooltip,
  Legend,
} from "recharts";

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

/** The deepmoon accent ramp — every chart cycles through these instead of a hardcoded palette. */
const CHART_COLORS = ["var(--chart-1)", "var(--chart-2)", "var(--chart-3)", "var(--chart-4)", "var(--chart-5)"];

function ChartTooltip({ active, payload, label }: TooltipContentProps) {
  if (!active || !payload?.length) return null;
  return (
    <div className="rounded-md border border-border bg-popover px-3 py-2 text-xs text-popover-foreground shadow-md">
      {label ? <p className="mb-1 font-medium text-foreground">{label}</p> : null}
      <div className="flex flex-col gap-0.5">
        {payload.map((entry, index) => (
          <div key={index} className="flex items-center gap-1.5">
            <span aria-hidden="true" className="size-2 rounded-full" style={{ backgroundColor: entry.color }} />
            <span className="text-muted-foreground">{entry.name}:</span>
            <span className="font-medium text-foreground">{entry.value}</span>
          </div>
        ))}
      </div>
    </div>
  );
}

const axisProps = {
  stroke: "var(--muted-foreground)",
  fontSize: 12,
  tickLine: false,
  axisLine: false,
} as const;
const legendStyle = { fontSize: 12, color: "var(--muted-foreground)" } as const;

export interface ChartSeries {
  key: string;
  label?: string;
}

export interface ChartProps {
  data: Record<string, string | number>[];
  index: string;
  series: ChartSeries[];
  height?: number;
  className?: string;
}

export function LineChart({ data, index, series, height = 300, className }: ChartProps) {
  return (
    <div className={cn("w-full", className)} style={{ height }}>
      <ResponsiveContainer width="100%" height="100%">
        <RLineChart data={data}>
          <CartesianGrid strokeDasharray="3 3" stroke="var(--border)" />
          <XAxis dataKey={index} {...axisProps} />
          <YAxis {...axisProps} />
          <Tooltip content={ChartTooltip} />
          <Legend wrapperStyle={legendStyle} />
          {series.map((s, i) => (
            <Line
              key={s.key}
              type="monotone"
              dataKey={s.key}
              name={s.label ?? s.key}
              stroke={CHART_COLORS[i % CHART_COLORS.length]}
              strokeWidth={2}
              dot={false}
            />
          ))}
        </RLineChart>
      </ResponsiveContainer>
    </div>
  );
}

export function BarChart({ data, index, series, height = 300, className }: ChartProps) {
  return (
    <div className={cn("w-full", className)} style={{ height }}>
      <ResponsiveContainer width="100%" height="100%">
        <RBarChart data={data}>
          <CartesianGrid strokeDasharray="3 3" stroke="var(--border)" />
          <XAxis dataKey={index} {...axisProps} />
          <YAxis {...axisProps} />
          <Tooltip content={ChartTooltip} cursor={{ fill: "var(--accent)" }} />
          <Legend wrapperStyle={legendStyle} />
          {series.map((s, i) => (
            <Bar key={s.key} dataKey={s.key} name={s.label ?? s.key} fill={CHART_COLORS[i % CHART_COLORS.length]} radius={[4, 4, 0, 0]} />
          ))}
        </RBarChart>
      </ResponsiveContainer>
    </div>
  );
}

export function AreaChart({ data, index, series, height = 300, className }: ChartProps) {
  return (
    <div className={cn("w-full", className)} style={{ height }}>
      <ResponsiveContainer width="100%" height="100%">
        <RAreaChart data={data}>
          <defs>
            {series.map((s, i) => (
              <linearGradient key={s.key} id={`deepmoon-area-${s.key}`} x1="0" y1="0" x2="0" y2="1">
                <stop offset="5%" stopColor={CHART_COLORS[i % CHART_COLORS.length]} stopOpacity={0.4} />
                <stop offset="95%" stopColor={CHART_COLORS[i % CHART_COLORS.length]} stopOpacity={0} />
              </linearGradient>
            ))}
          </defs>
          <CartesianGrid strokeDasharray="3 3" stroke="var(--border)" />
          <XAxis dataKey={index} {...axisProps} />
          <YAxis {...axisProps} />
          <Tooltip content={ChartTooltip} />
          <Legend wrapperStyle={legendStyle} />
          {series.map((s, i) => (
            <Area
              key={s.key}
              type="monotone"
              dataKey={s.key}
              name={s.label ?? s.key}
              stroke={CHART_COLORS[i % CHART_COLORS.length]}
              fill={`url(#deepmoon-area-${s.key})`}
              strokeWidth={2}
            />
          ))}
        </RAreaChart>
      </ResponsiveContainer>
    </div>
  );
}

export interface PieChartDatum {
  name: string;
  value: number;
}

export interface PieChartProps {
  data: PieChartDatum[];
  height?: number;
  className?: string;
}

export function PieChart({ data, height = 300, className }: PieChartProps) {
  return (
    <div className={cn("w-full", className)} style={{ height }}>
      <ResponsiveContainer width="100%" height="100%">
        <RPieChart>
          <Tooltip content={ChartTooltip} />
          <Legend wrapperStyle={legendStyle} />
          <Pie data={data} dataKey="value" nameKey="name" innerRadius="55%" outerRadius="80%" paddingAngle={2}>
            {data.map((_, index) => (
              <Cell key={index} fill={CHART_COLORS[index % CHART_COLORS.length]} stroke="var(--background)" strokeWidth={2} />
            ))}
          </Pie>
        </RPieChart>
      </ResponsiveContainer>
    </div>
  );
}