diff --git a/public/js/appChart.js b/public/js/appChart.js index 2c925e9e5d96980343f5c9eaabd2517ea7d54416..c66a8b8f99a9f641f2b47ad44789c68f86799517 100644 --- a/public/js/appChart.js +++ b/public/js/appChart.js @@ -464,6 +464,273 @@ const drawLineChartHC = function ( }); }; +/** + * 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) => !obsTimestampArrayTwo.includes(timestampTwo) + ) + ); + + return differenceBetweenArrays; +}; + +/** + * Determines the indexes of timestamps that are missing from a smaller set of observatiuons. Based on the comparison of two observation arrays, where one array is larger than the other + * @param {Array} missingTimestampsArr An array of strings representing the missing timestamps + * @param {Array} largerObsTimestampArr An array of timestamps for the larger array of observations + * @returns {Array} An array of the indexes of the missing observations + */ +const getIndexOfMissingObservation = function ( + missingTimestampsArr, + largerObsTimestampArr +) { + const indexesMissingObs = missingTimestampsArr.map((index) => + largerObsTimestampArr.indexOf(index) + ); + + return indexesMissingObs; +}; + +/** + * Removes observations (by modifying array in place) from a larger set of observations that are missing from a smaller set of observatiuons. Based on the comparison of two observation arrays, where one array is larger than the other + * @param {Array} missingIndexesArr An array of the indexes of the observations missing from the smaller set of observations + * @param {Array} largerObsArr The larger array of observations (timestamp + value) which is modified in place + * @returns {undefined} + */ +const removeMissingObservationFromLargerArray = function ( + missingIndexesArr, + largerObsArr +) { + missingIndexesArr.forEach((index) => { + if (index > -1) { + largerObsArr.splice(index, 1); + } + }); +}; + +/** + * 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; +}; + +/** + * Extracts and combines observation values from two imput 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 2*N 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 scatter plot + * @param {Array} obsArrayOne Response from SensorThings API as array + * @param {Array} obsArrayTwo Response from SensorThings API as array + * @returns {Array} Array of formatted observations suitable for use in a scatter plot + */ +const formatSTAResponseForScatterPlot = function (obsArrayOne, obsArrayTwo) { + // When our observation arrays have DIFFERENT lengths + if (obsArrayOne.length !== obsArrayTwo.length) { + // 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 = getIndexOfMissingObservation( + missingTimestamp, + biggerObsTimestampArr + ); + + // Determine the larger observation array + const biggerObsArr = getLargerArrayBetweenTwoInputArrays( + obsArrayOne, + obsArrayTwo + ); + + // Remove the missing observation from the larger array of observations + // Modifies the array in place + removeMissingObservationFromLargerArray(indexesMissingObsArr, biggerObsArr); + + return createCombinedObservationValues(obsArrayOne, obsArrayTwo); + } + + // When our observation arrays already have SAME lengths + return createCombinedObservationValues(obsArrayOne, obsArrayTwo); +}; + +/** + * Draw a scatter plot using Highcharts library + * @param {*} formattedObsArrayForSeriesOnePlusSeriesTwo Response from SensorThings API formatted for use in a scatter plot + * @param {*} formattedDatastreamMetadataSeriesOne Object containing Datastream metadata for the first chart series + * @param {*} formattedDatastreamMetadataSeriesTwo Object containing Datastream metadata for the second chart series + * @returns {undefined} + */ +const drawScatterPlotHC = 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}<br>", + pointFormat: `<b>{point.y:.2f} ${SERIES_1_SYMBOL}, {point.x:.2f} ${SERIES_2_SYMBOL}</b>`, + }, + }, + }, + + series: [ + { + name: SERIES_COMBINED_NAME, + color: SERIES_COMBINED_SYMBOL_COLOR, + data: formattedObsArrayForSeriesOnePlusSeriesTwo, + }, + ], + }); +}; + /** * Follows "@iot.nextLink" links in SensorThingsAPI's response * Appends new results to existing results @@ -625,305 +892,14 @@ const getMetadataPlusObservationsFromMultipleDatastreams = async function ( } }; -const drawScatterPlotHC = function ( - formattedObsArrayForSeriesOne, - formattedDatastreamMetadataSeriesOne, - formattedObsArrayForSeriesTwo, - 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; - - const CHART_TITLE = `${PHENOMENON_NAME_SERIES_1} Versus ${PHENOMENON_NAME_SERIES_2}`; - const CHART_SUBTITLE = `Source: ${DATASTREAM_NAME_SERIES_1} & ${DATASTREAM_NAME_SERIES_2}`; - - const X_AXIS_TITLE = `Date / Time`; - - const SERIES_1_NAME = `${PHENOMENON_NAME_SERIES_1}`; - const SERIES_1_SYMBOL_COLOR_RGB_ELEMENTS = "119, 152, 191"; - const SERIES_1_SYMBOL_COLOR = `rgba(${SERIES_1_SYMBOL_COLOR_RGB_ELEMENTS}, .5)`; - const SERIES_1_TEXT_COLOR = `rgb(${SERIES_1_SYMBOL_COLOR_RGB_ELEMENTS})`; // remove transparency from symbol color for a more "intense" color - const SERIES_1_SYMBOL = `${PHENOMENON_SYMBOL_SERIES_1}`; - - const SERIES_2_NAME = `${PHENOMENON_NAME_SERIES_2}`; - const SERIES_2_SYMBOL_COLOR_RGB_ELEMENTS = "223, 83, 83"; - const SERIES_2_SYMBOL_COLOR = `rgba(${SERIES_2_SYMBOL_COLOR_RGB_ELEMENTS}, .5)`; - const SERIES_2_TEXT_COLOR = `rgb(${SERIES_2_SYMBOL_COLOR_RGB_ELEMENTS})`; // remove transparency from symbol color for a more "intense" color - const SERIES_2_SYMBOL = `${PHENOMENON_SYMBOL_SERIES_2}`; - - 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: { - title: { - enabled: true, - text: X_AXIS_TITLE, - }, - type: "datetime", - startOnTick: true, - endOnTick: true, - showLastLabel: true, - }, - - yAxis: [ - { - // Primary yAxis - labels: { - format: `{value} ${SERIES_1_SYMBOL}`, - style: { - color: SERIES_1_TEXT_COLOR, - }, - }, - title: { - text: SERIES_1_NAME, - style: { - color: SERIES_1_TEXT_COLOR, - }, - }, - }, - { - // Secondary yAxis - title: { - text: SERIES_2_NAME, - style: { - color: SERIES_2_TEXT_COLOR, - }, - }, - labels: { - format: `{value} ${SERIES_2_SYMBOL}`, - style: { - color: SERIES_2_TEXT_COLOR, - }, - }, - opposite: true, - }, - ], - - // legend: { - // layout: "vertical", - // align: "left", - // verticalAlign: "top", - // x: 100, - // y: 70, - // floating: true, - // backgroundColor: Highcharts.defaultOptions.chart.backgroundColor, - // borderWidth: 1, - // }, - 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: "{point.x:%e %b, %Y %H:%M:%S}: <b>{point.y}</b>", - pointFormat: "{point.x} cm, {point.y} kg", - valueDecimals: 2, - }, - }, - }, - - series: [ - { - name: SERIES_1_NAME, - color: SERIES_1_SYMBOL_COLOR, - data: formattedObsArrayForSeriesOne, - tooltip: { - valueSuffix: ` ${SERIES_1_SYMBOL}`, - }, - }, - { - name: SERIES_2_NAME, - color: SERIES_2_SYMBOL_COLOR, - data: formattedObsArrayForSeriesTwo, - // need this property for the dual y-axes to work - // defines the y-axis that this series refers to - yAxis: 1, - tooltip: { - valueSuffix: ` ${SERIES_2_SYMBOL}`, - }, - }, - ], - }); -}; - -const formatSTAResponseForScatterPlot = function (obsArrayOne, obsArrayTwo) { - // Check if the arrays have the same length - // We want `obsArrayOne` to be the larger array - const [obsArrayOneChecked, obsArrayTwoChecked] = (() => { - if (obsArrayTwo.length > obsArrayOne.length) { - return [obsArrayTwo, obsArrayOne]; - } else if ( - obsArrayOne.length > obsArrayTwo.length || - obsArrayOne.length === obsArrayTwo.length - ) { - return [obsArrayOne, obsArrayTwo]; - } - })(); - - console.log(obsArrayOneChecked.length); - console.log(obsArrayTwoChecked.length); - - // Create arrays with timestamps only - const obsArrayOneTimestamp = obsArrayOneChecked.map( - (obsTimeValue) => obsTimeValue[0] - ); - const obsArrayTwoTimestamp = obsArrayTwoChecked.map( - (obsTimeValue) => obsTimeValue[0] - ); - - // console.log(obsArrayOneTimestamp); - - /** - * - * @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 the second set of observations - */ - const getMissingTimestampFromSecondArray = function ( - obsTimestampArrayOne, - obsTimestampArrayTwo - ) { - const differenceBetweenArrays = obsTimestampArrayOne.filter( - (timestampOne) => !obsTimestampArrayTwo.includes(timestampOne) - ); - // .concat( - // obsTimestampArrayTwo.filter( - // (timestampTwo) => !obsArrayTwo.includes(timestampTwo) - // ) - // ); - - return differenceBetweenArrays; - }; - - const missingTimestamp = getMissingTimestampFromSecondArray( - obsArrayOneTimestamp, - obsArrayTwoTimestamp - ); - console.log(missingTimestamp); - - /** - * - * @param {Array} missingTimestampsArr An array of strings representing the missing timestamps - * @param {Array} firstObsTimestampArr An array of timestamps for the first set of observations - * @returns {Array} An array of the indexes of the missing observations - */ - const getIndexOfMissingObservation = function ( - missingTimestampsArr, - firstObsTimestampArr - ) { - const indexesMissingObs = missingTimestampsArr.map((index) => - firstObsTimestampArr.indexOf(index) - ); - - // console.log(indexes); - return indexesMissingObs; - }; - - const indexesMissingObsArr = getIndexOfMissingObservation( - missingTimestamp, - obsArrayOneTimestamp - ); - - /** - * - * @param {*} missingIndexesArr An array of the indexes of the observations missing from the second set of observations - * @param {*} obsOneArr An array of the first set of observations (timestamp + value) - * @returns {undefined} - */ - const removeMissingObservationFromFirstArray = function ( - missingIndexesArr, - obsOneArr - ) { - missingIndexesArr.forEach((index) => { - if (index > -1) { - obsOneArr.splice(index, 1); - } - }); - }; - - removeMissingObservationFromFirstArray( - indexesMissingObsArr, - obsArrayOneChecked - ); - console.log(obsArrayOneChecked.length); -}; - -(async () => { - const sensorsOfInterestArr = [ - // ["225", "vl", "60min"], - ["125", "rl", "60min"], - ["weather_station_521", "outside_temp", "60min"], - ]; - - const observationsPlusMetadata = - await getMetadataPlusObservationsFromMultipleDatastreams( - sensorsOfInterestArr - ); - - // Extract the observations and metadata for each sensor - // Array elements in same order as input array - const [ - [obsSensorOneArr, obsSensorTwoArr], - [metadataSensorOne, metadataSensorTwo], - ] = observationsPlusMetadata; - - formatSTAResponseForScatterPlot(obsSensorOneArr, obsSensorTwoArr); -})(); - /** * Test drawing of scatter plot chart */ -const drawScatterPlotHCTest = async function () { - // Input array - building, sensor, samplingRate +const drawScatterPlotHCTest2 = async function () { const sensorsOfInterestArr = [ - ["weather_station_521", "outside_temp", "60min"], ["225", "vl", "60min"], + // ["125", "rl", "60min"], + ["weather_station_521", "outside_temp", "60min"], ]; const observationsPlusMetadata = @@ -939,15 +915,14 @@ const drawScatterPlotHCTest = async function () { ] = observationsPlusMetadata; drawScatterPlotHC( - formatSTAResponseForLineChartOrScatterPlot(obsSensorOneArr), + formatSTAResponseForScatterPlot(obsSensorOneArr, obsSensorTwoArr), formatDatastreamMetadataForChart(metadataSensorOne), - formatSTAResponseForLineChartOrScatterPlot(obsSensorTwoArr), formatDatastreamMetadataForChart(metadataSensorTwo) ); }; (async () => { - // await drawScatterPlotHCTest(); + await drawScatterPlotHCTest2(); })(); export {