Skip to main content
Compendium / Metrics & Telemetry / Charts / Spark Area
#682

Spark Area

Area-filled mini chart — sparkline plus tinted body, for emphasising volume rather than direction.

STABLE DE
#svg #chart #real-content #dark-mode-tested #responsive
Live
// volume · 12 pts AREA
58 now
Spec

Intent

When the magnitude of a trend matters as much as its slope, an area fill is more legible than a bare line. The fill uses a translucent pillar tint over the same polyline.

Acceptance Criteria

  • [ ] Filled polygon below the trend line
  • [ ] Same polyline stroked over the fill for crisp edge
  • [ ] Auto-scales to min/max
  • [ ] Negative: does NOT use a gradient

Constraints

  • Pure SVG, no JS
  • Fill is the stroke token at opacity 0.18
// spark-area.astro
---
import type { CompendiumMeta } from '../../../data/types';

export const meta: CompendiumMeta = {
  id: 682,
  slug: 'spark-area',
  name: 'Spark Area',
  category: 'metrics-telemetry',
  subcategory: 'charts',
  pillar: 'de',
  description: 'Area-filled mini chart — sparkline plus tinted body, for emphasising volume rather than direction.',
  spec: `## Intent
When the magnitude of a trend matters as much as its slope, an area fill is more legible than a bare line. The fill uses a translucent pillar tint over the same polyline.

## Acceptance Criteria
- [ ] Filled polygon below the trend line
- [ ] Same polyline stroked over the fill for crisp edge
- [ ] Auto-scales to min/max
- [ ] Negative: does NOT use a gradient

## Constraints
- Pure SVG, no JS
- Fill is the stroke token at opacity 0.18`,
  tags: ['svg', 'chart', 'real-content', 'dark-mode-tested', 'responsive'],
  status: 'stable',
  health: { ics: 87, a11y: 'aa', themed: true, animated: false },
};

const series = [22, 28, 26, 35, 31, 40, 44, 38, 47, 45, 52, 58];
const min = Math.min(...series);
const max = Math.max(...series);
const range = max - min || 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 area = `0,100 ${pts} 100,100`;
const current = series.at(-1)!;
---

<div data-this-component class="w-full bg-paper text-black border-4 border-black overflow-hidden">
  <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">// volume · 12 pts</span>
    <span class="font-mono text-[10px] uppercase tracking-widest">AREA</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={`Area trend, currently ${current}`}>
        <polygon points={area} fill="var(--color-de)" opacity="0.18" />
        <polyline points={pts} fill="none" stroke="var(--color-de)" stroke-width="2.5" vector-effect="non-scaling-stroke" />
      </svg>
    </div>
    <div class="p-3 flex flex-col items-center justify-center min-w-[72px]">
      <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>