Skip to content

Commit 4a75582

Browse files
authored
mortage example edits (#785)
* mortage example edits * close tr * fancier layout * fancier reflow
1 parent 1970db9 commit 4a75582

File tree

3 files changed

+119
-71
lines changed

3 files changed

+119
-71
lines changed

examples/mortgage-rates/docs/index.md

+108-67
Original file line numberDiff line numberDiff line change
@@ -6,94 +6,135 @@ const pmms = FileAttachment("data/pmms.csv").csv({typed: true});
66

77
```js
88
const color = Plot.scale({color: {domain: ["30Y FRM", "15Y FRM"]}});
9-
function colorLegend(y) {
10-
return html`<span style="border-bottom: solid 2px ${color.apply(`${y}Y FRM`)};">${y}-year fixed-rate</span>`;
11-
}
9+
const colorLegend = (y) => html`<span style="border-bottom: solid 2px ${color.apply(`${y}Y FRM`)};">${y}-year fixed-rate</span>`;
1210
```
1311

1412
```js
1513
const tidy = pmms.flatMap(({date, pmms30, pmms15}) => [{date, rate: pmms30, type: "30Y FRM"}, {date, rate: pmms15, type: "15Y FRM"}]);
16-
const recent = tidy.slice(-53 * 2);
1714
```
1815

1916
```js
2017
function frmCard(y, pmms) {
2118
const key = `pmms${y}`;
22-
const fmt = d3.format("+.2");
23-
const fmt2 = d3.format(".2f");
2419
const diff1 = pmms.at(-1)[key] - pmms.at(-2)[key];
2520
const diffY = pmms.at(-1)[key] - pmms.at(-53)[key];
26-
const range = d3.extent(pmms.slice(-52), d => d[key]);
21+
const range = d3.extent(pmms.slice(-52), (d) => d[key]);
2722
const stroke = color.apply(`${y}Y FRM`);
28-
const rangechart = Plot.plot({
29-
style: "overflow: visible;",
30-
width: 300,
31-
height: 40,
32-
axis: null,
33-
x: {inset: 40},
34-
marks: [
35-
Plot.tickX(pmms.slice(-52), {
36-
x: key,
37-
stroke,
38-
insetTop: 10,
39-
insetBottom: 10,
40-
title: (d) => `${d.date?.toLocaleDateString("en-us")}: ${d[key]}%`,
41-
tip: {anchor: "bottom"}
42-
}),
43-
Plot.tickX(pmms.slice(-1), {x: key, strokeWidth: 2}),
44-
Plot.text([`${range[0]}%`], {frameAnchor: "left"}),
45-
Plot.text([`${range[1]}%`], {frameAnchor: "right"})
46-
],
47-
caption: html`<span style="font-size: 11px">52 week range`
48-
});
49-
return html`<div class="card">
50-
<h2 style="color: ${stroke}">${y}Y FRM</b></h2>
51-
<h1 style="opacity: 0.8;">${pmms.at(-1)[key] ?? "N/A"}%</h1>
52-
<table class="table">
53-
<tr><td>1-week change <td>${fmt(diff1)}%<td>${trend(diff1)}
54-
<tr><td>1-year change <td>${fmt(diffY)}%<td>${trend(diffY)}
55-
<tr><td>4-week average <td>${fmt2(d3.mean(pmms.slice(-4), d => d[key]))}%<td>
56-
<tr><td>52-week average <td>${fmt2(d3.mean(pmms.slice(-52), d => d[key]))}%<td>
23+
return html.fragment`
24+
<h2 style="color: ${stroke}">${y}-year fixed-rate</b></h2>
25+
<h1>${formatPercent(pmms.at(-1)[key])}</h1>
26+
<table>
27+
<tr>
28+
<td>1-week change</td>
29+
<td align="right">${formatPercent(diff1, {signDisplay: "always"})}</td>
30+
<td>${trend(diff1)}</td>
31+
</tr>
32+
<tr>
33+
<td>1-year change</td>
34+
<td align="right">${formatPercent(diffY, {signDisplay: "always"})}</td>
35+
<td>${trend(diffY)}</td>
36+
</tr>
37+
<tr>
38+
<td>4-week average</td>
39+
<td align="right">${formatPercent(d3.mean(pmms.slice(-4), (d) => d[key]))}</td>
40+
</tr>
41+
<tr>
42+
<td>52-week average</td>
43+
<td align="right">${formatPercent(d3.mean(pmms.slice(-52), (d) => d[key]))}</td>
44+
</tr>
5745
</table>
58-
${rangechart}
59-
</div>`;
46+
${resize((width) =>
47+
Plot.plot({
48+
width,
49+
height: 40,
50+
axis: null,
51+
x: {inset: 40},
52+
marks: [
53+
Plot.tickX(pmms.slice(-52), {
54+
x: key,
55+
stroke,
56+
insetTop: 10,
57+
insetBottom: 10,
58+
title: (d) => `${d.date?.toLocaleDateString("en-us")}: ${d[key]}%`,
59+
tip: {anchor: "bottom"}
60+
}),
61+
Plot.tickX(pmms.slice(-1), {x: key, strokeWidth: 2}),
62+
Plot.text([`${range[0]}%`], {frameAnchor: "left"}),
63+
Plot.text([`${range[1]}%`], {frameAnchor: "right"})
64+
]
65+
})
66+
)}
67+
<span class="small muted">52-week range</span>
68+
`;
69+
}
70+
71+
function formatPercent(value, format) {
72+
return value == null
73+
? "N/A"
74+
: (value / 100).toLocaleString("en-US", {minimumFractionDigits: 2, style: "percent", ...format});
6075
}
6176
6277
function trend(v) {
63-
if (Math.abs(v) < 0.01) return "";
64-
return v > 0 ? html`<span style="color:steelblue">↗︎` : html`<span style="color:orange">↘︎`;
78+
return v >= 0.005 ? html`<span class="green">↗︎</span>`
79+
: v <= -0.005 ? html`<span class="red">↘︎</span>`
80+
: "→";
6581
}
6682
```
6783
68-
<style>
69-
table.table td:not(:first-child) {text-align:right;}
70-
</style>
84+
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"})}.
7185
72-
> _Each week since April 1971 [Freddie Mac](https://www.freddiemac.com/pmms/about-pmms.html) surveys lenders on the rates and points for their most popular ${colorLegend(30)}, ${colorLegend(15)} and other mortgage products._
86+
<style type="text/css">
7387
74-
<div class="grid grid-cols-2" style="max-width: 672px">${frmCard(30, pmms)} ${frmCard(15, pmms)}</div>
88+
@container (min-width: 560px) {
89+
.grid-cols-2-3 {
90+
grid-template-columns: 1fr 1fr;
91+
}
92+
.grid-cols-2-3 .grid-colspan-2 {
93+
grid-column: span 2;
94+
}
95+
}
96+
97+
@container (min-width: 840px) {
98+
.grid-cols-2-3 {
99+
grid-template-columns: 1fr 2fr;
100+
grid-auto-flow: column;
101+
}
102+
}
75103
76-
<p style="text-align: right; font-style: italic; font-size: smaller;">Data as of ${pmms.at(-1).date?.toLocaleDateString("en-us", {weekday: "long", year: "numeric", month: "short", day: "numeric", timeZone: "UTC"})
77-
}. Source: <a href="https://www.freddiemac.com/pmms">Freddie Mac</a></p>
104+
</style>
78105
79-
<p class="card">${Plot.plot({
80-
title: "Past year",
81-
height: 250,
82-
y: {nice: 5, grid: true, label: "rate (%)"},
83-
color,
84-
marks: [
85-
Plot.lineY(recent, {x: "date", y: "rate", stroke: "type", curve: "step", tip: true, markerEnd: true})
86-
]
87-
})}</p>
106+
<div class="grid grid-cols-2-3" style="margin-top: 2rem;">
107+
<div class="card">${frmCard(30, pmms)}</div>
108+
<div class="card">${frmCard(15, pmms)}</div>
109+
<div class="card grid-colspan-2 grid-rowspan-2" style="display: flex; flex-direction: column;">
110+
<h2>Rates over the past year</h2>
111+
<span style="flex-grow: 1;">${resize((width, height) =>
112+
Plot.plot({
113+
width,
114+
height,
115+
y: {grid: true, label: "rate (%)"},
116+
color,
117+
marks: [
118+
Plot.lineY(tidy.slice(-53 * 2), {x: "date", y: "rate", stroke: "type", curve: "step", tip: true, markerEnd: true})
119+
]
120+
})
121+
)}</span>
122+
</div>
123+
</div>
88124
89-
<p class="card">${
90-
Plot.plot({
91-
title: "Historical data",
92-
x: {nice: 20},
93-
y: {grid: true, label: "rate (%)"},
94-
color,
95-
marks: [
96-
Plot.ruleY([0]),
97-
Plot.lineY(tidy, {x: "date", y: "rate", stroke: "type", tip: true})
98-
]
99-
})}</p>
125+
<div class="grid">
126+
<div class="card">
127+
<h2>Rates over all time (${d3.extent(pmms, (d) => d.date.getUTCFullYear()).join("")})</h2>
128+
${resize((width) =>
129+
Plot.plot({
130+
width,
131+
y: {grid: true, label: "rate (%)"},
132+
color,
133+
marks: [
134+
Plot.ruleY([0]),
135+
Plot.lineY(tidy, {x: "date", y: "rate", stroke: "type", tip: true})
136+
]
137+
})
138+
)}
139+
</div>
140+
</div>

src/client/stdlib/resize.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
// TODO Automatically disconnect the observer when the returned DIV is detached.
22
export function resize(run: (width: number, height: number) => Node, invalidation?: Promise<void>): Node {
33
const div = document.createElement("div");
4-
div.style.cssText = "position: relative; height: 100%;";
4+
div.style.position = "relative";
5+
if (run.length !== 1) div.style.height = "100%";
56
const observer = new ResizeObserver(([entry]) => {
67
const {width, height} = entry.contentRect;
78
while (div.lastChild) div.lastChild.remove();

src/style/grid.css

+9-3
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,12 @@
3030
.grid-cols-4 {
3131
grid-template-columns: repeat(2, minmax(0, 1fr));
3232
}
33-
.grid-colspan-2,
34-
.grid-colspan-3,
35-
.grid-colspan-4 {
33+
.grid-cols-2 .grid-colspan-2,
34+
.grid-cols-2 .grid-colspan-3,
35+
.grid-cols-2 .grid-colspan-4,
36+
.grid-cols-4 .grid-colspan-2,
37+
.grid-cols-4 .grid-colspan-3,
38+
.grid-cols-4 .grid-colspan-4 {
3639
grid-column: span 2;
3740
}
3841
}
@@ -41,6 +44,9 @@
4144
.grid-cols-3 {
4245
grid-template-columns: repeat(3, minmax(0, 1fr));
4346
}
47+
.grid-cols-3 .grid-colspan-2 {
48+
grid-column: span 2;
49+
}
4450
.grid-cols-3 .grid-colspan-3 {
4551
grid-column: span 3;
4652
}

0 commit comments

Comments
 (0)