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