Skip to main content
Compendium / Metrics & Telemetry / Charts / ICS Sparkline
#057

ICS Sparkline

Single-line trend of the Intent Completion Score over time, with min/max ticks and a current readout.

STABLE DE
#svg #chart #real-content
Live
// ICS · 12 revisions TREND
min 42 max 85
85 now
Spec

Intent

Show how a spec's ICS has evolved across revisions. The trend matters more than any single value — a flat 65 is healthier than a volatile 78.

Acceptance Criteria

  • [ ] Sparkline plots ICS across N revisions
  • [ ] Min and max ticks labelled on the line
  • [ ] Current readout shown large to the side
  • [ ] Negative: does NOT show absolute zero baseline (sparkline auto-scales)

Constraints

  • Pure SVG, no JS
  • Tabular-nums for the readout
// ics-sparkline.astro
---
import type { CompendiumMeta } from '../../../data/types';

export const meta: CompendiumMeta = {
  id: 57,
  slug: 'ics-sparkline',
  name: 'ICS Sparkline',
  category: 'metrics-telemetry',
  subcategory: 'charts',
  pillar: 'de',
  description: 'Single-line trend of the Intent Completion Score over time, with min/max ticks and a current readout.',
  spec: `## Intent
Show how a spec's ICS has evolved across revisions. The trend matters more than any single value — a flat 65 is healthier than a volatile 78.

## Acceptance Criteria
- [ ] Sparkline plots ICS across N revisions
- [ ] Min and max ticks labelled on the line
- [ ] Current readout shown large to the side
- [ ] Negative: does NOT show absolute zero baseline (sparkline auto-scales)

## Constraints
- Pure SVG, no JS
- Tabular-nums for the readout`,
  tags: ['svg', 'chart', 'real-content'],
  status: 'stable',
  health: { ics: 86, a11y: 'aa', themed: true, animated: false },
};

const series = [42, 51, 58, 54, 63, 71, 68, 74, 79, 82, 78, 85];
const min = Math.min(...series);
const max = Math.max(...series);
const range = max - min || 1;
const current = series.at(-1)!;
const n = series.length - 1;
const xy = series.map((v, i) => ({
  x: (i / n) * 100,
  y: 100 - ((v - min) / range) * 100,
}));
const pts = xy.map((p) => `${p.x},${p.y}`).join(' ');
const minIdx = series.indexOf(min);
const maxIdx = series.indexOf(max);
---

<div class="w-full bg-paper text-black border-4 border-black">
  <header class="flex items-baseline justify-between px-3 py-2 border-b-2 border-black">
    <span class="font-mono text-[10px] uppercase tracking-widest opacity-60">// ICS · 12 revisions</span>
    <span class="font-mono text-[10px] uppercase tracking-widest">TREND</span>
  </header>
  <div class="grid grid-cols-[1fr_auto]">
    <div class="p-3 border-e-2 border-black">
      <svg viewBox="0 0 100 100" preserveAspectRatio="none" class="w-full h-20 block" aria-label={`ICS trend, currently ${current}`}>
        <polyline points={pts} fill="none" stroke="var(--color-de)" stroke-width="2.5" vector-effect="non-scaling-stroke" />
        <circle cx={xy[minIdx].x} cy={xy[minIdx].y} r="2.5" fill="var(--color-attention)" />
        <circle cx={xy[maxIdx].x} cy={xy[maxIdx].y} r="2.5" fill="var(--color-success)" />
        <line x1="100" y1="0" x2="100" y2="100" stroke="var(--color-ink)" stroke-width="0.5" stroke-dasharray="2 2" vector-effect="non-scaling-stroke" />
      </svg>
      <div class="mt-2 flex justify-between font-mono text-[9px] uppercase tracking-widest">
        <span class="opacity-60">min <span class="font-black text-black tabular-nums">{min}</span></span>
        <span class="opacity-60">max <span class="font-black text-black tabular-nums">{max}</span></span>
      </div>
    </div>
    <div class="p-3 flex flex-col items-center justify-center min-w-[80px]">
      <span class="font-black text-3xl tabular-nums leading-none">{current}</span>
      <span class="font-mono text-[9px] uppercase tracking-widest opacity-60 mt-1">now</span>
    </div>
  </div>
</div>