← back to Stayclaim

src/components/CurveSparkline.tsx

88 lines

/**
 * CurveSparkline — server-rendered SVG of the LA 1978-1984 cycle.
 * Reads the same data file the projection uses so the anchor and the
 * picture cannot drift apart.
 */
import fs from 'node:fs';
import path from 'node:path';

type Point = { year: number; value: number; yoy_pct: number | null };

let cached: Point[] | null = null;
function loadPriceYoy(): Point[] | null {
  if (cached) return cached;
  const p = path.resolve(process.cwd(), 'data/multiplier_1978_1984.json');
  if (!fs.existsSync(p)) return null;
  const j = JSON.parse(fs.readFileSync(p, 'utf8'));
  cached = j.price_yoy as Point[];
  return cached;
}

export function CurveSparkline({ height = 64, className = '' }: { height?: number; className?: string }) {
  const series = loadPriceYoy();
  if (!series || series.length < 2) return null;

  const w = 320;
  const h = height;
  const pad = 6;
  const xs = series.map(p => p.year);
  const ys = series.map(p => p.value);
  const minX = Math.min(...xs);
  const maxX = Math.max(...xs);
  const minY = Math.min(...ys);
  const maxY = Math.max(...ys);
  const range = Math.max(1, maxY - minY);

  const x = (year: number) => pad + ((year - minX) / (maxX - minX)) * (w - 2 * pad);
  const y = (val: number) => h - pad - ((val - minY) / range) * (h - 2 * pad);

  const path = series.map((p, i) => `${i === 0 ? 'M' : 'L'} ${x(p.year).toFixed(1)} ${y(p.value).toFixed(1)}`).join(' ');

  return (
    <figure className={`mt-3 ${className}`}>
      <svg
        viewBox={`0 0 ${w} ${h}`}
        width="100%"
        height={h}
        role="img"
        aria-label="LA County home-price index, 1978–1984"
        className="overflow-visible"
      >
        {/* Y baseline */}
        <line
          x1={pad}
          x2={w - pad}
          y1={h - pad}
          y2={h - pad}
          stroke="#1a141015"
          strokeWidth={1}
        />
        {/* curve */}
        <path d={path} fill="none" stroke="#4a5d45" strokeWidth={1.5} />
        {/* points */}
        {series.map(p => (
          <circle key={p.year} cx={x(p.year)} cy={y(p.value)} r={2.2} fill="#4a5d45" />
        ))}
        {/* year labels */}
        {series.map(p => (
          <text
            key={`l-${p.year}`}
            x={x(p.year)}
            y={h - 2}
            fontSize={9}
            textAnchor="middle"
            fill="#1a141060"
            fontFamily="IBM Plex Mono, monospace"
            style={{ letterSpacing: 0.5 }}
          >
            {String(p.year).slice(2)}
          </text>
        ))}
      </svg>
      <figcaption className="font-mono text-[10px] text-ink/50 mt-1">
        LA County HPI · {minX}–{maxX} · FRED ATNHPIUS06037A
      </figcaption>
    </figure>
  );
}