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) {
}
};
/**
* 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
* @param {Object} datastreamMetadata An object containing a Datastream's metadata
......@@ -219,20 +250,13 @@ const formatDatastreamMetadataForChart = function (datastreamMetadata) {
} = datastreamMetadata;
// Extract phenomenon name from Datastream name
const regex = /\/ (.*) DS/;
const phenomenonName = datastreamName.match(regex)[1]; // use second element in array
// Match the unitOfMeasurement's string representation of a symbol
// to an actual symbol, where necessary
const unitOfMeasurementSymbol = (() => {
if (unitOfMeasurement.symbol === "degC") {
return "";
} else if (unitOfMeasurement.symbol === "m3/h") {
return "m<sup>3</sup>/h";
} else {
return unitOfMeasurement.symbol;
}
})();
const phenomenonName =
extractPhenomenonNameFromDatastreamName(datastreamName);
// Get the unitOfMeasurement's symbol
const unitOfMeasurementSymbol = matchUnitOfMeasurementSymbolStringToSymbol(
unitOfMeasurement.symbol
);
return {
datastreamDescription,
......@@ -244,7 +268,7 @@ const formatDatastreamMetadataForChart = function (datastreamMetadata) {
/**
* 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
*/
const formatSTAResponseForHeatMap = function (obsArray) {
......@@ -270,6 +294,28 @@ const formatSTAResponseForHeatMap = function (obsArray) {
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
* @param {Array} formattedObsArrayForHeatmap Response from SensorThings API formatted for use in a heatmap
......@@ -287,24 +333,10 @@ const drawHeatMapHC = function (
unitOfMeasurementSymbol: PHENOMENON_SYMBOL,
} = formattedDatastreamMetadata;
// Function returns the min and max observation values
const {
minObsValue: MINIMUM_VALUE_COLOR_AXIS,
maxObsValue: MAXIMUM_VALUE_COLOR_AXIS,
} = (() => {
// 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 };
})();
} = calculateMinMaxValuesForHeatmapColorAxis(formattedObsArrayForHeatmap);
Highcharts.chart("chart-heatmap", {
chart: {
......@@ -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
* @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;
const dataSTAFormatted = obsArray.map((result) => {
......@@ -478,7 +510,7 @@ const getSymmetricDifferenceBetweenArrays = function (
.filter((timestampOne) => !obsTimestampArrayTwo.includes(timestampOne))
.concat(
obsTimestampArrayTwo.filter(
(timestampTwo) => !obsTimestampArrayTwo.includes(timestampTwo)
(timestampTwo) => !obsTimestampArrayOne.includes(timestampTwo)
)
);
......@@ -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
* @param {Array} missingTimestampsArr An array of strings representing the missing timestamps
* @param {Array} largerObsTimestampArr An array of timestamps for the larger array of observations
* 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 getIndexOfMissingObservation = function (
missingTimestampsArr,
const getIndexesOfUniqueObservations = function (
uniqueTimestampsArr,
largerObsTimestampArr
) {
const indexesMissingObs = missingTimestampsArr.map((index) =>
const indexesMissingObs = uniqueTimestampsArr.map((index) =>
largerObsTimestampArr.indexOf(index)
);
......@@ -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
* @param {Array} missingIndexesArr An array of the indexes of the observations missing from the smaller set of observations
* 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) which is modified in place
* @returns {undefined}
*/
const removeMissingObservationFromLargerArray = function (
missingIndexesArr,
const removeUniqueObservationsFromLargerArray = function (
uniqueIndexesArr,
largerObsArr
) {
missingIndexesArr.forEach((index) => {
// Reverse the indexes array so that the larger index is removed first
uniqueIndexesArr.reverse();
uniqueIndexesArr.forEach((index) => {
if (index > -1) {
largerObsArr.splice(index, 1);
}
......@@ -533,6 +568,98 @@ const getLargerArrayBetweenTwoInputArrays = function (firstArr, secondArr) {
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
// Modifies the array in place
removeUniqueObservationsFromLargerArray(indexesMissingObsArr, biggerObsArr);
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)
......@@ -554,49 +681,20 @@ const createCombinedObservationValues = function (obsArrayOne, obsArrayTwo) {
/**
* 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
* @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) {
// 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);
const [obsArrayOneFinal, obsArrayTwoFinal] =
checkForAndDeleteUniqueObservationsFromLargerArray(
obsArrayOne,
obsArrayTwo
);
return createCombinedObservationValues(obsArrayOne, obsArrayTwo);
return createCombinedObservationValues(obsArrayOneFinal, obsArrayTwoFinal);
}
// When our observation arrays already have SAME lengths
......@@ -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
*/
......@@ -922,7 +1107,8 @@ const drawScatterPlotHCTest2 = async function () {
};
(async () => {
await drawScatterPlotHCTest2();
// await drawScatterPlotHCTest2();
await drawHeatmapHCUsingTempDifference();
})();
export {
......@@ -937,7 +1123,7 @@ export {
formatDatastreamMetadataForChart,
formatSTAResponseForHeatMap,
drawHeatMapHC,
formatSTAResponseForLineChartOrScatterPlot,
formatSTAResponseForLineChart,
drawLineChartHC,
getCombinedObservationsFromAllNextLinks,
getMetadataPlusObservationsFromSingleDatastream,
......
......@@ -11,7 +11,7 @@ import {
formatDatastreamMetadataForChart,
formatSTAResponseForHeatMap,
drawHeatMapHC,
formatSTAResponseForLineChartOrScatterPlot,
formatSTAResponseForLineChart,
drawLineChartHC,
getCombinedObservationsFromAllNextLinks,
getMetadataPlusObservationsFromSingleDatastream,
......@@ -308,7 +308,7 @@ const selectChartTypeFromDropDown = async function () {
if (selectedChartType === "Line") {
drawLineChartHC(
formatSTAResponseForLineChartOrScatterPlot(combinedObs),
formatSTAResponseForLineChart(combinedObs),
formatDatastreamMetadataForChart(datastreamMetadata)
);
} 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