feat(dashboard): extend Chart component to include bounce rate and average duration metrics for enhanced analytics

This commit is contained in:
Usman Baig
2026-01-22 21:53:25 +01:00
parent 81cef1b485
commit ac427597b3
2 changed files with 44 additions and 17 deletions

View File

@@ -46,6 +46,8 @@ interface DailyStat {
date: string date: string
pageviews: number pageviews: number
visitors: number visitors: number
bounce_rate: number
avg_duration: number
} }
interface Stats { interface Stats {
@@ -77,9 +79,9 @@ function ChartTooltip({
colors, colors,
}: { }: {
active?: boolean active?: boolean
payload?: Array<{ payload: { prevPageviews?: number; prevVisitors?: number }; value: number }> payload?: Array<{ payload: { prevPageviews?: number; prevVisitors?: number; prevBounceRate?: number; prevAvgDuration?: number }; value: number }>
label?: string label?: string
metric: 'visitors' | 'pageviews' metric: MetricType
metricLabel: string metricLabel: string
formatNumberFn: (n: number) => string formatNumberFn: (n: number) => string
showComparison: boolean showComparison: boolean
@@ -88,17 +90,31 @@ function ChartTooltip({
if (!active || !payload?.length || !label) return null if (!active || !payload?.length || !label) return null
// * Recharts sends one payload entry per Area; order can be [prevSeries, currentSeries]. // * Recharts sends one payload entry per Area; order can be [prevSeries, currentSeries].
// * Use the entry for the current metric so the tooltip shows today's value, not yesterday's. // * Use the entry for the current metric so the tooltip shows today's value, not yesterday's.
type PayloadItem = { dataKey?: string; value?: number; payload: { prevPageviews?: number; prevVisitors?: number; visitors?: number; pageviews?: number } } type PayloadItem = { dataKey?: string; value?: number; payload: { prevPageviews?: number; prevVisitors?: number; prevBounceRate?: number; prevAvgDuration?: number; visitors?: number; pageviews?: number; bounce_rate?: number; avg_duration?: number } }
const items = payload as PayloadItem[] const items = payload as PayloadItem[]
const current = items.find((p) => p.dataKey === metric) ?? items[items.length - 1] const current = items.find((p) => p.dataKey === metric) ?? items[items.length - 1]
const value = Number(current?.value ?? (current?.payload as Record<string, number>)?.[metric] ?? 0) const value = Number(current?.value ?? (current?.payload as Record<string, number>)?.[metric] ?? 0)
const prev = metric === 'visitors' ? current?.payload?.prevVisitors : current?.payload?.prevPageviews
let prev: number | undefined
switch (metric) {
case 'visitors': prev = current?.payload?.prevVisitors; break;
case 'pageviews': prev = current?.payload?.prevPageviews; break;
case 'bounce_rate': prev = current?.payload?.prevBounceRate; break;
case 'avg_duration': prev = current?.payload?.prevAvgDuration; break;
}
const hasPrev = showComparison && prev != null const hasPrev = showComparison && prev != null
const delta = const delta =
hasPrev && (prev as number) > 0 hasPrev && (prev as number) > 0
? Math.round(((value - (prev as number)) / (prev as number)) * 100) ? Math.round(((value - (prev as number)) / (prev as number)) * 100)
: null : null
const formatValue = (v: number) => {
if (metric === 'bounce_rate') return `${Math.round(v)}%`
if (metric === 'avg_duration') return formatDuration(v)
return formatNumberFn(v)
}
return ( return (
<div <div
className="rounded-lg border px-4 py-3 shadow-lg" className="rounded-lg border px-4 py-3 shadow-lg"
@@ -112,7 +128,7 @@ function ChartTooltip({
</div> </div>
<div className="flex items-baseline gap-2"> <div className="flex items-baseline gap-2">
<span className="text-base font-bold" style={{ color: colors.text }}> <span className="text-base font-bold" style={{ color: colors.text }}>
{formatNumberFn(value)} {formatValue(value)}
</span> </span>
<span className="text-xs" style={{ color: colors.textMuted }}> <span className="text-xs" style={{ color: colors.textMuted }}>
{metricLabel} {metricLabel}
@@ -120,12 +136,12 @@ function ChartTooltip({
</div> </div>
{hasPrev && ( {hasPrev && (
<div className="mt-1.5 flex items-center gap-1.5 text-xs" style={{ color: colors.textMuted }}> <div className="mt-1.5 flex items-center gap-1.5 text-xs" style={{ color: colors.textMuted }}>
<span>vs {formatNumberFn(prev as number)} prev</span> <span>vs {formatValue(prev as number)} prev</span>
{delta !== null && ( {delta !== null && (
<span <span
className="font-medium" className="font-medium"
style={{ style={{
color: delta > 0 ? COLORS.success : delta < 0 ? COLORS.danger : colors.textMuted, color: delta > 0 ? (metric === 'bounce_rate' ? COLORS.danger : COLORS.success) : delta < 0 ? (metric === 'bounce_rate' ? COLORS.success : COLORS.danger) : colors.textMuted,
}} }}
> >
{delta > 0 ? '+' : ''}{delta}% {delta > 0 ? '+' : ''}{delta}%
@@ -180,8 +196,12 @@ export default function Chart({ data, prevData, stats, prevStats, interval }: Ch
originalDate: item.date, originalDate: item.date,
pageviews: item.pageviews, pageviews: item.pageviews,
visitors: item.visitors, visitors: item.visitors,
bounce_rate: item.bounce_rate,
avg_duration: item.avg_duration,
prevPageviews: prevItem?.pageviews, prevPageviews: prevItem?.pageviews,
prevVisitors: prevItem?.visitors, prevVisitors: prevItem?.visitors,
prevBounceRate: prevItem?.bounce_rate,
prevAvgDuration: prevItem?.avg_duration,
} }
}) })
@@ -242,8 +262,8 @@ export default function Chart({ data, prevData, stats, prevStats, interval }: Ch
] as const ] 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 chartMetric = metric
const metricLabel = chartMetric === 'pageviews' ? 'pageviews' : 'visitors' const metricLabel = metrics.find(m => m.id === metric)?.label || 'visitors'
const avg = chartData.length const avg = chartData.length
? chartData.reduce((s, d) => s + (d[chartMetric] as number), 0) / chartData.length ? chartData.reduce((s, d) => s + (d[chartMetric] as number), 0) / chartData.length
@@ -275,16 +295,12 @@ export default function Chart({ data, prevData, stats, prevStats, interval }: Ch
{metrics.map((item) => ( {metrics.map((item) => (
<button <button
key={item.id} key={item.id}
onClick={() => { onClick={() => setMetric(item.id as MetricType)}
if (item.id === 'visitors' || item.id === 'pageviews') {
setMetric(item.id as MetricType)
}
}}
className={` className={`
p-6 text-left transition-colors relative group p-6 text-left transition-colors relative group
hover:bg-neutral-50 dark:hover:bg-neutral-800/50 hover:bg-neutral-50 dark:hover:bg-neutral-800/50
${metric === item.id ? 'bg-neutral-50 dark:bg-neutral-800/50' : ''} ${metric === item.id ? 'bg-neutral-50 dark:bg-neutral-800/50' : ''}
${(item.id !== 'visitors' && item.id !== 'pageviews') ? 'cursor-default' : 'cursor-pointer'} cursor-pointer
`} `}
> >
<div className={`text-xs font-semibold uppercase tracking-wider mb-1 flex items-center gap-2 ${metric === item.id ? 'text-neutral-900 dark:text-white' : 'text-neutral-500'}`}> <div className={`text-xs font-semibold uppercase tracking-wider mb-1 flex items-center gap-2 ${metric === item.id ? 'text-neutral-900 dark:text-white' : 'text-neutral-500'}`}>
@@ -404,7 +420,11 @@ export default function Chart({ data, prevData, stats, prevStats, interval }: Ch
tickLine={false} tickLine={false}
axisLine={false} axisLine={false}
domain={[0, 'auto']} domain={[0, 'auto']}
tickFormatter={formatAxisValue} tickFormatter={(val) => {
if (metric === 'bounce_rate') return `${val}%`
if (metric === 'avg_duration') return formatDuration(val)
return formatAxisValue(val)
}}
/> />
<Tooltip <Tooltip
content={(p: TooltipProps<number, string>) => ( content={(p: TooltipProps<number, string>) => (
@@ -437,7 +457,12 @@ export default function Chart({ data, prevData, stats, prevStats, interval }: Ch
{hasPrev && ( {hasPrev && (
<Area <Area
type="monotone" type="monotone"
dataKey={chartMetric === 'visitors' ? 'prevVisitors' : 'prevPageviews'} dataKey={
chartMetric === 'visitors' ? 'prevVisitors' :
chartMetric === 'pageviews' ? 'prevPageviews' :
chartMetric === 'bounce_rate' ? 'prevBounceRate' :
'prevAvgDuration'
}
stroke={colors.axis} stroke={colors.axis}
strokeWidth={2} strokeWidth={2}
strokeDasharray="5 5" strokeDasharray="5 5"

View File

@@ -74,6 +74,8 @@ export interface DailyStat {
date: string date: string
pageviews: number pageviews: number
visitors: number visitors: number
bounce_rate: number
avg_duration: number
} }
export interface RealtimeStats { export interface RealtimeStats {