Mercurial > personal > weather-server
diff weather_server/static/graph.js @ 19:47987502bf4c
Add graph, make it public, and bump the version.
This checks in the tsc-compiled JS files because idklol.
author | Paul Fisher <paul@pfish.zone> |
---|---|
date | Sun, 13 Oct 2019 18:22:06 -0400 |
parents | |
children | f817fa785c93 |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/weather_server/static/graph.js Sun Oct 13 18:22:06 2019 -0400 @@ -0,0 +1,192 @@ +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + 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) : new P(function (resolve) { resolve(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 }); + /** Converts Celsius to Fahrenheit. */ + function cToF(tempC) { + return tempC * 9 / 5 + 32; + } + exports.cToF = cToF; + const MAGNUS_B = 17.62; + const MAGNUS_C = 243.12; + /** The gamma function to calculate dew point. */ + function gammaFn(tempC, rhPct) { + return Math.log(rhPct / 100) + MAGNUS_B * tempC / (MAGNUS_C + tempC); + } + /** Calculates the dew point. */ + 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 }); + /** The amount of time we will draw on the graph. */ + const HISTORY_SECONDS = 60 * 60 * 4; + /** The amount of buffer room we will request before HISTORY_SECONDS. */ + const BUFFER_SECONDS = 300; + /** + * Sets up everything. + * @param tempElement The element where temperature data is. + * @param dewPointElement The element where the dew point is. + */ + 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; + /** + * Sets up charting for this element. + * @param element The element to put a graph in. + * @param timeRange The `[start, end]` of the time range. + * @param data The data to chart. + */ + function setUpElement(element, timeRange, data) { + if (data.length === 0) + return; + const chart = new Chart(element, timeRange, data); + chart.resize(); + addEventListener('resize', () => chart.resize()); + } + /** The number of degrees that the graph shows vertically. */ + const Y_DEGREE_RANGE = 10; + const LINE_WIDTH_PX = 2; + const FONT_SIZE = '40px'; + class Chart { + /** + * Creates a new chart. + * @param element The parent element to create `<canvas>`-based chart in. + * @param timeRange `[start, end]` of the range to chart as Unix timestamps. + * @param data The data to chart. + */ + constructor(element, timeRange, data) { + this.element = element; + this.timeRange = timeRange; + this.data = data; + 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) { + const stroke = getComputedStyle(this.element).color; + const family = getComputedStyle(this.element).fontFamily; + ctx.strokeStyle = stroke; + ctx.lineJoin = 'round'; + ctx.lineWidth = LINE_WIDTH_PX; + ctx.font = `bold ${FONT_SIZE} ${family}`; + const yRange = calculateYRange(this.data.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(); + ctx.fillStyle = 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 + // margin to text + bbox.width + // max (?) width of text + 16; // Pixel margin to wall. + return [margin, -31.5 / 2]; + } + size() { + const cssSize = this.element.getBoundingClientRect(); + return [cssSize.width, cssSize.height]; + } + } + function niceNumber(n) { + return Math.round(n).toLocaleString('en-us').replace('-', '−'); + } + /** The closest that the last point will be allowed to get to the edge. */ + const EDGE_FRACTION = 0.125; + /** + * Determines what the Y range of the chart should be. + * @param ys The Y values of the chart. + * @return The lowest and highest values of the range. + */ + 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]; + } + /** + * Projects a Cartesian coordinate into Canvas space. + * + * @param coord The `[x, y]` coordinate to project. + * @param size The `[width, height]` of the context. + * @param xRange The range of X values in the context. + * @param yRange The range of Y values in the context. + * @return The `[x, y]` coordinate in Canvas space. + */ + function project(coord, size, xRange, yRange) { + const [x, y] = coord; + const [xMin, xMax] = xRange; + const xSpan = xMax - xMin; + const [yMin, yMax] = yRange; + const ySpan = yMax - yMin; + const [xSize, ySize] = size; + return [ + (x - xMin) / xSpan * xSize, + (yMax - y) / ySpan * ySize, + ]; + } +}); +//# sourceMappingURL=graph.js.map \ No newline at end of file