// ----------------------------------------------------------------------------- // Rent-Ka — Rental listings aggregator (Canada, outside Québec) // Author: Simon-Pierre Boucher — contact@spboucher.ai // components/PriceAnalysis.tsx: "Rent-Ka price analysis" block (detail page) // Estimated fair value + range + deviation + confidence + mini histogram // of the rent's position in the market segment distribution. // Indicative estimate computed over comparable listings (fairvalue.py). // ----------------------------------------------------------------------------- import { useEffect, useState } from "react"; import { Link } from "react-router-dom"; import { FairValueDetail, fetchFairValue, fmtPrice } from "../api"; import FairValueBadge, { fmtDeviation } from "./FairValueBadge"; const CONF_LABEL = { fort: "High confidence", moyen: "Medium confidence", faible: "Indicative estimate" } as const; /** Mini histogram: segment rent distribution, text markers for "this * rent" and the fair value (never colour alone). */ function MiniHisto({ d, price }: { d: FairValueDetail; price: number }) { const bins = d.histogram; if (!bins || bins.length < 4) return null; const W = 320, H = 96, top = 18, bottom = 16; const lo = bins[0].x0, hi = bins[bins.length - 1].x1; if (hi <= lo) return null; const max = Math.max(...bins.map((b) => b.n), 1); const x = (v: number) => ((Math.min(Math.max(v, lo), hi) - lo) / (hi - lo)) * W; const bw = W / bins.length; const plotH = H - top - bottom; const priceX = x(price), fvX = x(d.fv); // spread the labels if both markers are close const close = Math.abs(priceX - fvX) < 64; const lblAnchor = (px: number) => (px < 56 ? "start" : px > W - 56 ? "end" : "middle"); return ( {bins.map((b, i) => { const h = Math.max(1.5, (b.n / max) * plotH); const inBin = price >= b.x0 && price < b.x1; return ( {`$${b.x0} – $${b.x1}: ${b.n} listing${b.n > 1 ? "s" : ""}`} ); })} {/* fair-value range (band) */} {/* fair-value marker */} {!close && ( Fair value )} {/* asking-rent marker */} This rent{close ? " / fair value" : ""} {/* axis bounds */} ${lo} ${hi} ); } export default function PriceAnalysis({ uid, price }: { uid: string; price: number | null }) { const [d, setD] = useState(null); useEffect(() => { setD(null); fetchFairValue(uid).then(setD).catch(() => setD(null)); }, [uid]); if (!d || price == null) return null; const pct = fmtDeviation(d.deviation); return (

Rent-Ka price analysis

{fmtPrice(d.fv)} / month
Estimated fair value · range {fmtPrice(d.fv_low)} – {fmtPrice(d.fv_high)}
{d.verdict == null && (

{CONF_LABEL[d.confidence]} — not enough reliable comparables to classify this rent; the estimate is provided as an indication.

)}
{pct && Deviation: {pct} vs fair value} {CONF_LABEL[d.confidence]} {d.segment_n.toLocaleString("en-CA")} comparable listings {d.comps > 0 && {d.comps} neighbours kept}

Indicative estimate computed continuously from comparable market listings — not an official appraisal.{" "} How is fair value calculated?

); }