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