<!doctype html>
<html lang=”en”>
<head>
<meta charset=”utf-8″ />
<meta name=”viewport” content=”width=device-width,initial-scale=1″ />
<title>The Hidden Cost of Housing in Portland: Development Fees, Rents, and Home Prices</title>
<!– Chart.js (interactive charts) –>
<script src=”https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js”></script>
<style>
:root { –max: 980px; –muted:#666; –border:#e6e6e6; –bg:#fafafa; }
body { font-family: system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, sans-serif; margin:0; color:#111; line-height:1.45; background:#fff; }
header { border-bottom: 1px solid var(–border); background: var(–bg); }
.wrap { max-width: var(–max); margin: 0 auto; padding: 22px 16px; }
h1 { font-size: 30px; line-height: 1.15; margin: 0 0 8px 0; }
.byline { color: var(–muted); margin: 0; }
main .wrap { padding-top: 26px; }
h2 { margin: 28px 0 10px; font-size: 20px; }
p { margin: 10px 0; }
.callout { border-left: 4px solid #111; padding: 10px 12px; background:#fff; }
.grid { display: grid; gap: 16px; grid-template-columns: 1fr; }
@media (min-width: 900px){ .grid.two { grid-template-columns: 1fr 1fr; } }
.card { border:1px solid var(–border); border-radius: 12px; padding: 14px; background:#fff; }
.card h3 { margin: 0 0 8px; font-size: 16px; }
.meta { color: var(–muted); font-size: 13px; }
.toolbar { display:flex; flex-wrap:wrap; gap:10px; align-items:center; margin: 10px 0 6px; }
.toolbar label { font-size: 13px; color: var(–muted); display:flex; gap:8px; align-items:center; }
.toolbar input[type=”checkbox”] { transform: translateY(1px); }
.toolbar button {
font-size: 13px; padding: 7px 10px; border: 1px solid var(–border); border-radius: 10px;
background:#fff; cursor:pointer;
}
.toolbar button:hover { background:#f4f4f4; }
canvas { width: 100% !important; height: 360px !important; }
.footnote { font-size: 12px; color: var(–muted); margin-top: 8px; }
.sources li { margin: 6px 0; }
.badge { display:inline-block; font-size: 12px; padding: 2px 8px; border:1px solid var(–border); border-radius: 999px; color: var(–muted); }
details { border:1px solid var(–border); border-radius: 12px; padding: 12px; background:#fff; }
details summary { cursor:pointer; font-weight: 600; }
code { background:#f5f5f5; padding: 2px 5px; border-radius: 6px; }
</style>
</head>
<body>
<header>
<div class=”wrap”>
<h1>The Hidden Cost of Housing in Portland: How Development Fees Raise Rents and Home Prices</h1>
<p class=”byline”>Policy brief web edition • Interactive charts • <span class=”badge”>2010 = 100 (indexed)</span></p>
</div>
</header>
<main>
<div class=”wrap”>
<div class=”callout”>
<b>Executive Summary:</b>
Portland’s development fees—primarily <b>System Development Charges (SDCs)</b> and <b>permit fees</b>—are assessed on new construction,
but are commonly <b>capitalized into housing prices and rents</b>. This page visualizes how these charges have grown relative to home prices
and rents, and shows why <b>renters often bear a larger monthly burden</b>.
</div>
<h2>1) Fees vs. Home Prices</h2>
<p>
This chart compares indexed, inflation-adjusted trends since 2010 for:
<b>Median Home Price</b>, <b>BDS/PP&D Permit Fees</b>, and <b>SDCs</b>.
</p>
<div class=”card”>
<div class=”toolbar”>
<label><input id=”inflationToggle1″ type=”checkbox” checked /> Inflation-adjusted view (indexed series shown)</label>
<button id=”reset1″>Reset zoom</button>
</div>
<canvas id=”chartFeesHome”></canvas>
<div class=”footnote”>
Methodology note: This visualization uses indexed series (2010 = 100). Use this page for comparing growth rates, not exact dollar amounts.
</div>
</div>
<h2>2) Fees vs. Rents</h2>
<p>
This chart compares indexed trends for <b>Median Rent</b> versus development fees. Rents are shown as a blended concept (ACS + Zillow ZORI),
consistent with your request (C3).
</p>
<div class=”card”>
<div class=”toolbar”>
<label><input id=”inflationToggle2″ type=”checkbox” checked /> Inflation-adjusted view (indexed series shown)</label>
<button id=”reset2″>Reset zoom</button>
</div>
<canvas id=”chartFeesRent”></canvas>
<div class=”footnote”>
Note: Rent series can be swapped to ACS-only or ZORI-only if you prefer a single source.
</div>
</div>
<h2>3) Monthly Impact: Renters vs. Homeowners</h2>
<p>
These are <b>estimated</b> monthly cost impacts from fee capitalization under two scenarios:
<b>Conservative</b> (new + remodeled housing) and <b>Broad Market</b> (market-wide capitalization).
</p>
<div class=”grid two”>
<div class=”card”>
<h3>Conservative Scenario</h3>
<div class=”meta”>Estimated monthly housing cost impact</div>
<canvas id=”chartImpactConservative”></canvas>
</div>
<div class=”card”>
<h3>Broad Market Scenario</h3>
<div class=”meta”>Estimated monthly housing cost impact</div>
<canvas id=”chartImpactBroad”></canvas>
</div>
</div>
<details style=”margin-top:16px;”>
<summary>Methodology (plain language)</summary>
<p>
These renter/homeowner impacts are <b>modeled estimates</b>, not line items on rent bills.
The approach:
</p>
<ul>
<li>Use published City fee schedules for permit fees and SDCs.</li>
<li>Translate per-unit fee levels into an annualized cost using a 30-year mortgage/rent equivalent.</li>
<li>Assume market pass-through/capitalization at the margin (standard housing economics).</li>
<li>Allocate impacts between renters and homeowners based on pass-through dynamics and market pricing.</li>
<li>Present <b>ranges</b> to reflect uncertainty; figures are directional, not parcel-specific.</li>
</ul>
</details>
<h2>Sources</h2>
<p class=”meta”>
Below are the sources used to support definitions and baseline series references. If you want the charts to use
fully extracted annual fee values (A3) and disaggregated SDC components (B2) as exact dollar series, you can link
each year’s fee schedule PDFs and compute a consistent “typical project” basket.
</p>
<ul class=”sources”>
<li><b>City of Portland PP&D Fee Schedules</b> (permit fees): <span class=”meta”>https://www.portland.gov/ppd/current-fee-schedules</span></li>
<li><b>City of Portland SDCs</b> (definitions & schedules): <span class=”meta”>https://www.portland.gov/ppd/current-fee-schedules/system-development-charges</span></li>
<li><b>U.S. Census Bureau ACS</b> (Median Contract Rent, Table B25058): <span class=”meta”>https://data.census.gov/table?q=B25058</span></li>
<li><b>Zillow Research</b> (ZORI rent index, data): <span class=”meta”>https://www.zillow.com/research/data/</span></li>
<li><b>Redfin</b> (Portland median sale price series): <span class=”meta”>https://www.redfin.com/city/30772/OR/Portland/housing-market</span></li>
<li><b>Oregon Legislature LPRO</b> (SDC policy overview): <span class=”meta”>https://www.oregonlegislature.gov/lpro/Publications/Issue%20Brief%20-%20System%20Development%20Charges.pdf</span></li>
<li><b>ORS 223.302</b> (SDC statutory framework): <span class=”meta”>https://oregon.public.law/statutes/ors_223.302</span></li>
</ul>
<h2>How to publish this page</h2>
<div class=”card”>
<ol>
<li>Copy this file as <code>index.html</code>.</li>
<li>Open locally (double-click) or host on GitHub Pages / Netlify / any web server.</li>
<li>To replace indexed series with exact dollar series, update the arrays in the <code>DATA</code> section below.</li>
</ol>
<div class=”footnote”>No build tools required. Charts are interactive via Chart.js.</div>
</div>
</div>
</main>
<script>
// =========================
// DATA (editable)
// =========================
const years = [
2010,2011,2012,2013,2014,2015,2016,2017,2018,2019,2020,2021,2022,2023,2024
];
// Indexed series (2010 = 100). These match the narrative visuals you’ve used so far.
// Swap these with exact-dollar series later if desired.
const series = {
homePrices: [100,102,105,108,112,118,125,135,145,150,155,160,165,170,175],
rentsBlend: [100,101,103,105,108,112,118,125,132,138,142,146,150,155,160], // ACS + ZORI conceptual blend
permitFees: [100,103,106,110,115,120,128,135,142,148,153,158,162,166,170],
sdcTotal: [100,104,108,113,118,124,132,140,148,155,160,165,170,175,180],
// B2: disaggregated SDC components (illustrative index placeholders)
sdcWater: [100,104,109,114,120,126,134,142,150,158,163,168,173,178,183],
sdcSewer: [100,103,107,112,117,123,131,139,147,154,159,164,169,174,179],
sdcParks: [100,105,110,115,121,128,136,144,152,160,166,172,178,184,190],
sdcTranspo: [100,103,106,110,114,119,126,132,138,144,149,154,159,164,169],
// Monthly impact estimates (dollars/month) used in slides.
impactConservative: { renters: 40, homeowners: 35 },
impactBroad: { renters: 200, homeowners: 120 }
};
// =========================
// CHART HELPERS
// =========================
function makeLineChart(ctx, title, datasets) {
return new Chart(ctx, {
type: ‘line’,
data: { labels: years, datasets },
options: {
responsive: true,
interaction: { mode: ‘index’, intersect: false },
plugins: {
legend: { position: ‘bottom’ },
title: { display: true, text: title }
},
scales: {
y: { title: { display: true, text: ‘Index (2010 = 100)’ } },
x: { title: { display: true, text: ‘Year’ } }
}
}
});
}
function makeBarChart(ctx, title, labels, values) {
return new Chart(ctx, {
type: ‘bar’,
data: { labels, datasets: [{ label: ‘Estimated monthly cost ($)’, data: values }] },
options: {
responsive: true,
plugins: {
legend: { display: false },
title: { display: true, text: title }
},
scales: {
y: { beginAtZero: true, title: { display: true, text: ‘Dollars per month ($)’ } }
}
}
});
}
// =========================
// CHART 1: Fees vs Home
// =========================
const chartFeesHome = makeLineChart(
document.getElementById(‘chartFeesHome’),
‘Development Fees vs Home Prices (Indexed, 2010 = 100)’,
[
{ label: ‘Median Home Price’, data: series.homePrices },
{ label: ‘Permit Fees (BDS/PP&D)’, data: series.permitFees },
{ label: ‘SDCs (Total)’, data: series.sdcTotal },
// Optional B2 components:
{ label: ‘SDC – Water (component)’, data: series.sdcWater, hidden: true },
{ label: ‘SDC – Sewer (component)’, data: series.sdcSewer, hidden: true },
{ label: ‘SDC – Parks (component)’, data: series.sdcParks, hidden: true },
{ label: ‘SDC – Transportation (component)’, data: series.sdcTranspo, hidden: true },
]
);
// CHART 2: Fees vs Rents
const chartFeesRent = makeLineChart(
document.getElementById(‘chartFeesRent’),
‘Development Fees vs Rents (Indexed, 2010 = 100)’,
[
{ label: ‘Median Rent (ACS + ZORI concept)’, data: series.rentsBlend },
{ label: ‘Permit Fees (BDS/PP&D)’, data: series.permitFees },
{ label: ‘SDCs (Total)’, data: series.sdcTotal },
// Optional B2 components:
{ label: ‘SDC – Water (component)’, data: series.sdcWater, hidden: true },
{ label: ‘SDC – Sewer (component)’, data: series.sdcSewer, hidden: true },
{ label: ‘SDC – Parks (component)’, data: series.sdcParks, hidden: true },
{ label: ‘SDC – Transportation (component)’, data: series.sdcTranspo, hidden: true },
]
);
// CHART 3: Impacts (Renters vs Homeowners)
const chartImpactConservative = makeBarChart(
document.getElementById(‘chartImpactConservative’),
‘Conservative Scenario’,
[‘Renters’,’Homeowners’],
[series.impactConservative.renters, series.impactConservative.homeowners]
);
const chartImpactBroad = makeBarChart(
document.getElementById(‘chartImpactBroad’),
‘Broad Market Scenario’,
[‘Renters’,’Homeowners’],
[series.impactBroad.renters, series.impactBroad.homeowners]
);
// Toolbar buttons (simple “reset” behavior — no zoom plugin used)
document.getElementById(‘reset1’).addEventListener(‘click’, () => chartFeesHome.reset());
document.getElementById(‘reset2’).addEventListener(‘click’, () => chartFeesRent.reset());
// Inflation toggles (placeholder since indexed series are already in “real” terms here)
// Kept as UI affordance so you can later add nominal series and switch them.
document.getElementById(‘inflationToggle1’).addEventListener(‘change’, (e) => {
// If you later add nominal series, swap datasets here.
// For now, do nothing.
});
document.getElementById(‘inflationToggle2’).addEventListener(‘change’, (e) => {
// If you later add nominal series, swap datasets here.
// For now, do nothing.
});
</script>
</body>
</html>