SPB Git forge

spb/rent-ka

Public
8commits 1branches 0releases
7.4 MBsize
maindefault branch
19 days agolast push
Python 68.8% TypeScript 18.6% CSS 8.7% JavaScript 3.3% HTML 0.6%
5.1 KB · 111 lines tsx
Raw Blame History
1// -----------------------------------------------------------------------------2// Rent-Ka — Rental listings aggregator (Canada, outside Québec)3// Author: Simon-Pierre Boucher — contact@spboucher.ai4// components/PriceAnalysis.tsx: "Rent-Ka price analysis" block (detail page)5//   Estimated fair value + range + deviation + confidence + mini histogram6//   of the rent's position in the market segment distribution.7//   Indicative estimate computed over comparable listings (fairvalue.py).8// -----------------------------------------------------------------------------9import { useEffect, useState } from "react";10import { Link } from "react-router-dom";11import { FairValueDetail, fetchFairValue, fmtPrice } from "../api";12import FairValueBadge, { fmtDeviation } from "./FairValueBadge";1314const CONF_LABEL = { fort: "High confidence", moyen: "Medium confidence",15                     faible: "Indicative estimate" } as const;1617/** Mini histogram: segment rent distribution, text markers for "this18 *  rent" and the fair value (never colour alone). */19function MiniHisto({ d, price }: { d: FairValueDetail; price: number }) {20  const bins = d.histogram;21  if (!bins || bins.length < 4) return null;22  const W = 320, H = 96, top = 18, bottom = 16;23  const lo = bins[0].x0, hi = bins[bins.length - 1].x1;24  if (hi <= lo) return null;25  const max = Math.max(...bins.map((b) => b.n), 1);26  const x = (v: number) => ((Math.min(Math.max(v, lo), hi) - lo) / (hi - lo)) * W;27  const bw = W / bins.length;28  const plotH = H - top - bottom;29  const priceX = x(price), fvX = x(d.fv);30  // spread the labels if both markers are close31  const close = Math.abs(priceX - fvX) < 64;32  const lblAnchor = (px: number) => (px < 56 ? "start" : px > W - 56 ? "end" : "middle");33  return (34    <svg className="fv-histo" viewBox={`0 0 ${W} ${H}`} role="img"35         aria-label={`Position of the rent (${fmtPrice(price)}) among ${d.segment_n} comparable listings`}>36      {bins.map((b, i) => {37        const h = Math.max(1.5, (b.n / max) * plotH);38        const inBin = price >= b.x0 && price < b.x1;39        return (40          <rect key={i} x={i * bw + 1} y={H - bottom - h} rx="2"41                width={Math.max(1, bw - 2)} height={h}42                fill={inBin ? "var(--accent, #2456e6)" : "rgba(204, 85, 0, 0.28)"}>43            <title>{`$${b.x0} – $${b.x1}: ${b.n} listing${b.n > 1 ? "s" : ""}`}</title>44          </rect>45        );46      })}47      {/* fair-value range (band) */}48      <rect x={x(d.fv_low)} y={H - bottom} width={Math.max(2, x(d.fv_high) - x(d.fv_low))}49            height="3.5" rx="1.5" fill="rgba(204, 85, 0, 0.45)" />50      {/* fair-value marker */}51      <line x1={fvX} x2={fvX} y1={top - 2} y2={H - bottom} stroke="var(--accent-deep, #1738a8)"52            strokeWidth="1.6" strokeDasharray="3 3" />53      {!close && (54        <text x={fvX} y={top - 7} textAnchor={lblAnchor(fvX)}55              className="fv-histo-lbl fv-histo-lbl-fv">Fair value</text>56      )}57      {/* asking-rent marker */}58      <line x1={priceX} x2={priceX} y1={top - 2} y2={H - bottom}59            stroke="var(--ink, #141814)" strokeWidth="2" />60      <text x={priceX} y={close ? top - 7 : H - 4} textAnchor={lblAnchor(priceX)}61            className="fv-histo-lbl">This rent{close ? " / fair value" : ""}</text>62      {/* axis bounds */}63      <text x="1" y={H - 4} className="fv-histo-axis" textAnchor="start">${lo}</text>64      <text x={W - 1} y={H - 4} className="fv-histo-axis" textAnchor="end">${hi}</text>65    </svg>66  );67}6869export default function PriceAnalysis({ uid, price }: { uid: string; price: number | null }) {70  const [d, setD] = useState<FairValueDetail | null>(null);71  useEffect(() => {72    setD(null);73    fetchFairValue(uid).then(setD).catch(() => setD(null));74  }, [uid]);75  if (!d || price == null) return null;7677  const pct = fmtDeviation(d.deviation);78  return (79    <section className="f-bloc f-fairvalue" id="analyse-prix">80      <h2>Rent-Ka price analysis</h2>81      <div className="fv-head">82        <div>83          <div className="fv-value">{fmtPrice(d.fv)} <small>/ month</small></div>84          <div className="fv-range">85            Estimated fair value · range {fmtPrice(d.fv_low)} – {fmtPrice(d.fv_high)}86          </div>87        </div>88        <FairValueBadge verdict={d.verdict} deviation={d.deviation} />89      </div>90      {d.verdict == null && (91        <p className="fine">92          {CONF_LABEL[d.confidence]} — not enough reliable comparables to93          classify this rent; the estimate is provided as an indication.94        </p>95      )}96      <MiniHisto d={d} price={price} />97      <div className="fv-meta">98        {pct && <span>Deviation: <b>{pct}</b> vs fair value</span>}99        <span>{CONF_LABEL[d.confidence]}</span>100        <span>{d.segment_n.toLocaleString("en-CA")} comparable listings</span>101        {d.comps > 0 && <span>{d.comps} neighbours kept</span>}102      </div>103      <p className="fine">104        Indicative estimate computed continuously from comparable market105        listings — not an official appraisal.{" "}106        <Link to="/fair-value">How is fair value calculated?</Link>107      </p>108    </section>109  );110}111