Commit 0270f869 authored by Pithon Kabiro's avatar Pithon Kabiro
Browse files

Merge branch 'wip_scatter-plot-2b' into 'master'

Update scatter plot chart

Further improvements to logic for drawing scatter plot chart

See merge request !3
parents d5b98048 90e8a3a2
...@@ -206,6 +206,37 @@ const getMetadataFromMultipleDatastreams = async function (datastreamsUrlArr) { ...@@ -206,6 +206,37 @@ const getMetadataFromMultipleDatastreams = async function (datastreamsUrlArr) {
} }
}; };
/**
* Match the unitOfMeasurement's string representation of a symbol to an actual symbol, where necessary
* @param {String} unitOfMeasurementSymbolString String representation of the unitOfMeasurement's symbol
* @returns {String} The unitOfMeasurement's symbol
*/
const matchUnitOfMeasurementSymbolStringToSymbol = function (
unitOfMeasurementSymbolString
) {
const unicodeCodePointDegreeSymbol = "\u00B0";
const unicodeCodePointSuperscriptThree = "\u00B3";
if (unitOfMeasurementSymbolString === "degC")
return `${unicodeCodePointDegreeSymbol}C`;
if (unitOfMeasurementSymbolString === "m3/h")
return `m${unicodeCodePointSuperscriptThree}/h`;
// If no symbol exists
return unitOfMeasurementSymbolString;
};
/**
* Extract the phenomenon name from a Datastream's name
* @param {String} datastreamName A string representing the Datastream's name
* @returns {String} The extracted phenomenon name
*/
const extractPhenomenonNameFromDatastreamName = function (datastreamName) {
const regex = /\/ (.*) DS/;
return datastreamName.match(regex)[1]; // use second element in array
};
/** /**
* Format the response containing a Datastream's metadata from Sensorthings API * Format the response containing a Datastream's metadata from Sensorthings API
* @param {Object} datastreamMetadata An object containing a Datastream's metadata * @param {Object} datastreamMetadata An object containing a Datastream's metadata
...@@ -219,20 +250,13 @@ const formatDatastreamMetadataForChart = function (datastreamMetadata) { ...@@ -219,20 +250,13 @@ const formatDatastreamMetadataForChart = function (datastreamMetadata) {
} = datastreamMetadata; } = datastreamMetadata;
// Extract phenomenon name from Datastream name // Extract phenomenon name from Datastream name
const regex = /\/ (.*) DS/; const phenomenonName =
const phenomenonName = datastreamName.match(regex)[1]; // use second element in array extractPhenomenonNameFromDatastreamName(datastreamName);
// Match the unitOfMeasurement's string representation of a symbol // Get the unitOfMeasurement's symbol
// to an actual symbol, where necessary const unitOfMeasurementSymbol = matchUnitOfMeasurementSymbolStringToSymbol(
const unitOfMeasurementSymbol = (() => { unitOfMeasurement.symbol
if (unitOfMeasurement.symbol === "degC") { );
return "";
} else if (unitOfMeasurement.symbol === "m3/h") {
return "m<sup>3</sup>/h";
} else {
return unitOfMeasurement.symbol;
}
})();
return { return {
datastreamDescription, datastreamDescription,
...@@ -244,7 +268,7 @@ const formatDatastreamMetadataForChart = function (datastreamMetadata) { ...@@ -244,7 +268,7 @@ const formatDatastreamMetadataForChart = function (datastreamMetadata) {
/** /**
* Format the response from SensorThings API to make it suitable for heatmap * Format the response from SensorThings API to make it suitable for heatmap
* @param {Array} obsArray Response from SensorThings API as array * @param {Array} obsArray Array of observations (timestamp + value) that is response from SensorThings API
* @returns {Array} Array of formatted observations suitable for use in a heatmap * @returns {Array} Array of formatted observations suitable for use in a heatmap
*/ */
const formatSTAResponseForHeatMap = function (obsArray) { const formatSTAResponseForHeatMap = function (obsArray) {
...@@ -270,6 +294,28 @@ const formatSTAResponseForHeatMap = function (obsArray) { ...@@ -270,6 +294,28 @@ const formatSTAResponseForHeatMap = function (obsArray) {
return dataSTAFormatted; return dataSTAFormatted;
}; };
/**
* Calculate the minimum and maximum values for a heatmap's color axis
* @param {Array} formattedObsArrHeatmap Response from SensorThings API formatted for use in a heatmap
* @returns {Object} An object containing the minimum and maximum values
*/
const calculateMinMaxValuesForHeatmapColorAxis = function (
formattedObsArrHeatmap
) {
// The observation value is the third element in array
const obsValueArr = formattedObsArrHeatmap.map((obs) => obs[2]);
// Extract integer part
const minValue = Math.trunc(Math.min(...obsValueArr));
const maxValue = Math.trunc(Math.max(...obsValueArr));
// Calculate the closest multiple of 5
const minObsValue = minValue - (minValue % 5);
const maxObsValue = maxValue + (5 - (maxValue % 5));
return { minObsValue, maxObsValue };
};
/** /**
* Draw a heatmap using Highcharts library * Draw a heatmap using Highcharts library
* @param {Array} formattedObsArrayForHeatmap Response from SensorThings API formatted for use in a heatmap * @param {Array} formattedObsArrayForHeatmap Response from SensorThings API formatted for use in a heatmap
...@@ -287,24 +333,10 @@ const drawHeatMapHC = function ( ...@@ -287,24 +333,10 @@ const drawHeatMapHC = function (
unitOfMeasurementSymbol: PHENOMENON_SYMBOL, unitOfMeasurementSymbol: PHENOMENON_SYMBOL,
} = formattedDatastreamMetadata; } = formattedDatastreamMetadata;
// Function returns the min and max observation values
const { const {
minObsValue: MINIMUM_VALUE_COLOR_AXIS, minObsValue: MINIMUM_VALUE_COLOR_AXIS,
maxObsValue: MAXIMUM_VALUE_COLOR_AXIS, maxObsValue: MAXIMUM_VALUE_COLOR_AXIS,
} = (() => { } = calculateMinMaxValuesForHeatmapColorAxis(formattedObsArrayForHeatmap);
// The observation value is the third element in array
const obsValueArr = formattedObsArrayForHeatmap.map((obs) => obs[2]);
// Extract integer part
const minValue = Math.trunc(Math.min(...obsValueArr));
const maxValue = Math.trunc(Math.max(...obsValueArr));
// Calculate the closest multiple of 5
const minObsValue = minValue - (minValue % 5);
const maxObsValue = maxValue + (5 - (maxValue % 5));
return { minObsValue, maxObsValue };
})();
Highcharts.chart("chart-heatmap", { Highcharts.chart("chart-heatmap", {
chart: { chart: {
...@@ -399,11 +431,11 @@ const drawHeatMapHC = function ( ...@@ -399,11 +431,11 @@ const drawHeatMapHC = function (
}; };
/** /**
* Convert the observations' phenomenonTime from an ISO 8601 string to Unix epoch * Format the response from SensorThings API to make it suitable for line chart
* @param {Array} obsArray Response from SensorThings API as array * @param {Array} obsArray Response from SensorThings API as array
* @returns {Array} Array of formatted observations suitable for use in a line chart or scatter plot * @returns {Array} Array of formatted observations suitable for use in a line chart
*/ */
const formatSTAResponseForLineChartOrScatterPlot = function (obsArray) { const formatSTAResponseForLineChart = function (obsArray) {
if (!obsArray) return; if (!obsArray) return;
const dataSTAFormatted = obsArray.map((result) => { const dataSTAFormatted = obsArray.map((result) => {
...@@ -478,7 +510,7 @@ const getSymmetricDifferenceBetweenArrays = function ( ...@@ -478,7 +510,7 @@ const getSymmetricDifferenceBetweenArrays = function (
.filter((timestampOne) => !obsTimestampArrayTwo.includes(timestampOne)) .filter((timestampOne) => !obsTimestampArrayTwo.includes(timestampOne))
.concat( .concat(
obsTimestampArrayTwo.filter( obsTimestampArrayTwo.filter(
(timestampTwo) => !obsTimestampArrayTwo.includes(timestampTwo) (timestampTwo) => !obsTimestampArrayOne.includes(timestampTwo)
) )
); );
...@@ -486,16 +518,16 @@ const getSymmetricDifferenceBetweenArrays = function ( ...@@ -486,16 +518,16 @@ const getSymmetricDifferenceBetweenArrays = function (
}; };
/** /**
* 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 * 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} missingTimestampsArr An array of strings representing the missing timestamps * @param {Array} uniqueTimestampsArr An array of timestamps unique to the larger set of observations
* @param {Array} largerObsTimestampArr An array of timestamps for the larger array 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 * @returns {Array} An array of the indexes of the missing observations
*/ */
const getIndexOfMissingObservation = function ( const getIndexesOfUniqueObservations = function (
missingTimestampsArr, uniqueTimestampsArr,
largerObsTimestampArr largerObsTimestampArr
) { ) {
const indexesMissingObs = missingTimestampsArr.map((index) => const indexesMissingObs = uniqueTimestampsArr.map((index) =>
largerObsTimestampArr.indexOf(index) largerObsTimestampArr.indexOf(index)
); );
...@@ -503,16 +535,19 @@ const getIndexOfMissingObservation = function ( ...@@ -503,16 +535,19 @@ const getIndexOfMissingObservation = function (
}; };
/** /**
* 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 * 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} missingIndexesArr An array of the indexes of the observations missing from the smaller set of observations * @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) which is modified in place * @param {Array} largerObsArr The larger array of observations (timestamp + value) which is modified in place
* @returns {undefined} * @returns {undefined}
*/ */
const removeMissingObservationFromLargerArray = function ( const removeUniqueObservationsFromLargerArray = function (
missingIndexesArr, uniqueIndexesArr,
largerObsArr largerObsArr
) { ) {
missingIndexesArr.forEach((index) => { // Reverse the indexes array so that the larger index is removed first
uniqueIndexesArr.reverse();
uniqueIndexesArr.forEach((index) => {
if (index > -1) { if (index > -1) {
largerObsArr.splice(index, 1); largerObsArr.splice(index, 1);
} }
...@@ -534,33 +569,29 @@ const getLargerArrayBetweenTwoInputArrays = function (firstArr, secondArr) { ...@@ -534,33 +569,29 @@ const getLargerArrayBetweenTwoInputArrays = function (firstArr, secondArr) {
}; };
/** /**
* Extracts and combines observation values from two imput observation arrays of equal length * Compares the length of two input arrays to determine the smaller one
* @param {Array} obsArrayOne First set of N observations (timestamp + value) * @param {Array} firstArr First input array
* @param {Array} obsArrayTwo Second set of N observations (timestamp + value) * @param {Array} secondArr Second input array
* @returns {Array} A 2*N array of observation values from both input observation arrays * @returns {Array} The smaller array
*/ */
const createCombinedObservationValues = function (obsArrayOne, obsArrayTwo) { const getSmallerArrayBetweenTwoInputArrays = function (firstArr, secondArr) {
// Extract the values from the two observation arrays if (firstArr.length === secondArr.length) return;
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 if (firstArr.length < secondArr.length) return firstArr;
const obsValuesOnePlusTwo = obsValuesOne.map((obsValOne, i) => {
return [obsValOne, obsValuesTwo[i]];
});
return obsValuesOnePlusTwo; if (firstArr.length > secondArr.length) return secondArr;
}; };
/** /**
* Format the response from SensorThings API to make it suitable for scatter plot * Utility function for deleting the unique observations from a larger array
* @param {Array} obsArrayOne Response from SensorThings API as array * @param {Array} obsArrayOne Array of observations (timestamp + value) that is response from SensorThings API
* @param {Array} obsArrayTwo Response from SensorThings API as array * @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 * @returns {Array} Two arrays of observations (timestamp + value) with matching timestamps and equal lengths
*/ */
const formatSTAResponseForScatterPlot = function (obsArrayOne, obsArrayTwo) { const deleteUniqueObservationsFromLargerArray = function (
// When our observation arrays have DIFFERENT lengths obsArrayOne,
if (obsArrayOne.length !== obsArrayTwo.length) { obsArrayTwo
) {
// Create arrays with timestamps only // Create arrays with timestamps only
const obsArrayOneTimestamp = obsArrayOne.map( const obsArrayOneTimestamp = obsArrayOne.map(
(obsTimeValue) => obsTimeValue[0] (obsTimeValue) => obsTimeValue[0]
...@@ -581,7 +612,7 @@ const formatSTAResponseForScatterPlot = function (obsArrayOne, obsArrayTwo) { ...@@ -581,7 +612,7 @@ const formatSTAResponseForScatterPlot = function (obsArrayOne, obsArrayTwo) {
); );
// Indexes of the missing observations // Indexes of the missing observations
const indexesMissingObsArr = getIndexOfMissingObservation( const indexesMissingObsArr = getIndexesOfUniqueObservations(
missingTimestamp, missingTimestamp,
biggerObsTimestampArr biggerObsTimestampArr
); );
...@@ -592,11 +623,78 @@ const formatSTAResponseForScatterPlot = function (obsArrayOne, obsArrayTwo) { ...@@ -592,11 +623,78 @@ const formatSTAResponseForScatterPlot = function (obsArrayOne, obsArrayTwo) {
obsArrayTwo obsArrayTwo
); );
// Determine the smaller observation array
const smallerObsArr = getSmallerArrayBetweenTwoInputArrays(
obsArrayOne,
obsArrayTwo
);
// Remove the missing observation from the larger array of observations // Remove the missing observation from the larger array of observations
// Modifies the array in place // Modifies the array in place
removeMissingObservationFromLargerArray(indexesMissingObsArr, biggerObsArr); removeUniqueObservationsFromLargerArray(indexesMissingObsArr, biggerObsArr);
return createCombinedObservationValues(obsArrayOne, obsArrayTwo); return [biggerObsArr, 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 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 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 formatSTAResponseForScatterPlot = 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 // When our observation arrays already have SAME lengths
...@@ -892,6 +990,93 @@ const getMetadataPlusObservationsFromMultipleDatastreams = async function ( ...@@ -892,6 +990,93 @@ const getMetadataPlusObservationsFromMultipleDatastreams = async function (
} }
}; };
/**
* Calculates the temperature difference, dT, between Vorlauf temperature [VL] and Rücklauf temperature [RL] (i.e., dT = VL - RL)
* @async
* @param {String} buildingId The building ID as a string
* @param {String} samplingRate The sampling rate as a string
* @returns A promise that contains an array (that is made up of a temperature difference array and a metadata object) when fulfilled
*/
const calculateVorlaufMinusRuecklaufTemperature = async function (
buildingId,
samplingRate
) {
const bldgSensorSamplingRateArr = [
[buildingId, "vl", samplingRate],
[buildingId, "rl", samplingRate],
];
const BUILDING_ID = buildingId;
const SAMPLING_RATE = samplingRate;
const observationsPlusMetadata =
await getMetadataPlusObservationsFromMultipleDatastreams(
bldgSensorSamplingRateArr
);
// Extract Vorlauf temperature and Ruecklauf temperature
const [[vorlaufTemp, ruecklaufTemp], [metadataVorlauf, metadataRuecklauf]] =
observationsPlusMetadata;
const vorlaufTempValues = vorlaufTemp.map((obs) => obs[1]);
const ruecklaufTempValues = ruecklaufTemp.map((obs) => obs[1]);
// The arrays have equal length, we need only use one of them for looping
// Resulting array contains the following pairs (timestamp + dT)
const vorlaufMinusRuecklaufTemp = vorlaufTemp.map((obs, i) => [
obs[0],
vorlaufTempValues[i] - ruecklaufTempValues[i],
]);
// From Vorlauf metadata, extract `name` and `unitOfMeasurement`
const { name: datastreamNameVorlauf, unitOfMeasurement } = metadataVorlauf;
// From Ruecklauf metadata, extract `name`
const { name: datastreamNameRuecklauf } = metadataRuecklauf;
// Extract the phenomenon names from Datastream names
const phenomenonNameVorlauf = extractPhenomenonNameFromDatastreamName(
datastreamNameVorlauf
);
const phenomenonNameRuecklauf = extractPhenomenonNameFromDatastreamName(
datastreamNameRuecklauf
);
// Create our custom datastream description text
const descriptionCombined = `Computed dT: ${phenomenonNameVorlauf} minus ${phenomenonNameRuecklauf}`;
// The resulting datastream description string has two `temperature` substrings;
// replace the first occurence with an empty string
const description = descriptionCombined.replace("temperature", "");
// Create our custom datastream name text
const name = `BOSCH_${BUILDING_ID} / dT Temperature difference (VL-RL) DS:${SAMPLING_RATE}`;
return [
vorlaufMinusRuecklaufTemp,
// The datastream metadata object needs to have these property names
{
description,
name,
unitOfMeasurement,
},
];
};
/**
* Test plotting of temp difference (dT) using heatmap
*/
const drawHeatmapHCUsingTempDifference = async function () {
const [tempDifferenceObsArrBau225, tempDifferenceMetadataBau225] =
await calculateVorlaufMinusRuecklaufTemperature("225", "60min");
drawHeatMapHC(
formatSTAResponseForHeatMap(tempDifferenceObsArrBau225),
formatDatastreamMetadataForChart(tempDifferenceMetadataBau225)
);
};
/** /**
* Test drawing of scatter plot chart * Test drawing of scatter plot chart
*/ */
...@@ -922,7 +1107,8 @@ const drawScatterPlotHCTest2 = async function () { ...@@ -922,7 +1107,8 @@ const drawScatterPlotHCTest2 = async function () {
}; };
(async () => { (async () => {
await drawScatterPlotHCTest2(); // await drawScatterPlotHCTest2();
await drawHeatmapHCUsingTempDifference();
})(); })();
export { export {
...@@ -937,7 +1123,7 @@ export { ...@@ -937,7 +1123,7 @@ export {
formatDatastreamMetadataForChart, formatDatastreamMetadataForChart,
formatSTAResponseForHeatMap, formatSTAResponseForHeatMap,
drawHeatMapHC, drawHeatMapHC,
formatSTAResponseForLineChartOrScatterPlot, formatSTAResponseForLineChart,
drawLineChartHC, drawLineChartHC,
getCombinedObservationsFromAllNextLinks, getCombinedObservationsFromAllNextLinks,
getMetadataPlusObservationsFromSingleDatastream, getMetadataPlusObservationsFromSingleDatastream,
......
...@@ -11,7 +11,7 @@ import { ...@@ -11,7 +11,7 @@ import {
formatDatastreamMetadataForChart, formatDatastreamMetadataForChart,
formatSTAResponseForHeatMap, formatSTAResponseForHeatMap,
drawHeatMapHC, drawHeatMapHC,
formatSTAResponseForLineChartOrScatterPlot, formatSTAResponseForLineChart,
drawLineChartHC, drawLineChartHC,
getCombinedObservationsFromAllNextLinks, getCombinedObservationsFromAllNextLinks,
getMetadataPlusObservationsFromSingleDatastream, getMetadataPlusObservationsFromSingleDatastream,
...@@ -308,7 +308,7 @@ const selectChartTypeFromDropDown = async function () { ...@@ -308,7 +308,7 @@ const selectChartTypeFromDropDown = async function () {
if (selectedChartType === "Line") { if (selectedChartType === "Line") {
drawLineChartHC( drawLineChartHC(
formatSTAResponseForLineChartOrScatterPlot(combinedObs), formatSTAResponseForLineChart(combinedObs),
formatDatastreamMetadataForChart(datastreamMetadata) formatDatastreamMetadataForChart(datastreamMetadata)
); );
} else if (selectedChartType === "Heatmap") { } else if (selectedChartType === "Heatmap") {
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment