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

/**
 * 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
) {
39
40
41
42
43
44
45
46
47
  // Check if our observation arrays have equal lengths,
  // remove the unique observations, if necessary
  const [obsArrayOneFinal, obsArrayTwoFinal] =
    obsArrayOne.length !== obsArrayTwo.length
      ? checkForAndDeleteUniqueObservationsFromLargerArray(
          obsArrayOne,
          obsArrayTwo
        )
      : [obsArrayOne, obsArrayTwo];
48

49
50
  // Create the combined observations array
  return createCombinedObservationValues(obsArrayOneFinal, obsArrayTwoFinal);
51
52
};

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
/**
 * 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
112
  ) {
113
114
115
    throw new Error(
      "The phenomenon names array and unit of measurement symbols array have different lengths"
    );
116
  } else {
117
118
119
120
121
    const combinedNameSymbolArr = phenomenonNamesYAxisArr.map(
      (phenomenonNameYAxis, i) =>
        `${phenomenonNameYAxis} [${unitOfMeasurementSymbolsYAxisArr[i]}]`
    );

122
123
    return createCombinedTextDelimitedByComma(combinedNameSymbolArr);
  }
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
};

/**
 * 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;

139
140
  // Create a local copy of the colors array
  const highchartsColorsForScatterPlotArr = [...highchartsColorsArr];
141
142
143
144
145

  // Opacity value for symbol
  const SERIES_SYMBOL_COLOR_OPACITY = ".3";

  // Create array of colors in RGBA format
146
  const seriesColorsArr = highchartsColorsForScatterPlotArr.map(
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
    (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
163
  ) {
164
165
166
    throw new Error(
      "The observations array and phenomenon names array have different lengths"
    );
167
168
169
170
171
  } else {
    return formattedObsArraysForScatterPlot.map((formattedObsArray, i) => {
      return {
        name: `${phenomenonNamesYAxisArr[i]}, ${phenomenonNameXAxis}`,
        data: formattedObsArray,
172
        color: seriesColorsArr[i],
173
174
175
      };
    });
  }
176
177
};

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
/**
 * 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;
  }
};

208
209
/**
 * Draw a scatter plot using Highcharts library
210
 * @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
211
 * @param {Object} extractedFormattedDatastreamProperties An object that contains arrays of formatted Datastream properties
212
 * @returns {undefined} undefined
213
214
 */
const drawScatterPlotHighcharts = function (
215
  formattedObsArraysForScatterPlot,
Pithon Kabiro's avatar
Pithon Kabiro committed
216
  extractedFormattedDatastreamProperties
217
) {
Pithon Kabiro's avatar
Pithon Kabiro committed
218
  // Arrays of datastream properties
219
  const {
Pithon Kabiro's avatar
Pithon Kabiro committed
220
221
222
223
224
225
    datastreamDescriptionsArr,
    datastreamNamesArr,
    phenomenonNamesArr,
    unitOfMeasurementSymbolsArr,
  } = extractedFormattedDatastreamProperties;

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

232
233
  const CHART_TITLE =
    createCombinedTextForScatterPlotTitles(phenomenonNamesArr);
234

235
  const CHART_SUBTITLE = createSubtitleForChart(datastreamNamesArr);
236

237
238
239
240
  const X_AXIS_TITLE = createXAxisTitleTextScatterPlot(
    phenomenonNamesArr,
    unitOfMeasurementSymbolsArr
  );
241

242
  const Y_AXIS_TITLE = createYAxisTitleTextScatterPlot(
243
    abbreviateTemperaturePhenomenonNames(phenomenonNamesArr),
244
245
    unitOfMeasurementSymbolsArr
  );
246

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

  const MARKER_RADIUS = 2;

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

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

    title: {
      text: CHART_TITLE,
265
      "align": "center",
266
267
268
269
    },

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

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

    yAxis: [
      {
        labels: {
          format: `{value}`,
        },
        title: {
292
          text: Y_AXIS_TITLE,
293
294
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
        },
      },
    ],

    legend: {
      enabled: false,
    },

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

322
323
    tooltip: {
      formatter() {
324
325
326
327
328
329
        // 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>`;

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

336
337
338
339
340
341
        return headerString + pointString;
      },
    },

    exporting: chartExportOptions,

342
    series: seriesOptionsArr,
343
344
345
346
347
348
349
  });
};

export {
  formatSensorThingsApiResponseForScatterPlot,
  drawScatterPlotHighcharts,
};