Mercurial > personal > weather-server
comparison weather_server/static/graph.js @ 33:beedfa8eaa3f
Add grid lines to the graph.
- Adds time gridlines on the hour and 15 minutes.
- Adds temperature gridlines on 10, 5, and 1 degree intervals.
- Increases chart range.
- Changes from rounding temperature to floor()ing it.
author | Paul Fisher <paul@pfish.zone> |
---|---|
date | Sat, 12 Jun 2021 20:22:46 +0000 |
parents | f817fa785c93 |
children |
comparison
equal
deleted
inserted
replaced
32:2faf3499a226 | 33:beedfa8eaa3f |
---|---|
8 }); | 8 }); |
9 }; | 9 }; |
10 define("math", ["require", "exports"], function (require, exports) { | 10 define("math", ["require", "exports"], function (require, exports) { |
11 "use strict"; | 11 "use strict"; |
12 Object.defineProperty(exports, "__esModule", { value: true }); | 12 Object.defineProperty(exports, "__esModule", { value: true }); |
13 exports.dewPointC = exports.cToF = void 0; | |
13 function cToF(tempC) { | 14 function cToF(tempC) { |
14 return tempC * 9 / 5 + 32; | 15 return tempC * 9 / 5 + 32; |
15 } | 16 } |
16 exports.cToF = cToF; | 17 exports.cToF = cToF; |
17 const MAGNUS_B = 17.62; | 18 const MAGNUS_B = 17.62; |
26 exports.dewPointC = dewPointC; | 27 exports.dewPointC = dewPointC; |
27 }); | 28 }); |
28 define("graph", ["require", "exports", "math"], function (require, exports, math_1) { | 29 define("graph", ["require", "exports", "math"], function (require, exports, math_1) { |
29 "use strict"; | 30 "use strict"; |
30 Object.defineProperty(exports, "__esModule", { value: true }); | 31 Object.defineProperty(exports, "__esModule", { value: true }); |
32 exports.setUp = void 0; | |
31 const HISTORY_SECONDS = 60 * 60 * 4; | 33 const HISTORY_SECONDS = 60 * 60 * 4; |
32 const BUFFER_SECONDS = 300; | 34 const BUFFER_SECONDS = 300; |
33 function setUp(root, tempElement, dewPointElement) { | 35 function setUp(root, tempElement, dewPointElement) { |
34 return __awaiter(this, void 0, void 0, function* () { | 36 return __awaiter(this, void 0, void 0, function* () { |
35 const nowTS = new Date().getTime() / 1000; | 37 const nowTS = new Date().getTime() / 1000; |
58 return; | 60 return; |
59 const chart = new Chart(element, timeRange, data); | 61 const chart = new Chart(element, timeRange, data); |
60 chart.resize(); | 62 chart.resize(); |
61 addEventListener('resize', () => chart.resize()); | 63 addEventListener('resize', () => chart.resize()); |
62 } | 64 } |
63 const Y_DEGREE_RANGE = 10; | 65 const Y_DEGREE_RANGE = 12; |
64 const LINE_WIDTH_PX = 2; | 66 const LINE_WIDTH_PX = 2; |
67 const GRID_WIDTH_PX = 0.5; | |
65 const FONT_SIZE = '40px'; | 68 const FONT_SIZE = '40px'; |
66 class Chart { | 69 class Chart { |
67 constructor(element, timeRange, data) { | 70 constructor(element, timeRange, data, timezone) { |
68 this.element = element; | 71 this.element = element; |
69 this.timeRange = timeRange; | 72 this.timeRange = timeRange; |
70 this.data = data; | 73 this.data = data; |
74 this.timezone = timezone; | |
71 this.canvas = document.createElement('canvas'); | 75 this.canvas = document.createElement('canvas'); |
72 this.element.insertBefore(this.canvas, element.firstChild); | 76 this.element.insertBefore(this.canvas, element.firstChild); |
73 const unit = element.getElementsByClassName('unit')[0]; | 77 const unit = element.getElementsByClassName('unit')[0]; |
74 this.unit = unit && unit.textContent || ''; | 78 this.unit = unit && unit.textContent || ''; |
75 } | 79 } |
83 ctx.clearRect(0, 0, pxSize[0], pxSize[1]); | 87 ctx.clearRect(0, 0, pxSize[0], pxSize[1]); |
84 ctx.scale(dpr, dpr); | 88 ctx.scale(dpr, dpr); |
85 this.redraw(ctx); | 89 this.redraw(ctx); |
86 } | 90 } |
87 redraw(ctx) { | 91 redraw(ctx) { |
88 const stroke = getComputedStyle(this.element).color; | 92 ctx.strokeStyle = this.stroke(); |
89 const family = getComputedStyle(this.element).fontFamily; | |
90 ctx.strokeStyle = stroke; | |
91 ctx.lineJoin = 'round'; | 93 ctx.lineJoin = 'round'; |
92 ctx.lineWidth = LINE_WIDTH_PX; | 94 ctx.lineWidth = LINE_WIDTH_PX; |
93 ctx.font = `bold ${FONT_SIZE} ${family}`; | 95 ctx.font = `bold ${FONT_SIZE} ${this.font()}`; |
94 const onScreenData = this.data.filter(([time, _]) => this.timeRange[0] <= time); | 96 const onScreenData = this.data.filter(([time, _]) => this.timeRange[0] <= time); |
95 const yRange = calculateYRange(onScreenData.map(d => d[1])); | 97 const yRange = calculateYRange(onScreenData.map(d => d[1])); |
96 const [fullW, fullH] = this.size(); | 98 const [fullW, fullH] = this.size(); |
97 const [xPad, yMargin] = this.measureMargin(ctx); | 99 const [xPad, yMargin] = this.measureMargin(ctx); |
98 const graphSize = [fullW - xPad, fullH]; | 100 const graphSize = [fullW - xPad, fullH]; |
107 const center = project(lastPt, graphSize, this.timeRange, yRange); | 109 const center = project(lastPt, graphSize, this.timeRange, yRange); |
108 ctx.ellipse(center[0], center[1], 1.5 * LINE_WIDTH_PX, 1.5 * LINE_WIDTH_PX, 0, 0, 2 * Math.PI); | 110 ctx.ellipse(center[0], center[1], 1.5 * LINE_WIDTH_PX, 1.5 * LINE_WIDTH_PX, 0, 0, 2 * Math.PI); |
109 ctx.fillStyle = getComputedStyle(this.element).backgroundColor; | 111 ctx.fillStyle = getComputedStyle(this.element).backgroundColor; |
110 ctx.fill(); | 112 ctx.fill(); |
111 ctx.stroke(); | 113 ctx.stroke(); |
112 ctx.fillStyle = stroke; | 114 this.drawGrid(ctx, graphSize, yRange); |
115 ctx.fillStyle = this.stroke(); | |
113 ctx.textAlign = 'left'; | 116 ctx.textAlign = 'left'; |
114 ctx.textBaseline = 'top'; | 117 ctx.textBaseline = 'top'; |
115 ctx.fillText(`${niceNumber(lastPt[1])} ${this.unit}`, center[0] + 5 * LINE_WIDTH_PX, center[1] + yMargin); | 118 ctx.fillText(`${niceNumber(lastPt[1])} ${this.unit}`, center[0] + 5 * LINE_WIDTH_PX, center[1] + yMargin); |
116 } | 119 } |
117 measureMargin(ctx) { | 120 measureMargin(ctx) { |
119 const margin = 5 * LINE_WIDTH_PX + | 122 const margin = 5 * LINE_WIDTH_PX + |
120 bbox.width + | 123 bbox.width + |
121 16; | 124 16; |
122 return [margin, -31.5 / 2]; | 125 return [margin, -31.5 / 2]; |
123 } | 126 } |
127 drawGrid(ctx, size, yRange) { | |
128 ctx.save(); | |
129 ctx.lineCap = 'butt'; | |
130 ctx.lineWidth = GRID_WIDTH_PX; | |
131 this.drawTime(ctx, size); | |
132 this.drawTemps(ctx, size, yRange); | |
133 ctx.restore(); | |
134 } | |
135 drawTime(ctx, [w, h]) { | |
136 const minutes = minutesOf(this.timeRange, this.timezone); | |
137 for (const [ts, hhmm] of minutes) { | |
138 const mins = Number(hhmm.split(':')[1]); | |
139 let opacity; | |
140 if (mins === 0) { | |
141 opacity = 1; | |
142 } | |
143 else if (mins % 15 === 0) { | |
144 opacity = 0.5; | |
145 } | |
146 else { | |
147 continue; | |
148 } | |
149 const x = project1D(ts, w, this.timeRange); | |
150 ctx.save(); | |
151 ctx.globalAlpha = opacity; | |
152 ctx.beginPath(); | |
153 ctx.moveTo(x, 0); | |
154 ctx.lineTo(x, h); | |
155 ctx.stroke(); | |
156 ctx.restore(); | |
157 } | |
158 } | |
159 drawTemps(ctx, [w, h], [yLo, yHi]) { | |
160 const lo = Math.floor(yLo); | |
161 for (let deg = lo; deg < yHi; deg++) { | |
162 const ones = deg % 10; | |
163 let opacity; | |
164 if (ones === 0) { | |
165 opacity = 1; | |
166 } | |
167 else if (ones === 5) { | |
168 opacity = 0.5; | |
169 } | |
170 else { | |
171 opacity = 0.15; | |
172 } | |
173 const y = project1D(deg, h, [yHi, yLo]); | |
174 ctx.save(); | |
175 ctx.globalAlpha = opacity; | |
176 ctx.beginPath(); | |
177 ctx.moveTo(0, y); | |
178 ctx.lineTo(w, y); | |
179 ctx.stroke(); | |
180 ctx.restore(); | |
181 } | |
182 } | |
124 size() { | 183 size() { |
125 const cssSize = this.element.getBoundingClientRect(); | 184 const cssSize = this.element.getBoundingClientRect(); |
126 return [cssSize.width, cssSize.height]; | 185 return [cssSize.width, cssSize.height]; |
127 } | 186 } |
187 stroke() { | |
188 return getComputedStyle(this.element).color; | |
189 } | |
190 font() { | |
191 return getComputedStyle(this.element).fontFamily; | |
192 } | |
128 } | 193 } |
129 function niceNumber(n) { | 194 function niceNumber(n) { |
130 return Math.round(n).toLocaleString('en-us').replace('-', '−'); | 195 return Math.floor(n).toLocaleString('en-us').replace('-', '−'); |
131 } | 196 } |
132 const EDGE_FRACTION = 0.125; | 197 const EDGE_FRACTION = 0.125; |
133 function calculateYRange(ys) { | 198 function calculateYRange(ys) { |
134 const yMax = Math.max(...ys); | 199 const yMax = Math.max(...ys); |
135 const yMin = Math.min(...ys); | 200 const yMin = Math.min(...ys); |
140 const rangeLo = lastY - yProportion * Y_DEGREE_RANGE; | 205 const rangeLo = lastY - yProportion * Y_DEGREE_RANGE; |
141 return [rangeLo, rangeLo + Y_DEGREE_RANGE]; | 206 return [rangeLo, rangeLo + Y_DEGREE_RANGE]; |
142 } | 207 } |
143 function project(coord, size, xRange, yRange) { | 208 function project(coord, size, xRange, yRange) { |
144 const [x, y] = coord; | 209 const [x, y] = coord; |
145 const [xMin, xMax] = xRange; | 210 const [yMax, yMin] = yRange; |
146 const xSpan = xMax - xMin; | |
147 const [yMin, yMax] = yRange; | |
148 const ySpan = yMax - yMin; | |
149 const [xSize, ySize] = size; | 211 const [xSize, ySize] = size; |
150 return [ | 212 return [ |
151 (x - xMin) / xSpan * xSize, | 213 project1D(x, xSize, xRange), |
152 (yMax - y) / ySpan * ySize, | 214 project1D(y, ySize, [yMin, yMax]), |
153 ]; | 215 ]; |
216 } | |
217 function project1D(coord, size, range) { | |
218 const [min, max] = range; | |
219 const span = max - min; | |
220 return (coord - min) / span * size; | |
221 } | |
222 const MINUTE = 60; | |
223 function minutesOf([start, end], zone) { | |
224 const formatter = new Intl.DateTimeFormat('sv', { timeZone: zone, hour: '2-digit', minute: '2-digit' }); | |
225 const startSeconds = MINUTE * Math.floor(start / MINUTE); | |
226 const result = []; | |
227 for (let t = startSeconds; t <= end; t += MINUTE) { | |
228 result.push([t, goodBitsOnly(formatter.format(new Date(t * 1000)))]); | |
229 } | |
230 return result; | |
231 } | |
232 function goodBitsOnly(s) { | |
233 return s.replace(/[^0-9 :-]/, ''); | |
154 } | 234 } |
155 }); | 235 }); |
156 //# sourceMappingURL=graph.js.map | 236 //# sourceMappingURL=graph.js.map |