"use strict"; /** * 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 * @param {Array} obsTimestampArrayTwo An array of timstamps for the second set of observations * @returns {Array} An array of timestamps missing from either set of observations */ const getSymmetricDifferenceBetweenArrays = function ( obsTimestampArrayOne, obsTimestampArrayTwo ) { const differenceBetweenArrays = obsTimestampArrayOne .filter((timestampOne) => !obsTimestampArrayTwo.includes(timestampOne)) .concat( obsTimestampArrayTwo.filter( (timestampTwo) => !obsTimestampArrayOne.includes(timestampTwo) ) ); return differenceBetweenArrays; }; /** * Determines the indexes of timestamps that are unique to the larger set of observatiuons. Based on the comparison of two observation arrays, where one array is larger than the other * @param {Array} uniqueTimestampsArr An array of timestamps unique to the larger set of observations * @param {Array} largerObsTimestampArr An array of timestamps for the larger set of observations * @returns {Array} An array of the indexes of the missing observations */ const getIndexesOfUniqueObservations = function ( uniqueTimestampsArr, largerObsTimestampArr ) { const indexesMissingObs = uniqueTimestampsArr.map((index) => largerObsTimestampArr.indexOf(index) ); return indexesMissingObs; }; /** * Removes observations (by modifying array in place) that are unique to a larger set of observations. Based on the comparison of two observation arrays, where one array is larger than the other * @param {Array} uniqueIndexesArr An array of the indexes unique to the larger set of observations * @param {Array} largerObsArr The larger array of observations (timestamp + value) * @returns {Array} The larger array with the unique indexes removed */ const removeUniqueObservationsFromLargerArray = function ( uniqueIndexesArr, largerObsArr ) { // Create a reversed copy of the indexes array, so that the larger index is removed first const reversedUniqueIndexesArr = uniqueIndexesArr.reverse(); // Create a copy the larger observation array, will be modified in place const processedLargerObsArr = largerObsArr; reversedUniqueIndexesArr.forEach((index) => { if (index > -1) { processedLargerObsArr.splice(index, 1); } }); return processedLargerObsArr; }; /** * Compares the length of two input arrays to determine the larger one * @param {Array} firstArr First input array * @param {Array} secondArr Second input array * @returns {Array} The larger array */ const getLargerArrayBetweenTwoInputArrays = function (firstArr, secondArr) { if (firstArr.length === secondArr.length) return; if (firstArr.length > secondArr.length) return firstArr; if (firstArr.length < secondArr.length) return secondArr; }; /** * Compares the length of two input arrays to determine the smaller one * @param {Array} firstArr First input array * @param {Array} secondArr Second input array * @returns {Array} The smaller array */ const getSmallerArrayBetweenTwoInputArrays = function (firstArr, secondArr) { if (firstArr.length === secondArr.length) return; if (firstArr.length < secondArr.length) return firstArr; if (firstArr.length > secondArr.length) return secondArr; }; /** * Utility function for deleting the unique observations from a larger array * @param {Array} obsArrayOne Array of observations (timestamp + value) that is response from SensorThings API * @param {Array} obsArrayTwo Array of observations (timestamp + value) that is response from SensorThings API * @returns {Array} Two arrays of observations (timestamp + value) with matching timestamps and equal lengths */ const deleteUniqueObservationsFromLargerArray = function ( obsArrayOne, obsArrayTwo ) { // Create arrays with timestamps only const obsArrayOneTimestamp = obsArrayOne.map( (obsTimeValue) => obsTimeValue[0] ); const obsArrayTwoTimestamp = obsArrayTwo.map( (obsTimeValue) => obsTimeValue[0] ); const missingTimestamp = getSymmetricDifferenceBetweenArrays( obsArrayOneTimestamp, obsArrayTwoTimestamp ); // Determine the larger observation timestamp array const biggerObsTimestampArr = getLargerArrayBetweenTwoInputArrays( obsArrayOneTimestamp, obsArrayTwoTimestamp ); // Indexes of the missing observations const indexesMissingObsArr = getIndexesOfUniqueObservations( missingTimestamp, biggerObsTimestampArr ); // Determine the larger observation array const biggerObsArr = getLargerArrayBetweenTwoInputArrays( obsArrayOne, obsArrayTwo ); // Determine the smaller observation array const smallerObsArr = getSmallerArrayBetweenTwoInputArrays( obsArrayOne, obsArrayTwo ); // Remove the missing observation from the larger array of observations const modifiedBiggerObsArr = removeUniqueObservationsFromLargerArray( indexesMissingObsArr, biggerObsArr ); return [modifiedBiggerObsArr, smallerObsArr]; }; /** * Utility function for deleting the unique observations from a larger array AND ensuring the order of input arrays is maintained * @param {Array} obsArrayOne Array of observations (timestamp + value) that is response from SensorThings API * @param {Array} obsArrayTwo Array of observations (timestamp + value) that is response from SensorThings API * @returns {Array} Two arrays of observations (timestamp + value) with matching timestamps and equal lengths */ const checkForAndDeleteUniqueObservationsFromLargerArray = function ( obsArrayOne, obsArrayTwo ) { if (obsArrayOne.length === obsArrayTwo.length) return; // Case 1: obsArrayOne.length < obsArrayTwo.length if (obsArrayOne.length < obsArrayTwo.length) { const [biggerObsArr, smallerObsArr] = deleteUniqueObservationsFromLargerArray(obsArrayOne, obsArrayTwo); return [smallerObsArr, biggerObsArr]; } // Case 2: obsArrayOne.length > obsArrayTwo.length return deleteUniqueObservationsFromLargerArray(obsArrayOne, obsArrayTwo); }; /** * Extracts and combines observation values from two input observation arrays of equal length * @param {Array} obsArrayOne First set of N observations (timestamp + value) * @param {Array} obsArrayTwo Second set of N observations (timestamp + value) * @returns {Array} A N*2 array of observation values from both input observation arrays */ const createCombinedObservationValues = function (obsArrayOne, obsArrayTwo) { // Extract the values from the two observation arrays const obsValuesOne = obsArrayOne.map((result) => result[1]); const obsValuesTwo = obsArrayTwo.map((result) => result[1]); // Since the arrays are of equal length, we need only use one of the arrays for looping const obsValuesOnePlusTwo = obsValuesOne.map((obsValOne, i) => { return [obsValOne, obsValuesTwo[i]]; }); return obsValuesOnePlusTwo; }; /** * Format the response from SensorThings API to make it suitable for use in a scatter plot * @param {Array} obsArrayOne Array of observations (timestamp + value) that is response from SensorThings API * @param {Array} obsArrayTwo Array of observations (timestamp + value) that is response from SensorThings API * @returns {Array} Array of formatted observations suitable for use in a scatter plot */ const formatSensorThingsApiResponseForScatterPlot = function ( obsArrayOne, obsArrayTwo ) { // When our observation arrays have DIFFERENT lengths if (obsArrayOne.length !== obsArrayTwo.length) { const [obsArrayOneFinal, obsArrayTwoFinal] = checkForAndDeleteUniqueObservationsFromLargerArray( obsArrayOne, obsArrayTwo ); return createCombinedObservationValues(obsArrayOneFinal, obsArrayTwoFinal); } // When our observation arrays already have SAME lengths return createCombinedObservationValues(obsArrayOne, obsArrayTwo); }; /** * Draw a scatter plot using Highcharts library * @param {Array} formattedObsArrayForSeriesOnePlusSeriesTwo Response from SensorThings API formatted for use in a scatter plot * @param {Object} formattedDatastreamMetadataSeriesOne Object containing Datastream metadata for the first chart series * @param {Object} formattedDatastreamMetadataSeriesTwo Object containing Datastream metadata for the second chart series * @returns {undefined} */ const drawScatterPlotHighcharts = function ( formattedObsArrayForSeriesOnePlusSeriesTwo, formattedDatastreamMetadataSeriesOne, formattedDatastreamMetadataSeriesTwo ) { const { datastreamDescription: DATASTREAM_DESCRIPTION_SERIES_1, datastreamName: DATASTREAM_NAME_SERIES_1, phenomenonName: PHENOMENON_NAME_SERIES_1, unitOfMeasurementSymbol: PHENOMENON_SYMBOL_SERIES_1, } = formattedDatastreamMetadataSeriesOne; const { datastreamDescription: DATASTREAM_DESCRIPTION_SERIES_2, datastreamName: DATASTREAM_NAME_SERIES_2, phenomenonName: PHENOMENON_NAME_SERIES_2, unitOfMeasurementSymbol: PHENOMENON_SYMBOL_SERIES_2, } = formattedDatastreamMetadataSeriesTwo; // Order of axes // Y-Axis -- Series 2 // X-Axis -- Series 1 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 SERIES_1_NAME = `${PHENOMENON_NAME_SERIES_1}`; const SERIES_1_SYMBOL = `${PHENOMENON_SYMBOL_SERIES_1}`; const SERIES_2_NAME = `${PHENOMENON_NAME_SERIES_2}`; const SERIES_2_SYMBOL = `${PHENOMENON_SYMBOL_SERIES_2}`; 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})`; const MARKER_RADIUS = 2; Highcharts.chart("chart-scatter-plot", { chart: { type: "scatter", zoomType: "xy", }, boost: { useGPUTranslations: true, usePreAllocated: true, }, title: { text: CHART_TITLE, }, subtitle: { text: CHART_SUBTITLE, }, xAxis: { labels: { format: `{value}`, }, title: { enabled: true, text: `${SERIES_1_NAME} [${SERIES_1_SYMBOL}]`, }, startOnTick: true, endOnTick: true, showLastLabel: true, }, yAxis: [ { labels: { format: `{value}`, }, title: { text: `${SERIES_2_NAME} [${SERIES_2_SYMBOL}]`, }, }, ], legend: { enabled: false, }, plotOptions: { scatter: { marker: { radius: MARKER_RADIUS, states: { hover: { enabled: true, lineColor: "rgb(100,100,100)", }, }, }, states: { hover: { marker: { enabled: false, }, }, }, tooltip: { headerFormat: "{series.name}
", pointFormat: `{point.y:.2f} ${SERIES_1_SYMBOL}, {point.x:.2f} ${SERIES_2_SYMBOL}`, }, }, }, series: [ { name: SERIES_COMBINED_NAME, color: SERIES_COMBINED_SYMBOL_COLOR, data: formattedObsArrayForSeriesOnePlusSeriesTwo, }, ], }); }; export { formatSensorThingsApiResponseForScatterPlot, drawScatterPlotHighcharts, };