<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Morrison CO · Comp Analysis</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Bebas+Neue&family=DM+Mono:ital,wght@0,300;0,400;0,500;1,300&family=DM+Sans:wght@300;400;500;600&display=swap" rel="stylesheet">
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.4.1/chart.umd.min.js"></script>
<style>
:root {
--bg: #0a0b0d;
--surface: #111318;
--surface2: #181c24;
--border: #1f2530;
--border2: #2a3142;
--gold: #c9a84c;
--gold-dim: rgba(201,168,76,0.15);
--gold-glow: rgba(201,168,76,0.06);
--green: #3ecf8e;
--red: #f26b6b;
--blue: #5b8cf5;
--muted: #4a5568;
--muted2: #6b7a92;
--text: #e8ecf4;
--text-dim: #9aa5b8;
--mono: 'DM Mono', monospace;
--sans: 'DM Sans', sans-serif;
--display: 'Bebas Neue', sans-serif;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
background: var(--bg);
color: var(--text);
font-family: var(--sans);
min-height: 100vh;
overflow-x: hidden;
}
/* ── HEADER ── */
header {
padding: 48px 40px 32px;
border-bottom: 1px solid var(--border);
position: relative;
background: linear-gradient(180deg, rgba(201,168,76,0.04) 0%, transparent 100%);
}
.header-grid {
max-width: 1400px;
margin: 0 auto;
display: flex;
align-items: flex-end;
justify-content: space-between;
gap: 24px;
flex-wrap: wrap;
}
.eyebrow {
font-family: var(--mono);
font-size: 11px;
letter-spacing: 0.18em;
color: var(--gold);
text-transform: uppercase;
margin-bottom: 10px;
}
h1 {
font-family: var(--display);
font-size: clamp(40px, 6vw, 72px);
letter-spacing: 0.04em;
line-height: 0.92;
color: var(--text);
}
h1 span { color: var(--gold); }
.header-meta {
text-align: right;
font-family: var(--mono);
font-size: 11px;
color: var(--muted2);
line-height: 1.8;
}
.lot-warning {
max-width: 1400px;
margin: 20px auto 0;
background: rgba(242,107,107,0.08);
border: 1px solid rgba(242,107,107,0.25);
border-radius: 6px;
padding: 10px 16px;
font-family: var(--mono);
font-size: 11px;
color: #f26b6b;
letter-spacing: 0.05em;
}
.lot-warning strong { font-weight: 500; }
/* ── LAYOUT ── */
main {
max-width: 1400px;
margin: 0 auto;
padding: 40px 40px 80px;
}
/* ── SECTION TITLE ── */
.section-label {
font-family: var(--mono);
font-size: 10px;
letter-spacing: 0.2em;
color: var(--gold);
text-transform: uppercase;
padding-bottom: 12px;
border-bottom: 1px solid var(--border);
margin-bottom: 24px;
}
/* ── SUMMARY STATS ── */
.summary-row {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
gap: 1px;
background: var(--border);
border: 1px solid var(--border);
border-radius: 10px;
overflow: hidden;
margin-bottom: 48px;
}
.stat-box {
background: var(--surface);
padding: 20px 24px;
transition: background 0.2s;
}
.stat-box:hover { background: var(--surface2); }
.stat-label {
font-family: var(--mono);
font-size: 10px;
letter-spacing: 0.12em;
color: var(--muted2);
text-transform: uppercase;
margin-bottom: 8px;
}
.stat-value {
font-family: var(--display);
font-size: 28px;
letter-spacing: 0.03em;
color: var(--text);
}
.stat-sub {
font-family: var(--mono);
font-size: 10px;
color: var(--muted);
margin-top: 4px;
}
/* ── CARDS GRID ── */
.cards-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(310px, 1fr));
gap: 16px;
margin-bottom: 48px;
}
.card {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 12px;
overflow: hidden;
transition: border-color 0.25s, transform 0.2s;
cursor: default;
position: relative;
}
.card:hover {
border-color: var(--border2);
transform: translateY(-2px);
}
.card.best-value { border-color: rgba(201,168,76,0.4); }
.card.best-value .card-header { background: linear-gradient(135deg, rgba(201,168,76,0.12), transparent); }
.badge {
position: absolute;
top: 12px;
right: 12px;
font-family: var(--mono);
font-size: 9px;
letter-spacing: 0.12em;
text-transform: uppercase;
padding: 4px 8px;
border-radius: 4px;
font-weight: 500;
}
.badge-gold { background: var(--gold-dim); color: var(--gold); border: 1px solid rgba(201,168,76,0.3); }
.badge-green { background: rgba(62,207,142,0.12); color: var(--green); border: 1px solid rgba(62,207,142,0.3); }
.badge-active { background: rgba(91,140,245,0.12); color: var(--blue); border: 1px solid rgba(91,140,245,0.3); }
.card-header {
padding: 20px 20px 16px;
border-bottom: 1px solid var(--border);
}
.card-address {
font-family: var(--display);
font-size: 19px;
letter-spacing: 0.03em;
line-height: 1.1;
color: var(--text);
margin-bottom: 6px;
padding-right: 70px;
}
.card-price {
font-family: var(--mono);
font-size: 20px;
color: var(--gold);
font-weight: 500;
letter-spacing: 0.02em;
}
.card-price-sub {
font-family: var(--mono);
font-size: 10px;
color: var(--muted2);
margin-top: 2px;
}
.card-body { padding: 16px 20px; }
.metrics-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
margin-bottom: 16px;
}
.metric {
background: var(--surface2);
border-radius: 8px;
padding: 12px;
}
.metric-label {
font-family: var(--mono);
font-size: 9px;
color: var(--muted2);
letter-spacing: 0.1em;
text-transform: uppercase;
margin-bottom: 4px;
}
.metric-value {
font-family: var(--mono);
font-size: 16px;
color: var(--text);
font-weight: 500;
}
.metric-value.highlight { color: var(--gold); }
.metric-value.green { color: var(--green); }
/* ── BED/BATH PILLS ── */
.bb-row {
display: flex;
gap: 8px;
align-items: center;
margin-bottom: 14px;
}
.pill {
font-family: var(--mono);
font-size: 11px;
padding: 5px 12px;
border-radius: 20px;
display: flex;
align-items: center;
gap: 5px;
font-weight: 400;
}
.pill-bed { background: rgba(91,140,245,0.12); color: var(--blue); border: 1px solid rgba(91,140,245,0.25); }
.pill-bath { background: rgba(62,207,142,0.1); color: var(--green); border: 1px solid rgba(62,207,142,0.2); }
.pill-yr { background: var(--surface2); color: var(--muted2); border: 1px solid var(--border); }
/* ── SQFT BAR ── */
.sqft-section { margin-bottom: 14px; }
.sqft-label-row {
display: flex;
justify-content: space-between;
margin-bottom: 5px;
font-family: var(--mono);
font-size: 10px;
color: var(--muted2);
}
.sqft-track {
height: 6px;
background: var(--border);
border-radius: 4px;
overflow: hidden;
margin-bottom: 4px;
position: relative;
}
.sqft-fill-total {
height: 100%;
border-radius: 4px;
background: var(--border2);
position: absolute;
top: 0; left: 0;
}
.sqft-fill-finished {
height: 100%;
border-radius: 4px;
background: var(--gold);
position: absolute;
top: 0; left: 0;
transition: width 0.8s ease;
}
.sqft-fill-above {
height: 100%;
border-radius: 4px;
position: absolute;
top: 0; left: 0;
background: var(--blue);
opacity: 0.6;
}
/* ── FAMILY SCORE ── */
.family-score-row {
display: flex;
align-items: center;
gap: 10px;
padding: 12px;
background: var(--surface2);
border-radius: 8px;
border: 1px solid var(--border);
}
.score-label {
font-family: var(--mono);
font-size: 9px;
color: var(--muted2);
letter-spacing: 0.1em;
text-transform: uppercase;
flex: 1;
}
.score-bar-track {
flex: 3;
height: 4px;
background: var(--border);
border-radius: 4px;
overflow: hidden;
}
.score-bar-fill {
height: 100%;
background: linear-gradient(90deg, var(--gold), var(--green));
border-radius: 4px;
transition: width 0.8s ease;
}
.score-num {
font-family: var(--mono);
font-size: 13px;
font-weight: 500;
color: var(--gold);
width: 28px;
text-align: right;
}
/* ── CHARTS ── */
.charts-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
margin-bottom: 48px;
}
@media (max-width: 900px) { .charts-grid { grid-template-columns: 1fr; } }
.chart-card {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 12px;
padding: 24px;
}
.chart-title {
font-family: var(--mono);
font-size: 11px;
letter-spacing: 0.15em;
color: var(--gold);
text-transform: uppercase;
margin-bottom: 20px;
}
canvas { max-height: 260px; }
/* ── COMPARISON TABLE ── */
.table-wrap {
overflow-x: auto;
border: 1px solid var(--border);
border-radius: 12px;
margin-bottom: 48px;
}
table {
width: 100%;
border-collapse: collapse;
font-family: var(--mono);
font-size: 12px;
}
thead tr {
background: var(--surface2);
border-bottom: 1px solid var(--border2);
}
th {
padding: 14px 16px;
text-align: left;
font-size: 10px;
letter-spacing: 0.12em;
color: var(--muted2);
text-transform: uppercase;
white-space: nowrap;
}
th:first-child { color: var(--gold); }
tbody tr {
border-bottom: 1px solid var(--border);
transition: background 0.15s;
}
tbody tr:hover { background: var(--surface2); }
tbody tr:last-child { border-bottom: none; }
td {
padding: 13px 16px;
white-space: nowrap;
color: var(--text-dim);
}
td:first-child { color: var(--text); font-weight: 500; }
.td-gold { color: var(--gold) !important; }
.td-green { color: var(--green) !important; }
.td-best { background: rgba(201,168,76,0.06); }
.td-worst { opacity: 0.6; }
/* ── LEGEND ── */
.legend-row {
display: flex;
gap: 20px;
flex-wrap: wrap;
margin-bottom: 48px;
font-family: var(--mono);
font-size: 11px;
color: var(--muted2);
}
.legend-item { display: flex; align-items: center; gap: 8px; }
.legend-dot {
width: 10px; height: 10px;
border-radius: 50%;
}
/* ── FOOTER ── */
footer {
border-top: 1px solid var(--border);
padding: 24px 40px;
max-width: 1400px;
margin: 0 auto;
font-family: var(--mono);
font-size: 10px;
color: var(--muted);
display: flex;
justify-content: space-between;
flex-wrap: wrap;
gap: 12px;
}
@media (max-width: 640px) {
header, main, footer { padding-left: 20px; padding-right: 20px; }
.charts-grid { grid-template-columns: 1fr; }
h1 { font-size: 36px; }
}
/* ── ANIMATIONS ── */
@keyframes fadeUp {
from { opacity: 0; transform: translateY(16px); }
to { opacity: 1; transform: translateY(0); }
}
.card { animation: fadeUp 0.4s ease both; }
.card:nth-child(1) { animation-delay: 0.05s; }
.card:nth-child(2) { animation-delay: 0.1s; }
.card:nth-child(3) { animation-delay: 0.15s; }
.card:nth-child(4) { animation-delay: 0.2s; }
.card:nth-child(5) { animation-delay: 0.25s; }
.card:nth-child(6) { animation-delay: 0.3s; }
.card:nth-child(7) { animation-delay: 0.35s; }
</style>
</head>
<body>
<header>
<div class="header-grid">
<div>
<div class="eyebrow">Competitive Market Analysis · Morrison, CO</div>
<h1>HOME COMP<br><span>ANALYSIS</span></h1>
</div>
<div class="header-meta">
7 SFR PROPERTIES<br>
PRICE RANGE: $900K–$1.24M<br>
GENERATED: MARCH 2026<br>
SOURCE: MLS DATA
</div>
</div>
<div class="lot-warning" style="margin-top:20px;">
<strong>⚠ LOT SIZE NOT AVAILABLE:</strong> Lot size column was not included in the MLS export. Add manually to this analysis or re-run with that column included for a complete picture.
</div>
</header>
<main>
<!-- SUMMARY STATS -->
<div class="section-label">Market Summary</div>
<div class="summary-row" id="summaryRow"></div>
<!-- CARDS -->
<div class="section-label" style="margin-top:48px;">Property Profiles · Side-by-Side</div>
<div class="legend-row">
<div class="legend-item"><div class="legend-dot" style="background:var(--blue)"></div> Above Grade sqft</div>
<div class="legend-item"><div class="legend-dot" style="background:var(--gold)"></div> Finished sqft</div>
<div class="legend-item"><div class="legend-dot" style="background:var(--border2)"></div> Total sqft</div>
<div class="legend-item">🏆 Best Value = lowest $/finished sqft</div>
<div class="legend-item">★ Family Score = composite (beds, baths, finished sqft, year built)</div>
</div>
<div class="cards-grid" id="cardsGrid"></div>
<!-- CHARTS -->
<div class="section-label">Visual Analytics</div>
<div class="charts-grid">
<div class="chart-card">
<div class="chart-title">Price vs. Finished SQFT</div>
<canvas id="chartPriceVsSqft"></canvas>
</div>
<div class="chart-card">
<div class="chart-title">$/Finished SQFT · Value Ranking</div>
<canvas id="chartPpsf"></canvas>
</div>
<div class="chart-card">
<div class="chart-title">SQFT Breakdown · Above / Finished / Total</div>
<canvas id="chartSqft"></canvas>
</div>
<div class="chart-card">
<div class="chart-title">Family Usability Score (0–100)</div>
<canvas id="chartFamily"></canvas>
</div>
</div>
<!-- TABLE -->
<div class="section-label">Full Comparison Table</div>
<div class="table-wrap">
<table id="compTable">
<thead>
<tr>
<th>Address</th>
<th>Price</th>
<th>Status</th>
<th>Year</th>
<th>Bed</th>
<th>Bath</th>
<th>Above Grade</th>
<th>Finished SQFT</th>
<th>Total SQFT</th>
<th>Unfinished</th>
<th>$/Fin SQFT</th>
<th>Fin %</th>
<th>Family Score</th>
</tr>
</thead>
<tbody id="compTableBody"></tbody>
</table>
</div>
</main>
<footer>
<span>Nick Ahrens · The Apollo Group at eXp Realty · youranthemhome.com</span>
<span>Data sourced from MLS export · For professional use only</span>
</footer>
<script>
const homes = [
{ id: 1, address: "8722 S Ault Lane", price: 1050000, status: "Active", days: 3, year: 1998, bed: 5, bath: 4, above: 2956, fin: 4388, ttl: 4388, closeDate: null },
{ id: 2, address: "19736 Flint Lane", price: 1085000, status: "Closed", days: 2, year: 1978, bed: 3, bath: 3, above: 2993, fin: 2993, ttl: 2993, closeDate: "08/22" },
{ id: 3, address: "20425 Flint Lane", price: 965000, status: "Closed", days: 4, year: 1971, bed: 5, bath: 3, above: 2356, fin: 2356, ttl: 2356, closeDate: "05/12" },
{ id: 4, address: "8220 Iowa Gulch Rd", price: 900000, status: "Closed", days: 7, year: 1996, bed: 4, bath: 3, above: 1992, fin: 3108, ttl: 3308, closeDate: "03/05" },
{ id: 5, address: "20525 Flint Lane", price: 1225000, status: "Closed", days: 12, year: 2020, bed: 3, bath: 3, above: 1780, fin: 2612, ttl: 2612, closeDate: "04/24" },
{ id: 6, address: "8783 S Ault Lane", price: 1030000, status: "Closed", days: 25, year: 1972, bed: 4, bath: 4, above: 2776, fin: 3216, ttl: 3216, closeDate: "04/30" },
{ id: 7, address: "8782 S Ault Lane", price: 1240000, status: "Closed", days: 28, year: 1978, bed: 4, bath: 3, above: 3280, fin: 3280, ttl: 3280, closeDate: "06/30" },
];
// ── Computed fields
homes.forEach(h => {
h.ppsf = Math.round(h.price / h.fin);
h.unfin = h.ttl - h.fin;
h.finPct = Math.round((h.fin / h.ttl) * 100);
// Family score: beds (max 5→25pts), bath (max 4→20pts), fin sqft (max 4388→30pts), newness (max 2020→25pts)
const bedScore = Math.min(h.bed / 5, 1) * 25;
const bathScore = Math.min(h.bath / 4, 1) * 20;
const sqftScore = Math.min(h.fin / 4388, 1) * 30;
const yrScore = Math.min((h.year - 1965) / (2020 - 1965), 1) * 25;
h.familyScore = Math.round(bedScore + bathScore + sqftScore + yrScore);
});
const maxFin = Math.max(...homes.map(h => h.fin));
const minPpsf = Math.min(...homes.map(h => h.ppsf));
const shortAddr = a => a.split(" ").slice(0, -1).join(" ") + "\n" + a.split(" ").pop();
const fmtK = v => "$" + (v/1000).toFixed(0) + "K";
const fmtN = v => v.toLocaleString();
const colors = ["#c9a84c","#5b8cf5","#3ecf8e","#e88c3c","#a66cf0","#5ec8e5","#f26b6b"];
// ── SUMMARY ROW
const prices = homes.map(h=>h.price);
const medianPpsf = [...homes].sort((a,b)=>a.ppsf-b.ppsf)[Math.floor(homes.length/2)].ppsf;
const summaryStats = [
{ label: "Properties", value: homes.length, sub: "All SFR, Morrison CO" },
{ label: "Price Range", value: "$900K–$1.24M", sub: "Spread: $340K" },
{ label: "Avg Price", value: fmtK(prices.reduce((a,b)=>a+b)/prices.length), sub: "7 comparable sales" },
{ label: "Median $/Fin SQFT", value: "$"+medianPpsf, sub: "Range: $"+Math.min(...homes.map(h=>h.ppsf))+"–$"+Math.max(...homes.map(h=>h.ppsf)) },
{ label: "Largest Fin SQFT", value: fmtN(maxFin)+" sf", sub: "8722 S Ault Ln" },
{ label: "Best Family Score", value: Math.max(...homes.map(h=>h.familyScore))+"/100", sub: homes.sort((a,b)=>b.familyScore-a.familyScore)[0].address.split(",")[0] },
];
// re-sort back
homes.sort((a,b)=>a.id-b.id);
const sr = document.getElementById("summaryRow");
summaryStats.forEach(s => {
sr.innerHTML += `<div class="stat-box">
<div class="stat-label">${s.label}</div>
<div class="stat-value">${s.value}</div>
<div class="stat-sub">${s.sub}</div>
</div>`;
});
// ── CARDS
const grid = document.getElementById("cardsGrid");
homes.forEach((h, i) => {
const isBestValue = h.ppsf === minPpsf;
const isActive = h.status === "Active";
const isLargest = h.fin === maxFin;
let badge = "";
if (isBestValue) badge = `<div class="badge badge-gold">🏆 Best Value</div>`;
else if (isActive) badge = `<div class="badge badge-active">◉ Active</div>`;
else if (isLargest) badge = `<div class="badge badge-green">↑ Largest</div>`;
const finWidth = (h.fin / maxFin * 100).toFixed(1);
const abvWidth = (h.above / maxFin * 100).toFixed(1);
const ttlWidth = (h.ttl / maxFin * 100).toFixed(1);
grid.innerHTML += `
<div class="card${isBestValue ? " best-value" : ""}">
${badge}
<div class="card-header">
<div class="card-address">${h.address}</div>
<div class="card-price">${fmtK(h.price)}</div>
<div class="card-price-sub">${h.status === "Active" ? "◉ Active · "+h.days+" DOM" : "Closed "+h.closeDate+" · "+h.days+" DOM"}</div>
</div>
<div class="card-body">
<div class="bb-row">
<div class="pill pill-bed">🛏 ${h.bed} Bed</div>
<div class="pill pill-bath">🚿 ${h.bath} Bath</div>
<div class="pill pill-yr">⚒ ${h.year}</div>
</div>
<div class="metrics-grid">
<div class="metric">
<div class="metric-label">Finished SQFT</div>
<div class="metric-value highlight">${fmtN(h.fin)}</div>
</div>
<div class="metric">
<div class="metric-label">Above Grade</div>
<div class="metric-value">${fmtN(h.above)}</div>
</div>
<div class="metric">
<div class="metric-label">$/Fin SQFT</div>
<div class="metric-value${h.ppsf === minPpsf ? " green" : ""}">$${fmtN(h.ppsf)}</div>
</div>
<div class="metric">
<div class="metric-label">Unfinished</div>
<div class="metric-value">${fmtN(h.unfin)} sf</div>
</div>
</div>
<div class="sqft-section">
<div class="sqft-label-row">
<span>SQFT Breakdown</span>
<span>${h.finPct}% finished</span>
</div>
<div class="sqft-track">
<div class="sqft-fill-total" style="width:${ttlWidth}%"></div>
<div class="sqft-fill-finished" style="width:${finWidth}%"></div>
<div class="sqft-fill-above" style="width:${abvWidth}%"></div>
</div>
<div class="sqft-label-row" style="margin-top:3px">
<span style="color:var(--blue)">■ Above: ${fmtN(h.above)}</span>
<span style="color:var(--gold)">■ Fin: ${fmtN(h.fin)}</span>
<span style="color:var(--muted)">■ Tot: ${fmtN(h.ttl)}</span>
</div>
</div>
<div class="family-score-row">
<div class="score-label">Family Score</div>
<div class="score-bar-track">
<div class="score-bar-fill" style="width:${h.familyScore}%"></div>
</div>
<div class="score-num">${h.familyScore}</div>
</div>
</div>
</div>`;
});
// ── TABLE
const tbody = document.getElementById("compTableBody");
const minPpsfVal = Math.min(...homes.map(h=>h.ppsf));
const maxFamScore = Math.max(...homes.map(h=>h.familyScore));
homes.forEach(h => {
const isBest = h.ppsf === minPpsfVal;
const isTopFam = h.familyScore === maxFamScore;
tbody.innerHTML += `<tr>
<td>${h.address}</td>
<td class="td-gold">$${fmtN(h.price)}</td>
<td>${h.status}</td>
<td>${h.year}</td>
<td>${h.bed}</td>
<td>${h.bath}</td>
<td>${fmtN(h.above)}</td>
<td>${fmtN(h.fin)}</td>
<td>${fmtN(h.ttl)}</td>
<td>${h.unfin > 0 ? fmtN(h.unfin) : "—"}</td>
<td class="${isBest ? "td-green" : ""}">$${fmtN(h.ppsf)}</td>
<td>${h.finPct}%</td>
<td class="${isTopFam ? "td-gold" : ""}">${h.familyScore}</td>
</tr>`;
});
// ── CHART DEFAULTS
Chart.defaults.color = "#6b7a92";
Chart.defaults.borderColor = "#1f2530";
Chart.defaults.font.family = "'DM Mono', monospace";
Chart.defaults.font.size = 10;
const labels = homes.map(h => h.address.split(" ").slice(0,3).join(" "));
// Chart 1: Scatter — Price vs Finished SQFT
new Chart(document.getElementById("chartPriceVsSqft"), {
type: "scatter",
data: {
datasets: homes.map((h, i) => ({
label: labels[i],
data: [{ x: h.fin, y: h.price / 1000 }],
backgroundColor: colors[i] + "cc",
borderColor: colors[i],
pointRadius: 9,
pointHoverRadius: 12,
}))
},
options: {
responsive: true,
plugins: {
legend: { display: false },
tooltip: {
callbacks: {
label: ctx => {
const h = homes[ctx.datasetIndex];
return [`${h.address}`, `Price: $${fmtN(h.price)}`, `Fin SQFT: ${fmtN(h.fin)}`];
}
}
}
},
scales: {
x: { title: { display: true, text: "Finished SQFT", color: "#6b7a92" }, grid: { color: "#1f2530" } },
y: { title: { display: true, text: "Price ($K)", color: "#6b7a92" }, grid: { color: "#1f2530" } }
}
}
});
// Chart 2: $/Fin SQFT horizontal bar
const ppsfSorted = [...homes].sort((a,b) => a.ppsf - b.ppsf);
new Chart(document.getElementById("chartPpsf"), {
type: "bar",
data: {
labels: ppsfSorted.map(h => h.address.split(" ").slice(0,3).join(" ")),
datasets: [{
label: "$/Fin SQFT",
data: ppsfSorted.map(h => h.ppsf),
backgroundColor: ppsfSorted.map(h => h.ppsf === minPpsf ? "#c9a84c" : "#2a3142"),
borderColor: ppsfSorted.map(h => h.ppsf === minPpsf ? "#c9a84c" : "#3a4555"),
borderWidth: 1,
borderRadius: 4,
}]
},
options: {
indexAxis: "y",
responsive: true,
plugins: { legend: { display: false } },
scales: {
x: { grid: { color: "#1f2530" }, ticks: { callback: v => "$"+v } },
y: { grid: { display: false } }
}
}
});
// Chart 3: Grouped bar — SQFT breakdown
new Chart(document.getElementById("chartSqft"), {
type: "bar",
data: {
labels,
datasets: [
{ label: "Above Grade", data: homes.map(h=>h.above), backgroundColor: "rgba(91,140,245,0.6)", borderRadius: 3 },
{ label: "Finished", data: homes.map(h=>h.fin), backgroundColor: "rgba(201,168,76,0.7)", borderRadius: 3 },
{ label: "Total", data: homes.map(h=>h.ttl), backgroundColor: "rgba(42,49,66,0.8)", borderRadius: 3 },
]
},
options: {
responsive: true,
plugins: {
legend: { labels: { boxWidth: 10, padding: 16 } }
},
scales: {
x: { grid: { display: false }, ticks: { maxRotation: 40, minRotation: 30 } },
y: { grid: { color: "#1f2530" }, ticks: { callback: v => fmtN(v) } }
}
}
});
// Chart 4: Family score radar-style bar
new Chart(document.getElementById("chartFamily"), {
type: "bar",
data: {
labels,
datasets: [{
label: "Family Score",
data: homes.map(h=>h.familyScore),
backgroundColor: homes.map(h => h.familyScore === maxFamScore ? "#c9a84c" : "#2a3142"),
borderColor: homes.map(h => h.familyScore === maxFamScore ? "#c9a84c" : "#3a4555"),
borderWidth: 1,
borderRadius: 4,
}]
},
options: {
responsive: true,
plugins: {
legend: { display: false },
tooltip: {
callbacks: {
afterLabel: ctx => {
const h = homes[ctx.dataIndex];
return [`${h.bed}bd / ${h.bath}ba · ${fmtN(h.fin)} fin sqft · ${h.year}`];
}
}
}
},
scales: {
x: { grid: { display: false }, ticks: { maxRotation: 40, minRotation: 30 } },
y: { max: 100, grid: { color: "#1f2530" } }
}
}
});
</script>
</body>
</html>