Commit 9e9002e7 authored by Pithon Kabiro's avatar Pithon Kabiro
Browse files

Merge branch 'wip_scatter-plot-3' into 'master'

Update scatter plot chart

Scatter plot now supports visualization of more than two phenomena

See merge request !9
parents 4ae783c2 233f7b2c
This diff is collapsed.
"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 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);
};
import {
extractObservationValuesWithinDatesInterval,
extractObservationValuesWithinMonthInterval,
} from "./aggregateHelpers.mjs";
/**
* 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 (
);
};
/**
* 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
* @param {Array} obsValuesForMonthIntervalArr An array of observation values that fall within one calendar month
......@@ -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 {
calculateSumOfObservationValuesWithinInterval,
extractUniqueCalendarDatesFromTimestamp,
extractUniqueCalendarMonthsFromCalendarDates,
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";
import { chartExportOptions } from "./chartExport.mjs";
import { chartExportOptions } from "./chartHelpers.mjs";
/**
* Format a computed aggregation result to make it suitable for a column chart
......@@ -42,6 +42,13 @@ const createSeriesOptionsForColumnChart = function (
// 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
if (
formattedAggregatedResultForColumnChart.length !== phenomenonNamesArr.length
)
throw new Error(
"The observations array and phenomenon names array have different lengths"
);
return formattedAggregatedResultForColumnChart.map(
(formattedAggResArray, i) => {
return {
......@@ -57,7 +64,7 @@ const createSeriesOptionsForColumnChart = function (
* 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 {Object} extractedFormattedDatastreamProperties An object that contains arrays of formatted Datastream properties
* @returns {undefined}
* @returns {undefined} undefined
*/
const drawColumnChartHighcharts = function (
formattedAggResultArraysForColumnChart,
......
"use strict";
export const chartExportOptions = {
buttons: {
contextButton: {
menuItems: ["downloadPNG", "downloadJPEG", "downloadPDF", "downloadSVG"],
},
},
};
"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
......
"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";
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
......@@ -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
* @param {Array} datastreamMetadataPropArr An array of metadata property strings
* @returns {String} A string of comma separated metadata property strings
* @param {Array} phenomenonNamesArr An array of phenomenon name strings
* @returns {String} A string made up of combined phenomenon names
*/
const createCombinedTextForLineChartTitles = function (
datastreamMetadataPropArr
) {
return datastreamMetadataPropArr.join(", ");
const createCombinedTextForLineChartTitles = function (phenomenonNamesArr) {
return phenomenonNamesArr.join(", ");
};
/**
......@@ -50,17 +39,25 @@ const createSeriesOptionsForLineChart = function (
formattedObsArraysForLineChart,
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;
// Create a copy of the colors array
const seriesColorsArr = [...seriesColors];
// 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 (formattedObsArraysForLineChart.length !== phenomenonNamesArr.length)
throw new Error(
"The observations array and phenomenon names array have different lengths"
);
return formattedObsArraysForLineChart.map((formattedObsArray, i) => {
return {
name: `${phenomenonNamesArr[i]}`,
data: formattedObsArray,
color: seriesColors[i],
color: seriesColorsArr[i],
turboThreshold: Number.MAX_VALUE, // #3404, remove after 4.0.5 release
};
});
......@@ -83,6 +80,14 @@ const drawLineChartHighcharts = function (
unitOfMeasurementSymbolsArr,
} = 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)
const seriesOptionsArr = createSeriesOptionsForLineChart(
formattedObsArraysForLineChart,
......@@ -100,14 +105,12 @@ const drawLineChartHighcharts = function (
},
title: {
text: createCombinedTextForLineChartTitles(phenomenonNamesArr),
text: textChartTitle,
"align": "left",
},
subtitle: {
text: `Sampling rate(s): ${createCombinedTextForLineChartTitles(
extractSamplingRateFromDatastreamName(datastreamNamesArr)
)}`,
text: textChartSubtitle,
align: "left",
},
......
"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
......@@ -12,15 +18,13 @@ const getSymmetricDifferenceBetweenArrays = function (
obsTimestampArrayOne,
obsTimestampArrayTwo
) {
const differenceBetweenArrays = obsTimestampArrayOne
return obsTimestampArrayOne
.filter((timestampOne) => !obsTimestampArrayTwo.includes(timestampOne))
.concat(
obsTimestampArrayTwo.filter(
(timestampTwo) => !obsTimestampArrayOne.includes(timestampTwo)
)
);
return differenceBetweenArrays;
};
/**
......@@ -33,11 +37,9 @@ const getIndexesOfUniqueObservations = function (
uniqueTimestampsArr,
largerObsTimestampArr
) {
const indexesMissingObs = uniqueTimestampsArr.map((index) =>
return uniqueTimestampsArr.map((index) =>
largerObsTimestampArr.indexOf(index)
);
return indexesMissingObs;
};
/**
......@@ -213,11 +215,134 @@ const formatSensorThingsApiResponseForScatterPlot = function (
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
* @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
* @returns {undefined}
* @returns {undefined} undefined
*/
const drawScatterPlotHighcharts = function (
formattedObsArrayForSeriesOnePlusSeriesTwo,
......@@ -231,32 +356,32 @@ const drawScatterPlotHighcharts = function (
unitOfMeasurementSymbolsArr,
} = extractedFormattedDatastreamProperties;
const [DATASTREAM_DESCRIPTION_SERIES_1, DATASTREAM_DESCRIPTION_SERIES_2] =
datastreamDescriptionsArr;
const [DATASTREAM_NAME_SERIES_1, DATASTREAM_NAME_SERIES_2] =
datastreamNamesArr;
const [PHENOMENON_NAME_SERIES_1, PHENOMENON_NAME_SERIES_2] =
phenomenonNamesArr;
const [PHENOMENON_SYMBOL_SERIES_1, PHENOMENON_SYMBOL_SERIES_2] =
unitOfMeasurementSymbolsArr;
// Create the array of series options object(s)
const seriesOptionsArr = createSeriesOptionsForScatterPlot(
formattedObsArrayForSeriesOnePlusSeriesTwo,
phenomenonNamesArr
);
// Order of axes
// Y-Axis -- Series 2
// X-Axis -- Series 1
const CHART_TITLE =
createCombinedTextForScatterPlotTitles(phenomenonNamesArr);
const CHART_TITLE = `${PHENOMENON_NAME_SERIES_2} Versus ${PHENOMENON_NAME_SERIES_1}`;
const CHART_SUBTITLE = `Source: ${DATASTREAM_NAME_SERIES_2} & ${DATASTREAM_NAME_SERIES_1}`;
const CHART_SUBTITLE = `Sampling rate(s): ${createCombinedTextDelimitedByComma(
extractSamplingRateFromDatastreamName(datastreamNamesArr)
)}`;
const SERIES_1_NAME = `${PHENOMENON_NAME_SERIES_1}`;
const SERIES_1_SYMBOL = `${PHENOMENON_SYMBOL_SERIES_1}`;
const X_AXIS_TITLE = createXAxisTitleTextScatterPlot(
phenomenonNamesArr,
unitOfMeasurementSymbolsArr
);
const SERIES_2_NAME = `${PHENOMENON_NAME_SERIES_2}`;
const SERIES_2_SYMBOL = `${PHENOMENON_SYMBOL_SERIES_2}`;
const Y_AXIS_TITLE = createYAxisTitleTextScatterPlot(
phenomenonNamesArr,
unitOfMeasurementSymbolsArr
);
const SERIES_COMBINED_NAME = "Y, X";
const SERIES_COMBINED_SYMBOL_COLOR_RGB_ELEMENTS = "223, 83, 83";
const SERIES_COMBINED_SYMBOL_COLOR_OPACITY = ".3";
const SERIES_COMBINED_SYMBOL_COLOR = `rgba(${SERIES_COMBINED_SYMBOL_COLOR_RGB_ELEMENTS}, ${SERIES_COMBINED_SYMBOL_COLOR_OPACITY})`;
// The unit of measurement symbols for the x-axis is the first element of the array
// Assume that we will be comparing similar phenomena, so we can reuse this symbol
const UNIT_OF_MEASUREMENT_SYMBOL = unitOfMeasurementSymbolsArr[0];
const MARKER_RADIUS = 2;
......@@ -273,10 +398,12 @@ const drawScatterPlotHighcharts = function (
title: {
text: CHART_TITLE,
"align": "left",
},
subtitle: {
text: CHART_SUBTITLE,
"align": "left",
},
xAxis: {
......@@ -285,7 +412,7 @@ const drawScatterPlotHighcharts = function (
},
title: {
enabled: true,
text: `${SERIES_1_NAME} [${SERIES_1_SYMBOL}]`,
text: X_AXIS_TITLE,
},
startOnTick: true,
endOnTick: true,
......@@ -298,7 +425,7 @@ const drawScatterPlotHighcharts = function (
format: `{value}`,
},
title: {
text: `${SERIES_2_NAME} [${SERIES_2_SYMBOL}]`,
text: Y_AXIS_TITLE,
},
},
],
......@@ -333,22 +460,16 @@ const drawScatterPlotHighcharts = function (
const headerString = `${this.series.name}<br>`;
const pointString = `<b>${this.point.y.toFixed(
2
)} ${SERIES_1_SYMBOL}, ${this.point.x.toFixed(
)} ${UNIT_OF_MEASUREMENT_SYMBOL}, ${this.point.x.toFixed(
2
)} ${SERIES_2_SYMBOL}</b>`;
)} ${UNIT_OF_MEASUREMENT_SYMBOL}</b>`;
return headerString + pointString;
},
},
exporting: chartExportOptions,
series: [
{
name: SERIES_COMBINED_NAME,
color: SERIES_COMBINED_SYMBOL_COLOR,
data: formattedObsArrayForSeriesOnePlusSeriesTwo,
},
],
series: seriesOptionsArr,
});
};
......
......@@ -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)
* @async
* @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} 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
......
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