@@ -6,94 +6,135 @@ const pmms = FileAttachment("data/pmms.csv").csv({typed: true});
6
6
7
7
``` js
8
8
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 >` ;
12
10
```
13
11
14
12
``` js
15
13
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 );
17
14
```
18
15
19
16
``` js
20
17
function frmCard (y , pmms ) {
21
18
const key = ` pmms${ y} ` ;
22
- const fmt = d3 .format (" +.2" );
23
- const fmt2 = d3 .format (" .2f" );
24
19
const diff1 = pmms .at (- 1 )[key] - pmms .at (- 2 )[key];
25
20
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]);
27
22
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>
57
45
</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});
60
75
}
61
76
62
77
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
+ : "→";
65
81
}
66
82
` ` `
67
83
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"})}.
71
85
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 " >
73
87
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
+ }
75
103
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>
78
105
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>
88
124
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>
0 commit comments