# HG changeset patch # User Paul Fisher # Date 1571005326 14400 # Node ID 47987502bf4c603be1e964ecf9af36a9a07a8505 # Parent 9d07dc5c33408593efeee3cce002ed6b15da9376 Add graph, make it public, and bump the version. This checks in the tsc-compiled JS files because idklol. diff -r 9d07dc5c3340 -r 47987502bf4c MANIFEST.in --- a/MANIFEST.in Sat Oct 12 21:02:02 2019 -0400 +++ b/MANIFEST.in Sun Oct 13 18:22:06 2019 -0400 @@ -1,2 +1,2 @@ -include weather_server/static/* +recursive-include weather_server/static/ * include weather_server/templates/* diff -r 9d07dc5c3340 -r 47987502bf4c setup.py --- a/setup.py Sat Oct 12 21:02:02 2019 -0400 +++ b/setup.py Sun Oct 13 18:22:06 2019 -0400 @@ -2,7 +2,7 @@ setuptools.setup( name='weather-server', - version='0.0.3', + version='0.0.4', packages=setuptools.find_packages(), python_requires='>=3.7', install_requires=[ diff -r 9d07dc5c3340 -r 47987502bf4c weather_server/static/amd/mad.js --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/weather_server/static/amd/mad.js Sun Oct 13 18:22:06 2019 -0400 @@ -0,0 +1,34 @@ +"use strict"; +class MADRegistry { + constructor() { + this.mods = new Map(); + } + define(name, deps, factory) { + this.mods.set(name, { deps, factory, exports: null }); + } + require(dep, srcMod) { + if (dep === 'require') { + return (child) => this.require(child, srcMod); + } + if (dep === 'exports') { + if (!srcMod) + throw new Error('Internal consistency error.'); + return srcMod.exports; + } + const mod = this.mods.get(dep); + if (!mod) + throw new Error('Undefined module.'); + if (mod.exports) + return mod.exports; + mod.exports = {}; + const deps = mod.deps.map(child => this.require(child, mod)); + mod.factory(...deps); + return mod.exports; + } + install(to) { + to['define'] = + (name, deps, factory) => this.define(name, deps, factory); + to['require'] = (dep) => this.require(dep); + } +} +//# sourceMappingURL=mad.js.map \ No newline at end of file diff -r 9d07dc5c3340 -r 47987502bf4c weather_server/static/amd/mad.js.map --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/weather_server/static/amd/mad.js.map Sun Oct 13 18:22:06 2019 -0400 @@ -0,0 +1,1 @@ +{"version":3,"file":"mad.js","sourceRoot":"","sources":["../../typescript/amd/mad-amd.ts"],"names":[],"mappings":";AAyBA,MAAM,WAAW;IAAjB;QAEqB,SAAI,GAAG,IAAI,GAAG,EAAkB,CAAC;IAkEtD,CAAC;IApDG,MAAM,CAAC,IAAY,EAAE,IAAc,EAAE,OAAwB;QACzD,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,EAAC,IAAI,EAAE,OAAO,EAAE,OAAO,EAAE,IAAI,EAAC,CAAC,CAAC;IACxD,CAAC;IAeD,OAAO,CAAC,GAAW,EAAE,MAAe;QAChC,IAAI,GAAG,KAAK,SAAS,EAAE;YACnB,OAAO,CAAC,KAAa,EAAE,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC;SACzD;QACD,IAAI,GAAG,KAAK,SAAS,EAAE;YACnB,IAAI,CAAC,MAAM;gBAAE,MAAM,IAAI,KAAK,CAAC,6BAA6B,CAAC,CAAC;YAG5D,OAAO,MAAM,CAAC,OAAQ,CAAC;SAC1B;QACD,MAAM,GAAG,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QAC/B,IAAI,CAAC,GAAG;YAAE,MAAM,IAAI,KAAK,CAAC,mBAAmB,CAAC,CAAC;QAE/C,IAAI,GAAG,CAAC,OAAO;YAAE,OAAO,GAAG,CAAC,OAAO,CAAC;QAEpC,GAAG,CAAC,OAAO,GAAG,EAAE,CAAC;QACjB,MAAM,IAAI,GAAG,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC,CAAC;QAC7D,GAAG,CAAC,OAAO,CAAC,GAAG,IAAI,CAAC,CAAC;QACrB,OAAO,GAAG,CAAC,OAAO,CAAC;IACvB,CAAC;IAUD,OAAO,CAAC,EAAO;QACX,EAAE,CAAC,QAAQ,CAAC;YACR,CAAC,IAAY,EAAE,IAAc,EAAE,OAAwB,EAAE,EAAE,CACvD,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE,IAAI,EAAE,OAAO,CAAC,CAAC;QACzC,EAAE,CAAC,SAAS,CAAC,GAAG,CAAC,GAAW,EAAE,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;IACvD,CAAC;CACJ","sourcesContent":["/** Type of the AMD factory function. */\ntype FactoryFunction = (...deps: unknown[]) => void;\n\n/** An individual AMD module. */\ninterface Module {\n /** The names of the module's dependencies. */\n deps: string[];\n /**\n * The function that, when called with the module's list of dependencies,\n * creates the module.\n */\n factory: FactoryFunction;\n /**\n * When null, an indication that the module has not yet been reified.\n * When non-null, the members that the module has exported.\n */\n exports: {}|null;\n}\n\n/**\n * Minimal AMD Dumb Registry: the dumbest possible implementation of AMD,\n * to handle only the code that `tsc -m amd` produces.\n *\n * Supports `require` of absolute paths and `define`s.\n */\nclass MADRegistry {\n /** The registry itself, mapping from name to module. */\n private readonly mods = new Map();\n\n /**\n * The (subset of) the AMD `define` function we implement.\n *\n * This supports\n *\n * - Absolute paths\n * - `exports`-based module construction\n *\n * @param name The name of the module to define.\n * @param deps The dependencies of the module. Must be explicit.\n * @param factory The module's factory function.\n */\n define(name: string, deps: string[], factory: FactoryFunction) {\n this.mods.set(name, {deps, factory, exports: null});\n }\n\n /**\n * The (subset of) the AMD `require` function we implement.\n * Only `require(dep)` is exposed to users; `require(dep, srcMod)`\n * is internal-only.\n *\n * - Does not support relative paths.\n * - Does not define `require.amd`, because we do not fully support AMD\n * and don't want to give the impression that we do.\n *\n * @param dep The name of the dependency.\n * @param srcMod The module whence the dependency was requested.\n * Used for when the name `exports` is required.\n */\n require(dep: string, srcMod?: Module): {} {\n if (dep === 'require') {\n return (child: string) => this.require(child, srcMod);\n }\n if (dep === 'exports') {\n if (!srcMod) throw new Error('Internal consistency error.');\n // We know this is safe because a module can only ever require\n // its own exports after it is itself required.\n return srcMod.exports!;\n }\n const mod = this.mods.get(dep);\n if (!mod) throw new Error('Undefined module.');\n // If we've required the module before, return its exports.\n if (mod.exports) return mod.exports;\n // Otherwise, we need to prepare the module and require its parents.\n mod.exports = {};\n const deps = mod.deps.map(child => this.require(child, mod));\n mod.factory(...deps);\n return mod.exports;\n }\n\n /**\n * Installs this registry into the given object, usually `window` or `self`.\n * For usage with a separately-compiled JS file, do:\n *\n * ```typescript\n * new MADRegistry().install();\n * ```\n */\n install(to: any) {\n to['define'] =\n (name: string, deps: string[], factory: FactoryFunction) =>\n this.define(name, deps, factory);\n to['require'] = (dep: string) => this.require(dep);\n }\n}\n"]} \ No newline at end of file diff -r 9d07dc5c3340 -r 47987502bf4c weather_server/static/graph.js --- /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 ``-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 diff -r 9d07dc5c3340 -r 47987502bf4c weather_server/static/graph.js.map --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/weather_server/static/graph.js.map Sun Oct 13 18:22:06 2019 -0400 @@ -0,0 +1,1 @@ +{"version":3,"file":"graph.js","sourceRoot":"","sources":["../typescript/app/math.ts","../typescript/app/graph.ts"],"names":[],"mappings":"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","sourcesContent":["/** Converts Celsius to Fahrenheit. */\nexport function cToF(tempC: number): number {\n return tempC * 9 / 5 + 32;\n}\n\nconst MAGNUS_B = 17.62;\nconst MAGNUS_C = 243.12;\n\n/** The gamma function to calculate dew point. */\nfunction gammaFn(tempC: number, rhPct: number): number {\n return Math.log(rhPct / 100) + MAGNUS_B * tempC / (MAGNUS_C + tempC);\n}\n\n/** Calculates the dew point. */\nexport function dewPointC(tempC: any, rhPct: any): number {\n const gamma = gammaFn(tempC, rhPct);\n return MAGNUS_C * gamma / (MAGNUS_B - gamma);\n}\n","import { WeatherResponse } from './apis';\nimport { cToF, dewPointC } from './math';\n\n/** The amount of time we will draw on the graph. */\nconst HISTORY_SECONDS = 60 * 60 * 4;\n/** The amount of buffer room we will request before HISTORY_SECONDS. */\nconst BUFFER_SECONDS = 300;\n\n/**\n * Sets up everything.\n * @param tempElement The element where temperature data is.\n * @param dewPointElement The element where the dew point is.\n */\nexport async function setUp(\n root: HTMLElement,\n tempElement: HTMLElement,\n dewPointElement: HTMLElement) {\n const nowTS = new Date().getTime() / 1000;\n const startTS = nowTS - HISTORY_SECONDS;\n const query = new URL('?', location.href);\n query.pathname = query.pathname + '/recent';\n query.searchParams.set('seconds', String(HISTORY_SECONDS + BUFFER_SECONDS));\n const results = await fetch(query.href);\n if (!results.ok) return;\n const data: WeatherResponse = await results.json();\n const readings = data.readings;\n if (readings.length === 0) return;\n root.classList.remove('plain');\n root.classList.add('fancy');\n\n const tempsF =\n readings.map(s => [s.sample_time, cToF(s.temp_c)] as Point);\n const dewPointsF = readings.map(\n s => [s.sample_time, cToF(dewPointC(s.temp_c, s.rh_pct))] as Point);\n setUpElement(tempElement, [startTS, nowTS], tempsF);\n setUpElement(dewPointElement, [startTS, nowTS], dewPointsF);\n}\n\ntype Point = [number, number];\n\n/**\n * Sets up charting for this element.\n * @param element The element to put a graph in.\n * @param timeRange The `[start, end]` of the time range.\n * @param data The data to chart.\n */\nfunction setUpElement(\n element: HTMLElement, timeRange: [number, number], data: Point[]) {\n if (data.length === 0) return;\n const chart = new Chart(element, timeRange, data);\n chart.resize();\n addEventListener('resize', () => chart.resize());\n}\n\n/** The number of degrees that the graph shows vertically. */\nconst Y_DEGREE_RANGE = 10;\n\nconst LINE_WIDTH_PX = 2;\n\nconst FONT_SIZE = '40px';\n\nclass Chart {\n private readonly canvas: HTMLCanvasElement;\n private readonly unit: string;\n /**\n * Creates a new chart.\n * @param element The parent element to create ``-based chart in.\n * @param timeRange `[start, end]` of the range to chart as Unix timestamps.\n * @param data The data to chart.\n */\n constructor(\n private readonly element: HTMLElement,\n private readonly timeRange: [number, number],\n private readonly data: Point[]) {\n this.canvas = document.createElement('canvas');\n this.element.insertBefore(this.canvas, element.firstChild);\n const unit = element.getElementsByClassName('unit')[0];\n this.unit = unit && unit.textContent || '';\n }\n\n resize() {\n const dpr = self.devicePixelRatio || 1;\n const [w, h] = this.size();\n const pxSize: Point = [w * dpr, h * dpr];\n this.canvas.width = pxSize[0];\n this.canvas.height = pxSize[1];\n const ctx = this.canvas.getContext('2d')!;\n ctx.clearRect(0, 0, pxSize[0], pxSize[1]);\n ctx.scale(dpr, dpr);\n this.redraw(ctx);\n }\n\n private redraw(ctx: CanvasRenderingContext2D) {\n const stroke = getComputedStyle(this.element).color!;\n const family = getComputedStyle(this.element).fontFamily!;\n ctx.strokeStyle = stroke;\n ctx.lineJoin = 'round';\n ctx.lineWidth = LINE_WIDTH_PX;\n ctx.font = `bold ${FONT_SIZE} ${family}`;\n\n const yRange = calculateYRange(this.data.map(d => d[1]));\n\n const [fullW, fullH] = this.size();\n const [xPad, yMargin] = this.measureMargin(ctx);\n const graphSize: Point = [fullW - xPad, fullH];\n\n ctx.beginPath();\n for (const pt of this.data) {\n const projected = project(pt, graphSize, this.timeRange, yRange);\n ctx.lineTo(...projected);\n }\n ctx.stroke();\n ctx.beginPath();\n const lastPt = this.data[this.data.length - 1];\n const center = project(lastPt, graphSize, this.timeRange, yRange);\n ctx.ellipse(\n center[0],\n center[1],\n 1.5 * LINE_WIDTH_PX,\n 1.5 * LINE_WIDTH_PX,\n 0,\n 0,\n 2 * Math.PI);\n ctx.fillStyle = getComputedStyle(this.element).backgroundColor!;\n ctx.fill();\n ctx.stroke();\n\n ctx.fillStyle = stroke;\n ctx.textAlign = 'left';\n ctx.textBaseline = 'top';\n ctx.fillText(\n `${niceNumber(lastPt[1])} ${this.unit}`,\n center[0] + 5 * LINE_WIDTH_PX,\n center[1] + yMargin);\n }\n\n private measureMargin(ctx: CanvasRenderingContext2D): [number, number] {\n const bbox = ctx.measureText(`−99 ${this.unit}`);\n const margin =\n 5 * LINE_WIDTH_PX + // margin to text\n bbox.width + // max (?) width of text\n 16; // Pixel margin to wall.\n return [margin, -31.5 / 2];\n }\n\n private size(): Point {\n const cssSize = this.element.getBoundingClientRect();\n return [cssSize.width, cssSize.height];\n }\n}\n\nfunction niceNumber(n: number) {\n return Math.round(n).toLocaleString('en-us').replace('-', '−');\n}\n\n/** The closest that the last point will be allowed to get to the edge. */\nconst EDGE_FRACTION = 0.125;\n\n/**\n * Determines what the Y range of the chart should be.\n * @param ys The Y values of the chart.\n * @return The lowest and highest values of the range.\n */\nfunction calculateYRange(ys: number[]): [number, number] {\n const yMax = Math.max(...ys);\n const yMin = Math.min(...ys);\n const yMid = (yMin + yMax) / 2;\n const lastY = ys[ys.length - 1];\n const yLo = yMid - Y_DEGREE_RANGE / 2;\n const yProportion = Math.max(\n Math.min((lastY - yLo) / Y_DEGREE_RANGE, 1 - EDGE_FRACTION),\n EDGE_FRACTION);\n const rangeLo = lastY - yProportion * Y_DEGREE_RANGE;\n return [rangeLo, rangeLo + Y_DEGREE_RANGE];\n}\n\n/**\n * Projects a Cartesian coordinate into Canvas space.\n *\n * @param coord The `[x, y]` coordinate to project.\n * @param size The `[width, height]` of the context.\n * @param xRange The range of X values in the context.\n * @param yRange The range of Y values in the context.\n * @return The `[x, y]` coordinate in Canvas space.\n */\nfunction project(\n coord: Point,\n size: Point,\n xRange: [number, number],\n yRange: [number, number]): Point {\n const [x, y] = coord;\n const [xMin, xMax] = xRange;\n const xSpan = xMax - xMin;\n const [yMin, yMax] = yRange;\n const ySpan = yMax - yMin;\n const [xSize, ySize] = size;\n return [\n (x - xMin) / xSpan * xSize,\n (yMax - y) / ySpan * ySize,\n ]\n}\n"]} \ No newline at end of file diff -r 9d07dc5c3340 -r 47987502bf4c weather_server/static/style.css --- a/weather_server/static/style.css Sat Oct 12 21:02:02 2019 -0400 +++ b/weather_server/static/style.css Sun Oct 13 18:22:06 2019 -0400 @@ -47,9 +47,10 @@ padding: 0; font: inherit; flex: auto; + line-height: 1; } -p.plain { +.plain p { box-sizing: border-box; flex: 1; position: relative; @@ -57,35 +58,35 @@ flex-direction: column; align-items: baseline; padding: 24px; - line-height: 1; } -p.plain.important .key { +.plain p.important .key { display: block; opacity: 75%; font-size: 18px; } -p.plain.important .value { +.plain p.important .value { margin-top: 0; font-size: 150px; display: flex; flex-flow: row nowrap; } -p.plain.important .value .unit { +.plain p.important .value .unit { font-size: 33%; font-weight: bold; opacity: 66%; padding-top: 0.25em; } -p.fancy { +.fancy p.important { position: relative; flex: 1; + height: 200px; } -p.fancy canvas { +.fancy p.important canvas { display: block; position: absolute; top: 0; @@ -96,6 +97,23 @@ height: 100%; } +.fancy p.important .key { + display: block; + position: absolute; + top: 24px; + left: 24px; + margin: -4px; + padding: 4px; + background-color: inherit; + + font-size: 18px; + opacity: 75%; +} + +.fancy p.important .value { + display: none; +} + #temp { background: var(--temp-background); color: var(--temp-text); diff -r 9d07dc5c3340 -r 47987502bf4c weather_server/templates/location.html --- a/weather_server/templates/location.html Sat Oct 12 21:02:02 2019 -0400 +++ b/weather_server/templates/location.html Sun Oct 13 18:22:06 2019 -0400 @@ -7,10 +7,10 @@ rel="stylesheet" href="{{ url_for('static', filename='style.css') }}"> - +

{{ location.name }} conditions

{% if last_reading %} -

+

Temperature @@ -20,7 +20,7 @@ °F

-

+

Dew point @@ -39,15 +39,14 @@ {% else %}

?

{% endif %} - + + + + diff -r 9d07dc5c3340 -r 47987502bf4c weather_server/typescript/app/graph.ts --- a/weather_server/typescript/app/graph.ts Sat Oct 12 21:02:02 2019 -0400 +++ b/weather_server/typescript/app/graph.ts Sun Oct 13 18:22:06 2019 -0400 @@ -12,7 +12,9 @@ * @param dewPointElement The element where the dew point is. */ export async function setUp( - tempElement: HTMLElement, dewPointElement: HTMLElement) { + root: HTMLElement, + tempElement: HTMLElement, + dewPointElement: HTMLElement) { const nowTS = new Date().getTime() / 1000; const startTS = nowTS - HISTORY_SECONDS; const query = new URL('?', location.href); @@ -23,6 +25,8 @@ const data: WeatherResponse = await 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, cToF(s.temp_c)] as Point); @@ -44,22 +48,20 @@ element: HTMLElement, timeRange: [number, number], data: Point[]) { if (data.length === 0) return; const chart = new Chart(element, timeRange, data); - requestAnimationFrame(() => { - chart.resize(); - addEventListener('resize', () => chart.resize()); - }); + chart.resize(); + addEventListener('resize', () => chart.resize()); } /** The number of degrees that the graph shows vertically. */ const Y_DEGREE_RANGE = 10; -/** The width of the space on the right side of the graph. */ -const X_PADDING_PX = 80; +const LINE_WIDTH_PX = 2; -const LINE_WIDTH_PX = 2; +const FONT_SIZE = '40px'; class Chart { private readonly canvas: HTMLCanvasElement; + private readonly unit: string; /** * Creates a new chart. * @param element The parent element to create ``-based chart in. @@ -70,10 +72,10 @@ private readonly element: HTMLElement, private readonly timeRange: [number, number], private readonly data: Point[]) { - this.element.classList.remove('plain'); - this.element.classList.add('fancy'); this.canvas = document.createElement('canvas'); this.element.insertBefore(this.canvas, element.firstChild); + const unit = element.getElementsByClassName('unit')[0]; + this.unit = unit && unit.textContent || ''; } resize() { @@ -90,14 +92,17 @@ private redraw(ctx: CanvasRenderingContext2D) { 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 graphSize: Point = [fullW - X_PADDING_PX, fullH]; + const [xPad, yMargin] = this.measureMargin(ctx); + const graphSize: Point = [fullW - xPad, fullH]; ctx.beginPath(); for (const pt of this.data) { @@ -120,14 +125,22 @@ ctx.fill(); ctx.stroke(); - const family = getComputedStyle(this.element).fontFamily!; + 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); + } - ctx.fillStyle = stroke; - ctx.textBaseline = 'middle'; - ctx.textAlign = 'left'; - ctx.font = `bold 30px ${family}`; - ctx.fillText( - niceNumber(lastPt[1]), center[0] + 5 * LINE_WIDTH_PX, center[1]); + private measureMargin(ctx: CanvasRenderingContext2D): [number, number] { + 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]; } private size(): Point {