From 8df4c8881c8c9333fd51f9133e9de5fb8572fcfb Mon Sep 17 00:00:00 2001 From: Pithon Kabiro Date: Wed, 22 Sep 2021 19:52:22 +0200 Subject: [PATCH] Add calculation of average to aggregation logic --- index.html | 1 + public/js/appChart.js | 252 +++++++++-- public/js/dropDownList.js | 21 +- public/js/src_modules/aggregate.js | 234 ---------- public/js/src_modules/aggregate.mjs | 420 ++++++++++++++++++ ...reateUrl.js => baseUrlPlusQueryParams.mjs} | 0 .../src_modules/calculateTemperatureDiff.mjs | 106 +++++ .../{chartColumn.js => chartColumn.mjs} | 6 + public/js/src_modules/chartExport.mjs | 9 + .../{chartHeatmap.js => chartHeatmap.mjs} | 33 +- .../{chartLine.js => chartLine.mjs} | 32 +- ...artScatterPlot.js => chartScatterPlot.mjs} | 20 +- .../{fetchData.js => fetchData.mjs} | 196 +------- public/js/src_modules/getDatastreamId.mjs | 95 ++++ ...adingIndicator.js => loadingIndicator.mjs} | 0 15 files changed, 930 insertions(+), 495 deletions(-) delete mode 100644 public/js/src_modules/aggregate.js create mode 100644 public/js/src_modules/aggregate.mjs rename public/js/src_modules/{createUrl.js => baseUrlPlusQueryParams.mjs} (100%) create mode 100644 public/js/src_modules/calculateTemperatureDiff.mjs rename public/js/src_modules/{chartColumn.js => chartColumn.mjs} (97%) create mode 100644 public/js/src_modules/chartExport.mjs rename public/js/src_modules/{chartHeatmap.js => chartHeatmap.mjs} (85%) rename public/js/src_modules/{chartLine.js => chartLine.mjs} (81%) rename public/js/src_modules/{chartScatterPlot.js => chartScatterPlot.mjs} (96%) rename public/js/src_modules/{fetchData.js => fetchData.mjs} (63%) create mode 100644 public/js/src_modules/getDatastreamId.mjs rename public/js/src_modules/{loadingIndicator.js => loadingIndicator.mjs} (100%) diff --git a/index.html b/index.html index caf7012..efcc355 100644 --- a/index.html +++ b/index.html @@ -42,6 +42,7 @@ + diff --git a/public/js/appChart.js b/public/js/appChart.js index 85c3007..a73d8a7 100644 --- a/public/js/appChart.js +++ b/public/js/appChart.js @@ -1,40 +1,44 @@ "use strict"; -import { BASE_URL, QUERY_PARAMS_COMBINED } from "./src_modules/createUrl.js"; +import { + BASE_URL, + QUERY_PARAMS_COMBINED, +} from "./src_modules/baseUrlPlusQueryParams.mjs"; import { formatSensorThingsApiResponseForLineChart, drawLineChartHighcharts, -} from "./src_modules/chartLine.js"; +} from "./src_modules/chartLine.mjs"; import { formatSensorThingsApiResponseForHeatMap, drawHeatMapHighcharts, -} from "./src_modules/chartHeatmap.js"; +} from "./src_modules/chartHeatmap.mjs"; import { formatSensorThingsApiResponseForScatterPlot, drawScatterPlotHighcharts, -} from "./src_modules/chartScatterPlot.js"; +} from "./src_modules/chartScatterPlot.mjs"; import { formatAggregationResultForColumnChart, drawColumnChartHighcharts, -} from "./src_modules/chartColumn.js"; +} from "./src_modules/chartColumn.mjs"; import { formatDatastreamMetadataForChart, extractPropertiesFromFormattedDatastreamMetadata, getMetadataPlusObservationsFromSingleOrMultipleDatastreams, - calculateVorlaufMinusRuecklaufTemperature, -} from "./src_modules/fetchData.js"; +} from "./src_modules/fetchData.mjs"; + +import { calculateVorlaufMinusRuecklaufTemperature } from "./src_modules/calculateTemperatureDiff.mjs"; import { - calculateSumOfObservationValuesWithinDatesInterval, - calculateSumOfObservationValuesWithinMonthInterval, + calculateSumOfObservationValuesWithinInterval, extractUniqueCalendarDatesFromTimestamp, extractUniqueCalendarMonthsFromCalendarDates, -} from "./src_modules/aggregate.js"; + calculateAverageOfObservationValuesWithinInterval, +} from "./src_modules/aggregate.mjs"; /** * Test plotting of temp difference (dT) using heatmap @@ -169,7 +173,7 @@ const testLineChartMultipleSeries = async function () { }; /** - * Test drawing of column chart using aggregation result + * Test drawing of column chart using aggregation / sum result - monthly */ const drawColumnChartMonthlySumTest = async function () { const sensorsOfInterestNestedArr = [ @@ -200,42 +204,213 @@ const drawColumnChartMonthlySumTest = async function () { extractUniqueCalendarMonthsFromCalendarDates(uniqueCalendarDatesArr) ); - // Calculate sum of values of observations - daily - // Note the two nested `map` methods - const observationsSumDailyNestedArr = uniqueCalendarDatesNestedArr.map( - (uniqueCalendarDatesArr, i) => - uniqueCalendarDatesArr.map((uniqueCalendarDate) => - calculateSumOfObservationValuesWithinDatesInterval( - observationsNestedArr[i], - "60 min", - uniqueCalendarDate, - uniqueCalendarDate - ) + // Calculate sum of values of observations - monthly + const observationsSumMonthlyNestedArr = + calculateSumOfObservationValuesWithinInterval( + observationsNestedArr, + "60min", + uniqueCalendarMonthsNestedArr, + "monthly" + ); + + // Format the observations + const formattedObservationsSumMonthlyNestedArr = + observationsSumMonthlyNestedArr.map((obsSumMonthlyArr, i) => + formatAggregationResultForColumnChart( + uniqueCalendarMonthsNestedArr[i], + obsSumMonthlyArr ) + ); + + // Format the metadata + const formattedMetadataNestedArr = metadataNestedArr.map((metadataObj) => + formatDatastreamMetadataForChart(metadataObj) ); - // Calculate sum of values of observations - monthly - // Note the two nested `map` methods - const observationsSumMonthlyNestedArr = uniqueCalendarMonthsNestedArr.map( - (uniqueCalendarMonthsArr, i) => - uniqueCalendarMonthsArr.map((uniqueCalendarMonth) => - calculateSumOfObservationValuesWithinMonthInterval( - observationsNestedArr[i], - "60 min", - uniqueCalendarMonth - ) + // Extract the formatted metadata properties + const extractedFormattedDatastreamProperties = + extractPropertiesFromFormattedDatastreamMetadata( + formattedMetadataNestedArr + ); + + drawColumnChartHighcharts( + formattedObservationsSumMonthlyNestedArr, + extractedFormattedDatastreamProperties + ); +}; + +/** + * Test drawing of column chart using aggregation / sum result - daily + */ +const drawColumnChartDailySumTest = async function () { + const sensorsOfInterestNestedArr = [ + ["125", "vl", "60min"], + ["225", "vl", "60min"], + ]; + + const observationsPlusMetadata = + await getMetadataPlusObservationsFromSingleOrMultipleDatastreams( + BASE_URL, + QUERY_PARAMS_COMBINED, + sensorsOfInterestNestedArr + ); + + // Extract the observations and metadata for each sensor + // Array elements in same order as input array + const [observationsNestedArr, metadataNestedArr] = observationsPlusMetadata; + + // Unique calendar dates + const uniqueCalendarDatesNestedArr = observationsNestedArr.map( + (observationsArr) => + extractUniqueCalendarDatesFromTimestamp(observationsArr) + ); + + // Calculate sum of values of observations - daily + const observationsSumDailyNestedArr = + calculateSumOfObservationValuesWithinInterval( + observationsNestedArr, + "60min", + uniqueCalendarDatesNestedArr, + "daily" + ); + + // Format the observations - daily + const formattedObservationsSumDailyNestedArr = + observationsSumDailyNestedArr.map((obsSumDailyArr, i) => + formatAggregationResultForColumnChart( + uniqueCalendarDatesNestedArr[i], + obsSumDailyArr ) + ); + + // Format the metadata + const formattedMetadataNestedArr = metadataNestedArr.map((metadataObj) => + formatDatastreamMetadataForChart(metadataObj) + ); + + // Extract the formatted metadata properties + const extractedFormattedDatastreamProperties = + extractPropertiesFromFormattedDatastreamMetadata( + formattedMetadataNestedArr + ); + + drawColumnChartHighcharts( + formattedObservationsSumDailyNestedArr, + extractedFormattedDatastreamProperties + ); +}; + +/** + * Test drawing of line chart using aggregation / average result - monthly + */ +const drawLineChartMonthlyAverageTest = async function () { + const sensorsOfInterestNestedArr = [ + ["125", "vl", "60min"], + ["225", "vl", "60min"], + ]; + + const observationsPlusMetadata = + await getMetadataPlusObservationsFromSingleOrMultipleDatastreams( + BASE_URL, + QUERY_PARAMS_COMBINED, + sensorsOfInterestNestedArr + ); + + // Extract the observations and metadata for each sensor + // Array elements in same order as input array + const [observationsNestedArr, metadataNestedArr] = observationsPlusMetadata; + + // Unique calendar dates + const uniqueCalendarDatesNestedArr = observationsNestedArr.map( + (observationsArr) => + extractUniqueCalendarDatesFromTimestamp(observationsArr) ); + // Unique calendar months + const uniqueCalendarMonthsNestedArr = uniqueCalendarDatesNestedArr.map( + (uniqueCalendarDatesArr) => + extractUniqueCalendarMonthsFromCalendarDates(uniqueCalendarDatesArr) + ); + + // Calculate average of values of observations - monthly + const observationsAverageMonthlyNestedArr = + calculateAverageOfObservationValuesWithinInterval( + observationsNestedArr, + "60min", + uniqueCalendarMonthsNestedArr, + "monthly" + ); + // Format the observations - const formattedObservationsNestedArr = observationsSumMonthlyNestedArr.map( - (obsSumMonthlyArr, i) => + const formattedObservationsAverageMonthlyNestedArr = + observationsAverageMonthlyNestedArr.map((obsAverageMonthlyArr, i) => formatAggregationResultForColumnChart( uniqueCalendarMonthsNestedArr[i], - obsSumMonthlyArr + obsAverageMonthlyArr ) + ); + + // Format the metadata + const formattedMetadataNestedArr = metadataNestedArr.map((metadataObj) => + formatDatastreamMetadataForChart(metadataObj) + ); + + // Extract the formatted metadata properties + const extractedFormattedDatastreamProperties = + extractPropertiesFromFormattedDatastreamMetadata( + formattedMetadataNestedArr + ); + + drawLineChartHighcharts( + formattedObservationsAverageMonthlyNestedArr, + extractedFormattedDatastreamProperties + ); +}; + +/** + * Test drawing of line chart using aggregation / average result - daily + */ +const drawLineChartDailyAverageTest = async function () { + const sensorsOfInterestNestedArr = [ + ["125", "vl", "60min"], + ["225", "vl", "60min"], + ]; + + const observationsPlusMetadata = + await getMetadataPlusObservationsFromSingleOrMultipleDatastreams( + BASE_URL, + QUERY_PARAMS_COMBINED, + sensorsOfInterestNestedArr + ); + + // Extract the observations and metadata for each sensor + // Array elements in same order as input array + const [observationsNestedArr, metadataNestedArr] = observationsPlusMetadata; + + // Unique calendar dates + const uniqueCalendarDatesNestedArr = observationsNestedArr.map( + (observationsArr) => + extractUniqueCalendarDatesFromTimestamp(observationsArr) ); + // Calculate average of values of observations - daily + const observationsAverageDailyNestedArr = + calculateAverageOfObservationValuesWithinInterval( + observationsNestedArr, + "60min", + uniqueCalendarDatesNestedArr, + "daily" + ); + + // Format the observations - daily + const formattedObservationsAverageDailyNestedArr = + observationsAverageDailyNestedArr.map((obsAverageDailyArr, i) => + formatAggregationResultForColumnChart( + uniqueCalendarDatesNestedArr[i], + obsAverageDailyArr + ) + ); + // Format the metadata const formattedMetadataNestedArr = metadataNestedArr.map((metadataObj) => formatDatastreamMetadataForChart(metadataObj) @@ -247,8 +422,8 @@ const drawColumnChartMonthlySumTest = async function () { formattedMetadataNestedArr ); - drawColumnChartHighcharts( - formattedObservationsNestedArr, + drawLineChartHighcharts( + formattedObservationsAverageDailyNestedArr, extractedFormattedDatastreamProperties ); }; @@ -257,3 +432,6 @@ const drawColumnChartMonthlySumTest = async function () { // drawHeatmapHCUsingTempDifference(); // testLineChartMultipleSeries(); // drawColumnChartMonthlySumTest(); +// drawColumnChartDailySumTest(); +// drawLineChartMonthlyAverageTest(); +// drawLineChartDailyAverageTest(); diff --git a/public/js/dropDownList.js b/public/js/dropDownList.js index 9bda35e..e498776 100644 --- a/public/js/dropDownList.js +++ b/public/js/dropDownList.js @@ -1,27 +1,30 @@ "use strict"; -import { BASE_URL, QUERY_PARAMS_COMBINED } from "./src_modules/createUrl.js"; +import { + BASE_URL, + QUERY_PARAMS_COMBINED, +} from "./src_modules/baseUrlPlusQueryParams.mjs"; import { formatDatastreamMetadataForChart, extractPropertiesFromFormattedDatastreamMetadata, getMetadataPlusObservationsFromSingleOrMultipleDatastreams, -} from "./src_modules/fetchData.js"; +} from "./src_modules/fetchData.mjs"; import { formatSensorThingsApiResponseForLineChart, drawLineChartHighcharts, -} from "./src_modules/chartLine.js"; +} from "./src_modules/chartLine.mjs"; import { formatSensorThingsApiResponseForHeatMap, drawHeatMapHighcharts, -} from "./src_modules/chartHeatmap.js"; +} from "./src_modules/chartHeatmap.mjs"; import { showLoadingSpinner, hideLoadingSpinner, -} from "./src_modules/loadingIndicator.js"; +} from "./src_modules/loadingIndicator.mjs"; const buildingsAvailableSensorsArr = [ ["--Select--", "", ""], @@ -276,13 +279,13 @@ const selectChartTypeFromDropDown = async function () { const [observationsNestedArr, metadataNestedArr] = observationsPlusMetadata; // Create formatted array(s) for observations - line chart - const formattedObsLineChartArr = observationsNestedArr.map( + const formattedObsLineChartNestedArr = observationsNestedArr.map( (observationsArr) => formatSensorThingsApiResponseForLineChart(observationsArr) ); // Create formatted array(s) for observations - heatmap - const formattedObsHeatMapArr = observationsNestedArr.map( + const formattedObsHeatMapNestedArr = observationsNestedArr.map( (observationsArr) => formatSensorThingsApiResponseForHeatMap(observationsArr) ); @@ -298,14 +301,14 @@ const selectChartTypeFromDropDown = async function () { if (selectedChartType === "Line") { drawLineChartHighcharts( - formattedObsLineChartArr, + formattedObsLineChartNestedArr, extractedFormattedDatastreamProperties ); } else if (selectedChartType === "Heatmap") { // First need to extract the formatted observations from the nested array // Heatmap only needs one set of formatted observation values drawHeatMapHighcharts( - ...formattedObsHeatMapArr, + ...formattedObsHeatMapNestedArr, extractedFormattedDatastreamProperties ); } diff --git a/public/js/src_modules/aggregate.js b/public/js/src_modules/aggregate.js deleted file mode 100644 index 5c4bafb..0000000 --- a/public/js/src_modules/aggregate.js +++ /dev/null @@ -1,234 +0,0 @@ -"use strict"; - -/** - * Create 24-hour time strings for a time interval delimited by a start time and an end time. It is assumed that the start time is at "00:00:00" and the end time is at "23:45:00" (when the sampling rate of observations is 15 min) or "23:00:00" (when the sampling rate of observations is 60 min) - * @param {String} phenomenonSamplingRate The sampling rate of the phenomenon of interest represented as a string, e.g. "15 min", "60 min" - * @returns {Array} An array of two 24-hour strings representing the start time and end time - */ -const createTimeStringsForInterval = function (phenomenonSamplingRate) { - const fifteenMinutes = "15 min"; - const sixtyMinutes = "60 min"; - - const startTime = "00:00:00"; - const endTimeFifteenMinutes = "23:45:00"; - const endTimeSixtyMinutes = "23:00:00"; - - if ( - phenomenonSamplingRate !== fifteenMinutes && - phenomenonSamplingRate !== sixtyMinutes - ) - return; - - // 15 min sampling rate - if (phenomenonSamplingRate === fifteenMinutes) { - return [startTime, endTimeFifteenMinutes]; - } - - // 60 min sampling rate - if (phenomenonSamplingRate === sixtyMinutes) { - return [startTime, endTimeSixtyMinutes]; - } -}; - -/** - * Create an ISO 8601 date and time string - * @param {String} inputCalendarDate Calendar date string in "YYYY-MM-DD" format - * @param {String} inputTwentyFourHourTime 24-hour time string in "hh:mm:ss" format - * @returns {String} An ISO 8601 date and time string - */ -const createIso8601DateTimeString = function ( - inputCalendarDate, - inputTwentyFourHourTime -) { - return `${inputCalendarDate}T${inputTwentyFourHourTime}.000Z`; -}; - -/** - * Check whether a year is a leap year or not - * @param {Number} year Integer representing the year - * @returns {Boolean} true if leap year, false if not - */ -const checkIfLeapYear = function (year) { - return (year % 4 == 0 && year % 100 != 0) || year % 400 == 0; -}; - -/** - * Calculate the index of a timestamp in an array of timestamps - * @param {Array} inputTimestampArr An array of timestamps, extracted from an array of observations - * @param {String} timestampOfInterest A string representing the timestamp of interest in ISO 8601 format - * @returns {Number} An integer representing the index of the timestamp of interest in the array of timestamps - */ -const getIndexOfTimestamp = function (inputTimestampArr, timestampOfInterest) { - const timestampIndex = inputTimestampArr.findIndex( - (timestamp) => timestamp === timestampOfInterest - ); - - // If the timestamp does not exist in the timestamp array - if (timestampIndex === -1) - throw new Error( - "A start or end timestamp could not be found in the timestamp array" - ); - - // If the timestamp exists in the timestamp array - return timestampIndex; -}; - -/** - * Calculate the sum of observation values within a time interval delimited by a start date and end date. The start date may be the same as the end date. - * @param {Array} obsArray An array of observations (timestamp + value) that is response from SensorThings API - * @param {String} samplingRate The sampling rate of observations as a string, e.g. "15 min", "60 min" - * @param {String} startDate A 24-hour date string representing the start date - * @param {String} endDate A 24-hour date string representing the end date - * @returns {Number} A floating-point number representing the sum of observation values - */ -const calculateSumOfObservationValuesWithinDatesInterval = function ( - obsArray, - samplingRate, - startDate, - endDate -) { - // Extract the timestamps and values from the observations - const obsTimestampArr = obsArray.map((obs) => obs[0]); - const obsValuesArr = obsArray.map((obs) => obs[1]); - - // Create array of 24-hour strings for the start and end of interval - const startPlusEndTimeStrings = createTimeStringsForInterval(samplingRate); - - // Extract 24-hour strings for the start and end of interval - const [startTimeString, endTimeString] = startPlusEndTimeStrings; - - // Create ISO 8601 strings for the start and end of interval - const startIso8601DateTimeString = createIso8601DateTimeString( - startDate, - startTimeString - ); - const endIso8601DateTimeString = createIso8601DateTimeString( - endDate, - endTimeString - ); - - // Calculate the indexes of the timestamps for the start and end of interval - const indexStartTimestamp = getIndexOfTimestamp( - obsTimestampArr, - startIso8601DateTimeString - ); - const indexEndTimestamp = getIndexOfTimestamp( - obsTimestampArr, - endIso8601DateTimeString - ); - - // Extract the observations that fall within our time interval - const obsValuesForTimeIntervalArr = obsValuesArr.slice( - indexStartTimestamp, - indexEndTimestamp + 1 - ); - - return obsValuesForTimeIntervalArr.reduce( - (accumulator, currentValue) => accumulator + currentValue - ); -}; - -/** - * Calculate the sum of observation values within a time interval delimited by the start date and end date of a calendar month - * @param {Array} obsArray An array of observations (timestamp + value) that is response from SensorThings API - * @param {String} samplingRate The sampling rate of observations as a string, e.g. "15 min", "60 min" - * @param {String} calendarMonthStr Calendar month string in "YYYY-MM" format - * @returns {Number} A floating-point number representing the sum of observation values - */ -const calculateSumOfObservationValuesWithinMonthInterval = function ( - obsArray, - samplingRate, - calendarMonthStr -) { - // Extract year as integer - const yearNum = parseInt(calendarMonthStr.slice(0, 4), 10); - - // Extract month as integer - const monthNum = parseInt(calendarMonthStr.slice(-2), 10); - - if (monthNum < 1 || monthNum > 12) return; - - // All the months start on the first - const startDateStr = `${calendarMonthStr}-01`; - - // February - if (monthNum === 2) { - // Leap year - if (checkIfLeapYear(yearNum)) - return calculateSumOfObservationValuesWithinDatesInterval( - obsArray, - samplingRate, - startDateStr, - `${calendarMonthStr}-29` - ); - - // Non-leap year - return calculateSumOfObservationValuesWithinDatesInterval( - obsArray, - samplingRate, - startDateStr, - `${calendarMonthStr}-28` - ); - } - - // Months with 30 days - if (monthNum === 4 || monthNum === 6 || monthNum === 9 || monthNum === 11) - return calculateSumOfObservationValuesWithinDatesInterval( - obsArray, - samplingRate, - startDateStr, - `${calendarMonthStr}-30` - ); - - // Months with 31 days - return calculateSumOfObservationValuesWithinDatesInterval( - obsArray, - samplingRate, - startDateStr, - `${calendarMonthStr}-31` - ); -}; - -/** - * Extract unique calendar dates from date/time strings in ISO 8601 format - * @param {Array} obsArray An array of observations (timestamp + value) that is response from SensorThings API - * @returns {Array} An array of unique calendar date strings in "YYYY-MM-DD" format - */ -const extractUniqueCalendarDatesFromTimestamp = function (obsArray) { - // The timestamp is the first element of the observation array - const timestampArr = obsArray.map((obs) => obs[0]); - - // Extract the calendar date string from the timestamp string - const calendarDateArr = timestampArr.map((timestamp) => - timestamp.slice(0, 10) - ); - - // Use a set to remove duplicates - const uniqueCalendarDates = new Set(calendarDateArr); - - return [...uniqueCalendarDates]; -}; - -/** - * Extract unique calendar months from calendar date strings - * @param {Array} calendarDatesArr An array of unique calendar date strings in "YYYY-MM-DD" format - * @returns {Array} An array of unique calendar month strings in "YYYY-MM" format - */ -const extractUniqueCalendarMonthsFromCalendarDates = function ( - calendarDatesArr -) { - // Extract the calendar month strings - const calendarMonthsArr = calendarDatesArr.map((date) => date.slice(0, 7)); - - // Use a set to remove duplicates - const uniqueCalendarMonths = new Set(calendarMonthsArr); - - return [...uniqueCalendarMonths]; -}; - -export { - calculateSumOfObservationValuesWithinDatesInterval, - calculateSumOfObservationValuesWithinMonthInterval, - extractUniqueCalendarDatesFromTimestamp, - extractUniqueCalendarMonthsFromCalendarDates, -}; diff --git a/public/js/src_modules/aggregate.mjs b/public/js/src_modules/aggregate.mjs new file mode 100644 index 0000000..6c6a30c --- /dev/null +++ b/public/js/src_modules/aggregate.mjs @@ -0,0 +1,420 @@ +"use strict"; + +/** + * Create 24-hour time strings for a time interval delimited by a start time and an end time. It is assumed that the start time is at "00:00:00" and the end time is at "23:45:00" (when the sampling rate of observations is 15 min) or "23:00:00" (when the sampling rate of observations is 60 min) + * @param {String} phenomenonSamplingRate The sampling rate of the phenomenon of interest represented as a string, e.g. "15min", "60min" + * @returns {Array} An array of two 24-hour strings representing the start time and end time + */ +const createTimeStringsForInterval = function (phenomenonSamplingRate) { + const fifteenMinutes = "15min"; + const sixtyMinutes = "60min"; + + const startTime = "00:00:00"; + const endTimeFifteenMinutes = "23:45:00"; + const endTimeSixtyMinutes = "23:00:00"; + + if ( + phenomenonSamplingRate !== fifteenMinutes && + phenomenonSamplingRate !== sixtyMinutes + ) + throw new Error( + `Check that the provided phenomenon sampling rate string is in this format: "15min" or "60min"` + ); + + // 15 min sampling rate + if (phenomenonSamplingRate === fifteenMinutes) { + return [startTime, endTimeFifteenMinutes]; + } + + // 60 min sampling rate + if (phenomenonSamplingRate === sixtyMinutes) { + return [startTime, endTimeSixtyMinutes]; + } +}; + +/** + * Create an ISO 8601 date and time string + * @param {String} inputCalendarDate Calendar date string in "YYYY-MM-DD" format + * @param {String} inputTwentyFourHourTime 24-hour time string in "hh:mm:ss" format + * @returns {String} An ISO 8601 date and time string + */ +const createIso8601DateTimeString = function ( + inputCalendarDate, + inputTwentyFourHourTime +) { + return `${inputCalendarDate}T${inputTwentyFourHourTime}.000Z`; +}; + +/** + * Check whether a year is a leap year or not + * @param {Number} year Integer representing the year + * @returns {Boolean} true if leap year, false if not + */ +const checkIfLeapYear = function (year) { + return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0; +}; + +/** + * Calculate the index of a timestamp in an array of timestamps + * @param {Array} inputTimestampArr An array of timestamps, extracted from an array of observations + * @param {String} timestampOfInterest A string representing the timestamp of interest in ISO 8601 format + * @returns {Number} An integer representing the index of the timestamp of interest in the array of timestamps + */ +const getIndexOfTimestamp = function (inputTimestampArr, timestampOfInterest) { + const timestampIndex = inputTimestampArr.findIndex( + (timestamp) => timestamp === timestampOfInterest + ); + + // If the timestamp does not exist in the timestamp array + if (timestampIndex === -1) + throw new Error( + "A start or end timestamp could not be found in the timestamp array" + ); + + // If the timestamp exists in the timestamp array + return timestampIndex; +}; + +/** + * Calculate the indexes of the start and end timestamps + * @param {Array} obsTimestampArr An array of observations timestamps + * @param {String} samplingRate The sampling rate of observations as a string, e.g. "15min", "60min" + * @param {String} startDate A 24-hour date string representing the start date + * @param {String} endDate A 24-hour date string representing the end date + * @returns {Array} A 1*2 array tht contains integers representing the start index and end index respectively + */ +const calculateIndexStartEndTimestamp = function ( + obsTimestampArr, + samplingRate, + startDate, + endDate +) { + // Create and extract 24-hour strings for the start and end of interval + const [startTimeString, endTimeString] = + createTimeStringsForInterval(samplingRate); + + // Create ISO 8601 strings for the start and end of interval + const startIso8601DateTimeString = createIso8601DateTimeString( + startDate, + startTimeString + ); + const endIso8601DateTimeString = createIso8601DateTimeString( + endDate, + endTimeString + ); + + // Calculate the indexes of the timestamps for the start and end of interval + const indexStartTimestamp = getIndexOfTimestamp( + obsTimestampArr, + startIso8601DateTimeString + ); + const indexEndTimestamp = getIndexOfTimestamp( + obsTimestampArr, + endIso8601DateTimeString + ); + + return [indexStartTimestamp, indexEndTimestamp]; +}; + +/** + * Extract the set of observation values that fall within a time interval delimited by a start date and end date. The start date may be the same as the end date. + * @param {Array} obsArray An array of observations (timestamp + value) that is response from SensorThings API + * @param {String} samplingRate The sampling rate of observations as a string, e.g. "15min", "60min" + * @param {String} startDate A 24-hour date string representing the start date + * @param {String} endDate A 24-hour date string representing the end date + * @returns {Array} An array of observation values that fall within our time interval + */ +const extractObservationValuesWithinDatesInterval = function ( + obsArray, + samplingRate, + startDate, + endDate +) { + // Extract the timestamps and values from the observations + const obsTimestampArr = obsArray.map((obs) => obs[0]); + const obsValuesArr = obsArray.map((obs) => obs[1]); + + // Calculate the indexes of the timestamps for the start and end of interval + const [indexStartTimestamp, indexEndTimestamp] = + calculateIndexStartEndTimestamp( + obsTimestampArr, + samplingRate, + startDate, + endDate + ); + + // Extract the observations that fall within our time interval + return obsValuesArr.slice(indexStartTimestamp, indexEndTimestamp + 1); +}; + +/** + * Calculate the sum of observation values that fall within a time interval delimited by a start date and end date + * @param {Array} obsValuesForDaysIntervalArr An array of observation values that fall within our time interval + * @returns {Number} A floating-point number representing the sum of observation values + */ +const calculateSumOfObservationValuesWithinDatesInterval = function ( + obsValuesForDaysIntervalArr +) { + return obsValuesForDaysIntervalArr.reduce( + (accumulator, currentValue) => accumulator + currentValue + ); +}; + +/** + * Calculate the average (arithmetic mean) of observation values that fall within a time interval delimited by a start date and end date + * @param {Array} obsValuesForDaysIntervalArr An array of observation values that fall within our time interval + * @returns {Number} A floating-point number representing the average (arithmetic mean) of observation values + */ +const calculateAverageOfObservationValuesWithinDatesInterval = function ( + obsValuesForDaysIntervalArr +) { + return ( + calculateSumOfObservationValuesWithinDatesInterval( + obsValuesForDaysIntervalArr + ) / obsValuesForDaysIntervalArr.length + ); +}; + +/** + * Extract the year and month digits from a calendar month string + * @param {String} calendarMonthStr Calendar month string in "YYYY-MM" format + * @returns {Array} A 1*2 array tht contains integers representing the year and month respectively + */ +const extractMonthYearDigitsFromCalendarMonthString = function ( + calendarMonthStr +) { + // Extract year as integer + const yearNum = parseInt(calendarMonthStr.slice(0, 4), 10); + + // Extract month as integer + const monthNum = parseInt(calendarMonthStr.slice(-2), 10); + + return [yearNum, monthNum]; +}; + +/** + * Extract the set of observation values that fall within a time interval delimited by the first day and last day of a calendar month + * @param {Array} obsArray An array of observations (timestamp + value) that is response from SensorThings API + * @param {String} samplingRate The sampling rate of observations as a string, e.g. "15min", "60min" + * @param {String} calendarMonthStr Calendar month string in "YYYY-MM" format + * @returns {Array} An array of observation values that fall within one calendar month + */ +const extractObservationValuesWithinMonthInterval = function ( + obsArray, + samplingRate, + calendarMonthStr +) { + // Extract the year and month digits from the calendar month string + const [yearNum, monthNum] = + extractMonthYearDigitsFromCalendarMonthString(calendarMonthStr); + + if (monthNum < 1 || monthNum > 12) return; + + // All the months start on the first + const startDateStr = `${calendarMonthStr}-01`; + + // February + if (monthNum === 2) { + // Leap year + if (checkIfLeapYear(yearNum)) + return extractObservationValuesWithinDatesInterval( + obsArray, + samplingRate, + startDateStr, + `${calendarMonthStr}-29` + ); + + // Non-leap year + return extractObservationValuesWithinDatesInterval( + obsArray, + samplingRate, + startDateStr, + `${calendarMonthStr}-28` + ); + } + + // Months with 30 days + if (monthNum === 4 || monthNum === 6 || monthNum === 9 || monthNum === 11) + return extractObservationValuesWithinDatesInterval( + obsArray, + samplingRate, + startDateStr, + `${calendarMonthStr}-30` + ); + + // Months with 31 days + return extractObservationValuesWithinDatesInterval( + obsArray, + samplingRate, + startDateStr, + `${calendarMonthStr}-31` + ); +}; + +/** + * Calculate the sum of observation values that fall within a time interval delimited by the first day and last day of a calendar month + * @param {Array} obsValuesForMonthIntervalArr An array of observation values that fall within one calendar month + * @returns {Number} A floating-point number representing the sum of observation values within one calendar month + */ +const calculateSumOfObservationValuesWithinMonthInterval = function ( + obsValuesForMonthIntervalArr +) { + return obsValuesForMonthIntervalArr.reduce( + (accumulator, currentValue) => accumulator + currentValue + ); +}; + +/** + * Calculate the average (arithmetic mean) of observation values that fall within a time interval delimited by the first day and last day of a calendar month + * @param {Array} obsValuesForMonthIntervalArr An array of observation values that fall within one calendar month + * @returns {Number} A floating-point number representing the average (arithmetic mean) of observation values within one calendar month + */ +const calculateAverageOfObservationValuesWithinMonthInterval = function ( + obsValuesForMonthIntervalArr +) { + return ( + calculateSumOfObservationValuesWithinMonthInterval( + obsValuesForMonthIntervalArr + ) / obsValuesForMonthIntervalArr.length + ); +}; + +/** + * Calculate the sum of observation values within a time interval delimited by a start date and end date. The time interval may be daily or monthly + * @param {Array} obsNestedArr A 1*N array that contains N nested arrays of observations (timestamp + value) + * @param {String} samplingRate The sampling rate of observations as a string, e.g. "15min", "60min" + * @param {Array} uniqueCalendarDatesOrMonthsArr A 1*N array of unique calendar dates or calendar months strings + * @param {String} aggregationInterval The aggregation interval as a string e.g. "daily", "monthly" + * @returns {Array} A 1*N array that contains N nested arrays of sum values + */ +const calculateSumOfObservationValuesWithinInterval = function ( + obsNestedArr, + samplingRate, + uniqueCalendarDatesOrMonthsArr, + aggregationInterval +) { + // Calculate sum of values of observations - daily + // Note the use of the two nested `map` methods + if (aggregationInterval === "daily") { + return uniqueCalendarDatesOrMonthsArr.map((uniqueCalendarDatesArr, i) => + uniqueCalendarDatesArr.map((uniqueCalendarDate) => + calculateSumOfObservationValuesWithinDatesInterval( + extractObservationValuesWithinDatesInterval( + obsNestedArr[i], + samplingRate, + uniqueCalendarDate, + uniqueCalendarDate + ) + ) + ) + ); + } + + // Calculate sum of values of observations - monthly + // Note the use of the two nested `map` methods + if (aggregationInterval === "monthly") { + return uniqueCalendarDatesOrMonthsArr.map((uniqueCalendarMonthsArr, i) => + uniqueCalendarMonthsArr.map((uniqueCalendarMonth) => + calculateSumOfObservationValuesWithinMonthInterval( + extractObservationValuesWithinMonthInterval( + obsNestedArr[i], + samplingRate, + uniqueCalendarMonth + ) + ) + ) + ); + } +}; + +/** + * Calculate the average (arithmetic mean) of observation values within a time interval delimited by a start date and end date. The time interval may be daily or monthly + * @param {Array} obsNestedArr A 1*N array that contains N nested arrays of observations (timestamp + value) + * @param {String} samplingRate The sampling rate of observations as a string, e.g. "15min", "60min" + * @param {Array} uniqueCalendarDatesOrMonthsArr A 1*N array of unique calendar dates or calendar months strings + * @param {String} aggregationInterval The aggregation interval as a string e.g. "daily", "monthly" + * @returns {Array} A 1*N array that contains N nested arrays of average (arithmetic mean) values + */ +const calculateAverageOfObservationValuesWithinInterval = function ( + obsNestedArr, + samplingRate, + uniqueCalendarDatesOrMonthsArr, + aggregationInterval +) { + // Calculate average of values of observations - daily + // Note the use of the two nested `map` methods + if (aggregationInterval === "daily") { + return uniqueCalendarDatesOrMonthsArr.map((uniqueCalendarDatesArr, i) => + uniqueCalendarDatesArr.map((uniqueCalendarDate) => + calculateAverageOfObservationValuesWithinDatesInterval( + extractObservationValuesWithinDatesInterval( + obsNestedArr[i], + samplingRate, + uniqueCalendarDate, + uniqueCalendarDate + ) + ) + ) + ); + } + + // Calculate average of values of observations - monthly + // Note the use of the two nested `map` methods + if (aggregationInterval === "monthly") { + return uniqueCalendarDatesOrMonthsArr.map((uniqueCalendarMonthsArr, i) => + uniqueCalendarMonthsArr.map((uniqueCalendarMonth) => + calculateAverageOfObservationValuesWithinMonthInterval( + extractObservationValuesWithinMonthInterval( + obsNestedArr[i], + samplingRate, + uniqueCalendarMonth + ) + ) + ) + ); + } +}; + +/** + * Extract unique calendar dates from date/time strings in ISO 8601 format + * @param {Array} obsArray An array of observations (timestamp + value) that is response from SensorThings API + * @returns {Array} An array of unique calendar date strings in "YYYY-MM-DD" format + */ +const extractUniqueCalendarDatesFromTimestamp = function (obsArray) { + // The timestamp is the first element of the observation array + const timestampArr = obsArray.map((obs) => obs[0]); + + // Extract the calendar date string from the timestamp string + const calendarDateArr = timestampArr.map((timestamp) => + timestamp.slice(0, 10) + ); + + // Use a set to remove duplicates + const uniqueCalendarDates = new Set(calendarDateArr); + + return [...uniqueCalendarDates]; +}; + +/** + * Extract unique calendar months from calendar date strings + * @param {Array} calendarDatesArr An array of unique calendar date strings in "YYYY-MM-DD" format + * @returns {Array} An array of unique calendar month strings in "YYYY-MM" format + */ +const extractUniqueCalendarMonthsFromCalendarDates = function ( + calendarDatesArr +) { + // Extract the calendar month strings + const calendarMonthsArr = calendarDatesArr.map((date) => date.slice(0, 7)); + + // Use a set to remove duplicates + const uniqueCalendarMonths = new Set(calendarMonthsArr); + + return [...uniqueCalendarMonths]; +}; + +export { + calculateSumOfObservationValuesWithinInterval, + extractUniqueCalendarDatesFromTimestamp, + extractUniqueCalendarMonthsFromCalendarDates, + calculateAverageOfObservationValuesWithinInterval, +}; diff --git a/public/js/src_modules/createUrl.js b/public/js/src_modules/baseUrlPlusQueryParams.mjs similarity index 100% rename from public/js/src_modules/createUrl.js rename to public/js/src_modules/baseUrlPlusQueryParams.mjs diff --git a/public/js/src_modules/calculateTemperatureDiff.mjs b/public/js/src_modules/calculateTemperatureDiff.mjs new file mode 100644 index 0000000..afc2aac --- /dev/null +++ b/public/js/src_modules/calculateTemperatureDiff.mjs @@ -0,0 +1,106 @@ +"use strict"; + +import { + extractPhenomenonNameFromDatastreamName, + getMetadataPlusObservationsFromSingleOrMultipleDatastreams, +} from "./fetchData.mjs"; + +/** + * Calculate the temperature difference, dT, between Vorlauf temperature [VL] and Rücklauf temperature [RL] (i.e., dT = VL - RL) + * @param {String} baseUrl Base URL of the STA server + * @param {Object} urlParams The URL parameters to be sent together with the GET request + * @param {String} buildingId The building ID as a string + * @param {String} samplingRate The sampling rate as a string + * @returns {Promise} A promise that contains an array (that is made up of a temperature difference array and a metadata object) when fulfilled + */ +export const calculateVorlaufMinusRuecklaufTemperature = async function ( + baseUrl, + urlParams, + buildingId, + samplingRate +) { + try { + const bldgSensorSamplingRateNestedArr = [ + [buildingId, "vl", samplingRate], + [buildingId, "rl", samplingRate], + ]; + + const BUILDING_ID = buildingId; + const SAMPLING_RATE = samplingRate; + + const observationsPlusMetadata = + await getMetadataPlusObservationsFromSingleOrMultipleDatastreams( + baseUrl, + urlParams, + bldgSensorSamplingRateNestedArr + ); + + // Extract Vorlauf temperature, Ruecklauf temperature and metadata + const [ + [vorlaufTemperatureObsArr, ruecklaufTemperatureObsArr], + [metadataVorlauf, metadataRuecklauf], + ] = observationsPlusMetadata; + + // Extract the temperature values + const vorlaufTemperatureValues = vorlaufTemperatureObsArr.map( + (vlTempObs) => vlTempObs[1] + ); + const ruecklaufTemperatureValues = ruecklaufTemperatureObsArr.map( + (rlTempObs) => rlTempObs[1] + ); + + // The arrays have equal length, we need only use one of them for looping + // Resulting array contains the following pairs (timestamp + dT) + const vorlaufMinusRuecklaufTemperatureObs = vorlaufTemperatureObsArr.map( + (vlTempObs, i) => [ + vlTempObs[0], // timestamp + vorlaufTemperatureValues[i] - ruecklaufTemperatureValues[i], + ] + ); + + // From Vorlauf metadata, extract `name` and `unitOfMeasurement` + const { + name: datastreamNameVorlauf, + unitOfMeasurement: unitOfMeasurementVorlauf, + } = metadataVorlauf; + + // From Ruecklauf metadata, extract `name` + const { name: datastreamNameRuecklauf } = metadataRuecklauf; + + // Extract the phenomenon names from the Datastream names + const phenomenonNameVorlauf = extractPhenomenonNameFromDatastreamName( + datastreamNameVorlauf + ); + const phenomenonNameRuecklauf = extractPhenomenonNameFromDatastreamName( + datastreamNameRuecklauf + ); + + // Create our custom datastream description text + // The resulting datastream description string has two `temperature` substrings; + // replace the first occurence with an empty string + const descriptionTempDifference = + `Computed dT: ${phenomenonNameVorlauf} minus ${phenomenonNameRuecklauf}`.replace( + "temperature", + "" + ); + + // Create our custom datastream name text + const nameTempDifference = `BOSCH_${BUILDING_ID} / dT Temperature difference (VL-RL) DS:${SAMPLING_RATE}`; + + // The datastream object that we return needs to have these property names + const description = descriptionTempDifference; + const name = nameTempDifference; + const unitOfMeasurement = unitOfMeasurementVorlauf; + + return [ + vorlaufMinusRuecklaufTemperatureObs, + { + description, + name, + unitOfMeasurement, + }, + ]; + } catch (err) { + console.error(err); + } +}; diff --git a/public/js/src_modules/chartColumn.js b/public/js/src_modules/chartColumn.mjs similarity index 97% rename from public/js/src_modules/chartColumn.js rename to public/js/src_modules/chartColumn.mjs index 51f3224..9f259f3 100644 --- a/public/js/src_modules/chartColumn.js +++ b/public/js/src_modules/chartColumn.mjs @@ -1,3 +1,7 @@ +"use strict"; + +import { chartExportOptions } from "./chartExport.mjs"; + /** * Format a computed aggregation result to make it suitable for a column chart * @param {Array} calendarDatesMonthsStrArr An array of unique calendar dates strings (in "YYYY-MM-DD" fromat) or unique calendar months strings (in "YYYY-MM" format) @@ -123,6 +127,8 @@ const drawColumnChartHighcharts = function ( }, }, + exporting: chartExportOptions, + series: seriesOptionsArr, }); }; diff --git a/public/js/src_modules/chartExport.mjs b/public/js/src_modules/chartExport.mjs new file mode 100644 index 0000000..0e26d0e --- /dev/null +++ b/public/js/src_modules/chartExport.mjs @@ -0,0 +1,9 @@ +"use strict"; + +export const chartExportOptions = { + buttons: { + contextButton: { + menuItems: ["downloadPNG", "downloadJPEG", "downloadPDF", "downloadSVG"], + }, + }, +}; diff --git a/public/js/src_modules/chartHeatmap.js b/public/js/src_modules/chartHeatmap.mjs similarity index 85% rename from public/js/src_modules/chartHeatmap.js rename to public/js/src_modules/chartHeatmap.mjs index 5d15d4f..50534b4 100644 --- a/public/js/src_modules/chartHeatmap.js +++ b/public/js/src_modules/chartHeatmap.mjs @@ -1,5 +1,7 @@ "use strict"; +import { chartExportOptions } from "./chartExport.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 @@ -142,11 +144,32 @@ const drawHeatMapHighcharts = function ( startOnTick: false, endOnTick: false, labels: { - // format: "{value}℃", 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, @@ -154,14 +177,6 @@ const drawHeatMapHighcharts = function ( borderWidth: 0, nullColor: "#525252", colsize: 24 * 36e5, // one day - tooltip: { - headerFormat: `${PHENOMENON_NAME}
`, - valueDecimals: 2, - pointFormat: - // "{point.x:%e %b, %Y} {point.y}:00: {point.value} ℃", - `{point.x:%e %b, %Y} {point.y}:00: {point.value} ${PHENOMENON_SYMBOL}`, - nullFormat: `{point.x:%e %b, %Y} {point.y}:00: null`, - }, turboThreshold: Number.MAX_VALUE, // #3404, remove after 4.0.5 release }, ], diff --git a/public/js/src_modules/chartLine.js b/public/js/src_modules/chartLine.mjs similarity index 81% rename from public/js/src_modules/chartLine.js rename to public/js/src_modules/chartLine.mjs index 8c8e5bf..aaf3f4f 100644 --- a/public/js/src_modules/chartLine.js +++ b/public/js/src_modules/chartLine.mjs @@ -1,5 +1,7 @@ "use strict"; +import { chartExportOptions } from "./chartExport.mjs"; + /** * Format the response from SensorThings API to make it suitable for use in a line chart * @param {Array} obsArray Array of observations (timestamp + value) that is response from SensorThings API @@ -42,23 +44,21 @@ const createCombinedTextForLineChartTitles = function ( * Creates an options object for each series drawn in the line chart * @param {Array} formattedObsArraysForLineChart An array of formatted observation array(s) from one or more datastreams * @param {Array} phenomenonNamesArr An array of phenomenon name(s) - * @param {Array} phenomenonSymbolsArr An array of phenomenon symbol(s) * @returns {Array} An array made up of series options object(s) */ const createSeriesOptionsForLineChart = function ( formattedObsArraysForLineChart, - phenomenonNamesArr, - phenomenonSymbolsArr + phenomenonNamesArr ) { // An array of colors provided by the Highcharts object const seriesColors = Highcharts.getOptions().colors; // Create an array of seriesOptions objects - // Assumes that the observation array of arrays, phenomenon names array and phenomenon symbols array are of equal length + // Assumes that the observation array of arrays and phenomenon names array are of equal length // Use one of the arrays for looping return formattedObsArraysForLineChart.map((formattedObsArray, i) => { return { - name: `${phenomenonNamesArr[i]} (${phenomenonSymbolsArr[i]})`, + name: `${phenomenonNamesArr[i]}`, data: formattedObsArray, color: seriesColors[i], turboThreshold: Number.MAX_VALUE, // #3404, remove after 4.0.5 release @@ -112,11 +112,27 @@ const drawLineChartHighcharts = function ( }, tooltip: { - pointFormat: - '{series.name}: {point.y}
', - valueDecimals: 2, + formatter() { + // Our tooltip is split + // this.x -- common for all points + // this.points -- an array containing properties for each series + return [`${Highcharts.dateFormat("%A, %b %e, %Y", this.x)}`].concat( + this.points + ? this.points.map( + (point, i) => + `${ + point.series.name + }: ${point.y.toFixed(2)} ${ + unitOfMeasurementSymbolsArr[i] + }` + ) + : [] + ); + }, }, + exporting: chartExportOptions, + series: seriesOptionsArr, }); }; diff --git a/public/js/src_modules/chartScatterPlot.js b/public/js/src_modules/chartScatterPlot.mjs similarity index 96% rename from public/js/src_modules/chartScatterPlot.js rename to public/js/src_modules/chartScatterPlot.mjs index eb88b94..09b8592 100644 --- a/public/js/src_modules/chartScatterPlot.js +++ b/public/js/src_modules/chartScatterPlot.mjs @@ -1,5 +1,7 @@ "use strict"; +import { chartExportOptions } from "./chartExport.mjs"; + /** * Determines the timestamps that are missing from a smaller set of observations. Based on the comparison of two observation arrays, where one array is larger than the other * @param {Array} obsTimestampArrayOne An array of timestamps for the first set of observations @@ -323,13 +325,23 @@ const drawScatterPlotHighcharts = function ( }, }, }, - tooltip: { - headerFormat: "{series.name}
", - pointFormat: `{point.y:.2f} ${SERIES_1_SYMBOL}, {point.x:.2f} ${SERIES_2_SYMBOL}`, - }, }, }, + tooltip: { + formatter() { + const headerString = `${this.series.name}
`; + const pointString = `${this.point.y.toFixed( + 2 + )} ${SERIES_1_SYMBOL}, ${this.point.x.toFixed( + 2 + )} ${SERIES_2_SYMBOL}`; + return headerString + pointString; + }, + }, + + exporting: chartExportOptions, + series: [ { name: SERIES_COMBINED_NAME, diff --git a/public/js/src_modules/fetchData.js b/public/js/src_modules/fetchData.mjs similarity index 63% rename from public/js/src_modules/fetchData.js rename to public/js/src_modules/fetchData.mjs index 64ba329..718620d 100644 --- a/public/js/src_modules/fetchData.js +++ b/public/js/src_modules/fetchData.mjs @@ -1,98 +1,6 @@ "use strict"; -/** - * Retrieve the datastream ID that corresponds to a particular building - * @param {Number | String} buildingNumber Integer representing the building ID - * @param {String} phenomenon String representing the phenomenon of interest - * @param {String} samplingRate String representing the sampling rate of the observations - * @returns {Number} Datastream corresponding to the input building - */ -const getDatastreamIdFromBuildingNumber = function ( - buildingNumber, - phenomenon, - samplingRate -) { - const buildingToDatastreamMapping = { - 101: { - vl: { "15min": "69", "60min": "75" }, - rl: { "15min": "81", "60min": "87" }, - - // These Datastreams do not yet have Observations - // flow: { "15min": "93", "60min": "99" }, - // power: { "15min": "105", "60min": "111" }, - // energy: { "15min": "117", "60min": "123" }, - // energy_verb: { "15min": "129", "60min": "135" }, - }, - - 102: { - vl: { "15min": "70", "60min": "76" }, - rl: { "15min": "82", "60min": "88" }, - - // These Datastreams do not yet have Observations - // flow: { "15min": "94", "60min": "100" }, - // power: { "15min": "106", "60min": "112" }, - // energy: { "15min": "118", "60min": "124" }, - // energy_verb: { "15min": "130", "60min": "136" }, - }, - - 107: { - vl: { "15min": "71", "60min": "77" }, - rl: { "15min": "83", "60min": "89" }, - - // These Datastreams do not yet have Observations - // flow: { "15min": "95", "60min": "101" }, - // power: { "15min": "107", "60min": "113" }, - // energy: { "15min": "119", "60min": "125" }, - // energy_verb: { "15min": "131", "60min": "137" }, - }, - - "112, 118": { - vl: { "15min": "72", "60min": "78" }, - rl: { "15min": "84", "60min": "90" }, - - // These Datastreams do not yet have Observations - // flow: { "15min": "96", "60min": "102" }, - // power: { "15min": "108", "60min": "114" }, - // energy: { "15min": "120", "60min": "126" }, - // energy_verb: { "15min": "132", "60min": "138" }, - }, - - 125: { - vl: { "15min": "73", "60min": "79" }, - rl: { "15min": "85", "60min": "91" }, - - // These Datastreams do not yet have Observations - // flow: { "15min": "97", "60min": "103" }, - // power: { "15min": "109", "60min": "115" }, - // energy: { "15min": "121", "60min": "127" }, - // energy_verb: { "15min": "133", "60min": "139" }, - }, - - 225: { - vl: { "15min": "74", "60min": "80" }, - rl: { "15min": "86", "60min": "92" }, - flow: { "15min": "98", "60min": "104" }, - power: { "15min": "110", "60min": "116" }, - energy: { "15min": "122", "60min": "128" }, - energy_verb: { "15min": "134", "60min": "140" }, - }, - - weather_station_521: { - outside_temp: { "15min": "141", "60min": "142" }, - }, - }; - - if ( - buildingToDatastreamMapping?.[buildingNumber]?.[phenomenon]?.[ - samplingRate - ] === undefined - ) - return; - - return Number( - buildingToDatastreamMapping[buildingNumber][phenomenon][samplingRate] - ); -}; +import { getDatastreamIdFromBuildingNumber } from "./getDatastreamId.mjs"; /** * Create URL to fetch the details of single Datastream @@ -412,109 +320,9 @@ const getMetadataPlusObservationsFromSingleOrMultipleDatastreams = } }; -/** - * Calculate the temperature difference, dT, between Vorlauf temperature [VL] and Rücklauf temperature [RL] (i.e., dT = VL - RL) - * @param {String} baseUrl Base URL of the STA server - * @param {Object} urlParams The URL parameters to be sent together with the GET request - * @param {String} buildingId The building ID as a string - * @param {String} samplingRate The sampling rate as a string - * @returns {Promise} A promise that contains an array (that is made up of a temperature difference array and a metadata object) when fulfilled - */ -const calculateVorlaufMinusRuecklaufTemperature = async function ( - baseUrl, - urlParams, - buildingId, - samplingRate -) { - try { - const bldgSensorSamplingRateNestedArr = [ - [buildingId, "vl", samplingRate], - [buildingId, "rl", samplingRate], - ]; - - const BUILDING_ID = buildingId; - const SAMPLING_RATE = samplingRate; - - const observationsPlusMetadata = - await getMetadataPlusObservationsFromSingleOrMultipleDatastreams( - baseUrl, - urlParams, - bldgSensorSamplingRateNestedArr - ); - - // Extract Vorlauf temperature, Ruecklauf temperature and metadata - const [ - [vorlaufTemperatureObsArr, ruecklaufTemperatureObsArr], - [metadataVorlauf, metadataRuecklauf], - ] = observationsPlusMetadata; - - // Extract the temperature values - const vorlaufTemperatureValues = vorlaufTemperatureObsArr.map( - (vlTempObs) => vlTempObs[1] - ); - const ruecklaufTemperatureValues = ruecklaufTemperatureObsArr.map( - (rlTempObs) => rlTempObs[1] - ); - - // The arrays have equal length, we need only use one of them for looping - // Resulting array contains the following pairs (timestamp + dT) - const vorlaufMinusRuecklaufTemperatureObs = vorlaufTemperatureObsArr.map( - (vlTempObs, i) => [ - vlTempObs[0], // timestamp - vorlaufTemperatureValues[i] - ruecklaufTemperatureValues[i], - ] - ); - - // From Vorlauf metadata, extract `name` and `unitOfMeasurement` - const { - name: datastreamNameVorlauf, - unitOfMeasurement: unitOfMeasurementVorlauf, - } = metadataVorlauf; - - // From Ruecklauf metadata, extract `name` - const { name: datastreamNameRuecklauf } = metadataRuecklauf; - - // Extract the phenomenon names from the Datastream names - const phenomenonNameVorlauf = extractPhenomenonNameFromDatastreamName( - datastreamNameVorlauf - ); - const phenomenonNameRuecklauf = extractPhenomenonNameFromDatastreamName( - datastreamNameRuecklauf - ); - - // Create our custom datastream description text - // The resulting datastream description string has two `temperature` substrings; - // replace the first occurence with an empty string - const descriptionTempDifference = - `Computed dT: ${phenomenonNameVorlauf} minus ${phenomenonNameRuecklauf}`.replace( - "temperature", - "" - ); - - // Create our custom datastream name text - const nameTempDifference = `BOSCH_${BUILDING_ID} / dT Temperature difference (VL-RL) DS:${SAMPLING_RATE}`; - - // The datastream object that we return needs to have these property names - const description = descriptionTempDifference; - const name = nameTempDifference; - const unitOfMeasurement = unitOfMeasurementVorlauf; - - return [ - vorlaufMinusRuecklaufTemperatureObs, - { - description, - name, - unitOfMeasurement, - }, - ]; - } catch (err) { - console.error(err); - } -}; - export { + extractPhenomenonNameFromDatastreamName, formatDatastreamMetadataForChart, extractPropertiesFromFormattedDatastreamMetadata, getMetadataPlusObservationsFromSingleOrMultipleDatastreams, - calculateVorlaufMinusRuecklaufTemperature, }; diff --git a/public/js/src_modules/getDatastreamId.mjs b/public/js/src_modules/getDatastreamId.mjs new file mode 100644 index 0000000..ff5303a --- /dev/null +++ b/public/js/src_modules/getDatastreamId.mjs @@ -0,0 +1,95 @@ +"use strict"; + +/** + * Retrieve the datastream ID that corresponds to a particular building + * @param {Number | String} buildingNumber Integer representing the building ID + * @param {String} phenomenon String representing the phenomenon of interest + * @param {String} samplingRate String representing the sampling rate of the observations + * @returns {Number} Datastream corresponding to the input building + */ +export const getDatastreamIdFromBuildingNumber = function ( + buildingNumber, + phenomenon, + samplingRate +) { + const buildingToDatastreamMapping = { + 101: { + vl: { "15min": "69", "60min": "75" }, + rl: { "15min": "81", "60min": "87" }, + + // These Datastreams do not yet have Observations + // flow: { "15min": "93", "60min": "99" }, + // power: { "15min": "105", "60min": "111" }, + // energy: { "15min": "117", "60min": "123" }, + // energy_verb: { "15min": "129", "60min": "135" }, + }, + + 102: { + vl: { "15min": "70", "60min": "76" }, + rl: { "15min": "82", "60min": "88" }, + + // These Datastreams do not yet have Observations + // flow: { "15min": "94", "60min": "100" }, + // power: { "15min": "106", "60min": "112" }, + // energy: { "15min": "118", "60min": "124" }, + // energy_verb: { "15min": "130", "60min": "136" }, + }, + + 107: { + vl: { "15min": "71", "60min": "77" }, + rl: { "15min": "83", "60min": "89" }, + + // These Datastreams do not yet have Observations + // flow: { "15min": "95", "60min": "101" }, + // power: { "15min": "107", "60min": "113" }, + // energy: { "15min": "119", "60min": "125" }, + // energy_verb: { "15min": "131", "60min": "137" }, + }, + + "112, 118": { + vl: { "15min": "72", "60min": "78" }, + rl: { "15min": "84", "60min": "90" }, + + // These Datastreams do not yet have Observations + // flow: { "15min": "96", "60min": "102" }, + // power: { "15min": "108", "60min": "114" }, + // energy: { "15min": "120", "60min": "126" }, + // energy_verb: { "15min": "132", "60min": "138" }, + }, + + 125: { + vl: { "15min": "73", "60min": "79" }, + rl: { "15min": "85", "60min": "91" }, + + // These Datastreams do not yet have Observations + // flow: { "15min": "97", "60min": "103" }, + // power: { "15min": "109", "60min": "115" }, + // energy: { "15min": "121", "60min": "127" }, + // energy_verb: { "15min": "133", "60min": "139" }, + }, + + 225: { + vl: { "15min": "74", "60min": "80" }, + rl: { "15min": "86", "60min": "92" }, + flow: { "15min": "98", "60min": "104" }, + power: { "15min": "110", "60min": "116" }, + energy: { "15min": "122", "60min": "128" }, + energy_verb: { "15min": "134", "60min": "140" }, + }, + + weather_station_521: { + outside_temp: { "15min": "141", "60min": "142" }, + }, + }; + + if ( + buildingToDatastreamMapping?.[buildingNumber]?.[phenomenon]?.[ + samplingRate + ] === undefined + ) + return; + + return Number( + buildingToDatastreamMapping[buildingNumber][phenomenon][samplingRate] + ); +}; diff --git a/public/js/src_modules/loadingIndicator.js b/public/js/src_modules/loadingIndicator.mjs similarity index 100% rename from public/js/src_modules/loadingIndicator.js rename to public/js/src_modules/loadingIndicator.mjs -- GitLab