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

Use raw and aggregated data when drawing charts

parent dc2e7ae0
......@@ -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