"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,
};