"use strict";
import {
chartExportOptions,
createTitleForHeatmap,
createSubtitleForHeatmap,
} from "./chartHelpers.mjs";
/**
* Format the response from SensorThings API to make it suitable for use in a heatmap
* @param {Array} obsArray Array of observations (timestamp + value) that is response from SensorThings API
* @returns {Array} Array of formatted observations suitable for use in a heatmap
*/
const formatSensorThingsApiResponseForHeatMap = function (obsArray) {
if (!obsArray) return;
return obsArray.map((obs) => {
// Get the date/time string; first element in input array; remove trailing "Z"
const obsDateTimeInput = obs[0].slice(0, -1);
// Get the "date" part of an observation
const obsDateInput = obs[0].slice(0, 10);
// Create Date objects
const obsDateTime = new Date(obsDateTimeInput);
const obsDate = new Date(obsDateInput);
// x-axis -> timestamp; will be the same for observations from the same date
const timestamp = Date.parse(obsDate);
// y-axis -> hourOfDay
const hourOfDay = obsDateTime.getHours();
// value -> the observation's value; second element in input array
const value = obs[1];
return [timestamp, hourOfDay, value];
});
};
/**
* Calculate the minimum and maximum values for a heatmap's color axis
* @param {Array} formattedObsArrHeatmap Response from SensorThings API formatted for use in a heatmap
* @returns {Object} An object containing the minimum and maximum values
*/
const calculateMinMaxValuesForHeatmapColorAxis = function (
formattedObsArrHeatmap
) {
// The observation value is the third element in array
const obsValueArr = formattedObsArrHeatmap.map((obs) => obs[2]);
// Extract integer part
const minValue = Math.trunc(Math.min(...obsValueArr));
const maxValue = Math.trunc(Math.max(...obsValueArr));
// Calculate the closest multiple of 5
const minObsValue =
minValue > 0 ? minValue - (minValue % 5) : minValue + (minValue % 5);
const maxObsValue = maxValue + (5 - (maxValue % 5));
return { minObsValue, maxObsValue };
};
/**
* Draw a heatmap using Highcharts library
* @param {Array} formattedObsArrayForHeatmap Response from SensorThings API formatted for use in a heatmap. Currently, only raw observations are supported, i.e. no aggregation
* @param {Object} extractedFormattedDatastreamProperties An object that contains arrays of formatted Datastream properties
* @returns {undefined} undefined
*/
const drawHeatMapHighcharts = function (
formattedObsArrayForHeatmap,
extractedFormattedDatastreamProperties
) {
// Arrays of datastream properties
const {
datastreamDescriptionsArr,
datastreamNamesArr,
phenomenonNamesArr,
unitOfMeasurementSymbolsArr,
} = extractedFormattedDatastreamProperties;
const [DATASTREAM_DESCRIPTION] = datastreamDescriptionsArr;
const [DATASTREAM_NAME] = datastreamNamesArr;
const [PHENOMENON_NAME] = phenomenonNamesArr;
const [PHENOMENON_SYMBOL] = unitOfMeasurementSymbolsArr;
const TEXT_CHART_TITLE = createTitleForHeatmap(phenomenonNamesArr);
const TEXT_CHART_SUBTITLE = createSubtitleForHeatmap(datastreamNamesArr);
const {
minObsValue: MINIMUM_VALUE_COLOR_AXIS,
maxObsValue: MAXIMUM_VALUE_COLOR_AXIS,
} = calculateMinMaxValuesForHeatmapColorAxis(formattedObsArrayForHeatmap);
Highcharts.chart("chart-heatmap", {
chart: {
type: "heatmap",
zoomType: "x",
},
boost: {
useGPUTranslations: true,
},
title: {
text: TEXT_CHART_TITLE,
align: "center",
x: 40,
},
subtitle: {
text: TEXT_CHART_SUBTITLE,
align: "center",
x: 40,
},
xAxis: {
type: "datetime",
// min: Date.UTC(2017, 0, 1),
// max: Date.UTC(2017, 11, 31, 23, 59, 59),
labels: {
align: "left",
x: 5,
y: 14,
format: "{value:%B}", // long month
},
showLastLabel: false,
tickLength: 16,
},
yAxis: {
title: {
text: null,
},
labels: {
format: "{value}:00",
},
minPadding: 0,
maxPadding: 0,
startOnTick: false,
endOnTick: false,
tickPositions: [0, 3, 6, 9, 12, 15, 18, 21, 24],
tickWidth: 1,
min: 0,
max: 23,
reversed: true,
},
colorAxis: {
stops: [
[0, "#3060cf"],
[0.5, "#fffbbc"],
[0.9, "#c4463a"],
[1, "#c4463a"],
],
min: MINIMUM_VALUE_COLOR_AXIS,
max: MAXIMUM_VALUE_COLOR_AXIS,
startOnTick: false,
endOnTick: false,
labels: {
format: `{value}${PHENOMENON_SYMBOL}`,
},
},
exporting: chartExportOptions,
tooltip: {
formatter() {
const headerString = `${PHENOMENON_NAME}
`;
// Check whether the point value is null or not; this will determine the string that we'll render
const pointString =
this.point.value === null
? `${Highcharts.dateFormat("%e %b, %Y", this.point.x)} ${
this.point.y
}:00:00 null`
: `${Highcharts.dateFormat("%e %b, %Y", this.point.x)} ${
this.point.y
}:00:00 ${this.point.value.toFixed(
2
)} ${PHENOMENON_SYMBOL}`;
return headerString + pointString;
},
},
series: [
{
data: formattedObsArrayForHeatmap,
boostThreshold: 100,
borderWidth: 0,
nullColor: "#525252",
colsize: 24 * 36e5, // one day
turboThreshold: Number.MAX_VALUE, // #3404, remove after 4.0.5 release
},
],
});
};
export { formatSensorThingsApiResponseForHeatMap, drawHeatMapHighcharts };