diff --git a/public/js/appChart.js b/public/js/appChart.js index a73d8a73d008f6ea7d70b6737ced452265acf7ec..338efe4c53a2cb61a47f6a1c279ccac659c9e914 100644 --- a/public/js/appChart.js +++ b/public/js/appChart.js @@ -34,9 +34,13 @@ import { import { calculateVorlaufMinusRuecklaufTemperature } from "./src_modules/calculateTemperatureDiff.mjs"; import { - calculateSumOfObservationValuesWithinInterval, + extractObservationsWithinDatesInterval, extractUniqueCalendarDatesFromTimestamp, extractUniqueCalendarMonthsFromCalendarDates, +} from "./src_modules/aggregateHelpers.mjs"; + +import { + calculateSumOfObservationValuesWithinInterval, calculateAverageOfObservationValuesWithinInterval, } from "./src_modules/aggregate.mjs"; @@ -44,388 +48,477 @@ import { * Test plotting of temp difference (dT) using heatmap */ const drawHeatmapHCUsingTempDifference = async function () { - const [observationsTemperatureDiff225Arr, metadataTemperatureDiff225Arr] = - await calculateVorlaufMinusRuecklaufTemperature( - BASE_URL, - QUERY_PARAMS_COMBINED, - "225", - "60min" - ); - - // We want to have nested arrays, so as to mimick the nested responses we get from fetching observations + metadata - const observationsTemperatureDiff225NestedArr = [ - observationsTemperatureDiff225Arr, - ]; - - const metadataTemperatureDiff225NestedArr = [metadataTemperatureDiff225Arr]; - - // Format the observations - const formattedTempDiff225NestedArr = - observationsTemperatureDiff225NestedArr.map((obsArr) => - formatSensorThingsApiResponseForHeatMap(obsArr) - ); - - // Format the metadata - const formattedTempDiff225MetadataNestedArr = - metadataTemperatureDiff225NestedArr.map((metadataObj) => - formatDatastreamMetadataForChart(metadataObj) - ); - - // Extract the formatted metadata properties - const extractedFormattedTempDiff225Properties = - extractPropertiesFromFormattedDatastreamMetadata( - formattedTempDiff225MetadataNestedArr + try { + const [observationsTemperatureDiff225Arr, metadataTemperatureDiff225Arr] = + await calculateVorlaufMinusRuecklaufTemperature( + BASE_URL, + QUERY_PARAMS_COMBINED, + "225", + "60min" + ); + + // We want to have nested arrays, so as to mimick the nested responses we get from fetching observations + metadata + const observationsTemperatureDiff225NestedArr = [ + observationsTemperatureDiff225Arr, + ]; + + const metadataTemperatureDiff225NestedArr = [metadataTemperatureDiff225Arr]; + + // Format the observations + const formattedTempDiff225NestedArr = + observationsTemperatureDiff225NestedArr.map((obsArr) => + formatSensorThingsApiResponseForHeatMap(obsArr) + ); + + // Format the metadata + const formattedTempDiff225MetadataNestedArr = + metadataTemperatureDiff225NestedArr.map((metadataObj) => + formatDatastreamMetadataForChart(metadataObj) + ); + + // Extract the formatted metadata properties + const extractedFormattedTempDiff225Properties = + extractPropertiesFromFormattedDatastreamMetadata( + formattedTempDiff225MetadataNestedArr + ); + + // First need to extract the formatted observations from the nested array + // Heatmap only needs one set of formatted observation values + drawHeatMapHighcharts( + ...formattedTempDiff225NestedArr, + extractedFormattedTempDiff225Properties ); - - // First need to extract the formatted observations from the nested array - // Heatmap only needs one set of formatted observation values - drawHeatMapHighcharts( - ...formattedTempDiff225NestedArr, - extractedFormattedTempDiff225Properties - ); + } catch (err) { + console.error(err); + } }; /** * Test drawing of scatter plot chart */ const drawScatterPlotHCTest2 = async function () { - const sensorsOfInterestNestedArr = [ - ["225", "vl", "60min"], - // ["125", "rl", "60min"], - ["weather_station_521", "outside_temp", "60min"], - ]; - - const observationsPlusMetadata = - await getMetadataPlusObservationsFromSingleOrMultipleDatastreams( - BASE_URL, - QUERY_PARAMS_COMBINED, - sensorsOfInterestNestedArr + try { + const sensorsOfInterestNestedArr = [ + ["weather_station_521", "outside_temp", "60min"], + ["225", "vl", "60min"], + ["125", "rl", "60min"], + ]; + + const observationsPlusMetadata = + await getMetadataPlusObservationsFromSingleOrMultipleDatastreams( + BASE_URL, + QUERY_PARAMS_COMBINED, + sensorsOfInterestNestedArr + ); + + // Extract the combined arrays for observations and metadata + const [observationsNestedArr, metadataNestedArr] = observationsPlusMetadata; + + // Extract values for x-axis and y-axis + // x-axis values are first element of nested observations array + const [obsXAxisArr] = observationsNestedArr.slice(0, 1); + // y-axis values are rest of elements of nested observations array + const obsYAxisNestedArr = observationsNestedArr.slice(1); + + // Create formatted array(s) for observations + const formattedObservationsArr = obsYAxisNestedArr.map((obsYAxisArr) => + formatSensorThingsApiResponseForScatterPlot(obsXAxisArr, obsYAxisArr) ); - // Extract the combined arrays for observations and metadata - const [observationsNestedArr, metadataNestedArr] = observationsPlusMetadata; - - // Create formatted array(s) for observations - // This function expects two arguments, these are unpacked using the spread operator - const formattedObservationsArr = formatSensorThingsApiResponseForScatterPlot( - ...observationsNestedArr - ); + // Create formatted array(s) for metadata + const formattedMetadataNestedArr = metadataNestedArr.map((metadataObj) => + formatDatastreamMetadataForChart(metadataObj) + ); - // Create formatted array(s) for metadata - const formattedMetadataNestedArr = metadataNestedArr.map((metadataObj) => - formatDatastreamMetadataForChart(metadataObj) - ); + // Extract the formatted metadata properties + const extractedFormattedDatastreamProperties = + extractPropertiesFromFormattedDatastreamMetadata( + formattedMetadataNestedArr + ); - // Extract the formatted metadata properties - const extractedFormattedDatastreamProperties = - extractPropertiesFromFormattedDatastreamMetadata( - formattedMetadataNestedArr + drawScatterPlotHighcharts( + formattedObservationsArr, + extractedFormattedDatastreamProperties ); - - drawScatterPlotHighcharts( - formattedObservationsArr, - extractedFormattedDatastreamProperties - ); + } catch (err) { + console.error(err); + } }; /** * Test drawing of line chart with multiple series */ const testLineChartMultipleSeries = async function () { - const sensorsOfInterestNestedArr = [ - ["225", "vl", "60min"], - ["125", "rl", "60min"], - ["weather_station_521", "outside_temp", "60min"], - ]; - - const observationsPlusMetadataArr = - await getMetadataPlusObservationsFromSingleOrMultipleDatastreams( - BASE_URL, - QUERY_PARAMS_COMBINED, - sensorsOfInterestNestedArr + try { + const sensorsOfInterestNestedArr = [ + ["225", "vl", "60min"], + ["125", "rl", "60min"], + ["weather_station_521", "outside_temp", "60min"], + ]; + + const observationsPlusMetadataArr = + await getMetadataPlusObservationsFromSingleOrMultipleDatastreams( + BASE_URL, + QUERY_PARAMS_COMBINED, + sensorsOfInterestNestedArr + ); + + // Extract the observations and metadata arrays of arrays + const [observationsNestedArr, metadataNestedArr] = + observationsPlusMetadataArr; + + // Format the observations + const formattedObservationsNestedArr = observationsNestedArr.map( + (observationsArr) => + formatSensorThingsApiResponseForLineChart(observationsArr) ); - // Extract the observations and metadata arrays of arrays - const [observationsNestedArr, metadataNestedArr] = - observationsPlusMetadataArr; - - // Format the observations - const formattedObservationsNestedArr = observationsNestedArr.map( - (observationsArr) => - formatSensorThingsApiResponseForLineChart(observationsArr) - ); - - // Format the metadata - const formattedMetadataNestedArr = metadataNestedArr.map((metadataArr) => - formatDatastreamMetadataForChart(metadataArr) - ); - - // Extract the formatted metadata properties - const extractedFormattedDatastreamProperties = - extractPropertiesFromFormattedDatastreamMetadata( - formattedMetadataNestedArr + // Format the metadata + const formattedMetadataNestedArr = metadataNestedArr.map((metadataArr) => + formatDatastreamMetadataForChart(metadataArr) ); - drawLineChartHighcharts( - formattedObservationsNestedArr, - extractedFormattedDatastreamProperties - ); + // Extract the formatted metadata properties + const extractedFormattedDatastreamProperties = + extractPropertiesFromFormattedDatastreamMetadata( + formattedMetadataNestedArr + ); + + drawLineChartHighcharts( + formattedObservationsNestedArr, + extractedFormattedDatastreamProperties + ); + } catch (err) { + console.error(err); + } }; /** * Test drawing of column chart using aggregation / sum result - monthly */ const drawColumnChartMonthlySumTest = async function () { - const sensorsOfInterestNestedArr = [ - ["125", "vl", "60min"], - ["225", "vl", "60min"], - ]; - - const observationsPlusMetadata = - await getMetadataPlusObservationsFromSingleOrMultipleDatastreams( - BASE_URL, - QUERY_PARAMS_COMBINED, - sensorsOfInterestNestedArr + try { + 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 [obsNestedArr, metadataNestedArr] = observationsPlusMetadata; + + // User-specified start date and end date + const startDate = "2020-02-01"; + const endDate = "2020-05-31"; + + // Extract observations within the user-specified start and end date + const observationsNestedArr = obsNestedArr.map((obsArr) => + extractObservationsWithinDatesInterval( + obsArr, + "60min", + startDate, + endDate + ) ); - // 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 sum of values of observations - monthly - const observationsSumMonthlyNestedArr = - calculateSumOfObservationValuesWithinInterval( - observationsNestedArr, - "60min", - uniqueCalendarMonthsNestedArr, - "monthly" + // Unique calendar dates + const uniqueCalendarDatesNestedArr = observationsNestedArr.map( + (observationsArr) => + extractUniqueCalendarDatesFromTimestamp(observationsArr) ); - // Format the observations - const formattedObservationsSumMonthlyNestedArr = - observationsSumMonthlyNestedArr.map((obsSumMonthlyArr, i) => - formatAggregationResultForColumnChart( - uniqueCalendarMonthsNestedArr[i], - obsSumMonthlyArr - ) + // Unique calendar months + const uniqueCalendarMonthsNestedArr = uniqueCalendarDatesNestedArr.map( + (uniqueCalendarDatesArr) => + extractUniqueCalendarMonthsFromCalendarDates(uniqueCalendarDatesArr) ); - // Format the metadata - const formattedMetadataNestedArr = metadataNestedArr.map((metadataObj) => - formatDatastreamMetadataForChart(metadataObj) - ); - - // Extract the formatted metadata properties - const extractedFormattedDatastreamProperties = - extractPropertiesFromFormattedDatastreamMetadata( - formattedMetadataNestedArr + // 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) ); - drawColumnChartHighcharts( - formattedObservationsSumMonthlyNestedArr, - extractedFormattedDatastreamProperties - ); + // Extract the formatted metadata properties + const extractedFormattedDatastreamProperties = + extractPropertiesFromFormattedDatastreamMetadata( + formattedMetadataNestedArr + ); + + drawColumnChartHighcharts( + formattedObservationsSumMonthlyNestedArr, + extractedFormattedDatastreamProperties + ); + } catch (err) { + console.error(err); + } }; /** * 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 + try { + 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 [obsNestedArr, metadataNestedArr] = observationsPlusMetadata; + + // User-specified start date and end date + const startDate = "2020-02-01"; + const endDate = "2020-05-31"; + + // Extract observations within the user-specified start and end date + const observationsNestedArr = obsNestedArr.map((obsArr) => + extractObservationsWithinDatesInterval( + obsArr, + "60min", + startDate, + endDate + ) ); - // 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" + // Unique calendar dates + const uniqueCalendarDatesNestedArr = observationsNestedArr.map( + (observationsArr) => + extractUniqueCalendarDatesFromTimestamp(observationsArr) ); - // Format the observations - daily - const formattedObservationsSumDailyNestedArr = - observationsSumDailyNestedArr.map((obsSumDailyArr, i) => - formatAggregationResultForColumnChart( - uniqueCalendarDatesNestedArr[i], - obsSumDailyArr - ) + // 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) ); - // Format the metadata - const formattedMetadataNestedArr = metadataNestedArr.map((metadataObj) => - formatDatastreamMetadataForChart(metadataObj) - ); + // Extract the formatted metadata properties + const extractedFormattedDatastreamProperties = + extractPropertiesFromFormattedDatastreamMetadata( + formattedMetadataNestedArr + ); - // Extract the formatted metadata properties - const extractedFormattedDatastreamProperties = - extractPropertiesFromFormattedDatastreamMetadata( - formattedMetadataNestedArr + drawColumnChartHighcharts( + formattedObservationsSumDailyNestedArr, + extractedFormattedDatastreamProperties ); - - drawColumnChartHighcharts( - formattedObservationsSumDailyNestedArr, - extractedFormattedDatastreamProperties - ); + } catch (err) { + console.error(err); + } }; /** * 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 + try { + 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 [obsNestedArr, metadataNestedArr] = observationsPlusMetadata; + + // User-specified start date and end date + const startDate = "2020-02-01"; + const endDate = "2020-05-31"; + + // Extract observations within the user-specified start and end date + const observationsNestedArr = obsNestedArr.map((obsArr) => + extractObservationsWithinDatesInterval( + obsArr, + "60min", + startDate, + endDate + ) ); - // 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" + // Unique calendar dates + const uniqueCalendarDatesNestedArr = observationsNestedArr.map( + (observationsArr) => + extractUniqueCalendarDatesFromTimestamp(observationsArr) ); - // Format the observations - const formattedObservationsAverageMonthlyNestedArr = - observationsAverageMonthlyNestedArr.map((obsAverageMonthlyArr, i) => - formatAggregationResultForColumnChart( - uniqueCalendarMonthsNestedArr[i], - obsAverageMonthlyArr - ) + // Unique calendar months + const uniqueCalendarMonthsNestedArr = uniqueCalendarDatesNestedArr.map( + (uniqueCalendarDatesArr) => + extractUniqueCalendarMonthsFromCalendarDates(uniqueCalendarDatesArr) ); - // Format the metadata - const formattedMetadataNestedArr = metadataNestedArr.map((metadataObj) => - formatDatastreamMetadataForChart(metadataObj) - ); - - // Extract the formatted metadata properties - const extractedFormattedDatastreamProperties = - extractPropertiesFromFormattedDatastreamMetadata( - formattedMetadataNestedArr + // Calculate average of values of observations - monthly + const observationsAverageMonthlyNestedArr = + calculateAverageOfObservationValuesWithinInterval( + observationsNestedArr, + "60min", + uniqueCalendarMonthsNestedArr, + "monthly" + ); + + // Format the observations + const formattedObservationsAverageMonthlyNestedArr = + observationsAverageMonthlyNestedArr.map((obsAverageMonthlyArr, i) => + formatAggregationResultForColumnChart( + uniqueCalendarMonthsNestedArr[i], + obsAverageMonthlyArr + ) + ); + + // Format the metadata + const formattedMetadataNestedArr = metadataNestedArr.map((metadataObj) => + formatDatastreamMetadataForChart(metadataObj) ); - drawLineChartHighcharts( - formattedObservationsAverageMonthlyNestedArr, - extractedFormattedDatastreamProperties - ); + // Extract the formatted metadata properties + const extractedFormattedDatastreamProperties = + extractPropertiesFromFormattedDatastreamMetadata( + formattedMetadataNestedArr + ); + + drawLineChartHighcharts( + formattedObservationsAverageMonthlyNestedArr, + extractedFormattedDatastreamProperties + ); + } catch (err) { + console.error(err); + } }; /** * 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 + try { + 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 [obsNestedArr, metadataNestedArr] = observationsPlusMetadata; + + // User-specified start date and end date + const startDate = "2020-02-01"; + const endDate = "2020-05-31"; + + // Extract observations within the user-specified start and end date + const observationsNestedArr = obsNestedArr.map((obsArr) => + extractObservationsWithinDatesInterval( + obsArr, + "60min", + startDate, + endDate + ) ); - // 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" + // Unique calendar dates + const uniqueCalendarDatesNestedArr = observationsNestedArr.map( + (observationsArr) => + extractUniqueCalendarDatesFromTimestamp(observationsArr) ); - // Format the observations - daily - const formattedObservationsAverageDailyNestedArr = - observationsAverageDailyNestedArr.map((obsAverageDailyArr, i) => - formatAggregationResultForColumnChart( - uniqueCalendarDatesNestedArr[i], - obsAverageDailyArr - ) + // 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) ); - // Format the metadata - const formattedMetadataNestedArr = metadataNestedArr.map((metadataObj) => - formatDatastreamMetadataForChart(metadataObj) - ); + // Extract the formatted metadata properties + const extractedFormattedDatastreamProperties = + extractPropertiesFromFormattedDatastreamMetadata( + formattedMetadataNestedArr + ); - // Extract the formatted metadata properties - const extractedFormattedDatastreamProperties = - extractPropertiesFromFormattedDatastreamMetadata( - formattedMetadataNestedArr + drawLineChartHighcharts( + formattedObservationsAverageDailyNestedArr, + extractedFormattedDatastreamProperties ); - - drawLineChartHighcharts( - formattedObservationsAverageDailyNestedArr, - extractedFormattedDatastreamProperties - ); + } catch (err) { + console.error(err); + } }; // drawScatterPlotHCTest2(); diff --git a/public/js/src_modules/aggregate.mjs b/public/js/src_modules/aggregate.mjs index 6c6a30c7b0f8f48642015bad8085823746873cf6..1174ffc0543100212d46fe6eec644ca40efae4a2 100644 --- a/public/js/src_modules/aggregate.mjs +++ b/public/js/src_modules/aggregate.mjs @@ -1,151 +1,9 @@ "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); -}; +import { + extractObservationValuesWithinDatesInterval, + extractObservationValuesWithinMonthInterval, +} from "./aggregateHelpers.mjs"; /** * Calculate the sum of observation values that fall within a time interval delimited by a start date and end date @@ -175,82 +33,6 @@ const calculateAverageOfObservationValuesWithinDatesInterval = function ( ); }; -/** - * 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 @@ -375,46 +157,7 @@ const calculateAverageOfObservationValuesWithinInterval = function ( } }; -/** - * 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/aggregateHelpers.mjs b/public/js/src_modules/aggregateHelpers.mjs new file mode 100644 index 0000000000000000000000000000000000000000..7df61a993d04e61174eefa0cfe6a808786608e56 --- /dev/null +++ b/public/js/src_modules/aggregateHelpers.mjs @@ -0,0 +1,298 @@ +"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 calendar date string in "YYYY-MM-DD" format representing the start date + * @param {String} endDate A calendar date string in "YYYY-MM-DD" format 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 observations 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 calendar date string in "YYYY-MM-DD" format representing the start date + * @param {String} endDate A calendar date string in "YYYY-MM-DD" format representing the end date + * @returns {Array} An array of observations (timestamp + value) that fall within our time interval + */ +const extractObservationsWithinDatesInterval = function ( + obsArray, + samplingRate, + startDate, + endDate +) { + // Extract the timestamps from the observations + const obsTimestampArr = obsArray.map((obs) => obs[0]); + + // 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 obsArray.slice(indexStartTimestamp, indexEndTimestamp + 1); +}; + +/** + * 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 calendar date string in "YYYY-MM-DD" format representing the start date + * @param {String} endDate A calendar date string in "YYYY-MM-DD" format 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 observation values that fall within our time interval + return obsValuesArr.slice(indexStartTimestamp, indexEndTimestamp + 1); +}; + +/** + * 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` + ); +}; + +/** + * 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 { + extractObservationsWithinDatesInterval, + extractObservationValuesWithinDatesInterval, + extractObservationValuesWithinMonthInterval, + extractUniqueCalendarDatesFromTimestamp, + extractUniqueCalendarMonthsFromCalendarDates, +}; diff --git a/public/js/src_modules/chartColumn.mjs b/public/js/src_modules/chartColumn.mjs index 9f259f307b0de2944da33f7b596973bdbea814e5..1da6590c29060ef74d51c77b60e1e840903958d3 100644 --- a/public/js/src_modules/chartColumn.mjs +++ b/public/js/src_modules/chartColumn.mjs @@ -1,6 +1,6 @@ "use strict"; -import { chartExportOptions } from "./chartExport.mjs"; +import { chartExportOptions } from "./chartHelpers.mjs"; /** * Format a computed aggregation result to make it suitable for a column chart @@ -42,6 +42,13 @@ const createSeriesOptionsForColumnChart = function ( // Create an array of seriesOptions objects // Assumes that the observation array of arrays, phenomenon names array and phenomenon symbols array are of equal length // Use one of the arrays for looping + if ( + formattedAggregatedResultForColumnChart.length !== phenomenonNamesArr.length + ) + throw new Error( + "The observations array and phenomenon names array have different lengths" + ); + return formattedAggregatedResultForColumnChart.map( (formattedAggResArray, i) => { return { @@ -57,7 +64,7 @@ const createSeriesOptionsForColumnChart = function ( * Draw a column chart using Highcharts library * @param {Array} formattedAggResultArraysForColumnChart An array made up of formatted aggregated result array(s) suitable for use in a column chart * @param {Object} extractedFormattedDatastreamProperties An object that contains arrays of formatted Datastream properties - * @returns {undefined} + * @returns {undefined} undefined */ const drawColumnChartHighcharts = function ( formattedAggResultArraysForColumnChart, diff --git a/public/js/src_modules/chartExport.mjs b/public/js/src_modules/chartExport.mjs deleted file mode 100644 index 0e26d0edbf8637eb7813d7d3483bfeeab100c1af..0000000000000000000000000000000000000000 --- a/public/js/src_modules/chartExport.mjs +++ /dev/null @@ -1,9 +0,0 @@ -"use strict"; - -export const chartExportOptions = { - buttons: { - contextButton: { - menuItems: ["downloadPNG", "downloadJPEG", "downloadPDF", "downloadSVG"], - }, - }, -}; diff --git a/public/js/src_modules/chartHeatmap.mjs b/public/js/src_modules/chartHeatmap.mjs index 50534b41b2b560439dde1b8e22a61fa20a60baf4..a48c5bd582d503b531a2b18e71768b65cdc8284b 100644 --- a/public/js/src_modules/chartHeatmap.mjs +++ b/public/js/src_modules/chartHeatmap.mjs @@ -1,6 +1,6 @@ "use strict"; -import { chartExportOptions } from "./chartExport.mjs"; +import { chartExportOptions } from "./chartHelpers.mjs"; /** * Format the response from SensorThings API to make it suitable for use in a heatmap diff --git a/public/js/src_modules/chartHelpers.mjs b/public/js/src_modules/chartHelpers.mjs new file mode 100644 index 0000000000000000000000000000000000000000..10f3d51eeed8c26ef24cd791a13aa5de987a8cb6 --- /dev/null +++ b/public/js/src_modules/chartHelpers.mjs @@ -0,0 +1,78 @@ +"use strict"; + +const chartExportOptions = { + buttons: { + contextButton: { + menuItems: ["downloadPNG", "downloadJPEG", "downloadPDF", "downloadSVG"], + }, + }, +}; + +/** + * Convert a hexadecimal color code obtained from the Highcharts object (`Highcharts.getOptions().colors`) to its equivalent RGB color code + * @param {String} hexCode Input hex color code + * @returns {String} Output RGB color code + */ +const convertHexColorToRGBColor = function (hexCode) { + const hexToRGBMapping = { + "#7cb5ec": "rgb(124, 181, 236)", + "#434348": "rgb(67, 67, 72)", + "#90ed7d": "rgb(144, 237, 125)", + "#f7a35c": "rgb(247, 163, 92)", + "#8085e9": "rgb(128, 133, 233)", + "#f15c80": "rgb(241, 92, 128)", + "#e4d354": "rgb(228, 211, 84)", + "#2b908f": "rgb(228, 211, 84)", + "#f45b5b": "rgb(244, 91, 91)", + "#91e8e1": "rgb(145, 232, 225)", + }; + + if (hexToRGBMapping?.[hexCode] === undefined) + throw new Error( + "The provided hex code is not valid or is not supported by this function" + ); + + // Extract the RGB color elements as a single string + // The individual color elements are separated by commas + return (hexToRGBMapping?.[hexCode]).slice(4, -1); +}; + +/** + * Concatenates metadata properties into a single string with an ampersand as the delimiter + * @param {Array} metadataPropertiesArr An array of metadata property strings + * @returns {String} A string made up of combined metadata properties delimited by an ampersand + */ +const createCombinedTextDelimitedByAmpersand = function ( + metadataPropertiesArr +) { + return metadataPropertiesArr.join(" & "); +}; + +/** + * Concatenates metadata properties into a single string with a comma as the delimiter + * @param {Array} metadataPropertiesArr An array of metadata property strings + * @returns {String} A string made up of combined metadata properties delimited by a comma + */ +const createCombinedTextDelimitedByComma = function (metadataPropertiesArr) { + return metadataPropertiesArr.join(", "); +}; + +/** + * Extracts the sampling rate substring from a datastream name string + * @param {Array} datastreamNamesArr An array of datastream name(s) + * @returns {Array} An array containing the sampling rate substring(s) + */ +const extractSamplingRateFromDatastreamName = function (datastreamNamesArr) { + // The sampling rate string is the last word in the Datastream name string + return datastreamNamesArr.map((datastreamName) => + datastreamName.split(" ").pop() + ); +}; + +export { + chartExportOptions, + createCombinedTextDelimitedByAmpersand, + createCombinedTextDelimitedByComma, + convertHexColorToRGBColor, + extractSamplingRateFromDatastreamName, +}; diff --git a/public/js/src_modules/chartLine.mjs b/public/js/src_modules/chartLine.mjs index aaf3f4fd5774709d29cf353b07eece9505afd2a3..a8f66989cdfe508c9bdae28da6d3f9d8ed08a1dd 100644 --- a/public/js/src_modules/chartLine.mjs +++ b/public/js/src_modules/chartLine.mjs @@ -1,6 +1,9 @@ "use strict"; -import { chartExportOptions } from "./chartExport.mjs"; +import { + chartExportOptions, + extractSamplingRateFromDatastreamName, +} from "./chartHelpers.mjs"; /** * Format the response from SensorThings API to make it suitable for use in a line chart @@ -17,27 +20,13 @@ const formatSensorThingsApiResponseForLineChart = function (obsArray) { }); }; -/** - * Extracts the sampling rate substring from a datastream name string - * @param {Array} datastreamNamesArr An array of datastream name(s) - * @returns {Array} An array containing the sampling rate substring(s) - */ -const extractSamplingRateFromDatastreamName = function (datastreamNamesArr) { - // The sampling rate string is the last word in the Datastream name string - return datastreamNamesArr.map((datastreamName) => - datastreamName.split(" ").pop() - ); -}; - /** * Concatenates metadata properties to create a string for either the title or subtitle of a line chart - * @param {Array} datastreamMetadataPropArr An array of metadata property strings - * @returns {String} A string of comma separated metadata property strings + * @param {Array} phenomenonNamesArr An array of phenomenon name strings + * @returns {String} A string made up of combined phenomenon names */ -const createCombinedTextForLineChartTitles = function ( - datastreamMetadataPropArr -) { - return datastreamMetadataPropArr.join(", "); +const createCombinedTextForLineChartTitles = function (phenomenonNamesArr) { + return phenomenonNamesArr.join(", "); }; /** @@ -50,17 +39,25 @@ const createSeriesOptionsForLineChart = function ( formattedObsArraysForLineChart, phenomenonNamesArr ) { - // An array of colors provided by the Highcharts object + // An array of colors, in hexadecimal format, provided by the global Highcharts object const seriesColors = Highcharts.getOptions().colors; + // Create a copy of the colors array + const seriesColorsArr = [...seriesColors]; + // Create an array of seriesOptions objects // Assumes that the observation array of arrays and phenomenon names array are of equal length // Use one of the arrays for looping + if (formattedObsArraysForLineChart.length !== phenomenonNamesArr.length) + throw new Error( + "The observations array and phenomenon names array have different lengths" + ); + return formattedObsArraysForLineChart.map((formattedObsArray, i) => { return { name: `${phenomenonNamesArr[i]}`, data: formattedObsArray, - color: seriesColors[i], + color: seriesColorsArr[i], turboThreshold: Number.MAX_VALUE, // #3404, remove after 4.0.5 release }; }); @@ -83,6 +80,14 @@ const drawLineChartHighcharts = function ( unitOfMeasurementSymbolsArr, } = extractedFormattedDatastreamProperties; + // Chart title and subtitle text + const textChartTitle = + createCombinedTextForLineChartTitles(phenomenonNamesArr); + + const textChartSubtitle = `Sampling rate(s): ${createCombinedTextForLineChartTitles( + extractSamplingRateFromDatastreamName(datastreamNamesArr) + )}`; + // Create the array of series options object(s) const seriesOptionsArr = createSeriesOptionsForLineChart( formattedObsArraysForLineChart, @@ -100,14 +105,12 @@ const drawLineChartHighcharts = function ( }, title: { - text: createCombinedTextForLineChartTitles(phenomenonNamesArr), + text: textChartTitle, "align": "left", }, subtitle: { - text: `Sampling rate(s): ${createCombinedTextForLineChartTitles( - extractSamplingRateFromDatastreamName(datastreamNamesArr) - )}`, + text: textChartSubtitle, align: "left", }, diff --git a/public/js/src_modules/chartScatterPlot.mjs b/public/js/src_modules/chartScatterPlot.mjs index 09b8592d2bf9bf4cdb89f77d009b62c12ad3880c..3b9b878661e27bb363597eb254dc6de989053299 100644 --- a/public/js/src_modules/chartScatterPlot.mjs +++ b/public/js/src_modules/chartScatterPlot.mjs @@ -1,6 +1,12 @@ "use strict"; -import { chartExportOptions } from "./chartExport.mjs"; +import { + chartExportOptions, + convertHexColorToRGBColor, + createCombinedTextDelimitedByAmpersand, + createCombinedTextDelimitedByComma, + extractSamplingRateFromDatastreamName, +} from "./chartHelpers.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 @@ -12,15 +18,13 @@ const getSymmetricDifferenceBetweenArrays = function ( obsTimestampArrayOne, obsTimestampArrayTwo ) { - const differenceBetweenArrays = obsTimestampArrayOne + return obsTimestampArrayOne .filter((timestampOne) => !obsTimestampArrayTwo.includes(timestampOne)) .concat( obsTimestampArrayTwo.filter( (timestampTwo) => !obsTimestampArrayOne.includes(timestampTwo) ) ); - - return differenceBetweenArrays; }; /** @@ -33,11 +37,9 @@ const getIndexesOfUniqueObservations = function ( uniqueTimestampsArr, largerObsTimestampArr ) { - const indexesMissingObs = uniqueTimestampsArr.map((index) => + return uniqueTimestampsArr.map((index) => largerObsTimestampArr.indexOf(index) ); - - return indexesMissingObs; }; /** @@ -213,11 +215,134 @@ const formatSensorThingsApiResponseForScatterPlot = function ( return createCombinedObservationValues(obsArrayOne, obsArrayTwo); }; +/** + * Concatenates metadata properties to create a string for either the title or subtitle of a scatter plot + * @param {Array} phenomenonNamesArr An array of phenomenon name strings + * @returns {String} A string made up of combined phenomenon names + */ +const createCombinedTextForScatterPlotTitles = function (phenomenonNamesArr) { + // x-axis phenomenon name is the first element of array + const phenomenonNameXAxis = phenomenonNamesArr[0]; + + // y-axis phenomenon name(s) array is remaining elements of array + const phenomenonNamesYAxisArr = phenomenonNamesArr.slice(1); + // Use a set to remove duplicates + const uniquePhenomenonNamesYAxis = new Set(phenomenonNamesYAxisArr); + const uniquePhenomenonNamesYAxisArr = [...uniquePhenomenonNamesYAxis]; + + return `${createCombinedTextDelimitedByAmpersand( + uniquePhenomenonNamesYAxisArr + )} versus ${phenomenonNameXAxis}`; +}; + +/** + * Create string for the x-axis title of a scatter plot + * @param {Array} phenomenonNamesArr Array of phenomenon name strings + * @param {Array} unitOfMeasurementSymbolsArr rray of unit of measurement symbol strings + * @returns {String} X-axis title string for scatter plot + */ +const createXAxisTitleTextScatterPlot = function ( + phenomenonNamesArr, + unitOfMeasurementSymbolsArr +) { + // x-axis phenomenon name string is first element of array + const phenomenonNameXAxis = phenomenonNamesArr[0]; + + // x-axis phenomenon symbol string is first element of array + const unitOfMeasurementSymbolXAxis = unitOfMeasurementSymbolsArr[0]; + + return `${phenomenonNameXAxis} [${unitOfMeasurementSymbolXAxis}]`; +}; + +/** + * Create string for the y-axis title of a scatter plot + * @param {Array} phenomenonNamesArr Array of phenomenon name strings + * @param {Array} unitOfMeasurementSymbolsArr Array of unit of measurement symbol strings + * @returns {String} Y-axis title string for scatter plot + */ +const createYAxisTitleTextScatterPlot = function ( + phenomenonNamesArr, + unitOfMeasurementSymbolsArr +) { + // y-axis phenomenon names start at array index 1 + const phenomenonNamesYAxisArr = phenomenonNamesArr.slice(1); + + // y-axis phenomenon symbols start at array index 1 + const unitOfMeasurementSymbolsYAxisArr = unitOfMeasurementSymbolsArr.slice(1); + + // The phenomenon names and unit of measurement arrays should have equal lengths + // Use one of the arrays for looping + if ( + phenomenonNamesYAxisArr.length !== unitOfMeasurementSymbolsYAxisArr.length + ) + throw new Error( + "The phenomenon names array and unit of measurement symbols array have different lengths" + ); + + const combinedNameSymbolArr = phenomenonNamesYAxisArr.map( + (phenomenonNameYAxis, i) => + `${phenomenonNameYAxis} [${unitOfMeasurementSymbolsYAxisArr[i]}]` + ); + + return createCombinedTextDelimitedByComma(combinedNameSymbolArr); +}; + +/** + * Create an options object for each series drawn in the scatter plot + * @param {Array} formattedObsArraysForScatterPlot An array of formatted observation array(s) from one or more datastreams + * @param {Array} phenomenonNamesArr An array of phenomenon name(s) + * @returns {Array} An array made up of series options object(s) + */ +const createSeriesOptionsForScatterPlot = function ( + formattedObsArraysForScatterPlot, + phenomenonNamesArr +) { + // An array of colors, in hexadecimal format, provided by the global Highcharts object + const highchartsColorsArr = Highcharts.getOptions().colors; + + // Create a reversed copy of the colors array + const highchartsColorsReversedArr = [...highchartsColorsArr].reverse(); + + // Opacity value for symbol + const SERIES_SYMBOL_COLOR_OPACITY = ".3"; + + // Create array of colors in RGBA format + const seriesColors = highchartsColorsReversedArr.map( + (hexColorCode) => + `rgba(${convertHexColorToRGBColor( + hexColorCode + )}, ${SERIES_SYMBOL_COLOR_OPACITY})` + ); + + // x-axis phenomenon name is the first element of array + const phenomenonNameXAxis = phenomenonNamesArr[0]; + // y-axis phenomenon name(s) array is remaining elements of array + const phenomenonNamesYAxisArr = phenomenonNamesArr.slice(1); + + // Create an array of seriesOptions objects + // Assumes that the observation array of arrays and phenomenon names array are of equal length + // Use one of the arrays for looping + if ( + formattedObsArraysForScatterPlot.length !== phenomenonNamesYAxisArr.length + ) + throw new Error( + "The observations array and phenomenon names array have different lengths" + ); + + return formattedObsArraysForScatterPlot.map((formattedObsArray, i) => { + return { + name: `${phenomenonNamesYAxisArr[i]}, ${phenomenonNameXAxis}`, + data: formattedObsArray, + color: seriesColors[i], + }; + }); +}; + /** * Draw a scatter plot using Highcharts library * @param {Array} formattedObsArrayForSeriesOnePlusSeriesTwo Response from SensorThings API formatted for use in a scatter plot * @param {Object} extractedFormattedDatastreamProperties An object that contains arrays of formatted Datastream properties - * @returns {undefined} + * @returns {undefined} undefined */ const drawScatterPlotHighcharts = function ( formattedObsArrayForSeriesOnePlusSeriesTwo, @@ -231,32 +356,32 @@ const drawScatterPlotHighcharts = function ( unitOfMeasurementSymbolsArr, } = extractedFormattedDatastreamProperties; - const [DATASTREAM_DESCRIPTION_SERIES_1, DATASTREAM_DESCRIPTION_SERIES_2] = - datastreamDescriptionsArr; - const [DATASTREAM_NAME_SERIES_1, DATASTREAM_NAME_SERIES_2] = - datastreamNamesArr; - const [PHENOMENON_NAME_SERIES_1, PHENOMENON_NAME_SERIES_2] = - phenomenonNamesArr; - const [PHENOMENON_SYMBOL_SERIES_1, PHENOMENON_SYMBOL_SERIES_2] = - unitOfMeasurementSymbolsArr; + // Create the array of series options object(s) + const seriesOptionsArr = createSeriesOptionsForScatterPlot( + formattedObsArrayForSeriesOnePlusSeriesTwo, + phenomenonNamesArr + ); - // Order of axes - // Y-Axis -- Series 2 - // X-Axis -- Series 1 + const CHART_TITLE = + createCombinedTextForScatterPlotTitles(phenomenonNamesArr); - const CHART_TITLE = `${PHENOMENON_NAME_SERIES_2} Versus ${PHENOMENON_NAME_SERIES_1}`; - const CHART_SUBTITLE = `Source: ${DATASTREAM_NAME_SERIES_2} & ${DATASTREAM_NAME_SERIES_1}`; + const CHART_SUBTITLE = `Sampling rate(s): ${createCombinedTextDelimitedByComma( + extractSamplingRateFromDatastreamName(datastreamNamesArr) + )}`; - const SERIES_1_NAME = `${PHENOMENON_NAME_SERIES_1}`; - const SERIES_1_SYMBOL = `${PHENOMENON_SYMBOL_SERIES_1}`; + const X_AXIS_TITLE = createXAxisTitleTextScatterPlot( + phenomenonNamesArr, + unitOfMeasurementSymbolsArr + ); - const SERIES_2_NAME = `${PHENOMENON_NAME_SERIES_2}`; - const SERIES_2_SYMBOL = `${PHENOMENON_SYMBOL_SERIES_2}`; + const Y_AXIS_TITLE = createYAxisTitleTextScatterPlot( + phenomenonNamesArr, + unitOfMeasurementSymbolsArr + ); - const SERIES_COMBINED_NAME = "Y, X"; - const SERIES_COMBINED_SYMBOL_COLOR_RGB_ELEMENTS = "223, 83, 83"; - const SERIES_COMBINED_SYMBOL_COLOR_OPACITY = ".3"; - const SERIES_COMBINED_SYMBOL_COLOR = `rgba(${SERIES_COMBINED_SYMBOL_COLOR_RGB_ELEMENTS}, ${SERIES_COMBINED_SYMBOL_COLOR_OPACITY})`; + // The unit of measurement symbols for the x-axis is the first element of the array + // Assume that we will be comparing similar phenomena, so we can reuse this symbol + const UNIT_OF_MEASUREMENT_SYMBOL = unitOfMeasurementSymbolsArr[0]; const MARKER_RADIUS = 2; @@ -273,10 +398,12 @@ const drawScatterPlotHighcharts = function ( title: { text: CHART_TITLE, + "align": "left", }, subtitle: { text: CHART_SUBTITLE, + "align": "left", }, xAxis: { @@ -285,7 +412,7 @@ const drawScatterPlotHighcharts = function ( }, title: { enabled: true, - text: `${SERIES_1_NAME} [${SERIES_1_SYMBOL}]`, + text: X_AXIS_TITLE, }, startOnTick: true, endOnTick: true, @@ -298,7 +425,7 @@ const drawScatterPlotHighcharts = function ( format: `{value}`, }, title: { - text: `${SERIES_2_NAME} [${SERIES_2_SYMBOL}]`, + text: Y_AXIS_TITLE, }, }, ], @@ -333,22 +460,16 @@ const drawScatterPlotHighcharts = function ( const headerString = `${this.series.name}<br>`; const pointString = `<b>${this.point.y.toFixed( 2 - )} ${SERIES_1_SYMBOL}, ${this.point.x.toFixed( + )} ${UNIT_OF_MEASUREMENT_SYMBOL}, ${this.point.x.toFixed( 2 - )} ${SERIES_2_SYMBOL}</b>`; + )} ${UNIT_OF_MEASUREMENT_SYMBOL}</b>`; return headerString + pointString; }, }, exporting: chartExportOptions, - series: [ - { - name: SERIES_COMBINED_NAME, - color: SERIES_COMBINED_SYMBOL_COLOR, - data: formattedObsArrayForSeriesOnePlusSeriesTwo, - }, - ], + series: seriesOptionsArr, }); }; diff --git a/public/js/src_modules/fetchData.mjs b/public/js/src_modules/fetchData.mjs index 718620d2ae827553422df275126cf5931f0c9c5d..0311398ccdcb8ab4e088692357667ff38e95f137 100644 --- a/public/js/src_modules/fetchData.mjs +++ b/public/js/src_modules/fetchData.mjs @@ -271,6 +271,7 @@ const getObservationsFromMultipleDatastreams = async function ( /** * Retrieve the metadata from a single Datastream or multiple Datastreams and the Observations corresponding to the Datastream(s) + * @async * @param {String} baseUrl Base URL of the STA server * @param {Object} urlParamObj The URL parameters to be sent together with the GET request * @param {Array} bldgSensorSamplingRateNestedArr A N*1 array (where N >= 1) containing a nested array of buildings, sensors & sampling rates as strings, i.e. [["101", "rl", "15min"]] or [["101", "rl", "15min"], ["102", "vl", "60min"]] or [["101", "rl", "15min"], ["102", "vl", "60min"], ["225", "vl", "60min"]], etc