React Component Kit
Composite

Chart

Chart renders line, bar, radar, donut, pie, polar area, bubble, and scatter charts from a typed `{ type, data, options }` configuration. It includes signed and stacked bars, mixed bar-line datasets, reference-style 3D pie and donut rendering, animated legend visibility, square responsive radial layouts, hover tooltips, axes, grid lines, reduced-motion-aware animation, and an optional accessible data table.

Installation

npx shadcn@latest add https://rck.vibeboxph.com//r/chart.json

Live preview & controls

See related examples
Interactive line chart with 2 datasets.Chart playgroundCompletedTarget
Controls
<Chart
  type="line"
  data={chartData}
  options={{
    indexAxis: "x",
    plugins: {
      title: { display: true, text: "Chart playground", align: "start", fontSize: 15 },
      legend: { display: true },
      tooltip: { enabled: true },
      
    },
    scales: { x: { grid: { display: true } }, y: { grid: { display: true } } },
    animation={{ duration: 850 }}
  }}
/>

Anatomy

  • ChartThe responsive chart wrapper and SVG viewport.
  • dataLabels and dataset values used to calculate chart geometry.
  • optionsChart.js-inspired visual, scale, plugin, and animation configuration.
  • data tableOptional visually hidden table for screen-reader access to chart values.

Props

PropTypeDefaultDescription
type*"line" | "bar" | "radar" | "donut" | "pie" | "polarArea" | "bubble" | "scatter"Chart controller used to render the supplied data.
data*ChartDataChart.js-inspired labels and datasets configuration.
optionsChartOptions | ChartRadialOptions{}Typed subset for responsive sizing, axes, legends, tooltips, titles, styling, and animation.
heightnumber | stringExplicit chart height. Without it, the wrapper keeps a readable minimum height and responsive aspect ratio.
showDataTablebooleanfalseRenders a visually hidden accessible table containing the chart data.
fallbackReact.ReactNodeContent shown when the chart has no usable data.
classNamestringMerged onto the responsive chart wrapper.
ChartDataset.data*(number | null | { x: number; y: number; r?: number })[]Numeric values for line/bar/radial charts or coordinate points for scatter/bubble charts.
ChartDataset.type / stack"bar" | "line" / stringOptional Cartesian dataset controller and stack group. Use dataset types together for mixed bar-line charts.
ChartDataset.backgroundColor / borderColorstring | string[]Single or per-point colors; defaults to the kit palette and theme tokens. Pie and donut charts are intentionally borderless.
ChartDataset.fill / tension / pointRadius / showLineboolean | numberLine and point rendering controls used by line, radar, scatter, and bubble charts.
options.plugins.legend / tooltip / titleChartLegendOptions | ChartTooltipOptions | ChartTitleOptionsControls chart chrome, hover feedback, titles, and clickable dataset visibility.
ChartTitleOptions.align"start" | "center" | "end"startAligns the chart title to the start, center, or end of the chart. String titles stay on one row; pass an array to provide explicit title rows.
ChartTitleOptions.fontSizenumber15Sets the chart title's rendered size in pixels and is normalized across radial and Cartesian chart viewports. The default is 15; use a string for a single-row title or an array for explicit multiline content.
options.plugins.threeDChartThreeDOptions (pie/donut only)Opt-in oblique SVG pie/donut projection with a 28-unit default depth, complete colored outer, inner, and radial sidewalls, an elliptical top face, perspective, and labels.
options.scales.x / y / rChartScaleOptionsAxis visibility, bounds, grid, tick formatting, titles, and stacking.
options.scales.*.ticksChartTicksOptionsTick formatting with automatic dense-label skipping, tick limits, and optional rotation controls.
options.animationfalse | ChartAnimation{ duration: 900, easing: 'easeOutQuart' }Mount/update animation settings with an optional completion callback.
options.transitions.show / hidefalse | ChartAnimation{ duration: 450, easing: 'easeOutQuart' }Overrides the legend dataset show/hide transition; rapid toggles reverse from the current geometry.
options.transitions.hoverfalse | ChartAnimation{ duration: 180, easing: 'easeOutCubic' }Overrides animated pie, donut, and polar-area sector hover expansion. The transition respects reduced-motion preferences and does not enable 3D on polar-area charts.
options.responsive / maintainAspectRatio / aspectRatio / indexAxisboolean | number | 'x' | 'y'Responsive sizing, aspect-ratio behavior, and horizontal Cartesian layout.

Accessibility

  • Renders the visual as an SVG with `role="img"`, an accessible name, and a descriptive `<desc>`.
  • Legend dataset toggles are keyboard-focusable SVG controls and expose pressed state.
  • Set `showDataTable` when users need a complete, linearized representation of the plotted values.
  • Tooltips are supplementary visual feedback; important values should also be available through visible text or the optional data table.
  • The mount animation automatically disables itself when the user prefers reduced motion.

Source

registry/rck/ui/chart.tsx
"use client";

import * as React from "react";
import { cn } from "@/lib/utils";

export type ChartType =
  "line" | "bar" | "radar" | "donut" | "pie" | "polarArea" | "bubble" | "scatter";

export type ChartEasing =
  | "linear"
  | "easeInQuad"
  | "easeOutQuad"
  | "easeInOutQuad"
  | "easeInCubic"
  | "easeOutCubic"
  | "easeInOutCubic"
  | "easeInQuart"
  | "easeOutQuart"
  | "easeInOutQuart"
  | "easeInQuint"
  | "easeOutQuint"
  | "easeInOutQuint"
  | "easeInSine"
  | "easeOutSine"
  | "easeInOutSine"
  | "easeInExpo"
  | "easeOutExpo"
  | "easeInOutExpo"
  | "easeInCirc"
  | "easeOutCirc"
  | "easeInOutCirc";

export interface ChartPoint {
  x: number;
  y: number;
}

export interface ChartBubblePoint extends ChartPoint {
  r: number;
}

export type ChartDataPoint = number | null | ChartPoint | ChartBubblePoint;
export type ChartColor = string | readonly string[];

export interface ChartDataset {
  label?: string;
  data: readonly ChartDataPoint[];
  type?: "bar" | "line";
  stack?: string;
  backgroundColor?: ChartColor;
  /** Ignored by pie and donut charts, which are intentionally borderless. */
  borderColor?: ChartColor;
  /** Ignored by pie and donut charts, which are intentionally borderless. */
  borderWidth?: number;
  borderRadius?: number;
  borderDash?: readonly number[];
  fill?: boolean;
  tension?: number;
  pointRadius?: number;
  pointHoverRadius?: number;
  showLine?: boolean;
  hidden?: boolean;
  hoverOffset?: number;
}

export interface ChartData {
  labels?: readonly string[];
  datasets: readonly ChartDataset[];
}

export interface ChartTickContext {
  value: number | string;
  index: number;
}

export interface ChartTooltipContext {
  datasetIndex: number;
  dataIndex: number;
  datasetLabel?: string;
  label?: string;
  value: number | string;
  raw: ChartDataPoint;
}

export interface ChartAnimation {
  duration?: number;
  delay?: number;
  easing?: ChartEasing;
  onComplete?: () => void;
}

export interface ChartTitleOptions {
  display?: boolean;
  text?: string | readonly string[];
  color?: string;
  fontSize?: number;
  align?: "start" | "center" | "end";
}

export interface ChartLegendLabelOptions {
  color?: string;
  fontSize?: number;
  boxWidth?: number;
}

export interface ChartLegendOptions {
  display?: boolean;
  position?: "top" | "bottom" | "left" | "right";
  align?: "start" | "center" | "end";
  labels?: ChartLegendLabelOptions;
}

export interface ChartThreeDOptions {
  enabled?: boolean;
  /** Extrusion depth in SVG viewBox units. */
  depth?: number;
  /** Horizontal top-face offset in SVG viewBox units. */
  tilt?: number;
  /** Vertical compression of the projected top face, from 0.35 to 1. */
  perspective?: number;
  labels?: boolean;
  labelFormatter?: (context: ChartTooltipContext) => React.ReactNode;
}

export interface ChartScaleTitleOptions {
  display?: boolean;
  text?: string;
  color?: string;
  fontSize?: number;
}

export interface ChartGridOptions {
  display?: boolean;
  color?: string;
  lineWidth?: number;
  drawBorder?: boolean;
}

export interface ChartTicksOptions {
  display?: boolean;
  color?: string;
  fontSize?: number;
  count?: number;
  stepSize?: number;
  autoSkip?: boolean;
  maxTicksLimit?: number;
  minRotation?: number;
  maxRotation?: number;
  callback?: (context: ChartTickContext) => React.ReactNode;
}

export interface ChartScaleOptions {
  display?: boolean;
  min?: number;
  max?: number;
  beginAtZero?: boolean;
  stacked?: boolean;
  title?: ChartScaleTitleOptions;
  grid?: ChartGridOptions;
  ticks?: ChartTicksOptions;
}

export interface ChartTooltipOptions {
  enabled?: boolean;
  mode?: "nearest" | "index";
  callbacks?: {
    title?: (contexts: readonly ChartTooltipContext[]) => React.ReactNode;
    label?: (context: ChartTooltipContext) => React.ReactNode;
  };
}

interface ChartPluginOptions {
  title?: ChartTitleOptions;
  legend?: ChartLegendOptions;
  tooltip?: ChartTooltipOptions;
}

export interface ChartOptions {
  responsive?: boolean;
  maintainAspectRatio?: boolean;
  aspectRatio?: number;
  indexAxis?: "x" | "y";
  animation?: ChartAnimation | false;
  transitions?: {
    show?: ChartAnimation | false;
    hide?: ChartAnimation | false;
    hover?: ChartAnimation | false;
  };
  cutout?: number | string;
  layout?: {
    padding?: number | { top?: number; right?: number; bottom?: number; left?: number };
  };
  plugins?: ChartPluginOptions & { threeD?: never };
  scales?: {
    x?: ChartScaleOptions;
    y?: ChartScaleOptions;
    r?: ChartScaleOptions;
  };
  elements?: {
    line?: { borderWidth?: number; tension?: number };
    point?: { radius?: number; hoverRadius?: number; borderWidth?: number };
    bar?: { borderWidth?: number; borderRadius?: number };
    arc?: { borderWidth?: number };
  };
}

export interface ChartRadialOptions extends Omit<ChartOptions, "plugins"> {
  plugins?: ChartPluginOptions & { threeD?: ChartThreeDOptions };
}

type ChartOptionsLike = ChartOptions | ChartRadialOptions;
type NonRadialChartType = Exclude<ChartType, "pie" | "donut">;

interface ChartPropsBase extends Omit<React.HTMLAttributes<HTMLDivElement>, "title"> {
  data: ChartData;
  height?: number | string;
  fallback?: React.ReactNode;
  showDataTable?: boolean;
}

export type ChartProps = ChartPropsBase &
  (
    | { type: "pie" | "donut"; options?: ChartRadialOptions }
    | { type: NonRadialChartType; options?: ChartOptions }
  );

const VIEWBOX_WIDTH = 720;
const VIEWBOX_HEIGHT = 400;
const RADIAL_VIEWBOX_SIZE = 560;
const DEFAULT_PALETTE = [
  "var(--primary)",
  "var(--success)",
  "var(--warning)",
  "#8b5cf6",
  "#0ea5e9",
  "#f97316",
] as const;
const DEFAULT_GRID_COLOR = "var(--border)";
const DEFAULT_TEXT_COLOR = "var(--muted-foreground)";
const DEFAULT_FOREGROUND = "var(--foreground)";
const DEFAULT_TITLE_ALIGN = "start" as const;
const DEFAULT_TITLE_FONT_SIZE = 15;
const DEFAULT_DURATION = 900;
const TOOLTIP_TRANSITION_DURATION = 160;

interface Point2D {
  x: number;
  y: number;
}

interface CartesianPoint extends Point2D {
  dataIndex: number;
  datasetIndex: number;
  value: number;
  raw: ChartDataPoint;
}

interface BarCorners {
  topLeft: boolean;
  topRight: boolean;
  bottomRight: boolean;
  bottomLeft: boolean;
}

interface PlotLayout {
  viewBoxWidth: number;
  viewBoxHeight: number;
  left: number;
  right: number;
  top: number;
  bottom: number;
  width: number;
  height: number;
  centerX: number;
  centerY: number;
  radius: number;
  bottomLegendHeight: number;
}

interface CartesianScales {
  xMin: number;
  xMax: number;
  yMin: number;
  yMax: number;
  xIsCategory: boolean;
  x: (value: number) => number;
  y: (value: number) => number;
}

interface TooltipState {
  x: number;
  y: number;
  active: { datasetIndex: number; dataIndex: number };
  contexts: readonly ChartTooltipContext[];
}

interface ChartViewport {
  width: number;
  height: number;
}

interface ChartRenderProps {
  data: ChartData;
  datasets: readonly ChartDataset[];
  type: ChartType;
  options: ChartOptionsLike;
  layout: PlotLayout;
  progress: number;
  datasetProgress: Readonly<Record<number, number>>;
  labels: readonly string[];
  active?: { datasetIndex: number; dataIndex: number } | null;
  hoverProgress?: number;
  onTooltip: (x: number, y: number, context: ChartTooltipContext) => void;
  onTooltipClear?: (relatedTarget: EventTarget | null) => void;
}

function clamp(value: number, min: number, max: number): number {
  return Math.min(max, Math.max(min, value));
}

function finite(value: unknown): value is number {
  return typeof value === "number" && Number.isFinite(value);
}

function numberValue(point: ChartDataPoint): number | null {
  if (finite(point)) return point;
  if (point && "y" in point && finite(point.y)) return point.y;
  return null;
}

function pointValue(point: ChartDataPoint): number | string {
  if (finite(point)) return point;
  if (point && "y" in point) return point.y;
  return "—";
}

function asPoint(
  point: ChartDataPoint,
  index: number,
): ChartPoint | ChartBubblePoint | null {
  if (point && typeof point === "object" && finite(point.x) && finite(point.y)) {
    return point;
  }
  if (finite(point)) return { x: index, y: point };
  return null;
}

function roundedBarPath(
  x: number,
  y: number,
  width: number,
  height: number,
  radius: number,
  corners: BarCorners,
): string {
  const safeRadius = Math.min(
    Math.max(0, radius),
    Math.max(0, width / 2),
    Math.max(0, height / 2),
  );
  const topLeft = corners.topLeft ? safeRadius : 0;
  const topRight = corners.topRight ? safeRadius : 0;
  const bottomRight = corners.bottomRight ? safeRadius : 0;
  const bottomLeft = corners.bottomLeft ? safeRadius : 0;
  return [
    `M${x + topLeft},${y}`,
    `L${x + width - topRight},${y}`,
    topRight ? `Q${x + width},${y} ${x + width},${y + topRight}` : `L${x + width},${y}`,
    `L${x + width},${y + height - bottomRight}`,
    bottomRight
      ? `Q${x + width},${y + height} ${x + width - bottomRight},${y + height}`
      : `L${x + width},${y + height}`,
    `L${x + bottomLeft},${y + height}`,
    bottomLeft
      ? `Q${x},${y + height} ${x},${y + height - bottomLeft}`
      : `L${x},${y + height}`,
    `L${x},${y + topLeft}`,
    topLeft ? `Q${x},${y} ${x + topLeft},${y}` : `L${x},${y}`,
    "Z",
  ].join(" ");
}

function stackedBarCorners(
  indexAxis: "x" | "y",
  value: number,
  end: number,
  final: { positive: number; negative: number },
): BarCorners {
  const atPositiveEnd = Math.abs(end - final.positive) < 0.0001;
  const atNegativeEnd = Math.abs(end - final.negative) < 0.0001;
  if (indexAxis === "y") {
    return value >= 0
      ? {
          topLeft: false,
          topRight: atPositiveEnd,
          bottomRight: atPositiveEnd,
          bottomLeft: false,
        }
      : {
          topLeft: atNegativeEnd,
          topRight: false,
          bottomRight: false,
          bottomLeft: atNegativeEnd,
        };
  }
  return value >= 0
    ? {
        topLeft: atPositiveEnd,
        topRight: atPositiveEnd,
        bottomRight: false,
        bottomLeft: false,
      }
    : {
        topLeft: false,
        topRight: false,
        bottomRight: atNegativeEnd,
        bottomLeft: atNegativeEnd,
      };
}

function outerBarCorners(indexAxis: "x" | "y", value: number): BarCorners {
  if (indexAxis === "y") {
    return value >= 0
      ? { topLeft: false, topRight: true, bottomRight: true, bottomLeft: false }
      : { topLeft: true, topRight: false, bottomRight: false, bottomLeft: true };
  }
  return value >= 0
    ? { topLeft: true, topRight: true, bottomRight: false, bottomLeft: false }
    : { topLeft: false, topRight: false, bottomRight: true, bottomLeft: true };
}

function getColor(
  color: ChartColor | undefined,
  index: number,
  fallback: string,
): string {
  if (typeof color === "string" || color == null) return color ?? fallback;
  return color[index % color.length] ?? fallback;
}

function getDatasetColor(dataset: ChartDataset, index: number): string {
  return getColor(
    dataset.backgroundColor,
    index,
    DEFAULT_PALETTE[index % DEFAULT_PALETTE.length],
  );
}

function getBorderColor(dataset: ChartDataset, index: number): string {
  return getColor(dataset.borderColor, index, getDatasetColor(dataset, index));
}

function getBorderWidth(
  dataset: ChartDataset,
  options: ChartOptionsLike,
  type: ChartType,
): number {
  if (type === "pie" || type === "donut") return 0;
  if (dataset.borderWidth != null) return dataset.borderWidth;
  if (type === "bar") return options.elements?.bar?.borderWidth ?? 0;
  if (type === "polarArea") {
    return options.elements?.arc?.borderWidth ?? 2;
  }
  return dataset.borderWidth ?? options.elements?.line?.borderWidth ?? 2;
}

function getPadding(options: ChartOptionsLike): {
  top: number;
  right: number;
  bottom: number;
  left: number;
} {
  const padding = options.layout?.padding;
  if (typeof padding === "number") {
    return { top: padding, right: padding, bottom: padding, left: padding };
  }
  return {
    top: padding?.top ?? 0,
    right: padding?.right ?? 0,
    bottom: padding?.bottom ?? 0,
    left: padding?.left ?? 0,
  };
}

function titleLines(title: ChartTitleOptions | undefined): string[] {
  if (!title?.text) return [];
  return typeof title.text === "string" ? [title.text] : [...title.text];
}

function hasLegend(
  options: ChartOptionsLike,
  datasets: readonly ChartDataset[],
): boolean {
  return options.plugins?.legend?.display ?? datasets.some((dataset) => dataset.label);
}

interface LegendLayoutItem {
  label: string;
  index: number;
  x: number;
  y: number;
}

interface LegendLayout {
  items: readonly LegendLayoutItem[];
  rows: number;
  height: number;
  sideWidth: number;
}

function getLegendLayout(
  options: ChartOptionsLike,
  datasets: readonly ChartDataset[],
  viewBoxWidth: number,
  viewBoxHeight: number,
): LegendLayout {
  const legend = options.plugins?.legend;
  if (!hasLegend(options, datasets)) {
    return { items: [], rows: 0, height: 0, sideWidth: 0 };
  }
  const position = legend?.position ?? "top";
  const labels = legend?.labels;
  const boxWidth = labels?.boxWidth ?? 10;
  const itemHeight = 26;
  const itemWidth = (label: string) => boxWidth + 16 + label.length * 6.2;
  const items = datasets.map((dataset, index) => ({
    label: dataset.label ?? `Dataset ${index + 1}`,
    index,
  }));
  const sideWidth = Math.max(132, ...items.map((item) => itemWidth(item.label) + 12));
  if (position === "left" || position === "right") {
    const startX = position === "left" ? 10 : viewBoxWidth - sideWidth + 10;
    return {
      items: items.map((item, index) => ({
        ...item,
        x: startX,
        y: 43 + index * itemHeight,
      })),
      rows: items.length,
      height: 0,
      sideWidth,
    };
  }

  const availableWidth = Math.max(1, viewBoxWidth - 24);
  const rows: Array<typeof items> = [];
  let currentRow: typeof items = [];
  let currentWidth = 0;
  items.forEach((item) => {
    const width = itemWidth(item.label);
    if (currentRow.length > 0 && currentWidth + width > availableWidth) {
      rows.push(currentRow);
      currentRow = [];
      currentWidth = 0;
    }
    currentRow.push(item);
    currentWidth += width;
  });
  if (currentRow.length > 0) rows.push(currentRow);
  const align = legend?.align ?? "center";
  const positioned = rows.flatMap((row, rowIndex) => {
    const rowWidth = row.reduce((sum, item) => sum + itemWidth(item.label), 0);
    const startX =
      align === "start"
        ? 12
        : align === "end"
          ? viewBoxWidth - rowWidth - 12
          : (viewBoxWidth - rowWidth) / 2;
    let x = startX;
    const y =
      position === "top"
        ? 43 + rowIndex * itemHeight
        : viewBoxHeight - 17 - (rows.length - rowIndex - 1) * itemHeight;
    return row.map((item) => {
      const result = { ...item, x, y };
      x += itemWidth(item.label);
      return result;
    });
  });
  return {
    items: positioned,
    rows: rows.length,
    height: rows.length * itemHeight,
    sideWidth,
  };
}

function getLayout(
  options: ChartOptionsLike,
  datasets: readonly ChartDataset[],
  type: ChartType,
  titleFontSize = DEFAULT_TITLE_FONT_SIZE,
): PlotLayout {
  const radial =
    type === "radar" || type === "polarArea" || type === "pie" || type === "donut";
  const viewBoxWidth = radial ? RADIAL_VIEWBOX_SIZE : VIEWBOX_WIDTH;
  const viewBoxHeight = radial ? RADIAL_VIEWBOX_SIZE : VIEWBOX_HEIGHT;
  const padding = getPadding(options);
  const title = options.plugins?.title;
  const legend = options.plugins?.legend;
  const titleHeight =
    title?.display && titleLines(title).length > 0
      ? titleLines(title).length * Math.max(17, titleFontSize * 1.15) + 8
      : 0;
  const legendDisplay = hasLegend(options, datasets);
  const legendPosition = legend?.position ?? "top";
  const legendLayout = getLegendLayout(options, datasets, viewBoxWidth, viewBoxHeight);
  const threeDOptions = getThreeDOptions(options);
  const threeDDepth =
    (type === "pie" || type === "donut") && (threeDOptions?.enabled ?? false)
      ? Math.round(clamp(threeDOptions?.depth ?? 28, 0, 40))
      : 0;
  const xAxisTitleHeight =
    options.scales?.x?.title?.display && options.scales.x.title.text ? 20 : 0;
  let left = 62 + padding.left;
  let right = 24 + padding.right;
  let top = (radial ? 34 : 26) + padding.top + titleHeight;
  let bottom = (radial ? 34 : 46) + padding.bottom + threeDDepth;

  if (legendDisplay && (legendPosition === "left" || legendPosition === "right")) {
    if (legendPosition === "left") left += legendLayout.sideWidth;
    else right += legendLayout.sideWidth;
  } else if (legendDisplay && legendPosition === "top") {
    top += legendLayout.height;
  } else if (legendDisplay && legendPosition === "bottom") {
    bottom += legendLayout.height + xAxisTitleHeight;
  } else {
    bottom += xAxisTitleHeight;
  }

  const width = Math.max(1, viewBoxWidth - left - right);
  const height = Math.max(1, viewBoxHeight - top - bottom);

  return {
    viewBoxWidth,
    viewBoxHeight,
    left,
    right,
    top,
    bottom,
    width,
    height,
    centerX: left + width / 2,
    centerY: top + height / 2,
    radius: Math.max(1, Math.min(width, height) / 2 - (radial ? 28 : 10)),
    bottomLegendHeight:
      legendDisplay && legendPosition === "bottom" ? legendLayout.height : 0,
  };
}

function extent(
  values: readonly number[],
  fallbackMin = 0,
  fallbackMax = 1,
): [number, number] {
  const finiteValues = values.filter(finite);
  if (finiteValues.length === 0) return [fallbackMin, fallbackMax];
  const min = Math.min(...finiteValues);
  const max = Math.max(...finiteValues);
  if (min === max) {
    const offset = min === 0 ? 1 : Math.abs(min) * 0.1;
    return [min - offset, max + offset];
  }
  return [min, max];
}

function resolveExtent(
  values: readonly number[],
  scale: ChartScaleOptions | undefined,
  defaultBeginAtZero: boolean,
): [number, number] {
  let [min, max] = extent(values);
  if (scale?.beginAtZero ?? defaultBeginAtZero) {
    if (min > 0) min = 0;
    if (max < 0) max = 0;
  }
  if (scale?.min != null) min = scale.min;
  if (scale?.max != null) max = scale.max;
  if (min === max) max = min + 1;
  return [min, max];
}

function buildCartesianScales(
  datasets: readonly ChartDataset[],
  labels: readonly string[],
  type: ChartType,
  options: ChartOptionsLike,
  layout: PlotLayout,
): CartesianScales {
  const indexAxis = options.indexAxis ?? "x";
  const pointValues = datasets.flatMap((dataset) =>
    dataset.data.flatMap((value, index) => {
      const point = asPoint(value, index);
      return point ? [point] : [];
    }),
  );
  const xValues = pointValues.map((point) => point.x);
  const yValues = pointValues.map((point) => point.y);
  const stacked = options.scales?.x?.stacked || options.scales?.y?.stacked;
  if (stacked && (type === "bar" || type === "line")) {
    const stackTotals = new Map<string, { positive: number; negative: number }[]>();
    datasets.forEach((dataset) => {
      if ((dataset.type ?? type) !== "bar") return;
      const stack = dataset.stack ?? "__default";
      const totals = stackTotals.get(stack) ?? [];
      while (totals.length < Math.max(labels.length, dataset.data.length)) {
        totals.push({ positive: 0, negative: 0 });
      }
      stackTotals.set(stack, totals);
      dataset.data.forEach((raw, dataIndex) => {
        const value = numberValue(raw);
        if (value == null) return;
        const total = totals[dataIndex];
        if (!total) return;
        if (value >= 0) {
          total.positive += value;
          yValues.push(total.positive);
        } else {
          total.negative += value;
          yValues.push(total.negative);
        }
      });
    });
  }
  const category = type === "line" || type === "bar" || type === "radar";
  const xIsCategory = category && indexAxis === "x";
  const numericXValues = category && indexAxis === "y" ? yValues : xValues;
  const numericYValues = category && indexAxis === "y" ? xValues : yValues;
  const hasBarDataset =
    type === "bar" || datasets.some((dataset) => dataset.type === "bar");
  const xExtent = xIsCategory
    ? [0, Math.max(0, labels.length - 1)]
    : resolveExtent(
        numericXValues,
        options.scales?.x,
        hasBarDataset && indexAxis === "y",
      );
  const yExtent =
    indexAxis === "y" && category
      ? [0, Math.max(0, labels.length - 1)]
      : resolveExtent(numericYValues, options.scales?.y, hasBarDataset);
  const xRange = xExtent[1] - xExtent[0] || 1;
  const yRange = yExtent[1] - yExtent[0] || 1;

  return {
    xMin: xExtent[0],
    xMax: xExtent[1],
    yMin: yExtent[0],
    yMax: yExtent[1],
    xIsCategory,
    x: (value) => layout.left + ((value - xExtent[0]) / xRange) * layout.width,
    y: (value) =>
      layout.top + layout.height - ((value - yExtent[0]) / yRange) * layout.height,
  };
}

function tickValues(
  min: number,
  max: number,
  scale: ChartScaleOptions | undefined,
): number[] {
  const count = Math.max(2, scale?.ticks?.count ?? 5);
  if (scale?.ticks?.stepSize && scale.ticks.stepSize > 0) {
    const values: number[] = [];
    for (
      let value = min;
      value <= max + scale.ticks.stepSize / 2;
      value += scale.ticks.stepSize
    ) {
      values.push(Number(value.toFixed(8)));
    }
    return values.length > 1 ? values : [min, max];
  }
  return Array.from(
    { length: count },
    (_, index) => min + ((max - min) * index) / (count - 1),
  );
}

function formatNumber(value: number): string {
  if (Number.isInteger(value)) return String(value);
  return value.toFixed(Math.abs(value) < 10 ? 1 : 0);
}

function tickLabel(
  value: number | string,
  index: number,
  ticks: ChartTicksOptions | undefined,
): React.ReactNode {
  return (
    ticks?.callback?.({ value, index }) ??
    (typeof value === "number" ? formatNumber(value) : value)
  );
}

function categoryTickStep(count: number, ticks: ChartTicksOptions | undefined): number {
  if (ticks?.autoSkip === false) return 1;
  const maxTicks = ticks?.maxTicksLimit ?? 10;
  return Math.max(1, Math.ceil(count / Math.max(2, maxTicks)));
}

function shouldShowCategoryTick(index: number, count: number, step: number): boolean {
  return index % step === 0 || index === count - 1;
}

function pathWithTension(points: readonly Point2D[], tension: number): string {
  if (points.length === 0) return "";
  if (points.length === 1 || tension <= 0) {
    return points
      .map((point, index) => `${index === 0 ? "M" : "L"}${point.x},${point.y}`)
      .join(" ");
  }
  let path = `M${points[0].x},${points[0].y}`;
  for (let index = 0; index < points.length - 1; index += 1) {
    const current = points[index];
    const next = points[index + 1];
    const previous = points[index - 1] ?? current;
    const afterNext = points[index + 2] ?? next;
    const control1 = {
      x: current.x + ((next.x - previous.x) * tension) / 6,
      y: current.y + ((next.y - previous.y) * tension) / 6,
    };
    const control2 = {
      x: next.x - ((afterNext.x - current.x) * tension) / 6,
      y: next.y - ((afterNext.y - current.y) * tension) / 6,
    };
    path += ` C${control1.x},${control1.y} ${control2.x},${control2.y} ${next.x},${next.y}`;
  }
  return path;
}

function polarPoint(
  centerX: number,
  centerY: number,
  radius: number,
  angle: number,
): Point2D {
  return { x: centerX + Math.cos(angle) * radius, y: centerY + Math.sin(angle) * radius };
}

function arcPath(
  centerX: number,
  centerY: number,
  outerRadius: number,
  innerRadius: number,
  startAngle: number,
  endAngle: number,
): string {
  const startOuter = polarPoint(centerX, centerY, outerRadius, startAngle);
  const endOuter = polarPoint(centerX, centerY, outerRadius, endAngle);
  const largeArc = endAngle - startAngle > Math.PI ? 1 : 0;
  if (innerRadius <= 0.5) {
    return [
      `M${centerX},${centerY}`,
      `L${startOuter.x},${startOuter.y}`,
      `A${outerRadius},${outerRadius} 0 ${largeArc} 1 ${endOuter.x},${endOuter.y}`,
      "Z",
    ].join(" ");
  }
  const endInner = polarPoint(centerX, centerY, innerRadius, endAngle);
  const startInner = polarPoint(centerX, centerY, innerRadius, startAngle);
  return [
    `M${startOuter.x},${startOuter.y}`,
    `A${outerRadius},${outerRadius} 0 ${largeArc} 1 ${endOuter.x},${endOuter.y}`,
    `L${endInner.x},${endInner.y}`,
    `A${innerRadius},${innerRadius} 0 ${largeArc} 0 ${startInner.x},${startInner.y}`,
    "Z",
  ].join(" ");
}

function projectedRadialPoint(
  centerX: number,
  centerY: number,
  radius: number,
  angle: number,
  perspective: number,
  translateX = 0,
  translateY = 0,
): Point2D {
  return {
    x: centerX + translateX + Math.cos(angle) * radius,
    y: centerY + translateY + Math.sin(angle) * radius * perspective,
  };
}

function projectedRadialArcPath(
  centerX: number,
  centerY: number,
  outerRadius: number,
  innerRadius: number,
  startAngle: number,
  endAngle: number,
  perspective: number,
  translateX = 0,
  translateY = 0,
): string {
  const startOuter = projectedRadialPoint(
    centerX,
    centerY,
    outerRadius,
    startAngle,
    perspective,
    translateX,
    translateY,
  );
  const endOuter = projectedRadialPoint(
    centerX,
    centerY,
    outerRadius,
    endAngle,
    perspective,
    translateX,
    translateY,
  );
  const largeArc = endAngle - startAngle > Math.PI ? 1 : 0;
  const outerRy = Math.max(0.5, outerRadius * perspective);
  if (innerRadius <= 0.5) {
    return [
      `M${centerX + translateX},${centerY + translateY}`,
      `L${startOuter.x},${startOuter.y}`,
      `A${outerRadius},${outerRy} 0 ${largeArc} 1 ${endOuter.x},${endOuter.y}`,
      "Z",
    ].join(" ");
  }
  const endInner = projectedRadialPoint(
    centerX,
    centerY,
    innerRadius,
    endAngle,
    perspective,
    translateX,
    translateY,
  );
  const startInner = projectedRadialPoint(
    centerX,
    centerY,
    innerRadius,
    startAngle,
    perspective,
    translateX,
    translateY,
  );
  const innerRy = Math.max(0.5, innerRadius * perspective);
  return [
    `M${startOuter.x},${startOuter.y}`,
    `A${outerRadius},${outerRy} 0 ${largeArc} 1 ${endOuter.x},${endOuter.y}`,
    `L${endInner.x},${endInner.y}`,
    `A${innerRadius},${innerRy} 0 ${largeArc} 0 ${startInner.x},${startInner.y}`,
    "Z",
  ].join(" ");
}

function radialSidewallBandPath(
  centerX: number,
  centerY: number,
  radius: number,
  startAngle: number,
  endAngle: number,
  depth: number,
  perspective = 1,
  translateX = 0,
  translateY = 0,
): string {
  const startTop = projectedRadialPoint(
    centerX,
    centerY,
    radius,
    startAngle,
    perspective,
    translateX,
    translateY,
  );
  const endTop = projectedRadialPoint(
    centerX,
    centerY,
    radius,
    endAngle,
    perspective,
    translateX,
    translateY,
  );
  const largeArc = endAngle - startAngle > Math.PI ? 1 : 0;
  const ry = Math.max(0.5, radius * perspective);
  return [
    `M${startTop.x},${startTop.y}`,
    `A${radius},${ry} 0 ${largeArc} 1 ${endTop.x},${endTop.y}`,
    `L${endTop.x},${endTop.y + depth}`,
    `A${radius},${ry} 0 ${largeArc} 0 ${startTop.x},${startTop.y + depth}`,
    "Z",
  ].join(" ");
}

function radialSidewallFacePath(
  centerX: number,
  centerY: number,
  innerRadius: number,
  outerRadius: number,
  angle: number,
  depth: number,
  perspective = 1,
  translateX = 0,
  translateY = 0,
): string {
  const outerTop = projectedRadialPoint(
    centerX,
    centerY,
    outerRadius,
    angle,
    perspective,
    translateX,
    translateY,
  );
  const innerTop =
    innerRadius > 0.5
      ? projectedRadialPoint(
          centerX,
          centerY,
          innerRadius,
          angle,
          perspective,
          translateX,
          translateY,
        )
      : { x: centerX + translateX, y: centerY + translateY };
  return [
    `M${innerTop.x},${innerTop.y}`,
    `L${outerTop.x},${outerTop.y}`,
    `L${outerTop.x},${outerTop.y + depth}`,
    `L${innerTop.x},${innerTop.y + depth}`,
    "Z",
  ].join(" ");
}

function visibleRadialArcSegments(
  startAngle: number,
  endAngle: number,
  facing: "front" | "back",
): Array<[number, number]> {
  if (endAngle <= startAngle) return [];
  const boundaries = [startAngle, endAngle];
  const firstBoundary = Math.floor(startAngle / Math.PI) - 1;
  const lastBoundary = Math.ceil(endAngle / Math.PI) + 1;
  for (let index = firstBoundary; index <= lastBoundary; index += 1) {
    const angle = index * Math.PI;
    if (angle > startAngle && angle < endAngle) boundaries.push(angle);
  }
  boundaries.sort((a, b) => a - b);
  const segments: Array<[number, number]> = [];
  for (let index = 0; index < boundaries.length - 1; index += 1) {
    const segmentStart = boundaries[index];
    const segmentEnd = boundaries[index + 1];
    const isFront = Math.sin((segmentStart + segmentEnd) / 2) > 0.0001;
    if ((facing === "front" && isFront) || (facing === "back" && !isFront)) {
      segments.push([segmentStart, segmentEnd]);
    }
  }
  return segments;
}

function usePrefersReducedMotion(): boolean {
  const [reduced, setReduced] = React.useState(false);

  React.useEffect(() => {
    const mediaQuery = window.matchMedia("(prefers-reduced-motion: reduce)");
    const update = () => setReduced(mediaQuery.matches);
    update();
    mediaQuery.addEventListener?.("change", update);
    return () => mediaQuery.removeEventListener?.("change", update);
  }, []);

  return reduced;
}

function useChartViewport(ref: React.RefObject<HTMLDivElement | null>): ChartViewport {
  const [viewport, setViewport] = React.useState<ChartViewport>({ width: 0, height: 0 });

  React.useEffect(() => {
    const element = ref.current;
    if (!element) return;

    const update = () => {
      const bounds = element.getBoundingClientRect();
      setViewport((current) =>
        current.width === bounds.width && current.height === bounds.height
          ? current
          : { width: bounds.width, height: bounds.height },
      );
    };

    update();
    if (typeof ResizeObserver === "undefined") return;
    const observer = new ResizeObserver(update);
    observer.observe(element);
    return () => observer.disconnect();
  }, [ref]);

  return viewport;
}

function easingValue(name: ChartEasing | undefined, value: number): number {
  const t = clamp(value, 0, 1);
  switch (name) {
    case "linear":
      return t;
    case "easeInQuad":
      return t * t;
    case "easeOutQuad":
      return t * (2 - t);
    case "easeInOutQuad":
      return t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2;
    case "easeInCubic":
      return t * t * t;
    case "easeOutCubic":
      return 1 - Math.pow(1 - t, 3);
    case "easeInOutCubic":
      return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
    case "easeInQuart":
      return t * t * t * t;
    case "easeOutQuart":
      return 1 - Math.pow(1 - t, 4);
    case "easeInOutQuart":
      return t < 0.5 ? 8 * t * t * t * t : 1 - Math.pow(-2 * t + 2, 4) / 2;
    case "easeInQuint":
      return t * t * t * t * t;
    case "easeOutQuint":
      return 1 - Math.pow(1 - t, 5);
    case "easeInOutQuint":
      return t < 0.5 ? 16 * t * t * t * t * t : 1 - Math.pow(-2 * t + 2, 5) / 2;
    case "easeInSine":
      return 1 - Math.cos((t * Math.PI) / 2);
    case "easeOutSine":
      return Math.sin((t * Math.PI) / 2);
    case "easeInOutSine":
      return -(Math.cos(Math.PI * t) - 1) / 2;
    case "easeInExpo":
      return t === 0 ? 0 : Math.pow(2, 10 * t - 10);
    case "easeOutExpo":
      return t === 1 ? 1 : 1 - Math.pow(2, -10 * t);
    case "easeInOutExpo":
      if (t === 0 || t === 1) return t;
      return t < 0.5 ? Math.pow(2, 20 * t - 10) / 2 : (2 - Math.pow(2, -20 * t + 10)) / 2;
    case "easeInCirc":
      return 1 - Math.sqrt(1 - Math.pow(t, 2));
    case "easeOutCirc":
      return Math.sqrt(1 - Math.pow(t - 1, 2));
    case "easeInOutCirc":
      return t < 0.5
        ? (1 - Math.sqrt(1 - Math.pow(2 * t, 2))) / 2
        : (Math.sqrt(1 - Math.pow(-2 * t + 2, 2)) + 1) / 2;
    default:
      return 1 - Math.pow(1 - t, 4);
  }
}

function useChartProgress(
  type: ChartType,
  data: ChartData,
  animation: ChartAnimation | false | undefined,
  reducedMotion: boolean,
): number {
  const duration = animation === false ? 0 : (animation?.duration ?? DEFAULT_DURATION);
  const delay = animation === false ? 0 : (animation?.delay ?? 0);
  const shouldAnimate = !reducedMotion && duration > 0;
  const [progress, setProgress] = React.useState(shouldAnimate ? 0 : 1);

  React.useEffect(() => {
    let frame = 0;
    let completed = false;
    const finish = () => {
      if (completed) return;
      completed = true;
      if (animation !== false) animation?.onComplete?.();
    };

    if (!shouldAnimate) {
      // eslint-disable-next-line react-hooks/set-state-in-effect -- reset animation state when chart inputs change
      setProgress(1);
      finish();
      return () => undefined;
    }

    setProgress(0);
    const start = performance.now() + delay;
    const tick = (now: number) => {
      const raw = clamp((now - start) / duration, 0, 1);
      setProgress(easingValue(animation !== false ? animation?.easing : undefined, raw));
      if (raw >= 1) {
        finish();
        return;
      }
      frame = window.requestAnimationFrame(tick);
    };
    frame = window.requestAnimationFrame(tick);
    return () => window.cancelAnimationFrame(frame);
  }, [animation, data, delay, duration, shouldAnimate, type]);

  return progress;
}

interface RadialHoverState {
  active: { datasetIndex: number; dataIndex: number } | null;
  progress: number;
}

function useRadialHoverProgress(
  type: ChartType,
  active: { datasetIndex: number; dataIndex: number } | null,
  options: ChartOptionsLike,
  reducedMotion: boolean,
): RadialHoverState {
  const enabled = type === "pie" || type === "donut" || type === "polarArea";
  const animation = React.useMemo(
    () =>
      options.transitions?.hover ??
      (options.animation === false
        ? false
        : { duration: 180, easing: "easeOutCubic" as const }),
    [options.animation, options.transitions?.hover],
  );
  const activeKey = active ? `${active.datasetIndex}:${active.dataIndex}` : null;
  const stateRef = React.useRef<RadialHoverState>({ active: null, progress: 0 });
  const [state, setState] = React.useState<RadialHoverState>({
    active: null,
    progress: 0,
  });

  React.useEffect(() => {
    if (!enabled) return;
    const target = active ? 1 : 0;
    const previous = stateRef.current;
    const sameTarget =
      activeKey != null &&
      previous.active != null &&
      `${previous.active.datasetIndex}:${previous.active.dataIndex}` === activeKey;
    const nextActive = active ?? previous.active;
    const from = sameTarget || !active ? previous.progress : 0;
    const duration = animation === false ? 0 : (animation.duration ?? 180);
    const delay = animation === false ? 0 : (animation.delay ?? 0);

    if (reducedMotion || duration <= 0) {
      const next = { active: target === 0 ? null : nextActive, progress: target };
      stateRef.current = next;
      setState(next);
      return () => undefined;
    }

    let frame = 0;
    const start = performance.now() + delay;
    const tick = (now: number) => {
      const raw = clamp((now - start) / duration, 0, 1);
      const value =
        from +
        (target - from) *
          easingValue(animation === false ? undefined : animation.easing, raw);
      const next = {
        active: raw >= 1 && target === 0 ? null : nextActive,
        progress: value,
      };
      stateRef.current = next;
      setState(next);
      if (raw < 1) frame = window.requestAnimationFrame(tick);
    };
    frame = window.requestAnimationFrame(tick);
    return () => window.cancelAnimationFrame(frame);
  }, [active, activeKey, animation, enabled, reducedMotion]);

  return enabled ? state : { active: null, progress: 0 };
}

function getVisibilityAnimation(
  options: ChartOptionsLike,
  direction: "show" | "hide",
): ChartAnimation | false {
  const explicit = options.transitions?.[direction];
  if (explicit !== undefined) return explicit;
  if (options.animation !== undefined) return options.animation;
  return { duration: 450, easing: "easeOutQuart" };
}

function useDatasetVisibility(
  datasets: readonly ChartDataset[],
  options: ChartOptionsLike,
  reducedMotion: boolean,
): {
  hidden: ReadonlySet<number>;
  progress: Readonly<Record<number, number>>;
  toggle: (index: number) => void;
} {
  const [hidden, setHidden] = React.useState<ReadonlySet<number>>(
    () => new Set(datasets.flatMap((dataset, index) => (dataset.hidden ? [index] : []))),
  );
  const [progress, setProgress] = React.useState<Readonly<Record<number, number>>>(() =>
    Object.fromEntries(datasets.map((dataset, index) => [index, dataset.hidden ? 0 : 1])),
  );
  const hiddenRef = React.useRef(hidden);
  const progressRef = React.useRef(progress);
  const framesRef = React.useRef(new Map<number, number>());
  const runsRef = React.useRef(new Map<number, number>());

  const updateProgress = React.useCallback((index: number, value: number) => {
    const nextValue = clamp(value, 0, 1);
    setProgress((current) => {
      const next = { ...current, [index]: nextValue };
      progressRef.current = next;
      return next;
    });
  }, []);

  const toggle = React.useCallback(
    (index: number) => {
      const wasHidden = hiddenRef.current.has(index);
      const target = wasHidden ? 1 : 0;
      const nextHidden = new Set(hiddenRef.current);
      if (wasHidden) nextHidden.delete(index);
      else nextHidden.add(index);
      hiddenRef.current = nextHidden;
      setHidden(nextHidden);

      const previousFrame = framesRef.current.get(index);
      if (previousFrame != null) window.cancelAnimationFrame(previousFrame);
      const run = (runsRef.current.get(index) ?? 0) + 1;
      runsRef.current.set(index, run);

      const animation = getVisibilityAnimation(options, wasHidden ? "show" : "hide");
      const duration = animation === false ? 0 : (animation.duration ?? 450);
      const delay = animation === false ? 0 : (animation.delay ?? 0);
      const easing = animation === false ? undefined : animation.easing;
      const onComplete = animation === false ? undefined : animation.onComplete;
      if (reducedMotion || duration <= 0) {
        updateProgress(index, target);
        onComplete?.();
        return;
      }

      const from = progressRef.current[index] ?? (wasHidden ? 0 : 1);
      const start = performance.now() + delay;
      const tick = (now: number) => {
        if (runsRef.current.get(index) !== run) return;
        const raw = clamp((now - start) / duration, 0, 1);
        const eased = easingValue(easing, raw);
        updateProgress(index, from + (target - from) * eased);
        if (raw >= 1) {
          framesRef.current.delete(index);
          onComplete?.();
          return;
        }
        framesRef.current.set(index, window.requestAnimationFrame(tick));
      };
      framesRef.current.set(index, window.requestAnimationFrame(tick));
    },
    [options, reducedMotion, updateProgress],
  );

  React.useEffect(
    () => () => {
      framesRef.current.forEach((frame) => window.cancelAnimationFrame(frame));
      framesRef.current.clear();
    },
    [],
  );

  return { hidden, progress, toggle };
}

function makeTooltipContext(
  dataset: ChartDataset,
  datasetIndex: number,
  dataIndex: number,
  labels: readonly string[],
  raw: ChartDataPoint,
): ChartTooltipContext {
  return {
    datasetIndex,
    dataIndex,
    datasetLabel: dataset.label,
    label: labels[dataIndex],
    value: pointValue(raw),
    raw,
  };
}

function isRadialChart(type: ChartType): boolean {
  return type === "radar" || type === "polarArea" || type === "pie" || type === "donut";
}

function getChartViewportScale(type: ChartType, viewport: ChartViewport): number {
  if (viewport.width <= 0 || viewport.height <= 0) return 1;
  return isRadialChart(type)
    ? Math.min(
        viewport.width / RADIAL_VIEWBOX_SIZE,
        viewport.height / RADIAL_VIEWBOX_SIZE,
      )
    : viewport.height / VIEWBOX_HEIGHT;
}

function getTitleFontSize(
  title: ChartTitleOptions | undefined,
  type: ChartType,
  viewport: ChartViewport,
): number {
  const requestedSize = title?.fontSize ?? DEFAULT_TITLE_FONT_SIZE;
  const scale = getChartViewportScale(type, viewport);
  return scale > 0 ? requestedSize / scale : requestedSize;
}

function renderTitle(
  options: ChartOptionsLike,
  layout: PlotLayout,
  fontSize: number,
): React.ReactNode {
  const title = options.plugins?.title;
  const lines = titleLines(title);
  if (!title?.display || lines.length === 0) return null;
  const align = title.align ?? DEFAULT_TITLE_ALIGN;
  const lineHeight = Math.max(17, Math.round(fontSize * 1.15));
  const x =
    align === "start"
      ? 12
      : align === "end"
        ? layout.viewBoxWidth - 12
        : layout.viewBoxWidth / 2;
  return (
    <text
      data-slot="chart-title"
      x={x}
      y={Math.max(20, fontSize * 1.25)}
      textAnchor={align === "start" ? "start" : align === "end" ? "end" : "middle"}
      fill={title.color ?? DEFAULT_FOREGROUND}
      fontSize={fontSize}
      fontWeight="600"
      style={{ whiteSpace: "nowrap" }}
    >
      {lines.map((line, index) => (
        <tspan key={`${line}-${index}`} x={x} dy={index === 0 ? 0 : lineHeight}>
          {line}
        </tspan>
      ))}
    </text>
  );
}

function renderLegend(
  options: ChartOptionsLike,
  datasets: readonly ChartDataset[],
  hidden: ReadonlySet<number>,
  onToggle: (index: number) => void,
  layout: PlotLayout,
): React.ReactNode {
  if (!hasLegend(options, datasets)) return null;
  const labels = options.plugins?.legend?.labels;
  const legendLayout = getLegendLayout(
    options,
    datasets,
    layout.viewBoxWidth,
    layout.viewBoxHeight,
  );
  const boxWidth = labels?.boxWidth ?? 10;
  const fontSize = labels?.fontSize ?? 11;
  const color = labels?.color ?? DEFAULT_TEXT_COLOR;
  const position = options.plugins?.legend?.position ?? "top";

  return (
    <g
      data-slot="chart-legend"
      role="group"
      aria-label="Chart legend"
      transform={position === "left" ? "translate(0 0)" : undefined}
    >
      {legendLayout.items.map((item) => {
        const isHidden = hidden.has(item.index);
        return (
          <g
            key={`${item.label}-${item.index}`}
            data-slot="chart-legend-item"
            role="button"
            tabIndex={0}
            aria-label={`${isHidden ? "Show" : "Hide"} ${item.label}`}
            aria-pressed={!isHidden}
            opacity={isHidden ? 0.42 : 1}
            className="group cursor-pointer outline-none focus-visible:outline-1 focus-visible:outline-offset-1 focus-visible:outline-[var(--primary)]"
            onClick={() => onToggle(item.index)}
            onKeyDown={(event) => {
              if (event.key === "Enter" || event.key === " ") {
                event.preventDefault();
                onToggle(item.index);
              }
            }}
          >
            <rect
              x={item.x}
              y={item.y - boxWidth + 2}
              width={boxWidth}
              height={boxWidth}
              rx={2}
              fill={getDatasetColor(datasets[item.index], item.index)}
            />
            <text
              x={item.x + boxWidth + 6}
              y={item.y + 2}
              fill={color}
              fontSize={fontSize}
            >
              {item.label}
            </text>
          </g>
        );
      })}
    </g>
  );
}

function renderCartesianAxes(
  scales: CartesianScales,
  labels: readonly string[],
  options: ChartOptionsLike,
  layout: PlotLayout,
  indexAxis: "x" | "y",
  type: ChartType,
): React.ReactNode {
  const xOptions = options.scales?.x;
  const yOptions = options.scales?.y;
  const xDisplay = xOptions?.display ?? true;
  const yDisplay = yOptions?.display ?? true;
  const xGrid = xOptions?.grid?.display ?? true;
  const yGrid = yOptions?.grid?.display ?? true;
  const xTickDisplay = xOptions?.ticks?.display ?? true;
  const yTickDisplay = yOptions?.ticks?.display ?? true;
  const xTicks = scales.xIsCategory
    ? labels.map((label, index) => ({ value: label, position: scales.x(index), index }))
    : tickValues(scales.xMin, scales.xMax, xOptions).map((value, index) => ({
        value,
        position: scales.x(value),
        index,
      }));
  const yIsCategory = indexAxis === "y" && (type === "bar" || type === "line");
  const yTicks = yIsCategory
    ? labels.map((label, index) => ({ value: label, position: scales.y(index), index }))
    : tickValues(scales.yMin, scales.yMax, yOptions).map((value, index) => ({
        value,
        position: scales.y(value),
        index,
      }));
  const xTickStep = scales.xIsCategory
    ? categoryTickStep(labels.length, xOptions?.ticks)
    : categoryTickStep(xTicks.length, xOptions?.ticks);
  const yTickStep = yIsCategory
    ? categoryTickStep(labels.length, yOptions?.ticks)
    : categoryTickStep(yTicks.length, yOptions?.ticks);
  const xRotation =
    scales.xIsCategory && xTickStep === 1 && labels.length > 10
      ? clamp(xOptions?.ticks?.maxRotation ?? 35, 0, 90)
      : 0;
  const visibleXLabels = scales.xIsCategory
    ? xTicks.filter((tick) =>
        shouldShowCategoryTick(tick.index, labels.length, xTickStep),
      )
    : xTicks.filter((tick) =>
        shouldShowCategoryTick(tick.index, xTicks.length, xTickStep),
      );
  const visibleYLabels = yIsCategory
    ? yTicks.filter((tick) =>
        shouldShowCategoryTick(tick.index, labels.length, yTickStep),
      )
    : yTicks.filter((tick) =>
        shouldShowCategoryTick(tick.index, yTicks.length, yTickStep),
      );
  const gridColorX = xOptions?.grid?.color ?? DEFAULT_GRID_COLOR;
  const gridColorY = yOptions?.grid?.color ?? DEFAULT_GRID_COLOR;
  const tickColorX = xOptions?.ticks?.color ?? DEFAULT_TEXT_COLOR;
  const tickColorY = yOptions?.ticks?.color ?? DEFAULT_TEXT_COLOR;
  return (
    <g data-slot="chart-axes" aria-hidden="true">
      {xGrid &&
        xDisplay &&
        xTicks.map((tick) => (
          <line
            key={`x-grid-${tick.index}`}
            x1={tick.position}
            x2={tick.position}
            y1={layout.top}
            y2={layout.top + layout.height}
            stroke={gridColorX}
            strokeOpacity={0.7}
            strokeWidth={xOptions?.grid?.lineWidth ?? 1}
          />
        ))}
      {yGrid &&
        yDisplay &&
        yTicks.map((tick) => (
          <line
            key={`y-grid-${tick.index}`}
            x1={layout.left}
            x2={layout.left + layout.width}
            y1={tick.position}
            y2={tick.position}
            stroke={gridColorY}
            strokeOpacity={0.7}
            strokeWidth={yOptions?.grid?.lineWidth ?? 1}
          />
        ))}
      {xDisplay && (
        <line
          x1={layout.left}
          x2={layout.left + layout.width}
          y1={layout.top + layout.height}
          y2={layout.top + layout.height}
          stroke={gridColorX}
          strokeWidth={xOptions?.grid?.drawBorder === false ? 0 : 1}
        />
      )}
      {yDisplay && (
        <line
          x1={layout.left}
          x2={layout.left}
          y1={layout.top}
          y2={layout.top + layout.height}
          stroke={gridColorY}
          strokeWidth={yOptions?.grid?.drawBorder === false ? 0 : 1}
        />
      )}
      {indexAxis === "y" && scales.xMin < 0 && scales.xMax > 0 && (
        <line
          data-slot="chart-zero-axis"
          x1={scales.x(0)}
          x2={scales.x(0)}
          y1={layout.top}
          y2={layout.top + layout.height}
          stroke={DEFAULT_FOREGROUND}
          strokeOpacity={0.55}
          strokeWidth={1}
        />
      )}
      {indexAxis === "x" && scales.yMin < 0 && scales.yMax > 0 && (
        <line
          data-slot="chart-zero-axis"
          x1={layout.left}
          x2={layout.left + layout.width}
          y1={scales.y(0)}
          y2={scales.y(0)}
          stroke={DEFAULT_FOREGROUND}
          strokeOpacity={0.55}
          strokeWidth={1}
        />
      )}
      {xDisplay &&
        xTickDisplay &&
        visibleXLabels.map((tick) => (
          <text
            key={`x-label-${tick.index}`}
            x={tick.position}
            y={layout.top + layout.height + 21}
            textAnchor={xRotation ? "end" : "middle"}
            transform={
              xRotation
                ? `rotate(-${xRotation} ${tick.position} ${layout.top + layout.height + 21})`
                : undefined
            }
            fill={tickColorX}
            fontSize={xOptions?.ticks?.fontSize ?? 10}
          >
            {tickLabel(tick.value, tick.index, xOptions?.ticks)}
          </text>
        ))}
      {yDisplay &&
        yTickDisplay &&
        visibleYLabels.map((tick) => (
          <text
            key={`y-label-${tick.index}`}
            x={layout.left - 9}
            y={tick.position + 3}
            textAnchor="end"
            fill={tickColorY}
            fontSize={yOptions?.ticks?.fontSize ?? 10}
          >
            {tickLabel(tick.value, tick.index, yOptions?.ticks)}
          </text>
        ))}
      {xOptions?.title?.display && xOptions.title.text && (
        <text
          data-slot="chart-axis-title"
          x={layout.left + layout.width / 2}
          y={layout.viewBoxHeight - layout.bottomLegendHeight - 7}
          textAnchor="middle"
          fill={xOptions.title.color ?? DEFAULT_TEXT_COLOR}
          fontSize={xOptions.title.fontSize ?? 11}
        >
          {xOptions.title.text}
        </text>
      )}
      {yOptions?.title?.display && yOptions.title.text && (
        <text
          data-slot="chart-axis-title"
          transform={`translate(${options.plugins?.legend?.position === "left" ? layout.left - 42 : 14} ${layout.top + layout.height / 2}) rotate(-90)`}
          textAnchor="middle"
          fill={yOptions.title.color ?? DEFAULT_TEXT_COLOR}
          fontSize={yOptions.title.fontSize ?? 11}
        >
          {yOptions.title.text}
        </text>
      )}
    </g>
  );
}

function getCartesianDatasetType(
  dataset: ChartDataset,
  chartType: ChartType,
): "bar" | "line" {
  if (chartType === "bar" || chartType === "line") {
    return dataset.type ?? chartType;
  }
  return "line";
}

function renderMixedCartesianChart({
  datasets,
  type,
  options,
  layout,
  progress,
  datasetProgress,
  labels,
  onTooltip,
}: ChartRenderProps): React.ReactNode {
  const indexAxis = options.indexAxis ?? "x";
  const scales = buildCartesianScales(datasets, labels, type, options, layout);
  const axes = renderCartesianAxes(scales, labels, options, layout, indexAxis, type);
  const count = Math.max(
    labels.length,
    ...datasets.map((dataset) => dataset.data.length),
    1,
  );
  const stacked = options.scales?.x?.stacked || options.scales?.y?.stacked;
  const barDatasets = datasets.flatMap((dataset, datasetIndex) =>
    getCartesianDatasetType(dataset, type) === "bar" ? [{ dataset, datasetIndex }] : [],
  );
  const stackKeys = Array.from(
    new Set(barDatasets.map(({ dataset }) => dataset.stack ?? "__default")),
  );
  const stackIndex = new Map(stackKeys.map((key, index) => [key, index]));
  const stackTotals = stackKeys.map(() =>
    Array.from({ length: count }, () => ({ positive: 0, negative: 0 })),
  );
  const stackFinalTotals = stackKeys.map(() =>
    Array.from({ length: count }, () => ({ positive: 0, negative: 0 })),
  );
  if (stacked) {
    barDatasets.forEach(({ dataset }) => {
      const stackKey = dataset.stack ?? "__default";
      const slot = stackIndex.get(stackKey) ?? 0;
      dataset.data.forEach((raw, dataIndex) => {
        const value = numberValue(raw);
        if (value == null) return;
        const total = stackFinalTotals[slot][dataIndex];
        if (value >= 0) total.positive += value;
        else total.negative += value;
      });
    });
  }
  const groupSize = indexAxis === "x" ? layout.width / count : layout.height / count;
  const groupWidth = groupSize * 0.76;
  const slots = stacked ? Math.max(stackKeys.length, 1) : Math.max(barDatasets.length, 1);
  const barSize = groupWidth / slots;
  const bars: React.ReactNode[] = [];

  barDatasets.forEach(({ dataset, datasetIndex }, datasetOrder) => {
    const visibility = datasetProgress[datasetIndex] ?? 1;
    const stackKey = dataset.stack ?? "__default";
    const slot = stacked ? (stackIndex.get(stackKey) ?? 0) : datasetOrder;
    dataset.data.forEach((raw, dataIndex) => {
      const value = numberValue(raw);
      if (value == null) return;
      const groupStart =
        (indexAxis === "x" ? layout.left : layout.top) +
        groupSize * dataIndex +
        (groupSize - groupWidth) / 2;
      let start = 0;
      let end = value * visibility;
      if (stacked) {
        const totals = stackTotals[slot][dataIndex];
        const contribution = value * visibility;
        if (value >= 0) {
          start = totals.positive;
          totals.positive += contribution;
          end = totals.positive;
        } else {
          start = totals.negative;
          totals.negative += contribution;
          end = totals.negative;
        }
      }
      const animatedStart = start * progress;
      const animatedEnd = end * progress;
      const position = groupStart + barSize * slot;
      const bar =
        indexAxis === "x"
          ? {
              x: position,
              y: Math.min(scales.y(animatedStart), scales.y(animatedEnd)),
              width: Math.max(2, barSize - 2),
              height: Math.abs(scales.y(animatedStart) - scales.y(animatedEnd)),
            }
          : {
              x: Math.min(scales.x(animatedStart), scales.x(animatedEnd)),
              y: position,
              width: Math.abs(scales.x(animatedStart) - scales.x(animatedEnd)),
              height: Math.max(2, barSize - 2),
            };
      const context = makeTooltipContext(dataset, datasetIndex, dataIndex, labels, raw);
      const radius = dataset.borderRadius ?? options.elements?.bar?.borderRadius ?? 4;
      const path = roundedBarPath(
        bar.x,
        bar.y,
        bar.width,
        Math.max(0, bar.height),
        radius,
        stacked
          ? stackedBarCorners(indexAxis, value, end, stackFinalTotals[slot][dataIndex])
          : outerBarCorners(indexAxis, value),
      );
      bars.push(
        <path
          key={`mixed-bar-${datasetIndex}-${dataIndex}`}
          data-slot="chart-bar"
          data-dataset-index={datasetIndex}
          data-stack={stackKey}
          d={path}
          x={bar.x}
          y={bar.y}
          width={bar.width}
          height={Math.max(0, bar.height)}
          fill={getDatasetColor(dataset, dataIndex)}
          stroke={getBorderColor(dataset, dataIndex)}
          strokeWidth={getBorderWidth(dataset, options, "bar")}
          opacity={visibility}
          onMouseEnter={() =>
            onTooltip(
              indexAxis === "x" ? bar.x + bar.width / 2 : bar.x + bar.width,
              indexAxis === "x" ? bar.y : bar.y + bar.height / 2,
              context,
            )
          }
        />,
      );
    });
  });

  const lineSeries = datasets.flatMap((dataset, datasetIndex) => {
    if (getCartesianDatasetType(dataset, type) !== "line") return [];
    const points = dataset.data.flatMap((raw, dataIndex) => {
      const point = asPoint(raw, dataIndex);
      if (!point) return [];
      const xValue = indexAxis === "x" ? dataIndex : point.y;
      const yValue = indexAxis === "x" ? point.y : dataIndex;
      return [
        {
          x: scales.x(xValue),
          y: scales.y(yValue),
          dataIndex,
          datasetIndex,
          value: point.y,
          raw,
        } satisfies CartesianPoint,
      ];
    });
    return [{ dataset, datasetIndex, points }];
  });

  return (
    <g data-slot="chart-mixed-cartesian">
      {axes}
      <g data-slot="chart-bar-series">{bars}</g>
      <g data-slot="chart-cartesian-series">
        {lineSeries.map(({ dataset, datasetIndex, points }) => {
          const visibility = datasetProgress[datasetIndex] ?? 1;
          const seriesProgress = progress * visibility;
          const pointRadius = dataset.pointRadius ?? options.elements?.point?.radius ?? 3;
          const hoverRadius =
            dataset.pointHoverRadius ??
            options.elements?.point?.hoverRadius ??
            pointRadius + 2;
          const baselineX = clamp(scales.x(0), layout.left, layout.left + layout.width);
          const baselineY = clamp(scales.y(0), layout.top, layout.top + layout.height);
          const visiblePoints = points.map((point) => ({
            ...point,
            x: baselineX + (point.x - baselineX) * seriesProgress,
            y: baselineY + (point.y - baselineY) * seriesProgress,
          }));
          const path = pathWithTension(
            visiblePoints,
            dataset.tension ?? options.elements?.line?.tension ?? 0.28,
          );
          const fillPath =
            dataset.fill && visiblePoints.length > 1
              ? `${path} L${visiblePoints[visiblePoints.length - 1].x},${baselineY} L${visiblePoints[0].x},${baselineY} Z`
              : undefined;
          return (
            <g
              key={`mixed-line-${datasetIndex}`}
              data-slot="chart-series"
              data-dataset-type="line"
              opacity={visibility}
            >
              {fillPath && (
                <path
                  d={fillPath}
                  fill={getDatasetColor(dataset, datasetIndex)}
                  fillOpacity={0.14}
                  stroke="none"
                />
              )}
              {path && (
                <path
                  data-slot="chart-line"
                  d={path}
                  fill="none"
                  stroke={getBorderColor(dataset, datasetIndex)}
                  strokeDasharray={dataset.borderDash?.join(" ")}
                  strokeWidth={getBorderWidth(dataset, options, "line")}
                  strokeLinecap="round"
                  strokeLinejoin="round"
                />
              )}
              {visiblePoints.map((point) => {
                const context = makeTooltipContext(
                  dataset,
                  datasetIndex,
                  point.dataIndex,
                  labels,
                  point.raw,
                );
                return (
                  <g key={`mixed-point-${point.dataIndex}`} data-slot="chart-point">
                    <circle
                      cx={point.x}
                      cy={point.y}
                      r={Math.max(pointRadius, 7)}
                      fill="transparent"
                      onMouseEnter={() => onTooltip(point.x, point.y, context)}
                    />
                    <circle
                      cx={point.x}
                      cy={point.y}
                      r={pointRadius * seriesProgress}
                      fill={getDatasetColor(dataset, point.dataIndex)}
                      stroke={getBorderColor(dataset, point.dataIndex)}
                      strokeWidth={
                        dataset.borderWidth ?? options.elements?.point?.borderWidth ?? 2
                      }
                    />
                    {hoverRadius > pointRadius && (
                      <circle
                        cx={point.x}
                        cy={point.y}
                        r={hoverRadius}
                        fill="transparent"
                        pointerEvents="none"
                      />
                    )}
                  </g>
                );
              })}
            </g>
          );
        })}
      </g>
    </g>
  );
}

function renderCartesianChart({
  datasets,
  type,
  options,
  layout,
  progress,
  datasetProgress,
  labels,
  onTooltip,
}: ChartRenderProps): React.ReactNode {
  if (
    (type === "bar" || type === "line") &&
    datasets.some((dataset) => dataset.type != null || dataset.stack != null)
  ) {
    return renderMixedCartesianChart({
      data: { labels, datasets },
      datasets,
      type,
      options,
      layout,
      progress,
      datasetProgress,
      labels,
      onTooltip,
    });
  }
  const indexAxis = options.indexAxis ?? "x";
  const scales = buildCartesianScales(datasets, labels, type, options, layout);
  const isPointChart = type === "scatter" || type === "bubble";
  const isBar = type === "bar";
  const axes = renderCartesianAxes(scales, labels, options, layout, indexAxis, type);

  if (isBar) {
    const count = Math.max(
      labels.length,
      ...datasets.map((dataset) => dataset.data.length),
      1,
    );
    const stacked = options.scales?.x?.stacked || options.scales?.y?.stacked;
    const stackTotals = Array.from({ length: count }, () => ({
      positive: 0,
      negative: 0,
    }));
    const stackFinalTotals = Array.from({ length: count }, () => ({
      positive: 0,
      negative: 0,
    }));
    if (stacked) {
      datasets.forEach((dataset, datasetIndex) => {
        const visibility = datasetProgress[datasetIndex] ?? 1;
        dataset.data.forEach((raw, dataIndex) => {
          const value = numberValue(raw);
          if (value == null) return;
          if (value >= 0) stackFinalTotals[dataIndex].positive += value * visibility;
          else stackFinalTotals[dataIndex].negative += value * visibility;
        });
      });
    }
    const bars: React.ReactNode[] = [];
    const visibleCount = datasets.length || 1;
    datasets.forEach((dataset, datasetIndex) => {
      const visibility = datasetProgress[datasetIndex] ?? 1;
      dataset.data.forEach((raw, dataIndex) => {
        const value = numberValue(raw);
        if (value == null) return;
        const category = dataIndex;
        const groupSize =
          indexAxis === "x" ? layout.width / count : layout.height / count;
        const groupWidth = groupSize * 0.76;
        const barSize = stacked ? groupWidth : groupWidth / visibleCount;
        const groupStart =
          (indexAxis === "x" ? layout.left : layout.top) +
          groupSize * category +
          (groupSize - groupWidth) / 2;
        let start = 0;
        let end = value;
        if (stacked) {
          const total = stackTotals[dataIndex];
          const contribution = value * visibility;
          if (value >= 0) {
            start = total.positive;
            total.positive += contribution;
            end = total.positive;
          } else {
            start = total.negative;
            total.negative += contribution;
            end = total.negative;
          }
        } else {
          end *= visibility;
        }
        const animatedStart = start * progress;
        const animatedEnd = end * progress;
        const position = stacked ? groupStart : groupStart + barSize * datasetIndex;
        const bar =
          indexAxis === "x"
            ? {
                x: position,
                y: Math.min(scales.y(animatedStart), scales.y(animatedEnd)),
                width: Math.max(2, barSize - 2),
                height: Math.abs(scales.y(animatedStart) - scales.y(animatedEnd)),
              }
            : {
                x: Math.min(scales.x(animatedStart), scales.x(animatedEnd)),
                y: position,
                width: Math.abs(scales.x(animatedStart) - scales.x(animatedEnd)),
                height: Math.max(2, barSize - 2),
              };
        const context = makeTooltipContext(dataset, datasetIndex, dataIndex, labels, raw);
        const radius = dataset.borderRadius ?? options.elements?.bar?.borderRadius ?? 4;
        const path = roundedBarPath(
          bar.x,
          bar.y,
          bar.width,
          Math.max(0, bar.height),
          radius,
          stacked
            ? stackedBarCorners(indexAxis, value, end, stackFinalTotals[dataIndex])
            : outerBarCorners(indexAxis, value),
        );
        bars.push(
          <path
            key={`bar-${datasetIndex}-${dataIndex}`}
            data-slot="chart-bar"
            d={path}
            x={bar.x}
            y={bar.y}
            width={bar.width}
            height={Math.max(0, bar.height)}
            fill={getDatasetColor(dataset, dataIndex)}
            stroke={getBorderColor(dataset, dataIndex)}
            strokeWidth={getBorderWidth(dataset, options, type)}
            opacity={visibility}
            onMouseEnter={() =>
              onTooltip(
                indexAxis === "x" ? bar.x + bar.width / 2 : bar.x + bar.width,
                indexAxis === "x" ? bar.y : bar.y + bar.height / 2,
                context,
              )
            }
          />,
        );
      });
    });
    return (
      <g data-slot="chart-bar-series">
        {axes}
        {bars}
      </g>
    );
  }

  const series = datasets.map((dataset, datasetIndex) => {
    const points = dataset.data.flatMap((raw, dataIndex) => {
      const point = asPoint(raw, dataIndex);
      if (!point) return [];
      const xValue = isPointChart ? point.x : indexAxis === "x" ? dataIndex : point.y;
      const yValue = isPointChart ? point.y : indexAxis === "x" ? point.y : dataIndex;
      const pointData: CartesianPoint = {
        x: scales.x(xValue),
        y: scales.y(yValue),
        dataIndex,
        datasetIndex,
        value: point.y,
        raw,
      };
      return [pointData];
    });
    return { dataset, datasetIndex, points };
  });

  return (
    <g data-slot="chart-cartesian-series">
      {axes}
      {series.map(({ dataset, datasetIndex, points }) => {
        const visibility = datasetProgress[datasetIndex] ?? 1;
        const seriesProgress = progress * visibility;
        const isLine = type === "line" || (isPointChart && dataset.showLine);
        const pointRadius =
          dataset.pointRadius ??
          options.elements?.point?.radius ??
          (isPointChart ? 5 : 3);
        const hoverRadius =
          dataset.pointHoverRadius ??
          options.elements?.point?.hoverRadius ??
          pointRadius + 2;
        const borderWidth =
          dataset.borderWidth ?? options.elements?.point?.borderWidth ?? 2;
        const tension =
          dataset.tension ??
          options.elements?.line?.tension ??
          (type === "line" ? 0.28 : 0);
        const baselineX = clamp(scales.x(0), layout.left, layout.left + layout.width);
        const baselineY = clamp(scales.y(0), layout.top, layout.top + layout.height);
        const visiblePoints = points.map((point) => ({
          ...point,
          y: baselineY + (point.y - baselineY) * seriesProgress,
          x: baselineX + (point.x - baselineX) * seriesProgress,
        }));
        const path = pathWithTension(visiblePoints, tension);
        const fillPath =
          dataset.fill && visiblePoints.length > 1
            ? indexAxis === "x"
              ? `${path} L${visiblePoints[visiblePoints.length - 1].x},${baselineY} L${visiblePoints[0].x},${baselineY} Z`
              : `${path} L${baselineX},${visiblePoints[visiblePoints.length - 1].y} L${baselineX},${visiblePoints[0].y} Z`
            : undefined;
        return (
          <g key={`series-${datasetIndex}`} data-slot="chart-series" opacity={visibility}>
            {isLine && fillPath && (
              <path
                d={fillPath}
                fill={getDatasetColor(dataset, datasetIndex)}
                fillOpacity={0.14}
                stroke="none"
              />
            )}
            {isLine && path && (
              <path
                d={path}
                fill="none"
                stroke={getBorderColor(dataset, datasetIndex)}
                strokeDasharray={dataset.borderDash?.join(" ")}
                strokeWidth={getBorderWidth(dataset, options, type)}
                strokeLinecap="round"
                strokeLinejoin="round"
              />
            )}
            {visiblePoints.map((point) => {
              const context = makeTooltipContext(
                dataset,
                datasetIndex,
                point.dataIndex,
                labels,
                point.raw,
              );
              return (
                <g key={`point-${point.dataIndex}`} data-slot="chart-point">
                  <circle
                    cx={point.x}
                    cy={point.y}
                    r={Math.max(pointRadius, 7)}
                    fill="transparent"
                    onMouseEnter={() => onTooltip(point.x, point.y, context)}
                  />
                  {type !== "scatter" || pointRadius > 0 ? (
                    <circle
                      cx={point.x}
                      cy={point.y}
                      r={
                        type === "bubble" &&
                        point.raw &&
                        typeof point.raw === "object" &&
                        "r" in point.raw
                          ? point.raw.r * seriesProgress
                          : pointRadius * seriesProgress
                      }
                      fill={getDatasetColor(dataset, point.dataIndex)}
                      stroke={getBorderColor(dataset, point.dataIndex)}
                      strokeWidth={borderWidth}
                    />
                  ) : null}
                  {hoverRadius > pointRadius && (
                    <circle
                      cx={point.x}
                      cy={point.y}
                      r={hoverRadius}
                      fill="transparent"
                      pointerEvents="none"
                    />
                  )}
                </g>
              );
            })}
          </g>
        );
      })}
    </g>
  );
}

function renderRadarChart({
  datasets,
  options,
  layout,
  progress,
  datasetProgress,
  labels,
  onTooltip,
}: ChartRenderProps): React.ReactNode {
  const count = Math.max(labels.length, 1);
  const scale = options.scales?.r;
  const values = datasets.flatMap((dataset) =>
    dataset.data
      .map((value) => numberValue(value))
      .filter((value): value is number => value != null),
  );
  const [min, max] = resolveExtent(values, scale, true);
  const levels = tickValues(min, max, scale);
  const angleFor = (index: number) => -Math.PI / 2 + (index / count) * Math.PI * 2;
  const valueRadius = (value: number) =>
    ((value - min) / (max - min || 1)) * layout.radius;
  return (
    <g data-slot="chart-radar">
      {levels.map((level, levelIndex) => {
        const points = Array.from({ length: count }, (_, index) =>
          polarPoint(layout.centerX, layout.centerY, valueRadius(level), angleFor(index)),
        );
        return (
          <polygon
            key={`radar-grid-${levelIndex}`}
            points={points.map((point) => `${point.x},${point.y}`).join(" ")}
            fill="none"
            stroke={scale?.grid?.color ?? DEFAULT_GRID_COLOR}
            strokeOpacity={0.75}
          />
        );
      })}
      {labels.map((label, index) => {
        const point = polarPoint(
          layout.centerX,
          layout.centerY,
          layout.radius,
          angleFor(index),
        );
        const labelPoint = polarPoint(
          layout.centerX,
          layout.centerY,
          layout.radius + 18,
          angleFor(index),
        );
        return (
          <g key={`radar-axis-${label}`}>
            <line
              x1={layout.centerX}
              y1={layout.centerY}
              x2={point.x}
              y2={point.y}
              stroke={scale?.grid?.color ?? DEFAULT_GRID_COLOR}
              strokeOpacity={0.6}
            />
            <text
              x={labelPoint.x}
              y={labelPoint.y + 3}
              textAnchor={
                labelPoint.x < layout.centerX - 4
                  ? "end"
                  : labelPoint.x > layout.centerX + 4
                    ? "start"
                    : "middle"
              }
              fill={scale?.ticks?.color ?? DEFAULT_TEXT_COLOR}
              fontSize={scale?.ticks?.fontSize ?? 10}
            >
              {label}
            </text>
          </g>
        );
      })}
      {datasets.map((dataset, datasetIndex) => {
        const seriesProgress = progress * (datasetProgress[datasetIndex] ?? 1);
        const points = dataset.data.slice(0, count).map((raw, index) => {
          const value = numberValue(raw) ?? min;
          const point = polarPoint(
            layout.centerX,
            layout.centerY,
            valueRadius(value) * seriesProgress,
            angleFor(index),
          );
          return { ...point, raw, dataIndex: index };
        });
        const path = points.map((point) => `${point.x},${point.y}`).join(" ");
        return (
          <g
            key={`radar-series-${datasetIndex}`}
            opacity={datasetProgress[datasetIndex] ?? 1}
          >
            <polygon
              points={path}
              fill={getDatasetColor(dataset, datasetIndex)}
              fillOpacity={dataset.fill === false ? 0 : 0.16}
              stroke={getBorderColor(dataset, datasetIndex)}
              strokeWidth={getBorderWidth(dataset, options, "radar")}
              strokeLinejoin="round"
            />
            {points.map((point) => {
              const context = makeTooltipContext(
                dataset,
                datasetIndex,
                point.dataIndex,
                labels,
                point.raw,
              );
              return (
                <circle
                  key={`radar-point-${point.dataIndex}`}
                  cx={point.x}
                  cy={point.y}
                  r={dataset.pointRadius ?? options.elements?.point?.radius ?? 4}
                  fill={getDatasetColor(dataset, point.dataIndex)}
                  stroke={getBorderColor(dataset, point.dataIndex)}
                  strokeWidth={2}
                  onMouseEnter={() => onTooltip(point.x, point.y, context)}
                />
              );
            })}
          </g>
        );
      })}
    </g>
  );
}

function getThreeDOptions(options: ChartOptionsLike): ChartThreeDOptions | undefined {
  const plugins = options.plugins;
  return plugins && "threeD" in plugins ? plugins.threeD : undefined;
}

function radialSidewallColor(color: string): string {
  return `color-mix(in srgb, ${color} 58%, black)`;
}

function isRadialHoverTarget(target: EventTarget | null): boolean {
  return (
    typeof Element !== "undefined" &&
    target instanceof Element &&
    Boolean(target.closest('[data-slot="chart-arc-hit"], [data-slot="chart-tooltip"]'))
  );
}

function renderRadialChart({
  type,
  datasets,
  options,
  layout,
  progress,
  datasetProgress,
  labels,
  active,
  hoverProgress,
  onTooltip,
  onTooltipClear,
}: ChartRenderProps): React.ReactNode {
  const dataset = datasets[0];
  if (!dataset) return null;
  const visibility = datasetProgress[0] ?? 1;
  const radialProgress = progress * visibility;
  const values = dataset.data.map((value) => numberValue(value) ?? 0);
  const positiveValues = values.map((value) => Math.max(0, value));
  const total = positiveValues.reduce((sum, value) => sum + value, 0) || 1;
  const cutout = type === "donut" ? (options.cutout ?? "55%") : 0;
  const innerRadius =
    typeof cutout === "string"
      ? layout.radius * clamp(Number.parseFloat(cutout) / 100, 0, 0.95)
      : layout.radius * clamp(cutout / 100, 0, 0.95);
  const startAngle = -Math.PI / 2;
  let currentAngle = startAngle;
  const isPolar = type === "polarArea";
  const radialExtent = resolveExtent(positiveValues, options.scales?.r, true);
  const polarRadius = (value: number) =>
    ((value - radialExtent[0]) / (radialExtent[1] - radialExtent[0] || 1)) *
    layout.radius;
  const threeDOptions = getThreeDOptions(options);
  const threeD = !isPolar && (threeDOptions?.enabled ?? false);
  const depth = threeD ? Math.round(clamp(threeDOptions?.depth ?? 28, 0, 40)) : 0;
  const tilt = threeD ? clamp(threeDOptions?.tilt ?? 5, -30, 30) : 0;
  const perspective = threeD ? clamp(threeDOptions?.perspective ?? 0.58, 0.35, 1) : 1;
  const showLabels = threeD && (threeDOptions?.labels ?? true);
  const sectors = positiveValues.map((value, index) => {
    const sweep = isPolar
      ? (Math.PI * 2) / Math.max(values.length, 1)
      : (value / total) * Math.PI * 2;
    const sector = { value, index, start: currentAngle, sweep };
    currentAngle += sweep;
    return sector;
  });
  const sectorGeometry = sectors.map((sector) => {
    const outerRadius = isPolar
      ? polarRadius(sector.value) * radialProgress
      : layout.radius * radialProgress;
    const animatedInnerRadius = innerRadius * radialProgress;
    const endAngle = sector.start + sector.sweep * radialProgress;
    const isActive = active?.datasetIndex === 0 && active.dataIndex === sector.index;
    const offset = isActive
      ? (dataset.hoverOffset ?? 0) * clamp(hoverProgress ?? 1, 0, 1)
      : 0;
    const midpoint = sector.start + sector.sweep / 2;
    const context = makeTooltipContext(
      dataset,
      0,
      sector.index,
      labels,
      dataset.data[sector.index] ?? null,
    );
    const offsetX = Math.cos(midpoint) * offset;
    const offsetY = Math.sin(midpoint) * offset;
    const path = threeD
      ? projectedRadialArcPath(
          layout.centerX,
          layout.centerY,
          Math.max(1, outerRadius),
          animatedInnerRadius,
          sector.start,
          endAngle,
          perspective,
          tilt,
        )
      : arcPath(
          layout.centerX,
          layout.centerY,
          Math.max(1, outerRadius),
          animatedInnerRadius,
          sector.start,
          endAngle,
        );
    const motionTransform = offset ? `translate(${offsetX} ${offsetY})` : undefined;
    const labelRadius = layout.radius + 24;
    const labelPoint = {
      x: layout.centerX + Math.cos(midpoint) * labelRadius + (threeD ? tilt : 0),
      y: layout.centerY + Math.sin(midpoint) * labelRadius * (threeD ? perspective : 1),
    };
    const calloutPoint = {
      x: layout.centerX + Math.cos(midpoint) * (layout.radius + 3) + (threeD ? tilt : 0),
      y:
        layout.centerY +
        Math.sin(midpoint) * (layout.radius + 3) * (threeD ? perspective : 1),
    };
    const outerSegments = visibleRadialArcSegments(sector.start, endAngle, "front");
    const innerSegments =
      animatedInnerRadius > 0.5
        ? visibleRadialArcSegments(sector.start, endAngle, "back")
        : [];
    const sidewallOuterPaths = outerSegments.map(([segmentStart, segmentEnd]) =>
      radialSidewallBandPath(
        layout.centerX,
        layout.centerY,
        outerRadius,
        segmentStart,
        segmentEnd,
        depth,
        perspective,
        tilt,
      ),
    );
    const sidewallInnerPaths =
      animatedInnerRadius > 0.5
        ? innerSegments.map(([segmentStart, segmentEnd]) =>
            radialSidewallBandPath(
              layout.centerX,
              layout.centerY,
              animatedInnerRadius,
              segmentStart,
              segmentEnd,
              depth,
              perspective,
              tilt,
            ),
          )
        : [];
    const sidewallStartPath =
      sector.sweep > 0 && endAngle > sector.start
        ? radialSidewallFacePath(
            layout.centerX,
            layout.centerY,
            animatedInnerRadius,
            outerRadius,
            sector.start,
            depth,
            perspective,
            tilt,
          )
        : null;
    const sidewallEndPath =
      sector.sweep > 0 && endAngle > sector.start
        ? radialSidewallFacePath(
            layout.centerX,
            layout.centerY,
            animatedInnerRadius,
            outerRadius,
            endAngle,
            depth,
            perspective,
            tilt,
          )
        : null;
    const labelText =
      threeDOptions?.labelFormatter?.(context) ??
      `${labels[sector.index] ?? `Segment ${sector.index + 1}`}, ${context.value}`;
    return {
      sector,
      outerRadius,
      path,
      context,
      offsetX,
      offsetY,
      midpoint,
      topTransform: motionTransform,
      labelPoint,
      calloutPoint,
      labelText,
      isActive,
      sidewallTransform: motionTransform,
      sidewallOuterPaths,
      sidewallInnerPaths,
      sidewallStartPath,
      sidewallEndPath,
    };
  });
  const depthGeometry = [...sectorGeometry].sort(
    (a, b) => Math.sin(a.midpoint) - Math.sin(b.midpoint),
  );
  return (
    <g data-slot="chart-radial">
      {isPolar &&
        tickValues(radialExtent[0], radialExtent[1], options.scales?.r).map(
          (value, index) => (
            <circle
              key={`polar-grid-${index}`}
              cx={layout.centerX}
              cy={layout.centerY}
              r={polarRadius(value)}
              fill="none"
              stroke={options.scales?.r?.grid?.color ?? DEFAULT_GRID_COLOR}
              strokeOpacity={0.7}
            />
          ),
        )}
      {threeD && depth > 0 && (
        <g data-slot="chart-3d-depth" data-depth={depth} pointerEvents="none">
          {depthGeometry.map(
            ({
              sector,
              sidewallTransform,
              sidewallOuterPaths,
              sidewallInnerPaths,
              sidewallStartPath,
              sidewallEndPath,
            }) => {
              const sideColor = radialSidewallColor(
                getDatasetColor(dataset, sector.index),
              );
              const commonProps = {
                fill: sideColor,
                stroke: sideColor,
                strokeWidth: getBorderWidth(dataset, options, type),
                opacity: visibility,
                transform: sidewallTransform,
                pointerEvents: "none" as const,
              };
              return (
                <React.Fragment key={`arc-depth-${sector.index}`}>
                  {sidewallOuterPaths.map((d, index) => (
                    <path
                      key={`outer-${index}`}
                      data-slot="chart-arc-depth"
                      data-depth-side="outer"
                      data-depth-segment={index}
                      d={d}
                      {...commonProps}
                    />
                  ))}
                  {sidewallInnerPaths.map((d, index) => (
                    <path
                      key={`inner-${index}`}
                      data-slot="chart-arc-depth"
                      data-depth-side="inner"
                      data-depth-segment={index}
                      d={d}
                      {...commonProps}
                    />
                  ))}
                  {sidewallStartPath && (
                    <path
                      data-slot="chart-arc-depth"
                      data-depth-side="start"
                      d={sidewallStartPath}
                      {...commonProps}
                    />
                  )}
                  {sidewallEndPath && (
                    <path
                      data-slot="chart-arc-depth"
                      data-depth-side="end"
                      d={sidewallEndPath}
                      {...commonProps}
                    />
                  )}
                </React.Fragment>
              );
            },
          )}
        </g>
      )}
      {sectorGeometry.map(
        ({ sector, path, topTransform, labelPoint, calloutPoint, labelText }) => (
          <g key={`arc-${sector.index}`} data-slot="chart-arc-group">
            <path
              data-slot="chart-arc"
              d={path}
              transform={topTransform}
              fill={getDatasetColor(dataset, sector.index)}
              stroke={getBorderColor(dataset, sector.index)}
              strokeWidth={getBorderWidth(dataset, options, type)}
              opacity={visibility}
              pointerEvents="none"
            />
            {showLabels && (
              <g data-slot="chart-3d-label" opacity={visibility} pointerEvents="none">
                <line
                  data-slot="chart-3d-label-line"
                  x1={calloutPoint.x}
                  y1={calloutPoint.y}
                  x2={labelPoint.x}
                  y2={labelPoint.y}
                  stroke={DEFAULT_TEXT_COLOR}
                  strokeOpacity={0.72}
                  strokeWidth={1}
                />
                <text
                  data-slot="chart-3d-label-text"
                  x={labelPoint.x + (labelPoint.x < layout.centerX ? -4 : 4)}
                  y={labelPoint.y + 3}
                  textAnchor={labelPoint.x < layout.centerX ? "end" : "start"}
                  fill={DEFAULT_TEXT_COLOR}
                  stroke="#ffffff"
                  strokeWidth={2}
                  strokeOpacity={0.95}
                  strokeLinecap="round"
                  strokeLinejoin="round"
                  paintOrder="stroke"
                  fontSize={12}
                  fontWeight="600"
                >
                  {labelText}
                </text>
              </g>
            )}
          </g>
        ),
      )}
      <g data-slot="chart-radial-hit-area">
        {sectorGeometry.map(({ sector, path, context, midpoint, outerRadius }) => (
          <path
            key={`arc-hit-${sector.index}`}
            data-slot="chart-arc-hit"
            data-index={sector.index}
            d={path}
            fill="transparent"
            stroke="transparent"
            strokeWidth={1}
            pointerEvents="all"
            aria-hidden="true"
            onMouseEnter={() =>
              onTooltip(
                layout.centerX + tilt + Math.cos(midpoint) * outerRadius * 0.7,
                layout.centerY + Math.sin(midpoint) * outerRadius * 0.7 * perspective,
                context,
              )
            }
            onMouseLeave={(event) => {
              if (!isRadialHoverTarget(event.relatedTarget)) {
                onTooltipClear?.(event.relatedTarget);
              }
            }}
          />
        ))}
      </g>
    </g>
  );
}

function tooltipValue(
  context: ChartTooltipContext,
  options: ChartTooltipOptions | undefined,
): React.ReactNode {
  return (
    options?.callbacks?.label?.(context) ??
    `${context.datasetLabel ? `${context.datasetLabel}: ` : ""}${context.value}`
  );
}

function ChartTooltip({
  tooltip,
  options,
  viewBoxWidth,
  viewBoxHeight,
  reducedMotion,
  onTooltipClear,
}: {
  tooltip: TooltipState | null;
  options: ChartTooltipOptions | undefined;
  viewBoxWidth: number;
  viewBoxHeight: number;
  reducedMotion: boolean;
  onTooltipClear?: (relatedTarget: EventTarget | null) => void;
}): React.ReactNode {
  const [displayedTooltip, setDisplayedTooltip] = React.useState<TooltipState | null>(
    null,
  );
  const displayedTooltipRef = React.useRef<TooltipState | null>(null);
  const [visible, setVisible] = React.useState(false);

  React.useEffect(() => {
    if (tooltip) {
      const entering = displayedTooltipRef.current == null;
      displayedTooltipRef.current = tooltip;
      // eslint-disable-next-line react-hooks/set-state-in-effect -- synchronize tooltip content with the hovered chart value
      setDisplayedTooltip(tooltip);
      if (!entering || reducedMotion) {
        setVisible(true);
        return;
      }

      setVisible(false);
      const frame = window.requestAnimationFrame(() => setVisible(true));
      return () => window.cancelAnimationFrame(frame);
    }

    setVisible(false);
    const timeout = window.setTimeout(
      () => {
        displayedTooltipRef.current = null;
        setDisplayedTooltip(null);
      },
      reducedMotion ? 0 : TOOLTIP_TRANSITION_DURATION,
    );
    return () => window.clearTimeout(timeout);
  }, [reducedMotion, tooltip]);

  if (!displayedTooltip) return null;

  const title =
    options?.callbacks?.title?.(displayedTooltip.contexts) ??
    displayedTooltip.contexts[0]?.label;
  return (
    <div
      data-slot="chart-tooltip"
      data-state={visible ? "visible" : "hidden"}
      role="status"
      className="bg-foreground text-background pointer-events-auto absolute z-10 max-w-56 rounded-md px-2.5 py-2 text-xs shadow-lg"
      onMouseLeave={(event) => {
        if (!isRadialHoverTarget(event.relatedTarget)) {
          onTooltipClear?.(event.relatedTarget);
        }
      }}
      style={{
        left: `${(displayedTooltip.x / viewBoxWidth) * 100}%`,
        top: `${(displayedTooltip.y / viewBoxHeight) * 100}%`,
        opacity: visible ? 1 : 0,
        transform: `translate(-50%, calc(-100% - 10px)) scale(${visible ? 1 : 0.96}) translateY(${visible ? 0 : 4}px)`,
        transition: reducedMotion
          ? "none"
          : `opacity ${TOOLTIP_TRANSITION_DURATION}ms ease-out, transform ${TOOLTIP_TRANSITION_DURATION}ms cubic-bezier(0.16, 1, 0.3, 1)`,
        willChange: "opacity, transform",
      }}
    >
      {title ? <div className="mb-1 font-medium">{title}</div> : null}
      {displayedTooltip.contexts.map((context) => (
        <div key={`${context.datasetIndex}-${context.dataIndex}`}>
          {tooltipValue(context, options)}
        </div>
      ))}
    </div>
  );
}

function renderDataTable(
  data: ChartData,
  labels: readonly string[],
  type: ChartType,
  label: string,
): React.ReactNode {
  return (
    <table className="sr-only" data-slot="chart-data-table">
      <caption>{label}</caption>
      <thead>
        <tr>
          <th scope="col">
            {type === "scatter" || type === "bubble" ? "Point" : "Label"}
          </th>
          {data.datasets.map((dataset, index) => (
            <th scope="col" key={`${dataset.label ?? "dataset"}-${index}`}>
              {dataset.label ?? `Dataset ${index + 1}`}
            </th>
          ))}
        </tr>
      </thead>
      <tbody>
        {Array.from(
          {
            length: Math.max(
              labels.length,
              ...data.datasets.map((dataset) => dataset.data.length),
              0,
            ),
          },
          (_, rowIndex) => (
            <tr key={rowIndex}>
              <th scope="row">{labels[rowIndex] ?? `Point ${rowIndex + 1}`}</th>
              {data.datasets.map((dataset, datasetIndex) => {
                const raw = dataset.data[rowIndex];
                const value =
                  raw && typeof raw === "object"
                    ? `${raw.x}, ${raw.y}${"r" in raw ? `, ${raw.r}` : ""}`
                    : (raw ?? "—");
                return <td key={`${datasetIndex}-${rowIndex}`}>{String(value)}</td>;
              })}
            </tr>
          ),
        )}
      </tbody>
    </table>
  );
}

function assignRef<T>(ref: React.ForwardedRef<T>, value: T | null): void {
  if (typeof ref === "function") {
    ref(value);
  } else if (ref) {
    (ref as React.MutableRefObject<T | null>).current = value;
  }
}

export const Chart = React.forwardRef<HTMLDivElement, ChartProps>(
  (
    {
      className,
      style,
      type,
      data,
      options = {},
      height,
      fallback,
      showDataTable = false,
      "aria-label": ariaLabel,
      "aria-describedby": ariaDescribedBy,
      ...props
    },
    ref,
  ) => {
    const chartRef = React.useRef<HTMLDivElement | null>(null);
    const setChartRef = React.useCallback(
      (node: HTMLDivElement | null) => {
        chartRef.current = node;
        assignRef(ref, node);
      },
      [ref],
    );
    const viewport = useChartViewport(chartRef);
    const reducedMotion = usePrefersReducedMotion();
    const progress = useChartProgress(type, data, options.animation, reducedMotion);
    const isRadialVisual = isRadialChart(type);
    const {
      hidden,
      progress: datasetProgress,
      toggle: toggleDataset,
    } = useDatasetVisibility(data.datasets, options, reducedMotion);
    const [tooltip, setTooltip] = React.useState<TooltipState | null>(null);
    const labels = data.labels ?? [];
    const interactiveDatasets = data.datasets.filter(
      (_, index) => (datasetProgress[index] ?? 1) > 0,
    );
    const titleFontSize = getTitleFontSize(options.plugins?.title, type, viewport);
    const layout = getLayout(options, data.datasets, type, titleFontSize);
    const hoverState = useRadialHoverProgress(
      type,
      tooltip?.active ?? null,
      options,
      reducedMotion,
    );
    const accessibleLabel = ariaLabel ?? `${type} chart`;
    const hasData =
      data.datasets.length > 0 &&
      data.datasets.some((dataset) =>
        dataset.data.some(
          (point) =>
            numberValue(point) != null ||
            (point && typeof point === "object" && finite(point.x)),
        ),
      );
    const responsive = options.responsive !== false;
    const maintainAspectRatio = options.maintainAspectRatio !== false;
    const aspectRatio = options.aspectRatio ?? (isRadialVisual ? 1 : 2);
    const computedStyle: React.CSSProperties = {
      display: "block",
      flexShrink: 0,
      minWidth: 0,
      ...(responsive ? { width: "100%" } : { width: `${layout.viewBoxWidth}px` }),
      ...(height != null
        ? { height: typeof height === "number" ? `${height}px` : height }
        : maintainAspectRatio
          ? { aspectRatio: String(aspectRatio) }
          : { height: `${layout.viewBoxHeight}px` }),
      ...(height == null ? { minHeight: "320px" } : {}),
      ...style,
    };

    const onTooltip = (x: number, y: number, context: ChartTooltipContext) => {
      const tooltipOptions = options.plugins?.tooltip;
      if (tooltipOptions?.enabled === false) return;
      const contexts =
        tooltipOptions?.mode === "index"
          ? interactiveDatasets.flatMap((dataset) => {
              const actualIndex = data.datasets.indexOf(dataset);
              const raw = dataset.data[context.dataIndex];
              return raw == null
                ? []
                : [
                    makeTooltipContext(
                      dataset,
                      actualIndex,
                      context.dataIndex,
                      labels,
                      raw,
                    ),
                  ];
            })
          : [context];
      setTooltip({
        x,
        y,
        active: { datasetIndex: context.datasetIndex, dataIndex: context.dataIndex },
        contexts,
      });
    };

    return (
      <div
        ref={setChartRef}
        data-slot="chart"
        data-chart-type={type}
        className={cn("relative min-w-0", className)}
        style={computedStyle}
        onMouseLeave={(event) => {
          if (!isRadialHoverTarget(event.relatedTarget)) setTooltip(null);
        }}
        {...props}
      >
        {hasData ? (
          <svg
            data-slot="chart-svg"
            role="img"
            aria-label={accessibleLabel}
            aria-describedby={ariaDescribedBy}
            viewBox={`0 0 ${layout.viewBoxWidth} ${layout.viewBoxHeight}`}
            preserveAspectRatio={isRadialVisual ? "xMidYMid meet" : "none"}
            className="block size-full overflow-visible"
          >
            {ariaDescribedBy ? null : (
              <desc>{`Interactive ${type} chart with ${data.datasets.length} dataset${data.datasets.length === 1 ? "" : "s"}.`}</desc>
            )}
            {renderTitle(options, layout, titleFontSize)}
            {renderLegend(options, data.datasets, hidden, toggleDataset, layout)}
            {type === "radar"
              ? renderRadarChart({
                  data,
                  datasets: data.datasets,
                  type,
                  options,
                  layout,
                  progress,
                  datasetProgress,
                  labels,
                  onTooltip,
                })
              : type === "pie" || type === "donut" || type === "polarArea"
                ? renderRadialChart({
                    data,
                    datasets: data.datasets,
                    type,
                    options,
                    layout,
                    progress,
                    datasetProgress,
                    labels,
                    active: hoverState.active,
                    hoverProgress: hoverState.progress,
                    onTooltip,
                    onTooltipClear: (relatedTarget) => {
                      if (!isRadialHoverTarget(relatedTarget)) setTooltip(null);
                    },
                  })
                : renderCartesianChart({
                    data,
                    datasets: data.datasets,
                    type,
                    options,
                    layout,
                    progress,
                    datasetProgress,
                    labels,
                    onTooltip,
                  })}
          </svg>
        ) : (
          <div
            role="img"
            aria-label={accessibleLabel}
            className="text-muted-foreground flex size-full min-h-32 items-center justify-center text-sm"
          >
            {fallback ?? "No chart data"}
          </div>
        )}
        <ChartTooltip
          tooltip={tooltip}
          options={options.plugins?.tooltip}
          viewBoxWidth={layout.viewBoxWidth}
          viewBoxHeight={layout.viewBoxHeight}
          reducedMotion={reducedMotion}
          onTooltipClear={(relatedTarget) => {
            if (!isRadialHoverTarget(relatedTarget)) setTooltip(null);
          }}
        />
        {showDataTable && renderDataTable(data, labels, type, accessibleLabel)}
      </div>
    );
  },
);

Chart.displayName = "Chart";