"use strict"; const chartExportOptions = { buttons: { contextButton: { menuItems: ["downloadPNG", "downloadJPEG", "downloadPDF", "downloadSVG"], }, }, }; /** * 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 * @param {Array} obsTimestampArrayOne An array of timestamps for the first set of observations * @param {Array} obsTimestampArrayTwo An array of timstamps for the second set of observations * @returns {Array} An array of timestamps missing from either set of observations */ const getSymmetricDifferenceBetweenArrays = function ( obsTimestampArrayOne, obsTimestampArrayTwo ) { return obsTimestampArrayOne .filter((timestampOne) => !obsTimestampArrayTwo.includes(timestampOne)) .concat( obsTimestampArrayTwo.filter( (timestampTwo) => !obsTimestampArrayOne.includes(timestampTwo) ) ); }; /** * Determines the indexes of timestamps that are unique to the larger set of observatiuons. Based on the comparison of two observation arrays, where one array is larger than the other * @param {Array} uniqueTimestampsArr An array of timestamps unique to the larger set of observations * @param {Array} largerObsTimestampArr An array of timestamps for the larger set of observations * @returns {Array} An array of the indexes of the missing observations */ const getIndexesOfUniqueObservations = function ( uniqueTimestampsArr, largerObsTimestampArr ) { return uniqueTimestampsArr.map((index) => largerObsTimestampArr.indexOf(index) ); }; /** * Removes observations (by modifying array in place) that are unique to a larger set of observations. Based on the comparison of two observation arrays, where one array is larger than the other * @param {Array} uniqueIndexesArr An array of the indexes unique to the larger set of observations * @param {Array} largerObsArr The larger array of observations (timestamp + value) * @returns {Array} The larger array with the unique indexes removed */ const removeUniqueObservationsFromLargerArray = function ( uniqueIndexesArr, largerObsArr ) { // Create a reversed copy of the indexes array, so that the larger index is removed first const uniqueIndexesReversedCopyArr = [...uniqueIndexesArr].reverse(); // Create a copy of the larger observation array, will be modified in place const largerObsCopyArr = [...largerObsArr]; uniqueIndexesReversedCopyArr.forEach((uniqueIndex) => { if (uniqueIndex > -1) { largerObsCopyArr.splice(uniqueIndex, 1); } }); return largerObsCopyArr; }; /** * Compares the length of two input arrays to determine the larger one * @param {Array} firstArr First input array * @param {Array} secondArr Second input array * @returns {Array} The larger array */ const getLargerArrayBetweenTwoInputArrays = function (firstArr, secondArr) { if (firstArr.length === secondArr.length) return; else if (firstArr.length > secondArr.length) { return firstArr; } else if (firstArr.length < secondArr.length) { return secondArr; } }; /** * Compares the length of two input arrays to determine the smaller one * @param {Array} firstArr First input array * @param {Array} secondArr Second input array * @returns {Array} The smaller array */ const getSmallerArrayBetweenTwoInputArrays = function (firstArr, secondArr) { if (firstArr.length === secondArr.length) return; else if (firstArr.length < secondArr.length) { return firstArr; } else if (firstArr.length > secondArr.length) { return secondArr; } }; /** * Utility function for deleting the unique observations from a larger array * @param {Array} obsArrayOne Array of observations (timestamp + value) that is response from SensorThings API * @param {Array} obsArrayTwo Array of observations (timestamp + value) that is response from SensorThings API * @returns {Array} Two arrays of observations (timestamp + value) with matching timestamps and equal lengths */ const deleteUniqueObservationsFromLargerArray = function ( obsArrayOne, obsArrayTwo ) { // Create arrays with timestamps only const obsArrayOneTimestamp = obsArrayOne.map( (obsTimeValue) => obsTimeValue[0] ); const obsArrayTwoTimestamp = obsArrayTwo.map( (obsTimeValue) => obsTimeValue[0] ); const missingTimestamp = getSymmetricDifferenceBetweenArrays( obsArrayOneTimestamp, obsArrayTwoTimestamp ); // Determine the larger observation timestamp array const biggerObsTimestampArr = getLargerArrayBetweenTwoInputArrays( obsArrayOneTimestamp, obsArrayTwoTimestamp ); // Indexes of the missing observations const indexesMissingObsArr = getIndexesOfUniqueObservations( missingTimestamp, biggerObsTimestampArr ); // Determine the larger observation array const biggerObsArr = getLargerArrayBetweenTwoInputArrays( obsArrayOne, obsArrayTwo ); // Determine the smaller observation array const smallerObsArr = getSmallerArrayBetweenTwoInputArrays( obsArrayOne, obsArrayTwo ); // Remove the missing observation from the larger array of observations const modifiedBiggerObsArr = removeUniqueObservationsFromLargerArray( indexesMissingObsArr, biggerObsArr ); return [modifiedBiggerObsArr, smallerObsArr]; }; /** * Utility function for deleting the unique observations from a larger array AND ensuring the order of input arrays is maintained * @param {Array} obsArrayOne Array of observations (timestamp + value) that is response from SensorThings API * @param {Array} obsArrayTwo Array of observations (timestamp + value) that is response from SensorThings API * @returns {Array} Two arrays of observations (timestamp + value) with matching timestamps and equal lengths */ const checkForAndDeleteUniqueObservationsFromLargerArray = function ( obsArrayOne, obsArrayTwo ) { if (obsArrayOne.length === obsArrayTwo.length) return; // Case 1: obsArrayTwo larger than obsArrayOne else if (obsArrayOne.length < obsArrayTwo.length) { const [biggerObsArr, smallerObsArr] = deleteUniqueObservationsFromLargerArray(obsArrayOne, obsArrayTwo); return [smallerObsArr, biggerObsArr]; } // Case 2: obsArrayOne larger than obsArrayTwo else if (obsArrayOne.length > obsArrayTwo.length) { return deleteUniqueObservationsFromLargerArray(obsArrayOne, obsArrayTwo); } }; /** * 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 else { 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) { // First split the Datastream name string based on a single space (" "). // The sampling rate string is the last word in the resulting string. // We then split the resulting string using the ':' character. // Our interest is also in the last word in the resulting string return datastreamNamesArr.map((datastreamName) => datastreamName.split(" ").pop().split(":").pop() ); }; /** * Extract the building ID substring from a datastream name string * * @param {Array} datastreamNamesArr An array of datastream name(s) * @returns {Array} An array containing the building ID substring(s) */ const extractBuildingIdFromDatastreamName = function (datastreamNamesArr) { // The building ID string is the first word in the Datastream name string return datastreamNamesArr.map((datastreamName) => datastreamName.split(" ").shift() ); }; /** * 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 else { 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} phenomenonNamesArr An array of phenomenon 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 else { return `${createPartialTitleForLineOrColumnChart( aggregationInterval, aggregationType )}: ${createCombinedTextDelimitedByComma(phenomenonNamesArr)}`; } }; /** * Create a title for a heatmap * * @param {Array} phenomenonNamesArr An array of phenomenon names as strings * @returns {String} A string that represents the heatmap title */ const createTitleForHeatmap = function (phenomenonNamesArr) { return createCombinedTextDelimitedByComma(phenomenonNamesArr); }; /** * Create a subtitle for the following charts: column chart, line chart and scatter plot * * @param {Array} datastreamNamesArr An array of datastream name(s) * @returns {String} A subtitle string */ const createSubtitleForChart = function (datastreamNamesArr) { // Case 1: We only have one sampling rate string if (datastreamNamesArr.length === 1) { return `Sampling rate: ${createCombinedTextDelimitedByComma( extractSamplingRateFromDatastreamName(datastreamNamesArr) )}`; } // Case 2: We have more than one sampling rate string else if (datastreamNamesArr.length > 1) { return `Sampling rate(s): ${createCombinedTextDelimitedByComma( extractSamplingRateFromDatastreamName(datastreamNamesArr) )} respectively`; } }; /** * Create a subtitle for a heatmap which is different from the subtitles used for the other * types of charts, i.e. column charts, line charts and scatter plots * * @param {Array} datastreamNamesArr An array of datastream name(s) * @returns {String} A subtitle string */ const createSubtitleForHeatmap = function (datastreamNamesArr) { // Note: the `datastreamNamesArr` here contains only one element // We use the `createCombinedTextDelimitedByComma` function to "spread" the resulting arrays return `Building, Sampling rate: ${createCombinedTextDelimitedByComma( extractBuildingIdFromDatastreamName(datastreamNamesArr) )}, ${createCombinedTextDelimitedByComma( extractSamplingRateFromDatastreamName(datastreamNamesArr) )}`; }; /** * Abbreviate temperature phenomenon names for use in chart y-axis title strings where space is limited * * @param {Array} phenomenonNamesArr An array of phenomenon name strings * @returns {Array} An array that contains abbreviated temperature phenomenon strings */ const abbreviateTemperaturePhenomenonNames = function (phenomenonNamesArr) { // We're interested in phenomenon names that contain the substrings // `temperature` or `Temperature` return phenomenonNamesArr.map((phenomenonName) => { // Case 1: Temperature phenomenon name string variant 1 if (phenomenonName.includes("temperature")) { return phenomenonName.replace("temperature", "temp."); } // Case 2: Temperature phenomenon name string variant 2 else if (phenomenonName.includes("Temperature")) { return phenomenonName.replace("Temperature", "Temp."); } // Case 3: The other phenomenon name strings else { return phenomenonName; } }); }; /** * 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)}`; }; /** * 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, checkForAndDeleteUniqueObservationsFromLargerArray, createCombinedTextDelimitedByAmpersand, createCombinedTextDelimitedByComma, extractSamplingRateFromDatastreamName, extractBuildingIdFromDatastreamName, createFullTitleForLineOrColumnChart, createTitleForHeatmap, createSubtitleForChart, createSubtitleForHeatmap, abbreviateTemperaturePhenomenonNames, createTooltipDateString, convertHexColorToRGBColor, removeTransparencyFromColor, };