shadimage
corr
componentbar-chartsource: ok · hit

Bar Chart

A styled EvilCharts bar chart for comparing categories with stacked, gradient, hatched, and horizontal variants.

npx shadcn add "https://ui.corr.sh/r/bar-chart.json"
free

Visual style

gradient

Tags auto-generated by rules on 9/24/2026 — empty means no evidence, not lack of support.

Dependencies

Bundle weight · medium
animationmotion
chartsrecharts

Registry dependencies

https://ui.corr.sh/r/chart-empty-loading-error.json

Files7 · source cached 10/14/2026, 11:30:03 AM

src/components/evilcharts/charts/bar-chart.tsx
"use client"

import {
  type ChartConfig,
  ChartContainer,
  getColorsCount,
  getLoadingData,
  LoadingIndicator,
} from "@/components/evilcharts/ui/chart"
import {
  EvilBrush,
  useEvilBrush,
  type EvilBrushRange,
} from "@/components/evilcharts/ui/evil-brush"
import {
  ChartTooltip,
  ChartTooltipContent,
  type TooltipRoundness,
  type TooltipVariant,
} from "@/components/evilcharts/ui/tooltip"
import {
  ChartLegend,
  ChartLegendContent,
  type ChartLegendVariant,
} from "@/components/evilcharts/ui/legend"
import {
  Bar,
  BarChart,
  CartesianGrid,
  LabelList,
  Rectangle,
  ReferenceLine,
  XAxis,
  YAxis,
} from "recharts"
import {
  useCallback,
  useId,
  useMemo,
  useRef,
  useState,
  type ComponentProps,
} from "react"
import {
  ChartBackground,
  type BackgroundVariant,
} from "@/components/evilcharts/ui/background"
import type { RectRadius } from "recharts/types/shape/Rectangle"
import { motion } from "motion/react"

// Constants
const DEFAULT_BAR_RADIUS = 2
const LOADING_BAR_DATA_KEY = "loading"
const LOADING_ANIMATION_DURATION = 2000 // in milliseconds

type ChartProps = ComponentProps<typeof BarChart>
type XAxisProps = ComponentProps<typeof XAxis>
type YAxisProps = ComponentProps<typeof YAxis>
type LabelListProps = ComponentProps<typeof LabelList>
type BarVariant =
  | "default"
  | "hatched"
  | "duotone"
  | "duotone-reverse"
  | "gradient"
  | "stripped"
type StackType = "default" | "stacked" | "percent"
type BarLayout = "vertical" | "horizontal"

// Validating Types to make sure user have provided valid data according to chartConfig
type ValidateConfigKeys<TData, TConfig> = {
  [K in keyof TConfig]: K extends keyof TData ? ChartConfig[string] : never
}

// Extract only keys from TData where the value is a number (not string, boolean, etc.)
type NumericDataKeys<T> = {
  [K in keyof T]: T[K] extends number ? K : never
}[keyof T]

type EvilBarChartProps<
  TData extends Record<string, unknown>,
  TConfig extends Record<string, ChartConfig[string]>,
> = {
  chartConfig: TConfig & ValidateConfigKeys<TData, TConfig>
  data: TData[]
  xDataKey?: keyof TData & string
  yDataKey?: keyof TData & string
  className?: string
  chartProps?: ChartProps
  xAxisProps?: XAxisProps
  yAxisProps?: YAxisProps
  defaultSelectedDataKey?: string | null
  barVariant?: BarVariant
  stackType?: StackType
  layout?: BarLayout
  barRadius?: number
  barGap?: number
  barCategoryGap?: number
  showLabels?: boolean
  labelListProps?: Omit<LabelListProps, "dataKey">
  tickGap?: number
  legendVariant?: ChartLegendVariant
  // Hide Stuffs
  hideTooltip?: boolean
  hideCartesianGrid?: boolean
  hideLegend?: boolean
  // Tooltip
  tooltipRoundness?: TooltipRoundness
  tooltipVariant?: TooltipVariant
  tooltipDefaultIndex?: number
  // Interactive Stuffs
  enableHoverHighlight?: boolean
  isLoading?: boolean
  loadingBars?: number
  // Glow Effects
  glowingBars?: NumericDataKeys<TData>[]
  // Brush
  showBrush?: boolean
  brushHeight?: number
  brushFormatLabel?: (value: unknown, index: number) => string
  onBrushChange?: (range: EvilBrushRange) => void
  // Background
  backgroundVariant?: BackgroundVariant
  // Buffer Bar - renders last data point bars as hatched/lines style
  enableBufferBar?: boolean
}

type EvilBarChartClickable = {
  isClickable: true
  onSelectionChange?: (selectedDataKey: string | null) => void
}

type EvilBarChartNotClickable = {
  isClickable?: false
  onSelectionChange?: never
}

type EvilBarChartPropsWithCallback<
  TData extends Record<string, unknown>,
  TConfig extends Record<string, ChartConfig[string]>,
> = EvilBarChartProps<TData, TConfig> &
  (EvilBarChartClickable | EvilBarChartNotClickable)

export function EvilBarChart<
  TData extends Record<string, unknown>,
  TConfig extends Record<string, ChartConfig[string]>,
>({
  chartConfig,
  data,
  xDataKey,
  yDataKey,
  className,
  chartProps,
  xAxisProps,
  yAxisProps,
  defaultSelectedDataKey = null,
  barVariant = "default",
  stackType = "default",
  layout = "vertical",
  barRadius = DEFAULT_BAR_RADIUS,
  barGap,
  barCategoryGap,
  showLabels = false,
  labelListProps,
  tickGap = 8,
  legendVariant,
  hideTooltip = false,
  hideCartesianGrid = false,
  hideLegend = false,
  tooltipRoundness,
  tooltipVariant,
  tooltipDefaultIndex,
  isClickable = false,
  enableHoverHighlight = false,
  isLoading = false,
  loadingBars,
  glowingBars = [],
  showBrush = false,
  brushHeight,
  brushFormatLabel,
  onBrushChange,
  onSelectionChange,
  backgroundVariant,
  enableBufferBar = false,
}: EvilBarChartPropsWithCallback<TData, TConfig>) {
  const [selectedDataKey, setSelectedDataKey] = useState<string | null>(
    defaultSelectedDataKey
  )
  const [isMouseInChart, setIsMouseInChart] = useState(false)
  const { loadingData, onShimmerExit } = useLoadingData(isLoading, loadingBars)
  const chartId = useId().replace(/:/g, "") // Remove colons for valid CSS selectors

  // ── Zoom state ──────────────────────────────────────────────────────────
  const { visibleData, brushProps } = useEvilBrush({ data })
  const displayData = showBrush && !isLoading ? visibleData : data

  // Wrapper function to update state and call parent callback
  const handleSelectionChange = useCallback(
    (newSelectedDataKey: string | null) => {
      setSelectedDataKey(newSelectedDataKey)
      if (isClickable && onSelectionChange) {
        onSelectionChange(newSelectedDataKey)
      }
    },
    [onSelectionChange, isClickable]
  )

  const isStacked = stackType === "stacked" || stackType === "percent"
  const isHorizontal = layout === "horizontal"

  return (
    <ChartContainer
      className={className}
      config={chartConfig}
      footer={
        showBrush &&
        !isLoading && (
          <EvilBrush
            data={data}
            chartConfig={chartConfig}
            xDataKey={xDataKey}
            variant="bar"
            barRadius={barRadius}
            height={brushHeight}
            formatLabel={brushFormatLabel}
            stacked={isStacked}
            skipStyle
            className="mt-1"
            {...brushProps}
            onChange={(range) => {
              brushProps.onChange(range)
              onBrushChange?.(range)
            }}
          />
        )
      }
    >
      <LoadingIndicator isLoading={isLoading} />
      <BarChart
        id="evil-charts-bar-chart"
        accessibilityLayer
        layout={isHorizontal ? "vertical" : "horizontal"}
        data={isLoading ? loadingData : displayData}
        barGap={barGap}
        barCategoryGap={barCategoryGap}
        stackOffset={stackType === "percent" ? "expand" : undefined}
        onMouseEnter={() => setIsMouseInChart(true)}
        onMouseLeave={() => setIsMouseInChart(false)}
        {...chartProps}
      >
        {backgroundVariant && <ChartBackground variant={backgroundVariant} />}
        <ReferenceLine color="white" />
        {!hideCartesianGrid && !backgroundVariant && (
          <CartesianGrid
            vertical={isHorizontal}
            horizontal={!isHorizontal}
            strokeDasharray="3 3"
          />
        )}
        {!hideLegend && (
          <ChartLegend
            verticalAlign="top"
            align="right"
            content={
              <ChartLegendContent
                selected={selectedDataKey}
                onSelectChange={handleSelectionChange}
                isClickable={isClickable}
                variant={legendVariant}
              />
            }
          />
        )}
        {xDataKey && !isLoading && (
          <XAxis
            dataKey={isHorizontal ? undefined : xDataKey}
            type={isHorizontal ? "number" : "category"}
            tickLine={false}
            axisLine={false}
            tickMargin={8}
            minTickGap={tickGap}
            {...xAxisProps}
          />
        )}
        {(isHorizontal ? xDataKey : yDataKey) && !isLoading && (
          <YAxis
            dataKey={isHorizontal ? xDataKey : yDataKey}
            type={isHorizontal ? "category" : "number"}
            tickLine={false}
            axisLine={false}
            tickMargin={8}
            minTickGap={tickGap}
            width="auto"
            {...yAxisProps}
          />
        )}
        {!hideTooltip && !isLoading && (
          <ChartTooltip
            cursor={false}
            defaultIndex={tooltipDefaultIndex}
            content={
              <ChartTooltipContent
                selected={selectedDataKey}
                roundness={tooltipRoundness}
                variant={tooltipVariant}
              />
            }
          />
        )}
        {!isLoading &&
          Object.keys(chartConfig).map((dataKey) => {
            const isGlowing = glowingBars.includes(
              dataKey as NumericDataKeys<TData>
            )
            const filter = isGlowing
              ? `url(#${chartId}-bar-glow-${dataKey})`
              : undefined

            // Shared props for both shape and activeBar
            const customBarProps = {
              chartId,
              dataKey,
              barVariant,
              barRadius,
              filter,
              isClickable,
              enableHoverHighlight,
              isMouseInChart,
              selectedDataKey,
              enableBufferBar,
              dataLength: displayData.length,
              onClick: () => {
                if (!isClickable) return
                handleSelectionChange(
                  selectedDataKey === dataKey ? null : dataKey
                )
              },
            }

            return (
              <Bar
                key={dataKey}
                dataKey={dataKey}
                stackId={isStacked ? "evil-stacked" : undefined}
                fill={`url(#${chartId}-colors-${dataKey})`}
                radius={barRadius}
                style={
                  isClickable || enableHoverHighlight
                    ? { cursor: "pointer" }
                    : undefined
                }
                shape={(props: unknown) => (
                  <CustomBar
                    {...(props as BarShapeProps)}
                    {...customBarProps}
                  />
                )}
                activeBar={(props: unknown) => (
                  <CustomBar
                    {...(props as BarShapeProps)}
                    {...customBarProps}
                  />
                )}
              >
                {showLabels && (
                  <LabelList
                    dataKey={dataKey}
                    position="top"
                    className="fill-muted-foreground text-[10px]"
                    {...labelListProps}
                  />
                )}
              </Bar>
            )
          })}
        {/* ======== LOADING BAR ======== */}
        {isLoading && (
          <Bar
            dataKey={LOADING_BAR_DATA_KEY}
            fill="currentColor"
            fillOpacity={0.15}
            radius={barRadius}
            isAnimationActive={false}
            legendType="none"
            style={{ mask: `url(#${chartId}-loading-mask)` }}
          />
        )}
        {/* ======== CHART STYLES ======== */}
        <defs>
          {isLoading && (
            <LoadingBarPatternStyle
              chartId={chartId}
              onShimmerExit={onShimmerExit}
            />
          )}
          {/* Shared vertical color gradient - always rendered for fill */}
          <VerticalColorGradientStyle
            chartConfig={chartConfig}
            chartId={chartId}
          />
          {/* Variant-specific styles */}
          {barVariant === "hatched" && (
            <HatchedPatternStyle chartConfig={chartConfig} chartId={chartId} />
          )}
          {barVariant === "duotone" && (
            <DuotonePatternStyle chartConfig={chartConfig} chartId={chartId} />
          )}
          {barVariant === "duotone-reverse" && (
            <DuotoneReversePatternStyle
              chartConfig={chartConfig}
              chartId={chartId}
            />
          )}
          {barVariant === "gradient" && (
            <GradientPatternStyle chartConfig={chartConfig} chartId={chartId} />
          )}
          {barVariant === "stripped" && (
            <StrippedPatternStyle chartConfig={chartConfig} chartId={chartId} />
          )}
          {/* Buffer bar hatched pattern - always rendered when enableBufferBar */}
          {enableBufferBar && (
            <BufferHatchedPatternStyle
              chartConfig={chartConfig}
              chartId={chartId}
            />
          )}
          {/* Glow filter for glowing bars */}
          {glowingBars.length > 0 && (
            <GlowFilterStyle
              chartId={chartId}
              glowingBars={glowingBars as string[]}
            />
          )}
        </defs>
      </BarChart>
    </ChartContainer>
  )
}

// Types for custom bar shape
type BarShapeProps = {
  x?: number
  y?: number
  width?: number
  height?: number
  fill?: string
  fillOpacity?: number
  dataKey?: string
  index?: number
  [key: string]: unknown
}

type CustomBarProps = {
  chartId: string
  dataKey: string
  barVariant: BarVariant
  barRadius: number
  filter?: string
  isClickable?: boolean
  enableHoverHighlight?: boolean
  isMouseInChart?: boolean
  selectedDataKey?: string | null
  isActive?: boolean
  enableBufferBar?: boolean
  dataLength?: number
  onClick?: () => void
} & BarShapeProps

// Custom bar shape component for different variants
const CustomBar = (props: CustomBarProps) => {
  const {
    x = 0,
    y = 0,
    width = 0,
    height = 0,
    chartId,
    dataKey,
    barVariant,
    barRadius,
    filter,
    isClickable,
    enableHoverHighlight,
    isMouseInChart,
    selectedDataKey,
    isActive,
    enableBufferBar,
    dataLength = 0,
    onClick,
  } = props

  const index = typeof props.index === "number" ? props.index : -1
  const isLastBar =
    enableBufferBar && dataLength > 0 && index === dataLength - 1
  const isStripped = barVariant === "stripped"

  const getFill = () => {
    // Buffer bar: last bar always uses hatched pattern
    if (isLastBar) {
      return `url(#${chartId}-buffer-hatched-${dataKey})`
    }

    switch (barVariant) {
      case "hatched":
        return `url(#${chartId}-hatched-${dataKey})`
      case "duotone":
        return `url(#${chartId}-duotone-${dataKey})`
      case "duotone-reverse":
        return `url(#${chartId}-duotone-reverse-${dataKey})`
      case "gradient":
        return `url(#${chartId}-gradient-${dataKey})`
      case "stripped":
        return `url(#${chartId}-stripped-${dataKey})`
      default:
        return `url(#${chartId}-colors-${dataKey})`
    }
  }

  const fillOpacity = getBarOpacity({
    isClickable,
    selectedDataKey,
    dataKey,
    enableHoverHighlight,
    isMouseInChart,
    isActive,
  })
  const cursorStyle =
    isClickable || enableHoverHighlight ? { cursor: "pointer" } : undefined

  // For stripped: top corners rounded, bottom flat [topLeft, topRight, bottomRight, bottomLeft]
  // For others: all corners rounded
  const radius: RectRadius = isStripped
    ? [barRadius, barRadius, 0, 0]
    : barRadius

  return (
    <g style={cursorStyle} onClick={onClick}>
      {/* Transparent rectangle for full column hit area */}
      <Rectangle {...props} fill="transparent" />
      {/* Visible bar with animated opacity */}
      <Rectangle
        x={x}
        y={y}
        width={width}
        opacity={fillOpacity}
        height={Math.max(0, height - 3)}
        radius={radius}
        fill={getFill()}
        filter={filter}
        stroke={isLastBar ? `url(#${chartId}-colors-${dataKey})` : undefined}
        strokeWidth={isLastBar ? 1 : undefined}
      />
      {/* Top border strip for stripped variant */}
      {isStripped && (
        <Rectangle
          x={x}
          y={y - 4}
          width={width}
          height={2}
          radius={1}
          fill={`url(#${chartId}-colors-${dataKey})`}
        />
      )}
    </g>
  )
}

// Shared vertical color gradient (top to bottom) - used for bar fill
const VerticalColorGradientStyle = ({
  chartConfig,
  chartId,
}: {
  chartConfig: ChartConfig
  chartId: string
}) => {
  return (
    <>
      {Object.entries(chartConfig).map(([dataKey, config]) => {
        const colorsCount = getColorsCount(config)

        return (
          <linearGradient
            key={`${chartId}-colors-${dataKey}`}
            id={`${chartId}-colors-${dataKey}`}
            x1="0"
            y1="0"
            x2="0"
            y2="1"
          >
            {colorsCount === 1 ? (
              <>
                <stop offset="0%" stopColor={`var(--color-${dataKey}-0)`} />
                <stop offset="100%" stopColor={`var(--color-${dataKey}-0)`} />
              </>
            ) : (
              Array.from({ length: colorsCount }, (_, index) => (
                <stop
                  key={index}
                  offset={`${(index / (colorsCount - 1)) * 100}%`}
                  stopColor={`var(--color-${dataKey}-${index}, var(--color-${dataKey}-0))`}
                />
              ))
            )}
          </linearGradient>
        )
      })}
    </>
  )
}

// Hatched pattern style for bars - uses mask to preserve gradient colors
const HatchedPatternStyle = ({
  chartConfig,
  chartId,
}: {
  chartConfig: ChartConfig
  chartId: string
}) => {
  return (
    <>
      {/* Shared hatched stripes mask pattern */}
      <pattern
        id={`${chartId}-hatched-mask-pattern`}
        x="0"
        y="0"
        width="5"
        height="5"
        patternUnits="userSpaceOnUse"
        patternTransform="rotate(-45)"
      >
        <rect width="5" height="5" fill="white" fillOpacity={0.3} />
        <rect width="1.5" height="5" fill="white" fillOpacity={1} />
      </pattern>

      {Object.keys(chartConfig).map((dataKey) => (
        <g key={`${chartId}-hatched-group-${dataKey}`}>
          {/* Mask using hatched stripes */}
          <mask id={`${chartId}-hatched-mask-${dataKey}`}>
            <rect
              width="100%"
              height="100%"
              fill={`url(#${chartId}-hatched-mask-pattern)`}
            />
          </mask>

          {/* Pattern: gradient fill masked by hatched stripes */}
          <pattern
            id={`${chartId}-hatched-${dataKey}`}
            patternUnits="userSpaceOnUse"
            width="100%"
            height="100%"
          >
            <rect
              width="100%"
              height="100%"
              fill={`url(#${chartId}-colors-${dataKey})`}
              mask={`url(#${chartId}-hatched-mask-${dataKey})`}
            />
          </pattern>
        </g>
      ))}
    </>
  )
}

// Buffer hatched pattern style - diagonal lines only (no background fill), used for the last bar when enableBufferBar is true
const BufferHatchedPatternStyle = ({
  chartConfig,
  chartId,
}: {
  chartConfig: ChartConfig
  chartId: string
}) => {
  return (
    <>
      {/* Shared buffer hatched stripes mask pattern - lines only, no background */}
      <pattern
        id={`${chartId}-buffer-hatched-mask-pattern`}
        x="0"
        y="0"
        width="5"
        height="5"
        patternUnits="userSpaceOnUse"
        patternTransform="rotate(-45)"
      >
        <rect width="5" height="5" fill="black" fillOpacity={0} />
        <rect width="1" height="5" fill="white" fillOpacity={1} />
      </pattern>

      {Object.keys(chartConfig).map((dataKey) => (
        <g key={`${chartId}-buffer-hatched-group-${dataKey}`}>
          {/* Mask using buffer hatched stripes */}
          <mask id={`${chartId}-buffer-hatched-mask-${dataKey}`}>
            <rect
              width="100%"
              height="100%"
              fill={`url(#${chartId}-buffer-hatched-mask-pattern)`}
            />
          </mask>

          {/* Pattern: gradient fill masked by buffer hatched stripes - lines only */}
          <pattern
            id={`${chartId}-buffer-hatched-${dataKey}`}
            patternUnits="userSpaceOnUse"
            width="100%"
            height="100%"
          >
            <rect
              width="100%"
              height="100%"
              fill={`url(#${chartId}-colors-${dataKey})`}
              mask={`url(#${chartId}-buffer-hatched-mask-${dataKey})`}
            />
          </pattern>
        </g>
      ))}
    </>
  )
}

// Duotone pattern style for bars (half opacity, half full) - uses objectBoundingBox for per-bar effect
const DuotonePatternStyle = ({
  chartConfig,
  chartId,
}: {
  chartConfig: ChartConfig
  chartId: string
}) => {
  return (
    <>
      {Object.entries(chartConfig).map(([dataKey, config]) => {
        const colorsCount = getColorsCount(config)

        return (
          <g key={`${chartId}-duotone-group-${dataKey}`}>
            {/* Duotone mask gradient - applies to each bar's bounding box */}
            <linearGradient
              id={`${chartId}-duotone-mask-gradient-${dataKey}`}
              gradientUnits="objectBoundingBox"
              x1="0"
              y1="0"
              x2="1"
              y2="0"
            >
              <stop offset="50%" stopColor="white" stopOpacity={0.4} />
              <stop offset="50%" stopColor="white" stopOpacity={1} />
            </linearGradient>

            {/* Color gradient for this dataKey - applies to each bar's bounding box */}
            <linearGradient
              id={`${chartId}-duotone-colors-${dataKey}`}
              gradientUnits="objectBoundingBox"
              x1="0"
              y1="0"
              x2="0"
              y2="1"
            >
              {colorsCount === 1 ? (
                <>
                  <stop offset="0%" stopColor={`var(--color-${dataKey}-0)`} />
                  <stop offset="100%" stopColor={`var(--color-${dataKey}-0)`} />
                </>
              ) : (
                Array.from({ length: colorsCount }, (_, index) => (
                  <stop
                    key={index}
                    offset={`${(index / (colorsCount - 1)) * 100}%`}
                    stopColor={`var(--color-${dataKey}-${index}, var(--color-${dataKey}-0))`}
                  />
                ))
              )}
            </linearGradient>

            {/* Mask for duotone effect */}
            <mask
              id={`${chartId}-duotone-mask-${dataKey}`}
              maskContentUnits="objectBoundingBox"
            >
              <rect
                x="0"
                y="0"
                width="1"
                height="1"
                fill={`url(#${chartId}-duotone-mask-gradient-${dataKey})`}
              />
            </mask>

            {/* Pattern: gradient fill with duotone mask */}
            <pattern
              id={`${chartId}-duotone-${dataKey}`}
              patternUnits="objectBoundingBox"
              patternContentUnits="objectBoundingBox"
              width="1"
              height="1"
            >
              <rect
                x="0"
                y="0"
                width="1"
                height="1"
                fill={`url(#${chartId}-duotone-colors-${dataKey})`}
                mask={`url(#${chartId}-duotone-mask-${dataKey})`}
              />
            </pattern>
          </g>
        )
      })}
    </>
  )
}

// Duotone reverse pattern style for bars (full opacity first, then half) - uses objectBoundingBox for per-bar effect
const DuotoneReversePatternStyle = ({
  chartConfig,
  chartId,
}: {
  chartConfig: ChartConfig
  chartId: string
}) => {
  return (
    <>
      {Object.entries(chartConfig).map(([dataKey, config]) => {
        const colorsCount = getColorsCount(config)

        return (
          <g key={`${chartId}-duotone-reverse-group-${dataKey}`}>
            {/* Duotone reverse mask gradient - applies to each bar's bounding box */}
            <linearGradient
              id={`${chartId}-duotone-reverse-mask-gradient-${dataKey}`}
              gradientUnits="objectBoundingBox"
              x1="0"
              y1="0"
              x2="1"
              y2="0"
            >
              <stop offset="50%" stopColor="white" stopOpacity={1} />
              <stop offset="50%" stopColor="white" stopOpacity={0.4} />
            </linearGradient>

            {/* Color gradient for this dataKey - applies to each bar's bounding box */}
            <linearGradient
              id={`${chartId}-duotone-reverse-colors-${dataKey}`}
              gradientUnits="objectBoundingBox"
              x1="0"
              y1="0"
              x2="0"
              y2="1"
            >
              {colorsCount === 1 ? (
                <>
                  <stop offset="0%" stopColor={`var(--color-${dataKey}-0)`} />
                  <stop offset="100%" stopColor={`var(--color-${dataKey}-0)`} />
                </>
              ) : (
                Array.from({ length: colorsCount }, (_, index) => (
                  <stop
                    key={index}
                    offset={`${(index / (colorsCount - 1)) * 100}%`}
                    stopColor={`var(--color-${dataKey}-${index}, var(--color-${dataKey}-0))`}
                  />
                ))
              )}
            </linearGradient>

            {/* Mask for duotone reverse effect */}
            <mask
              id={`${chartId}-duotone-reverse-mask-${dataKey}`}
              maskContentUnits="objectBoundingBox"
            >
              <rect
                x="0"
                y="0"
                width="1"
                height="1"
                fill={`url(#${chartId}-duotone-reverse-mask-gradient-${dataKey})`}
              />
            </mask>

            {/* Pattern: gradient fill with duotone reverse mask */}
            <pattern
              id={`${chartId}-duotone-reverse-${dataKey}`}
              patternUnits="objectBoundingBox"
              patternContentUnits="objectBoundingBox"
              width="1"
              height="1"
            >
              <rect
                x="0"
                y="0"
                width="1"
                height="1"
                fill={`url(#${chartId}-duotone-reverse-colors-${dataKey})`}
                mask={`url(#${chartId}-duotone-reverse-mask-${dataKey})`}
              />
            </pattern>
          </g>
        )
      })}
    </>
  )
}

// Gradient pattern style for bars (top to bottom fade) - uses mask to preserve gradient colors
const GradientPatternStyle = ({
  chartConfig,
  chartId,
}: {
  chartConfig: ChartConfig
  chartId: string
}) => {
  return (
    <>
      {/* Shared vertical fade gradient for mask */}
      <linearGradient
        id={`${chartId}-gradient-mask-gradient`}
        x1="0"
        y1="0"
        x2="0"
        y2="1"
      >
        <stop offset="20%" stopColor="white" stopOpacity={1} />
        <stop offset="90%" stopColor="white" stopOpacity={0} />
      </linearGradient>

      {Object.keys(chartConfig).map((dataKey) => (
        <g key={`${chartId}-gradient-group-${dataKey}`}>
          {/* Mask for vertical fade */}
          <mask id={`${chartId}-gradient-mask-${dataKey}`}>
            <rect
              width="100%"
              height="100%"
              fill={`url(#${chartId}-gradient-mask-gradient)`}
            />
          </mask>

          {/* Pattern: gradient fill with vertical fade mask */}
          <pattern
            id={`${chartId}-gradient-${dataKey}`}
            patternUnits="userSpaceOnUse"
            width="100%"
            height="100%"
          >
            <rect
              width="100%"
              height="100%"
              fill={`url(#${chartId}-colors-${dataKey})`}
              mask={`url(#${chartId}-gradient-mask-${dataKey})`}
            />
          </pattern>
        </g>
      ))}
    </>
  )
}

// Stripped pattern style for bars (low opacity body with full opacity top) - uses mask to preserve gradient colors
const StrippedPatternStyle = ({
  chartConfig,
  chartId,
}: {
  chartConfig: ChartConfig
  chartId: string
}) => {
  return (
    <>
      {/* Shared stripped fade gradient for mask */}
      <linearGradient
        id={`${chartId}-stripped-mask-gradient`}
        x1="0"
        y1="0"
        x2="0"
        y2="1"
      >
        <stop offset="0%" stopColor="white" stopOpacity={0.2} />
        <stop offset="100%" stopColor="white" stopOpacity={0.2} />
      </linearGradient>

      {Object.keys(chartConfig).map((dataKey) => (
        <g key={`${chartId}-stripped-group-${dataKey}`}>
          {/* Mask for stripped fade */}
          <mask id={`${chartId}-stripped-mask-${dataKey}`}>
            <rect
              width="100%"
              height="100%"
              fill={`url(#${chartId}-stripped-mask-gradient)`}
            />
          </mask>

          {/* Pattern: gradient fill with stripped fade mask */}
          <pattern
            id={`${chartId}-stripped-${dataKey}`}
            patternUnits="userSpaceOnUse"
            width="100%"
            height="100%"
          >
            <rect
              width="100%"
              height="100%"
              fill={`url(#${chartId}-colors-${dataKey})`}
              mask={`url(#${chartId}-stripped-mask-${dataKey})`}
            />
          </pattern>
        </g>
      ))}
    </>
  )
}

// Glow filter style for glowing bars
const GlowFilterStyle = ({
  chartId,
  glowingBars,
}: {
  chartId: string
  glowingBars: string[]
}) => {
  return (
    <>
      {glowingBars.map((dataKey) => (
        <filter
          key={`${chartId}-bar-glow-${dataKey}`}
          id={`${chartId}-bar-glow-${dataKey}`}
          x="-100%"
          y="-100%"
          width="300%"
          height="300%"
        >
          <feGaussianBlur in="SourceGraphic" stdDeviation="8" result="blur" />
          <feColorMatrix
            in="blur"
            type="matrix"
            values="1 0 0 0 0
                    0 1 0 0 0
                    0 0 1 0 0
                    0 0 0 0.5 0"
            result="glow"
          />
          <feMerge>
            <feMergeNode in="glow" />
            <feMergeNode in="SourceGraphic" />
          </feMerge>
        </filter>
      ))}
    </>
  )
}

// Generate gradient stops with smooth easing for loading animation
const generateEasedGradientStops = (
  steps: number = 17,
  minOpacity: number = 0.05,
  maxOpacity: number = 0.9
) => {
  return Array.from({ length: steps }, (_, i) => {
    const t = i / (steps - 1)
    const eased = Math.sin(t * Math.PI) ** 2
    const opacity = minOpacity + eased * (maxOpacity - minOpacity)
    return {
      offset: `${(t * 100).toFixed(0)}%`,
      opacity: Number(opacity.toFixed(3)),
    }
  })
}

/**
 * Hook to manage loading data with pixel-perfect shimmer synchronization.
 */
export function useLoadingData(isLoading: boolean, loadingBars: number = 12) {
  const [loadingDataKey, setLoadingDataKey] = useState(false)

  const onShimmerExit = useCallback(() => {
    if (isLoading) {
      setLoadingDataKey((prev) => !prev)
    }
  }, [isLoading])

  const loadingData = useMemo(
    () => getLoadingData(loadingBars, 20, 80),
    // eslint-disable-next-line react-hooks/exhaustive-deps
    [loadingBars, loadingDataKey]
  )

  return { loadingData, onShimmerExit }
}

/**
 * Loading bar pattern with animated skeleton effect
 */
const LoadingBarPatternStyle = ({
  chartId,
  onShimmerExit,
}: {
  chartId: string
  onShimmerExit: () => void
}) => {
  const gradientStops = generateEasedGradientStops()
  const patternWidth = 3
  const startX = -1
  const endX = 2
  const lastXRef = useRef(startX)

  return (
    <>
      <linearGradient
        id={`${chartId}-loading-mask-gradient`}
        x1="0"
        y1="0"
        x2="1"
        y2="0"
      >
        {gradientStops.map(({ offset, opacity }) => (
          <stop
            key={offset}
            offset={offset}
            stopColor="white"
            stopOpacity={opacity}
          />
        ))}
      </linearGradient>
      <pattern
        id={`${chartId}-loading-mask-pattern`}
        patternUnits="objectBoundingBox"
        patternContentUnits="objectBoundingBox"
        patternTransform="rotate(25)"
        width={patternWidth}
        height="1"
        x="0"
        y="0"
      >
        <motion.rect
          y="0"
          width="1"
          height="1"
          fill={`url(#${chartId}-loading-mask-gradient)`}
          initial={{ x: startX }}
          animate={{ x: endX }}
          transition={{
            duration: LOADING_ANIMATION_DURATION / 1000,
            ease: "linear",
            repeat: Infinity,
            repeatType: "loop",
          }}
          onUpdate={(latest) => {
            const xValue = typeof latest.x === "number" ? latest.x : startX
            const lastX = lastXRef.current
            if (xValue >= 1 && lastX < 1) {
              onShimmerExit()
            }
            lastXRef.current = xValue
          }}
        />
      </pattern>
      <mask id={`${chartId}-loading-mask`} maskUnits="userSpaceOnUse">
        <rect
          width="100%"
          height="100%"
          fill={`url(#${chartId}-loading-mask-pattern)`}
        />
      </mask>
    </>
  )
}

/**
 * Calculate bar opacity based on click selection and hover highlight state
 */
const getBarOpacity = ({
  isClickable,
  selectedDataKey,
  dataKey,
  enableHoverHighlight,
  isMouseInChart,
  isActive,
}: {
  isClickable?: boolean
  selectedDataKey?: string | null
  dataKey: string
  enableHoverHighlight?: boolean
  isMouseInChart?: boolean
  isActive?: boolean
}) => {
  // Check if this dataKey is selected (for click selection)
  const isSelectedDataKey =
    selectedDataKey === null || selectedDataKey === dataKey

  // Base opacity from click selection
  const clickOpacity =
    isClickable && selectedDataKey !== null ? (isSelectedDataKey ? 1 : 0.3) : 1

  // If hover highlight is enabled and mouse is in chart
  if (enableHoverHighlight && isMouseInChart) {
    // Combine: if this bar is active/hovered, show full opacity (respecting click selection)
    // If not hovered, dim it further
    return isActive ? clickOpacity : clickOpacity * 0.3
  }

  return clickOpacity
}
src/components/evilcharts/ui/chart.tsx
"use client";

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

// Format: { THEME_NAME: CSS_SELECTOR }
const THEMES = { light: "", dark: ".dark" } as const;

type ThemeKey = keyof typeof THEMES;

// All Keys are optional at first
type ThemeColorsBase = {
  [K in ThemeKey]?: string[];
};

// Require at least one theme key
type AtLeastOneThemeColor = {
  [K in ThemeKey]: Required<Pick<ThemeColorsBase, K>> & Partial<Omit<ThemeColorsBase, K>>;
}[ThemeKey];

const VALID_THEME_KEYS = Object.keys(THEMES) as ThemeKey[];

// Validation for chart config colors at runtime
function validateChartConfigColors(config: ChartConfig): void {
  for (const [key, value] of Object.entries(config)) {
    if (value.colors) {
      const hasValidThemeKey = VALID_THEME_KEYS.some(
        (themeKey) => value.colors?.[themeKey] !== undefined,
      );

      if (!hasValidThemeKey) {
        throw new Error(
          `[EvilCharts] Invalid chart config for "${key}": colors object must have at least one theme key (${VALID_THEME_KEYS.join(", ")}). Received empty object or invalid keys.`,
        );
      }
    }
  }
}

export type ChartConfig = Record<
  string,
  {
    label?: React.ReactNode;
    icon?: React.ComponentType;
    colors?: AtLeastOneThemeColor;
  }
>;

interface ChartContextProps {
  config: ChartConfig;
}

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

export function useChart() {
  const context = React.useContext(ChartContext);

  if (!context) {
    throw new Error("useChart must be used within a <ChartContainer />");
  }

  return context;
}

interface ChartContainerProps
  extends
    Omit<React.ComponentProps<"div">, "children">,
    Pick<
      React.ComponentProps<typeof RechartsPrimitive.ResponsiveContainer>,
      | "initialDimension"
      | "aspect"
      | "debounce"
      | "minHeight"
      | "minWidth"
      | "maxHeight"
      | "height"
      | "width"
      | "onResize"
      | "children"
    > {
  config: ChartConfig;
  innerResponsiveContainerStyle?: React.ComponentProps<
    typeof RechartsPrimitive.ResponsiveContainer
  >["style"];
  /** Optional content rendered below the chart (e.g. EvilBrush) */
  footer?: React.ReactNode;
}

function ChartContainer({
  id,
  config,
  initialDimension = { width: 320, height: 200 },
  className,
  children,
  footer,
  ...props
}: Readonly<ChartContainerProps>) {
  const uniqueId = React.useId();
  const chartId = `chart-${id ?? uniqueId.replace(/:/g, "")}`;

  // Validate chart config at runtime
  validateChartConfigColors(config);

  return (
    <ChartContext.Provider value={{ config }}>
      <div
        data-slot="chart"
        data-chart={chartId}
        className={cn(
          "min-h-0 w-full flex-1",
          "[&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border relative flex flex-col justify-center text-xs [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-hidden [&_.recharts-sector]:outline-hidden [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-surface]:outline-hidden",
          !footer && "aspect-video",
          className,
        )}
        {...props}
      >
        <ChartStyle id={chartId} config={config} />
        <RechartsPrimitive.ResponsiveContainer
          className="min-h-0 w-full flex-1"
          initialDimension={initialDimension}
        >
          {children}
        </RechartsPrimitive.ResponsiveContainer>
        {footer}
      </div>
    </ChartContext.Provider>
  );
}

function LoadingIndicator({ isLoading }: { isLoading: boolean }) {
  if (!isLoading) {
    return null;
  }

  return (
    <div className="pointer-events-none absolute inset-0 z-20 flex items-center justify-center">
      <div className="text-primary bg-background flex items-center justify-center gap-2 rounded-md border px-2 py-0.5 text-sm">
        <div className="border-border border-t-primary h-3 w-3 animate-spin rounded-full border" />
        <span>Loading</span>
      </div>
    </div>
  );
}

// Distribute colors evenly across slots, extra slots go to last color(s)
// Example: 2 colors for 4 slots → [red, red, pink, pink]
// Example: 3 colors for 4 slots → [red, pink, blue, blue]
function distributeColors(colorsArray: string[], maxCount: number): string[] {
  const availableCount = colorsArray.length;
  if (availableCount >= maxCount) {
    return colorsArray.slice(0, maxCount);
  }

  const result: string[] = [];
  const baseSlots = Math.floor(maxCount / availableCount);
  const extraSlots = maxCount % availableCount;

  // First (availableCount - extraSlots) colors get baseSlots each
  // Last extraSlots colors get (baseSlots + 1) each
  for (let colorIdx = 0; colorIdx < availableCount; colorIdx++) {
    const isExtraColor = colorIdx >= availableCount - extraSlots;
    const slotsForThisColor = baseSlots + (isExtraColor ? 1 : 0);
    for (let j = 0; j < slotsForThisColor; j++) {
      result.push(colorsArray[colorIdx]);
    }
  }

  return result;
}

const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
  const colorConfig = Object.entries(config).filter(([, config]) => config.colors);

  if (!colorConfig.length) {
    return null;
  }

  const generateCssVars = (theme: keyof typeof THEMES) =>
    colorConfig
      .flatMap(([key, itemConfig]) => {
        const colorsArray = itemConfig.colors?.[theme];
        if (!colorsArray || !Array.isArray(colorsArray) || colorsArray.length === 0) {
          return [];
        }

        // Get max count across all themes for this key
        const maxCount = getColorsCount(itemConfig);

        // Distribute colors evenly across all required slots
        const distributedColors = distributeColors(colorsArray, maxCount);

        return distributedColors.map((color, index) => `  --color-${key}-${index}: ${color};`);
      })
      .filter(Boolean)
      .join("\n");

  const css = Object.entries(THEMES)
    .map(
      ([theme, prefix]) =>
        `${prefix} [data-chart=${id}] {\n${generateCssVars(theme as keyof typeof THEMES)}\n}`,
    )
    .join("\n");

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

// Helper to extract item config from a payload.
export function getPayloadConfigFromPayload(config: ChartConfig, payload: unknown, key: string) {
  if (typeof payload !== "object" || payload === null) {
    return undefined;
  }

  const payloadPayload =
    "payload" in payload && typeof payload.payload === "object" && payload.payload !== null
      ? payload.payload
      : undefined;

  let configLabelKey: string = key;

  if (key in payload && typeof payload[key as keyof typeof payload] === "string") {
    configLabelKey = payload[key as keyof typeof payload] as string;
  } else if (
    payloadPayload &&
    key in payloadPayload &&
    typeof payloadPayload[key as keyof typeof payloadPayload] === "string"
  ) {
    configLabelKey = payloadPayload[key as keyof typeof payloadPayload] as string;
  }

  return configLabelKey in config ? config[configLabelKey] : config[key];
}

// Format values to percent for expanded charts
function axisValueToPercentFormatter(value: number) {
  return `${Math.round(value * 100).toFixed(0)}%`;
}

// Get max colors count across all themes for a config entry
function getColorsCount(config: ChartConfig[string]): number {
  if (!config.colors) return 1;
  const counts = VALID_THEME_KEYS.map((theme) => config.colors?.[theme]?.length ?? 0);
  return Math.max(...counts, 1);
}

// Generate random loading data for skeleton/loading state
// min/max represent percentage of the range (0-100), defaults to 20-80 for realistic look
export const getLoadingData = (points: number = 10, min: number = 0, max: number = 70) => {
  const range = max - min;
  return Array.from({ length: points }, () => ({
    loading: Math.floor(Math.random() * range) + min,
  }));
};

export {
  ChartContainer,
  ChartStyle,
  axisValueToPercentFormatter,
  LoadingIndicator,
  getColorsCount,
};
src/components/evilcharts/ui/tooltip.tsx
import { getPayloadConfigFromPayload, getColorsCount, useChart } from "@/components/evilcharts/ui/chart";
import type { NameType, ValueType } from "recharts/types/component/DefaultTooltipContent";
import * as RechartsPrimitive from "recharts";
import { cn } from "@/lib/utils";
import * as React from "react";

type TooltipRoundness = "sm" | "md" | "lg" | "xl";
type TooltipVariant = "default" | "frosted-glass";

const roundnessMap: Record<TooltipRoundness, string> = {
  sm: "rounded-sm",
  md: "rounded-md",
  lg: "rounded-lg",
  xl: "rounded-xl",
};

const variantMap: Record<TooltipVariant, string> = {
  default: "bg-background",
  "frosted-glass": "bg-background/70 backdrop-blur-sm",
};

function ChartTooltipContent({
  active,
  payload,
  className,
  indicator = "dot",
  hideLabel = false,
  hideIndicator = false,
  label,
  labelFormatter,
  labelClassName,
  formatter,
  nameKey,
  labelKey,
  selected,
  roundness = "lg",
  variant = "default",
}: React.ComponentProps<typeof RechartsPrimitive.Tooltip> &
  React.ComponentProps<"div"> & {
    hideLabel?: boolean;
    hideIndicator?: boolean;
    indicator?: "line" | "dot" | "dashed";
    nameKey?: string;
    labelKey?: string;
    selected?: string | null;
    roundness?: TooltipRoundness;
    variant?: TooltipVariant;
  } & Omit<
    RechartsPrimitive.DefaultTooltipContentProps<ValueType, NameType>,
    "accessibilityLayer"
  >) {
  const { config } = useChart();

  const tooltipLabel = React.useMemo(() => {
    if (hideLabel || !payload?.length) {
      return null;
    }

    const [item] = payload;
    const key = `${labelKey ?? item?.dataKey ?? item?.name ?? "value"}`;
    const itemConfig = getPayloadConfigFromPayload(config, item, key);
    const value =
      !labelKey && typeof label === "string" ? (config[label]?.label ?? label) : itemConfig?.label;

    if (labelFormatter) {
      return (
        <div className={cn("font-medium", labelClassName)}>{labelFormatter(value, payload)}</div>
      );
    }

    if (!value) {
      return null;
    }

    return <div className={cn("font-medium", labelClassName)}>{value}</div>;
  }, [label, labelFormatter, payload, hideLabel, labelClassName, config, labelKey]);

  if (!active || !payload?.length) {
    // Empty tooltip - to prevent position getting 0.0 so it doesnt animate tooltip every time from 0.0 origin
    return <span className="p-4" />;
  }

  const nestLabel = payload.length === 1 && indicator !== "dot";

  return (
    <div
      className={cn(
        "border-border/50 grid min-w-32 items-start gap-1.5 border px-2.5 py-1.5 text-xs shadow-xl",
        roundnessMap[roundness],
        variantMap[variant],
        className,
      )}
    >
      {!nestLabel ? tooltipLabel : null}
      <div className="grid gap-1.5">
        {payload
          .filter((item) => item.type !== "none")
          .map((item, index) => {
            // For pie charts, item.name contains the sector name (e.g., "chrome")
            // For radial charts, the name is in item.payload[nameKey]
            // For other charts, item.name or item.dataKey contains the series name
            const payloadName =
              nameKey && item.payload
                ? (item.payload as Record<string, unknown>)[nameKey]
                : undefined;
            const key = `${payloadName ?? item.name ?? item.dataKey ?? "value"}`;
            const itemConfig = getPayloadConfigFromPayload(config, item, key);

            // Get colors count for this item to determine gradient vs solid
            const colorsCount = itemConfig ? getColorsCount(itemConfig) : 1;

            return (
              <div
                key={index}
                className={cn(
                  "[&>svg]:text-muted-foreground flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5",
                  indicator === "dot" && "items-center",
                  selected != null && selected !== item.dataKey && "opacity-30",
                )}
              >
                {formatter && item?.value !== undefined && item.name ? (
                  formatter(item.value, item.name, item, index, item.payload)
                ) : (
                  <>
                    {itemConfig?.icon ? (
                      <itemConfig.icon />
                    ) : (
                      !hideIndicator && (
                        <div
                          className={cn("shrink-0 rounded-[2px]", {
                            "h-2.5 w-2.5": indicator === "dot",
                            "w-1": indicator === "line",
                            "w-0 border-[1.5px] border-dashed bg-transparent!":
                              indicator === "dashed",
                            "my-0.5": nestLabel && indicator === "dashed",
                          })}
                          style={getIndicatorColorStyle(key, colorsCount)}
                        />
                      )
                    )}
                    <div
                      className={cn(
                        "flex flex-1 justify-between gap-4 leading-none",
                        nestLabel ? "items-end" : "items-center",
                      )}
                    >
                      <div className="grid gap-1.5">
                        {nestLabel ? tooltipLabel : null}
                        <span className="text-muted-foreground">
                          {itemConfig?.label ?? item.name}
                        </span>
                      </div>
                      {item.value != null && (
                        <span className="text-foreground font-mono font-medium tabular-nums">
                          {typeof item.value === "number"
                            ? item.value.toLocaleString()
                            : String(item.value)}
                        </span>
                      )}
                    </div>
                  </>
                )}
              </div>
            );
          })}
      </div>
    </div>
  );
}

function getIndicatorColorStyle(dataKey: string, colorsCount: number): React.CSSProperties {
  if (colorsCount <= 1) {
    return { background: `var(--color-${dataKey}-0)` };
  }

  // Multiple colors: create linear gradient with evenly distributed stops
  const stops = Array.from({ length: colorsCount }, (_, index) => {
    const offset = (index / (colorsCount - 1)) * 100;
    return `var(--color-${dataKey}-${index}) ${offset}%`;
  }).join(", ");

  return { background: `linear-gradient(to right, ${stops})` };
}

const ChartTooltip = ({
  animationDuration = 200,
  ...props
}: React.ComponentProps<typeof RechartsPrimitive.Tooltip>) => (
  <RechartsPrimitive.Tooltip animationDuration={animationDuration} {...props} />
);

export { ChartTooltip, ChartTooltipContent };
export type { TooltipRoundness, TooltipVariant };
src/components/evilcharts/ui/legend.tsx
import { getPayloadConfigFromPayload, getColorsCount, useChart } from "@/components/evilcharts/ui/chart";
import * as RechartsPrimitive from "recharts";
import { cn } from "@/lib/utils";
import * as React from "react";

type ChartLegendVariant =
  | "square"
  | "circle"
  | "circle-outline"
  | "rounded-square"
  | "rounded-square-outline"
  | "vertical-bar"
  | "horizontal-bar";

function ChartLegendContent({
  className,
  hideIcon = false,
  nameKey,
  payload,
  verticalAlign,
  align = "right",
  selected,
  onSelectChange,
  isClickable,
  variant = "rounded-square",
}: React.ComponentProps<"div"> & {
  hideIcon?: boolean;
  nameKey?: string;
  selected?: string | null;
  isClickable?: boolean;
  onSelectChange?: (selected: string | null) => void;
  variant?: ChartLegendVariant;
} & RechartsPrimitive.DefaultLegendContentProps) {
  const { config } = useChart();

  if (!payload?.length) {
    return null;
  }

  return (
    <div
      className={cn(
        "flex items-center gap-4 select-none",
        align === "left" && "justify-start",
        align === "center" && "justify-center",
        align === "right" && "justify-end",
        verticalAlign === "top" ? "pb-4" : "pt-4",
        className,
      )}
    >
      {payload
        .filter((item) => item.type !== "none")
        .map((item) => {
          // For pie charts, item.value contains the sector name (e.g., "chrome")
          // For radial charts, the name is in item.payload[nameKey]
          // For other charts, item.dataKey contains the series name (e.g., "desktop")
          const payloadName =
            nameKey && item.payload
              ? (item.payload as Record<string, unknown>)[nameKey]
              : undefined;
          const key = `${payloadName ?? item.value ?? item.dataKey ?? "value"}`;
          const itemConfig = getPayloadConfigFromPayload(config, item, key);
          const isSelected = selected === null || selected === key;

          // Get colors count for this item to determine gradient vs solid
          const colorsCount = itemConfig ? getColorsCount(itemConfig) : 1;

          return (
            <div
              key={key}
              className={cn(
                "[&>svg]:text-muted-foreground flex items-center gap-1.5 transition-opacity [&>svg]:h-3 [&>svg]:w-3",
                !isSelected && "opacity-30",
                isClickable && "cursor-pointer",
              )}
              onClick={() => {
                if (!isClickable) return;

                onSelectChange?.(selected === key ? null : key);
              }}
            >
              {itemConfig?.icon && !hideIcon ? (
                <itemConfig.icon />
              ) : (
                <LegendIndicator
                  variant={variant}
                  dataKey={key}
                  colorsCount={colorsCount}
                />
              )}
              {itemConfig?.label}
            </div>
          );
        })}
    </div>
  );
}

// ---------------------------------------------------------------------------
// Legend indicator — each variant gets its own branch so future variants
// can diverge freely in markup & style.
// ---------------------------------------------------------------------------

function LegendIndicator({
  variant,
  dataKey,
  colorsCount,
}: {
  variant: ChartLegendVariant;
  dataKey: string;
  colorsCount: number;
}) {
  const fillStyle = getLegendFillStyle(dataKey, colorsCount);
  const outlineStyle = getLegendOutlineStyle(dataKey, colorsCount);

  switch (variant) {
    case "square":
      return <div className="h-2 w-2 shrink-0" style={fillStyle} />;

    case "circle":
      return <div className="h-2 w-2 shrink-0 rounded-full" style={fillStyle} />;

    case "circle-outline":
      return (
        <div
          className="h-2.5 w-2.5 shrink-0 rounded-full p-[1.5px]"
          style={outlineStyle}
        />
      );

    case "vertical-bar":
      return <div className="h-3 w-1 shrink-0 rounded-[2px]" style={fillStyle} />;

    case "horizontal-bar":
      return <div className="h-1 w-3 shrink-0 rounded-[2px]" style={fillStyle} />;

    case "rounded-square-outline":
      return (
        <div
          className="h-2.5 w-2.5 shrink-0 rounded-[3px] p-[1.5px]"
          style={outlineStyle}
        />
      );

    case "rounded-square":
    default:
      return <div className="h-2 w-2 shrink-0 rounded-[2px]" style={fillStyle} />;
  }
}

// ---------------------------------------------------------------------------
// Style helpers
// ---------------------------------------------------------------------------

/** Solid fill / gradient background for filled variants. */
function getLegendFillStyle(dataKey: string, colorsCount: number): React.CSSProperties {
  if (colorsCount <= 1) {
    return { backgroundColor: `var(--color-${dataKey}-0)` };
  }

  const stops = Array.from({ length: colorsCount }, (_, i) => {
    const offset = (i / (colorsCount - 1)) * 100;
    return `var(--color-${dataKey}-${i}) ${offset}%`;
  }).join(", ");

  return { background: `linear-gradient(to right, ${stops})` };
}

/**
 * Outline style for stroke variants.
 * Uses background + mask-composite to punch out the center, leaving only the
 * "border" visible. Works with both solid colors and gradients, and respects
 * border-radius — unlike plain `border-color`.
 */
function getLegendOutlineStyle(dataKey: string, colorsCount: number): React.CSSProperties {
  const maskStyle: React.CSSProperties = {
    WebkitMask:
      "linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0)",
    WebkitMaskComposite: "xor",
    mask: "linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0)",
    maskComposite: "exclude",
  };

  if (colorsCount <= 1) {
    return {
      backgroundColor: `var(--color-${dataKey}-0)`,
      ...maskStyle,
    };
  }

  const stops = Array.from({ length: colorsCount }, (_, i) => {
    const offset = (i / (colorsCount - 1)) * 100;
    return `var(--color-${dataKey}-${i}) ${offset}%`;
  }).join(", ");

  return {
    background: `linear-gradient(to right, ${stops})`,
    ...maskStyle,
  };
}

const ChartLegend = RechartsPrimitive.Legend;

export { ChartLegend, ChartLegendContent, type ChartLegendVariant };
src/components/evilcharts/ui/background.tsx
"use client";

import { ZIndexLayer } from "recharts";
import { useId } from "react";

// ── Background Variant Types ─────────────────────────────────────────────────
// To add a new variant:
// 1. Add its name to the BackgroundVariant union type below
// 2. Create a pattern component with PatternProps
// 3. Register it in PATTERN_MAP

export type BackgroundVariant =
  | "dots"
  | "grid"
  | "cross-hatch"
  | "diagonal-lines"
  | "plus"
  | "falling-triangles"
  | "4-pointed-star"
  | "tiny-checkers"
  | "overlapping-circles"
  | "wiggle-lines"
  | "bubbles";

// ── Pattern Components ───────────────────────────────────────────────────────

type PatternProps = { id: string };

const DotsPattern = ({ id }: PatternProps) => (
  <pattern id={id} x="0" y="0" width="20" height="20" patternUnits="userSpaceOnUse">
    <circle className="text-border dark:text-border" cx="2" cy="2" r="1" fill="currentColor" />
  </pattern>
);

const GridPattern = ({ id }: PatternProps) => (
  <pattern id={id} x="0" y="0" width="20" height="20" patternUnits="userSpaceOnUse">
    <path
      className="text-border dark:text-border"
      d="M 20 0 L 0 0 0 20"
      fill="none"
      stroke="currentColor"
      strokeWidth="0.5"
    />
  </pattern>
);

const CrossHatchPattern = ({ id }: PatternProps) => (
  <pattern id={id} x="0" y="0" width="20" height="20" patternUnits="userSpaceOnUse">
    <path
      className="text-border/60 dark:text-border/50"
      d="M 0 0 L 20 20 M 20 0 L 0 20"
      fill="none"
      stroke="currentColor"
      strokeWidth="0.5"
    />
  </pattern>
);

const DiagonalLinesPattern = ({ id }: PatternProps) => (
  <pattern
    id={id}
    x="0"
    y="0"
    width="6"
    height="6"
    patternUnits="userSpaceOnUse"
    patternTransform="rotate(45)"
  >
    <line
      className="text-border dark:text-border"
      x1="0"
      y1="0"
      x2="0"
      y2="6"
      stroke="currentColor"
      strokeWidth="0.5"
    />
  </pattern>
);

const PlusPattern = ({ id }: PatternProps) => (
  <pattern id={id} x="0" y="0" width="16" height="16" patternUnits="userSpaceOnUse">
    <path
      className="text-border dark:text-border"
      d="M 8 4 L 8 12 M 4 8 L 12 8"
      fill="none"
      stroke="currentColor"
      strokeWidth="0.5"
      strokeLinecap="round"
    />
  </pattern>
);

const FallingTrianglesPattern = ({ id }: PatternProps) => (
  <pattern id={id} x="0" y="0" width="18" height="36" patternUnits="userSpaceOnUse">
    <path
      className="text-border dark:text-border"
      d="M2 6h12L8 18 2 6zm18 36h12l-6 12-6-12z"
      transform="scale(0.5)"
      fill="currentColor"
      fillOpacity="0.4"
    />
  </pattern>
);

const FourPointedStarPattern = ({ id }: PatternProps) => (
  <pattern id={id} x="0" y="0" width="16" height="16" patternUnits="userSpaceOnUse">
    <polygon
      className="text-border dark:text-border"
      fillRule="evenodd"
      points="5 3 8 4 5 5 4 8 3 5 0 4 3 3 4 0 5 3"
      fill="currentColor"
      fillOpacity="0.4"
    />
  </pattern>
);

const TinyCheckersPattern = ({ id }: PatternProps) => (
  <pattern id={id} x="0" y="0" width="8" height="8" patternUnits="userSpaceOnUse">
    <path
      className="text-border dark:text-border"
      fillRule="evenodd"
      d="M0 0h4v4H0V0zm4 4h4v4H4V4z"
      fill="currentColor"
      fillOpacity="0.2"
    />
  </pattern>
);

const OverlappingCirclesPattern = ({ id }: PatternProps) => (
  <pattern id={id} x="0" y="0" width="40" height="40" patternUnits="userSpaceOnUse">
    <path
      className="text-border dark:text-border"
      fillRule="evenodd"
      d="M25 25c0-2.762 2.238-5 5-5s5 2.238 5 5-2.238 5-5 5c0 2.762-2.238 5-5 5s-5-2.238-5-5 2.238-5 5-5zM5 5c0-2.762 2.238-5 5-5s5 2.238 5 5-2.238 5-5 5c0 2.762-2.238 5-5 5S0 12.762 0 10s2.238-5 5-5zm5 4c2.209 0 4-1.791 4-4s-1.791-4-4-4-4 1.791-4 4 1.791 4 4 4zm20 20c2.209 0 4-1.791 4-4s-1.791-4-4-4-4 1.791-4 4 1.791 4 4 4z"
      fill="currentColor"
      fillOpacity="0.4"
    />
  </pattern>
);

const WiggleLinesPattern = ({ id }: PatternProps) => (
  <pattern
    id={id}
    x="0"
    y="0"
    width="52"
    height="26"
    patternUnits="userSpaceOnUse"
    patternTransform="scale(0.6)"
  >
    <path
      className="text-border dark:text-border"
      d="M10 10c0-2.21-1.79-4-4-4-3.314 0-6-2.686-6-6h2c0 2.21 1.79 4 4 4 3.314 0 6 2.686 6 6 0 2.21 1.79 4 4 4 3.314 0 6 2.686 6 6 0 2.21 1.79 4 4 4v2c-3.314 0-6-2.686-6-6 0-2.21-1.79-4-4-4-3.314 0-6-2.686-6-6zm25.464-1.95l8.486 8.486-1.414 1.414-8.486-8.486 1.414-1.414z"
      fill="currentColor"
      fillOpacity="0.4"
    />
  </pattern>
);

const BubblesPattern = ({ id }: PatternProps) => (
  <pattern
    id={id}
    x="0"
    y="0"
    width="100"
    height="100"
    patternUnits="userSpaceOnUse"
    patternTransform="scale(0.6667)"
  >
    <path
      className="text-border dark:text-border"
      d="M11 18c3.866 0 7-3.134 7-7s-3.134-7-7-7-7 3.134-7 7 3.134 7 7 7zm48 25c3.866 0 7-3.134 7-7s-3.134-7-7-7-7 3.134-7 7 3.134 7 7 7zm-43-7c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zm63 31c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zM34 90c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zm56-76c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zM12 86c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm28-65c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm23-11c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zm-6 60c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm29 22c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zM32 63c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zm57-13c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zm-9-21c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2zM60 91c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2zM35 41c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2zM12 60c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2z"
      fill="currentColor"
      fillOpacity="0.4"
      fillRule="evenodd"
    />
  </pattern>
);

// ── Pattern Registry ─────────────────────────────────────────────────────────
// Map variant names to pattern components

const PATTERN_MAP: Record<BackgroundVariant, React.FC<PatternProps>> = {
  dots: DotsPattern,
  grid: GridPattern,
  plus: PlusPattern,
  bubbles: BubblesPattern,
  "cross-hatch": CrossHatchPattern,
  "diagonal-lines": DiagonalLinesPattern,
  "falling-triangles": FallingTrianglesPattern,
  "4-pointed-star": FourPointedStarPattern,
  "tiny-checkers": TinyCheckersPattern,
  "overlapping-circles": OverlappingCirclesPattern,
  "wiggle-lines": WiggleLinesPattern,
};

// ── Main Component ───────────────────────────────────────────────────────────
// Usage: Place <ChartBackground variant="dots" /> inside any Recharts chart component.
// ZIndexLayer with zIndex={-1} ensures the background renders behind all chart content.

interface ChartBackgroundProps {
  variant: BackgroundVariant;
}

export function ChartBackground({ variant }: ChartBackgroundProps) {
  const baseId = useId().replace(/:/g, "");
  const patternId = `${baseId}-bg-${variant}`;
  const maskId = `${baseId}-bg-edge-fade`;
  const filterId = `${baseId}-bg-blur`;
  const PatternComponent = PATTERN_MAP[variant];

  return (
    <ZIndexLayer zIndex={-1}>
      <defs>
        <PatternComponent id={patternId} />
        {/* Gaussian blur filter for soft edge fade */}
        <filter id={filterId}>
          <feGaussianBlur stdDeviation="25" />
        </filter>
        {/* Mask: a slightly inset white rect with blur creates smooth transparent edges */}
        <mask id={maskId} maskUnits="userSpaceOnUse">
          <rect x="8%" y="20%" width="85%" height="60%" fill="white" filter={`url(#${filterId})`} />
        </mask>
      </defs>
      <rect width="100%" height="100%" fill={`url(#${patternId})`} mask={`url(#${maskId})`} />
    </ZIndexLayer>
  );
}
src/components/evilcharts/ui/dot.tsx
import { cn } from "@/lib/utils";
import * as React from "react";

export type DotVariant = "default" | "border" | "colored-border";

type ChartDotProps = {
  cx?: number;
  cy?: number;
  dataKey: string;
  chartId: string;
  className?: string;
  fillOpacity?: number;
  type?: DotVariant;
};

const ChartDot = React.memo(function ChartDot({
  cx,
  cy,
  dataKey,
  chartId,
  className,
  fillOpacity = 1,
  type = "default",
}: ChartDotProps) {
  const dotId = React.useId().replace(/:/g, "");
  const gradientUrl = `url(#${chartId}-colors-${String(dataKey)})`;

  if (cx === undefined || cy === undefined) return null;

  switch (type) {
    case "border":
      return (
        <PrimaryBorderDot
          cx={cx}
          cy={cy}
          dotId={dotId}
          fillOpacity={fillOpacity}
          gradientUrl={gradientUrl}
          className={className}
        />
      );
    case "colored-border":
      return (
        <ColoredBorderDot
          cx={cx}
          cy={cy}
          dotId={dotId}
          fillOpacity={fillOpacity}
          gradientUrl={gradientUrl}
          className={className}
        />
      );
    default:
      return (
        <DefaultDot
          cx={cx}
          cy={cy}
          dotId={dotId}
          fillOpacity={fillOpacity}
          gradientUrl={gradientUrl}
          className={className}
        />
      );
  }
});

type DotVariantProps = {
  cx: number;
  cy: number;
  dotId: string;
  fillOpacity: number;
  gradientUrl: string;
  className?: string;
};

const DefaultDot = React.memo(
  ({ cx, cy, dotId, fillOpacity, gradientUrl, className }: DotVariantProps) => {
    const r = 3;
    return (
      <g className={className}>
        <defs>
          <clipPath id={`dot-clip-${dotId}`}>
            <circle cx={cx} cy={cy} r={r} />
          </clipPath>
        </defs>
        {/* Full-width gradient rectangle clipped to dot shape */}
        <rect
          x="0"
          y={cy - r}
          width="100%"
          height={r * 2}
          fill={gradientUrl}
          fillOpacity={fillOpacity}
          clipPath={`url(#dot-clip-${dotId})`}
        />
      </g>
    );
  },
);

DefaultDot.displayName = "DefaultDot";

const PrimaryBorderDot = React.memo(
  ({ cx, cy, dotId, fillOpacity, gradientUrl, className }: DotVariantProps) => {
    const r = 6;
    const strokeWidth = 5;
    return (
      <g className={cn(className, "text-background")}>
        <defs>
          <clipPath id={`dot-clip-${dotId}`}>
            <circle cx={cx} cy={cy} r={r} />
          </clipPath>
        </defs>
        {/* Background stroke (border) */}
        <circle cx={cx} cy={cy} r={r} fill="currentColor" />
        {/* Inner gradient circle clipped */}
        <rect
          x="0"
          y={cy - (r - strokeWidth / 2)}
          width="100%"
          height={(r - strokeWidth / 2) * 2}
          fill={gradientUrl}
          fillOpacity={fillOpacity}
          clipPath={`url(#dot-clip-inner-${dotId})`}
        />
        <defs>
          <clipPath id={`dot-clip-inner-${dotId}`}>
            <circle cx={cx} cy={cy} r={r - strokeWidth / 2} />
          </clipPath>
        </defs>
      </g>
    );
  },
);

PrimaryBorderDot.displayName = "PrimaryBorderDot";

const ColoredBorderDot = React.memo(
  ({ cx, cy, dotId, fillOpacity, gradientUrl, className }: DotVariantProps) => {
    const r = 3;
    const strokeWidth = 1;
    return (
      <g className={cn(className, "text-background")}>
        <defs>
          <clipPath id={`dot-clip-${dotId}`}>
            <circle cx={cx} cy={cy} r={r + strokeWidth / 2} />
          </clipPath>
        </defs>
        {/* Gradient stroke (border) via clipped rect */}
        <rect
          x="0"
          y={cy - r - strokeWidth / 2}
          width="100%"
          height={(r + strokeWidth / 2) * 2}
          fill={gradientUrl}
          fillOpacity={fillOpacity}
          clipPath={`url(#dot-clip-${dotId})`}
        />
        {/* Inner solid fill */}
        <circle cx={cx} cy={cy} r={r - strokeWidth / 2} fill="currentColor" />
      </g>
    );
  },
);

ColoredBorderDot.displayName = "ColoredBorderDot";

export { ChartDot };
src/components/evilcharts/ui/evil-brush.tsx
"use client";

import { motion, useMotionValue, useMotionValueEvent, useSpring, useTransform } from "motion/react";
import { ResponsiveContainer, AreaChart, Area, LineChart, Line, BarChart, Bar } from "recharts";
import { ChartStyle, getColorsCount, type ChartConfig } from "@/components/evilcharts/ui/chart";
import type { MotionValue } from "motion/react";
import { useCallback, useEffect, type ComponentProps } from "react";
import { cn } from "@/lib/utils";
import * as React from "react";

// ─── Types ──────────────────────────────────────────────────────────────────

type EvilBrushVariant = "line" | "area" | "bar";
type CurveType = ComponentProps<typeof Area>["type"];

interface EvilBrushRange {
  startIndex: number;
  endIndex: number;
}

interface EvilBrushProps {
  /** Full dataset – always rendered in the miniature chart */
  data: Record<string, unknown>[];
  /** Chart config with colour definitions */
  chartConfig: ChartConfig;
  /** Data keys to plot (default: all keys from chartConfig) */
  dataKeys?: string[];
  /** X-axis data key – used for handle labels */
  xDataKey?: string;
  /** Visual variant of the mini chart */
  variant?: EvilBrushVariant;
  /** Pixel height of the brush */
  height?: number;
  /** Extra className */
  className?: string;
  /** Whether areas/bars should be stacked in the mini chart */
  stacked?: boolean;
  /** Stroke variant for line / area strokes in the mini chart */
  strokeVariant?: "solid" | "dashed" | "animated-dashed";
  /** Whether to connect null data points in line / area variants */
  connectNulls?: boolean;
  /** Radius for bar corners in the bar variant */
  barRadius?: number;

  // ── Controlled mode ──────────────────────────────────────────────────
  /** Controlled start index */
  startIndex?: number;
  /** Controlled end index */
  endIndex?: number;

  // ── Uncontrolled mode ────────────────────────────────────────────────
  /** Initial start index (uncontrolled) */
  defaultStartIndex?: number;
  /** Initial end index (uncontrolled) */
  defaultEndIndex?: number;

  /** Fired whenever the visible range changes */
  onChange?: (range: EvilBrushRange) => void;
  /** Format the handle label from the xDataKey value */
  formatLabel?: (value: unknown, index: number) => string;
  /** Curve type for line / area variants */
  curveType?: CurveType;
  /** Minimum number of data points that must remain selected */
  minSpan?: number;
  /** Whether to render labels on the handles */
  showLabels?: boolean;
  /** Skip rendering own ChartStyle (when inside a ChartContainer that already provides CSS vars) */
  skipStyle?: boolean;
}

// ─── Spring config ──────────────────────────────────────────────────────────

const SPRING_CONFIG = { stiffness: 300, damping: 35, mass: 0.8 };

// ─── Pointer-capture drag hook ──────────────────────────────────────────────
// Replaces raw addEventListener with the modern Pointer Events API.
// setPointerCapture routes all pointer events to the originating element,
// so we get mouse + touch + pen support with zero global listeners.

type DragType = "left" | "right" | "middle";

interface DragState {
  type: DragType;
  originX: number;
  originRange: EvilBrushRange;
}

function useBrushDrag({
  range,
  totalPoints,
  containerRef,
  commit,
}: {
  range: EvilBrushRange;
  totalPoints: number;
  containerRef: React.RefObject<HTMLDivElement | null>;
  commit: (next: EvilBrushRange, mode?: DragType) => void;
}) {
  const dragRef = React.useRef<DragState | null>(null);
  const [isDragging, setIsDragging] = React.useState(false);

  const toIndexDelta = useCallback(
    (px: number) => {
      if (!containerRef.current || totalPoints <= 1) return 0;
      return Math.round(
        (px / containerRef.current.getBoundingClientRect().width) * (totalPoints - 1),
      );
    },
    [totalPoints, containerRef],
  );

  const onPointerDown = useCallback(
    (e: React.PointerEvent, type: DragType) => {
      e.preventDefault();
      (e.target as HTMLElement).setPointerCapture(e.pointerId);
      dragRef.current = { type, originX: e.clientX, originRange: { ...range } };
      setIsDragging(true);
    },
    [range],
  );

  const onPointerMove = useCallback(
    (e: React.PointerEvent) => {
      const d = dragRef.current;
      if (!d) return;

      const delta = toIndexDelta(e.clientX - d.originX);
      const { type, originRange: o } = d;

      if (type === "left") {
        commit({ startIndex: o.startIndex + delta, endIndex: o.endIndex }, "left");
      } else if (type === "right") {
        commit({ startIndex: o.startIndex, endIndex: o.endIndex + delta }, "right");
      } else {
        const span = o.endIndex - o.startIndex;
        let s = o.startIndex + delta;
        let e2 = s + span;
        if (s < 0) {
          s = 0;
          e2 = span;
        }
        if (e2 > totalPoints - 1) {
          e2 = totalPoints - 1;
          s = Math.max(0, e2 - span);
        }
        commit({ startIndex: s, endIndex: e2 }, "middle");
      }
    },
    [toIndexDelta, totalPoints, commit],
  );

  const onPointerUp = useCallback((e: React.PointerEvent) => {
    (e.target as HTMLElement).releasePointerCapture(e.pointerId);
    dragRef.current = null;
    setIsDragging(false);
  }, []);

  // Helper to bind all three pointer handlers for a given drag type
  const bind = useCallback(
    (type: DragType) => ({
      onPointerDown: (e: React.PointerEvent) => onPointerDown(e, type),
      onPointerMove,
      onPointerUp,
    }),
    [onPointerDown, onPointerMove, onPointerUp],
  );

  return { isDragging, bind };
}

// ─── EvilBrush ────────────────────────────────────────────────────────────

function EvilBrush({
  data,
  chartConfig,
  dataKeys,
  xDataKey,
  variant = "area",
  height = 56,
  className,
  stacked = false,
  strokeVariant = "solid",
  connectNulls = false,
  barRadius,
  startIndex: controlledStart,
  endIndex: controlledEnd,
  defaultStartIndex = 0,
  defaultEndIndex,
  onChange,
  formatLabel,
  curveType = "monotone",
  minSpan = 2,
  showLabels = true,
  skipStyle = false,
}: EvilBrushProps) {
  const containerRef = React.useRef<HTMLDivElement>(null);
  const keys = React.useMemo(() => dataKeys ?? Object.keys(chartConfig), [dataKeys, chartConfig]);
  const totalPoints = data.length;
  const chartId = React.useId().replace(/:/g, "");

  // ── Controlled vs uncontrolled ──────────────────────────────────────────

  const isControlled = controlledStart !== undefined && controlledEnd !== undefined;

  const [internalRange, setInternalRange] = React.useState<EvilBrushRange>(() => ({
    startIndex: Math.max(0, Math.min(defaultStartIndex, totalPoints - 1)),
    endIndex: Math.max(0, Math.min(defaultEndIndex ?? totalPoints - 1, totalPoints - 1)),
  }));

  // Track the last committed range to avoid duplicate updates when small
  // mouse movements don't produce index changes (e.g., at boundaries)
  const lastCommittedRef = React.useRef<EvilBrushRange>(internalRange);

  useEffect(() => {
    if (!isControlled) {
      setInternalRange((prev) => {
        const adjusted = {
          startIndex: Math.min(prev.startIndex, Math.max(0, totalPoints - 1)),
          endIndex: Math.min(prev.endIndex, Math.max(0, totalPoints - 1)),
        };
        lastCommittedRef.current = adjusted;
        return adjusted;
      });
    }
  }, [totalPoints, isControlled]);

  // ── Clamping & committing ───────────────────────────────────────────────

  const clampRange = useCallback(
    (range: EvilBrushRange, mode?: DragType): EvilBrushRange => {
      let { startIndex, endIndex } = range;
      const maxIndex = Math.max(0, totalPoints - 1);

      startIndex = Math.max(0, Math.min(startIndex, maxIndex));
      endIndex = Math.max(0, Math.min(endIndex, maxIndex));

      if (mode === "left") {
        const maxStart = Math.max(0, endIndex - minSpan);
        startIndex = Math.min(startIndex, maxStart);
        return { startIndex, endIndex };
      }

      if (mode === "right") {
        const minEnd = Math.min(maxIndex, startIndex + minSpan);
        endIndex = Math.max(endIndex, minEnd);
        return { startIndex, endIndex };
      }

      if (endIndex - startIndex < minSpan) {
        endIndex = Math.min(startIndex + minSpan, maxIndex);
        if (endIndex - startIndex < minSpan) {
          startIndex = Math.max(0, endIndex - minSpan);
        }
      }
      return { startIndex, endIndex };
    },
    [totalPoints, minSpan],
  );

  const commit = useCallback(
    (next: EvilBrushRange, mode?: DragType) => {
      const clamped = clampRange(next, mode);
      const last = lastCommittedRef.current;

      // Only update if the range has actually changed — avoids unnecessary
      // re-renders when the brush is at a boundary and small mouse movements
      // don't produce index changes
      if (last.startIndex === clamped.startIndex && last.endIndex === clamped.endIndex) {
        return;
      }

      lastCommittedRef.current = clamped;
      setInternalRange(clamped);
      // Defer the parent callback — chart re-render happens at lower priority,
      // React can skip intermediate frames during fast drags
      React.startTransition(() => {
        onChange?.(clamped);
      });
    },
    [clampRange, onChange],
  );

  // ── Drag ────────────────────────────────────────────────────────────────

  const { isDragging, bind } = useBrushDrag({
    range: internalRange,
    totalPoints,
    containerRef,
    commit,
  });

  // Position always driven by internalRange (never lags behind controlled props)
  const range = internalRange;

  // Sync internalRange with controlled props when not dragging
  useEffect(() => {
    if (isControlled && !isDragging) {
      const syncedRange = { startIndex: controlledStart, endIndex: controlledEnd };
      // eslint-disable-next-line react-hooks/set-state-in-effect
      setInternalRange(syncedRange);
      lastCommittedRef.current = syncedRange;
    }
  }, [isControlled, controlledStart, controlledEnd, isDragging]);

  // ── Computed positions (%) ──────────────────────────────────────────────

  const leftPct = totalPoints > 1 ? (range.startIndex / (totalPoints - 1)) * 100 : 0;
  const rightPct = totalPoints > 1 ? (range.endIndex / (totalPoints - 1)) * 100 : 100;

  // Drive all moving brush UI from the same springed edge values.
  const leftTarget = useMotionValue(leftPct);
  const rightTarget = useMotionValue(rightPct);
  if (leftTarget.get() !== leftPct) leftTarget.set(leftPct);
  if (rightTarget.get() !== rightPct) rightTarget.set(rightPct);

  const leftSpring = useSpring(leftTarget, SPRING_CONFIG);
  const rightSpring = useSpring(rightTarget, SPRING_CONFIG);
  const leftPosition = useTransform(leftSpring, (v) => `${v}%`);
  const rightPosition = useTransform(rightSpring, (v) => `${v}%`);
  const leftOverlayWidth = useTransform(leftSpring, (v) => `${v}%`);
  const rightOverlayWidth = useTransform(rightSpring, (v) => `${Math.max(0, 100 - v)}%`);
  const selectedWidth = useMotionValue(`${Math.max(0, rightPct - leftPct)}%`);

  const updateSelectedWidth = useCallback(() => {
    selectedWidth.set(`${Math.max(0, rightSpring.get() - leftSpring.get())}%`);
  }, [leftSpring, rightSpring, selectedWidth]);

  useMotionValueEvent(leftSpring, "change", updateSelectedWidth);
  useMotionValueEvent(rightSpring, "change", updateSelectedWidth);

  const getLabel = useCallback(
    (idx: number) => {
      if (!xDataKey) return String(idx);
      const v = data[idx]?.[xDataKey];
      return formatLabel ? formatLabel(v, idx) : String(v ?? idx);
    },
    [data, xDataKey, formatLabel],
  );

  // ── Render ──────────────────────────────────────────────────────────────

  if (totalPoints === 0) return null;

  return (
    <div
      ref={containerRef}
      data-chart={skipStyle ? undefined : chartId}
      className={cn("group relative select-none", className)}
      style={{ height }}
    >
      {!skipStyle && <ChartStyle id={chartId} config={chartConfig} />}

      {/* Mini chart – always shows all data */}
      <div className="absolute inset-0 overflow-hidden rounded-md">
        <MiniChart
          data={data}
          keys={keys}
          chartConfig={chartConfig}
          variant={variant}
          curveType={curveType}
          chartId={chartId}
          stacked={stacked}
          strokeVariant={strokeVariant === "animated-dashed" ? "dashed" : strokeVariant}
          connectNulls={connectNulls}
          barRadius={barRadius}
        />
      </div>

      {/* Dim overlay – left */}
      <motion.div
        className="bg-background/70 pointer-events-none absolute inset-y-0 left-0 rounded-l-md"
        style={{ width: leftOverlayWidth }}
      />
      {/* Dim overlay – right */}
      <motion.div
        className="bg-background/70 pointer-events-none absolute inset-y-0 right-0 rounded-r-md"
        style={{ width: rightOverlayWidth }}
      />

      {/* Selected region – draggable to pan */}
      <motion.div
        className="absolute inset-y-0 cursor-grab touch-none rounded-sm border active:cursor-grabbing"
        style={{ left: leftPosition, width: selectedWidth }}
        {...bind("middle")}
      />

      {/* Left handle */}
      <BrushHandle
        side="left"
        position={leftPosition}
        label={showLabels ? getLabel(range.startIndex) : undefined}
        bind={bind("left")}
      />

      {/* Right handle */}
      <BrushHandle
        side="right"
        position={rightPosition}
        label={showLabels ? getLabel(range.endIndex) : undefined}
        bind={bind("right")}
      />
    </div>
  );
}

// ─── Brush Handle ───────────────────────────────────────────────────────────

function BrushHandle({
  side,
  position,
  label,
  bind,
}: {
  side: "left" | "right";
  position: MotionValue<string>;
  label?: string;
  bind: {
    onPointerDown: (e: React.PointerEvent) => void;
    onPointerMove: (e: React.PointerEvent) => void;
    onPointerUp: (e: React.PointerEvent) => void;
  };
}) {
  const isLeft = side === "left";

  return (
    <motion.div className="absolute inset-y-0 z-10" style={{ left: position }}>
      <div
        className={cn(
          "group absolute inset-y-0 flex w-3 cursor-ew-resize touch-none items-center justify-center after:absolute after:inset-y-0 after:-left-4 after:w-11 after:content-['']",
          isLeft ? "" : "-translate-x-full",
        )}
        {...bind}
      >
        <div
          className={cn(
            "bg-muted-foreground group-hover:bg-foreground relative flex h-4 w-1.5 items-center justify-center rounded-md transition-colors",
            isLeft ? "-left-[5.5px]" : "-right-[5.5px]",
          )}
        >
          <div className="flex flex-col gap-[2px]">
            <div className="bg-background/70 h-[2px] w-[2px] rounded-full" />
            <div className="bg-background/70 h-[2px] w-[2px] rounded-full" />
            <div className="bg-background/70 h-[2px] w-[2px] rounded-full" />
          </div>
        </div>
      </div>

      {label && (
        <div
          className={cn(
            "bg-foreground text-background pointer-events-none absolute -bottom-3 -translate-y-1/2 rounded-[3px] px-1 py-px text-[8px] leading-tight font-medium whitespace-nowrap opacity-0 group-hover:opacity-100",
            isLeft ? "left-1.5" : "right-1.5",
          )}
        >
          {label}
        </div>
      )}
    </motion.div>
  );
}

// ─── Mini Chart ─────────────────────────────────────────────────────────────

function MiniChart({
  data,
  keys,
  chartConfig,
  variant,
  curveType,
  chartId,
  stacked,
  strokeVariant = "solid",
  connectNulls = false,
  barRadius,
}: {
  data: Record<string, unknown>[];
  keys: string[];
  chartConfig: ChartConfig;
  variant: EvilBrushVariant;
  curveType: CurveType;
  chartId: string;
  stacked: boolean;
  strokeVariant?: "solid" | "dashed" | "animated-dashed";
  connectNulls?: boolean;
  barRadius?: number;
}) {
  const gradients = React.useMemo(
    () =>
      Object.entries(chartConfig)
        .filter(([key]) => keys.includes(key))
        .map(([dataKey, config]) => ({
          dataKey,
          colorsCount: getColorsCount(config),
        })),
    [chartConfig, keys],
  );

  const dashArray =
    strokeVariant === "dashed" || strokeVariant === "animated-dashed" ? "4 4" : undefined;

  const defsContent = (
    <>
      {/* Vertical fade gradient for area fill mask */}
      {variant === "area" && (
        <linearGradient id={`${chartId}-zm-vertical-fade`} x1="0" y1="0" x2="0" y2="1">
          <stop offset="0%" stopColor="white" stopOpacity={0.15} />
          <stop offset="100%" stopColor="white" stopOpacity={0} />
        </linearGradient>
      )}
      {gradients.map(({ dataKey, colorsCount }) => {
        const colorStops =
          colorsCount === 1 ? (
            <>
              <stop offset="0%" stopColor={`var(--color-${dataKey}-0)`} />
              <stop offset="100%" stopColor={`var(--color-${dataKey}-0)`} />
            </>
          ) : (
            Array.from({ length: colorsCount }, (_, i) => (
              <stop
                key={i}
                offset={`${(i / (colorsCount - 1)) * 100}%`}
                stopColor={`var(--color-${dataKey}-${i}, var(--color-${dataKey}-0))`}
              />
            ))
          );

        return (
          <React.Fragment key={dataKey}>
            {/* Vertical color gradient (stroke + bar fill) */}
            <linearGradient id={`${chartId}-zm-${dataKey}`} x1="0" y1="0" x2="0" y2="1">
              {colorStops}
            </linearGradient>

            {/* Area fill: color gradient masked with vertical fade */}
            {variant === "area" && (
              <>
                <mask id={`${chartId}-zm-fill-mask-${dataKey}`}>
                  <rect width="100%" height="100%" fill={`url(#${chartId}-zm-vertical-fade)`} />
                </mask>
                <pattern
                  id={`${chartId}-zm-fill-${dataKey}`}
                  patternUnits="userSpaceOnUse"
                  width="100%"
                  height="100%"
                >
                  <rect
                    width="100%"
                    height="100%"
                    fill={`url(#${chartId}-zm-${dataKey})`}
                    mask={`url(#${chartId}-zm-fill-mask-${dataKey})`}
                  />
                </pattern>
              </>
            )}
          </React.Fragment>
        );
      })}
    </>
  );

  if (variant === "line") {
    return (
      <ResponsiveContainer width="100%" height="100%">
        <LineChart data={data} margin={{ top: 4, right: 0, bottom: 0, left: 0 }}>
          <defs>{defsContent}</defs>
          {keys.map((dk) => (
            <Line
              key={dk}
              type={curveType}
              dataKey={dk}
              stroke={`url(#${chartId}-zm-${dk})`}
              strokeWidth={1}
              strokeOpacity={0.5}
              strokeDasharray={dashArray}
              connectNulls={connectNulls}
              dot={false}
              activeDot={false}
              isAnimationActive={false}
            />
          ))}
        </LineChart>
      </ResponsiveContainer>
    );
  }

  if (variant === "bar") {
    const r = barRadius ?? 3;
    return (
      <ResponsiveContainer width="100%" height="100%">
        <BarChart
          data={data}
          margin={{ top: 2, right: 0, bottom: 0, left: 0 }}
          barGap={2}
          barSize={14}
        >
          <defs>{defsContent}</defs>
          {keys.map((dk) => (
            <Bar
              key={dk}
              dataKey={dk}
              fill={`url(#${chartId}-zm-${dk})`}
              fillOpacity={0.35}
              stackId={stacked ? "zm-stack" : undefined}
              isAnimationActive={false}
              radius={[r, r, r, r]}
            />
          ))}
        </BarChart>
      </ResponsiveContainer>
    );
  }

  // Default: area
  return (
    <ResponsiveContainer width="100%" height="100%">
      <AreaChart data={data} margin={{ top: 4, right: 0, bottom: 0, left: 0 }}>
        <defs>{defsContent}</defs>
        {keys.map((dk) => (
          <Area
            key={dk}
            type={curveType}
            dataKey={dk}
            stroke={`url(#${chartId}-zm-${dk})`}
            fill={`url(#${chartId}-zm-fill-${dk})`}
            strokeWidth={1}
            strokeOpacity={0.5}
            strokeDasharray={dashArray}
            connectNulls={connectNulls}
            fillOpacity={1}
            stackId={stacked ? "zm-stack" : undefined}
            dot={false}
            activeDot={false}
            isAnimationActive={false}
          />
        ))}
      </AreaChart>
    </ResponsiveContainer>
  );
}

// ─── useEvilBrush Hook ──────────────────────────────────────────────────────

function useEvilBrush<TData extends Record<string, unknown>>({
  data,
  defaultStartIndex = 0,
  defaultEndIndex,
}: {
  data: TData[];
  defaultStartIndex?: number;
  defaultEndIndex?: number;
}) {
  const [range, setRange] = React.useState<EvilBrushRange>({
    startIndex: defaultStartIndex,
    endIndex: defaultEndIndex ?? Math.max(0, data.length - 1),
  });

  // Defer the range used for data slicing — the brush handles move at the

  // immediate `range` cadence while the expensive chart re-render uses the
  // deferred value.  React can skip intermediate slices during fast drags.
  const deferredRange = React.useDeferredValue(range);

  useEffect(() => {
    // eslint-disable-next-line react-hooks/set-state-in-effect
    setRange({
      startIndex: 0,
      endIndex: Math.max(0, data.length - 1),
    });
  }, [data.length]);

  const visibleData = React.useMemo(
    () => data.slice(deferredRange.startIndex, deferredRange.endIndex + 1),
    [data, deferredRange.startIndex, deferredRange.endIndex],
  );

  return {
    range,
    visibleData,
    brushProps: {
      startIndex: range.startIndex,
      endIndex: range.endIndex,
      onChange: setRange,
    } satisfies Pick<EvilBrushProps, "startIndex" | "endIndex" | "onChange">,
  };
}

export { EvilBrush, useEvilBrush, type EvilBrushProps, type EvilBrushRange, type EvilBrushVariant };

More from corr

Browse all components in this library →