view weather_server/static/script.js @ 15:df3e0534c994

Tighten up MADRegistry: - Use only one map for registry. - Make the user construct it, to avoid modifying global state.
author Paul Fisher <paul@pfish.zone>
date Fri, 11 Oct 2019 20:50:50 -0400
parents 4eaa9d69c4e2
children
line wrap: on
line source

'use strict';

/**
 * Converts Celsius to Fahrenheit.
 * @param {number} tempC
 * @return {number} The temperature in Fahrenheit.
 */
function cToF(tempC) {
    return tempC * 9 / 5 + 32;
}

const MAGNUS_B = 17.62;
const MAGNUS_C = 243.12;

/**
 * The gamma function to calculate dew point.
 *
 * @param {number} tempC The temperature, in degrees Celsius.
 * @param {number} rhPct The relative humidity, in percent.
 * @return {number} The value of the gamma function.
 */
function gammaFn(tempC, rhPct) {
    return Math.log(rhPct / 100) + MAGNUS_B * tempC / (MAGNUS_C + tempC);
}

/**
 * Calculates the dew point.
 *
 * @param {number} tempC The temperature, in degrees Celsius.
 * @param {number} rhPct The relative humidity, in percent.
 * @return {number} The dew point, in degrees Celsius.
 */
function dewPointC(tempC, rhPct) {
    const gamma = gammaFn(tempC, rhPct);
    return MAGNUS_C * gamma / (MAGNUS_B - gamma);
}

const HISTORY_SECONDS = 86400;

/**
 * Sets up everything.
 * @param {HTMLElement} tempElement The element where temperature data is.
 * @param {HTMLElement} dewPointElement The element where the dew point is.
 */
async function setUp(tempElement, dewPointElement) {
    const nowTS = new Date().getTime() / 1000;
    const startTS = nowTS - HISTORY_SECONDS;
    const query = new URL(location.href);
    query.pathname = query.pathname + '/recent';
    query.search = '';
    query.searchParams.set('seconds', String(HISTORY_SECONDS));
    const results = await fetch(query.href);
    if (!results.ok) return;
    const data = await results.json();
    if (data.length === 0) return;

    const tempsF = data.map(s => [s.sample_time, cToF(s.temp_c)]);
    const dewPointsF = data.map(
        s => [s.sample_time, cToF(dewPointC(s.temp_c, s.rh_pct))]);
    setUpElement(tempElement, [startTS, nowTS], tempsF);
    setUpElement(dewPointElement, [startTS, nowTS], dewPointsF);
}

/**
 * Sets up charting for this element.
 * @param {HTMLElement} element The element to put a graph in.
 * @param {[number, number]} timeRange The `[start, end]` of the time range.
 * @param {[number, number][]} data The data to chart.
 */
function setUpElement(element, timeRange, data) {
    element.insertBefore(document.createElement('canvas'), element.firstChild);
    element.classList.remove('plain');
    element.classList.add('fancy');
    const doDraw = () => redrawCanvas(element, data, timeRange);
    doDraw();
    addEventListener('resize', doDraw);
}

/**
 *
 * @param {HTMLElement} element The parent element to put the `<canvas>` in.
 * @param {[number, number][]} data The data to chart.
 * @param {[number, number]} xRange The `[start, end]` of the X range to plot.
 */
function redrawCanvas(element, data, xRange) {
    let canvas = element.getElementsByTagName('canvas')[0];
    if (!canvas) {
        canvas = document.createElement('canvas');
        element.insertBefore(canvas, element.firstChild);
    }
    const dpr = window.devicePixelRatio || 1;
    const cssSize = element.getBoundingClientRect();
    const pxSize = [cssSize.width * dpr, cssSize.height * dpr];
    canvas.width = pxSize[0];
    canvas.height = pxSize[1];
    const ctx = canvas.getContext('2d');
    ctx.clearRect(0, 0, pxSize[0], pxSize[1]);
    const computed = getComputedStyle(element);
    ctx.strokeStyle = computed.color;
    drawChart(ctx, data, xRange, pxSize);
}

/** The height of the chart in degrees. */
const CHART_RANGE_DEGREES = 15;

/**
 * Charts some data.
 *
 * @param {CanvasRenderingContext2D} ctx The context to draw in.
 * @param {[number, number][]} data The data to chart, as `[x, y]` pairs.
 * @param {[number, number]} xRange The bounds of the X axis to draw.
 * @param {[number, number]} size The `[width, height]` of the context.
 */
function drawChart(ctx, data, xRange, size) {
    const yRange = calculateYRange(data.map(d => d[1]));

    ctx.lineWidth = 1.5;

    ctx.beginPath();
    const first = project(data[0], size, xRange, yRange);
    ctx.moveTo(...first);
    for (const pt of data) {
        const projected = project(pt, size, xRange, yRange);
        ctx.lineTo(...projected);
    }
    ctx.stroke();
}

/**
 * Determines what the Y range of the chart should be.
 * @param {number[]} ys The Y values of the chart.
 * @return {[number, number]} 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];

    // We want the last value to be, at most, at the top or bottom 1/4 line
    // of the chart.

    // If the middle of the range is already close enough, just use that.
    if (CHART_RANGE_DEGREES / 4 <= Math.abs(yMid - lastY)) {
        return [yMid - CHART_RANGE_DEGREES / 2, yMid + CHART_RANGE_DEGREES / 2];
    }
    // Otherwise, clamp the chart range.
    if (lastY < yMid) {
        return [lastY - CHART_RANGE_DEGREES / 4, lastY + 3 * CHART_RANGE_DEGREES / 4];
    }
    return [lastY - 3 * CHART_RANGE_DEGREES / 4, lastY + CHART_RANGE_DEGREES / 4];
}

/**
 * Projects a Cartesian coordinate into Canvas space.
 *
 * @param {[number, number]} coord The `[x, y]` coordinate to project.
 * @param {[number, number]} size The `[width, height]` of the context.
 * @param {[number, number]} xRange The range of X values in the context.
 * @param {[number, number]} yRange The range of Y values in the context.
 * @return {[number, number]} 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,
    ]
}