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 | 4af79d69b12e |
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 |
