shadimage

Chart

Modern, smooth Geist-inspired chart primitives: config, container, tooltip, legend, grid, and theme tokens.

components/ui/chart.tsx

"use client";

import * as React from "react";
import * as RechartsPrimitive from "recharts";

import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";
import { ChartReactionScope, ChartSkeleton, useChartReaction, useChartReactions, useChartReducedMotion, type ChartReactionOptions } from "@/components/ui/chart-reactions";

export type ChartConfig = Record<
  string,
  {
    label?: React.ReactNode;
    icon?: React.ComponentType<{ className?: string }>;
    color?: string;
    valueFormatter?: (value: number | string) => React.ReactNode;
    colors?: {
      light?: string[];
      dark?: string[];
    };
  }
>;

type ChartContextValue = {
  id: string;
  config: ChartConfig;
  data: Record<string, unknown>[];
  selected?: string;
  setSelected: (key?: string) => void;
};

const ChartContext = React.createContext<ChartContextValue | null>(null);

export function useChart() {
  const context = React.useContext(ChartContext);
  if (!context) {
    throw new Error("useChart must be used inside a chart");
  }
  return context;
}

export function colorVar(key: string, stop = 0) {
  return stop === 0
    ? `var(--color-${key})`
    : `var(--color-${key}-${stop})`;
}

export function ChartContainer({
  id,
  config,
  data,
  className,
  children,
  defaultSelectedDataKey,
  onSelectionChange,
  variant = "panel",
  isLoading = false,
  loadingVariant,
  reaction,
  style,
}: {
  id?: string;
  config: ChartConfig;
  data: Record<string, unknown>[];
  className?: string;
  children: React.ReactNode;
  defaultSelectedDataKey?: string;
  onSelectionChange?: (key?: string) => void;
  variant?: "panel" | "plain";
  isLoading?: boolean;
  loadingVariant?: ChartReactionOptions["loadingVariant"];
  reaction?: ChartReactionOptions;
  style?: React.CSSProperties;
}) {
  const settings = useChartReactions();
  const reducedMotion = useChartReducedMotion();
  isLoading = isLoading || Boolean(settings.isLoading);
  const { emotion, animationsEnabled } = useChartReaction({ isLoading, reaction });
  const contentRef = React.useRef<HTMLDivElement>(null);
  React.useEffect(() => {
    if (!isLoading && animationsEnabled && !reducedMotion) {
      const animation = contentRef.current?.animate?.(
        [{ opacity: 0, transform: "translateY(6px)" }, { opacity: 1, transform: "translateY(0)" }],
        { duration: 450, easing: "cubic-bezier(0.22,1,0.36,1)" }
      );
      return () => animation?.cancel();
    }
  }, [isLoading, animationsEnabled, reducedMotion, settings.replayKey]);
  const generatedId = React.useId().replace(/:/g, "");
  const chartId = id ?? generatedId;
  const [selected, setSelectedState] = React.useState<string | undefined>(
    defaultSelectedDataKey
  );

  const setSelected = React.useCallback(
    (key?: string) => {
      setSelectedState((current) => {
        const next = current === key ? undefined : key;
        onSelectionChange?.(next);
        return next;
      });
    },
    [onSelectionChange]
  );

  return (
    <ChartContext.Provider
      value={{ id: chartId, config, data, selected, setSelected }}
    >
      <div
        data-chart={chartId}
        aria-busy={isLoading}
        style={style}
        className={cn(
          "relative flex aspect-auto w-full flex-col justify-end text-xs",
          "[&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-axis-tick_text]:font-mono",
          "[&_.recharts-cartesian-grid-horizontal_line]:stroke-border [&_.recharts-cartesian-grid-vertical_line]:stroke-border",
          "[&_.recharts-dot]:stroke-background [&_.recharts-curve]:[stroke-linejoin:round] [&_.recharts-curve]:[stroke-linecap:round]", 
          "[&_.recharts-surface]:relative [&_.recharts-surface]:z-0 [&_.recharts-tooltip-wrapper]:z-10",
          variant === "panel" &&
            "rounded-lg border border-border bg-card p-4 sm:p-5",
          variant === "plain" && "rounded-none border-0 bg-transparent shadow-none",
          className
        )}
      >
        <ChartStyle id={chartId} config={config} />
        <div ref={contentRef} key={settings.replayKey} className="relative z-[1] flex h-full min-h-0 w-full flex-1 flex-col justify-end">
          <ChartSkeleton isLoading={isLoading}>
            <ChartReactionScope active={Boolean(emotion)}>{children}</ChartReactionScope>
          </ChartSkeleton>
        </div>
      </div>
    </ChartContext.Provider>
  );
}

function ChartStyle({ id, config }: { id: string; config: ChartConfig }) {
  const rules = Object.entries(config)
    .map(([key, item]) => {
      const dark = item.colors?.dark ?? (item.color ? [item.color] : ["var(--chart-1)"]);
      const light =
        item.colors?.light ?? (item.color ? [item.color] : ["var(--chart-1)"]);
      const darkStops = dark
        .map((color, index) =>
          index === 0
            ? `--color-${key}: ${color}; --color-${key}-0: ${color};`
            : `--color-${key}-${index}: ${color};`
        )
        .join(" ");
      const lightStops = light
        .map((color, index) =>
          index === 0
            ? `--color-${key}: ${color}; --color-${key}-0: ${color};`
            : `--color-${key}-${index}: ${color};`
        )
        .join(" ");
      return `[data-chart="${id}"]{${darkStops}}.light [data-chart="${id}"]{${lightStops}}`;
    })
    .join("\n");

  return <style dangerouslySetInnerHTML={{ __html: rules }} />;
}

function seriesKey(
  item: { dataKey?: string | number; name?: string; value?: string | number },
  config: ChartConfig
) {
  const fromDataKey = String(item.dataKey ?? "");
  const fromName = String(item.name ?? "");
  const fromValue = String(item.value ?? "");
  if (config[fromName]) return fromName;
  if (config[fromValue]) return fromValue;
  if (config[fromDataKey]) return fromDataKey;
  return fromDataKey || fromName || fromValue;
}

function uniquePayload<T extends { dataKey?: string | number; name?: string; value?: string | number }>(
  payload: T[] | undefined,
  config: ChartConfig
) {
  if (!payload?.length) return [];
  const seen = new Set<string>();
  const rows: Array<{ item: T; key: string; index: number }> = [];
  payload.forEach((item, index) => {
    const key = seriesKey(item, config);
    if (!key || seen.has(key)) return;
    seen.add(key);
    rows.push({ item, key, index });
  });
  return rows;
}

function formatChartNumber(value: number | string | undefined) {
  if (typeof value === "number") return value.toLocaleString("en-US");
  return value ?? "";
}

type ChartTooltipPayload = {
  dataKey?: string | number;
  name?: string;
  value?: number | string;
  color?: string;
  payload?: Record<string, unknown>;
};

export function ChartTooltipContent({
  active,
  payload,
  label,
  labelFormatter,
  roundness = "sm",
  variant = "reference",
  className,
}: {
  active?: boolean;
  payload?: ChartTooltipPayload[];
  label?: string | number;
  labelFormatter?: (label: React.ReactNode, payload: ChartTooltipPayload[]) => React.ReactNode;
  roundness?: "sm" | "md" | "lg" | "full";
  variant?: "reference" | "default";
  className?: string;
}) {
  const { config, selected } = useChart();
  if (!active || !payload?.length) return null;

  const items = uniquePayload(payload, config);
  const rows = (items.length <= 1 ? items : items.filter(
    ({ key, item }) =>
      !selected ||
      selected === key ||
      selected === String(item.dataKey ?? "") ||
      selected === String(item.name ?? "")
  ));

  if (!rows.length) return null;

  const header = labelFormatter ? labelFormatter(label, payload) : label;

  return (
    <div role="tooltip" data-variant={variant} className={cn(
      "pointer-events-none relative min-w-48 overflow-hidden border-0 px-3.5 py-3 text-xs shadow-lg",
      variant === "reference"
        ? "bg-[var(--chart-tooltip-background)] text-[var(--chart-tooltip-foreground)]"
        : "bg-popover text-popover-foreground",
      { sm: "rounded-sm", md: "rounded-md", lg: "rounded-lg", full: "rounded-2xl" }[roundness],
      className
    )}>
      {header !== undefined && header !== null && header !== "" ? (
        <p className="mb-2 pr-12 text-[11px] font-bold">{header}</p>
      ) : null}
      <div className="absolute right-3 top-3"><SadTooltipGif /></div>
      <div className="space-y-2">
        {rows.map(({ item, key, index }) => {
          const series = config[key];
          const Icon = series?.icon;
          return (
            <div
              key={`${key}-${index}`}
              className="flex items-center justify-between gap-6"
            >
              <span className={cn("flex items-center gap-2", variant === "reference" ? "text-[var(--chart-tooltip-muted)]" : "text-muted-foreground")}>
                {Icon ? (
                  <Icon className="size-3" />
                ) : (
                  <PixelSwatch color={series ? colorVar(key) : item.color ?? "var(--chart-1)"} />
                )}
                {series?.label ?? key}
              </span>
              <span className="ml-auto text-right font-mono font-bold tabular-nums">
                {series?.valueFormatter && item.value !== undefined
                  ? series.valueFormatter(item.value)
                  : formatChartNumber(item.value)}
              </span>
            </div>
          );
        })}
      </div>
    </div>
  );
}

export function SadTooltipGif({ className }: { className?: string }) {
  const settings = useChartReactions();
  const reducedMotion = useChartReducedMotion();
  if (settings.isLoading || reducedMotion) return null;
  if (settings.reaction?.emotion !== "sad") return null;
  const asset = { ...settings.assets?.sad, ...settings.reaction?.assets?.sad };
  const src = asset.src?.trim();
  if (!src) return null;
  return <img src={src} alt="" width={40} height={40} className={cn("size-10 shrink-0 rounded-md object-cover", className)} />;
}

export function ChartTooltipSurface({
  title,
  children,
  id,
  className,
}: {
  title?: React.ReactNode;
  children?: React.ReactNode;
  id?: string;
  className?: string;
}) {
  return (
    <div
      id={id}
      role="tooltip"
      className={cn(
        "pointer-events-none relative min-w-48 overflow-hidden rounded-sm border-0 bg-[var(--chart-tooltip-background)] px-3.5 py-3 text-xs text-[var(--chart-tooltip-foreground)] shadow-lg",
        className
      )}
    >
      {title !== undefined && title !== null && title !== "" ? (
        <p className="mb-2 pr-12 text-[11px] font-bold">{title}</p>
      ) : null}
      <div className="absolute right-3 top-3"><SadTooltipGif /></div>
      <div className="space-y-2">{children}</div>
    </div>
  );
}

export function ChartHoverTooltip({
  children,
  content,
  className,
  onActiveChange,
}: {
  children: React.ReactNode;
  content: React.ReactNode;
  className?: string;
  onActiveChange?: (active: boolean) => void;
}) {
  const [open, setOpen] = React.useState(false);
  const set = React.useCallback(
    (next: boolean) => {
      setOpen(next);
      onActiveChange?.(next);
    },
    [onActiveChange]
  );
  return (
    <div
      className={cn("relative", className)}
      onMouseEnter={() => set(true)}
      onMouseLeave={() => set(false)}
      onFocus={() => set(true)}
      onBlur={() => set(false)}
      onKeyDown={(event) => {
        if (event.key === "Escape") set(false);
      }}
    >
      {children}
      {open ? (
        <div className="pointer-events-none absolute left-1/2 top-full z-50 mt-2 w-max max-w-60 -translate-x-1/2">
          {content}
        </div>
      ) : null}
    </div>
  );
}

export function ChartHeading({
  title,
  value,
}: {
  title?: string;
  value?: string;
}) {
  if (!title && !value) return null;
  return (
    <div className="min-w-0">
      {title ? <p className="truncate text-[15px] font-medium tracking-tight">{title}</p> : null}
      {value ? <p className="mt-1 tabular-nums text-5xl font-medium tracking-tight">{value}</p> : null}
    </div>
  );
}

export function ChartTooltipCursor({
  axisLabelFormatter,
}: {
  axisLabelFormatter?: (label: string | number) => string;
}) {
  const label = RechartsPrimitive.useActiveTooltipLabel();
  const coordinate = RechartsPrimitive.useActiveTooltipCoordinate();
  const plot = RechartsPrimitive.usePlotArea();
  const layout = RechartsPrimitive.useChartLayout();
  const active = RechartsPrimitive.useIsTooltipActive();
  if (!active || !plot || !coordinate || layout !== "horizontal") return null;

  const text = label === undefined ? "" : axisLabelFormatter?.(label) ?? String(label);
  const width = Math.min(plot.width, Math.max(40, text.length * 7 + 20));
  const x = Math.max(plot.x + width / 2, Math.min(coordinate.x, plot.x + plot.width - width / 2));
  const bottom = plot.y + plot.height;

  return (
    <RechartsPrimitive.ZIndexLayer zIndex={3000}>
      <g aria-hidden="true" className="pointer-events-none" data-chart-cursor="">
        <line x1={coordinate.x} x2={coordinate.x} y1={plot.y} y2={bottom} stroke="var(--chart-cursor)" strokeWidth={1} strokeDasharray="none" />
        {text ? (
          <g
            ref={(node) => {
              if (!node) return;
              const pill = node.getBoundingClientRect();
              const ticks = node.ownerSVGElement?.querySelectorAll<SVGGElement>(".recharts-xAxis-tick-labels > g");
              const hidden: Array<{ tick: SVGGElement; visibility: string }> = [];
              ticks?.forEach((tick) => {
                const bounds = tick.getBoundingClientRect();
                if (bounds.right > pill.left - 4 && bounds.left < pill.right + 4 && bounds.bottom > pill.top && bounds.top < pill.bottom) {
                  hidden.push({ tick, visibility: tick.style.visibility });
                  tick.style.visibility = "hidden";
                }
              });
              return () => hidden.forEach(({ tick, visibility }) => { tick.style.visibility = visibility; });
            }}
          >
            <rect x={x - width / 2} y={bottom + 4} width={width} height={24} rx={12} fill="var(--chart-cursor)" />
            <text x={x} y={bottom + 16} textAnchor="middle" dominantBaseline="central" fill="var(--chart-cursor-foreground)" className="font-mono text-xs font-semibold">{text}</text>
          </g>
        ) : null}
      </g>
    </RechartsPrimitive.ZIndexLayer>
  );
}

type ChartTooltipProps = React.ComponentProps<typeof RechartsPrimitive.Tooltip> & {
  contentProps?: Pick<React.ComponentProps<typeof ChartTooltipContent>, "variant" | "roundness" | "className" | "labelFormatter">;
  axisLabelFormatter?: (label: string | number) => string;
};

export function ChartTooltip({ contentProps, axisLabelFormatter, labelFormatter, cursor, ...props }: ChartTooltipProps) {
  return (
    <RechartsPrimitive.Tooltip
      content={<ChartTooltipContent labelFormatter={labelFormatter as React.ComponentProps<typeof ChartTooltipContent>["labelFormatter"]} {...contentProps} />}
      cursor={cursor ?? <ChartTooltipCursor axisLabelFormatter={axisLabelFormatter} />}
      {...props}
    />
  );
}
ChartTooltip.displayName = "Tooltip";

export function ChartTooltipLabelFormatter(props: Omit<ChartTooltipProps, "content">) {
  return <ChartTooltip {...props} />;
}
ChartTooltipLabelFormatter.displayName = "Tooltip";

export function ChartLegendContent({
  payload,
  isClickable,
}: {
  payload?: Array<{ dataKey?: string | number; value?: string; color?: string }>;
  isClickable?: boolean;
}) {
  const { config, selected, setSelected } = useChart();
  if (!payload?.length) return null;

  return (
    <div className="flex justify-center pt-3">
      <div
        role="group"
        aria-label="Chart series"
        className="inline-flex max-w-full flex-wrap items-center justify-center gap-1 rounded-full border border-border bg-muted/40 p-1"
      >
        {uniquePayload(payload, config).map(({ key, index }) => {
          const series = config[key];
          const Icon = series?.icon;
          const active = selected === key;
          return (
            <Button
              key={`${key}-${index}`}
              type="button"
              variant="ghost"
              size="sm"
              disabled={!isClickable}
              aria-pressed={isClickable ? active : undefined}
              onClick={() => isClickable && setSelected(key)}
              className={cn(
                "h-8 min-w-0 shrink gap-2 rounded-full border border-transparent px-3 font-sans text-xs font-medium normal-case tracking-normal text-muted-foreground disabled:opacity-100",
                active && "border-border bg-background text-foreground shadow-sm",
                isClickable && "cursor-pointer hover:bg-accent hover:text-foreground"
              )}
            >
              {Icon ? (
                <Icon className="size-3 shrink-0" aria-hidden="true" />
              ) : (
                <PixelSwatch color={colorVar(key)} />
              )}
              <span className="truncate">{series?.label ?? key}</span>
            </Button>
          );
        })}
      </div>
    </div>
  );
}

export function ChartLegend({
  isClickable,
  ...props
}: React.ComponentProps<typeof RechartsPrimitive.Legend> & {
  isClickable?: boolean;
}) {
  return (
    <RechartsPrimitive.Legend
      content={<ChartLegendContent isClickable={isClickable} />}
      {...props}
    />
  );
}
ChartLegend.displayName = "Legend";

export function ChartGrid(props: React.ComponentProps<typeof RechartsPrimitive.CartesianGrid>) {
  return (
    <RechartsPrimitive.CartesianGrid
      vertical={false}
      stroke="var(--border)"
      strokeDasharray="4 4"
      {...props}
    />
  );
}
ChartGrid.displayName = "CartesianGrid";

export function pixelPatternId(scope: string, key: string) {
  return `${scope}-${String(key).replace(/[^a-zA-Z0-9_-]/g, "_")}-px`;
}

export function pixelPatternUrl(scope: string, key: string) {
  return `url(#${pixelPatternId(scope, key)})`;
}

export function pixelFillStyle(color: string): React.CSSProperties {
  return { backgroundColor: color };
}

export function PixelSwatch({ color }: { color: string }) {
  return (
    <span
      aria-hidden="true"
      className="size-2 shrink-0 rounded-full"
      style={pixelFillStyle(color)}
    />
  );
}

export function HatchPattern({
  id,
  color,
}: {
  id: string;
  color: string;
}) {
  return (
    <pattern id={id} width="6" height="6" patternUnits="userSpaceOnUse" patternTransform="rotate(45)">
      <rect width="6" height="6" fill={color} fillOpacity={0.08} />
      <line x1="0" y1="0" x2="0" y2="6" stroke={color} strokeOpacity={0.45} strokeWidth={1.5} />
    </pattern>
  );
}

export function GradientFill({
  id,
  color,
  startOpacity = 0.5,
  endOpacity = 0.1,
}: {
  id: string;
  color: string;
  startOpacity?: number;
  endOpacity?: number;
}) {
  return (
    <linearGradient id={id} x1="0" y1="0" x2="0" y2="1">
      <stop offset="0%" stopColor={color} stopOpacity={startOpacity} />
      <stop offset="100%" stopColor={color} stopOpacity={endOpacity} />
    </linearGradient>
  );
}

export const monthlyData = [
  { month: "January", desktop: 342, mobile: 184, tablet: 98 },
  { month: "February", desktop: 876, mobile: 491, tablet: 210 },
  { month: "March", desktop: 512, mobile: 290, tablet: 140 },
  { month: "April", desktop: 629, mobile: 391, tablet: 168 },
  { month: "May", desktop: 458, mobile: 309, tablet: 132 },
  { month: "June", desktop: 781, mobile: 449, tablet: 190 },
  { month: "July", desktop: 394, mobile: 234, tablet: 110 },
  { month: "August", desktop: 925, mobile: 557, tablet: 240 },
  { month: "September", desktop: 647, mobile: 367, tablet: 155 },
  { month: "October", desktop: 532, mobile: 357, tablet: 148 },
  { month: "November", desktop: 803, mobile: 515, tablet: 205 },
  { month: "December", desktop: 271, mobile: 149, tablet: 88 },
];

export const trafficConfig = {
  desktop: {
    label: "Desktop",
    colors: { dark: ["var(--chart-1)"], light: ["var(--chart-1)"] },
  },
  mobile: {
    label: "Mobile",
    colors: { dark: ["var(--chart-2)"], light: ["var(--chart-2)"] },
  },
  tablet: {
    label: "Tablet",
    colors: { dark: ["var(--chart-3)"], light: ["var(--chart-3)"] },
  },
} satisfies ChartConfig;

export const shareData = [
  { browser: "chrome", visitors: 275 },
  { browser: "safari", visitors: 200 },
  { browser: "firefox", visitors: 187 },
  { browser: "edge", visitors: 173 },
  { browser: "other", visitors: 90 },
];

export const salesByCategory = [
  { category: "electronics", sales: 4250 },
  { category: "clothing", sales: 3120 },
  { category: "food", sales: 2100 },
  { category: "home", sales: 1580 },
  { category: "other", sales: 1050 },
];

export const salesByCategoryConfig = {
  electronics: {
    label: "Electronics",
    colors: { dark: ["var(--chart-1)"], light: ["var(--chart-1)"] },
  },
  clothing: {
    label: "Clothing",
    colors: { dark: ["var(--chart-4)"], light: ["var(--chart-4)"] },
  },
  food: {
    label: "Food",
    colors: { dark: ["var(--chart-3)"], light: ["var(--chart-3)"] },
  },
  home: {
    label: "Home",
    colors: { dark: ["var(--chart-2)"], light: ["var(--chart-2)"] },
  },
  other: {
    label: "Other",
    colors: { dark: ["var(--chart-5)"], light: ["var(--chart-5)"] },
  },
} satisfies ChartConfig;

export const shareConfig = {
  chrome: {
    label: "Chrome",
    colors: { dark: ["var(--chart-1)"], light: ["var(--chart-1)"] },
  },
  safari: {
    label: "Safari",
    colors: { dark: ["var(--chart-2)"], light: ["var(--chart-2)"] },
  },
  firefox: {
    label: "Firefox",
    colors: { dark: ["var(--chart-3)"], light: ["var(--chart-3)"] },
  },
  edge: {
    label: "Edge",
    colors: { dark: ["var(--chart-4)"], light: ["var(--chart-4)"] },
  },
  other: {
    label: "Other",
    colors: { dark: ["var(--chart-5)"], light: ["var(--chart-5)"] },
  },
  visitors: {
    label: "Visitors",
    colors: { dark: ["var(--chart-1)"], light: ["var(--chart-1)"] },
  },
} satisfies ChartConfig;

export function formatChartCurrency(value: number | string) {
  return Number(value).toLocaleString("en-US", {
    style: "currency",
    currency: "USD",
    maximumFractionDigits: 0,
  });
}

export const composedDaily = Array.from({ length: 30 }, (_, index) => {
  const daily = Math.round(
    10200 + index * 90 + Math.sin(index / 2.6) * 1200 + Math.exp(-((index - 14) ** 2) / 40) * 3600
  );
  const average = Math.round(daily * 0.95 + 2600);
  const trend = Math.round(daily * 0.9 + 6400);
  return { day: `Jan ${index + 1}`, daily, average, trend };
});

export const composedDailyConfig = {
  daily: {
    label: "Sales",
    valueFormatter: formatChartCurrency,
    colors: { dark: ["var(--chart-teal)"], light: ["var(--chart-teal)"] },
  },
  average: {
    label: "Average",
    valueFormatter: formatChartCurrency,
    colors: { dark: ["var(--chart-turquoise)"], light: ["var(--chart-turquoise)"] },
  },
  trend: {
    label: "Trend",
    valueFormatter: formatChartCurrency,
    colors: { dark: ["var(--chart-teal-strong)"], light: ["var(--chart-teal-strong)"] },
  },
} satisfies ChartConfig;

export const radarData = [
  { metric: "Design", current: 120, previous: 110 },
  { metric: "Code", current: 98, previous: 130 },
  { metric: "Speed", current: 86, previous: 90 },
  { metric: "Docs", current: 99, previous: 85 },
  { metric: "Support", current: 85, previous: 90 },
  { metric: "Sales", current: 65, previous: 74 },
];

export const radarConfig = {
  current: {
    label: "This quarter",
    colors: { dark: ["var(--chart-1)"], light: ["var(--chart-1)"] },
  },
  previous: {
    label: "Last quarter",
    colors: { dark: ["var(--chart-2)"], light: ["var(--chart-2)"] },
  },
} satisfies ChartConfig;

export const radialData = [
  { browser: "chrome", visitors: 90 },
  { browser: "safari", visitors: 72 },
  { browser: "firefox", visitors: 64 },
  { browser: "edge", visitors: 48 },
];

export const sankeyNodes = [
  { name: "Visit" },
  { name: "Signup" },
  { name: "Activate" },
  { name: "Paid" },
  { name: "Churn" },
];

export const sankeyLinks = [
  { source: 0, target: 1, value: 180 },
  { source: 0, target: 4, value: 40 },
  { source: 1, target: 2, value: 140 },
  { source: 1, target: 4, value: 40 },
  { source: 2, target: 3, value: 110 },
  { source: 2, target: 4, value: 30 },
];

export const sankeyConfig = {
  Visit: {
    label: "Visit",
    colors: { dark: ["var(--chart-1)"], light: ["var(--chart-1)"] },
  },
  Signup: {
    label: "Signup",
    colors: { dark: ["var(--chart-2)"], light: ["var(--chart-2)"] },
  },
  Activate: {
    label: "Activate",
    colors: { dark: ["var(--chart-3)"], light: ["var(--chart-3)"] },
  },
  Paid: {
    label: "Paid",
    colors: { dark: ["var(--chart-4)"], light: ["var(--chart-4)"] },
  },
  Churn: {
    label: "Churn",
    colors: { dark: ["var(--chart-5)"], light: ["var(--chart-5)"] },
  },
} satisfies ChartConfig;

export const dailyOverlay = [
  { day: "Mon", current: 42, previous: 38 },
  { day: "Tue", current: 68, previous: 51 },
  { day: "Wed", current: 51, previous: 47 },
  { day: "Thu", current: 44, previous: 62 },
  { day: "Fri", current: 71, previous: 58 },
  { day: "Sat", current: 63, previous: 49 },
  { day: "Sun", current: 28, previous: 41 },
];

export const overlayConfig = {
  current: {
    label: "This period",
    colors: { dark: ["var(--chart-1)"], light: ["var(--chart-1)"] },
  },
  previous: {
    label: "Last period",
    colors: { dark: ["var(--muted-foreground)"], light: ["var(--muted-foreground)"] },
  },
} satisfies ChartConfig;

export const metricSeries = [
  { month: "Jan", period: 82, today: 40 },
  { month: "Feb", period: 118, today: 52 },
  { month: "Mar", period: 168, today: 71 },
  { month: "Apr", period: 214, today: 88 },
  { month: "May", period: 236, today: 104 },
  { month: "Jun", period: 248, today: 121 },
  { month: "Jul", period: 258, today: 136 },
  { month: "Aug", period: 264, today: 148 },
  { month: "Sep", period: 272, today: 168 },
];

export const metricConfig = {
  period: {
    label: "Current period",
    colors: { dark: ["var(--chart-1)"], light: ["var(--chart-1)"] },
  },
  today: {
    label: "Today",
    colors: { dark: ["var(--chart-2)"], light: ["var(--chart-2)"] },
  },
} satisfies ChartConfig;

export const yearCompare = [
  { month: "Nov 3", thisYear: 48, lastYear: 36 },
  { month: "Nov 6", thisYear: 62, lastYear: 44 },
  { month: "Nov 9", thisYear: 91, lastYear: 58 },
  { month: "Nov 12", thisYear: 54, lastYear: 61 },
  { month: "Nov 15", thisYear: 73, lastYear: 49 },
  { month: "Nov 18", thisYear: 41, lastYear: 38 },
];

export const yearCompareConfig = {
  thisYear: {
    label: "This year",
    colors: { dark: ["var(--chart-1)"], light: ["var(--chart-1)"] },
  },
  lastYear: {
    label: "Last year",
    colors: { dark: ["var(--chart-2)"], light: ["var(--chart-2)"] },
  },
} satisfies ChartConfig;

export const rangeBand = [
  { month: "Jan", low: 18, high: 46, value: 32 },
  { month: "Feb", low: 22, high: 58, value: 41 },
  { month: "Mar", low: 19, high: 51, value: 28 },
  { month: "Apr", low: 26, high: 64, value: 49 },
  { month: "May", low: 24, high: 61, value: 44 },
  { month: "Jun", low: 31, high: 72, value: 58 },
];

export const rangeConfig = {
  high: {
    label: "Upper",
    colors: { dark: ["var(--chart-1)"], light: ["var(--chart-1)"] },
  },
  low: {
    label: "Lower",
    colors: { dark: ["var(--muted)"], light: ["var(--muted)"] },
  },
  value: {
    label: "Actual",
    colors: { dark: ["var(--chart-1)"], light: ["var(--chart-1)"] },
  },
} satisfies ChartConfig;

export const paymentMix = [
  { key: "paid", label: "Settled", value: 48210, percent: 64.2 },
  { key: "open", label: "Open", value: 12840, percent: 17.1 },
  { key: "retry", label: "Retrying", value: 7620, percent: 10.2 },
  { key: "void", label: "Voided", value: 4310, percent: 5.7 },
  { key: "back", label: "Chargeback", value: 2110, percent: 2.8 },
];

export const cohortMix = [
  { key: "renew", label: "Renewing", value: 4120, percent: 46.1 },
  { key: "join", label: "Joined", value: 2680, percent: 30.0 },
  { key: "pause", label: "Paused", value: 980, percent: 11.0 },
  { key: "once", label: "One-time", value: 710, percent: 7.9 },
  { key: "trial", label: "Trial", value: 450, percent: 5.0 },
];

export const mixConfig = {
  paid: { label: "Settled", colors: { dark: ["var(--chart-2)"], light: ["var(--chart-2)"] } },
  open: { label: "Open", colors: { dark: ["var(--chart-4)"], light: ["var(--chart-4)"] } },
  retry: { label: "Retrying", colors: { dark: ["var(--chart-3)"], light: ["var(--chart-3)"] } },
  void: { label: "Voided", colors: { dark: ["var(--chart-1)"], light: ["var(--chart-1)"] } },
  back: { label: "Chargeback", colors: { dark: ["var(--chart-5)"], light: ["var(--chart-5)"] } },
  renew: { label: "Renewing", colors: { dark: ["var(--chart-2)"], light: ["var(--chart-2)"] } },
  join: { label: "Joined", colors: { dark: ["var(--chart-4)"], light: ["var(--chart-4)"] } },
  pause: { label: "Paused", colors: { dark: ["var(--chart-3)"], light: ["var(--chart-3)"] } },
  once: { label: "One-time", colors: { dark: ["var(--chart-1)"], light: ["var(--chart-1)"] } },
  trial: { label: "Trial", colors: { dark: ["var(--chart-5)"], light: ["var(--chart-5)"] } },
} satisfies ChartConfig;

export const marketRank = [
  { region: "United States", code: "US", current: 58200, previous: 49400 },
  { region: "India", code: "IN", current: 14500, previous: 16200 },
  { region: "United Kingdom", code: "GB", current: 9800, previous: 9100 },
  { region: "Germany", code: "DE", current: 7200, previous: 7800 },
  { region: "Canada", code: "CA", current: 5100, previous: 4300 },
  { region: "Australia", code: "AU", current: 3600, previous: 3900 },
];

export const marketConfig = {
  current: {
    label: "This period",
    colors: { dark: ["var(--chart-1)"], light: ["var(--chart-1)"] },
  },
  previous: {
    label: "Last period",
    colors: { dark: ["var(--chart-2)"], light: ["var(--chart-2)"] },
  },
} satisfies ChartConfig;

export const ringMembers = [
  { key: "new", label: "New", value: 6123 },
  { key: "existing", label: "Existing", value: 6000 },
];

export const ringPayments = [
  { key: "captured", label: "Captured", value: 1000 },
  { key: "refunded", label: "Refunded", value: 900 },
  { key: "charged", label: "Chargebacks", value: 42 },
];

export const ringConfig = {
  new: { label: "New", colors: { dark: ["var(--chart-1)"], light: ["var(--chart-1)"] } },
  existing: { label: "Existing", colors: { dark: ["var(--chart-2)"], light: ["var(--chart-2)"] } },
  captured: { label: "Captured", colors: { dark: ["var(--chart-1)"], light: ["var(--chart-1)"] } },
  refunded: { label: "Refunded", colors: { dark: ["var(--chart-2)"], light: ["var(--chart-2)"] } },
  charged: { label: "Chargebacks", colors: { dark: ["var(--chart-3)"], light: ["var(--chart-3)"] } },
} satisfies ChartConfig;

export const cashflowMonths = [
  { month: "Feb", inflow: 78, outflow: -32 },
  { month: "Mar", inflow: 61, outflow: -41 },
  { month: "Apr", inflow: 112, outflow: -18 },
  { month: "May", inflow: 58, outflow: -44 },
  { month: "Jun", inflow: 134, outflow: -52 },
  { month: "Jul", inflow: 141, outflow: -38 },
  { month: "Aug", inflow: 99, outflow: -29 },
  { month: "Sep", inflow: 118, outflow: -47 },
  { month: "Oct", inflow: 64, outflow: -36 },
  { month: "Nov", inflow: 31, outflow: -22 },
  { month: "Dec", inflow: 88, outflow: -40 },
  { month: "Jan", inflow: 42, outflow: -19 },
];

export const cashflowConfig = {
  inflow: {
    label: "Inflow",
    colors: { dark: ["var(--chart-2)"], light: ["var(--chart-2)"] },
  },
  outflow: {
    label: "Outflow",
    colors: { dark: ["var(--chart-3)"], light: ["var(--chart-3)"] },
  },
} satisfies ChartConfig;

export const spotlightSeries = Array.from({ length: 24 }, (_, index) => {
  const wave = Math.sin(index / 2.4) * 18 + 42;
  const spike = index === 16 ? 28 : index === 7 ? 14 : 0;
  return {
    day: `Jun ${index + 1}`,
    current: Math.round(wave + spike + index * 1.4),
    previous: Math.round(wave * 0.72 + 8),
  };
});

export const spotlightConfig = {
  current: {
    label: "This month",
    colors: { dark: ["var(--chart-1)"], light: ["var(--chart-1)"] },
  },
  previous: {
    label: "Last month",
    colors: { dark: ["var(--muted-foreground)"], light: ["var(--muted-foreground)"] },
  },
} satisfies ChartConfig;

export const laneRows = [
  { key: "total", label: "Total", value: 640 },
  { key: "ok", label: "Succeeded", value: 418 },
  { key: "issuer", label: "Issuer decline", value: 36 },
  { key: "buyer", label: "Buyer decline", value: 22 },
  { key: "idle", label: "Not started", value: 164 },
];

export const laneConfig = {
  total: { label: "Total", colors: { dark: ["var(--chart-4)"], light: ["var(--chart-4)"] } },
  ok: { label: "Succeeded", colors: { dark: ["var(--chart-2)"], light: ["var(--chart-2)"] } },
  issuer: { label: "Issuer decline", colors: { dark: ["var(--destructive)"], light: ["var(--destructive)"] } },
  buyer: { label: "Buyer decline", colors: { dark: ["var(--chart-3)"], light: ["var(--chart-3)"] } },
  idle: { label: "Not started", colors: { dark: ["var(--muted-foreground)"], light: ["var(--muted-foreground)"] } },
} satisfies ChartConfig;

export { RechartsPrimitive };
Interaction
tooltip