You need to sign in or sign up before continuing.
Commit 233f7b2c authored by Pithon Kabiro's avatar Pithon Kabiro
Browse files

Draw multiple series on same scatter plot

parent 4ae783c2
This diff is collapsed.
"use strict"; "use strict";
/** import {
* Create 24-hour time strings for a time interval delimited by a start time and an end time. It is assumed that the start time is at "00:00:00" and the end time is at "23:45:00" (when the sampling rate of observations is 15 min) or "23:00:00" (when the sampling rate of observations is 60 min) extractObservationValuesWithinDatesInterval,
* @param {String} phenomenonSamplingRate The sampling rate of the phenomenon of interest represented as a string, e.g. "15min", "60min" extractObservationValuesWithinMonthInterval,
* @returns {Array} An array of two 24-hour strings representing the start time and end time } from "./aggregateHelpers.mjs";
*/
const createTimeStringsForInterval = function (phenomenonSamplingRate) {
const fifteenMinutes = "15min";
const sixtyMinutes = "60min";
const startTime = "00:00:00";
const endTimeFifteenMinutes = "23:45:00";
const endTimeSixtyMinutes = "23:00:00";
if (
phenomenonSamplingRate !== fifteenMinutes &&
phenomenonSamplingRate !== sixtyMinutes
)
throw new Error(
`Check that the provided phenomenon sampling rate string is in this format: "15min" or "60min"`
);
// 15 min sampling rate
if (phenomenonSamplingRate === fifteenMinutes) {
return [startTime, endTimeFifteenMinutes];
}
// 60 min sampling rate
if (phenomenonSamplingRate === sixtyMinutes) {
return [startTime, endTimeSixtyMinutes];
}
};
/**
* Create an ISO 8601 date and time string
* @param {String} inputCalendarDate Calendar date string in "YYYY-MM-DD" format
* @param {String} inputTwentyFourHourTime 24-hour time string in "hh:mm:ss" format
* @returns {String} An ISO 8601 date and time string
*/
const createIso8601DateTimeString = function (
inputCalendarDate,
inputTwentyFourHourTime
) {
return `${inputCalendarDate}T${inputTwentyFourHourTime}.000Z`;
};
/**
* Check whether a year is a leap year or not
* @param {Number} year Integer representing the year
* @returns {Boolean} true if leap year, false if not
*/
const checkIfLeapYear = function (year) {
return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0;
};
/**
* Calculate the index of a timestamp in an array of timestamps
* @param {Array} inputTimestampArr An array of timestamps, extracted from an array of observations
* @param {String} timestampOfInterest A string representing the timestamp of interest in ISO 8601 format
* @returns {Number} An integer representing the index of the timestamp of interest in the array of timestamps
*/
const getIndexOfTimestamp = function (inputTimestampArr, timestampOfInterest) {
const timestampIndex = inputTimestampArr.findIndex(
(timestamp) => timestamp === timestampOfInterest
);
// If the timestamp does not exist in the timestamp array
if (timestampIndex === -1)
throw new Error(
"A start or end timestamp could not be found in the timestamp array"
);
// If the timestamp exists in the timestamp array
return timestampIndex;
};
/**
* Calculate the indexes of the start and end timestamps
* @param {Array} obsTimestampArr An array of observations timestamps
* @param {String} samplingRate The sampling rate of observations as a string, e.g. "15min", "60min"
* @param {String} startDate A 24-hour date string representing the start date
* @param {String} endDate A 24-hour date string representing the end date
* @returns {Array} A 1*2 array tht contains integers representing the start index and end index respectively
*/
const calculateIndexStartEndTimestamp = function (
obsTimestampArr,
samplingRate,
startDate,
endDate
) {
// Create and extract 24-hour strings for the start and end of interval
const [startTimeString, endTimeString] =
createTimeStringsForInterval(samplingRate);
// Create ISO 8601 strings for the start and end of interval
const startIso8601DateTimeString = createIso8601DateTimeString(
startDate,
startTimeString
);
const endIso8601DateTimeString = createIso8601DateTimeString(
endDate,
endTimeString
);
// Calculate the indexes of the timestamps for the start and end of interval
const indexStartTimestamp = getIndexOfTimestamp(
obsTimestampArr,
startIso8601DateTimeString
);
const indexEndTimestamp = getIndexOfTimestamp(
obsTimestampArr,
endIso8601DateTimeString
);
return [indexStartTimestamp, indexEndTimestamp];
};
/**
* Extract the set of observation values that fall within a time interval delimited by a start date and end date. The start date may be the same as the end date.
* @param {Array} obsArray An array of observations (timestamp + value) that is response from SensorThings API
* @param {String} samplingRate The sampling rate of observations as a string, e.g. "15min", "60min"
* @param {String} startDate A 24-hour date string representing the start date
* @param {String} endDate A 24-hour date string representing the end date
* @returns {Array} An array of observation values that fall within our time interval
*/
const extractObservationValuesWithinDatesInterval = function (
obsArray,
samplingRate,
startDate,
endDate
) {
// Extract the timestamps and values from the observations
const obsTimestampArr = obsArray.map((obs) => obs[0]);
const obsValuesArr = obsArray.map((obs) => obs[1]);
// Calculate the indexes of the timestamps for the start and end of interval
const [indexStartTimestamp, indexEndTimestamp] =
calculateIndexStartEndTimestamp(
obsTimestampArr,
samplingRate,
startDate,
endDate
);
// Extract the observations that fall within our time interval
return obsValuesArr.slice(indexStartTimestamp, indexEndTimestamp + 1);
};
/** /**
* Calculate the sum of observation values that fall within a time interval delimited by a start date and end date * Calculate the sum of observation values that fall within a time interval delimited by a start date and end date
...@@ -175,82 +33,6 @@ const calculateAverageOfObservationValuesWithinDatesInterval = function ( ...@@ -175,82 +33,6 @@ const calculateAverageOfObservationValuesWithinDatesInterval = function (
); );
}; };
/**
* Extract the year and month digits from a calendar month string
* @param {String} calendarMonthStr Calendar month string in "YYYY-MM" format
* @returns {Array} A 1*2 array tht contains integers representing the year and month respectively
*/
const extractMonthYearDigitsFromCalendarMonthString = function (
calendarMonthStr
) {
// Extract year as integer
const yearNum = parseInt(calendarMonthStr.slice(0, 4), 10);
// Extract month as integer
const monthNum = parseInt(calendarMonthStr.slice(-2), 10);
return [yearNum, monthNum];
};
/**
* Extract the set of observation values that fall within a time interval delimited by the first day and last day of a calendar month
* @param {Array} obsArray An array of observations (timestamp + value) that is response from SensorThings API
* @param {String} samplingRate The sampling rate of observations as a string, e.g. "15min", "60min"
* @param {String} calendarMonthStr Calendar month string in "YYYY-MM" format
* @returns {Array} An array of observation values that fall within one calendar month
*/
const extractObservationValuesWithinMonthInterval = function (
obsArray,
samplingRate,
calendarMonthStr
) {
// Extract the year and month digits from the calendar month string
const [yearNum, monthNum] =
extractMonthYearDigitsFromCalendarMonthString(calendarMonthStr);
if (monthNum < 1 || monthNum > 12) return;
// All the months start on the first
const startDateStr = `${calendarMonthStr}-01`;
// February
if (monthNum === 2) {
// Leap year
if (checkIfLeapYear(yearNum))
return extractObservationValuesWithinDatesInterval(
obsArray,
samplingRate,
startDateStr,
`${calendarMonthStr}-29`
);
// Non-leap year
return extractObservationValuesWithinDatesInterval(
obsArray,
samplingRate,
startDateStr,
`${calendarMonthStr}-28`
);
}
// Months with 30 days
if (monthNum === 4 || monthNum === 6 || monthNum === 9 || monthNum === 11)
return extractObservationValuesWithinDatesInterval(
obsArray,
samplingRate,
startDateStr,
`${calendarMonthStr}-30`
);
// Months with 31 days
return extractObservationValuesWithinDatesInterval(
obsArray,
samplingRate,
startDateStr,
`${calendarMonthStr}-31`
);
};
/** /**
* Calculate the sum of observation values that fall within a time interval delimited by the first day and last day of a calendar month * Calculate the sum of observation values that fall within a time interval delimited by the first day and last day of a calendar month
* @param {Array} obsValuesForMonthIntervalArr An array of observation values that fall within one calendar month * @param {Array} obsValuesForMonthIntervalArr An array of observation values that fall within one calendar month
...@@ -375,46 +157,7 @@ const calculateAverageOfObservationValuesWithinInterval = function ( ...@@ -375,46 +157,7 @@ const calculateAverageOfObservationValuesWithinInterval = function (
} }
}; };
/**
* Extract unique calendar dates from date/time strings in ISO 8601 format
* @param {Array} obsArray An array of observations (timestamp + value) that is response from SensorThings API
* @returns {Array} An array of unique calendar date strings in "YYYY-MM-DD" format
*/
const extractUniqueCalendarDatesFromTimestamp = function (obsArray) {
// The timestamp is the first element of the observation array
const timestampArr = obsArray.map((obs) => obs[0]);
// Extract the calendar date string from the timestamp string
const calendarDateArr = timestampArr.map((timestamp) =>
timestamp.slice(0, 10)
);
// Use a set to remove duplicates
const uniqueCalendarDates = new Set(calendarDateArr);
return [...uniqueCalendarDates];
};
/**
* Extract unique calendar months from calendar date strings
* @param {Array} calendarDatesArr An array of unique calendar date strings in "YYYY-MM-DD" format
* @returns {Array} An array of unique calendar month strings in "YYYY-MM" format
*/
const extractUniqueCalendarMonthsFromCalendarDates = function (
calendarDatesArr
) {
// Extract the calendar month strings
const calendarMonthsArr = calendarDatesArr.map((date) => date.slice(0, 7));
// Use a set to remove duplicates
const uniqueCalendarMonths = new Set(calendarMonthsArr);
return [...uniqueCalendarMonths];
};
export { export {
calculateSumOfObservationValuesWithinInterval, calculateSumOfObservationValuesWithinInterval,
extractUniqueCalendarDatesFromTimestamp,
extractUniqueCalendarMonthsFromCalendarDates,
calculateAverageOfObservationValuesWithinInterval, calculateAverageOfObservationValuesWithinInterval,
}; };
"use strict";
/**
* Create 24-hour time strings for a time interval delimited by a start time and an end time. It is assumed that the start time is at "00:00:00" and the end time is at "23:45:00" (when the sampling rate of observations is 15 min) or "23:00:00" (when the sampling rate of observations is 60 min)
* @param {String} phenomenonSamplingRate The sampling rate of the phenomenon of interest represented as a string, e.g. "15min", "60min"
* @returns {Array} An array of two 24-hour strings representing the start time and end time
*/
const createTimeStringsForInterval = function (phenomenonSamplingRate) {
const fifteenMinutes = "15min";
const sixtyMinutes = "60min";
const startTime = "00:00:00";
const endTimeFifteenMinutes = "23:45:00";
const endTimeSixtyMinutes = "23:00:00";
if (
phenomenonSamplingRate !== fifteenMinutes &&
phenomenonSamplingRate !== sixtyMinutes
)
throw new Error(
`Check that the provided phenomenon sampling rate string is in this format: "15min" or "60min"`
);
// 15 min sampling rate
if (phenomenonSamplingRate === fifteenMinutes) {
return [startTime, endTimeFifteenMinutes];
}
// 60 min sampling rate
if (phenomenonSamplingRate === sixtyMinutes) {
return [startTime, endTimeSixtyMinutes];
}
};
/**
* Create an ISO 8601 date and time string
* @param {String} inputCalendarDate Calendar date string in "YYYY-MM-DD" format
* @param {String} inputTwentyFourHourTime 24-hour time string in "hh:mm:ss" format
* @returns {String} An ISO 8601 date and time string
*/
const createIso8601DateTimeString = function (
inputCalendarDate,
inputTwentyFourHourTime
) {
return `${inputCalendarDate}T${inputTwentyFourHourTime}.000Z`;
};
/**
* Check whether a year is a leap year or not
* @param {Number} year Integer representing the year
* @returns {Boolean} true if leap year, false if not
*/
const checkIfLeapYear = function (year) {
return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0;
};
/**
* Calculate the index of a timestamp in an array of timestamps
* @param {Array} inputTimestampArr An array of timestamps, extracted from an array of observations
* @param {String} timestampOfInterest A string representing the timestamp of interest in ISO 8601 format
* @returns {Number} An integer representing the index of the timestamp of interest in the array of timestamps
*/
const getIndexOfTimestamp = function (inputTimestampArr, timestampOfInterest) {
const timestampIndex = inputTimestampArr.findIndex(
(timestamp) => timestamp === timestampOfInterest
);
// If the timestamp does not exist in the timestamp array
if (timestampIndex === -1)
throw new Error(
"A start or end timestamp could not be found in the timestamp array"
);
// If the timestamp exists in the timestamp array
return timestampIndex;
};
/**
* Calculate the indexes of the start and end timestamps
* @param {Array} obsTimestampArr An array of observations timestamps
* @param {String} samplingRate The sampling rate of observations as a string, e.g. "15min", "60min"
* @param {String} startDate A calendar date string in "YYYY-MM-DD" format representing the start date
* @param {String} endDate A calendar date string in "YYYY-MM-DD" format representing the end date
* @returns {Array} A 1*2 array tht contains integers representing the start index and end index respectively
*/
const calculateIndexStartEndTimestamp = function (
obsTimestampArr,
samplingRate,
startDate,
endDate
) {
// Create and extract 24-hour strings for the start and end of interval
const [startTimeString, endTimeString] =
createTimeStringsForInterval(samplingRate);
// Create ISO 8601 strings for the start and end of interval
const startIso8601DateTimeString = createIso8601DateTimeString(
startDate,
startTimeString
);
const endIso8601DateTimeString = createIso8601DateTimeString(
endDate,
endTimeString
);
// Calculate the indexes of the timestamps for the start and end of interval
const indexStartTimestamp = getIndexOfTimestamp(
obsTimestampArr,
startIso8601DateTimeString
);
const indexEndTimestamp = getIndexOfTimestamp(
obsTimestampArr,
endIso8601DateTimeString
);
return [indexStartTimestamp, indexEndTimestamp];
};
/**
* Extract the set of observations that fall within a time interval delimited by a start date and end date. The start date may be the same as the end date.
* @param {Array} obsArray An array of observations (timestamp + value) that is response from SensorThings API
* @param {String} samplingRate The sampling rate of observations as a string, e.g. "15min", "60min"
* @param {String} startDate A calendar date string in "YYYY-MM-DD" format representing the start date
* @param {String} endDate A calendar date string in "YYYY-MM-DD" format representing the end date
* @returns {Array} An array of observations (timestamp + value) that fall within our time interval
*/
const extractObservationsWithinDatesInterval = function (
obsArray,
samplingRate,
startDate,
endDate
) {
// Extract the timestamps from the observations
const obsTimestampArr = obsArray.map((obs) => obs[0]);
// Calculate the indexes of the timestamps for the start and end of interval
const [indexStartTimestamp, indexEndTimestamp] =
calculateIndexStartEndTimestamp(
obsTimestampArr,
samplingRate,
startDate,
endDate
);
// Extract the observations that fall within our time interval
return obsArray.slice(indexStartTimestamp, indexEndTimestamp + 1);
};
/**
* Extract the set of observation values that fall within a time interval delimited by a start date and end date. The start date may be the same as the end date.
* @param {Array} obsArray An array of observations (timestamp + value) that is response from SensorThings API
* @param {String} samplingRate The sampling rate of observations as a string, e.g. "15min", "60min"
* @param {String} startDate A calendar date string in "YYYY-MM-DD" format representing the start date
* @param {String} endDate A calendar date string in "YYYY-MM-DD" format representing the end date
* @returns {Array} An array of observation values that fall within our time interval
*/
const extractObservationValuesWithinDatesInterval = function (
obsArray,
samplingRate,
startDate,
endDate
) {
// Extract the timestamps and values from the observations
const obsTimestampArr = obsArray.map((obs) => obs[0]);
const obsValuesArr = obsArray.map((obs) => obs[1]);
// Calculate the indexes of the timestamps for the start and end of interval
const [indexStartTimestamp, indexEndTimestamp] =
calculateIndexStartEndTimestamp(
obsTimestampArr,
samplingRate,
startDate,
endDate
);
// Extract the observation values that fall within our time interval
return obsValuesArr.slice(indexStartTimestamp, indexEndTimestamp + 1);
};
/**
* Extract the year and month digits from a calendar month string
* @param {String} calendarMonthStr Calendar month string in "YYYY-MM" format
* @returns {Array} A 1*2 array tht contains integers representing the year and month respectively
*/
const extractMonthYearDigitsFromCalendarMonthString = function (
calendarMonthStr
) {
// Extract year as integer
const yearNum = parseInt(calendarMonthStr.slice(0, 4), 10);
// Extract month as integer
const monthNum = parseInt(calendarMonthStr.slice(-2), 10);
return [yearNum, monthNum];
};
/**
* Extract the set of observation values that fall within a time interval delimited by the first day and last day of a calendar month
* @param {Array} obsArray An array of observations (timestamp + value) that is response from SensorThings API
* @param {String} samplingRate The sampling rate of observations as a string, e.g. "15min", "60min"
* @param {String} calendarMonthStr Calendar month string in "YYYY-MM" format
* @returns {Array} An array of observation values that fall within one calendar month
*/
const extractObservationValuesWithinMonthInterval = function (
obsArray,
samplingRate,
calendarMonthStr
) {
// Extract the year and month digits from the calendar month string
const [yearNum, monthNum] =
extractMonthYearDigitsFromCalendarMonthString(calendarMonthStr);
if (monthNum < 1 || monthNum > 12) return;
// All the months start on the first
const startDateStr = `${calendarMonthStr}-01`;
// February
if (monthNum === 2) {
// Leap year
if (checkIfLeapYear(yearNum))
return extractObservationValuesWithinDatesInterval(
obsArray,
samplingRate,
startDateStr,
`${calendarMonthStr}-29`
);
// Non-leap year
return extractObservationValuesWithinDatesInterval(
obsArray,
samplingRate,
startDateStr,
`${calendarMonthStr}-28`
);
}
// Months with 30 days
if (monthNum === 4 || monthNum === 6 || monthNum === 9 || monthNum === 11)
return extractObservationValuesWithinDatesInterval(
obsArray,
samplingRate,
startDateStr,
`${calendarMonthStr}-30`
);
// Months with 31 days
return extractObservationValuesWithinDatesInterval(
obsArray,
samplingRate,
startDateStr,
`${calendarMonthStr}-31`
);
};
/**
* Extract unique calendar dates from date/time strings in ISO 8601 format
* @param {Array} obsArray An array of observations (timestamp + value) that is response from SensorThings API
* @returns {Array} An array of unique calendar date strings in "YYYY-MM-DD" format
*/
const extractUniqueCalendarDatesFromTimestamp = function (obsArray) {
// The timestamp is the first element of the observation array
const timestampArr = obsArray.map((obs) => obs[0]);
// Extract the calendar date string from the timestamp string
const calendarDateArr = timestampArr.map((timestamp) =>
timestamp.slice(0, 10)
);
// Use a set to remove duplicates
const uniqueCalendarDates = new Set(calendarDateArr);
return [...uniqueCalendarDates];
};
/**
* Extract unique calendar months from calendar date strings
* @param {Array} calendarDatesArr An array of unique calendar date strings in "YYYY-MM-DD" format
* @returns {Array} An array of unique calendar month strings in "YYYY-MM" format
*/
const extractUniqueCalendarMonthsFromCalendarDates = function (
calendarDatesArr
) {
// Extract the calendar month strings
const calendarMonthsArr = calendarDatesArr.map((date) => date.slice(0, 7));
// Use a set to remove duplicates
const uniqueCalendarMonths = new Set(calendarMonthsArr);
return [...uniqueCalendarMonths];
};
export {
extractObservationsWithinDatesInterval,
extractObservationValuesWithinDatesInterval,
extractObservationValuesWithinMonthInterval,
extractUniqueCalendarDatesFromTimestamp,
extractUniqueCalendarMonthsFromCalendarDates,
};
"use strict"; "use strict";
import { chartExportOptions } from "./chartExport.mjs"; import { chartExportOptions } from "./chartHelpers.mjs";
/** /**
* Format a computed aggregation result to make it suitable for a column chart * Format a computed aggregation result to make it suitable for a column chart
...@@ -42,6 +42,13 @@ const createSeriesOptionsForColumnChart = function ( ...@@ -42,6 +42,13 @@ const createSeriesOptionsForColumnChart = function (
// Create an array of seriesOptions objects // Create an array of seriesOptions objects
// Assumes that the observation array of arrays, phenomenon names array and phenomenon symbols array are of equal length // 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 // Use one of the arrays for looping
if (
formattedAggregatedResultForColumnChart.length !== phenomenonNamesArr.length
)
throw new Error(
"The observations array and phenomenon names array have different lengths"
);
return formattedAggregatedResultForColumnChart.map( return formattedAggregatedResultForColumnChart.map(
(formattedAggResArray, i) => { (formattedAggResArray, i) => {
return { return {
...@@ -57,7 +64,7 @@ const createSeriesOptionsForColumnChart = function ( ...@@ -57,7 +64,7 @@ const createSeriesOptionsForColumnChart = function (
* Draw a column chart using Highcharts library * 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 * @param {Array} formattedAggResultArraysForColumnChart An array made up of formatted aggregated result array(s) suitable for use in a column chart
* @param {Object} extractedFormattedDatastreamProperties An object that contains arrays of formatted Datastream properties * @param {Object} extractedFormattedDatastreamProperties An object that contains arrays of formatted Datastream properties
* @returns {undefined} * @returns {undefined} undefined
*/ */
const drawColumnChartHighcharts = function ( const drawColumnChartHighcharts = function (
formattedAggResultArraysForColumnChart, formattedAggResultArraysForColumnChart,
......
"use strict";
export const chartExportOptions = {
buttons: {
contextButton: {
menuItems: ["downloadPNG", "downloadJPEG", "downloadPDF", "downloadSVG"],
},
},
};
"use strict"; "use strict";
import { chartExportOptions } from "./chartExport.mjs"; import { chartExportOptions } from "./chartHelpers.mjs";
/** /**
* Format the response from SensorThings API to make it suitable for use in a heatmap * Format the response from SensorThings API to make it suitable for use in a heatmap
......
"use strict";
const chartExportOptions = {
buttons: {
contextButton: {
menuItems: ["downloadPNG", "downloadJPEG", "downloadPDF", "downloadSVG"],
},
},
};
/**
* Convert a hexadecimal color code obtained from the Highcharts object (`Highcharts.getOptions().colors`) to its equivalent RGB color code
* @param {String} hexCode Input hex color code
* @returns {String} Output RGB color code
*/
const convertHexColorToRGBColor = function (hexCode) {
const hexToRGBMapping = {
"#7cb5ec": "rgb(124, 181, 236)",
"#434348": "rgb(67, 67, 72)",
"#90ed7d": "rgb(144, 237, 125)",
"#f7a35c": "rgb(247, 163, 92)",
"#8085e9": "rgb(128, 133, 233)",
"#f15c80": "rgb(241, 92, 128)",
"#e4d354": "rgb(228, 211, 84)",
"#2b908f": "rgb(228, 211, 84)",
"#f45b5b": "rgb(244, 91, 91)",
"#91e8e1": "rgb(145, 232, 225)",
};
if (hexToRGBMapping?.[hexCode] === undefined)
throw new Error(
"The provided hex code is not valid or is not supported by this function"
);
// Extract the RGB color elements as a single string
// The individual color elements are separated by commas
return (hexToRGBMapping?.[hexCode]).slice(4, -1);
};
/**
* Concatenates metadata properties into a single string with an ampersand as the delimiter
* @param {Array} metadataPropertiesArr An array of metadata property strings
* @returns {String} A string made up of combined metadata properties delimited by an ampersand
*/
const createCombinedTextDelimitedByAmpersand = function (
metadataPropertiesArr
) {
return metadataPropertiesArr.join(" & ");
};
/**
* Concatenates metadata properties into a single string with a comma as the delimiter
* @param {Array} metadataPropertiesArr An array of metadata property strings
* @returns {String} A string made up of combined metadata properties delimited by a comma
*/
const createCombinedTextDelimitedByComma = function (metadataPropertiesArr) {
return metadataPropertiesArr.join(", ");
};
/**
* 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()
);
};
export {
chartExportOptions,
createCombinedTextDelimitedByAmpersand,
createCombinedTextDelimitedByComma,
convertHexColorToRGBColor,
extractSamplingRateFromDatastreamName,
};
"use strict"; "use strict";
import { chartExportOptions } from "./chartExport.mjs"; import {
chartExportOptions,
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
...@@ -17,27 +20,13 @@ const formatSensorThingsApiResponseForLineChart = function (obsArray) { ...@@ -17,27 +20,13 @@ const formatSensorThingsApiResponseForLineChart = function (obsArray) {
}); });
}; };
/**
* 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 * 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 * @param {Array} phenomenonNamesArr An array of phenomenon name strings
* @returns {String} A string of comma separated metadata property strings * @returns {String} A string made up of combined phenomenon names
*/ */
const createCombinedTextForLineChartTitles = function ( const createCombinedTextForLineChartTitles = function (phenomenonNamesArr) {
datastreamMetadataPropArr return phenomenonNamesArr.join(", ");
) {
return datastreamMetadataPropArr.join(", ");
}; };
/** /**
...@@ -50,17 +39,25 @@ const createSeriesOptionsForLineChart = function ( ...@@ -50,17 +39,25 @@ const createSeriesOptionsForLineChart = function (
formattedObsArraysForLineChart, formattedObsArraysForLineChart,
phenomenonNamesArr phenomenonNamesArr
) { ) {
// An array of colors provided by the Highcharts object // An array of colors, in hexadecimal format, provided by the global Highcharts object
const seriesColors = Highcharts.getOptions().colors; const seriesColors = Highcharts.getOptions().colors;
// Create a copy of the colors array
const seriesColorsArr = [...seriesColors];
// Create an array of seriesOptions objects // 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 phenomenon names array are of equal length
// Use one of the arrays for looping // Use one of the arrays for looping
if (formattedObsArraysForLineChart.length !== phenomenonNamesArr.length)
throw new Error(
"The observations array and phenomenon names array have different lengths"
);
return formattedObsArraysForLineChart.map((formattedObsArray, i) => { return formattedObsArraysForLineChart.map((formattedObsArray, i) => {
return { return {
name: `${phenomenonNamesArr[i]}`, name: `${phenomenonNamesArr[i]}`,
data: formattedObsArray, data: formattedObsArray,
color: seriesColors[i], color: seriesColorsArr[i],
turboThreshold: Number.MAX_VALUE, // #3404, remove after 4.0.5 release turboThreshold: Number.MAX_VALUE, // #3404, remove after 4.0.5 release
}; };
}); });
...@@ -83,6 +80,14 @@ const drawLineChartHighcharts = function ( ...@@ -83,6 +80,14 @@ const drawLineChartHighcharts = function (
unitOfMeasurementSymbolsArr, unitOfMeasurementSymbolsArr,
} = extractedFormattedDatastreamProperties; } = extractedFormattedDatastreamProperties;
// Chart title and subtitle text
const textChartTitle =
createCombinedTextForLineChartTitles(phenomenonNamesArr);
const textChartSubtitle = `Sampling rate(s): ${createCombinedTextForLineChartTitles(
extractSamplingRateFromDatastreamName(datastreamNamesArr)
)}`;
// Create the array of series options object(s) // Create the array of series options object(s)
const seriesOptionsArr = createSeriesOptionsForLineChart( const seriesOptionsArr = createSeriesOptionsForLineChart(
formattedObsArraysForLineChart, formattedObsArraysForLineChart,
...@@ -100,14 +105,12 @@ const drawLineChartHighcharts = function ( ...@@ -100,14 +105,12 @@ const drawLineChartHighcharts = function (
}, },
title: { title: {
text: createCombinedTextForLineChartTitles(phenomenonNamesArr), text: textChartTitle,
"align": "left", "align": "left",
}, },
subtitle: { subtitle: {
text: `Sampling rate(s): ${createCombinedTextForLineChartTitles( text: textChartSubtitle,
extractSamplingRateFromDatastreamName(datastreamNamesArr)
)}`,
align: "left", align: "left",
}, },
......
"use strict"; "use strict";
import { chartExportOptions } from "./chartExport.mjs"; import {
chartExportOptions,
convertHexColorToRGBColor,
createCombinedTextDelimitedByAmpersand,
createCombinedTextDelimitedByComma,
extractSamplingRateFromDatastreamName,
} from "./chartHelpers.mjs";
/** /**
* Determines the timestamps that are missing from a smaller set of observations. Based on the comparison of two observation arrays, where one array is larger than the other * Determines the timestamps that are missing from a smaller set of observations. Based on the comparison of two observation arrays, where one array is larger than the other
...@@ -12,15 +18,13 @@ const getSymmetricDifferenceBetweenArrays = function ( ...@@ -12,15 +18,13 @@ const getSymmetricDifferenceBetweenArrays = function (
obsTimestampArrayOne, obsTimestampArrayOne,
obsTimestampArrayTwo obsTimestampArrayTwo
) { ) {
const differenceBetweenArrays = obsTimestampArrayOne return obsTimestampArrayOne
.filter((timestampOne) => !obsTimestampArrayTwo.includes(timestampOne)) .filter((timestampOne) => !obsTimestampArrayTwo.includes(timestampOne))
.concat( .concat(
obsTimestampArrayTwo.filter( obsTimestampArrayTwo.filter(
(timestampTwo) => !obsTimestampArrayOne.includes(timestampTwo) (timestampTwo) => !obsTimestampArrayOne.includes(timestampTwo)
) )
); );
return differenceBetweenArrays;
}; };
/** /**
...@@ -33,11 +37,9 @@ const getIndexesOfUniqueObservations = function ( ...@@ -33,11 +37,9 @@ const getIndexesOfUniqueObservations = function (
uniqueTimestampsArr, uniqueTimestampsArr,
largerObsTimestampArr largerObsTimestampArr
) { ) {
const indexesMissingObs = uniqueTimestampsArr.map((index) => return uniqueTimestampsArr.map((index) =>
largerObsTimestampArr.indexOf(index) largerObsTimestampArr.indexOf(index)
); );
return indexesMissingObs;
}; };
/** /**
...@@ -213,11 +215,134 @@ const formatSensorThingsApiResponseForScatterPlot = function ( ...@@ -213,11 +215,134 @@ const formatSensorThingsApiResponseForScatterPlot = function (
return createCombinedObservationValues(obsArrayOne, obsArrayTwo); return createCombinedObservationValues(obsArrayOne, obsArrayTwo);
}; };
/**
* Concatenates metadata properties to create a string for either the title or subtitle of a scatter plot
* @param {Array} phenomenonNamesArr An array of phenomenon name strings
* @returns {String} A string made up of combined phenomenon names
*/
const createCombinedTextForScatterPlotTitles = function (phenomenonNamesArr) {
// x-axis phenomenon name is the first element of array
const phenomenonNameXAxis = phenomenonNamesArr[0];
// y-axis phenomenon name(s) array is remaining elements of array
const phenomenonNamesYAxisArr = phenomenonNamesArr.slice(1);
// Use a set to remove duplicates
const uniquePhenomenonNamesYAxis = new Set(phenomenonNamesYAxisArr);
const uniquePhenomenonNamesYAxisArr = [...uniquePhenomenonNamesYAxis];
return `${createCombinedTextDelimitedByAmpersand(
uniquePhenomenonNamesYAxisArr
)} versus ${phenomenonNameXAxis}`;
};
/**
* Create string for the x-axis title of a scatter plot
* @param {Array} phenomenonNamesArr Array of phenomenon name strings
* @param {Array} unitOfMeasurementSymbolsArr rray of unit of measurement symbol strings
* @returns {String} X-axis title string for scatter plot
*/
const createXAxisTitleTextScatterPlot = function (
phenomenonNamesArr,
unitOfMeasurementSymbolsArr
) {
// x-axis phenomenon name string is first element of array
const phenomenonNameXAxis = phenomenonNamesArr[0];
// x-axis phenomenon symbol string is first element of array
const unitOfMeasurementSymbolXAxis = unitOfMeasurementSymbolsArr[0];
return `${phenomenonNameXAxis} [${unitOfMeasurementSymbolXAxis}]`;
};
/**
* Create string for the y-axis title of a scatter plot
* @param {Array} phenomenonNamesArr Array of phenomenon name strings
* @param {Array} unitOfMeasurementSymbolsArr Array of unit of measurement symbol strings
* @returns {String} Y-axis title string for scatter plot
*/
const createYAxisTitleTextScatterPlot = function (
phenomenonNamesArr,
unitOfMeasurementSymbolsArr
) {
// y-axis phenomenon names start at array index 1
const phenomenonNamesYAxisArr = phenomenonNamesArr.slice(1);
// y-axis phenomenon symbols start at array index 1
const unitOfMeasurementSymbolsYAxisArr = unitOfMeasurementSymbolsArr.slice(1);
// The phenomenon names and unit of measurement arrays should have equal lengths
// Use one of the arrays for looping
if (
phenomenonNamesYAxisArr.length !== unitOfMeasurementSymbolsYAxisArr.length
)
throw new Error(
"The phenomenon names array and unit of measurement symbols array have different lengths"
);
const combinedNameSymbolArr = phenomenonNamesYAxisArr.map(
(phenomenonNameYAxis, i) =>
`${phenomenonNameYAxis} [${unitOfMeasurementSymbolsYAxisArr[i]}]`
);
return createCombinedTextDelimitedByComma(combinedNameSymbolArr);
};
/**
* Create an options object for each series drawn in the scatter plot
* @param {Array} formattedObsArraysForScatterPlot An array of formatted observation array(s) from one or more datastreams
* @param {Array} phenomenonNamesArr An array of phenomenon name(s)
* @returns {Array} An array made up of series options object(s)
*/
const createSeriesOptionsForScatterPlot = function (
formattedObsArraysForScatterPlot,
phenomenonNamesArr
) {
// An array of colors, in hexadecimal format, provided by the global Highcharts object
const highchartsColorsArr = Highcharts.getOptions().colors;
// Create a reversed copy of the colors array
const highchartsColorsReversedArr = [...highchartsColorsArr].reverse();
// Opacity value for symbol
const SERIES_SYMBOL_COLOR_OPACITY = ".3";
// Create array of colors in RGBA format
const seriesColors = highchartsColorsReversedArr.map(
(hexColorCode) =>
`rgba(${convertHexColorToRGBColor(
hexColorCode
)}, ${SERIES_SYMBOL_COLOR_OPACITY})`
);
// x-axis phenomenon name is the first element of array
const phenomenonNameXAxis = phenomenonNamesArr[0];
// y-axis phenomenon name(s) array is remaining elements of array
const phenomenonNamesYAxisArr = phenomenonNamesArr.slice(1);
// Create an array of seriesOptions objects
// Assumes that the observation array of arrays and phenomenon names array are of equal length
// Use one of the arrays for looping
if (
formattedObsArraysForScatterPlot.length !== phenomenonNamesYAxisArr.length
)
throw new Error(
"The observations array and phenomenon names array have different lengths"
);
return formattedObsArraysForScatterPlot.map((formattedObsArray, i) => {
return {
name: `${phenomenonNamesYAxisArr[i]}, ${phenomenonNameXAxis}`,
data: formattedObsArray,
color: seriesColors[i],
};
});
};
/** /**
* Draw a scatter plot using Highcharts library * Draw a scatter plot using Highcharts library
* @param {Array} formattedObsArrayForSeriesOnePlusSeriesTwo Response from SensorThings API formatted for use in a scatter plot * @param {Array} formattedObsArrayForSeriesOnePlusSeriesTwo Response from SensorThings API formatted for use in a scatter plot
* @param {Object} extractedFormattedDatastreamProperties An object that contains arrays of formatted Datastream properties * @param {Object} extractedFormattedDatastreamProperties An object that contains arrays of formatted Datastream properties
* @returns {undefined} * @returns {undefined} undefined
*/ */
const drawScatterPlotHighcharts = function ( const drawScatterPlotHighcharts = function (
formattedObsArrayForSeriesOnePlusSeriesTwo, formattedObsArrayForSeriesOnePlusSeriesTwo,
...@@ -231,32 +356,32 @@ const drawScatterPlotHighcharts = function ( ...@@ -231,32 +356,32 @@ const drawScatterPlotHighcharts = function (
unitOfMeasurementSymbolsArr, unitOfMeasurementSymbolsArr,
} = extractedFormattedDatastreamProperties; } = extractedFormattedDatastreamProperties;
const [DATASTREAM_DESCRIPTION_SERIES_1, DATASTREAM_DESCRIPTION_SERIES_2] = // Create the array of series options object(s)
datastreamDescriptionsArr; const seriesOptionsArr = createSeriesOptionsForScatterPlot(
const [DATASTREAM_NAME_SERIES_1, DATASTREAM_NAME_SERIES_2] = formattedObsArrayForSeriesOnePlusSeriesTwo,
datastreamNamesArr; phenomenonNamesArr
const [PHENOMENON_NAME_SERIES_1, PHENOMENON_NAME_SERIES_2] = );
phenomenonNamesArr;
const [PHENOMENON_SYMBOL_SERIES_1, PHENOMENON_SYMBOL_SERIES_2] =
unitOfMeasurementSymbolsArr;
// Order of axes const CHART_TITLE =
// Y-Axis -- Series 2 createCombinedTextForScatterPlotTitles(phenomenonNamesArr);
// X-Axis -- Series 1
const CHART_TITLE = `${PHENOMENON_NAME_SERIES_2} Versus ${PHENOMENON_NAME_SERIES_1}`; const CHART_SUBTITLE = `Sampling rate(s): ${createCombinedTextDelimitedByComma(
const CHART_SUBTITLE = `Source: ${DATASTREAM_NAME_SERIES_2} & ${DATASTREAM_NAME_SERIES_1}`; extractSamplingRateFromDatastreamName(datastreamNamesArr)
)}`;
const SERIES_1_NAME = `${PHENOMENON_NAME_SERIES_1}`; const X_AXIS_TITLE = createXAxisTitleTextScatterPlot(
const SERIES_1_SYMBOL = `${PHENOMENON_SYMBOL_SERIES_1}`; phenomenonNamesArr,
unitOfMeasurementSymbolsArr
);
const SERIES_2_NAME = `${PHENOMENON_NAME_SERIES_2}`; const Y_AXIS_TITLE = createYAxisTitleTextScatterPlot(
const SERIES_2_SYMBOL = `${PHENOMENON_SYMBOL_SERIES_2}`; phenomenonNamesArr,
unitOfMeasurementSymbolsArr
);
const SERIES_COMBINED_NAME = "Y, X"; // The unit of measurement symbols for the x-axis is the first element of the array
const SERIES_COMBINED_SYMBOL_COLOR_RGB_ELEMENTS = "223, 83, 83"; // Assume that we will be comparing similar phenomena, so we can reuse this symbol
const SERIES_COMBINED_SYMBOL_COLOR_OPACITY = ".3"; const UNIT_OF_MEASUREMENT_SYMBOL = unitOfMeasurementSymbolsArr[0];
const SERIES_COMBINED_SYMBOL_COLOR = `rgba(${SERIES_COMBINED_SYMBOL_COLOR_RGB_ELEMENTS}, ${SERIES_COMBINED_SYMBOL_COLOR_OPACITY})`;
const MARKER_RADIUS = 2; const MARKER_RADIUS = 2;
...@@ -273,10 +398,12 @@ const drawScatterPlotHighcharts = function ( ...@@ -273,10 +398,12 @@ const drawScatterPlotHighcharts = function (
title: { title: {
text: CHART_TITLE, text: CHART_TITLE,
"align": "left",
}, },
subtitle: { subtitle: {
text: CHART_SUBTITLE, text: CHART_SUBTITLE,
"align": "left",
}, },
xAxis: { xAxis: {
...@@ -285,7 +412,7 @@ const drawScatterPlotHighcharts = function ( ...@@ -285,7 +412,7 @@ const drawScatterPlotHighcharts = function (
}, },
title: { title: {
enabled: true, enabled: true,
text: `${SERIES_1_NAME} [${SERIES_1_SYMBOL}]`, text: X_AXIS_TITLE,
}, },
startOnTick: true, startOnTick: true,
endOnTick: true, endOnTick: true,
...@@ -298,7 +425,7 @@ const drawScatterPlotHighcharts = function ( ...@@ -298,7 +425,7 @@ const drawScatterPlotHighcharts = function (
format: `{value}`, format: `{value}`,
}, },
title: { title: {
text: `${SERIES_2_NAME} [${SERIES_2_SYMBOL}]`, text: Y_AXIS_TITLE,
}, },
}, },
], ],
...@@ -333,22 +460,16 @@ const drawScatterPlotHighcharts = function ( ...@@ -333,22 +460,16 @@ const drawScatterPlotHighcharts = function (
const headerString = `${this.series.name}<br>`; const headerString = `${this.series.name}<br>`;
const pointString = `<b>${this.point.y.toFixed( const pointString = `<b>${this.point.y.toFixed(
2 2
)} ${SERIES_1_SYMBOL}, ${this.point.x.toFixed( )} ${UNIT_OF_MEASUREMENT_SYMBOL}, ${this.point.x.toFixed(
2 2
)} ${SERIES_2_SYMBOL}</b>`; )} ${UNIT_OF_MEASUREMENT_SYMBOL}</b>`;
return headerString + pointString; return headerString + pointString;
}, },
}, },
exporting: chartExportOptions, exporting: chartExportOptions,
series: [ series: seriesOptionsArr,
{
name: SERIES_COMBINED_NAME,
color: SERIES_COMBINED_SYMBOL_COLOR,
data: formattedObsArrayForSeriesOnePlusSeriesTwo,
},
],
}); });
}; };
......
...@@ -271,6 +271,7 @@ const getObservationsFromMultipleDatastreams = async function ( ...@@ -271,6 +271,7 @@ const getObservationsFromMultipleDatastreams = async function (
/** /**
* Retrieve the metadata from a single Datastream or multiple Datastreams and the Observations corresponding to the Datastream(s) * Retrieve the metadata from a single Datastream or multiple Datastreams and the Observations corresponding to the Datastream(s)
* @async
* @param {String} baseUrl Base URL of the STA server * @param {String} baseUrl Base URL of the STA server
* @param {Object} urlParamObj The URL parameters to be sent together with the GET request * @param {Object} urlParamObj The URL parameters to be sent together with the GET request
* @param {Array} bldgSensorSamplingRateNestedArr 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 * @param {Array} bldgSensorSamplingRateNestedArr 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
......
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