Skip to content

Commit 0dc0fe4

Browse files
authored
Add interactive version of PMMS dashboard (#1170)
* Add interactive version of PMMS dashboard * Add links to the interactive version and blog post * Add sidebar:false * Add "interactive" to the title to differntiate
1 parent 8738d23 commit 0dc0fe4

File tree

4 files changed

+154
-1
lines changed

4 files changed

+154
-1
lines changed

examples/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,5 @@
1010
- **Google analytics** — Load data from the G.A. API and analyze your website’s traffic. [[code](./google-analytics)] [[live project](https://observablehq.com/framework/examples/google-analytics/)]
1111
- **Hello, world!** — A minimal test. [[code](./hello-world)] [[live project](https://observablehq.com/framework/examples/hello-world/)]
1212
- **Plot** — TypeScript data loaders that access metadata from the Observable Plot Github repo. [[code](./plot)] [[live project](https://observablehq.com/framework/examples/plot/)]
13-
- **Mortgage rates** — A small dashboard reproducing Freddie Mac’s historic mortgage rates dashboard. [[code](./mortgage-rates)] [[live project](https://observablehq.com/framework/examples/mortgage-rates/)]
13+
- **Mortgage rates** — A small dashboard reproducing Freddie Mac’s historic mortgage rates dashboard. [[code](./mortgage-rates)] [[live project](https://observablehq.com/framework/examples/mortgage-rates/)][[interactive version](https://observablehq.com/framework/examples/mortgage-rates/interactive)]
1414
- **Penguin classification** — In a Python data loader, we use logistic regression to predict penguin species based on body size measurements. [[code](./penguin-classification)] [[live project](https://observablehq.com/framework/examples/penguin-classification/)]

examples/mortgage-rates/README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
This is an example Observable Framework project that tracks mortage rates published by Freddie Mac — Federal Home Loan Mortgage Corporation — every week since 1971.
44

55
View the [live project](https://observablehq.com/framework/examples/mortgage-rates/).
6+
View the [interactive version](https://observablehq.com/framework/examples/mortgage-rates/interactive).
67

78
## Data loader
89

@@ -11,3 +12,7 @@ A single TypeScript data loader `docs/data/pmms.csv.ts` fetches the data from Fr
1112
## Charts
1213

1314
The cards and charts reinterpret the original elements of Freddie Mac’s [PMMS dashboard](https://www.freddiemac.com/pmms). We use [Observable Plot](https://observablehq.com/plot/) to draw the charts. The chart code is simple enough to be directly inlined in the page’s markdown `docs/index.md`.
15+
16+
## Interactive version
17+
18+
There is also [an interactive version](https://observablehq.com/framework/examples/mortgage-rates/interactive) that adds control over the year being shown. See [this blog post to read about how it was done](https://observablehq.com/blog/how-to-add-interactivity-observable-framework-dashboard).
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
# Primary mortgage market survey (interactive)
2+
3+
```js
4+
const pmms = FileAttachment("data/pmms.csv").csv({typed: true});
5+
```
6+
7+
```js
8+
const color = Plot.scale({color: {domain: ["30Y FRM", "15Y FRM"]}});
9+
const colorLegend = (y) => html`<span style="border-bottom: solid 2px ${color.apply(`${y}Y FRM`)};">${y}-year fixed-rate</span>`;
10+
```
11+
12+
```js
13+
const tidy = pmms.flatMap(({date, pmms30, pmms15}) => [{date, rate: pmms30, type: "30Y FRM"}, {date, rate: pmms15, type: "15Y FRM"}]);
14+
```
15+
16+
```js
17+
function frmCard(y, pmms) {
18+
const key = `pmms${y}`;
19+
const pmmsSubset = pmms.filter(d => d.date.getFullYear() === selectedYear);
20+
const diff1 = pmmsSubset.at(-1)[key] - pmmsSubset.at(-2)[key];
21+
const diffY = pmmsSubset.at(-1)[key] - pmmsSubset.at(0)[key];
22+
const range = d3.extent(pmmsSubset, (d) => d[key]);
23+
const stroke = color.apply(`${y}Y FRM`);
24+
return html.fragment`
25+
<h2 style="color: ${stroke}">${y}-year fixed-rate in ${selectedYear}</b></h2>
26+
<h1>${formatPercent(pmmsSubset.at(-1)[key])}</h1>
27+
<table>
28+
<tr>
29+
<td>1-week change</td>
30+
<td align="right">${formatPercent(diff1, {signDisplay: "always"})}</td>
31+
<td>${trend(diff1)}</td>
32+
</tr>
33+
<tr>
34+
<td>1-year change</td>
35+
<td align="right">${formatPercent(diffY, {signDisplay: "always"})}</td>
36+
<td>${trend(diffY)}</td>
37+
</tr>
38+
<tr>
39+
<td>4-week average</td>
40+
<td align="right">${formatPercent(d3.mean(pmmsSubset.slice(-4), (d) => d[key]))}</td>
41+
</tr>
42+
<tr>
43+
<td>52-week average</td>
44+
<td align="right">${formatPercent(d3.mean(pmmsSubset, (d) => d[key]))}</td>
45+
</tr>
46+
</table>
47+
${resize((width) =>
48+
Plot.plot({
49+
width,
50+
height: 40,
51+
axis: null,
52+
x: {inset: 40},
53+
marks: [
54+
Plot.tickX(pmmsSubset, {
55+
x: key,
56+
stroke,
57+
insetTop: 10,
58+
insetBottom: 10,
59+
title: (d) => `${d.date?.toLocaleDateString("en-us")}: ${d[key]}%`,
60+
tip: {anchor: "bottom"}
61+
}),
62+
Plot.tickX(pmmsSubset.slice(-1), {x: key, strokeWidth: 2}),
63+
Plot.text([`${range[0]}%`], {frameAnchor: "left"}),
64+
Plot.text([`${range[1]}%`], {frameAnchor: "right"})
65+
]
66+
})
67+
)}
68+
<span class="small muted">52-week range</span>
69+
`;
70+
}
71+
72+
function formatPercent(value, format) {
73+
return value == null
74+
? "N/A"
75+
: (value / 100).toLocaleString("en-US", {minimumFractionDigits: 2, style: "percent", ...format});
76+
}
77+
78+
function trend(v) {
79+
return v >= 0.005 ? html`<span class="green">↗︎</span>`
80+
: v <= -0.005 ? html`<span class="red">↘︎</span>`
81+
: "→";
82+
}
83+
84+
```
85+
86+
Each week, [Freddie Mac](https://www.freddiemac.com/pmms/about-pmms.html) surveys lenders on rates and points for their ${colorLegend(30)}, ${colorLegend(15)}, and other mortgage products. Data as of ${pmms.at(-1).date?.toLocaleDateString("en-US", {year: "numeric", month: "long", day: "numeric"})}.
87+
88+
```js
89+
const selectedYear = view(Inputs.range(d3.extent(pmms, d => d.date.getFullYear()), {label: 'Year:', step: 1, value: 2023}));
90+
```
91+
92+
<style type="text/css">
93+
94+
@container (min-width: 560px) {
95+
.grid-cols-2-3 {
96+
grid-template-columns: 1fr 1fr;
97+
}
98+
.grid-cols-2-3 .grid-colspan-2 {
99+
grid-column: span 2;
100+
}
101+
}
102+
103+
@container (min-width: 840px) {
104+
.grid-cols-2-3 {
105+
grid-template-columns: 1fr 2fr;
106+
grid-auto-flow: column;
107+
}
108+
}
109+
110+
</style>
111+
112+
<div class="grid grid-cols-2-3">
113+
<div class="card">${frmCard(30, pmms)}</div>
114+
<div class="card">${frmCard(15, pmms)}</div>
115+
<div class="card grid-colspan-2 grid-rowspan-2" style="display: flex; flex-direction: column;">
116+
<h2>Rates over the year ${selectedYear}</h2>
117+
<span style="flex-grow: 1;">${resize((width, height) =>
118+
Plot.plot({
119+
width,
120+
height,
121+
y: {grid: true, label: "rate (%)"},
122+
color,
123+
marks: [
124+
Plot.lineY(tidy.filter(d => d.date.getFullYear() === selectedYear), {x: "date", y: "rate", stroke: "type", curve: "step", tip: true, markerEnd: true})
125+
]
126+
})
127+
)}</span>
128+
</div>
129+
</div>
130+
131+
<div class="grid">
132+
<div class="card">
133+
<h2>Rates over all time (${d3.extent(pmms, (d) => d.date.getUTCFullYear()).join("")})</h2>
134+
${resize((width) =>
135+
Plot.plot({
136+
width,
137+
y: {grid: true, label: "rate (%)"},
138+
color,
139+
marks: [
140+
Plot.rectY([{year: selectedYear}], {x1: d => new Date(d.year, 0, 1), x2: d => new Date(d.year+1, 0), y1: 0, y2: d3.max(tidy, d => d.rate), fill: "var(--theme-foreground)", opacity: 0.15}),
141+
Plot.ruleY([0]),
142+
Plot.lineY(tidy, {x: "date", y: "rate", stroke: "type", tip: true})
143+
]
144+
})
145+
)}
146+
</div>
147+
</div>

examples/mortgage-rates/observablehq.config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ export default {
22
title: "Primary mortgage market survey",
33
pager: false,
44
toc: false,
5+
sidebar: false,
56
head:
67
process.env.CI &&
78
`<script type="module" async src="https://events.observablehq.com/client.js"></script>

0 commit comments

Comments
 (0)