feat: enhance chart tooltip and legend components with improved type definitions and payload handling

This commit is contained in:
Julien Froidefond
2025-11-27 10:02:27 +01:00
parent 66c4ead350
commit c6299de8b2
5 changed files with 2405 additions and 18 deletions

View File

@@ -104,6 +104,15 @@ ${colorConfig
const ChartTooltip = RechartsPrimitive.Tooltip const ChartTooltip = RechartsPrimitive.Tooltip
type TooltipPayloadItem = {
dataKey?: string | number
name?: string
value?: number | string
color?: string
payload?: Record<string, unknown> & { fill?: string }
fill?: string
}
function ChartTooltipContent({ function ChartTooltipContent({
active, active,
payload, payload,
@@ -118,13 +127,15 @@ function ChartTooltipContent({
color, color,
nameKey, nameKey,
labelKey, labelKey,
}: React.ComponentProps<typeof RechartsPrimitive.Tooltip> & }: Omit<React.ComponentProps<typeof RechartsPrimitive.Tooltip>, 'payload' | 'label'> &
React.ComponentProps<'div'> & { React.ComponentProps<'div'> & {
hideLabel?: boolean hideLabel?: boolean
hideIndicator?: boolean hideIndicator?: boolean
indicator?: 'line' | 'dot' | 'dashed' indicator?: 'line' | 'dot' | 'dashed'
nameKey?: string nameKey?: string
labelKey?: string labelKey?: string
payload?: TooltipPayloadItem[]
label?: string | number
}) { }) {
const { config } = useChart() const { config } = useChart()
@@ -179,10 +190,10 @@ function ChartTooltipContent({
> >
{!nestLabel ? tooltipLabel : null} {!nestLabel ? tooltipLabel : null}
<div className="grid gap-1.5"> <div className="grid gap-1.5">
{payload.map((item, index) => { {payload.map((item: TooltipPayloadItem, index: number) => {
const key = `${nameKey || item.name || item.dataKey || 'value'}` const key = `${nameKey || item.name || item.dataKey || 'value'}`
const itemConfig = getPayloadConfigFromPayload(config, item, key) const itemConfig = getPayloadConfigFromPayload(config, item, key)
const indicatorColor = color || item.payload.fill || item.color const indicatorColor = color || item.payload?.fill || item.color
return ( return (
<div <div
@@ -193,7 +204,7 @@ function ChartTooltipContent({
)} )}
> >
{formatter && item?.value !== undefined && item.name ? ( {formatter && item?.value !== undefined && item.name ? (
formatter(item.value, item.name, item, index, item.payload) formatter(item.value, item.name, item as never, index, item.payload as never)
) : ( ) : (
<> <>
{itemConfig?.icon ? ( {itemConfig?.icon ? (
@@ -250,16 +261,23 @@ function ChartTooltipContent({
const ChartLegend = RechartsPrimitive.Legend const ChartLegend = RechartsPrimitive.Legend
type LegendPayloadItem = {
value?: string
dataKey?: string | number
color?: string
}
function ChartLegendContent({ function ChartLegendContent({
className, className,
hideIcon = false, hideIcon = false,
payload, payload,
verticalAlign = 'bottom', verticalAlign = 'bottom',
nameKey, nameKey,
}: React.ComponentProps<'div'> & }: React.ComponentProps<'div'> & {
Pick<RechartsPrimitive.LegendProps, 'payload' | 'verticalAlign'> & {
hideIcon?: boolean hideIcon?: boolean
nameKey?: string nameKey?: string
payload?: LegendPayloadItem[]
verticalAlign?: 'top' | 'bottom' | 'middle'
}) { }) {
const { config } = useChart() const { config } = useChart()
@@ -275,7 +293,7 @@ function ChartLegendContent({
className, className,
)} )}
> >
{payload.map((item) => { {payload.map((item: LegendPayloadItem) => {
const key = `${nameKey || item.dataKey || 'value'}` const key = `${nameKey || item.dataKey || 'value'}`
const itemConfig = getPayloadConfigFromPayload(config, item, key) const itemConfig = getPayloadConfigFromPayload(config, item, key)

36
eslint.config.mjs Normal file
View File

@@ -0,0 +1,36 @@
import nextPlugin from '@next/eslint-plugin-next'
import reactPlugin from 'eslint-plugin-react'
import hooksPlugin from 'eslint-plugin-react-hooks'
import tseslint from 'typescript-eslint'
export default [
{
ignores: ['node_modules/**', '.next/**', 'out/**', 'build/**'],
},
...tseslint.configs.recommended,
{
files: ['**/*.{js,jsx,ts,tsx}'],
plugins: {
'@next/next': nextPlugin,
react: reactPlugin,
'react-hooks': hooksPlugin,
},
rules: {
...nextPlugin.configs.recommended.rules,
...nextPlugin.configs['core-web-vitals'].rules,
'react/react-in-jsx-scope': 'off',
'react-hooks/rules-of-hooks': 'error',
'react-hooks/exhaustive-deps': 'warn',
'@typescript-eslint/no-unused-vars': [
'warn',
{ argsIgnorePattern: '^_', varsIgnorePattern: '^_' },
],
'@typescript-eslint/no-explicit-any': 'warn',
},
settings: {
react: {
version: 'detect',
},
},
},
]

View File

@@ -42,7 +42,7 @@ export function parseOFX(content: string): OFXAccount | null {
amount: Number.parseFloat(amountStr), amount: Number.parseFloat(amountStr),
name: cleanString(name), name: cleanString(name),
memo: memo ? cleanString(memo) : undefined, memo: memo ? cleanString(memo) : undefined,
checkNum, checkNum: checkNum ?? undefined,
type, type,
}) })
} }

View File

@@ -71,15 +71,22 @@
"zod": "3.25.76" "zod": "3.25.76"
}, },
"devDependencies": { "devDependencies": {
"@eslint/eslintrc": "^3.3.1",
"@next/eslint-plugin-next": "^16.0.5",
"@tailwindcss/postcss": "^4.1.9", "@tailwindcss/postcss": "^4.1.9",
"@types/node": "^22", "@types/node": "^22",
"@types/react": "^19", "@types/react": "^19",
"@types/react-dom": "^19", "@types/react-dom": "^19",
"dotenv": "^17.2.3", "dotenv": "^17.2.3",
"eslint": "^9.39.1",
"eslint-config-next": "^16.0.5",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^7.0.1",
"postcss": "^8.5", "postcss": "^8.5",
"tailwindcss": "^4.1.9", "tailwindcss": "^4.1.9",
"tsx": "^4.20.6", "tsx": "^4.20.6",
"tw-animate-css": "1.3.3", "tw-animate-css": "1.3.3",
"typescript": "^5" "typescript": "^5",
"typescript-eslint": "^8.48.0"
} }
} }

2344
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff