"use strict"; // import CreateElement from 'dom-create-element-query-selector'; // We're using the git repo for now until an update is released, and rollup doesn't like that apparently import CreateElement from '../../node_modules/dom-create-element-query-selector/src/index.js'; import moment from 'moment'; // import Chart from 'chart.js'; // Chart.js pollutes the global scope, but the main entry point is going to change soon in v2.8 - which should fix our issue here import Chart from '../../node_modules/chart.js/dist/Chart.bundle.min.js'; import GetFromUrl from './Helpers/GetFromUrl.mjs'; import Postify from './Helpers/Postify.mjs'; class DeviceReadingDisplay { constructor(in_config, in_device_id) { this.config = in_config; /** The ID of the device to display a graph for. @type {int} */ this.device_id = in_device_id; /** The current reading type to display a graph for. @type {Object} */ this.reading_type = null; this.default_colours = { borderColor: "hsla(0, 0%, 50%, 1)", backgroundColor: "hsla(0, 0%, 65%, 0.5)" }; this.reading_type_defs = { "PM10": { suggestedMin: 0, suggestedMax: 110, borderColor: "hsla(0, 82%, 56%, 1)", backgroundColor: "hsla(14, 94%, 71%, 0.57)" }, "PM25": { suggestedMin: 0, suggestedMax: 75, borderColor: "hsla(33, 70%, 51%, 1)", backgroundColor: "hsla(28, 77%, 58%, 0.63)" }, "temperature": { suggestedMin: 0, suggestedMax: 20, borderColor: "hsla(0, 77%, 45%, 1)", backgroundColor: "hsla(0, 61%, 58%, 0.59)" }, "humidity": { suggestedMin: 0, suggestedMax: 100, borderColor: "hsla(184, 69%, 40%, 1)", backgroundColor: "hsla(188, 53%, 46%, 0.58)" }, "pressure": { borderColor: "hsla(258, 67%, 40%, 1)", backgroundColor: "hsla(249, 56%, 40%, 0.66)" } } } async setup(default_reading_type) { // Create the display element first, as we need it to be immediately available for inclusion in the popup window /** @type {HTMLElement} */ this.display = CreateElement("div.chart-device-data", CreateElement("canvas.canvas-chart"), CreateElement("ul.reading-types") ); await this.fetch_reading_types(); this.reading_type = this.reading_types.find((type) => type.id == default_reading_type); // Create the reading type buttons let reading_type_list = this.display.querySelector(".reading-types"); for(let reading_type of this.reading_types) { let new_element = CreateElement("li", CreateElement("button", reading_type.friendly_text) ); let button = new_element.querySelector("button"); if(reading_type.id == this.reading_type.id) button.classList.add("selected"); button.addEventListener("click", this.switch_graph_type_handler.bind(this)); new_element.dataset.id = reading_type.id; reading_type_list.appendChild(new_element); } // ---------------------------------------------------- // Setup the chart itself this.setup_chart(); } setup_chart() { this.chart = new Chart(this.display.querySelector("canvas").getContext("2d"), { type: "line", data: { // We need to define an initial dataset here because otherwise // Chart.js gets confused 'cause it has nothing to animate from labels: [], datasets: [] }, options: { scales: { xAxes: [{ type: "time", time: { format: "YYYY-MM-DD HH:mm", tooltipFormat: 'll HH:mm' }, scaleLabel: { display: true, labelString: "Time" } }], yAxes: [{ ticks: { }, scaleLabel: { display: true, labelString: "Value" } }] } } }); this.update_chart(); } async get_data() { let new_data = null; try { new_data = JSON.parse(await GetFromUrl(`${this.config.api_root}?` + Postify({ action: "device-data", "device-id": this.device_id, "reading-type": this.reading_type.id, start: moment().subtract(1, "days").toISOString(), end: moment().toISOString(), "average-seconds": 3600 }))); } catch(error) { // TODO: Display a nice error message here instead of an alert() alert(error); console.error(error); return null; } console.log("[marker/popup/device-graph] Fetched data:", new_data); return new_data.map((data_point) => { return { t: moment(data_point.datetime), y: Math.round(data_point.value*10000)/10000 }}); } async fetch_reading_types() { this.reading_types = JSON.parse(await GetFromUrl(`${this.config.api_root}?action=list-reading-types&device-id=${this.device_id}`)); } async switch_graph_type_handler(event) { // Figure out what the new reading type is this.reading_type = this.reading_types.find((type) => type.id == event.target.parentNode.dataset.id); console.log("[marker/device-graph] Reading type is now", this.reading_type); // Update the button list to highlight the newly-selected reading type, but only if we managed to update the chart successfully if(await this.update_chart()) { // Remove the selected class from the old one(s?) event.target.parentNode.parentNode.querySelectorAll(`button`) .forEach((next_button) => next_button.classList.remove("selected")); // Add the selected class to the new one event.target.classList.add("selected"); } } async update_chart() { this.chart.data.datasets.length = 0; // Get the Chart.js data object let new_data_obj = { label: this.reading_type.friendly_text, data: await this.get_data() }; if(new_data_obj.data == null) return false; // Update the colour & suggested min/max values let def = this.reading_type_defs[this.reading_type.id], y_axis = this.chart.options.scales.yAxes[0]; if(typeof def !== "undefined") { new_data_obj.borderColor = def.borderColor; new_data_obj.backgroundColor = def.backgroundColor; } else { new_data_obj.borderColor = this.default_colours.borderColor; new_data_obj.backgroundColor = this.default_colours.backgroundColor; } if(typeof def !== "undefined" && typeof def.suggestedMin !== "undefined") { y_axis.ticks.suggestedMin = def.suggestedMin; y_axis.ticks.suggestedMax = def.suggestedMax; } else { delete y_axis.ticks.suggestedMin; delete y_axis.ticks.suggestedMax; } this.chart.data.datasets.push(new_data_obj); // Update the x axis labels this.chart.data.labels = new_data_obj.data.map((point) => point.t); // Update the chart this.chart.update(); return true; } } export default DeviceReadingDisplay;