Files
FusionAGI/frontend/src/components/SparklineChart.tsx
Devin AI 0b583cdd07
Some checks failed
CI / lint (pull_request) Failing after 54s
CI / test (3.10) (pull_request) Failing after 30s
CI / test (3.11) (pull_request) Failing after 33s
CI / test (3.12) (pull_request) Successful in 1m7s
CI / docker (pull_request) Has been skipped
Next-level improvements: 15 items across backend, frontend, and testing
Backend:
- SQLiteStateBackend: persistent task/trace storage with SQLite
- InMemoryStateBackend: in-memory impl of StateBackend interface
- Redis cache backend (CacheBackend ABC + MemoryCacheBackend + RedisCacheBackend)
- OpenAI adapter: async acomplete() with retry logic
- Per-tenant + per-IP rate limiting in middleware

Frontend:
- State management: useStore + useAppState (zero-dep, context + reducer)
- React Router integration: URL-based navigation (usePageNavigation)
- WebSocket streaming: sendPrompt + StreamCallbacks for token-by-token updates
- File preview: inline image/text/binary preview with expand/collapse
- Sparkline charts + MetricCard + BarChart for dashboard visualization
- Push notifications hook (useNotifications) with browser Notification API
- i18n system: 6 locales (en, es, fr, de, ja, zh) with interpolation
- 6 new Storybook stories (ChatMessage, Skeleton, Markdown, SearchFilter, Toast, FilePreview)

Testing:
- Playwright E2E config + 6 browser specs (desktop + mobile)
- 18 new Python tests (SQLiteStateBackend, InMemoryStateBackend, cache backends)

570 Python tests + 45 frontend tests = 615 total, 0 ruff errors.

Co-Authored-By: Nakamoto, S <defi@defi-oracle.io>
2026-05-02 03:17:14 +00:00

142 lines
4.0 KiB
TypeScript

/**
* Lightweight SVG sparkline chart component.
*
* Zero-dependency mini chart for rendering inline metrics
* in the Admin and Ethics dashboards.
*/
interface SparklineProps {
data: number[]
width?: number
height?: number
color?: string
fillColor?: string
strokeWidth?: number
showDots?: boolean
label?: string
}
export function Sparkline({
data,
width = 120,
height = 32,
color = 'var(--accent)',
fillColor,
strokeWidth = 1.5,
showDots = false,
label,
}: SparklineProps) {
if (data.length < 2) {
return <svg width={width} height={height} aria-label={label || 'No data'} />
}
const min = Math.min(...data)
const max = Math.max(...data)
const range = max - min || 1
const padding = 2
const points = data.map((val, i) => {
const x = padding + (i / (data.length - 1)) * (width - 2 * padding)
const y = height - padding - ((val - min) / range) * (height - 2 * padding)
return { x, y }
})
const pathD = points.map((p, i) => `${i === 0 ? 'M' : 'L'} ${p.x} ${p.y}`).join(' ')
const fillD = fillColor
? `${pathD} L ${points[points.length - 1].x} ${height} L ${points[0].x} ${height} Z`
: undefined
return (
<svg
width={width}
height={height}
viewBox={`0 0 ${width} ${height}`}
role="img"
aria-label={label || `Sparkline chart with ${data.length} data points`}
>
{fillD && (
<path d={fillD} fill={fillColor} opacity={0.15} />
)}
<path d={pathD} fill="none" stroke={color} strokeWidth={strokeWidth} strokeLinecap="round" strokeLinejoin="round" />
{showDots && points.map((p, i) => (
<circle key={i} cx={p.x} cy={p.y} r={2} fill={color} />
))}
</svg>
)
}
interface MetricCardProps {
title: string
value: string | number
unit?: string
data?: number[]
trend?: 'up' | 'down' | 'flat'
color?: string
}
export function MetricCard({ title, value, unit, data, trend, color = 'var(--accent)' }: MetricCardProps) {
const trendSymbol = trend === 'up' ? '\u2191' : trend === 'down' ? '\u2193' : '\u2192'
const trendColor = trend === 'up' ? 'var(--color-success, #4caf50)' : trend === 'down' ? 'var(--color-error, #f44336)' : 'var(--text-muted)'
return (
<div className="metric-card" role="group" aria-label={title}>
<div className="metric-header">
<span className="metric-title">{title}</span>
{trend && <span className="metric-trend" style={{ color: trendColor }}>{trendSymbol}</span>}
</div>
<div className="metric-value">
<span className="metric-number">{value}</span>
{unit && <span className="metric-unit">{unit}</span>}
</div>
{data && data.length > 1 && (
<Sparkline data={data} color={color} fillColor={color} width={120} height={28} label={`${title} trend`} />
)}
</div>
)
}
interface BarChartProps {
data: { label: string; value: number; color?: string }[]
width?: number
height?: number
barColor?: string
}
export function BarChart({ data, width = 200, height = 60, barColor = 'var(--accent)' }: BarChartProps) {
if (data.length === 0) return null
const max = Math.max(...data.map((d) => d.value)) || 1
const barWidth = Math.max(8, (width - data.length * 4) / data.length)
return (
<svg width={width} height={height + 16} viewBox={`0 0 ${width} ${height + 16}`} role="img" aria-label="Bar chart">
{data.map((d, i) => {
const barHeight = (d.value / max) * (height - 4)
const x = i * (barWidth + 4) + 2
const y = height - barHeight
return (
<g key={i}>
<rect
x={x}
y={y}
width={barWidth}
height={barHeight}
fill={d.color || barColor}
rx={2}
opacity={0.85}
/>
<text
x={x + barWidth / 2}
y={height + 12}
textAnchor="middle"
fontSize={8}
fill="var(--text-muted)"
>
{d.label}
</text>
</g>
)
})}
</svg>
)
}