component — charts

Chart

Themeable recharts wrapper with styled axes, grid, tooltip, and an interactive legend, using an ink series palette (gray ramp, no hue). Requires the recharts package.

  • chart
  • recharts
  • data
  • graph
  • visualization

06 examples

Line

A line chart with two ink series, an interactive legend, and a tooltip.

Bar

Grouped bars sharing the same config and tooltip.

Area

A filled area chart with the ink as the primary series.

Custom legend content

`ChartLegend` renders whatever you give its `content`. The default is the interactive `ChartLegendContent`; this one is a plain read-only key.

Synchronized charts

Two charts sharing a `syncId` share a hover: pointing at March in one moves the tooltip and cursor in the other, which is what makes a stack of small charts read as one picture.

Responsive sizing

`Chart` wraps a `ResponsiveContainer`, so width always comes from the parent — never from a prop. Only the height is yours: `containerHeight`, defaulting to 370 and to 200 under the mobile breakpoint. These two charts are the same component in two grid tracks.

source

chart.tsx

Copy this into your project. Resolve its dependencies from the registryDependencies in the component's API entry.

"use client"

import {
  createContext,
  type ReactElement,
  use,
  useCallback,
  useEffect,
  useId,
  useMemo,
  useState,
} from "react"
import {
  composeRenderProps,
  ToggleButton,
  ToggleButtonGroup,
  type ToggleButtonGroupProps,
} from "react-aria-components"
import type {
  CartesianGridProps as CartesianGridPrimitiveProps,
  CartesianGridProps,
  LegendPayload,
  LegendProps,
  XAxisProps as XAxisPropsPrimitive,
  YAxisProps as YAxisPrimitiveProps,
} from "recharts"
import {
  CartesianGrid as CartesianGridPrimitive,
  Legend as LegendPrimitive,
  ResponsiveContainer,
  Tooltip as TooltipPrimitive,
  XAxis as XAxisPrimitive,
  YAxis as YAxisPrimitive,
} from "recharts"
import type { ContentType as LegendContentType } from "recharts/types/component/DefaultLegendContent"
import type {
  NameType,
  Props as TooltipContentProps,
  ValueType,
} from "recharts/types/component/DefaultTooltipContent"
import type {
  TooltipProps as RechartsTooltipProps,
  ContentType as TooltipContentType,
} from "recharts/types/component/Tooltip"
import { cn } from "@/lib/utils"

/**
 * Chart — quebi design system
 *
 * A themeable recharts wrapper: provides a `Chart` container plus styled
 * axes, grid, tooltip, and an interactive legend. Series colors come from an
 * ink ramp (`chart-1` is the ink itself) and can be overridden per-key via the
 * `config` prop. The chrome is hairline grid, subtle caption-size ticks and a
 * floating tooltip — the data carries the contrast, the frame does not.
 *
 * Requires the `recharts` npm package as a peer dependency.
 */

// Small inline mobile detector so the component stays self-contained.
function useIsMobile(breakpoint = 768) {
  const [isMobile, setIsMobile] = useState(false)

  useEffect(() => {
    if (typeof window === "undefined") {
      return
    }
    const mql = window.matchMedia(`(max-width: ${breakpoint - 1}px)`)
    const onChange = () => setIsMobile(mql.matches)
    onChange()
    mql.addEventListener("change", onChange)
    return () => mql.removeEventListener("change", onChange)
  }, [breakpoint])

  return isMobile
}

// #region Chart Types
type ChartType = "default" | "stacked" | "percent"
type ChartLayout = "horizontal" | "vertical" | "radial"
type IntervalType = "preserveStartEnd" | "equidistantPreserveStart"

type ChartConfig = {
  [k in string]: {
    label?: React.ReactNode
    icon?: React.ComponentType
  } & (
    | { color?: ChartColorKeys | (string & {}); theme?: never }
    | { color?: never; theme: Record<keyof typeof THEMES, string> }
  )
}

// Ink & Paper series palette: no hue, only ink. Each step is a rung of the
// gray ramp, ordered so that neighbouring series alternate dark and light and
// never sit on adjacent rungs; the values flip with the theme through the
// `--q-chart-*` variables in quebi-theme.css. chart-6 to chart-10 are the
// overflow ring for hierarchical charts (Treemap, Sunburst), which routinely
// have more branches than a five-series line chart. Past five series, label
// the marks — grey alone cannot carry ten categories.
const CHART_COLORS = {
  "chart-1": "var(--q-chart-1)",
  "chart-2": "var(--q-chart-2)",
  "chart-3": "var(--q-chart-3)",
  "chart-4": "var(--q-chart-4)",
  "chart-5": "var(--q-chart-5)",
  "chart-6": "var(--q-chart-6)",
  "chart-7": "var(--q-chart-7)",
  "chart-8": "var(--q-chart-8)",
  "chart-9": "var(--q-chart-9)",
  "chart-10": "var(--q-chart-10)",
} as const

type ChartColorKeys = keyof typeof CHART_COLORS | (string & {})

const DEFAULT_COLORS = ["chart-1", "chart-2", "chart-3", "chart-4", "chart-5"] as const

/**
 * The full ten-step ring. `DEFAULT_COLORS` stays at five so the series charts
 * keep the palette they were designed against; pass this where a chart has more
 * categories than series, such as a Treemap or a Sunburst.
 */
const EXTENDED_COLORS = [
  "chart-1",
  "chart-2",
  "chart-3",
  "chart-4",
  "chart-5",
  "chart-6",
  "chart-7",
  "chart-8",
  "chart-9",
  "chart-10",
] as const

type ChartContextProps = {
  config: ChartConfig
  data?: Record<string, unknown>[]
  layout: ChartLayout
  dataKey?: string
  selectedLegend: string | null
  onLegendSelect: (legendItem: string | null) => void
}

const ChartContext = createContext<ChartContextProps | null>(null)

export function useChart() {
  const context = use(ChartContext)

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

  return context
}

export function valueToPercent(value: number) {
  return `${(value * 100).toFixed(0)}%`
}

const constructCategoryColors = (
  categories: string[],
  colors: readonly ChartColorKeys[],
): Map<string, ChartColorKeys> => {
  const categoryColors = new Map<string, ChartColorKeys>()

  categories.forEach((category, index) => {
    const color = colors[index % colors.length]
    if (color !== undefined) {
      categoryColors.set(category, color)
    }
  })

  return categoryColors
}

const getColorValue = (color?: string): string => {
  if (!color) {
    return CHART_COLORS["chart-1"]
  }

  return CHART_COLORS[color as "chart-1"] ?? color
}

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 as keyof typeof config]
}

interface BaseChartProps<TValue extends ValueType, TName extends NameType>
  extends React.HTMLAttributes<HTMLDivElement> {
  containerHeight?: number
  config: ChartConfig
  data: Record<string, unknown>[]
  dataKey: string
  colors?: readonly (ChartColorKeys | (string & {}))[]
  type?: ChartType
  intervalType?: IntervalType
  layout?: ChartLayout
  valueFormatter?: (value: number) => string

  tooltip?: TooltipContentType<TValue, TName> | boolean
  tooltipProps?: Omit<ChartTooltipProps<TValue, TName>, "content"> & {
    hideLabel?: boolean
    labelSeparator?: boolean
    hideIndicator?: boolean
    indicator?: "line" | "dot" | "dashed"
    nameKey?: string
    labelKey?: string
  }

  cartesianGridProps?: CartesianGridProps

  /**
   * Extra recharts children — a `Brush`, a `ReferenceLine`, a `ReferenceArea` —
   * rendered *alongside* the series the config generates. `children` replaces
   * those series; this is the slot for the case where you want to keep them and
   * add something next to them.
   */
  overlays?: React.ReactNode

  legend?: LegendContentType | boolean
  legendProps?: Omit<React.ComponentProps<typeof LegendPrimitive>, "content" | "ref">

  xAxisProps?: XAxisPropsPrimitive
  yAxisProps?: YAxisPrimitiveProps

  // XAxis
  displayEdgeLabelsOnly?: boolean

  hideGridLines?: boolean
  hideXAxis?: boolean
  hideYAxis?: boolean
}

const Chart = ({
  id,
  className,
  children,
  config,
  data,
  dataKey,
  ref,
  layout = "horizontal",
  containerHeight,
  ...props
}: Omit<React.ComponentProps<"div">, "children"> &
  Pick<ChartContextProps, "data" | "dataKey"> & {
    config: ChartConfig
    containerHeight?: number
    layout?: ChartLayout
    children: ReactElement | ((props: ChartContextProps) => ReactElement)
  }) => {
  const isMobile = useIsMobile()
  const uniqueId = useId()
  const chartId = useMemo(() => `chart-${id || uniqueId.replace(/:/g, "")}`, [id, uniqueId])

  const [selectedLegend, setSelectedLegend] = useState<string | null>(null)

  const onLegendSelect = useCallback((legendItem: string | null) => {
    setSelectedLegend(legendItem)
  }, [])

  const _data = data ?? []
  const _dataKey = dataKey ?? "value"

  const value = useMemo(
    () => ({
      config,
      selectedLegend,
      onLegendSelect,
      data: _data,
      dataKey: _dataKey,
      layout,
    }),
    [config, selectedLegend, onLegendSelect, _data, _dataKey, layout],
  )

  return (
    <ChartContext value={value}>
      <div
        data-chart={chartId}
        ref={ref}
        className={cn(
          "z-20 flex w-full justify-center text-quebi-caption text-quebi-fg-subtle",
          "[&_.recharts-cartesian-axis-tick_text]:fill-quebi-fg-subtle [&_.recharts-polar-angle-axis-tick_text]:fill-quebi-fg-subtle [&_.recharts-polar-radius-axis-tick_text]:fill-quebi-fg-subtle [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-quebi-hairline [&_.recharts-curve.recharts-tooltip-cursor]:stroke-quebi-hairline [&_.recharts-layer]:outline-hidden [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-quebi-hairline [&_.recharts-radial-bar-background-sector]:fill-quebi-raised [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-quebi-raised [&_.recharts-reference-line_[stroke='#ccc']]:stroke-quebi-hairline [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-sector]:outline-hidden [&_.recharts-surface]:outline-hidden",
          "[&_.recharts-dot[fill='#fff']]:fill-(--line-color)",
          "[&_.recharts-active-dot>.recharts-dot]:stroke-quebi-hairline",
          "[&_.recharts-surface_g]:focus:outline-hidden",
          className,
        )}
        {...props}
      >
        <ChartStyle id={chartId} config={config} />
        <ResponsiveContainer height={containerHeight ?? (isMobile ? 200 : 370)}>
          {typeof children === "function" ? children(value) : children}
        </ResponsiveContainer>
      </div>
    </ChartContext>
  )
}

const THEMES = { light: "", dark: ".dark" } as const
const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
  const colorConfig = Object.entries(config).filter(([_, config]) => config.theme || config.color)

  if (!colorConfig.length) {
    return null
  }

  return (
    <style
      // biome-ignore lint/security/noDangerouslySetInnerHtml: dynamic chart theme CSS injection
      dangerouslySetInnerHTML={{
        __html: Object.entries(THEMES)
          .map(
            ([theme, prefix]) => `
${prefix} [data-chart=${id}] {
${colorConfig
  .map(([key, itemConfig]) => {
    const color = itemConfig.theme?.[theme as keyof typeof itemConfig.theme] || itemConfig.color
    return color ? `  --color-${key}: ${getColorValue(color)};` : null
  })
  .join("\n")}
}
`,
          )
          .join("\n"),
      }}
    />
  )
}

type ChartTooltipProps<TValue extends ValueType, TName extends NameType> = RechartsTooltipProps<
  TValue,
  TName
>

const tooltipWrapperStyle = { outline: "none" } as const

// Theme-aware cursor tint: quebi-surface flips (white on dark, ink on light),
// so the hover cursor stays a faint neutral wash in both themes. A wash, not
// `raised`: the cursor sits over the marks, and a solid ground would hide them.
const cursorTint = "color-mix(in oklab, var(--color-quebi-surface) 6%, transparent)"

const cursorStyleRadial = {
  stroke: cursorTint,
  strokeWidth: 0.1,
  fill: cursorTint,
  fillOpacity: 0.5,
} as const

const cursorStyleDefault = {
  stroke: cursorTint,
  strokeWidth: 1,
  fill: cursorTint,
  fillOpacity: 0.5,
} as const

const ChartTooltip = <TValue extends ValueType, TName extends NameType>(
  props: ChartTooltipProps<TValue, TName>,
) => {
  const { layout } = useChart()
  const cursorStyle = layout === "radial" ? cursorStyleRadial : cursorStyleDefault

  return (
    <TooltipPrimitive
      wrapperStyle={tooltipWrapperStyle}
      cursor={cursorStyle}
      {...(props as RechartsTooltipProps<ValueType, NameType>)}
    />
  )
}

type ChartLegendProps = Omit<React.ComponentProps<typeof LegendPrimitive>, "ref">

const ChartLegend = (props: ChartLegendProps) => {
  return <LegendPrimitive align="center" verticalAlign="bottom" {...props} />
}

interface XAxisProps extends Omit<XAxisPropsPrimitive, "ref"> {
  displayEdgeLabelsOnly?: boolean
  intervalType?: IntervalType
}

const tickHorizontal = {
  transform: "translate(32, 6)",
} as const

const XAxis = ({
  displayEdgeLabelsOnly,
  className,
  intervalType = "preserveStartEnd",
  minTickGap = 5,
  domain,
  ...props
}: XAxisProps) => {
  const { dataKey, data, layout } = useChart()

  const ticks =
    displayEdgeLabelsOnly && data?.length && dataKey
      ? ([data[0]?.[dataKey], data[data.length - 1]?.[dataKey]] as (string | number)[])
      : undefined

  const tick = layout === "horizontal" ? tickHorizontal : undefined
  return (
    <XAxisPrimitive
      className={cn(
        "font-mono text-quebi-fg-subtle text-quebi-caption tabular-nums **:[text]:fill-quebi-fg-subtle",
        className,
      )}
      interval={displayEdgeLabelsOnly ? "preserveStartEnd" : intervalType}
      tick={tick}
      ticks={ticks}
      tickLine={false}
      axisLine={false}
      // Destructured but never forwarded before this line, so a numeric X axis
      // (a scatter plot's) could not be given a domain at all.
      domain={domain}
      minTickGap={minTickGap}
      dataKey={layout === "horizontal" ? dataKey : undefined}
      {...props}
    />
  )
}

const yAxisTickHorizontal = {
  transform: "translate(-3, 0)",
} as const

const yAxisTickVertical = {
  transform: "translate(0, 0)",
} as const

const YAxis = ({
  className,
  width,
  domain = ["auto", "auto"],
  type,
  ...props
}: Omit<YAxisPrimitiveProps, "ref">) => {
  const { layout, dataKey } = useChart()

  const tick = layout === "horizontal" ? yAxisTickHorizontal : yAxisTickVertical

  return (
    <YAxisPrimitive
      className={cn(
        "font-mono text-quebi-fg-subtle text-quebi-caption tabular-nums **:[text]:fill-quebi-fg-subtle",
        className,
      )}
      // Both of these used to bind tighter than intended — `width ?? layout ===
      // "horizontal"` is a boolean, so an explicit `width` was always discarded,
      // and `type || layout === "horizontal"` made every axis numeric, so
      // `type="category"` could not be asked for. The parentheses are the fix.
      width={width ?? (layout === "horizontal" ? 40 : 80)}
      domain={domain}
      tick={tick}
      dataKey={layout === "horizontal" ? undefined : dataKey}
      type={type ?? (layout === "horizontal" ? "number" : "category")}
      interval={layout === "horizontal" ? undefined : "equidistantPreserveStart"}
      axisLine={false}
      tickLine={false}
      {...props}
    />
  )
}

const CartesianGrid = ({ className, ...props }: CartesianGridPrimitiveProps) => {
  const { layout } = useChart()
  return (
    <CartesianGridPrimitive
      className={cn("stroke-1 stroke-quebi-hairline", className)}
      horizontal={layout !== "vertical"}
      vertical={layout === "vertical"}
      {...props}
    />
  )
}

const ChartTooltipContent = <TValue extends ValueType, TName extends NameType>({
  payload,
  className,
  indicator = "dot",
  hideLabel = false,
  hideIndicator = false,
  label,
  labelSeparator = true,
  labelFormatter,
  labelClassName,
  formatter,
  color,
  nameKey,
  labelKey,
  ref,
}: TooltipContentProps<TValue, TName> &
  React.ComponentProps<"div"> & {
    hideLabel?: boolean
    labelSeparator?: boolean
    hideIndicator?: boolean
    indicator?: "line" | "dot" | "dashed"
    nameKey?: string
    labelKey?: string
  }) => {
  const { config } = useChart()

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

    const [item] = payload

    if (!item) {
      return null
    }

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

    if (labelFormatter) {
      return <div className={labelClassName}>{labelFormatter(value, payload)}</div>
    }

    if (!value) {
      return null
    }

    return <div className={labelClassName}>{value}</div>
  }, [label, labelFormatter, payload, hideLabel, labelClassName, config, labelKey])

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

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

  return (
    <div
      ref={ref}
      className={cn(
        "grid min-w-48 items-start rounded-quebi-s border border-quebi-hairline bg-quebi-elevated px-3 py-2 text-quebi-caption text-quebi-fg shadow-quebi-float",
        className,
      )}
    >
      {!hideLabel && (
        <>
          {!nestLabel ? <span className="font-medium">{tooltipLabel}</span> : null}
          {labelSeparator && (
            <span aria-hidden className="mt-2 mb-3 block h-px w-full bg-quebi-hairline" />
          )}
        </>
      )}
      <div className="grid gap-3">
        {payload.map((item, index) => {
          const key = `${nameKey || item.name || item.dataKey || "value"}`
          const itemConfig = getPayloadConfigFromPayload(config, item, key)
          const indicatorColor = color || item.payload.fill || item.color

          return (
            <div
              key={key}
              className={cn(
                "flex w-full flex-wrap items-stretch gap-2 *:data-[slot=icon]:text-quebi-fg-muted",
                indicator === "dot" && "items-center *:data-[slot=icon]:size-2.5",
                indicator === "line" && "*:data-[slot=icon]:h-full *:data-[slot=icon]:w-2.5",
              )}
            >
              {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-full border-(--color-border) bg-(--color-bg)",
                          indicator === "dot" && "size-2.5",
                          indicator === "line" && "w-1",
                          indicator === "dashed" &&
                            "w-0 border-[1.5px] border-dashed bg-transparent",
                          nestLabel && indicator === "dashed" && "my-0.5",
                        )}
                        style={
                          {
                            "--color-bg": indicatorColor,
                            "--color-border": indicatorColor,
                          } as React.CSSProperties
                        }
                      />
                    )
                  )}
                  <div
                    className={cn(
                      "flex flex-1 justify-between leading-none",
                      nestLabel ? "items-end" : "items-center",
                    )}
                  >
                    <div className="grid gap-1.5">
                      {nestLabel ? tooltipLabel : null}
                      <span className="text-quebi-fg-muted">{itemConfig?.label || item.name}</span>
                    </div>

                    {item.value && (
                      <span className="font-mono text-quebi-fg tabular-nums">
                        {item.value.toString()}
                      </span>
                    )}
                  </div>
                </>
              )}
            </div>
          )
        })}
      </div>
    </div>
  )
}

type ChartLegendContentProps = ToggleButtonGroupProps &
  Pick<LegendProps, "align" | "verticalAlign"> & {
    payload?: ReadonlyArray<LegendPayload>
    hideIcon?: boolean
    nameKey?: string
    ref?: React.Ref<HTMLDivElement>
  }

const ChartLegendContent = ({
  className,
  hideIcon = false,
  payload,
  align = "right",
  verticalAlign = "bottom",
  nameKey,
  ref,
}: ChartLegendContentProps) => {
  const { config, selectedLegend, onLegendSelect } = useChart()

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

  // `item.value` is the fallback for a series identified by name rather than by
  // data key — a Scatter carries its points in its own `data`, so every scatter
  // series would otherwise land on the literal "value" and collide with the
  // others on both the React key and the toggle id.
  const keyFor = (item: LegendPayload) => `${nameKey || item.dataKey || item.value || "value"}`

  // Recharts can report one entry several times: a radial bar chart builds its
  // legend from the rows, and registers all of them once per series. Two items
  // with one id are a duplicate React key and a toggle that selects both.
  const items = payload.filter(
    (item, index) => payload.findIndex((other) => keyFor(other) === keyFor(item)) === index,
  )

  return (
    <ToggleButtonGroup
      ref={ref}
      className={composeRenderProps(className, (resolved) =>
        cn(
          "flex flex-wrap items-center gap-x-1",
          verticalAlign === "top" ? "pb-3" : "pt-3",
          align === "right" ? "justify-end" : align === "left" ? "justify-start" : "justify-center",
          resolved,
        ),
      )}
      // `[]`, not `undefined`: an undefined `selectedKeys` makes the group
      // uncontrolled, and the first click flips it to controlled — React logs
      // the switch and the group briefly owns state the chart is meant to own.
      selectedKeys={selectedLegend ? [selectedLegend] : []}
      onSelectionChange={(v) => {
        const key = [...v][0]?.toString() ?? null
        onLegendSelect(key)
      }}
      selectionMode="single"
    >
      {items.map((item: LegendPayload) => {
        const key = keyFor(item)
        const itemConfig = getPayloadConfigFromPayload(config, item, key)

        return (
          <ToggleButton
            key={key}
            id={key}
            className={cn(
              "flex items-center gap-2 rounded-(--q-radius-control) px-2 py-1 text-quebi-caption text-quebi-fg-muted *:data-[slot=icon]:-mx-0.5 *:data-[slot=icon]:size-2.5 *:data-[slot=icon]:shrink-0 *:data-[slot=icon]:text-quebi-fg-muted",
              // Selected is ink and an underline: no fill, because the swatch
              // beside the label is itself ink-on-paper and a ground would eat it.
              "selected:text-quebi-fg selected:underline selected:decoration-1 selected:underline-offset-5",
              "hover:text-quebi-fg",
              "outline-hidden focus-visible:ring-2 focus-visible:ring-quebi-focus focus-visible:ring-inset",
              "cursor-pointer transition-colors duration-150",
            )}
            aria-label={"Legend Item"}
          >
            {itemConfig?.icon && !hideIcon ? (
              <itemConfig.icon data-slot="icon" />
            ) : (
              <div
                data-slot="icon"
                className="rounded-full"
                style={{
                  backgroundColor: item.color,
                }}
              />
            )}
            {itemConfig?.label}
          </ToggleButton>
        )
      })}
    </ToggleButtonGroup>
  )
}

export type {
  BaseChartProps,
  ChartColorKeys,
  ChartConfig,
  ChartLayout,
  ChartLegendContentProps,
  ChartLegendProps,
  ChartTooltipProps,
  ChartType,
  IntervalType,
  XAxisProps,
}
export {
  CartesianGrid,
  CHART_COLORS,
  Chart,
  ChartLegend,
  ChartLegendContent,
  ChartTooltip,
  ChartTooltipContent,
  constructCategoryColors,
  DEFAULT_COLORS,
  EXTENDED_COLORS,
  getColorValue,
  XAxis,
  YAxis,
}