From 500310048a62aa4c5a21132160edcf664052085a Mon Sep 17 00:00:00 2001 From: Usman Baig Date: Mon, 19 Jan 2026 18:25:49 +0100 Subject: [PATCH] feat: enhance Chart component with theme-aware tooltip, comparison feature, and improved axis formatting --- components/dashboard/Chart.tsx | 334 ++++++++++++++++++++++++++------- 1 file changed, 261 insertions(+), 73 deletions(-) diff --git a/components/dashboard/Chart.tsx b/components/dashboard/Chart.tsx index 38f0b40..d7884e0 100644 --- a/components/dashboard/Chart.tsx +++ b/components/dashboard/Chart.tsx @@ -1,18 +1,43 @@ 'use client' -import { useState } from 'react' -import { AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts' +import { useState, useMemo } from 'react' +import { useTheme } from 'next-themes' +import { + AreaChart, + Area, + XAxis, + YAxis, + CartesianGrid, + Tooltip, + ResponsiveContainer, + ReferenceLine, +} from 'recharts' +import type { TooltipProps } from 'recharts' import { formatNumber, formatDuration } from '@/lib/utils/format' -import { ArrowTopRightIcon, ArrowBottomRightIcon, DownloadIcon } from '@radix-ui/react-icons' +import { ArrowTopRightIcon, ArrowBottomRightIcon, DownloadIcon, BarChartIcon } from '@radix-ui/react-icons' const COLORS = { brand: '#FD5E0F', success: '#10B981', // Emerald-500 danger: '#EF4444', // Red-500 - border: '#E5E5E5', // Neutral-200 - text: '#171717', // Neutral-900 - textMuted: '#737373', // Neutral-500 - axis: '#A3A3A3', // Neutral-400 +} + +const CHART_COLORS_LIGHT = { + border: '#E5E5E5', + text: '#171717', + textMuted: '#737373', + axis: '#A3A3A3', + tooltipBg: '#ffffff', + tooltipBorder: '#E5E5E5', +} + +const CHART_COLORS_DARK = { + border: '#404040', + text: '#fafafa', + textMuted: '#a3a3a3', + axis: '#737373', + tooltipBg: '#262626', + tooltipBorder: '#404040', } interface DailyStat { @@ -38,14 +63,96 @@ interface ChartProps { type MetricType = 'pageviews' | 'visitors' | 'bounce_rate' | 'avg_duration' +// * Custom tooltip with comparison and theme-aware styling +function ChartTooltip({ + active, + payload, + label, + metric, + metricLabel, + formatNumberFn, + showComparison, + colors, +}: { + active?: boolean + payload?: Array<{ payload: { prevPageviews?: number; prevVisitors?: number }; value: number }> + label?: string + metric: 'visitors' | 'pageviews' + metricLabel: string + formatNumberFn: (n: number) => string + showComparison: boolean + colors: typeof CHART_COLORS_LIGHT +}) { + if (!active || !payload?.length || !label) return null + const d = payload[0] + const value = d.value as number + const prev = metric === 'visitors' ? d.payload.prevVisitors : d.payload.prevPageviews + const hasPrev = showComparison && prev != null + const delta = + hasPrev && (prev as number) > 0 + ? Math.round(((value - (prev as number)) / (prev as number)) * 100) + : null + + return ( +
+
+ {label} +
+
+ + {formatNumberFn(value)} + + + {metricLabel} + +
+ {hasPrev && ( +
+ vs {formatNumberFn(prev as number)} prev + {delta !== null && ( + 0 ? COLORS.success : delta < 0 ? COLORS.danger : colors.textMuted, + }} + > + {delta > 0 ? '+' : ''}{delta}% + + )} +
+ )} +
+ ) +} + +// * Compact Y-axis formatter: 1.5M, 12k, 99 +function formatAxisValue(value: number): string { + if (value >= 1e6) return `${value / 1e6}M` + if (value >= 1000) return `${value / 1000}k` + return String(value) +} + export default function Chart({ data, prevData, stats, prevStats, interval }: ChartProps) { const [metric, setMetric] = useState('visitors') + const [showComparison, setShowComparison] = useState(true) + const { resolvedTheme } = useTheme() + + const colors = useMemo( + () => (resolvedTheme === 'dark' ? CHART_COLORS_DARK : CHART_COLORS_LIGHT), + [resolvedTheme] + ) // * Align current and previous data const chartData = data.map((item, i) => { // * Try to find matching previous item (assuming same length/order) // * For more robustness, we could match by relative index - const prevItem = prevData && prevData[i] + const prevItem = prevData?.[i] // * Format date based on interval let formattedDate: string @@ -123,7 +230,15 @@ export default function Chart({ data, prevData, stats, prevStats, interval }: Ch }, ] as const - const activeMetric = metrics.find(m => m.id === metric) || metrics[0] + const activeMetric = metrics.find((m) => m.id === metric) || metrics[0] + const chartMetric = metric === 'visitors' || metric === 'pageviews' ? metric : 'visitors' + const metricLabel = chartMetric === 'pageviews' ? 'pageviews' : 'visitors' + + const avg = chartData.length + ? chartData.reduce((s, d) => s + (d[chartMetric] as number), 0) / chartData.length + : 0 + + const hasPrev = !!(prevData?.length && showComparison) return (
@@ -177,8 +292,19 @@ export default function Chart({ data, prevData, stats, prevStats, interval }: Ch {/* Chart Area */}
-
-
-
- - - - - - - - - - - value >= 1000 ? `${value/1000}k` : value} - /> - - - {/* Previous Period Line (Dashed) */} - {prevData && ( - - )} - {/* Current Period Area */} - + + - - -
+ This period + + + + Previous period + +
+ )} + + {data.length === 0 ? ( +
+ +

+ No data for this period +

+

Try a different date range

+
+ ) : ( +
+ + + + + + + + + + + + + ) => ( + } + label={p.label as string} + metric={chartMetric} + metricLabel={metricLabel} + formatNumberFn={formatNumber} + showComparison={hasPrev} + colors={colors} + /> + )} + cursor={{ stroke: activeMetric.color, strokeDasharray: '4 4', strokeWidth: 1 }} + /> + + {avg > 0 && ( + + )} + + {hasPrev && ( + + )} + + + + +
+ )}
)