Mercurial > personal > weather-server
comparison weather_server/static/graph.js @ 28:f817fa785c93
Make graph only consider visible points when setting range.
| author | Paul Fisher <paul@pfish.zone> |
|---|---|
| date | Sun, 19 Jan 2020 17:05:11 -0500 |
| parents | 47987502bf4c |
| children | beedfa8eaa3f |
comparison
equal
deleted
inserted
replaced
| 27:99b0759386b1 | 28:f817fa785c93 |
|---|---|
| 1 var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { | 1 var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { |
| 2 function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } | |
| 2 return new (P || (P = Promise))(function (resolve, reject) { | 3 return new (P || (P = Promise))(function (resolve, reject) { |
| 3 function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } | 4 function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } |
| 4 function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } | 5 function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } |
| 5 function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); } | 6 function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } |
| 6 step((generator = generator.apply(thisArg, _arguments || [])).next()); | 7 step((generator = generator.apply(thisArg, _arguments || [])).next()); |
| 7 }); | 8 }); |
| 8 }; | 9 }; |
| 9 define("math", ["require", "exports"], function (require, exports) { | 10 define("math", ["require", "exports"], function (require, exports) { |
| 10 "use strict"; | 11 "use strict"; |
| 11 Object.defineProperty(exports, "__esModule", { value: true }); | 12 Object.defineProperty(exports, "__esModule", { value: true }); |
| 12 /** Converts Celsius to Fahrenheit. */ | |
| 13 function cToF(tempC) { | 13 function cToF(tempC) { |
| 14 return tempC * 9 / 5 + 32; | 14 return tempC * 9 / 5 + 32; |
| 15 } | 15 } |
| 16 exports.cToF = cToF; | 16 exports.cToF = cToF; |
| 17 const MAGNUS_B = 17.62; | 17 const MAGNUS_B = 17.62; |
| 18 const MAGNUS_C = 243.12; | 18 const MAGNUS_C = 243.12; |
| 19 /** The gamma function to calculate dew point. */ | |
| 20 function gammaFn(tempC, rhPct) { | 19 function gammaFn(tempC, rhPct) { |
| 21 return Math.log(rhPct / 100) + MAGNUS_B * tempC / (MAGNUS_C + tempC); | 20 return Math.log(rhPct / 100) + MAGNUS_B * tempC / (MAGNUS_C + tempC); |
| 22 } | 21 } |
| 23 /** Calculates the dew point. */ | |
| 24 function dewPointC(tempC, rhPct) { | 22 function dewPointC(tempC, rhPct) { |
| 25 const gamma = gammaFn(tempC, rhPct); | 23 const gamma = gammaFn(tempC, rhPct); |
| 26 return MAGNUS_C * gamma / (MAGNUS_B - gamma); | 24 return MAGNUS_C * gamma / (MAGNUS_B - gamma); |
| 27 } | 25 } |
| 28 exports.dewPointC = dewPointC; | 26 exports.dewPointC = dewPointC; |
| 29 }); | 27 }); |
| 30 define("graph", ["require", "exports", "math"], function (require, exports, math_1) { | 28 define("graph", ["require", "exports", "math"], function (require, exports, math_1) { |
| 31 "use strict"; | 29 "use strict"; |
| 32 Object.defineProperty(exports, "__esModule", { value: true }); | 30 Object.defineProperty(exports, "__esModule", { value: true }); |
| 33 /** The amount of time we will draw on the graph. */ | |
| 34 const HISTORY_SECONDS = 60 * 60 * 4; | 31 const HISTORY_SECONDS = 60 * 60 * 4; |
| 35 /** The amount of buffer room we will request before HISTORY_SECONDS. */ | |
| 36 const BUFFER_SECONDS = 300; | 32 const BUFFER_SECONDS = 300; |
| 37 /** | |
| 38 * Sets up everything. | |
| 39 * @param tempElement The element where temperature data is. | |
| 40 * @param dewPointElement The element where the dew point is. | |
| 41 */ | |
| 42 function setUp(root, tempElement, dewPointElement) { | 33 function setUp(root, tempElement, dewPointElement) { |
| 43 return __awaiter(this, void 0, void 0, function* () { | 34 return __awaiter(this, void 0, void 0, function* () { |
| 44 const nowTS = new Date().getTime() / 1000; | 35 const nowTS = new Date().getTime() / 1000; |
| 45 const startTS = nowTS - HISTORY_SECONDS; | 36 const startTS = nowTS - HISTORY_SECONDS; |
| 46 const query = new URL('?', location.href); | 37 const query = new URL('?', location.href); |
| 60 setUpElement(tempElement, [startTS, nowTS], tempsF); | 51 setUpElement(tempElement, [startTS, nowTS], tempsF); |
| 61 setUpElement(dewPointElement, [startTS, nowTS], dewPointsF); | 52 setUpElement(dewPointElement, [startTS, nowTS], dewPointsF); |
| 62 }); | 53 }); |
| 63 } | 54 } |
| 64 exports.setUp = setUp; | 55 exports.setUp = setUp; |
| 65 /** | |
| 66 * Sets up charting for this element. | |
| 67 * @param element The element to put a graph in. | |
| 68 * @param timeRange The `[start, end]` of the time range. | |
| 69 * @param data The data to chart. | |
| 70 */ | |
| 71 function setUpElement(element, timeRange, data) { | 56 function setUpElement(element, timeRange, data) { |
| 72 if (data.length === 0) | 57 if (data.length === 0) |
| 73 return; | 58 return; |
| 74 const chart = new Chart(element, timeRange, data); | 59 const chart = new Chart(element, timeRange, data); |
| 75 chart.resize(); | 60 chart.resize(); |
| 76 addEventListener('resize', () => chart.resize()); | 61 addEventListener('resize', () => chart.resize()); |
| 77 } | 62 } |
| 78 /** The number of degrees that the graph shows vertically. */ | |
| 79 const Y_DEGREE_RANGE = 10; | 63 const Y_DEGREE_RANGE = 10; |
| 80 const LINE_WIDTH_PX = 2; | 64 const LINE_WIDTH_PX = 2; |
| 81 const FONT_SIZE = '40px'; | 65 const FONT_SIZE = '40px'; |
| 82 class Chart { | 66 class Chart { |
| 83 /** | |
| 84 * Creates a new chart. | |
| 85 * @param element The parent element to create `<canvas>`-based chart in. | |
| 86 * @param timeRange `[start, end]` of the range to chart as Unix timestamps. | |
| 87 * @param data The data to chart. | |
| 88 */ | |
| 89 constructor(element, timeRange, data) { | 67 constructor(element, timeRange, data) { |
| 90 this.element = element; | 68 this.element = element; |
| 91 this.timeRange = timeRange; | 69 this.timeRange = timeRange; |
| 92 this.data = data; | 70 this.data = data; |
| 93 this.canvas = document.createElement('canvas'); | 71 this.canvas = document.createElement('canvas'); |
| 111 const family = getComputedStyle(this.element).fontFamily; | 89 const family = getComputedStyle(this.element).fontFamily; |
| 112 ctx.strokeStyle = stroke; | 90 ctx.strokeStyle = stroke; |
| 113 ctx.lineJoin = 'round'; | 91 ctx.lineJoin = 'round'; |
| 114 ctx.lineWidth = LINE_WIDTH_PX; | 92 ctx.lineWidth = LINE_WIDTH_PX; |
| 115 ctx.font = `bold ${FONT_SIZE} ${family}`; | 93 ctx.font = `bold ${FONT_SIZE} ${family}`; |
| 116 const yRange = calculateYRange(this.data.map(d => d[1])); | 94 const onScreenData = this.data.filter(([time, _]) => this.timeRange[0] <= time); |
| 95 const yRange = calculateYRange(onScreenData.map(d => d[1])); | |
| 117 const [fullW, fullH] = this.size(); | 96 const [fullW, fullH] = this.size(); |
| 118 const [xPad, yMargin] = this.measureMargin(ctx); | 97 const [xPad, yMargin] = this.measureMargin(ctx); |
| 119 const graphSize = [fullW - xPad, fullH]; | 98 const graphSize = [fullW - xPad, fullH]; |
| 120 ctx.beginPath(); | 99 ctx.beginPath(); |
| 121 for (const pt of this.data) { | 100 for (const pt of this.data) { |
| 135 ctx.textBaseline = 'top'; | 114 ctx.textBaseline = 'top'; |
| 136 ctx.fillText(`${niceNumber(lastPt[1])} ${this.unit}`, center[0] + 5 * LINE_WIDTH_PX, center[1] + yMargin); | 115 ctx.fillText(`${niceNumber(lastPt[1])} ${this.unit}`, center[0] + 5 * LINE_WIDTH_PX, center[1] + yMargin); |
| 137 } | 116 } |
| 138 measureMargin(ctx) { | 117 measureMargin(ctx) { |
| 139 const bbox = ctx.measureText(`−99 ${this.unit}`); | 118 const bbox = ctx.measureText(`−99 ${this.unit}`); |
| 140 const margin = 5 * LINE_WIDTH_PX + // margin to text | 119 const margin = 5 * LINE_WIDTH_PX + |
| 141 bbox.width + // max (?) width of text | 120 bbox.width + |
| 142 16; // Pixel margin to wall. | 121 16; |
| 143 return [margin, -31.5 / 2]; | 122 return [margin, -31.5 / 2]; |
| 144 } | 123 } |
| 145 size() { | 124 size() { |
| 146 const cssSize = this.element.getBoundingClientRect(); | 125 const cssSize = this.element.getBoundingClientRect(); |
| 147 return [cssSize.width, cssSize.height]; | 126 return [cssSize.width, cssSize.height]; |
| 148 } | 127 } |
| 149 } | 128 } |
| 150 function niceNumber(n) { | 129 function niceNumber(n) { |
| 151 return Math.round(n).toLocaleString('en-us').replace('-', '−'); | 130 return Math.round(n).toLocaleString('en-us').replace('-', '−'); |
| 152 } | 131 } |
| 153 /** The closest that the last point will be allowed to get to the edge. */ | |
| 154 const EDGE_FRACTION = 0.125; | 132 const EDGE_FRACTION = 0.125; |
| 155 /** | |
| 156 * Determines what the Y range of the chart should be. | |
| 157 * @param ys The Y values of the chart. | |
| 158 * @return The lowest and highest values of the range. | |
| 159 */ | |
| 160 function calculateYRange(ys) { | 133 function calculateYRange(ys) { |
| 161 const yMax = Math.max(...ys); | 134 const yMax = Math.max(...ys); |
| 162 const yMin = Math.min(...ys); | 135 const yMin = Math.min(...ys); |
| 163 const yMid = (yMin + yMax) / 2; | 136 const yMid = (yMin + yMax) / 2; |
| 164 const lastY = ys[ys.length - 1]; | 137 const lastY = ys[ys.length - 1]; |
| 165 const yLo = yMid - Y_DEGREE_RANGE / 2; | 138 const yLo = yMid - Y_DEGREE_RANGE / 2; |
| 166 const yProportion = Math.max(Math.min((lastY - yLo) / Y_DEGREE_RANGE, 1 - EDGE_FRACTION), EDGE_FRACTION); | 139 const yProportion = Math.max(Math.min((lastY - yLo) / Y_DEGREE_RANGE, 1 - EDGE_FRACTION), EDGE_FRACTION); |
| 167 const rangeLo = lastY - yProportion * Y_DEGREE_RANGE; | 140 const rangeLo = lastY - yProportion * Y_DEGREE_RANGE; |
| 168 return [rangeLo, rangeLo + Y_DEGREE_RANGE]; | 141 return [rangeLo, rangeLo + Y_DEGREE_RANGE]; |
| 169 } | 142 } |
| 170 /** | |
| 171 * Projects a Cartesian coordinate into Canvas space. | |
| 172 * | |
| 173 * @param coord The `[x, y]` coordinate to project. | |
| 174 * @param size The `[width, height]` of the context. | |
| 175 * @param xRange The range of X values in the context. | |
| 176 * @param yRange The range of Y values in the context. | |
| 177 * @return The `[x, y]` coordinate in Canvas space. | |
| 178 */ | |
| 179 function project(coord, size, xRange, yRange) { | 143 function project(coord, size, xRange, yRange) { |
| 180 const [x, y] = coord; | 144 const [x, y] = coord; |
| 181 const [xMin, xMax] = xRange; | 145 const [xMin, xMax] = xRange; |
| 182 const xSpan = xMax - xMin; | 146 const xSpan = xMax - xMin; |
| 183 const [yMin, yMax] = yRange; | 147 const [yMin, yMax] = yRange; |
