Mercurial > personal > weather-server
changeset 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 | 2faf3499a226 |
children | 8d3f32455575 |
files | weather_server/static/graph.js weather_server/static/graph.js.map weather_server/typescript/app/graph.ts |
diffstat | 3 files changed, 214 insertions(+), 31 deletions(-) [+] |
line wrap: on
line diff
--- a/weather_server/static/graph.js Sat Jun 12 18:43:49 2021 +0000 +++ b/weather_server/static/graph.js Sat Jun 12 20:22:46 2021 +0000 @@ -10,6 +10,7 @@ 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; } @@ -28,6 +29,7 @@ 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) { @@ -60,14 +62,16 @@ chart.resize(); addEventListener('resize', () => chart.resize()); } - const Y_DEGREE_RANGE = 10; + 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) { + 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]; @@ -85,12 +89,10 @@ this.redraw(ctx); } redraw(ctx) { - const stroke = getComputedStyle(this.element).color; - const family = getComputedStyle(this.element).fontFamily; - ctx.strokeStyle = stroke; + ctx.strokeStyle = this.stroke(); ctx.lineJoin = 'round'; ctx.lineWidth = LINE_WIDTH_PX; - ctx.font = `bold ${FONT_SIZE} ${family}`; + 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(); @@ -109,7 +111,8 @@ ctx.fillStyle = getComputedStyle(this.element).backgroundColor; ctx.fill(); ctx.stroke(); - ctx.fillStyle = 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); @@ -121,13 +124,75 @@ 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.round(n).toLocaleString('en-us').replace('-', '−'); + return Math.floor(n).toLocaleString('en-us').replace('-', '−'); } const EDGE_FRACTION = 0.125; function calculateYRange(ys) { @@ -142,15 +207,30 @@ } 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 [yMax, yMin] = yRange; const [xSize, ySize] = size; return [ - (x - xMin) / xSpan * xSize, - (yMax - y) / ySpan * ySize, + 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 \ No newline at end of file
--- a/weather_server/static/graph.js.map Sat Jun 12 18:43:49 2021 +0000 +++ b/weather_server/static/graph.js.map Sat Jun 12 20:22:46 2021 +0000 @@ -1,1 +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 `<canvas>`-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 onScreenData = this.data.filter(\n ([time, _]) => this.timeRange[0] <= time);\n\n const yRange = calculateYRange(onScreenData.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 +{"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 = 12;\n\nconst LINE_WIDTH_PX = 2;\nconst GRID_WIDTH_PX = 0.5;\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 `<canvas>`-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 private readonly timezone?: string) {\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 ctx.strokeStyle = this.stroke();\n ctx.lineJoin = 'round';\n ctx.lineWidth = LINE_WIDTH_PX;\n ctx.font = `bold ${FONT_SIZE} ${this.font()}`;\n\n const onScreenData = this.data.filter(\n ([time, _]) => this.timeRange[0] <= time);\n\n const yRange = calculateYRange(onScreenData.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 this.drawGrid(ctx, graphSize, yRange);\n\n ctx.fillStyle = this.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 drawGrid(\n ctx: CanvasRenderingContext2D,\n size: Point,\n yRange: [number, number],\n ) {\n ctx.save();\n ctx.lineCap = 'butt';\n ctx.lineWidth = GRID_WIDTH_PX;\n\n this.drawTime(ctx, size);\n this.drawTemps(ctx, size, yRange);\n\n ctx.restore();\n }\n\n private drawTime(ctx: CanvasRenderingContext2D, [w, h]: Point): void {\n const minutes = minutesOf(this.timeRange, this.timezone);\n for (const [ts, hhmm] of minutes) {\n const mins = Number(hhmm.split(':')[1]);\n let opacity: number;\n if (mins === 0) {\n opacity = 1;\n } else if (mins % 15 === 0) {\n opacity = 0.5;\n } else {\n continue;\n }\n const x = project1D(ts, w, this.timeRange);\n ctx.save();\n ctx.globalAlpha = opacity;\n ctx.beginPath();\n ctx.moveTo(x, 0);\n ctx.lineTo(x, h);\n ctx.stroke();\n ctx.restore();\n }\n }\n\n private drawTemps(ctx: CanvasRenderingContext2D, [w, h]: Point, [yLo, yHi]: [number, number]): void {\n const lo = Math.floor(yLo);\n for (let deg = lo; deg < yHi; deg++) {\n const ones = deg % 10;\n let opacity: number;\n if (ones === 0) {\n opacity = 1;\n } else if (ones === 5) {\n opacity = 0.5;\n } else {\n opacity = 0.15;\n }\n const y = project1D(deg, h, [yHi, yLo]);\n ctx.save();\n ctx.globalAlpha = opacity;\n ctx.beginPath();\n ctx.moveTo(0, y);\n ctx.lineTo(w, y);\n ctx.stroke();\n ctx.restore();\n }\n }\n\n private size(): Point {\n const cssSize = this.element.getBoundingClientRect();\n return [cssSize.width, cssSize.height];\n }\n\n private stroke() {\n return getComputedStyle(this.element).color;\n }\n\n private font() {\n return getComputedStyle(this.element).fontFamily;\n }\n}\n\nfunction niceNumber(n: number) {\n return Math.floor(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],\n): Point {\n const [x, y] = coord;\n // Canvas coordinates are upside-down, so we invert the Y range.\n const [yMax, yMin] = yRange;\n const [xSize, ySize] = size;\n return [\n project1D(x, xSize, xRange),\n project1D(y, ySize, [yMin, yMax]),\n ];\n}\n\nfunction project1D(coord: number, size: number, range: [number, number]): number {\n const [min, max] = range;\n const span = max - min;\n return (coord - min) / span * size;\n}\n\nconst MINUTE = 60;\n\n/**\n * Produces a list of every minute in the graph, in the form of\n * `[unix time, \"HH:MM\"][]`.\n * @param start The Unix start time.\n * @param end The Unix end time.\n * @param zone The timezone name.\n */\nfunction minutesOf(\n [start, end]: [number, number], zone?: string): Array<[number, string]> {\n const formatter = new Intl.DateTimeFormat(\n 'sv', {timeZone: zone, hour: '2-digit', minute: '2-digit'})\n const startSeconds = MINUTE * Math.floor(start / MINUTE);\n const result: Array<[number, string]> = [];\n for (let t = startSeconds; t <= end; t += MINUTE) {\n result.push([t, goodBitsOnly(formatter.format(new Date(t * 1000)))])\n }\n return result;\n}\n\n\n/** Takes all the special characters out of the given datetime string. */\nfunction goodBitsOnly(s: string): string {\n return s.replace(/[^0-9 :-]/, '');\n}\n"]} \ No newline at end of file
--- a/weather_server/typescript/app/graph.ts Sat Jun 12 18:43:49 2021 +0000 +++ b/weather_server/typescript/app/graph.ts Sat Jun 12 20:22:46 2021 +0000 @@ -53,9 +53,10 @@ } /** The number of degrees that the graph shows vertically. */ -const Y_DEGREE_RANGE = 10; +const Y_DEGREE_RANGE = 12; const LINE_WIDTH_PX = 2; +const GRID_WIDTH_PX = 0.5; const FONT_SIZE = '40px'; @@ -71,7 +72,8 @@ constructor( private readonly element: HTMLElement, private readonly timeRange: [number, number], - private readonly data: Point[]) { + private readonly data: Point[], + private readonly timezone?: string) { this.canvas = document.createElement('canvas'); this.element.insertBefore(this.canvas, element.firstChild); const unit = element.getElementsByClassName('unit')[0]; @@ -91,12 +93,10 @@ } private redraw(ctx: CanvasRenderingContext2D) { - const stroke = getComputedStyle(this.element).color!; - const family = getComputedStyle(this.element).fontFamily!; - ctx.strokeStyle = stroke; + ctx.strokeStyle = this.stroke(); ctx.lineJoin = 'round'; ctx.lineWidth = LINE_WIDTH_PX; - ctx.font = `bold ${FONT_SIZE} ${family}`; + ctx.font = `bold ${FONT_SIZE} ${this.font()}`; const onScreenData = this.data.filter( ([time, _]) => this.timeRange[0] <= time); @@ -128,7 +128,9 @@ ctx.fill(); ctx.stroke(); - ctx.fillStyle = stroke; + this.drawGrid(ctx, graphSize, yRange); + + ctx.fillStyle = this.stroke(); ctx.textAlign = 'left'; ctx.textBaseline = 'top'; ctx.fillText( @@ -146,14 +148,83 @@ return [margin, -31.5 / 2]; } + private drawGrid( + ctx: CanvasRenderingContext2D, + size: Point, + yRange: [number, number], + ) { + ctx.save(); + ctx.lineCap = 'butt'; + ctx.lineWidth = GRID_WIDTH_PX; + + this.drawTime(ctx, size); + this.drawTemps(ctx, size, yRange); + + ctx.restore(); + } + + private drawTime(ctx: CanvasRenderingContext2D, [w, h]: Point): void { + const minutes = minutesOf(this.timeRange, this.timezone); + for (const [ts, hhmm] of minutes) { + const mins = Number(hhmm.split(':')[1]); + let opacity: number; + 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(); + } + } + + private drawTemps(ctx: CanvasRenderingContext2D, [w, h]: Point, [yLo, yHi]: [number, number]): void { + const lo = Math.floor(yLo); + for (let deg = lo; deg < yHi; deg++) { + const ones = deg % 10; + let opacity: number; + 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(); + } + } + private size(): Point { const cssSize = this.element.getBoundingClientRect(); return [cssSize.width, cssSize.height]; } + + private stroke() { + return getComputedStyle(this.element).color; + } + + private font() { + return getComputedStyle(this.element).fontFamily; + } } function niceNumber(n: number) { - return Math.round(n).toLocaleString('en-us').replace('-', '−'); + return Math.floor(n).toLocaleString('en-us').replace('-', '−'); } /** The closest that the last point will be allowed to get to the edge. */ @@ -190,15 +261,47 @@ coord: Point, size: Point, xRange: [number, number], - yRange: [number, number]): Point { + yRange: [number, number], +): Point { const [x, y] = coord; - const [xMin, xMax] = xRange; - const xSpan = xMax - xMin; - const [yMin, yMax] = yRange; - const ySpan = yMax - yMin; + // Canvas coordinates are upside-down, so we invert the Y range. + const [yMax, yMin] = yRange; const [xSize, ySize] = size; return [ - (x - xMin) / xSpan * xSize, - (yMax - y) / ySpan * ySize, - ] + project1D(x, xSize, xRange), + project1D(y, ySize, [yMin, yMax]), + ]; +} + +function project1D(coord: number, size: number, range: [number, number]): number { + const [min, max] = range; + const span = max - min; + return (coord - min) / span * size; } + +const MINUTE = 60; + +/** + * Produces a list of every minute in the graph, in the form of + * `[unix time, "HH:MM"][]`. + * @param start The Unix start time. + * @param end The Unix end time. + * @param zone The timezone name. + */ +function minutesOf( + [start, end]: [number, number], zone?: string): Array<[number, string]> { + const formatter = new Intl.DateTimeFormat( + 'sv', {timeZone: zone, hour: '2-digit', minute: '2-digit'}) + const startSeconds = MINUTE * Math.floor(start / MINUTE); + const result: Array<[number, string]> = []; + for (let t = startSeconds; t <= end; t += MINUTE) { + result.push([t, goodBitsOnly(formatter.format(new Date(t * 1000)))]) + } + return result; +} + + +/** Takes all the special characters out of the given datetime string. */ +function goodBitsOnly(s: string): string { + return s.replace(/[^0-9 :-]/, ''); +}