Mercurial > personal > weather-server
view 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 |
line wrap: on
line source
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; define("math", ["require", "exports"], function (require, exports) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.dewPointC = exports.cToF = void 0; function cToF(tempC) { return tempC * 9 / 5 + 32; } exports.cToF = cToF; const MAGNUS_B = 17.62; const MAGNUS_C = 243.12; function gammaFn(tempC, rhPct) { return Math.log(rhPct / 100) + MAGNUS_B * tempC / (MAGNUS_C + tempC); } function dewPointC(tempC, rhPct) { const gamma = gammaFn(tempC, rhPct); return MAGNUS_C * gamma / (MAGNUS_B - gamma); } exports.dewPointC = dewPointC; }); define("graph", ["require", "exports", "math"], function (require, exports, math_1) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.setUp = void 0; const HISTORY_SECONDS = 60 * 60 * 4; const BUFFER_SECONDS = 300; function setUp(root, tempElement, dewPointElement) { return __awaiter(this, void 0, void 0, function* () { const nowTS = new Date().getTime() / 1000; const startTS = nowTS - HISTORY_SECONDS; const query = new URL('?', location.href); query.pathname = query.pathname + '/recent'; query.searchParams.set('seconds', String(HISTORY_SECONDS + BUFFER_SECONDS)); const results = yield fetch(query.href); if (!results.ok) return; const data = yield results.json(); const readings = data.readings; if (readings.length === 0) return; root.classList.remove('plain'); root.classList.add('fancy'); const tempsF = readings.map(s => [s.sample_time, math_1.cToF(s.temp_c)]); const dewPointsF = readings.map(s => [s.sample_time, math_1.cToF(math_1.dewPointC(s.temp_c, s.rh_pct))]); setUpElement(tempElement, [startTS, nowTS], tempsF); setUpElement(dewPointElement, [startTS, nowTS], dewPointsF); }); } exports.setUp = setUp; function setUpElement(element, timeRange, data) { if (data.length === 0) return; const chart = new Chart(element, timeRange, data); chart.resize(); addEventListener('resize', () => chart.resize()); } const Y_DEGREE_RANGE = 12; const LINE_WIDTH_PX = 2; const GRID_WIDTH_PX = 0.5; const FONT_SIZE = '40px'; class Chart { constructor(element, timeRange, data, timezone) { this.element = element; this.timeRange = timeRange; this.data = data; this.timezone = timezone; this.canvas = document.createElement('canvas'); this.element.insertBefore(this.canvas, element.firstChild); const unit = element.getElementsByClassName('unit')[0]; this.unit = unit && unit.textContent || ''; } resize() { const dpr = self.devicePixelRatio || 1; const [w, h] = this.size(); const pxSize = [w * dpr, h * dpr]; this.canvas.width = pxSize[0]; this.canvas.height = pxSize[1]; const ctx = this.canvas.getContext('2d'); ctx.clearRect(0, 0, pxSize[0], pxSize[1]); ctx.scale(dpr, dpr); this.redraw(ctx); } redraw(ctx) { ctx.strokeStyle = this.stroke(); ctx.lineJoin = 'round'; ctx.lineWidth = LINE_WIDTH_PX; ctx.font = `bold ${FONT_SIZE} ${this.font()}`; const onScreenData = this.data.filter(([time, _]) => this.timeRange[0] <= time); const yRange = calculateYRange(onScreenData.map(d => d[1])); const [fullW, fullH] = this.size(); const [xPad, yMargin] = this.measureMargin(ctx); const graphSize = [fullW - xPad, fullH]; ctx.beginPath(); for (const pt of this.data) { const projected = project(pt, graphSize, this.timeRange, yRange); ctx.lineTo(...projected); } ctx.stroke(); ctx.beginPath(); const lastPt = this.data[this.data.length - 1]; const center = project(lastPt, graphSize, this.timeRange, yRange); ctx.ellipse(center[0], center[1], 1.5 * LINE_WIDTH_PX, 1.5 * LINE_WIDTH_PX, 0, 0, 2 * Math.PI); ctx.fillStyle = getComputedStyle(this.element).backgroundColor; ctx.fill(); ctx.stroke(); this.drawGrid(ctx, graphSize, yRange); ctx.fillStyle = this.stroke(); ctx.textAlign = 'left'; ctx.textBaseline = 'top'; ctx.fillText(`${niceNumber(lastPt[1])} ${this.unit}`, center[0] + 5 * LINE_WIDTH_PX, center[1] + yMargin); } measureMargin(ctx) { const bbox = ctx.measureText(`−99 ${this.unit}`); const margin = 5 * LINE_WIDTH_PX + bbox.width + 16; return [margin, -31.5 / 2]; } drawGrid(ctx, size, yRange) { ctx.save(); ctx.lineCap = 'butt'; ctx.lineWidth = GRID_WIDTH_PX; this.drawTime(ctx, size); this.drawTemps(ctx, size, yRange); ctx.restore(); } drawTime(ctx, [w, h]) { const minutes = minutesOf(this.timeRange, this.timezone); for (const [ts, hhmm] of minutes) { const mins = Number(hhmm.split(':')[1]); let opacity; if (mins === 0) { opacity = 1; } else if (mins % 15 === 0) { opacity = 0.5; } else { continue; } const x = project1D(ts, w, this.timeRange); ctx.save(); ctx.globalAlpha = opacity; ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, h); ctx.stroke(); ctx.restore(); } } drawTemps(ctx, [w, h], [yLo, yHi]) { const lo = Math.floor(yLo); for (let deg = lo; deg < yHi; deg++) { const ones = deg % 10; let opacity; if (ones === 0) { opacity = 1; } else if (ones === 5) { opacity = 0.5; } else { opacity = 0.15; } const y = project1D(deg, h, [yHi, yLo]); ctx.save(); ctx.globalAlpha = opacity; ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(w, y); ctx.stroke(); ctx.restore(); } } size() { const cssSize = this.element.getBoundingClientRect(); return [cssSize.width, cssSize.height]; } stroke() { return getComputedStyle(this.element).color; } font() { return getComputedStyle(this.element).fontFamily; } } function niceNumber(n) { return Math.floor(n).toLocaleString('en-us').replace('-', '−'); } const EDGE_FRACTION = 0.125; function calculateYRange(ys) { const yMax = Math.max(...ys); const yMin = Math.min(...ys); const yMid = (yMin + yMax) / 2; const lastY = ys[ys.length - 1]; const yLo = yMid - Y_DEGREE_RANGE / 2; const yProportion = Math.max(Math.min((lastY - yLo) / Y_DEGREE_RANGE, 1 - EDGE_FRACTION), EDGE_FRACTION); const rangeLo = lastY - yProportion * Y_DEGREE_RANGE; return [rangeLo, rangeLo + Y_DEGREE_RANGE]; } function project(coord, size, xRange, yRange) { const [x, y] = coord; const [yMax, yMin] = yRange; const [xSize, ySize] = size; return [ project1D(x, xSize, xRange), project1D(y, ySize, [yMin, yMax]), ]; } function project1D(coord, size, range) { const [min, max] = range; const span = max - min; return (coord - min) / span * size; } const MINUTE = 60; function minutesOf([start, end], zone) { const formatter = new Intl.DateTimeFormat('sv', { timeZone: zone, hour: '2-digit', minute: '2-digit' }); const startSeconds = MINUTE * Math.floor(start / MINUTE); const result = []; for (let t = startSeconds; t <= end; t += MINUTE) { result.push([t, goodBitsOnly(formatter.format(new Date(t * 1000)))]); } return result; } function goodBitsOnly(s) { return s.replace(/[^0-9 :-]/, ''); } }); //# sourceMappingURL=graph.js.map