f2a12f9fed
v0.4 (Operator Tier — Cohort Dashboard + Auth + Postgres) milestone complete. Phases: ✓ P0 pre-execution (planning) → v0.1.6 ✓ P1 operator foundation (Postgres+auth+VC migration) → v0.1.7 ✓ P2 cohort dashboard + aggregation → v0.1.8 ✓ P3 final review + ship → v0.1.9 (= v0.4 milestone release) Requirements covered (8/8): REQ-MT-01 (Postgres store), REQ-MT-02 (aggregation pipeline), REQ-AUTH-01 (operator auth), REQ-DASH-01 (cohort dashboard), REQ-NFR-AUTH-01 (auth NFRs), REQ-NFR-MT-01 (Postgres-in-LXC), REQ-NFR-DASH-01 (k-anonymity ≥10), REQ-NFR-DASH-02 (freshness ≤24h) Grill MUSTs honored (6/6): G-008, G-011, G-027, G-031, G-038, G-041 Tests: 317 pytest pass, 36 skip (Postgres-requiring), 0 fail; 17/17 vitest pass Review: APPROVE_WITH_NOTES (6/6 personas, 0 P0, 8 P1+ carry-forward) Audit: HEALTHY (reconstruction PASS, 8/8 REQ, 6/6 grill) ---ci--- project: praxis phase: 3 milestone: v0.4 status: complete phase_role: final milestone_complete: true milestone_merged_to_main: true tag: v0.1.9 requirements: covered: [REQ-MT-01, REQ-MT-02, REQ-AUTH-01, REQ-DASH-01, REQ-NFR-AUTH-01, REQ-NFR-MT-01, REQ-NFR-DASH-01, REQ-NFR-DASH-02] partial: [] ---/ci---
49 lines
1.4 KiB
TypeScript
49 lines
1.4 KiB
TypeScript
/**
|
|
* Inline SVG sparkline (SLICE-09 TASK-09-05, RESEARCH-v0.4 §4.3).
|
|
*
|
|
* Zero-dep ~50 LOC. Renders a polyline from `data`. Handles empty (renders
|
|
* nothing), single point (dot), all-same (flat line). stroke=currentColor.
|
|
* No axes/tooltips — sparklines are compact trend indicators.
|
|
*/
|
|
interface SparklineProps {
|
|
data: number[]
|
|
width?: number
|
|
height?: number
|
|
}
|
|
|
|
export default function Sparkline({ data, width = 60, height = 20 }: SparklineProps) {
|
|
if (!data || data.length === 0) {
|
|
return null
|
|
}
|
|
if (data.length === 1) {
|
|
return (
|
|
<svg width={width} height={height} viewBox={`0 0 ${width} ${height}`} aria-hidden="true">
|
|
<circle cx={width / 2} cy={height / 2} r={1.5} fill="currentColor" />
|
|
</svg>
|
|
)
|
|
}
|
|
const min = Math.min(...data)
|
|
const max = Math.max(...data)
|
|
const span = max - min || 1
|
|
const pad = 2
|
|
const w = width - pad * 2
|
|
const h = height - pad * 2
|
|
const stepX = w / (data.length - 1)
|
|
const points = data.map((v, i) => {
|
|
const x = pad + i * stepX
|
|
const y = pad + h - ((v - min) / span) * h
|
|
return `${x.toFixed(2)},${y.toFixed(2)}`
|
|
})
|
|
return (
|
|
<svg width={width} height={height} viewBox={`0 0 ${width} ${height}`} aria-hidden="true">
|
|
<polyline
|
|
points={points.join(' ')}
|
|
fill="none"
|
|
stroke="currentColor"
|
|
strokeWidth={1.25}
|
|
strokeLinejoin="round"
|
|
strokeLinecap="round"
|
|
/>
|
|
</svg>
|
|
)
|
|
} |