chartScatterPlot.mjs 10.1 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
/**
 * Draw a scatter plot using Highcharts library
 * @param {Array} formattedObsArrayForSeriesOnePlusSeriesTwo Response from SensorThings API formatted for use in a scatter plot
Pithon Kabiro's avatar
Pithon Kabiro committed
183
 * @param {Object} extractedFormattedDatastreamProperties An object that contains arrays of formatted Datastream properties
184
 * @returns {undefined} undefined
185
186
187
 */
const drawScatterPlotHighcharts = function (
  formattedObsArrayForSeriesOnePlusSeriesTwo,
Pithon Kabiro's avatar
Pithon Kabiro committed
188
  extractedFormattedDatastreamProperties
189
) {
Pithon Kabiro's avatar
Pithon Kabiro committed
190
  // Arrays of datastream properties
191
  const {
Pithon Kabiro's avatar
Pithon Kabiro committed
192
193
194
195
196
197
    datastreamDescriptionsArr,
    datastreamNamesArr,
    phenomenonNamesArr,
    unitOfMeasurementSymbolsArr,
  } = extractedFormattedDatastreamProperties;

198
199
200
201
202
  // Create the array of series options object(s)
  const seriesOptionsArr = createSeriesOptionsForScatterPlot(
    formattedObsArrayForSeriesOnePlusSeriesTwo,
    phenomenonNamesArr
  );
203

204
205
  const CHART_TITLE =
    createCombinedTextForScatterPlotTitles(phenomenonNamesArr);
206

207
  const CHART_SUBTITLE = createSubtitleForChart(datastreamNamesArr);
208

209
210
211
212
  const X_AXIS_TITLE = createXAxisTitleTextScatterPlot(
    phenomenonNamesArr,
    unitOfMeasurementSymbolsArr
  );
213

214
  const Y_AXIS_TITLE = createYAxisTitleTextScatterPlot(
215
    abbreviateTemperaturePhenomenonNames(phenomenonNamesArr),
216
217
    unitOfMeasurementSymbolsArr
  );
218

219
220
221
  // The unit of measurement symbols for the x-axis is the first element of the array
  // Assume that we will be comparing similar phenomena, so we can reuse this symbol
  const UNIT_OF_MEASUREMENT_SYMBOL = unitOfMeasurementSymbolsArr[0];
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237

  const MARKER_RADIUS = 2;

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

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

    title: {
      text: CHART_TITLE,
238
      "align": "center",
239
240
241
242
    },

    subtitle: {
      text: CHART_SUBTITLE,
243
      "align": "center",
244
245
246
247
248
249
250
251
    },

    xAxis: {
      labels: {
        format: `{value}`,
      },
      title: {
        enabled: true,
252
        text: X_AXIS_TITLE,
253
254
255
256
257
258
259
260
261
262
263
264
      },
      startOnTick: true,
      endOnTick: true,
      showLastLabel: true,
    },

    yAxis: [
      {
        labels: {
          format: `{value}`,
        },
        title: {
265
          text: Y_AXIS_TITLE,
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
        },
      },
    ],

    legend: {
      enabled: false,
    },

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

295
296
    tooltip: {
      formatter() {
297
298
299
300
301
302
        // 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>`;

303
304
        const pointString = `<b>${this.point.y.toFixed(
          2
305
        )} ${UNIT_OF_MEASUREMENT_SYMBOL}, ${this.point.x.toFixed(
306
          2
307
        )} ${UNIT_OF_MEASUREMENT_SYMBOL}</b>`;
308

309
310
311
312
313
314
        return headerString + pointString;
      },
    },

    exporting: chartExportOptions,

315
    series: seriesOptionsArr,
316
317
318
319
320
321
322
  });
};

export {
  formatSensorThingsApiResponseForScatterPlot,
  drawScatterPlotHighcharts,
};