mirror of
https://github.com/ConnectedHumber/Air-Quality-Web
synced 2024-11-17 05:43:01 +00:00
Add heatmap guage to right-hand-side (part of #31)
This commit is contained in:
parent
e5303d62f0
commit
721c813599
9 changed files with 295 additions and 0 deletions
|
@ -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
|
||||
|
|
7
client_src/css/guage.css
Normal file
7
client_src/css/guage.css
Normal file
|
@ -0,0 +1,7 @@
|
|||
#canvas-guage {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 3.5em;
|
||||
|
||||
z-index: 10000;
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -13,6 +13,8 @@
|
|||
|
||||
</main>
|
||||
|
||||
<canvas id="canvas-guage" width="50" height="300"></canvas>
|
||||
|
||||
<!---------------->
|
||||
<link rel="stylesheet" href="app.css" />
|
||||
<script src="app.js" charset="utf-8"></script>
|
||||
|
|
89
client_src/js/Guage.mjs
Normal file
89
client_src/js/Guage.mjs
Normal file
|
@ -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;
|
37
client_src/js/Helpers/Canvas.mjs
Normal file
37
client_src/js/Helpers/Canvas.mjs
Normal file
|
@ -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 };
|
|
@ -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) {
|
||||
|
|
18
test/page.html
Normal file
18
test/page.html
Normal file
|
@ -0,0 +1,18 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width">
|
||||
<title>Test</title>
|
||||
<style>
|
||||
canvas {
|
||||
border: 1px dashed blue;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<canvas id="main" width="50" height="300"></canvas>
|
||||
|
||||
<script src="script.js"></script>
|
||||
</body>
|
||||
</html>
|
129
test/script.js
Normal file
129
test/script.js
Normal file
|
@ -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;
|
||||
}
|
Loading…
Reference in a new issue