# HG changeset patch # User Paul Fisher # Date 1570927211 14400 # Node ID fdb874e0b2707b497c40ece78f78fc95d3be6bf3 # Parent 9a609bcf08091bd1d252e3927aebbf89d6dda800 graph: Update style and do HiDPI properly. diff -r 9a609bcf0809 -r fdb874e0b270 weather_server/static/style.css --- a/weather_server/static/style.css Sat Oct 12 14:03:52 2019 -0400 +++ b/weather_server/static/style.css Sat Oct 12 20:40:11 2019 -0400 @@ -50,6 +50,8 @@ } p.plain { + box-sizing: border-box; + flex: 1; position: relative; display: flex; flex-direction: column; @@ -80,6 +82,7 @@ p.fancy { position: relative; + flex: 1; } p.fancy canvas { diff -r 9a609bcf0809 -r fdb874e0b270 weather_server/templates/location.html --- a/weather_server/templates/location.html Sat Oct 12 14:03:52 2019 -0400 +++ b/weather_server/templates/location.html Sat Oct 12 20:40:11 2019 -0400 @@ -39,7 +39,7 @@ {% else %}

?

{% endif %} - diff -r 9a609bcf0809 -r fdb874e0b270 weather_server/typescript/amd/tsconfig.json --- a/weather_server/typescript/amd/tsconfig.json Sat Oct 12 14:03:52 2019 -0400 +++ b/weather_server/typescript/amd/tsconfig.json Sat Oct 12 20:40:11 2019 -0400 @@ -1,4 +1,5 @@ { + "compileOnSave": true, "compilerOptions": { /* Basic Options */ "target": "es6", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */ diff -r 9a609bcf0809 -r fdb874e0b270 weather_server/typescript/app/graph.ts --- a/weather_server/typescript/app/graph.ts Sat Oct 12 14:03:52 2019 -0400 +++ b/weather_server/typescript/app/graph.ts Sat Oct 12 20:40:11 2019 -0400 @@ -1,7 +1,10 @@ import { WeatherResponse } from './apis'; import { cToF, dewPointC } from './math'; +/** The amount of time we will draw on the graph. */ const HISTORY_SECONDS = 86400; +/** The amount of buffer room we will request before HISTORY_SECONDS. */ +const BUFFER_SECONDS = 300; /** * Sets up everything. @@ -12,10 +15,9 @@ tempElement: HTMLElement, dewPointElement: HTMLElement) { const nowTS = new Date().getTime() / 1000; const startTS = nowTS - HISTORY_SECONDS; - const query = new URL(location.href); + const query = new URL('?', location.href); query.pathname = query.pathname + '/recent'; - query.search = ''; - query.searchParams.set('seconds', String(HISTORY_SECONDS)); + query.searchParams.set('seconds', String(HISTORY_SECONDS + BUFFER_SECONDS)); const results = await fetch(query.href); if (!results.ok) return; const data: WeatherResponse = await results.json(); @@ -40,69 +42,91 @@ */ function setUpElement( element: HTMLElement, timeRange: [number, number], data: Point[]) { - 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); + if (data.length === 0) return; + const chart = new Chart(element, timeRange, data); + chart.resize(); + addEventListener('resize', () => chart.resize()); } -/** - * - * @param element The parent element to put the `` in. - * @param data The data to chart. - * @param xRange The `[start, end]` of the X range to plot. - */ -function redrawCanvas( - element: HTMLElement, data: Point[], xRange: [number, number]) { - let canvas = element.getElementsByTagName('canvas')[0]; - if (!canvas) { - canvas = document.createElement('canvas'); - element.insertBefore(canvas, element.firstChild); +/** The number of degrees that the graph shows vertically. */ +const Y_DEGREE_RANGE = 16; + +/** The width of the space on the right side of the graph. */ +const X_PADDING_PX = 40; + +const LINE_WIDTH_PX = 2; + +class Chart { + private readonly canvas: HTMLCanvasElement; + /** + * 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( + 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); + } + + resize() { + const dpr = self.devicePixelRatio || 1; + const [w, h] = this.size(); + const pxSize: Point = [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); } - const dpr = window.devicePixelRatio || 1; - const cssSize = element.getBoundingClientRect(); - const pxSize: [number, number] = - [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 || 'white'; - drawChart(ctx, data, xRange, pxSize); + + private redraw(ctx: CanvasRenderingContext2D) { + const stroke = getComputedStyle(this.element).color!; + ctx.strokeStyle = stroke; + ctx.lineJoin = 'round'; + ctx.lineWidth = LINE_WIDTH_PX; + + const yRange = calculateYRange(this.data.map(d => d[1])); + + const [fullW, fullH] = this.size(); + const graphSize: Point = [fullW - X_PADDING_PX, 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], + 2 * LINE_WIDTH_PX, + 2 * LINE_WIDTH_PX, + 0, + 0, + 2 * Math.PI); + ctx.fillStyle = getComputedStyle(this.element).backgroundColor!; + ctx.fill(); + ctx.stroke(); + } + + private size(): Point { + const cssSize = this.element.getBoundingClientRect(); + return [cssSize.width, cssSize.height]; + } } -/** The height of the chart in degrees. */ -const CHART_RANGE_DEGREES = 15; - -/** - * Charts some data. - * - * @param ctx The context to draw in. - * @param data The data to chart, as `[x, y]` pairs. - * @param xRange The bounds of the X axis to draw. - * @param size The `[width, height]` of the context. - */ -function drawChart( - ctx: CanvasRenderingContext2D, - data: Point[], - xRange: [number, number], - size: Point) { - 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(); -} +/** 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. @@ -114,19 +138,12 @@ 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]; + 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]; } /** diff -r 9a609bcf0809 -r fdb874e0b270 weather_server/typescript/app/tsconfig.json --- a/weather_server/typescript/app/tsconfig.json Sat Oct 12 14:03:52 2019 -0400 +++ b/weather_server/typescript/app/tsconfig.json Sat Oct 12 20:40:11 2019 -0400 @@ -1,5 +1,6 @@ { "files": ["graph.ts"], + "compileOnSave": true, "compilerOptions": { /* Basic Options */ "target": "es6", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */