"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, };