diff --git a/Changelog.md b/Changelog.md index 6547c0b..e1cfcf5 100644 --- a/Changelog.md +++ b/Changelog.md @@ -1,5 +1,8 @@ # Changelog +# v0.9 - 9th May 2019 + - Add heatmap gauge at the right-hand-side + # v0.8 - 23rd April 2019 - Update heatmap colours to match the [official DEFRA standards](https://uk-air.defra.gov.uk/air-pollution/daqi?view=more-info&pollutant=pm25#pollutant) - Bugfix: Allow different reading types to be selected once more in the bottom-left diff --git a/client_src/css/guage.css b/client_src/css/guage.css new file mode 100644 index 0000000..6f8f882 --- /dev/null +++ b/client_src/css/guage.css @@ -0,0 +1,7 @@ +#canvas-guage { + position: absolute; + right: 0; + bottom: 3.5em; + + z-index: 10000; +} diff --git a/client_src/css/main.css b/client_src/css/main.css index 89944ac..9730922 100644 --- a/client_src/css/main.css +++ b/client_src/css/main.css @@ -7,6 +7,7 @@ @import "./nanomodal.css"; @import "./popup.css"; +@import "./guage.css"; /** Ensure that some assets are copied that aren't by default **/ .non-existent { diff --git a/client_src/index.html b/client_src/index.html index c2d33d7..b0e2c8e 100644 --- a/client_src/index.html +++ b/client_src/index.html @@ -13,6 +13,8 @@ + + diff --git a/client_src/js/Guage.mjs b/client_src/js/Guage.mjs new file mode 100644 index 0000000..9754019 --- /dev/null +++ b/client_src/js/Guage.mjs @@ -0,0 +1,89 @@ +"use strict"; + +import { set_hidpi_canvas, pixel_ratio } from './Helpers/Canvas.mjs'; +import { RenderGradient } from './Helpers/GradientHelpers.mjs'; + + +class Guage { + constructor(in_canvas) { + this.canvas = in_canvas; + + set_hidpi_canvas(this.canvas); + + this.context = this.canvas.getContext("2d"); + } + + set_spec(spec, max) { + this.spec = spec; + this.max = max; + } + + render() { + let guage_size = { + x: this.canvas.width * 0.1, + y: this.canvas.height * 0.05, + width: this.canvas.width * 0.3, + height: this.canvas.height * 0.9 + }; + + this.clear_canvas(); + this.render_gauge(guage_size); + this.render_labels(guage_size); + + } + + clear_canvas() { + this.context.clearRect( + 0, 0, + this.canvas.width, this.canvas.height + ); + } + + render_gauge(guage_size) { + this.context.save(); + + let gradient_spec = RenderGradient(this.spec, this.max); + // console.log(gradient_spec); + + let gradient = this.context.createLinearGradient( + 0, guage_size.y, + 0, guage_size.y + guage_size.height + ); + for (let point in gradient_spec) + gradient.addColorStop(parseFloat(point), gradient_spec[point]); + + this.context.fillStyle = gradient; + this.context.fillRect(guage_size.x, guage_size.y, guage_size.width, guage_size.height); + + this.context.restore(); + } + + render_labels(guage_size) { + this.context.save(); + + this.context.font = "12px Ubuntu, sans-serif"; + this.context.textBaseline = "middle"; + this.context.strokeStyle = "rgba(0, 0, 0, 0.5)"; + this.context.lineWidth = 1.5 * pixel_ratio; + + for (let point in this.spec) { + let value = parseFloat(point) / this.max; + + let draw_x = guage_size.x + guage_size.width + 3; + let draw_y = guage_size.y + (value * guage_size.height); + + // this.context.fillStyle = "black"; + console.log(`Writing '${point}' to (${draw_x}, ${draw_y})`); + this.context.fillText(point, draw_x, draw_y); + + this.context.beginPath(); + this.context.moveTo(guage_size.x, draw_y); + this.context.lineTo(draw_x, draw_y); + this.context.stroke(); + } + + this.context.restore(); + } +} + +export default Guage; diff --git a/client_src/js/Helpers/Canvas.mjs b/client_src/js/Helpers/Canvas.mjs new file mode 100644 index 0000000..9596568 --- /dev/null +++ b/client_src/js/Helpers/Canvas.mjs @@ -0,0 +1,37 @@ +"use strict"; + +// From https://stackoverflow.com/a/15666143/1460422 + +let pixel_ratio = (function () { + let ctx = document.createElement("canvas").getContext("2d"), + dpr = window.devicePixelRatio || 1, + bsr = ctx.webkitBackingStorePixelRatio || + ctx.mozBackingStorePixelRatio || + ctx.msBackingStorePixelRatio || + ctx.oBackingStorePixelRatio || + ctx.backingStorePixelRatio || 1; + + return dpr / bsr; +})(); + +/** + * De-blurrificates a canvas on Hi-DPI screens. + * @param {HTMLCanvasElement} canvas The canvas element to alter. + * @param {Number} width=canvas.width Optional. The width of the canvas in question. + * @param {Number} height=canvas.height Optional. The height of the canvas in question. + * @returns {HTMLCanvasElement} The canvas we operated on. Useful for daisy-chaining. + */ +function set_hidpi_canvas(canvas, width, height) { + width = width || canvas.width; + height = height || canvas.height; + + canvas.width = width * pixel_ratio; + canvas.height = height * pixel_ratio; + canvas.style.width = width + "px"; + canvas.style.height = height + "px"; + // canvas.getContext("2d").setTransform(pixel_ratio, 0, 0, pixel_ratio, 0, 0); + return canvas; +} + + +export { pixel_ratio, set_hidpi_canvas }; diff --git a/client_src/js/LayerHeatmap.mjs b/client_src/js/LayerHeatmap.mjs index 9e972ac..e0db8b1 100644 --- a/client_src/js/LayerHeatmap.mjs +++ b/client_src/js/LayerHeatmap.mjs @@ -6,6 +6,7 @@ import Config from './Config.mjs'; import GetFromUrl from './Helpers/GetFromUrl.mjs'; import { RenderGradient } from './Helpers/GradientHelpers.mjs'; +import Guage from './Guage.mjs'; class LayerHeatmap { @@ -124,6 +125,8 @@ class LayerHeatmap { } }; + this.guage = new Guage(document.getElementById("canvas-guage")); + this.reading_cache = new Map(); } @@ -195,6 +198,12 @@ class LayerHeatmap { delete this.overlay_config.gradient; } + this.guage.set_spec( + this.reading_type_configs[this.reading_type].gradient, + this.reading_type_configs[this.reading_type].max + ); + this.guage.render(); + try { this.set_data(await this.fetch_data(this.datetime, this.reading_type)); } catch(error) { diff --git a/test/page.html b/test/page.html new file mode 100644 index 0000000..d7c98b0 --- /dev/null +++ b/test/page.html @@ -0,0 +1,18 @@ + + + + + + Test + + + + + + + + diff --git a/test/script.js b/test/script.js new file mode 100644 index 0000000..e842dd5 --- /dev/null +++ b/test/script.js @@ -0,0 +1,129 @@ +window.spec = { + "0": "#9CFF9C", "5.5": "#9CFF9C", // Low 1 + "17.5": "#31FF00", // Low 2 + "29.5": "#31CF00", // Low 3 + "38.5": "#FFFF00", // Moderate 1 + "44.5": "#FFCF00", // Moderate 2 + "50.5": "#FF9A00", // Moderate 3 + "56": "#FF6464", // High 1 + "61.5": "#FF0000", // High 2 + "67.5": "#990000", // High 3 + "72.5": "#CE30FF", "75": "#CE30FF", // Very high +} +window.max = 75; + +window.addEventListener("load", (_event) => { + let canvas = document.getElementById("main"); + + let guage = new Guage(canvas, window.spec, window.max); + guage.render(); +}); + +window.pixel_ratio = (function () { + let ctx = document.createElement("canvas").getContext("2d"), + dpr = window.devicePixelRatio || 1, + bsr = ctx.webkitBackingStorePixelRatio || + ctx.mozBackingStorePixelRatio || + ctx.msBackingStorePixelRatio || + ctx.oBackingStorePixelRatio || + ctx.backingStorePixelRatio || 1; + + return dpr / bsr; +})(); + +function set_hidpi_canvas(canvas, width, height) { + width = width || canvas.width; + height = height || canvas.height; + + canvas.width = width * pixel_ratio; + canvas.height = height * pixel_ratio; + canvas.style.width = width + "px"; + canvas.style.height = height + "px"; + // canvas.getContext("2d").setTransform(pixel_ratio, 0, 0, pixel_ratio, 0, 0); + return canvas; +} + +class Guage { + constructor(in_canvas, in_spec, in_max) { + this.canvas = in_canvas; + this.spec = in_spec; + this.max = in_max; + + set_hidpi_canvas(this.canvas); + + // TODO: Use Helpers/Canvas.mjs here + this.pixelRatio = (function () { + var ctx = document.createElement("canvas").getContext("2d"), + dpr = window.devicePixelRatio || 1, + bsr = ctx.webkitBackingStorePixelRatio || + ctx.mozBackingStorePixelRatio || + ctx.msBackingStorePixelRatio || + ctx.oBackingStorePixelRatio || + ctx.backingStorePixelRatio || 1; + + return dpr / bsr; + })(); + + this.context = this.canvas.getContext("2d"); + } + + render() { + let guage_size = { + x: this.canvas.width * 0.1, + y: this.canvas.height * 0.05, + width: this.canvas.width * 0.3, + height: this.canvas.height * 0.9 + }; + + // --------------------------------------------- + // Draw the guage + this.context.save(); + let gradient_spec = RenderGradient(spec, max); + console.log(gradient_spec); + + let gradient = this.context.createLinearGradient( + 0, guage_size.y, + 0, guage_size.y + guage_size.height + ); + for (let point in gradient_spec) + gradient.addColorStop(parseFloat(point), gradient_spec[point]); + + this.context.fillStyle = gradient; + this.context.fillRect(guage_size.x, guage_size.y, guage_size.width, guage_size.height); + this.context.restore(); + // --------------------------------------------- + // Draw the numbers + + this.context.save(); + this.context.font = "12px Ubuntu, sans-serif"; + this.context.textBaseline = "middle"; + this.context.strokeStyle = "rgba(0, 0, 0, 0.5)"; + this.context.lineWidth = 1.5 * window.pixel_ratio; + + for (let point in spec) { + let value = parseFloat(point) / max; + + let draw_x = guage_size.x + guage_size.width + 3; + let draw_y = guage_size.y + (value * guage_size.height); + + // this.context.fillStyle = "black"; + console.log(`Writing '${point}' to (${draw_x}, ${draw_y})`); + this.context.fillText(point, draw_x, draw_y); + + this.context.beginPath(); + this.context.moveTo(guage_size.x, draw_y); + this.context.lineTo(draw_x, draw_y); + this.context.stroke(); + } + this.context.restore(); + } +} + +function RenderGradient(stops, max) { + let result = {}; + + for (let value in stops) + result[value / max] = stops[value]; + + return result; +}