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