ui icon indicating copy to clipboard operation
ui copied to clipboard

Support Recharts v3

Open wottpal opened this issue 6 months ago • 22 comments

Describe the bug

https://github.com/recharts/recharts/releases

Affected component/components

Charts

How to reproduce

Clone the chart.tsx components which throw several errors.

Codesandbox/StackBlitz link

No response

Logs


System Info

-

Before submitting

  • [x] I've made research efforts and searched the documentation
  • [x] I've searched for existing issues

wottpal avatar Jun 23 '25 07:06 wottpal

This happens due to Charts component referring heavily to internal props of Recharts, which have now been removed.

As a consequence, our UI app which uses the shadcn-ui Charts component, and where we are also using those internal props such as payload, label, item and index, completely fails to build.

Error: src/components/charts/job-durations.tsx(303,36): error TS2741: Property 'payload' is missing in type '{}' but required in type 'Pick<Props, "payload" | "verticalAlign">'.
Error: src/components/ui/chart.tsx(116,3): error TS2339: Property 'payload' does not exist on type 'Omit<Props<ValueType, NameType>, PropertiesReadFromContext> & { active?: boolean | undefined; includeHidden?: boolean | undefined; ... 17 more ...; axisId?: AxisId | undefined; } & ClassAttributes<...> & HTMLAttributes<...> & { ...; }'.
Error: src/components/ui/chart.tsx(121,3): error TS2339: Property 'label' does not exist on type 'Omit<Props<ValueType, NameType>, PropertiesReadFromContext> & { active?: boolean | undefined; includeHidden?: boolean | undefined; ... 17 more ...; axisId?: AxisId | undefined; } & ClassAttributes<...> & HTMLAttributes<...> & { ...; }'.
Error: src/components/ui/chart.tsx(191,23): error TS7006: Parameter 'item' implicitly has an 'any' type.
Error: src/components/ui/chart.tsx(191,29): error TS7006: Parameter 'index' implicitly has an 'any' type.
Error: src/components/ui/chart.tsx(270,39): error TS2344: Type '"payload" | "verticalAlign"' does not satisfy the constraint '"string" | "filter" | "fill" | "values" | "children" | "className" | "suppressHydrationWarning" | "color" | "height" | "id" | "lang" | "max" | "media" | "method" | "min" | "name" | ... 430 more ... | "onBBoxUpdate"'.
  Type '"payload"' is not assignable to type '"string" | "filter" | "fill" | "values" | "children" | "className" | "suppressHydrationWarning" | "color" | "height" | "id" | "lang" | "max" | "media" | "method" | "min" | "name" | ... 430 more ... | "onBBoxUpdate"'.
Error: src/components/ui/chart.tsx(276,17): error TS2339: Property 'length' does not exist on type '{}'.
Error: src/components/ui/chart.tsx(288,16): error TS2339: Property 'map' does not exist on type '{}'.
Error: src/components/ui/chart.tsx(288,21): error TS7006: Parameter 'item' implicitly has an 'any' type.

Etsija avatar Jun 23 '25 07:06 Etsija

here the updated code until we get an official fix :)

Gist link: https://gist.github.com/noxify/92bc410cc2d01109f4160002da9a61e5


Tested it locally with the "Chart Area - Interactive" and it seems to work, there was an error on hover. This is now fixed.

noxify avatar Jun 23 '25 09:06 noxify

"use client"

import * as React from "react"
import * as RechartsPrimitive from "recharts"
import type { LegendPayload } from "recharts/types/component/DefaultLegendContent"
import {
  NameType,
  Payload,
  ValueType,
} from "recharts/types/component/DefaultTooltipContent"
import type { Props as LegendProps } from "recharts/types/component/Legend"
import { TooltipContentProps } from "recharts/types/component/Tooltip"

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

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

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

type ChartContextProps = {
  config: ChartConfig
}

export type CustomTooltipProps = TooltipContentProps<ValueType, NameType> & {
  className?: string
  hideLabel?: boolean
  hideIndicator?: boolean
  indicator?: "line" | "dot" | "dashed"
  nameKey?: string
  labelKey?: string
  labelFormatter?: (
    label: TooltipContentProps<number, string>["label"],
    payload: TooltipContentProps<number, string>["payload"]
  ) => React.ReactNode
  formatter?: (
    value: number | string,
    name: string,
    item: Payload<number | string, string>,
    index: number,
    payload: ReadonlyArray<Payload<number | string, string>>
  ) => React.ReactNode
  labelClassName?: string
  color?: string
}

export type ChartLegendContentProps = {
  className?: string
  hideIcon?: boolean
  verticalAlign?: LegendProps["verticalAlign"]
  payload?: LegendPayload[]
  nameKey?: string
}

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

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

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

  return context
}

function ChartContainer({
  id,
  className,
  children,
  config,
  ...props
}: React.ComponentProps<"div"> & {
  config: ChartConfig
  children: React.ComponentProps<
    typeof RechartsPrimitive.ResponsiveContainer
  >["children"]
}) {
  const uniqueId = React.useId()
  const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`

  return (
    <ChartContext.Provider value={{ config }}>
      <div
        data-slot="chart"
        data-chart={chartId}
        className={cn(
          "[&_.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 flex aspect-video 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",
          className
        )}
        {...props}
      >
        <ChartStyle id={chartId} config={config} />
        <RechartsPrimitive.ResponsiveContainer>
          {children}
        </RechartsPrimitive.ResponsiveContainer>
      </div>
    </ChartContext.Provider>
  )
}

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
      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}: ${color};` : null
              })
              .join("\n")}
            }
            `
          )
          .join("\n"),
      }}
    />
  )
}

const ChartTooltip = RechartsPrimitive.Tooltip

function ChartTooltipContent({
  active,
  payload,
  label,
  className,
  indicator = "dot",
  hideLabel = false,
  hideIndicator = false,
  labelFormatter,
  formatter,
  labelClassName,
  color,
  nameKey,
  labelKey,
}: CustomTooltipProps) {
  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 = (() => {
      const v =
        !labelKey && typeof label === "string"
          ? config[label as keyof typeof config]?.label ?? label
          : itemConfig?.label

      return typeof v === "string" || typeof v === "number" ? v : undefined
    })()

    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) {
    return null
  }

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

  return (
    <div
      className={cn(
        "border-border/50 bg-background grid min-w-[8rem] items-start gap-1.5 rounded-lg border px-2.5 py-1.5 text-xs shadow-xl",
        className
      )}
    >
      {!nestLabel ? tooltipLabel : null}
      <div className="grid gap-1.5">
        {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={item.dataKey}
              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"
              )}
            >
              {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] border-(--color-border) bg-(--color-bg)",
                          {
                            "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={
                          {
                            "--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-muted-foreground">
                        {itemConfig?.label || item.name}
                      </span>
                    </div>
                    {item.value && (
                      <span className="text-foreground font-mono font-medium tabular-nums">
                        {item.value.toLocaleString()}
                      </span>
                    )}
                  </div>
                </>
              )}
            </div>
          )
        })}
      </div>
    </div>
  )
}

const ChartLegend = RechartsPrimitive.Legend

function ChartLegendContent({
  className,
  hideIcon = false,
  payload,
  verticalAlign = "bottom",
  nameKey,
}: ChartLegendContentProps) {
  const { config } = useChart()

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

  return (
    <div
      className={cn(
        "flex items-center justify-center gap-4",
        verticalAlign === "top" ? "pb-3" : "pt-3",
        className
      )}
    >
      {payload.map((item) => {
        const key = `${nameKey || item.dataKey || "value"}`
        const itemConfig = getPayloadConfigFromPayload(config, item, key)

        return (
          <div
            key={item.value}
            className={cn(
              "[&>svg]:text-muted-foreground flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3"
            )}
          >
            {itemConfig?.icon && !hideIcon ? (
              <itemConfig.icon />
            ) : (
              <div
                className="h-2 w-2 shrink-0 rounded-[2px]"
                style={{
                  backgroundColor: item.color,
                }}
              />
            )}
            {itemConfig?.label}
          </div>
        )
      })}
    </div>
  )
}

// Helper to extract item config from a payload.
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]
}

export {
  ChartContainer,
  ChartTooltip,
  ChartTooltipContent,
  ChartLegend,
  ChartLegendContent,
  ChartStyle,
}

Just updated due to the change with 3.0.2

Also exporting types for the custom types. Reason why the types is needed...

import type { CustomTooltipProps } from '@/components/ui/chart'

    <ChartTooltip
      cursor={false}
      content={(props: CustomTooltipProps) => (
        <ChartTooltipContent {...props} hideIndicator hideLabel />
      )}
    />

rubixvi avatar Jun 23 '25 23:06 rubixvi

@rubixvi you are a god send thank you

andrewsmithdesign avatar Jun 24 '25 23:06 andrewsmithdesign

@noxify Huge thanks!

OctavioSerpe avatar Jun 26 '25 23:06 OctavioSerpe

"use client"

import * as React from "react"
import * as RechartsPrimitive from "recharts"
import type { LegendPayload } from "recharts/types/component/DefaultLegendContent"
import {
  NameType,
  Payload,
  ValueType,
} from "recharts/types/component/DefaultTooltipContent"
import type { Props as LegendProps } from "recharts/types/component/Legend"
import { TooltipContentProps } from "recharts/types/component/Tooltip"

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

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

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

type ChartContextProps = {
  config: ChartConfig
}

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

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

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

  return context
}

function ChartContainer({
  id,
  className,
  children,
  config,
  ...props
}: React.ComponentProps<"div"> & {
  config: ChartConfig
  children: React.ComponentProps<
    typeof RechartsPrimitive.ResponsiveContainer
  >["children"]
}) {
  const uniqueId = React.useId()
  const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`

  return (
    <ChartContext.Provider value={{ config }}>
      <div
        data-slot="chart"
        data-chart={chartId}
        className={cn(
          "[&_.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 flex aspect-video 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",
          className
        )}
        {...props}
      >
        <ChartStyle id={chartId} config={config} />
        <RechartsPrimitive.ResponsiveContainer>
          {children}
        </RechartsPrimitive.ResponsiveContainer>
      </div>
    </ChartContext.Provider>
  )
}

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
      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}: ${color};` : null
              })
              .join("\n")}
            }
            `
          )
          .join("\n"),
      }}
    />
  )
}

const ChartTooltip = RechartsPrimitive.Tooltip

type CustomTooltipProps = TooltipContentProps<ValueType, NameType> & {
  className?: string
  hideLabel?: boolean
  hideIndicator?: boolean
  indicator?: "line" | "dot" | "dashed"
  nameKey?: string
  labelKey?: string
  labelFormatter?: (
    label: TooltipContentProps<number, string>["label"],
    payload: TooltipContentProps<number, string>["payload"]
  ) => React.ReactNode
  formatter?: (
    value: number | string,
    name: string,
    item: Payload<number | string, string>,
    index: number,
    payload: ReadonlyArray<Payload<number | string, string>>
  ) => React.ReactNode
  labelClassName?: string
  color?: string
}

function ChartTooltipContent({
  active,
  payload,
  label,
  className,
  indicator = "dot",
  hideLabel = false,
  hideIndicator = false,
  labelFormatter,
  formatter,
  labelClassName,
  color,
  nameKey,
  labelKey,
}: CustomTooltipProps) {
  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 as keyof typeof config]?.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) {
    return null
  }

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

  return (
    <div
      className={cn(
        "border-border/50 bg-background grid min-w-[8rem] items-start gap-1.5 rounded-lg border px-2.5 py-1.5 text-xs shadow-xl",
        className
      )}
    >
      {!nestLabel ? tooltipLabel : null}
      <div className="grid gap-1.5">
        {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={item.dataKey}
              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"
              )}
            >
              {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] border-(--color-border) bg-(--color-bg)",
                          {
                            "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={
                          {
                            "--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-muted-foreground">
                        {itemConfig?.label || item.name}
                      </span>
                    </div>
                    {item.value && (
                      <span className="text-foreground font-mono font-medium tabular-nums">
                        {item.value.toLocaleString()}
                      </span>
                    )}
                  </div>
                </>
              )}
            </div>
          )
        })}
      </div>
    </div>
  )
}

const ChartLegend = RechartsPrimitive.Legend

type ChartLegendContentProps = {
  className?: string
  hideIcon?: boolean
  verticalAlign?: LegendProps["verticalAlign"]
  payload?: LegendPayload[]
  nameKey?: string
}

function ChartLegendContent({
  className,
  hideIcon = false,
  payload,
  verticalAlign = "bottom",
  nameKey,
}: ChartLegendContentProps) {
  const { config } = useChart()

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

  return (
    <div
      className={cn(
        "flex items-center justify-center gap-4",
        verticalAlign === "top" ? "pb-3" : "pt-3",
        className
      )}
    >
      {payload.map((item) => {
        const key = `${nameKey || item.dataKey || "value"}`
        const itemConfig = getPayloadConfigFromPayload(config, item, key)

        return (
          <div
            key={item.value}
            className={cn(
              "[&>svg]:text-muted-foreground flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3"
            )}
          >
            {itemConfig?.icon && !hideIcon ? (
              <itemConfig.icon />
            ) : (
              <div
                className="h-2 w-2 shrink-0 rounded-[2px]"
                style={{
                  backgroundColor: item.color,
                }}
              />
            )}
            {itemConfig?.label}
          </div>
        )
      })}
    </div>
  )
}

// Helper to extract item config from a payload.
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]
}

export {
  ChartContainer,
  ChartTooltip,
  ChartTooltipContent,
  ChartLegend,
  ChartLegendContent,
  ChartStyle,
}

Here you go

I just added the code to my chart.tsx and it doesn't work somehow.

I also added the react-is in my package.json so what am I doing wrong?

Bewenben avatar Jun 26 '25 23:06 Bewenben

code was just updated again...

Change:

    const value =
      !labelKey && typeof label === "string"
        ? config[label as keyof typeof config]?.label || label
        : itemConfig?.label

to

const value = (() => {
  const v =
    !labelKey && typeof label === "string"
      ? config[label as keyof typeof config]?.label ?? label
      : itemConfig?.label

  return typeof v === "string" || typeof v === "number" ? v : undefined
})()

rubixvi avatar Jun 27 '25 00:06 rubixvi

export type CustomTooltipProps = TooltipContentProps<ValueType, NameType> & {
  className?: string
  hideLabel?: boolean
  hideIndicator?: boolean
  indicator?: "line" | "dot" | "dashed"
  nameKey?: string
  labelKey?: string
  labelFormatter?: (
    label: TooltipContentProps<number, string>["label"],
    payload: TooltipContentProps<number, string>["payload"]
  ) => React.ReactNode
  formatter?: (
    value: number | string,
    name: string,
    item: Payload<number | string, string>,
    index: number,
    payload: ReadonlyArray<Payload<number | string, string>>
  ) => React.ReactNode
  labelClassName?: string
  color?: string
}

This was moved to the top from my previous comment. This type might be used in several cases. For example:

const formatter: CustomTooltipProps["formatter"] = (value, name) => [value, name]

    <ChartTooltip
      labelFormatter={formatLongDate}
      formatter={formatter}
      content={(props: CustomTooltipProps) => (
        <ChartTooltipContent {...props} indicator="dashed" />
      )}
    />

Otherwise, you'll end up getting complaints about type safety 'any'.

rubixvi avatar Jun 27 '25 01:06 rubixvi

Does anyone else get the following Error randomly?

Error: Cannot destructure property 'activeIndex' of 'z(...)' as it is undefined.

3 |           <ChartTooltip
      |           ^
  184 |             cursor={false}
  185 |             content={(props: CustomTooltipProps) => (
  186 |               <ChartTooltipContent {...props} hideIndicator hideLabel />

I have used the chart.tsx shown here in the discussion and the tooltip like this:

          <ChartTooltip
            cursor={false}
            content={(props: CustomTooltipProps) => (
              <ChartTooltipContent {...props} hideIndicator hideLabel />
            )}
          />

Megajin avatar Jun 27 '25 07:06 Megajin

Does anyone else get the following Error randomly?

Error: Cannot destructure property 'activeIndex' of 'z(...)' as it is undefined.

3 |           <ChartTooltip
      |           ^
  184 |             cursor={false}
  185 |             content={(props: CustomTooltipProps) => (
  186 |               <ChartTooltipContent {...props} hideIndicator hideLabel />

I have used the chart.tsx shown here in the discussion and the tooltip like this:

          <ChartTooltip
            cursor={false}
            content={(props: CustomTooltipProps) => (
              <ChartTooltipContent {...props} hideIndicator hideLabel />
            )}
          />

You can use:

          <ChartTooltip
            cursor={false}
            content={(props) => (
              <ChartTooltipContent {...props} hideIndicator hideLabel />
            )}
          />

See if that helps, however, you will get some type safety warning.

Unless we set up guards for the tooltip, haven't had time to go through that and set up all the guards yet.

rubixvi avatar Jun 27 '25 08:06 rubixvi

Does anyone else get the following Error randomly? Error: Cannot destructure property 'activeIndex' of 'z(...)' as it is undefined.

3 |           <ChartTooltip
      |           ^
  184 |             cursor={false}
  185 |             content={(props: CustomTooltipProps) => (
  186 |               <ChartTooltipContent {...props} hideIndicator hideLabel />

I have used the chart.tsx shown here in the discussion and the tooltip like this:

          <ChartTooltip
            cursor={false}
            content={(props: CustomTooltipProps) => (
              <ChartTooltipContent {...props} hideIndicator hideLabel />
            )}
          />

You can use:

          <ChartTooltip
            cursor={false}
            content={(props) => (
              <ChartTooltipContent {...props} hideIndicator hideLabel />
            )}
          />

See if that helps, however, you will get some type safety warning.

Unless we set up guards for the tooltip, haven't had time to go through that and set up all the guards yet.

That didn't work however when I just import Tooltip from recharts where I build my chart, it works fine...

Megajin avatar Jun 27 '25 10:06 Megajin

That works, depending on how your data is structured and how you've designed your charts.

I've tested in a live environment, in development Tooltip type may be fine. But when you're deploying it, it may crash depending on how your custom component is structured.

rubixvi avatar Jun 27 '25 19:06 rubixvi

@rubixvi, your ChartTooltipContent indicators are not consuming the border/bg variables and thus never showing.

- border-(--color-border) bg-(--color-bg)
+ border-[var(--color-border)] bg-[var(--color-bg)]

edit: @rubixvi, sorry the issue is main. Tooltips are showing, but the indicator (dot/line/dashed) is not.

ShawnStewart avatar Jul 03 '25 05:07 ShawnStewart

@rubixvi, your ChartTooltipContent indicators are not consuming the border/bg variables and thus never showing.

- border-(--color-border) bg-(--color-bg)
+ border-[var(--color-border)] bg-[var(--color-bg)]

What are you on about... All my charts are working. I think you quoted the wrong thing.

Not consuming the border colour is not going to prevent a tool tip component from showing up.

rubixvi avatar Jul 03 '25 05:07 rubixvi

I've pinned recharts to 2.15.4 for now. I've also added a callout on the docs page. @noxify @rubixvi thanks for the code examples. I'll take a look and start the upgrade.

shadcn avatar Jul 09 '25 08:07 shadcn

Still errors, [email protected] does not fix this.

@noxify @rubixvi What am I doing wrong?

item.payload seems to be not existing or any!?

label is of type any

Image Image

henningsieh avatar Jul 14 '25 08:07 henningsieh

@henningsieh any chance to provide the link to your repo or at least a minimal repro with your implementation?

Could take a look in the evening but need more information about your implementation.

Feel free to ping me via discord --> noxy88

noxify avatar Jul 14 '25 09:07 noxify

Hi, check this link

  • https://intentui.com/docs/components/visualizations/chart
  • https://intentui.com/docs/components/visualizations/area-chart

Its using Recharts v3 you can use the source for inspiration


PS. intentui is like shadcn but for react-aria

sadeghbarati avatar Jul 14 '25 10:07 sadeghbarati

Any recent progress or changes?

shhhwepsss avatar Aug 15 '25 06:08 shhhwepsss

Any recent progress or changes?

pedrobonifacio avatar Sep 23 '25 19:09 pedrobonifacio

Does anyone else get the following Error randomly?

Error: Cannot destructure property 'activeIndex' of 'z(...)' as it is undefined.

3 |           <ChartTooltip
      |           ^
  184 |             cursor={false}
  185 |             content={(props: CustomTooltipProps) => (
  186 |               <ChartTooltipContent {...props} hideIndicator hideLabel />

I have used the chart.tsx shown here in the discussion and the tooltip like this:

          <ChartTooltip
            cursor={false}
            content={(props: CustomTooltipProps) => (
              <ChartTooltipContent {...props} hideIndicator hideLabel />
            )}
          />

Im hitting this issue, only way I could figure out a fix was patch-package

diff --git a/es6/component/Tooltip.js b/es6/component/Tooltip.js
index 08a1416abff8def9df9b45468f34720f553fe117..4a8746ae9a2a66ac7072fb7c53fc02c54f31d082 100644
--- a/es6/component/Tooltip.js
+++ b/es6/component/Tooltip.js
@@ -99,7 +99,7 @@ export function Tooltip(outsideProps) {
   var {
     activeIndex,
     isActive
-  } = useAppSelector(state => selectIsTooltipActive(state, tooltipEventType, trigger, defaultIndexAsString));
+  } = useAppSelector(state => selectIsTooltipActive(state, tooltipEventType, trigger, defaultIndexAsString))  || { activeIndex: null, isActive: false };
   var payloadFromRedux = useAppSelector(state => selectTooltipPayload(state, tooltipEventType, trigger, defaultIndexAsString));
   var labelFromRedux = useAppSelector(state => selectActiveLabel(state, tooltipEventType, trigger, defaultIndexAsString));
   var coordinate = useAppSelector(state => selectActiveCoordinate(state, tooltipEventType, trigger, defaultIndexAsString));

arjunyel avatar Sep 30 '25 04:09 arjunyel

Why not merge to main?

wsvkmt avatar Dec 12 '25 12:12 wsvkmt