SPB Git forge

spb/house-ka

Public
18commits 1branches 0releases
1.9 MBsize
maindefault branch
19 days agolast push
Python 67% TypeScript 18.2% CSS 14.4%
4.1 KB · 102 lines tsx
Raw Blame History
1// -----------------------------------------------------------------------------2// House-Ka — Homes-for-sale aggregator (Canada outside Québec, Ontario first)3// Author: Simon-Pierre Boucher — contact@spboucher.ai4// components/RateHistory.tsx : SVG chart of the best observed rate over time5//   (validity periods valid_from/valid_to rebuilt as a step curve — a rate6//   stays in force until it changes).7// -----------------------------------------------------------------------------8import { useEffect, useMemo, useState } from "react";9import { MortgageHistoryRow, fetchMortgageHistory, fmtRate } from "../api";1011/** Best rate (all institutions) at every instant: for each period boundary,12 *  the min of the rates whose period covers that instant. */13function bestCurve(rows: MortgageHistoryRow[], now: number) {14  const stamps = new Set<number>();15  for (const r of rows) {16    stamps.add(r.valid_from);17    if (r.valid_to != null) stamps.add(r.valid_to);18  }19  stamps.add(now);20  const ts = [...stamps].sort((a, b) => a - b);21  const pts: { t: number; rate: number }[] = [];22  for (const t of ts) {23    let best: number | null = null;24    for (const r of rows) {25      if (r.valid_from <= t && (r.valid_to == null || r.valid_to > t))26        best = best == null ? r.rate : Math.min(best, r.rate);27    }28    if (best != null) pts.push({ t, rate: best });29  }30  return pts;31}3233export default function RateHistory({ rateType, termMonths, days = 365 }:34    { rateType: string; termMonths: number; days?: number }) {35  const [rows, setRows] = useState<MortgageHistoryRow[] | null>(null);3637  useEffect(() => {38    setRows(null);39    fetchMortgageHistory(rateType, termMonths, days, "special")40      .then((r) => setRows(r.history))41      .catch(() => setRows([]));42  }, [rateType, termMonths, days]);4344  const now = Math.floor(Date.now() / 1000);45  const pts = useMemo(() => bestCurve(rows ?? [], now), [rows, now]);4647  if (rows == null) return <div className="fine">Loading the history…</div>;48  if (pts.length === 0)49    return <div className="fine">No history for this product yet.</div>;5051  const W = 640, H = 180, PAD = { l: 44, r: 10, t: 10, b: 22 };52  const t0 = pts[0].t, t1 = now;53  const rates = pts.map((p) => p.rate);54  const rMin = Math.floor(Math.min(...rates) * 10) / 10 - 0.1;55  const rMax = Math.ceil(Math.max(...rates) * 10) / 10 + 0.1;56  const x = (t: number) =>57    PAD.l + ((t - t0) / Math.max(1, t1 - t0)) * (W - PAD.l - PAD.r);58  const y = (r: number) =>59    PAD.t + (1 - (r - rMin) / Math.max(0.01, rMax - rMin)) * (H - PAD.t - PAD.b);6061  // step curve: the rate holds until the next change62  let d = `M ${x(pts[0].t).toFixed(1)} ${y(pts[0].rate).toFixed(1)}`;63  for (let i = 1; i < pts.length; i++) {64    d += ` H ${x(pts[i].t).toFixed(1)} V ${y(pts[i].rate).toFixed(1)}`;65  }66  d += ` H ${x(t1).toFixed(1)}`;6768  const yTicks: number[] = [];69  for (let r = Math.ceil(rMin * 4) / 4; r <= rMax + 1e-9; r += 0.25)70    yTicks.push(Math.round(r * 100) / 100);71  const fmtD = (t: number) =>72    new Date(t * 1000).toLocaleDateString("en-CA", { month: "short", day: "numeric" });73  const last = pts[pts.length - 1];7475  return (76    <div className="mtg-chart">77      <svg viewBox={`0 0 ${W} ${H}`} role="img"78           aria-label={`Best ${rateType} ${termMonths}-month rate over time`}>79        {yTicks.map((r) => (80          <g key={r}>81            <line x1={PAD.l} x2={W - PAD.r} y1={y(r)} y2={y(r)} className="mtg-grid" />82            <text x={PAD.l - 6} y={y(r) + 3} className="mtg-tick" textAnchor="end">83              {r.toFixed(2)}84            </text>85          </g>86        ))}87        <text x={x(t0)} y={H - 6} className="mtg-tick">{fmtD(t0)}</text>88        <text x={W - PAD.r} y={H - 6} className="mtg-tick" textAnchor="end">89          today90        </text>91        <path d={d} className="mtg-line" />92        <circle cx={x(t1)} cy={y(last.rate)} r={3.5} className="mtg-dot" />93      </svg>94      <div className="fine">95        Best “special offer” rate observed across all institutions —96        currently <b>{fmtRate(last.rate)}</b>. The history builds up as the97        collections run (no data is extrapolated).98      </div>99    </div>100  );101}102