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 && (
+
+ )}
+
+
+
+
+
+ )}
)