"use strict";
import {
chartExportOptions,
checkForAndDeleteUniqueObservationsFromLargerArray,
convertHexColorToRGBColor,
createCombinedTextDelimitedByAmpersand,
createCombinedTextDelimitedByComma,
abbreviateTemperaturePhenomenonNames,
createSubtitleForChart,
removeTransparencyFromColor,
} from "./chartHelpers.mjs";
/**
* Extracts and combines observation values from two input observation arrays of equal length
* @param {Array} obsArrayOne First set of N observations (timestamp + value)
* @param {Array} obsArrayTwo Second set of N observations (timestamp + value)
* @returns {Array} A N*2 array of observation values from both input observation arrays
*/
const createCombinedObservationValues = function (obsArrayOne, obsArrayTwo) {
// Extract the values from the two observation arrays
const obsValuesOne = obsArrayOne.map((result) => result[1]);
const obsValuesTwo = obsArrayTwo.map((result) => result[1]);
// Since the arrays are of equal length, we need only use one of the arrays for looping
return obsValuesOne.map((obsValOne, i) => [obsValOne, obsValuesTwo[i]]);
};
/**
* Format the response from SensorThings API to make it suitable for use in a scatter plot
* @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} Array of formatted observations suitable for use in a scatter plot
*/
const formatSensorThingsApiResponseForScatterPlot = function (
obsArrayOne,
obsArrayTwo
) {
// When our observation arrays have DIFFERENT lengths
if (obsArrayOne.length !== obsArrayTwo.length) {
const [obsArrayOneFinal, obsArrayTwoFinal] =
checkForAndDeleteUniqueObservationsFromLargerArray(
obsArrayOne,
obsArrayTwo
);
return createCombinedObservationValues(obsArrayOneFinal, obsArrayTwoFinal);
}
// When our observation arrays already have SAME lengths
else {
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"
);
} else {
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"
);
} else {
return formattedObsArraysForScatterPlot.map((formattedObsArray, i) => {
return {
name: `${phenomenonNamesYAxisArr[i]}, ${phenomenonNameXAxis}`,
data: formattedObsArray,
color: seriesColors[i],
};
});
}
};
/**
* Match a scatter plot's y-axis phenomenon name to its corresponding symbol
*
* @param {String} seriesName A string representing a scatter plot's series name. It is made up of two phenomenon names separated by a comma
* @returns {String} The phenomenon's symbol
*/
const getYAxisUnitOfMeasurementSymbol = function (seriesName) {
const phenomenonNameToSymbolMapping = {
temperature: "\u00B0C",
flow: "m\u00B3/h",
power: "kW",
energy: "MWh",
};
// The `series.name` property for the scatter plot is made up of
// two phenomenon names delimited by a comma
// We are interested in the first string
const phenomenonNameYAxis = seriesName.split(",")[0].toLowerCase();
if (phenomenonNameYAxis.includes("temperature")) {
return phenomenonNameToSymbolMapping.temperature;
} else if (phenomenonNameYAxis.includes("flow")) {
return phenomenonNameToSymbolMapping.flow;
} else if (phenomenonNameYAxis.includes("power")) {
return phenomenonNameToSymbolMapping.power;
} else if (phenomenonNameYAxis.includes("energy")) {
return phenomenonNameToSymbolMapping.energy;
}
};
/**
* 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} undefined
*/
const drawScatterPlotHighcharts = function (
formattedObsArrayForSeriesOnePlusSeriesTwo,
extractedFormattedDatastreamProperties
) {
// Arrays of datastream properties
const {
datastreamDescriptionsArr,
datastreamNamesArr,
phenomenonNamesArr,
unitOfMeasurementSymbolsArr,
} = extractedFormattedDatastreamProperties;
// Create the array of series options object(s)
const seriesOptionsArr = createSeriesOptionsForScatterPlot(
formattedObsArrayForSeriesOnePlusSeriesTwo,
phenomenonNamesArr
);
const CHART_TITLE =
createCombinedTextForScatterPlotTitles(phenomenonNamesArr);
const CHART_SUBTITLE = createSubtitleForChart(datastreamNamesArr);
const X_AXIS_TITLE = createXAxisTitleTextScatterPlot(
phenomenonNamesArr,
unitOfMeasurementSymbolsArr
);
const Y_AXIS_TITLE = createYAxisTitleTextScatterPlot(
abbreviateTemperaturePhenomenonNames(phenomenonNamesArr),
unitOfMeasurementSymbolsArr
);
// The unit of measurement symbols for the x-axis is the first element of the array
const unitOfMeasurementXAxisSymbol = unitOfMeasurementSymbolsArr[0];
const MARKER_RADIUS = 2;
Highcharts.chart("chart-scatter-plot", {
chart: {
type: "scatter",
zoomType: "xy",
},
boost: {
useGPUTranslations: true,
usePreAllocated: true,
},
title: {
text: CHART_TITLE,
"align": "center",
},
subtitle: {
text: CHART_SUBTITLE,
"align": "center",
},
xAxis: {
labels: {
format: `{value}`,
},
title: {
enabled: true,
text: X_AXIS_TITLE,
},
startOnTick: true,
endOnTick: true,
showLastLabel: true,
},
yAxis: [
{
labels: {
format: `{value}`,
},
title: {
text: Y_AXIS_TITLE,
},
},
],
legend: {
enabled: false,
},
plotOptions: {
scatter: {
marker: {
radius: MARKER_RADIUS,
states: {
hover: {
enabled: true,
lineColor: "rgb(100,100,100)",
},
},
},
states: {
hover: {
marker: {
enabled: false,
},
},
},
},
},
tooltip: {
formatter() {
// The color contained in the series object is in RGBA format,
// convert it to RGB format so that the text in the tooltip is more legible
const headerString = `${this.series.name}
`;
const pointString = `${this.point.y.toFixed(
2
)} ${getYAxisUnitOfMeasurementSymbol(
this.series.name
)}, ${this.point.x.toFixed(2)} ${unitOfMeasurementXAxisSymbol}`;
return headerString + pointString;
},
},
exporting: chartExportOptions,
series: seriesOptionsArr,
});
};
export {
formatSensorThingsApiResponseForScatterPlot,
drawScatterPlotHighcharts,
};