Commit f54e5fc4 authored by Pithon Kabiro's avatar Pithon Kabiro
Browse files

Merge branch 'wip_raw-vs-aggregated-data' into 'master'

Handling of raw vs aggregated data

Introduce logic that determines how to deal with raw vs aggregated 
data when drawing line charts and column charts

See merge request !11
parents dc2e7ae0 f000787d
......@@ -6,7 +6,7 @@ import {
} from "./src_modules/baseUrlPlusQueryParams.mjs";
import {
formatSensorThingsApiResponseForLineChart,
formatSensorThingsApiResponseForLineOrColumnChart,
drawLineChartHighcharts,
} from "./src_modules/chartLine.mjs";
......@@ -30,7 +30,7 @@ import { getMetadataPlusObservationsFromSingleOrMultipleDatastreams } from "./sr
import {
formatDatastreamMetadataForChart,
extractPropertiesFromFormattedDatastreamMetadata,
} from "./src_modules/fetchedDataProcess.mjs";
} from "./src_modules/fetchedDataProcessing.mjs";
import { calculateVorlaufMinusRuecklaufTemperature } from "./src_modules/calculateTemperatureDiff.mjs";
......@@ -74,7 +74,7 @@ const drawHeatmapHCUsingTempDifference = async function () {
// Format the metadata
const formattedTempDiff225MetadataNestedArr =
metadataTemperatureDiff225NestedArr.map((metadataObj) =>
formatDatastreamMetadataForChart(metadataObj, false)
formatDatastreamMetadataForChart(metadataObj)
);
// Extract the formatted metadata properties
......@@ -173,12 +173,12 @@ const testLineChartMultipleSeries = async function () {
// Format the observations
const formattedObservationsNestedArr = observationsNestedArr.map(
(observationsArr) =>
formatSensorThingsApiResponseForLineChart(observationsArr)
formatSensorThingsApiResponseForLineOrColumnChart(observationsArr)
);
// Format the metadata
const formattedMetadataNestedArr = metadataNestedArr.map((metadataArr) =>
formatDatastreamMetadataForChart(metadataArr, false)
formatDatastreamMetadataForChart(metadataArr)
);
// Extract the formatted metadata properties
......@@ -264,7 +264,7 @@ const drawColumnChartMonthlySumTest = async function () {
// Format the metadata
const formattedMetadataNestedArr = metadataNestedArr.map((metadataObj) =>
formatDatastreamMetadataForChart(metadataObj, true)
formatDatastreamMetadataForChart(metadataObj)
);
// Extract the formatted metadata properties
......@@ -346,7 +346,7 @@ const drawColumnChartDailySumTest = async function () {
// Format the metadata
const formattedMetadataNestedArr = metadataNestedArr.map((metadataObj) =>
formatDatastreamMetadataForChart(metadataObj, true)
formatDatastreamMetadataForChart(metadataObj)
);
// Extract the formatted metadata properties
......@@ -367,6 +367,54 @@ const drawColumnChartDailySumTest = async function () {
}
};
/**
* Test drawing of column chart using raw observations
*/
const drawColumnChartNonAggregationTest = async function () {
try {
const sensorsOfInterestNestedArr = [
["125", "vl", "60min"],
["225", "vl", "60min"],
];
const observationsPlusMetadata =
await getMetadataPlusObservationsFromSingleOrMultipleDatastreams(
BASE_URL,
QUERY_PARAMS_COMBINED,
sensorsOfInterestNestedArr
);
// Extract the observations and metadata for each sensor
// Array elements in same order as input array
const [observationsNestedArr, metadataNestedArr] = observationsPlusMetadata;
// Format the observations
const formattedObservationsNestedArr = observationsNestedArr.map(
(observationsArr) =>
formatSensorThingsApiResponseForLineOrColumnChart(observationsArr)
);
// Format the metadata
const formattedMetadataNestedArr = metadataNestedArr.map((metadataArr) =>
formatDatastreamMetadataForChart(metadataArr)
);
// Extract the formatted metadata properties
const extractedFormattedDatastreamProperties =
extractPropertiesFromFormattedDatastreamMetadata(
formattedMetadataNestedArr,
false
);
drawColumnChartHighcharts(
formattedObservationsNestedArr,
extractedFormattedDatastreamProperties
);
} catch (err) {
console.error(err);
}
};
/**
* Test drawing of line chart using aggregation / average result - monthly
*/
......@@ -433,17 +481,17 @@ const drawLineChartMonthlyAverageTest = async function () {
);
// Format the metadata
// NOTE: we use the `false` flag here because line charts are meant to work with non-aggregated data
const formattedMetadataNestedArr = metadataNestedArr.map((metadataObj) =>
formatDatastreamMetadataForChart(metadataObj, false)
formatDatastreamMetadataForChart(metadataObj)
);
// Extract the formatted metadata properties
// NOTE: we use the `false` flag here because line charts are meant to work with non-aggregated data
const extractedFormattedDatastreamProperties =
extractPropertiesFromFormattedDatastreamMetadata(
formattedMetadataNestedArr,
false
true,
"monthly",
"average"
);
drawLineChartHighcharts(
......@@ -515,17 +563,17 @@ const drawLineChartDailyAverageTest = async function () {
);
// Format the metadata
// NOTE: we use the `false` flag here because line charts are meant to work with non-aggregated data
const formattedMetadataNestedArr = metadataNestedArr.map((metadataObj) =>
formatDatastreamMetadataForChart(metadataObj, false)
formatDatastreamMetadataForChart(metadataObj)
);
// Extract the formatted metadata properties
// NOTE: we use the `false` flag here because line charts are meant to work with non-aggregated data
const extractedFormattedDatastreamProperties =
extractPropertiesFromFormattedDatastreamMetadata(
formattedMetadataNestedArr,
false
true,
"daily",
"average"
);
drawLineChartHighcharts(
......@@ -542,5 +590,6 @@ const drawLineChartDailyAverageTest = async function () {
// testLineChartMultipleSeries();
// drawColumnChartMonthlySumTest();
// drawColumnChartDailySumTest();
// drawColumnChartNonAggregationTest();
// drawLineChartMonthlyAverageTest();
// drawLineChartDailyAverageTest();
......@@ -10,10 +10,10 @@ import { getMetadataPlusObservationsFromSingleOrMultipleDatastreams } from "./sr
import {
formatDatastreamMetadataForChart,
extractPropertiesFromFormattedDatastreamMetadata,
} from "./src_modules/fetchedDataProcess.mjs";
} from "./src_modules/fetchedDataProcessing.mjs";
import {
formatSensorThingsApiResponseForLineChart,
formatSensorThingsApiResponseForLineOrColumnChart,
drawLineChartHighcharts,
} from "./src_modules/chartLine.mjs";
......@@ -282,7 +282,7 @@ const selectChartTypeFromDropDown = async function () {
// Create formatted array(s) for observations - line chart
const formattedObsLineChartNestedArr = observationsNestedArr.map(
(observationsArr) =>
formatSensorThingsApiResponseForLineChart(observationsArr)
formatSensorThingsApiResponseForLineOrColumnChart(observationsArr)
);
// Create formatted array(s) for observations - heatmap
......@@ -293,7 +293,7 @@ const selectChartTypeFromDropDown = async function () {
// Create formatted array(s) for metadata - same for both chart types
const formattedMetadataArr = metadataNestedArr.map((metadataObj) =>
formatDatastreamMetadataForChart(metadataObj, false)
formatDatastreamMetadataForChart(metadataObj)
);
// Extract the formatted metadata properties
......
......@@ -2,7 +2,7 @@
import { getMetadataPlusObservationsFromSingleOrMultipleDatastreams } from "./fetchData.mjs";
import { extractPhenomenonNameFromDatastreamName } from "./fetchedDataProcess.mjs";
import { extractPhenomenonNameFromDatastreamName } from "./fetchedDataProcessing.mjs";
/**
* Calculate the temperature difference, dT, between Vorlauf temperature [VL] and Rücklauf temperature [RL] (i.e., dT = VL - RL)
......
......@@ -3,11 +3,11 @@
import {
chartExportOptions,
createCombinedTextDelimitedByComma,
createFullTitleForLineOrColumnChart,
createTooltipDateString,
extractSamplingRateFromDatastreamName,
} from "./chartHelpers.mjs";
import { extractPhenomenonNameFromDatastreamName } from "./fetchedDataProcess.mjs";
/**
* Format a computed aggregation result to make it suitable for a column chart
* @param {Array} calendarDatesMonthsStrArr An array of unique calendar dates strings (in "YYYY-MM-DD" fromat) or unique calendar months strings (in "YYYY-MM" format)
......@@ -65,64 +65,6 @@ const createSeriesOptionsForColumnChart = function (
);
};
/**
* Creates a date string that is used as a header for a shared tooltip string for a column chart
* @param {Number} pointXAxisValue The x-axis value (Unix timestamp) which is common for a set of data points
* @param {String} aggregationInterval The aggregation interval as a string, either "daily" or "monthly"
* @returns {String} A calendar date or calendar month string that is common for a set of data points
*/
const createHeaderDateString = function (pointXAxisValue, aggregationInterval) {
if (aggregationInterval === "daily")
return `${Highcharts.dateFormat("%A, %b %e, %Y", pointXAxisValue)}`;
else if (aggregationInterval === "monthly")
return `${Highcharts.dateFormat("%b %Y", pointXAxisValue)}`;
};
/**
* Create a partial string for a column's chart title
* @param {String} aggregationInterval The aggregation interval as a string, either "daily" or "monthly"
* @param {String} aggregationType The aggregation type as a string, either "sum" or "average"
* @returns {String} Partial string for chart title
*/
const createPartialTitleForColumnChart = function (
aggregationInterval,
aggregationType
) {
// Case 1: No aggregation; return empty string
if (!aggregationInterval && !aggregationType) return ``;
// Case 2: Aggregation; capitalize the first characters
return `${
aggregationInterval.slice(0, 1).toUpperCase() + aggregationInterval.slice(1)
} ${aggregationType.slice(0, 1).toUpperCase() + aggregationType.slice(1)}`;
};
/**
* Create a full string for a column's chart title
* @param {Array} datastreamNamesArr An array of datastream names as strings
* @param {String} aggregationInterval The aggregation interval as a string, either "daily" or "monthly"
* @param {String} aggregationType The aggregation type as a string, either "sum" or "average"
* @returns {String} Full string for chart title
*/
const createFullTitleForColumnChart = function (
phenomenonNamesArr,
aggregationInterval,
aggregationType
) {
// Case 1: No aggregation; create a comma separated string of phenomenon names
if (!aggregationInterval && !aggregationType)
return `${createPartialTitleForColumnChart(
aggregationInterval,
aggregationType
)}${createCombinedTextDelimitedByComma(phenomenonNamesArr)}`;
// Case 2: Aggregation
return `${createPartialTitleForColumnChart(
aggregationInterval,
aggregationType
)} ${phenomenonNamesArr[0]}`;
};
/**
* Draw a column chart using Highcharts library
* @param {Array} formattedAggResultArraysForColumnChart An array made up of formatted aggregated result array(s) suitable for use in a column chart
......@@ -133,19 +75,34 @@ const drawColumnChartHighcharts = function (
formattedAggResultArraysForColumnChart,
extractedFormattedDatastreamProperties
) {
// Arrays of datastream properties
const {
datastreamNamesArr,
// Formatted datastream properties
let datastreamNamesArr,
phenomenonNamesArr,
buildingIdsPhenomenonNamesArr,
unitOfMeasurementSymbolsArr,
aggregationInterval,
aggregationType,
} = extractedFormattedDatastreamProperties;
// Create an array of phenomenon names
const phenomenonNamesArr = datastreamNamesArr.map((datastreamName) =>
extractPhenomenonNameFromDatastreamName(datastreamName)
);
aggregationType;
// Check whether the datastream properties are for aggregated observations
if (extractedFormattedDatastreamProperties?.aggregationType === undefined) {
// Case 1: No aggregation
({
datastreamNamesArr,
phenomenonNamesArr,
buildingIdsPhenomenonNamesArr,
unitOfMeasurementSymbolsArr,
} = extractedFormattedDatastreamProperties);
} else {
// Case 2: Aggregation
({
datastreamNamesArr,
phenomenonNamesArr,
buildingIdsPhenomenonNamesArr,
unitOfMeasurementSymbolsArr,
aggregationInterval,
aggregationType,
} = extractedFormattedDatastreamProperties);
}
// Create the array of series options object(s)
const seriesOptionsArr = createSeriesOptionsForColumnChart(
......@@ -159,7 +116,7 @@ const drawColumnChartHighcharts = function (
// Assume that we will be comparing similar phenomena, so we can use the first phenomenon symbol
const unitOfMeasurementSymbol = unitOfMeasurementSymbolsArr[0];
const textChartTitle = createFullTitleForColumnChart(
const textChartTitle = createFullTitleForLineOrColumnChart(
phenomenonNamesArr,
aggregationInterval,
aggregationType
......@@ -209,7 +166,7 @@ const drawColumnChartHighcharts = function (
}</span>: <b>${point.y.toFixed(2)} ${
unitOfMeasurementSymbolsArr[i]
}</b>`,
`${createHeaderDateString(this.x, aggregationInterval)}`
`${createTooltipDateString(this.x, aggregationInterval)}`
);
},
shared: true,
......
......@@ -57,6 +57,68 @@ const createCombinedTextDelimitedByComma = function (metadataPropertiesArr) {
return metadataPropertiesArr.join(", ");
};
/**
* Create a partial string for a line chart or column chart title
* @param {String} aggregationInterval The aggregation interval as a string, either "daily" or "monthly"
* @param {String} aggregationType The aggregation type as a string, either "sum" or "average"
* @returns {String} Partial string for chart title
*/
const createPartialTitleForLineOrColumnChart = function (
aggregationInterval,
aggregationType
) {
// Case 1: No aggregation; return empty string
if (!aggregationInterval && !aggregationType) return ``;
// Case 2: Aggregation; capitalize the first characters
return `${
aggregationInterval.slice(0, 1).toUpperCase() + aggregationInterval.slice(1)
} ${aggregationType.slice(0, 1).toUpperCase() + aggregationType.slice(1)}`;
};
/**
* Create a full string for a line chart or column chart title
* @param {Array} datastreamNamesArr An array of datastream names as strings
* @param {String} aggregationInterval The aggregation interval as a string, either "daily" or "monthly"
* @param {String} aggregationType The aggregation type as a string, either "sum" or "average"
* @returns {String} Full string for chart title
*/
const createFullTitleForLineOrColumnChart = function (
phenomenonNamesArr,
aggregationInterval,
aggregationType
) {
// Case 1: No aggregation; create a comma separated string of phenomenon names
if (!aggregationInterval && !aggregationType)
return `${createPartialTitleForLineOrColumnChart(
aggregationInterval,
aggregationType
)}${createCombinedTextDelimitedByComma(phenomenonNamesArr)}`;
// Case 2: Aggregation
return `${createPartialTitleForLineOrColumnChart(
aggregationInterval,
aggregationType
)} ${phenomenonNamesArr[0]}`;
};
/**
* Creates a date string that is used in a shared tooltip for a line or column chart
* @param {Number} pointXAxisValue The x-axis value (Unix timestamp) which is common for a set of data points
* @param {String} aggregationInterval The aggregation interval as a string, either "daily" or "monthly"
* @returns {String} A calendar date or calendar month string that is common for a set of data points
*/
const createTooltipDateString = function (
pointXAxisValue,
aggregationInterval
) {
if (aggregationInterval === undefined || aggregationInterval === "daily")
// When `aggregationInterval === undefined`, assume that we are displaying raw observations
return `${Highcharts.dateFormat("%A, %b %e, %Y", pointXAxisValue)}`;
else if (aggregationInterval === "monthly")
return `${Highcharts.dateFormat("%b %Y", pointXAxisValue)}`;
};
/**
* Extracts the sampling rate substring from a datastream name string
* @param {Array} datastreamNamesArr An array of datastream name(s)
......@@ -69,10 +131,22 @@ const extractSamplingRateFromDatastreamName = function (datastreamNamesArr) {
);
};
/**
* Remove the transparency (alpha channel) from a color
* @param {String} rgbaColor A color expressed in RGBA format
* @returns {String} A color in RGB format
*/
const removeTransparencyFromColor = function (rgbaColor) {
return `rgb(${rgbaColor.slice(5, -5)})`;
};
export {
chartExportOptions,
createCombinedTextDelimitedByAmpersand,
createCombinedTextDelimitedByComma,
createFullTitleForLineOrColumnChart,
createTooltipDateString,
convertHexColorToRGBColor,
extractSamplingRateFromDatastreamName,
removeTransparencyFromColor,
};
......@@ -2,15 +2,17 @@
import {
chartExportOptions,
createFullTitleForLineOrColumnChart,
createTooltipDateString,
extractSamplingRateFromDatastreamName,
} from "./chartHelpers.mjs";
/**
* Format the response from SensorThings API to make it suitable for use in a line chart
* Format the response from SensorThings API to make it suitable for use in a line chart or column chart
* @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 line chart
*/
const formatSensorThingsApiResponseForLineChart = function (obsArray) {
const formatSensorThingsApiResponseForLineOrColumnChart = function (obsArray) {
if (!obsArray) return;
return obsArray.map((result) => {
......@@ -32,12 +34,12 @@ const createCombinedTextForLineChartTitles = function (phenomenonNamesArr) {
/**
* 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} buildingIdsPhenomenonNamesArr An array of string(s) made up of building ID(s) + phenomenon name(s)
* @returns {Array} An array made up of series options object(s)
*/
const createSeriesOptionsForLineChart = function (
formattedObsArraysForLineChart,
phenomenonNamesArr
buildingIdsPhenomenonNamesArr
) {
// An array of colors, in hexadecimal format, provided by the global Highcharts object
const seriesColors = Highcharts.getOptions().colors;
......@@ -46,16 +48,19 @@ const createSeriesOptionsForLineChart = function (
const seriesColorsArr = [...seriesColors];
// Create an array of seriesOptions objects
// Assumes that the observation array of arrays and phenomenon names array are of equal length
// Assumes that the observation array of arrays and building IDs + phenomenon names array are of equal length
// Use one of the arrays for looping
if (formattedObsArraysForLineChart.length !== phenomenonNamesArr.length)
if (
formattedObsArraysForLineChart.length !==
buildingIdsPhenomenonNamesArr.length
)
throw new Error(
"The observations array and phenomenon names array have different lengths"
);
return formattedObsArraysForLineChart.map((formattedObsArray, i) => {
return {
name: `${phenomenonNamesArr[i]}`,
name: `${buildingIdsPhenomenonNamesArr[i]}`,
data: formattedObsArray,
color: seriesColorsArr[i],
turboThreshold: Number.MAX_VALUE, // #3404, remove after 4.0.5 release
......@@ -74,15 +79,40 @@ const drawLineChartHighcharts = function (
extractedFormattedDatastreamProperties
) {
// Arrays of datastream properties
const {
datastreamNamesArr,
let datastreamNamesArr,
phenomenonNamesArr,
buildingIdsPhenomenonNamesArr,
unitOfMeasurementSymbolsArr,
} = extractedFormattedDatastreamProperties;
aggregationInterval,
aggregationType;
// Check whether the datastream properties are for aggregated observations
if (extractedFormattedDatastreamProperties?.aggregationType === undefined) {
// Case 1: No aggregation
({
datastreamNamesArr,
phenomenonNamesArr,
buildingIdsPhenomenonNamesArr,
unitOfMeasurementSymbolsArr,
} = extractedFormattedDatastreamProperties);
} else {
// Case 2: Aggregation
({
datastreamNamesArr,
phenomenonNamesArr,
buildingIdsPhenomenonNamesArr,
unitOfMeasurementSymbolsArr,
aggregationInterval,
aggregationType,
} = extractedFormattedDatastreamProperties);
}
// Chart title and subtitle text
const textChartTitle =
createCombinedTextForLineChartTitles(phenomenonNamesArr);
const textChartTitle = createFullTitleForLineOrColumnChart(
phenomenonNamesArr,
aggregationInterval,
aggregationType
);
const textChartSubtitle = `Sampling rate(s): ${createCombinedTextForLineChartTitles(
extractSamplingRateFromDatastreamName(datastreamNamesArr)
......@@ -91,7 +121,7 @@ const drawLineChartHighcharts = function (
// Create the array of series options object(s)
const seriesOptionsArr = createSeriesOptionsForLineChart(
formattedObsArraysForLineChart,
phenomenonNamesArr,
buildingIdsPhenomenonNamesArr,
unitOfMeasurementSymbolsArr
);
......@@ -119,7 +149,9 @@ const drawLineChartHighcharts = function (
// Our tooltip is split
// this.x -- common for all points
// this.points -- an array containing properties for each series
return [`${Highcharts.dateFormat("%A, %b %e, %Y", this.x)}`].concat(
return [
`${createTooltipDateString(this.x, aggregationInterval)}`,
].concat(
this.points
? this.points.map(
(point, i) =>
......@@ -140,4 +172,7 @@ const drawLineChartHighcharts = function (
});
};
export { formatSensorThingsApiResponseForLineChart, drawLineChartHighcharts };
export {
formatSensorThingsApiResponseForLineOrColumnChart,
drawLineChartHighcharts,
};
......@@ -6,6 +6,7 @@ import {
createCombinedTextDelimitedByAmpersand,
createCombinedTextDelimitedByComma,
extractSamplingRateFromDatastreamName,
removeTransparencyFromColor,
} from "./chartHelpers.mjs";
/**
......@@ -457,12 +458,18 @@ const drawScatterPlotHighcharts = function (
tooltip: {
formatter() {
const headerString = `${this.series.name}<br>`;
// The color contained in the series object is in RGBA format,
// convert it to RGB format so that the text in the tooltip is more legible
const headerString = `<span style="color:${removeTransparencyFromColor(
this.point.color
)}">${this.series.name}</span> <br>`;
const pointString = `<b>${this.point.y.toFixed(
2
)} ${UNIT_OF_MEASUREMENT_SYMBOL}, ${this.point.x.toFixed(
2
)} ${UNIT_OF_MEASUREMENT_SYMBOL}</b>`;
return headerString + pointString;
},
},
......
......@@ -46,21 +46,9 @@ const extractBuildingIdPhenomenonNameFromDatastreamName = function (
/**
* Format the response containing a Datastream's metadata from Sensorthings API
* @param {Object} datastreamMetadata An object containing a Datastream's metadata
* @param {Boolean} isMetadataForAggregation A flag to determine if the datastream metadata will be used for aggregation. Set to `true` if metadata will be used for aggregation, `false` if not
* @returns {Object} An object containing the formatted metadata that is suitable for use in a chart
*/
const formatDatastreamMetadataForChart = function (
datastreamMetadata,
isMetadataForAggregation
) {
if (
datastreamMetadata === undefined ||
isMetadataForAggregation === undefined
)
throw new Error(
"This function expects two arguments, ensure that both have been supplied"
);
const formatDatastreamMetadataForChart = function (datastreamMetadata) {
const {
description: datastreamDescription,
name: datastreamName,
......@@ -80,19 +68,10 @@ const formatDatastreamMetadataForChart = function (
unitOfMeasurement.symbol
);
// Case 1: Metadata NOT USED for aggregation; "isMetadataForAggregation" = false
if (!isMetadataForAggregation)
return {
datastreamDescription,
datastreamName,
phenomenonName,
unitOfMeasurementSymbol,
};
// Case 2: Metadata USED for aggregation; "isMetadataForAggregation" = true
return {
datastreamDescription,
datastreamName,
phenomenonName,
buildingIdPhenomenonName,
unitOfMeasurementSymbol,
};
......@@ -174,6 +153,7 @@ const extractPropertiesFromFormattedDatastreamMetadata = function (
datastreamDescriptionsArr,
datastreamNamesArr,
phenomenonNamesArr,
buildingIdsPhenomenonNamesArr,
unitOfMeasurementSymbolsArr,
};
......@@ -181,6 +161,7 @@ const extractPropertiesFromFormattedDatastreamMetadata = function (
return {
datastreamDescriptionsArr,
datastreamNamesArr,
phenomenonNamesArr,
buildingIdsPhenomenonNamesArr,
unitOfMeasurementSymbolsArr,
aggregationInterval,
......
Markdown is supported
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