Commit 4d484f25 authored by Pithon Kabiro's avatar Pithon Kabiro
Browse files

Merge branch 'wip_chart-line-multiple-series' into 'master'

Add functionality to draw multi-series line chart

See merge request !5
parents 6e518409 cc82b8a6
......@@ -3,10 +3,7 @@
import {
BASE_URL,
QUERY_PARAMS_COMBINED,
getDatastreamIdFromBuildingNumber,
createObservationsUrl,
performGetRequestUsingAxios,
extractCombinedObservationsFromAllPages,
getMetadataPlusObservationsFromSingleOrMultipleDatastreams,
} from "./appChart.js";
/**
......@@ -135,28 +132,23 @@ const aggregateObservationsWithinTimeInterval = function (
* Test aggregation of observations from a single datastream
*/
const testAggregation = async function () {
// Datastream ID
const datastreamIdBau225VL = getDatastreamIdFromBuildingNumber(
"225",
"vl",
"60min"
);
const sensorOfInterestNestedArr = [["225", "vl", "60min"]];
// Observations URL
const observationsUrlBau225VL = createObservationsUrl(
const observationsPlusMetadata =
await getMetadataPlusObservationsFromSingleOrMultipleDatastreams(
BASE_URL,
datastreamIdBau225VL
QUERY_PARAMS_COMBINED,
sensorOfInterestNestedArr
);
// Observations array
const observationsBau225VL = await extractCombinedObservationsFromAllPages(
performGetRequestUsingAxios(observationsUrlBau225VL, QUERY_PARAMS_COMBINED)
);
// Extract the observations and metadata for each sensor
// Array elements in same order as input array
const [[obsSensorOneArr], [metadataSensorOne]] = observationsPlusMetadata;
// Aggregated observations
const observationsBau225VLAggregated =
aggregateObservationsWithinTimeInterval(
observationsBau225VL,
obsSensorOneArr,
"60 min",
"2020-02-01",
"2020-03-31"
......
......@@ -91,11 +91,9 @@ const getDatastreamIdFromBuildingNumber = function (
)
return;
const datastreamIdMatched = Number(
return Number(
buildingToDatastreamMapping[buildingNumber][phenomenon][samplingRate]
);
return datastreamIdMatched;
};
/**
......@@ -106,8 +104,7 @@ const getDatastreamIdFromBuildingNumber = function (
*/
const createDatastreamUrl = function (baseUrl, datastreamID) {
if (!datastreamID) return;
const fullDatastreamURL = `${baseUrl}/Datastreams(${datastreamID})`;
return fullDatastreamURL;
return `${baseUrl}/Datastreams(${datastreamID})`;
};
/**
......@@ -118,8 +115,7 @@ const createDatastreamUrl = function (baseUrl, datastreamID) {
*/
const createObservationsUrl = function (baseUrl, datastreamID) {
if (!datastreamID) return;
const fullObservationsURL = `${baseUrl}/Datastreams(${datastreamID})/Observations`;
return fullObservationsURL;
return `${baseUrl}/Datastreams(${datastreamID})/Observations`;
};
/**
......@@ -130,24 +126,34 @@ const createObservationsUrl = function (baseUrl, datastreamID) {
*/
const createTemporalFilterString = function (dateStart, dateStop) {
if (!dateStart || !dateStop) return;
const filterString = `resultTime ge ${dateStart}T00:00:00.000Z and resultTime le ${dateStop}T00:00:00.000Z`;
return filterString;
return `resultTime ge ${dateStart}T00:00:00.000Z and resultTime le ${dateStop}T00:00:00.000Z`;
};
const QUERY_PARAM_RESULT_FORMAT = "dataArray";
const QUERY_PARAM_ORDER_BY = "phenomenonTime asc";
const QUERY_PARAM_FILTER = createTemporalFilterString(
"2020-01-01",
"2021-01-01"
);
const QUERY_PARAM_SELECT = "result,phenomenonTime";
const QUERY_PARAMS_COMBINED = {
/**
* Create a query parameter object that should be sent together with a HTTP GET request using the Axios library
* @param {String} dateStart Start date (for temporal filter) in YYYY-MM-DD format
* @param {String} dateStop Stop date (for temporal filter) in YYYY-MM-DD format
* @returns {Object} A query parameter object
*/
const createUrlParametersForGetRequest = function (dateStart, dateStop) {
const QUERY_PARAM_RESULT_FORMAT = "dataArray";
const QUERY_PARAM_ORDER_BY = "phenomenonTime asc";
const QUERY_PARAM_FILTER = createTemporalFilterString(dateStart, dateStop);
const QUERY_PARAM_SELECT = "result,phenomenonTime";
return {
"$resultFormat": QUERY_PARAM_RESULT_FORMAT,
"$orderBy": QUERY_PARAM_ORDER_BY,
"$filter": QUERY_PARAM_FILTER,
"$select": QUERY_PARAM_SELECT,
};
};
const QUERY_PARAMS_COMBINED = createUrlParametersForGetRequest(
"2020-01-01",
"2021-01-01"
);
/**
* Perform a GET request using the Axios library
* @param {String} urlObservations A URL that fetches Observations from an STA instance
......@@ -446,22 +452,113 @@ const formatSensorThingsApiResponseForLineChart = function (obsArray) {
return dataSTAFormatted;
};
/**
* Extract the properties that make up the formatted datastream metadata object(s)
* @param {Array} formattedDatastreamsMetadataArr An array of formatted metadata object(s) from one or more datastreams
* @returns {Object} An object that contains array(s) of formatted datastream metadata properties
*/
const extractPropertiesFromDatastreamMetadata = function (
formattedDatastreamsMetadataArr
) {
// Create arrays from the properties of the formatted datastream metadata
const datastreamDescriptionsArr = formattedDatastreamsMetadataArr.map(
(datastreamMetadata) => datastreamMetadata.datastreamDescription
);
const datastreamNamesArr = formattedDatastreamsMetadataArr.map(
(datastreamMetadata) => datastreamMetadata.datastreamName
);
const phenomenonNamesArr = formattedDatastreamsMetadataArr.map(
(datastreamMetadata) => datastreamMetadata.phenomenonName
);
const unitOfMeasurementSymbolsArr = formattedDatastreamsMetadataArr.map(
(datastreamMetadata) => datastreamMetadata.unitOfMeasurementSymbol
);
return {
datastreamDescriptionsArr,
datastreamNamesArr,
phenomenonNamesArr,
unitOfMeasurementSymbolsArr,
};
};
/**
* Extracts the sampling rate substring from a datastream name string
* @param {Array} datastreamNamesArr An array of datastream name(s)
* @returns {Array} An array containing the sampling rate substring(s)
*/
const extractSamplingRateFromDatastreamName = function (datastreamNamesArr) {
// The sampling rate string is the last word in the Datastream name string
return datastreamNamesArr.map((datastreamName) =>
datastreamName.split(" ").pop()
);
};
/**
* Concatenates metadata properties to create a string for either the title or subtitle of a line chart
* @param {Array} datastreamMetadataPropArr An array of metadata property strings
* @returns {String} A string of comma separated metadata property strings
*/
const createCombinedTextForLineChartTitles = function (
datastreamMetadataPropArr
) {
return datastreamMetadataPropArr.join(", ");
};
/**
* Creates an options object for each series drawn in the line chart
* @param {Array} formattedObsArraysForLineChart An array of formatted observation array(s) from one or more datastreams
* @param {Array} phenomenonNamesArr An array of phenomenon name(s)
* @param {Array} phenomenonSymbolsArr An array of phenomenon symbol(s)
* @returns {Array} An array made up of series options object(s)
*/
const createSeriesOptionsForLineChart = function (
formattedObsArraysForLineChart,
phenomenonNamesArr,
phenomenonSymbolsArr
) {
// An array of colors provided by the Highcharts object
const seriesColors = Highcharts.getOptions().colors;
// Create an array of seriesOptions objects
// Assumes that the observation array of arrays, phenomenon names array and phenomenon symbols array are of equal length
// Use one of the arrays for looping
return formattedObsArraysForLineChart.map((formattedObsArray, i) => {
return {
name: `${phenomenonNamesArr[i]} (${phenomenonSymbolsArr[i]})`,
data: formattedObsArray,
color: seriesColors[i],
turboThreshold: Number.MAX_VALUE, // #3404, remove after 4.0.5 release
};
});
};
/**
* Draw a line chart using Highcharts library
* @param {Array} formattedObsArrayForLineChart Response from SensorThings API formatted for use in a line chart
* @param {Object} formattedDatastreamMetadata Object containing Datastream metadata
* @param {Array} formattedObsArraysForLineChart An array made up of formatted observation array(s) suitable for use in a line chart
* @param {Object} formattedDatastreamMetadataArr An array made up of object(s) containing Datastream metadata
* @returns {undefined} undefined
*/
const drawLineChartHighcharts = function (
formattedObsArrayForLineChart,
formattedDatastreamMetadata
formattedObsArraysForLineChart,
formattedDatastreamMetadataArr
) {
// Arrays of datastream properties
const {
datastreamDescription: DATASTREAM_DESCRIPTION,
datastreamName: DATASTREAM_NAME,
phenomenonName: PHENOMENON_NAME,
unitOfMeasurementSymbol: PHENOMENON_SYMBOL,
} = formattedDatastreamMetadata;
datastreamNamesArr,
phenomenonNamesArr,
unitOfMeasurementSymbolsArr,
} = extractPropertiesFromDatastreamMetadata(formattedDatastreamMetadataArr);
// Create the array of series options object(s)
const seriesOptionsArr = createSeriesOptionsForLineChart(
formattedObsArraysForLineChart,
phenomenonNamesArr,
unitOfMeasurementSymbolsArr
);
Highcharts.stockChart("chart-line", {
chart: {
......@@ -473,25 +570,24 @@ const drawLineChartHighcharts = function (
},
title: {
text: DATASTREAM_DESCRIPTION,
text: createCombinedTextForLineChartTitles(phenomenonNamesArr),
"align": "left",
},
subtitle: {
text: DATASTREAM_NAME,
text: `Sampling rate(s): ${createCombinedTextForLineChartTitles(
extractSamplingRateFromDatastreamName(datastreamNamesArr)
)}`,
align: "left",
},
series: [
{
name: `${PHENOMENON_NAME} (${PHENOMENON_SYMBOL})`,
data: formattedObsArrayForLineChart,
tooltip: {
pointFormat:
'<span style="color:{series.color}">{series.name}</span>: <b>{point.y}</b> <br/>',
valueDecimals: 2,
},
turboThreshold: Number.MAX_VALUE, // #3404, remove after 4.0.5 release
},
],
series: seriesOptionsArr,
});
};
......@@ -670,7 +766,7 @@ const checkForAndDeleteUniqueObservationsFromLargerArray = function (
* 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 2*N array of observation values from both input observation arrays
* @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
......@@ -712,9 +808,9 @@ const formatSensorThingsApiResponseForScatterPlot = function (
/**
* 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
* @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 (
......@@ -849,10 +945,15 @@ const combineResultsFromAllPages = async function (httpGetRequestPromise) {
if (!httpGetRequestPromise) return;
const lastSuccess = await httpGetRequestPromise;
// The "success" objects contain a "data" object which in turn has a "value" property
// If the "data" object in turn has a "@iot.nextLink" property, then a next page exists
if (lastSuccess.data["@iot.nextLink"]) {
const nextLinkSuccess = await combineResultsFromAllPages(
axios.get(lastSuccess.data["@iot.nextLink"])
);
// The "data" object in turn has a "value" property
// The "value" property's value is an array
nextLinkSuccess.data.value = lastSuccess.data.value.concat(
nextLinkSuccess.data.value
);
......@@ -902,32 +1003,6 @@ const extractCombinedObservationsFromAllPages = async function (
}
};
/**
* Retrieve the metadata for a Datastream as well as the Observations corresponding to it
* @async
* @param {Promise} metadataPlusObsPromiseArray An array that contains two promises, one for datastream metadata, the other for observations
* @returns {Promise} A promise that contains two arrays when fulfilled, one for datastream metadata and the other for observations
*/
const getMetadataPlusObservationsFromSingleDatastream = async function (
metadataPlusObsPromiseArray
) {
try {
// Array to store our final result
const combinedResolvedPromises = [];
// Use for/of loop - we need to maintain the order of execution of the async operations
for (const promise of metadataPlusObsPromiseArray) {
// Resolved value of a single promise
const resolvedPromise = await promise;
combinedResolvedPromises.push(resolvedPromise);
}
return combinedResolvedPromises;
} catch (err) {
console.error(err);
}
};
/**
* Retrieve all the Observations from an array of Observations promises
* @async
......@@ -955,14 +1030,14 @@ const getObservationsFromMultipleDatastreams = async function (
};
/**
* Retrieve the metadata from multiple Datastreams and the Observations corresponding to these Datastreams
* @async
* @param {Array} bldgSensorSamplingRateArr A 3*N array containing buildings, sensors & sampling rates as strings, e.g. ["101", "rl", "60min"]
* @returns {Promise} A promise that contains a N*2 array (the first element is an array of Observations and the second element is an array of Datastream metadata objects) when fulfilled
* Retrieve the metadata from a single Datastream or multiple Datastreams and the Observations corresponding to the Datastream(s)
* @param {String} baseUrl Base URL of the STA server
* @param {Object} urlParamObj The URL parameters to be sent together with the GET request
* @param {Array} bldgSensorSamplingRateArr A N*1 array (where N >= 1) containing a nested array of buildings, sensors & sampling rates as strings, i.e. [["101", "rl", "15min"]] or [["101", "rl", "15min"], ["102", "vl", "60min"]] or [["101", "rl", "15min"], ["102", "vl", "60min"], ["225", "vl", "60min"]], etc
* @returns {Promise} A promise that contains a 1*2 array (the first element is an array that contans N Observations arrays; and the second element is an array of N Datastream metadata objects) when fulfilled
*/
const getMetadataPlusObservationsFromMultipleDatastreams = async function (
bldgSensorSamplingRateArr
) {
const getMetadataPlusObservationsFromSingleOrMultipleDatastreams =
async function (baseUrl, urlParamObj, bldgSensorSamplingRateArr) {
try {
if (!bldgSensorSamplingRateArr) return;
......@@ -974,18 +1049,18 @@ const getMetadataPlusObservationsFromMultipleDatastreams = async function (
// Observations URLs
const observationsUrlArr = datastreamsIdsArr.map((datastreamId) =>
createObservationsUrl(BASE_URL, datastreamId)
createObservationsUrl(baseUrl, datastreamId)
);
// Datastreams URLs
const datastreamsUrlArr = datastreamsIdsArr.map((datastreamId) =>
createDatastreamUrl(BASE_URL, datastreamId)
createDatastreamUrl(baseUrl, datastreamId)
);
// Promise objects - Observations
const observationsPromisesArr = observationsUrlArr.map((obsUrl) =>
extractCombinedObservationsFromAllPages(
performGetRequestUsingAxios(obsUrl, QUERY_PARAMS_COMBINED)
performGetRequestUsingAxios(obsUrl, urlParamObj)
)
);
......@@ -1003,7 +1078,7 @@ const getMetadataPlusObservationsFromMultipleDatastreams = async function (
} catch (err) {
console.error(err);
}
};
};
/**
* Calculates the temperature difference, dT, between Vorlauf temperature [VL] and Rücklauf temperature [RL] (i.e., dT = VL - RL)
......@@ -1026,7 +1101,9 @@ const calculateVorlaufMinusRuecklaufTemperature = async function (
const SAMPLING_RATE = samplingRate;
const observationsPlusMetadata =
await getMetadataPlusObservationsFromMultipleDatastreams(
await getMetadataPlusObservationsFromSingleOrMultipleDatastreams(
BASE_URL,
QUERY_PARAMS_COMBINED,
bldgSensorSamplingRateArr
);
......@@ -1116,46 +1193,75 @@ const drawScatterPlotHCTest2 = async function () {
];
const observationsPlusMetadata =
await getMetadataPlusObservationsFromMultipleDatastreams(
await getMetadataPlusObservationsFromSingleOrMultipleDatastreams(
BASE_URL,
QUERY_PARAMS_COMBINED,
sensorsOfInterestArr
);
// Extract the observations and metadata for each sensor
// Array elements in same order as input array
const [
[obsSensorOneArr, obsSensorTwoArr],
[metadataSensorOne, metadataSensorTwo],
] = observationsPlusMetadata;
// Extract the combined arrays for observations and metadata
const [observationsArr, metadataArr] = observationsPlusMetadata;
// Create formatted array(s) for observations
// This function expects two arguments, these are unpacked using the spread operator
const formattedObsScatterPlotArr =
formatSensorThingsApiResponseForScatterPlot(...observationsArr);
// Create formatted array(s) for metadata
const formattedMetadataArr = metadataArr.map((metadata) =>
formatDatastreamMetadataForChart(metadata)
);
// This function expects three arguments, the second and third are unpacked using the spread operator
drawScatterPlotHighcharts(
formatSensorThingsApiResponseForScatterPlot(
obsSensorOneArr,
obsSensorTwoArr
),
formatDatastreamMetadataForChart(metadataSensorOne),
formatDatastreamMetadataForChart(metadataSensorTwo)
formattedObsScatterPlotArr,
...formattedMetadataArr
);
};
/**
* Test drawing of line chart with multiple series
*/
const testLineChartMultipleSeries = async function () {
const sensorsOfInterestArr = [
["225", "vl", "60min"],
["125", "rl", "60min"],
["weather_station_521", "outside_temp", "60min"],
];
const observationsPlusMetadata =
await getMetadataPlusObservationsFromSingleOrMultipleDatastreams(
BASE_URL,
QUERY_PARAMS_COMBINED,
sensorsOfInterestArr
);
// Extract the observations and metadata arrays
const [observationsArr, metadataArr] = observationsPlusMetadata;
// Format the observations and metadata
const formattedObservationsArr = observationsArr.map((observations) =>
formatSensorThingsApiResponseForLineChart(observations)
);
const formattedMetadataArr = metadataArr.map((metadata) =>
formatDatastreamMetadataForChart(metadata)
);
drawLineChartHighcharts(formattedObservationsArr, formattedMetadataArr);
};
(async () => {
// await drawScatterPlotHCTest2();
await drawHeatmapHCUsingTempDifference();
})();
// drawScatterPlotHCTest2();
// drawHeatmapHCUsingTempDifference();
// testLineChartMultipleSeries()
export {
BASE_URL,
QUERY_PARAMS_COMBINED,
getDatastreamIdFromBuildingNumber,
createDatastreamUrl,
createObservationsUrl,
createTemporalFilterString,
performGetRequestUsingAxios,
getMetadataFromSingleDatastream,
formatDatastreamMetadataForChart,
formatSensorThingsApiResponseForHeatMap,
drawHeatMapHighcharts,
formatSensorThingsApiResponseForLineChart,
drawLineChartHighcharts,
extractCombinedObservationsFromAllPages,
getMetadataPlusObservationsFromSingleDatastream,
getMetadataPlusObservationsFromSingleOrMultipleDatastreams,
};
......@@ -3,18 +3,12 @@
import {
BASE_URL,
QUERY_PARAMS_COMBINED,
getDatastreamIdFromBuildingNumber,
createDatastreamUrl,
createObservationsUrl,
performGetRequestUsingAxios,
getMetadataFromSingleDatastream,
formatDatastreamMetadataForChart,
formatSensorThingsApiResponseForHeatMap,
drawHeatMapHighcharts,
formatSensorThingsApiResponseForLineChart,
drawLineChartHighcharts,
extractCombinedObservationsFromAllPages,
getMetadataPlusObservationsFromSingleDatastream,
getMetadataPlusObservationsFromSingleOrMultipleDatastreams,
} from "./appChart.js";
const buildingsAvailableSensorsArr = [
......@@ -268,13 +262,8 @@ const selectChartTypeFromDropDown = async function () {
if (selectedOptions === undefined) return;
const abbreviationsArr = getBuildingSensorSamplingRateAbbreviation(
...selectedOptions
);
const selectedDatastream = getDatastreamIdFromBuildingNumber(
...abbreviationsArr
);
const selectedOptionsAbbreviationsArr =
getBuildingSensorSamplingRateAbbreviation(...selectedOptions);
const selectedChartType = document.querySelector(
"#drop-down--chart-type"
......@@ -285,40 +274,39 @@ const selectChartTypeFromDropDown = async function () {
// Display the loading indicator
showLoadingSpinner();
const URL_DATASTREAM = createDatastreamUrl(BASE_URL, selectedDatastream);
const URL_OBSERVATIONS = createObservationsUrl(
// The `getMetadataPlusObservationsFromSingleOrMultipleDatastreams` function expects a nested array structure
const abbreviationsNestedArr = [selectedOptionsAbbreviationsArr];
const observationsPlusMetadata =
await getMetadataPlusObservationsFromSingleOrMultipleDatastreams(
BASE_URL,
selectedDatastream
QUERY_PARAMS_COMBINED,
abbreviationsNestedArr
);
// Create promises
const promiseDatastreamMetadata =
getMetadataFromSingleDatastream(URL_DATASTREAM);
const promiseCombinedObservations = extractCombinedObservationsFromAllPages(
performGetRequestUsingAxios(URL_OBSERVATIONS, QUERY_PARAMS_COMBINED)
// Extract the combined arrays for observations and metadata
const [observationsArr, metadataArr] = observationsPlusMetadata;
// Create formatted array(s) for observations - line chart
const formattedObsLineChartArr = observationsArr.map((observations) =>
formatSensorThingsApiResponseForLineChart(observations)
);
// Pass promises to our async function
const metadataPlusObservations =
await getMetadataPlusObservationsFromSingleDatastream([
promiseCombinedObservations,
promiseDatastreamMetadata,
]);
// Create formatted array(s) for observations - heatmap
const formattedObsHeatMapArr = observationsArr.map((observations) =>
formatSensorThingsApiResponseForHeatMap(observations)
);
// Extract the metadata and the observations from resulting array
const combinedObs = metadataPlusObservations[0];
const datastreamMetadata = metadataPlusObservations[1];
// Create formatted array(s) for metadata - same for both chart types
const formattedMetadataArr = metadataArr.map((metadata) =>
formatDatastreamMetadataForChart(metadata)
);
if (selectedChartType === "Line") {
drawLineChartHighcharts(
formatSensorThingsApiResponseForLineChart(combinedObs),
formatDatastreamMetadataForChart(datastreamMetadata)
);
drawLineChartHighcharts(formattedObsLineChartArr, formattedMetadataArr);
} else if (selectedChartType === "Heatmap") {
drawHeatMapHighcharts(
formatSensorThingsApiResponseForHeatMap(combinedObs),
formatDatastreamMetadataForChart(datastreamMetadata)
);
// First need to extract the nested arrays for the formatted observations and metadata
drawHeatMapHighcharts(...formattedObsHeatMapArr, ...formattedMetadataArr);
}
} catch (err) {
console.error(err);
......
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