Add stat card sparkline graphs with timeseries backend endpoint
New /search/stats/timeseries endpoint returns bucketed counts/metrics over a time window using ClickHouse toStartOfInterval(). Frontend Sparkline component renders SVG polyline + gradient fill on each stat card, driven by a useStatsTimeseries query hook. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
57
ui/src/components/shared/Sparkline.tsx
Normal file
57
ui/src/components/shared/Sparkline.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
import { useMemo, useId } from 'react';
|
||||
|
||||
interface SparklineProps {
|
||||
data: number[];
|
||||
color: string;
|
||||
}
|
||||
|
||||
export function Sparkline({ data, color }: SparklineProps) {
|
||||
const gradientId = useId();
|
||||
|
||||
const { linePath, fillPath } = useMemo(() => {
|
||||
if (data.length < 2) return { linePath: '', fillPath: '' };
|
||||
|
||||
const w = 200;
|
||||
const h = 24;
|
||||
const max = Math.max(...data);
|
||||
const min = Math.min(...data);
|
||||
const range = max - min || 1;
|
||||
const step = w / (data.length - 1);
|
||||
|
||||
const points = data.map(
|
||||
(v, i) => `${i * step},${h - ((v - min) / range) * (h - 2) - 1}`,
|
||||
);
|
||||
|
||||
return {
|
||||
linePath: points.join(' '),
|
||||
fillPath: `0,${h} ${points.join(' ')} ${w},${h}`,
|
||||
};
|
||||
}, [data]);
|
||||
|
||||
if (data.length < 2) return null;
|
||||
|
||||
return (
|
||||
<div style={{ marginTop: 10, height: 24 }}>
|
||||
<svg
|
||||
viewBox="0 0 200 24"
|
||||
preserveAspectRatio="none"
|
||||
style={{ width: '100%', height: '100%' }}
|
||||
>
|
||||
<defs>
|
||||
<linearGradient id={gradientId} x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0%" stopColor={color} stopOpacity={0.3} />
|
||||
<stop offset="100%" stopColor={color} stopOpacity={0} />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<polygon points={fillPath} fill={`url(#${gradientId})`} />
|
||||
<polyline
|
||||
points={linePath}
|
||||
fill="none"
|
||||
stroke={color}
|
||||
strokeWidth="1.5"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,4 +1,13 @@
|
||||
import styles from './shared.module.css';
|
||||
import { Sparkline } from './Sparkline';
|
||||
|
||||
const ACCENT_COLORS: Record<string, string> = {
|
||||
amber: 'var(--amber)',
|
||||
cyan: 'var(--cyan)',
|
||||
rose: 'var(--rose)',
|
||||
green: 'var(--green)',
|
||||
blue: 'var(--blue)',
|
||||
};
|
||||
|
||||
interface StatCardProps {
|
||||
label: string;
|
||||
@@ -6,9 +15,10 @@ interface StatCardProps {
|
||||
accent: 'amber' | 'cyan' | 'rose' | 'green' | 'blue';
|
||||
change?: string;
|
||||
changeDirection?: 'up' | 'down' | 'neutral';
|
||||
sparkData?: number[];
|
||||
}
|
||||
|
||||
export function StatCard({ label, value, accent, change, changeDirection = 'neutral' }: StatCardProps) {
|
||||
export function StatCard({ label, value, accent, change, changeDirection = 'neutral', sparkData }: StatCardProps) {
|
||||
return (
|
||||
<div className={`${styles.statCard} ${styles[accent]}`}>
|
||||
<div className={styles.statLabel}>{label}</div>
|
||||
@@ -16,6 +26,9 @@ export function StatCard({ label, value, accent, change, changeDirection = 'neut
|
||||
{change && (
|
||||
<div className={`${styles.statChange} ${styles[changeDirection]}`}>{change}</div>
|
||||
)}
|
||||
{sparkData && sparkData.length >= 2 && (
|
||||
<Sparkline data={sparkData} color={ACCENT_COLORS[accent] ?? ACCENT_COLORS.amber} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user