Mercurial > personal > weather-server
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, ] }