changeset 17:fdb874e0b270

graph: Update style and do HiDPI properly.
author Paul Fisher <paul@pfish.zone>
date Sat, 12 Oct 2019 20:40:11 -0400
parents 9a609bcf0809
children 9d07dc5c3340
files weather_server/static/style.css weather_server/templates/location.html weather_server/typescript/amd/tsconfig.json weather_server/typescript/app/graph.ts weather_server/typescript/app/tsconfig.json
diffstat 5 files changed, 97 insertions(+), 75 deletions(-) [+]
line wrap: on
line diff
--- 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 {
--- 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 %}
             <p id="big-question-mark">?</p>
         {% endif %}
-        <!--
+        <!-- -->
             <script src="{{ url_for('static', filename='amd/mad.js') }}"></script>
             <script>new MADRegistry().install(self);</script>
             <script src="{{ url_for('static', filename='graph.js') }}"></script>
--- 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'. */
--- 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 `<canvas>` 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 `<canvas>`-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];
 }
 
 /**
--- 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'. */