chartScatterPlot.mjs 11.2 KB
Newer Older
1
2
"use strict";

3
4
import {
  chartExportOptions,
5
  checkForAndDeleteUniqueObservationsFromLargerArray,
6
7
8
  convertHexColorToRGBColor,
  createCombinedTextDelimitedByAmpersand,
  createCombinedTextDelimitedByComma,
9
  abbreviateTemperaturePhenomenonNames,
10
  createSubtitleForChart,
11
  removeTransparencyFromColor,
12
} from "./chartHelpers.mjs";
13

14
15
16
17
18
19
20
21
22
23
24
25
/**
 * 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
Pithon Kabiro's avatar
Pithon Kabiro committed
26
  return obsValuesOne.map((obsValOne, i) => [obsValOne, obsValuesTwo[i]]);
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
};

/**
 * 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
50
51
52
  else {
    return createCombinedObservationValues(obsArrayOne, obsArrayTwo);
  }
53
54
};

55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
/**
 * 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
114
  ) {
115
116
117
    throw new Error(
      "The phenomenon names array and unit of measurement symbols array have different lengths"
    );
118
  } else {
119
120
121
122
123
    const combinedNameSymbolArr = phenomenonNamesYAxisArr.map(
      (phenomenonNameYAxis, i) =>
        `${phenomenonNameYAxis} [${unitOfMeasurementSymbolsYAxisArr[i]}]`
    );

124
125
    return createCombinedTextDelimitedByComma(combinedNameSymbolArr);
  }
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
};

/**
 * 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
165
  ) {
166
167
168
    throw new Error(
      "The observations array and phenomenon names array have different lengths"
    );
169
170
171
172
173
174
175
176
177
  } else {
    return formattedObsArraysForScatterPlot.map((formattedObsArray, i) => {
      return {
        name: `${phenomenonNamesYAxisArr[i]}, ${phenomenonNameXAxis}`,
        data: formattedObsArray,
        color: seriesColors[i],
      };
    });
  }
178
179
};

180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
/**
 * 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;
  }
};

210
211
/**
 * Draw a scatter plot using Highcharts library
212
 * @param {Array} formattedObsArraysForScatterPlot Response from SensorThings API formatted for use in a scatter plot. Currently, only raw observations are supported, i.e. no aggregation
Pithon Kabiro's avatar
Pithon Kabiro committed
213
 * @param {Object} extractedFormattedDatastreamProperties An object that contains arrays of formatted Datastream properties
214
 * @returns {undefined} undefined
215
216
 */
const drawScatterPlotHighcharts = function (
217
  formattedObsArraysForScatterPlot,
Pithon Kabiro's avatar
Pithon Kabiro committed
218
  extractedFormattedDatastreamProperties
219
) {
Pithon Kabiro's avatar
Pithon Kabiro committed
220
  // Arrays of datastream properties
221
  const {
Pithon Kabiro's avatar
Pithon Kabiro committed
222
223
224
225
226
227
    datastreamDescriptionsArr,
    datastreamNamesArr,
    phenomenonNamesArr,
    unitOfMeasurementSymbolsArr,
  } = extractedFormattedDatastreamProperties;

228
229
  // Create the array of series options object(s)
  const seriesOptionsArr = createSeriesOptionsForScatterPlot(
230
    formattedObsArraysForScatterPlot,
231
232
    phenomenonNamesArr
  );
233

234
235
  const CHART_TITLE =
    createCombinedTextForScatterPlotTitles(phenomenonNamesArr);
236

237
  const CHART_SUBTITLE = createSubtitleForChart(datastreamNamesArr);
238

239
240
241
242
  const X_AXIS_TITLE = createXAxisTitleTextScatterPlot(
    phenomenonNamesArr,
    unitOfMeasurementSymbolsArr
  );
243

244
  const Y_AXIS_TITLE = createYAxisTitleTextScatterPlot(
245
    abbreviateTemperaturePhenomenonNames(phenomenonNamesArr),
246
247
    unitOfMeasurementSymbolsArr
  );
248

249
  // The unit of measurement symbols for the x-axis is the first element of the array
250
  const unitOfMeasurementXAxisSymbol = unitOfMeasurementSymbolsArr[0];
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266

  const MARKER_RADIUS = 2;

  Highcharts.chart("chart-scatter-plot", {
    chart: {
      type: "scatter",
      zoomType: "xy",
    },

    boost: {
      useGPUTranslations: true,
      usePreAllocated: true,
    },

    title: {
      text: CHART_TITLE,
267
      "align": "center",
268
269
270
271
    },

    subtitle: {
      text: CHART_SUBTITLE,
272
      "align": "center",
273
274
275
276
277
278
279
280
    },

    xAxis: {
      labels: {
        format: `{value}`,
      },
      title: {
        enabled: true,
281
        text: X_AXIS_TITLE,
282
283
284
285
286
287
288
289
290
291
292
293
      },
      startOnTick: true,
      endOnTick: true,
      showLastLabel: true,
    },

    yAxis: [
      {
        labels: {
          format: `{value}`,
        },
        title: {
294
          text: Y_AXIS_TITLE,
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
        },
      },
    ],

    legend: {
      enabled: false,
    },

    plotOptions: {
      scatter: {
        marker: {
          radius: MARKER_RADIUS,
          states: {
            hover: {
              enabled: true,
              lineColor: "rgb(100,100,100)",
            },
          },
        },
        states: {
          hover: {
            marker: {
              enabled: false,
            },
          },
        },
      },
    },

324
325
    tooltip: {
      formatter() {
326
327
328
329
330
331
        // 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 = `<span style="color:${removeTransparencyFromColor(
          this.point.color
        )}">${this.series.name}</span> <br>`;

332
333
        const pointString = `<b>${this.point.y.toFixed(
          2
334
335
336
        )} ${getYAxisUnitOfMeasurementSymbol(
          this.series.name
        )}, ${this.point.x.toFixed(2)} ${unitOfMeasurementXAxisSymbol}</b>`;
337

338
339
340
341
342
343
        return headerString + pointString;
      },
    },

    exporting: chartExportOptions,

344
    series: seriesOptionsArr,
345
346
347
348
349
350
351
  });
};

export {
  formatSensorThingsApiResponseForScatterPlot,
  drawScatterPlotHighcharts,
};