feat: add compact duration formatting for Y-axis ticks and improve trend display in Chart component

This commit is contained in:
Usman Baig
2026-02-11 20:16:07 +01:00
parent c623ae1e9b
commit 37257c40ad

View File

@@ -11,7 +11,6 @@ import {
Tooltip, Tooltip,
ResponsiveContainer, ResponsiveContainer,
ReferenceLine, ReferenceLine,
Label,
} from 'recharts' } from 'recharts'
import type { TooltipProps } from 'recharts' import type { TooltipProps } from 'recharts'
import { formatNumber, formatDuration } from '@/lib/utils/format' import { formatNumber, formatDuration } from '@/lib/utils/format'
@@ -169,6 +168,15 @@ function formatAxisValue(value: number): string {
return String(value) return String(value)
} }
// * Compact duration for Y-axis ticks (avoids truncation: "5m" not "5m 0s")
function formatAxisDuration(seconds: number): string {
if (!seconds) return '0s'
const m = Math.floor(seconds / 60)
const s = Math.floor(seconds % 60)
if (m > 0) return s > 0 ? `${m}m ${s}s` : `${m}m`
return `${s}s`
}
// * Returns human-readable label for the previous comparison period (e.g. "Feb 10" or "Jan 5 Feb 4") // * Returns human-readable label for the previous comparison period (e.g. "Feb 10" or "Jan 5 Feb 4")
function getPrevDateRangeLabel(dateRange: { start: string; end: string }): string { function getPrevDateRangeLabel(dateRange: { start: string; end: string }): string {
const startDate = new Date(dateRange.start) const startDate = new Date(dateRange.start)
@@ -368,7 +376,7 @@ export default function Chart({
const chartMetric = metric const chartMetric = metric
const metricLabel = metrics.find(m => m.id === metric)?.label || 'visitors' const metricLabel = metrics.find(m => m.id === metric)?.label || 'visitors'
const prevPeriodLabel = prevData?.length ? getPrevDateRangeLabel(dateRange) : '' const prevPeriodLabel = prevData?.length ? getPrevDateRangeLabel(dateRange) : ''
const trendContext = prevStats ? getTrendContext(dateRange) : '' const trendContext = getTrendContext(dateRange)
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
@@ -425,26 +433,30 @@ export default function Chart({
<span className="text-xl sm:text-2xl font-bold text-neutral-900 dark:text-white"> <span className="text-xl sm:text-2xl font-bold text-neutral-900 dark:text-white">
{item.value} {item.value}
</span> </span>
{item.trend !== null && ( <span className="flex items-center text-sm font-medium">
<span className={`flex items-center text-sm font-medium ${ {item.trend !== null ? (
(item.invertTrend ? -item.trend : item.trend) > 0 <>
? 'text-emerald-600 dark:text-emerald-500' <span className={
: (item.invertTrend ? -item.trend : item.trend) < 0 (item.invertTrend ? -item.trend : item.trend) > 0
? 'text-red-600 dark:text-red-500' ? 'text-emerald-600 dark:text-emerald-500'
: 'text-neutral-500' : (item.invertTrend ? -item.trend : item.trend) < 0
}`}> ? 'text-red-600 dark:text-red-500'
{(item.invertTrend ? -item.trend : item.trend) > 0 ? ( : 'text-neutral-500'
<ArrowUpRightIcon className="w-3 h-3 mr-0.5" /> }>
) : (item.invertTrend ? -item.trend : item.trend) < 0 ? ( {(item.invertTrend ? -item.trend : item.trend) > 0 ? (
<ArrowDownRightIcon className="w-3 h-3 mr-0.5" /> <ArrowUpRightIcon className="w-3 h-3 mr-0.5 inline" />
) : null} ) : (item.invertTrend ? -item.trend : item.trend) < 0 ? (
{Math.abs(item.trend)}% <ArrowDownRightIcon className="w-3 h-3 mr-0.5 inline" />
</span> ) : null}
)} {Math.abs(item.trend)}%
</span>
</>
) : (
<span className="text-neutral-500 dark:text-neutral-400"></span>
)}
</span>
</div> </div>
{trendContext && item.trend !== null && ( <p className="text-xs text-neutral-500 dark:text-neutral-400 mt-0.5">{trendContext}</p>
<p className="text-xs text-neutral-500 dark:text-neutral-400 mt-0.5">{trendContext}</p>
)}
{hasData && ( {hasData && (
<div className="mt-2"> <div className="mt-2">
<Sparkline data={chartData} dataKey={item.id} color={item.color} /> <Sparkline data={chartData} dataKey={item.id} color={item.color} />
@@ -516,7 +528,7 @@ export default function Chart({
<Checkbox <Checkbox
checked={showComparison} checked={showComparison}
onCheckedChange={setShowComparison} onCheckedChange={setShowComparison}
label="Compare with previous period" label="Compare"
/> />
{showComparison && prevPeriodLabel && ( {showComparison && prevPeriodLabel && (
<span className="text-xs text-neutral-500 dark:text-neutral-400"> <span className="text-xs text-neutral-500 dark:text-neutral-400">
@@ -558,9 +570,26 @@ export default function Chart({
<p className="text-xs text-neutral-400 dark:text-neutral-500">Try selecting another metric or date range</p> <p className="text-xs text-neutral-400 dark:text-neutral-500">Try selecting another metric or date range</p>
</div> </div>
) : ( ) : (
<div className="h-[360px] w-full"> <div className="h-[360px] w-full flex flex-row items-stretch gap-2">
<ResponsiveContainer width="100%" height="100%"> {/* * Vertical Y-axis label (text reads bottom-to-top) */}
<AreaChart data={chartData} margin={{ top: 10, right: 8, left: -16, bottom: 0 }}> <div
className="flex items-center justify-center flex-shrink-0"
style={{ width: 28 }}
>
<span
className="text-xs font-medium whitespace-nowrap"
style={{
color: colors.axis,
transform: 'rotate(-90deg)',
transformOrigin: 'center center',
}}
>
{metricLabel}
</span>
</div>
<div className="flex-1 min-h-0 min-w-0">
<ResponsiveContainer width="100%" height="100%">
<AreaChart data={chartData} margin={{ top: 4, right: 8, left: 50, bottom: 0 }}>
<defs> <defs>
<linearGradient id={`gradient-${metric}`} x1="0" y1="0" x2="0" y2="1"> <linearGradient id={`gradient-${metric}`} x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor={activeMetric.color} stopOpacity={0.35} /> <stop offset="0%" stopColor={activeMetric.color} stopOpacity={0.35} />
@@ -584,19 +613,13 @@ export default function Chart({
tickLine={false} tickLine={false}
axisLine={false} axisLine={false}
domain={[0, 'auto']} domain={[0, 'auto']}
width={48}
tickFormatter={(val) => { tickFormatter={(val) => {
if (metric === 'bounce_rate') return `${val}%` if (metric === 'bounce_rate') return `${val}%`
if (metric === 'avg_duration') return formatDuration(val) if (metric === 'avg_duration') return formatAxisDuration(val)
return formatAxisValue(val) return formatAxisValue(val)
}} }}
> />
<Label
value={metricLabel}
position="insideTopLeft"
offset={8}
style={{ fill: colors.axis, fontSize: 11, fontWeight: 500 }}
/>
</YAxis>
<Tooltip <Tooltip
content={(p: TooltipProps<number, string>) => ( content={(p: TooltipProps<number, string>) => (
<ChartTooltip <ChartTooltip
@@ -675,8 +698,9 @@ export default function Chart({
animationDuration={500} animationDuration={500}
animationEasing="ease-out" animationEasing="ease-out"
/> />
</AreaChart> </AreaChart>
</ResponsiveContainer> </ResponsiveContainer>
</div>
</div> </div>
)} )}
</div> </div>