Skip to content

Commit

Permalink
mortage example edits (#785)
Browse files Browse the repository at this point in the history
* mortage example edits

* close tr

* fancier layout

* fancier reflow
  • Loading branch information
mbostock authored Feb 14, 2024
1 parent 1970db9 commit 4a75582
Show file tree
Hide file tree
Showing 3 changed files with 119 additions and 71 deletions.
175 changes: 108 additions & 67 deletions examples/mortgage-rates/docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,94 +6,135 @@ const pmms = FileAttachment("data/pmms.csv").csv({typed: true});

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

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

```js
function frmCard(y, pmms) {
const key = `pmms${y}`;
const fmt = d3.format("+.2");
const fmt2 = d3.format(".2f");
const diff1 = pmms.at(-1)[key] - pmms.at(-2)[key];
const diffY = pmms.at(-1)[key] - pmms.at(-53)[key];
const range = d3.extent(pmms.slice(-52), d => d[key]);
const range = d3.extent(pmms.slice(-52), (d) => d[key]);
const stroke = color.apply(`${y}Y FRM`);
const rangechart = Plot.plot({
style: "overflow: visible;",
width: 300,
height: 40,
axis: null,
x: {inset: 40},
marks: [
Plot.tickX(pmms.slice(-52), {
x: key,
stroke,
insetTop: 10,
insetBottom: 10,
title: (d) => `${d.date?.toLocaleDateString("en-us")}: ${d[key]}%`,
tip: {anchor: "bottom"}
}),
Plot.tickX(pmms.slice(-1), {x: key, strokeWidth: 2}),
Plot.text([`${range[0]}%`], {frameAnchor: "left"}),
Plot.text([`${range[1]}%`], {frameAnchor: "right"})
],
caption: html`<span style="font-size: 11px">52 week range`
});
return html`<div class="card">
<h2 style="color: ${stroke}">${y}Y FRM</b></h2>
<h1 style="opacity: 0.8;">${pmms.at(-1)[key] ?? "N/A"}%</h1>
<table class="table">
<tr><td>1-week change <td>${fmt(diff1)}%<td>${trend(diff1)}
<tr><td>1-year change <td>${fmt(diffY)}%<td>${trend(diffY)}
<tr><td>4-week average <td>${fmt2(d3.mean(pmms.slice(-4), d => d[key]))}%<td>
<tr><td>52-week average <td>${fmt2(d3.mean(pmms.slice(-52), d => d[key]))}%<td>
return html.fragment`
<h2 style="color: ${stroke}">${y}-year fixed-rate</b></h2>
<h1>${formatPercent(pmms.at(-1)[key])}</h1>
<table>
<tr>
<td>1-week change</td>
<td align="right">${formatPercent(diff1, {signDisplay: "always"})}</td>
<td>${trend(diff1)}</td>
</tr>
<tr>
<td>1-year change</td>
<td align="right">${formatPercent(diffY, {signDisplay: "always"})}</td>
<td>${trend(diffY)}</td>
</tr>
<tr>
<td>4-week average</td>
<td align="right">${formatPercent(d3.mean(pmms.slice(-4), (d) => d[key]))}</td>
</tr>
<tr>
<td>52-week average</td>
<td align="right">${formatPercent(d3.mean(pmms.slice(-52), (d) => d[key]))}</td>
</tr>
</table>
${rangechart}
</div>`;
${resize((width) =>
Plot.plot({
width,
height: 40,
axis: null,
x: {inset: 40},
marks: [
Plot.tickX(pmms.slice(-52), {
x: key,
stroke,
insetTop: 10,
insetBottom: 10,
title: (d) => `${d.date?.toLocaleDateString("en-us")}: ${d[key]}%`,
tip: {anchor: "bottom"}
}),
Plot.tickX(pmms.slice(-1), {x: key, strokeWidth: 2}),
Plot.text([`${range[0]}%`], {frameAnchor: "left"}),
Plot.text([`${range[1]}%`], {frameAnchor: "right"})
]
})
)}
<span class="small muted">52-week range</span>
`;
}
function formatPercent(value, format) {
return value == null
? "N/A"
: (value / 100).toLocaleString("en-US", {minimumFractionDigits: 2, style: "percent", ...format});
}
function trend(v) {
if (Math.abs(v) < 0.01) return "";
return v > 0 ? html`<span style="color:steelblue">↗︎` : html`<span style="color:orange">↘︎`;
return v >= 0.005 ? html`<span class="green">↗︎</span>`
: v <= -0.005 ? html`<span class="red">↘︎</span>`
: "→";
}
```
<style>
table.table td:not(:first-child) {text-align:right;}
</style>
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"})}.
> _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._
<style type="text/css">
<div class="grid grid-cols-2" style="max-width: 672px">${frmCard(30, pmms)} ${frmCard(15, pmms)}</div>
@container (min-width: 560px) {
.grid-cols-2-3 {
grid-template-columns: 1fr 1fr;
}
.grid-cols-2-3 .grid-colspan-2 {
grid-column: span 2;
}
}
@container (min-width: 840px) {
.grid-cols-2-3 {
grid-template-columns: 1fr 2fr;
grid-auto-flow: column;
}
}
<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"})
}. Source: <a href="https://www.freddiemac.com/pmms">Freddie Mac</a></p>
</style>
<p class="card">${Plot.plot({
title: "Past year",
height: 250,
y: {nice: 5, grid: true, label: "rate (%)"},
color,
marks: [
Plot.lineY(recent, {x: "date", y: "rate", stroke: "type", curve: "step", tip: true, markerEnd: true})
]
})}</p>
<div class="grid grid-cols-2-3" style="margin-top: 2rem;">
<div class="card">${frmCard(30, pmms)}</div>
<div class="card">${frmCard(15, pmms)}</div>
<div class="card grid-colspan-2 grid-rowspan-2" style="display: flex; flex-direction: column;">
<h2>Rates over the past year</h2>
<span style="flex-grow: 1;">${resize((width, height) =>
Plot.plot({
width,
height,
y: {grid: true, label: "rate (%)"},
color,
marks: [
Plot.lineY(tidy.slice(-53 * 2), {x: "date", y: "rate", stroke: "type", curve: "step", tip: true, markerEnd: true})
]
})
)}</span>
</div>
</div>
<p class="card">${
Plot.plot({
title: "Historical data",
x: {nice: 20},
y: {grid: true, label: "rate (%)"},
color,
marks: [
Plot.ruleY([0]),
Plot.lineY(tidy, {x: "date", y: "rate", stroke: "type", tip: true})
]
})}</p>
<div class="grid">
<div class="card">
<h2>Rates over all time (${d3.extent(pmms, (d) => d.date.getUTCFullYear()).join("")})</h2>
${resize((width) =>
Plot.plot({
width,
y: {grid: true, label: "rate (%)"},
color,
marks: [
Plot.ruleY([0]),
Plot.lineY(tidy, {x: "date", y: "rate", stroke: "type", tip: true})
]
})
)}
</div>
</div>
3 changes: 2 additions & 1 deletion src/client/stdlib/resize.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
// TODO Automatically disconnect the observer when the returned DIV is detached.
export function resize(run: (width: number, height: number) => Node, invalidation?: Promise<void>): Node {
const div = document.createElement("div");
div.style.cssText = "position: relative; height: 100%;";
div.style.position = "relative";
if (run.length !== 1) div.style.height = "100%";
const observer = new ResizeObserver(([entry]) => {
const {width, height} = entry.contentRect;
while (div.lastChild) div.lastChild.remove();
Expand Down
12 changes: 9 additions & 3 deletions src/style/grid.css
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,12 @@
.grid-cols-4 {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.grid-colspan-2,
.grid-colspan-3,
.grid-colspan-4 {
.grid-cols-2 .grid-colspan-2,
.grid-cols-2 .grid-colspan-3,
.grid-cols-2 .grid-colspan-4,
.grid-cols-4 .grid-colspan-2,
.grid-cols-4 .grid-colspan-3,
.grid-cols-4 .grid-colspan-4 {
grid-column: span 2;
}
}
Expand All @@ -41,6 +44,9 @@
.grid-cols-3 {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.grid-cols-3 .grid-colspan-2 {
grid-column: span 2;
}
.grid-cols-3 .grid-colspan-3 {
grid-column: span 3;
}
Expand Down

0 comments on commit 4a75582

Please sign in to comment.