Python 68.8%
TypeScript 18.6%
CSS 8.7%
JavaScript 3.3%
HTML 0.6%
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