appChart.js 24.3 KB
Newer Older
Pithon Kabiro's avatar
Pithon Kabiro committed
1
"use strict";
Pithon Kabiro's avatar
Pithon Kabiro committed
2

3
const BASE_URL = "http://193.196.39.91:8080/frost-icity-tp31/v1.1";
4

5
6
/**
 * Retrieve the datastream ID that corresponds to a particular building
7
8
9
 * @param {Number | String} buildingNumber Integer representing the building ID
 * @param {String} phenomenon String representing the phenomenon of interest
 * @param {String} samplingRate String representing the sampling rate of the observations
10
11
 * @returns {Number} Datastream corresponding to the input building
 */
12
const getDatastreamIdFromBuildingNumber = function (
13
14
15
16
17
18
19
20
  buildingNumber,
  phenomenon,
  samplingRate
) {
  const buildingToDatastreamMapping = {
    101: {
      vl: { "15min": "69", "60min": "75" },
      rl: { "15min": "81", "60min": "87" },
21
22
23
24
25
26

      // These Datastreams do not yet have Observations
      // flow: { "15min": "93", "60min": "99" },
      // power: { "15min": "105", "60min": "111" },
      // energy: { "15min": "117", "60min": "123" },
      // energy_verb: { "15min": "129", "60min": "135" },
27
    },
28

29
30
31
    102: {
      vl: { "15min": "70", "60min": "76" },
      rl: { "15min": "82", "60min": "88" },
32
33
34
35
36
37

      // These Datastreams do not yet have Observations
      // flow: { "15min": "94", "60min": "100" },
      // power: { "15min": "106", "60min": "112" },
      // energy: { "15min": "118", "60min": "124" },
      // energy_verb: { "15min": "130", "60min": "136" },
38
    },
39

40
41
42
    107: {
      vl: { "15min": "71", "60min": "77" },
      rl: { "15min": "83", "60min": "89" },
43
44
45
46
47
48

      // These Datastreams do not yet have Observations
      // flow: { "15min": "95", "60min": "101" },
      // power: { "15min": "107", "60min": "113" },
      // energy: { "15min": "119", "60min": "125" },
      // energy_verb: { "15min": "131", "60min": "137" },
49
    },
50

51
    "112, 118": {
52
53
      vl: { "15min": "72", "60min": "78" },
      rl: { "15min": "84", "60min": "90" },
54
55
56
57
58
59

      // These Datastreams do not yet have Observations
      // flow: { "15min": "96", "60min": "102" },
      // power: { "15min": "108", "60min": "114" },
      // energy: { "15min": "120", "60min": "126" },
      // energy_verb: { "15min": "132", "60min": "138" },
60
    },
61

62
63
64
    125: {
      vl: { "15min": "73", "60min": "79" },
      rl: { "15min": "85", "60min": "91" },
65
66
67
68
69
70

      // These Datastreams do not yet have Observations
      // flow: { "15min": "97", "60min": "103" },
      // power: { "15min": "109", "60min": "115" },
      // energy: { "15min": "121", "60min": "127" },
      // energy_verb: { "15min": "133", "60min": "139" },
71
    },
72

73
74
75
76
77
78
79
80
    225: {
      vl: { "15min": "74", "60min": "80" },
      rl: { "15min": "86", "60min": "92" },
      flow: { "15min": "98", "60min": "104" },
      power: { "15min": "110", "60min": "116" },
      energy: { "15min": "122", "60min": "128" },
      energy_verb: { "15min": "134", "60min": "140" },
    },
81
82
83
84

    weather_station_521: {
      outside_temp: { "15min": "141", "60min": "142" },
    },
85
86
  };

87
88
89
90
91
92
  if (
    buildingToDatastreamMapping?.[buildingNumber]?.[phenomenon]?.[
      samplingRate
    ] === undefined
  )
    return;
93

94
95
96
97
98
99
100
  const datastreamIdMatched = Number(
    buildingToDatastreamMapping[buildingNumber][phenomenon][samplingRate]
  );

  return datastreamIdMatched;
};

101
102
103
104
105
106
/**
 * Create URL to fetch the details of single Datastream
 * @param {String} baseUrl Base URL of the STA server
 * @param {Number} datastreamID Integer representing the Datastream ID
 * @returns {String} URL string for fetching a single Datastream
 */
107
const getDatastreamUrl = function (baseUrl, datastreamID) {
108
109
110
111
112
  if (!datastreamID) return;
  const fullDatastreamURL = `${baseUrl}/Datastreams(${datastreamID})`;
  return fullDatastreamURL;
};

113
/**
114
 * Create URL to fetch Observations
115
 * @param {String} baseUrl Base URL of the STA server
116
117
 * @param {Number} datastreamID Integer representing the Datastream ID
 * @returns {String} URL string for fetching Observations
118
 */
119
const getObservationsUrl = function (baseUrl, datastreamID) {
120
  if (!datastreamID) return;
121
122
  const fullObservationsURL = `${baseUrl}/Datastreams(${datastreamID})/Observations`;
  return fullObservationsURL;
123
124
125
126
};

/**
 * Create a temporal filter string for the fetched Observations
127
128
129
 * @param {String} dateStart Start date in YYYY-MM-DD format
 * @param {String} dateStop Stop date in YYYY-MM-DD format
 * @returns {String} Temporal filter string
130
 */
131
const createTemporalFilterString = function (dateStart, dateStop) {
132
133
134
135
136
  if (!dateStart || !dateStop) return;
  const filterString = `resultTime ge ${dateStart}T00:00:00.000Z and resultTime le ${dateStop}T00:00:00.000Z`;
  return filterString;
};

137
// const BASE_URL_OBSERVATIONS = getObservationsUrl(80);
138
139
140
141
142
143
144
const QUERY_PARAM_RESULT_FORMAT = "dataArray";
const QUERY_PARAM_ORDER_BY = "phenomenonTime asc";
const QUERY_PARAM_FILTER = createTemporalFilterString(
  "2020-01-01",
  "2021-01-01"
);
const QUERY_PARAM_SELECT = "result,phenomenonTime";
145
const QUERY_PARAMS_COMBINED = {
146
147
148
149
  "$resultFormat": QUERY_PARAM_RESULT_FORMAT,
  "$orderBy": QUERY_PARAM_ORDER_BY,
  "$filter": QUERY_PARAM_FILTER,
  "$select": QUERY_PARAM_SELECT,
150
};
Pithon Kabiro's avatar
Pithon Kabiro committed
151

152
153
/**
 * Perform a GET request using the Axios library
154
 * @param {String} urlObservations A URL that fetches Observations from an STA instance
155
 * @param {Object} urlParamObj The URL parameters to be sent together with the GET request
156
 * @returns {Promise} A promise that contains the first page of results when fulfilled
157
 */
158
const axiosGetRequest = function (urlObservations, urlParamObj) {
159
160
161
162
  return axios.get(urlObservations, {
    params: urlParamObj,
  });
};
163

164
165
166
167
168
169
/**
 * Retrieve the metadata for a single datastream
 * @async
 * @param {String} urlDatastream A URL that fetches a Datastream from an STA instance
 * @returns {Promise} A promise that contains a metadata object for a Datastream when fulfilled
 */
170
const getMetadataFromSingleDatastream = async function (urlDatastream) {
171
172
173
174
175
176
177
178
  try {
    // Extract properties of interest
    const {
      data: { description, name, unitOfMeasurement },
    } = await axiosGetRequest(urlDatastream);

    return { description, name, unitOfMeasurement };
  } catch (err) {
179
    console.error(err);
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
/**
 * Retrieve metadata from multiple datastreams
 * @param {Array} datastreamsUrlArr An array that contains N Datastream URL strings
 * @returns {Promise} A promise that contains an array of N Datastream metadata objects when fulfilled
 */
const getMetadataFromMultipleDatastreams = async function (datastreamsUrlArr) {
  try {
    // Array to store our final result
    const datastreamMetadataArr = [];

    // Use for/of loop - we need to maintain the order of execution of the async operations
    for (const datastreamUrl of datastreamsUrlArr) {
      // Metadata from a single Datastream
      const datastreamMetadata = await getDatastreamMetadata(datastreamUrl);
      datastreamMetadataArr.push(datastreamMetadata);
    }

    return datastreamMetadataArr;
  } catch (err) {
    console.error(err);
  }
};

206
207
208
209
210
/**
 * Format the response containing a Datastream's metadata from Sensorthings API
 * @param {Object} datastreamMetadata An object containing a Datastream's metadata
 * @returns {Object} An object containing the formatted metadata that is suitable for use in a line chart or heatmap
 */
211
const formatDatastreamMetadataForChart = function (datastreamMetadata) {
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
  const {
    description: datastreamDescription,
    name: datastreamName,
    unitOfMeasurement,
  } = datastreamMetadata;

  // Extract phenomenon name from Datastream name
  const regex = /\/ (.*) DS/;
  const phenomenonName = datastreamName.match(regex)[1]; // use second element in array

  // Match the unitOfMeasurement's string representation of a symbol
  // to an actual symbol, where necessary
  const unitOfMeasurementSymbol = (() => {
    if (unitOfMeasurement.symbol === "degC") {
      return "";
    } else if (unitOfMeasurement.symbol === "m3/h") {
      return "m<sup>3</sup>/h";
    } else {
      return unitOfMeasurement.symbol;
    }
  })();

  return {
    datastreamDescription,
    datastreamName,
    phenomenonName,
    unitOfMeasurementSymbol,
  };
};

242
/**
243
 * Format the response from SensorThings API to make it suitable for heatmap
244
245
 * @param {Array} obsArray Response from SensorThings API as array
 * @returns {Array} Array of formatted observations suitable for use in a heatmap
246
 */
247
const formatSTAResponseForHeatMap = function (obsArray) {
Pithon Kabiro's avatar
Pithon Kabiro committed
248
  if (!obsArray) return;
249
250

  const dataSTAFormatted = obsArray.map((obs) => {
251
252
253
254
255
256
257
258
259
260
261
262
263
    // Get the date/time string; first element in input array; remove trailing "Z"
    const obsDateTimeInput = obs[0].slice(0, -1);
    // Get the "date" part of an observation
    const obsDateInput = obs[0].slice(0, 10);
    // Create Date objects
    const obsDateTime = new Date(obsDateTimeInput);
    const obsDate = new Date(obsDateInput);
    // x-axis -> timestamp; will be the same for observations from the same date
    const timestamp = Date.parse(obsDate);
    // y-axis -> hourOfDay
    const hourOfDay = obsDateTime.getHours();
    // value -> the observation's value; second element in input array
    const value = obs[1];
264
    return [timestamp, hourOfDay, value];
265
  });
266

267
268
269
270
271
  return dataSTAFormatted;
};

/**
 * Draw a heatmap using Highcharts library
272
 * @param {Array} formattedObsArrayForHeatmap Response from SensorThings API formatted for use in a heatmap
273
 * @param {Object} formattedDatastreamMetadata Object containing Datastream metadata
274
 * @returns {undefined} undefined
275
 */
276
const drawHeatMapHC = function (
277
  formattedObsArrayForHeatmap,
278
  formattedDatastreamMetadata
279
) {
280
281
282
283
284
285
286
  const {
    datastreamDescription: DATASTREAM_DESCRIPTION,
    datastreamName: DATASTREAM_NAME,
    phenomenonName: PHENOMENON_NAME,
    unitOfMeasurementSymbol: PHENOMENON_SYMBOL,
  } = formattedDatastreamMetadata;

Pithon Kabiro's avatar
Pithon Kabiro committed
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
  // Function returns the min and max observation values
  const {
    minObsValue: MINIMUM_VALUE_COLOR_AXIS,
    maxObsValue: MAXIMUM_VALUE_COLOR_AXIS,
  } = (() => {
    // The observation value is the third element in array
    const obsValueArr = formattedObsArrayForHeatmap.map((obs) => obs[2]);

    // Extract integer part
    const minValue = Math.trunc(Math.min(...obsValueArr));
    const maxValue = Math.trunc(Math.max(...obsValueArr));

    // Calculate the closest multiple of 5
    const minObsValue = minValue - (minValue % 5);
    const maxObsValue = maxValue + (5 - (maxValue % 5));

    return { minObsValue, maxObsValue };
  })();

306
307
308
309
310
311
312
313
314
315
316
  Highcharts.chart("chart-heatmap", {
    chart: {
      type: "heatmap",
      zoomType: "x",
    },

    boost: {
      useGPUTranslations: true,
    },

    title: {
317
      text: DATASTREAM_DESCRIPTION,
318
319
320
321
322
      align: "left",
      x: 40,
    },

    subtitle: {
323
      text: DATASTREAM_NAME,
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
      align: "left",
      x: 40,
    },

    xAxis: {
      type: "datetime",
      // min: Date.UTC(2017, 0, 1),
      // max: Date.UTC(2017, 11, 31, 23, 59, 59),
      labels: {
        align: "left",
        x: 5,
        y: 14,
        format: "{value:%B}", // long month
      },
      showLastLabel: false,
      tickLength: 16,
    },

    yAxis: {
      title: {
        text: null,
      },
      labels: {
        format: "{value}:00",
      },
      minPadding: 0,
      maxPadding: 0,
      startOnTick: false,
      endOnTick: false,
      tickPositions: [0, 3, 6, 9, 12, 15, 18, 21, 24],
      tickWidth: 1,
      min: 0,
      max: 23,
      reversed: true,
    },

    colorAxis: {
      stops: [
        [0, "#3060cf"],
        [0.5, "#fffbbc"],
        [0.9, "#c4463a"],
        [1, "#c4463a"],
      ],
Pithon Kabiro's avatar
Pithon Kabiro committed
367
368
      min: MINIMUM_VALUE_COLOR_AXIS,
      max: MAXIMUM_VALUE_COLOR_AXIS,
369
370
371
      startOnTick: false,
      endOnTick: false,
      labels: {
372
373
        // format: "{value}℃",
        format: `{value}${PHENOMENON_SYMBOL}`,
374
375
376
377
378
      },
    },

    series: [
      {
379
        data: formattedObsArrayForHeatmap,
380
381
382
383
384
        boostThreshold: 100,
        borderWidth: 0,
        nullColor: "#525252",
        colsize: 24 * 36e5, // one day
        tooltip: {
385
386
          headerFormat: `${PHENOMENON_NAME}<br/>`,
          valueDecimals: 2,
387
          pointFormat:
388
389
            // "{point.x:%e %b, %Y} {point.y}:00: <b>{point.value} ℃</b>",
            `{point.x:%e %b, %Y} {point.y}:00: <b>{point.value} ${PHENOMENON_SYMBOL}</b>`,
Pithon Kabiro's avatar
Pithon Kabiro committed
390
          nullFormat: `{point.x:%e %b, %Y} {point.y}:00: <b>null</b>`,
391
392
393
394
395
396
397
        },
        turboThreshold: Number.MAX_VALUE, // #3404, remove after 4.0.5 release
      },
    ],
  });
};

Pithon Kabiro's avatar
Pithon Kabiro committed
398
399
/**
 * Convert the observations' phenomenonTime from an ISO 8601 string to Unix epoch
400
 * @param {Array} obsArray Response from SensorThings API as array
401
 * @returns {Array} Array of formatted observations suitable for use in a line chart or scatter plot
Pithon Kabiro's avatar
Pithon Kabiro committed
402
 */
403
const formatSTAResponseForLineChartOrScatterPlot = function (obsArray) {
Pithon Kabiro's avatar
Pithon Kabiro committed
404
  if (!obsArray) return;
405
406

  const dataSTAFormatted = obsArray.map((result) => {
Pithon Kabiro's avatar
Pithon Kabiro committed
407
408
    const timestampObs = new Date(result[0].slice(0, -1)).getTime(); // slice() removes trailing "Z" character in timestamp
    const valueObs = result[1];
409
    return [timestampObs, valueObs];
Pithon Kabiro's avatar
Pithon Kabiro committed
410
  });
411

Pithon Kabiro's avatar
Pithon Kabiro committed
412
413
414
415
416
  return dataSTAFormatted;
};

/**
 * Draw a line chart using Highcharts library
417
 * @param {Array} formattedObsArrayForLineChart Response from SensorThings API formatted for use in a line chart
418
 * @param {Object} formattedDatastreamMetadata Object containing Datastream metadata
419
 * @returns {undefined} undefined
Pithon Kabiro's avatar
Pithon Kabiro committed
420
 */
421
const drawLineChartHC = function (
422
  formattedObsArrayForLineChart,
423
  formattedDatastreamMetadata
424
) {
425
426
427
428
429
430
431
  const {
    datastreamDescription: DATASTREAM_DESCRIPTION,
    datastreamName: DATASTREAM_NAME,
    phenomenonName: PHENOMENON_NAME,
    unitOfMeasurementSymbol: PHENOMENON_SYMBOL,
  } = formattedDatastreamMetadata;

Pithon Kabiro's avatar
Pithon Kabiro committed
432
433
434
435
436
437
  Highcharts.stockChart("chart-line", {
    chart: {
      zoomType: "x",
    },

    rangeSelector: {
438
      selected: 5,
Pithon Kabiro's avatar
Pithon Kabiro committed
439
440
441
    },

    title: {
442
      text: DATASTREAM_DESCRIPTION,
443
444
445
446
      "align": "left",
    },

    subtitle: {
447
      text: DATASTREAM_NAME,
448
      align: "left",
Pithon Kabiro's avatar
Pithon Kabiro committed
449
450
451
452
    },

    series: [
      {
453
        name: `${PHENOMENON_NAME} (${PHENOMENON_SYMBOL})`,
454
        data: formattedObsArrayForLineChart,
Pithon Kabiro's avatar
Pithon Kabiro committed
455
456
457
458
459
460
461
462
463
        tooltip: {
          valueDecimals: 2,
        },
        turboThreshold: Number.MAX_VALUE, // #3404, remove after 4.0.5 release
      },
    ],
  });
};

Pithon Kabiro's avatar
Pithon Kabiro committed
464
/**
Pithon Kabiro's avatar
Pithon Kabiro committed
465
 * Follows "@iot.nextLink" links in SensorThingsAPI's response
Pithon Kabiro's avatar
Pithon Kabiro committed
466
 * Appends new results to existing results
Pithon Kabiro's avatar
Pithon Kabiro committed
467
 * @async
468
469
 * @param {Promise} responsePromise Promise object resulting from an Axios GET request
 * @returns {Object} Object containing results from all the "@iot.nextLink" links
Pithon Kabiro's avatar
Pithon Kabiro committed
470
 */
471
const followNextLink = function (responsePromise) {
Pithon Kabiro's avatar
Pithon Kabiro committed
472
  if (!responsePromise) return;
Pithon Kabiro's avatar
Pithon Kabiro committed
473
  return responsePromise
474
    .then((lastSuccess) => {
Pithon Kabiro's avatar
Pithon Kabiro committed
475
476
477
      if (lastSuccess.data["@iot.nextLink"]) {
        return followNextLink(
          axios.get(lastSuccess.data["@iot.nextLink"])
478
        ).then((nextLinkSuccess) => {
Pithon Kabiro's avatar
Pithon Kabiro committed
479
480
481
482
483
484
485
486
487
          nextLinkSuccess.data.value = lastSuccess.data.value.concat(
            nextLinkSuccess.data.value
          );
          return nextLinkSuccess;
        });
      } else {
        return lastSuccess;
      }
    })
488
    .catch((err) => {
489
      console.error(err);
Pithon Kabiro's avatar
Pithon Kabiro committed
490
491
492
    });
};

493
494
495
/**
 * Retrieve all the Observations from a Datastream after traversing all the "@iot.nextLink" links
 * @async
496
497
 * @param {Promise} httpGetRequestPromise Promise object resulting from an Axios GET request
 * @returns {Promise} A promise that contains an array of Observations from a single Datastream when fulfilled
498
 */
499
const getCombinedObservationsFromAllNextLinks = function (
500
  httpGetRequestPromise
501
) {
502
  return followNextLink(httpGetRequestPromise)
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
    .then((success) => {
      const successValue = success.data.value;
      // Array that will hold the combined observations
      const combinedObservations = [];
      successValue.forEach((dataObj) => {
        // Each page of results will have a dataArray that holds the observations
        const dataArrays = dataObj.dataArray;
        combinedObservations.push(...dataArrays);
      });

      return new Promise((resolve, reject) => {
        resolve(combinedObservations);
      });
    })
    .catch((err) => {
518
      console.error(err);
519
520
    });
};
521

522
523
524
525
526
527
/**
 * Retrieve the metadata for a Datastream as well as the Observations corresponding to it
 * @async
 * @param {Promise} metadataPlusObsPromiseArray An array that contains two promises, one for datastream metadata, the other for observations
 * @returns {Promise} A promise that contains two arrays when fulfilled, one for datastream metadata and the other for observations
 */
528
const getMetadataPlusObservationsForChart = async function (
529
  metadataPlusObsPromiseArray
530
) {
531
532
533
  try {
    // Array to store our final result
    const combinedResolvedPromises = [];
534

535
536
    // Use for/of loop - we need to maintain the order of execution of the async operations
    for (const promise of metadataPlusObsPromiseArray) {
537
      // Resolved value of a single promise
538
539
540
      const resolvedPromise = await promise;
      combinedResolvedPromises.push(resolvedPromise);
    }
541
542
543
544

    return combinedResolvedPromises;
  } catch (err) {
    console.error(err);
545
546
547
  }
};

548
549
550
/**
 * Retrieve all the Observations from an array of Observations promises
 * @async
551
 * @param {Promise} observationPromiseArray An array that contains N observation promises
552
553
554
555
556
 * @returns {Promise} A promise that contains an array of Observations from multiple Datastreams when fulfilled
 */
const getObservationsFromMultipleDatastreams = async function (
  observationPromiseArray
) {
557
558
559
  try {
    // Array to store our final result
    const observationsAllDatastreamsArr = [];
560

561
562
    // Use for/of loop - we need to maintain the order of execution of the async operations
    for (const observationPromise of observationPromiseArray) {
563
564
565
566
      // Observations from a single Datastream
      const observations = await observationPromise;
      observationsAllDatastreamsArr.push(observations);
    }
567
568
569
570

    return observationsAllDatastreamsArr;
  } catch (err) {
    console.error(err);
571
572
  }
};
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603

// Building + phenomenon + sampling rate
const buildingsSensorSamplingRateRLArr = [
  ["101", "rl", "60min"],
  ["102", "rl", "60min"],
  ["107", "rl", "60min"],
  ["112, 118", "rl", "60min"],
  ["125", "rl", "60min"],
  ["225", "rl", "60min"],
];

// Datastreams IDs
const datastreamsRLArr = buildingsSensorSamplingRateRLArr.map((bldg) =>
  getDatastreamIdFromBuildingNumber(...bldg)
);

// Datastreams URLs
const datastreamsUrlRLArr = datastreamsRLArr.map((datastreamId) =>
  getObservationsUrl(BASE_URL, datastreamId)
);

// Promise objects - Observations / RL
const observationsPromisesRLArr = datastreamsUrlRLArr.map((obsUrl) =>
  getCombinedObservationsFromAllNextLinks(
    axiosGetRequest(obsUrl, QUERY_PARAMS_COMBINED)
  )
);

// getObservationsFromMultipleDatastreams(observationsPromisesRLArr).then((x) =>
//   console.log(x)
// );
604

605
606
607
608
609
610
const drawScatterPlotHC = function (
  formattedObsArrayForSeriesOne,
  formattedDatastreamMetadataSeriesOne = "",
  formattedObsArrayForSeriesTwo,
  formattedDatastreamMetadataSeriesTwo = ""
) {
611
612
613
614
615
616
617
  const CHART_TITLE = "Height Versus Weight of 507 Individuals by Gender";
  const CHART_SUBTITLE = "Source: Heinz  2003";

  const X_AXIS_TITLE = "Height (cm)";

  const SERIES_1_NAME = "Rücklauftemp";
  const SERIES_1_SYMBOL_COLOR = "rgba(119, 152, 191, .5)";
Pithon Kabiro's avatar
Pithon Kabiro committed
618
  const SERIES_1_TEXT_COLOR = "rgb(119, 152, 191)"; // remove transparency from symbol color for a more "intense" color
619
620
621
622
  const SERIES_1_SYMBOL = "°C";

  const SERIES_2_NAME = "Power";
  const SERIES_2_SYMBOL_COLOR = "rgba(223, 83, 83, .5)";
Pithon Kabiro's avatar
Pithon Kabiro committed
623
  const SERIES_2_TEXT_COLOR = "rgb(223, 83, 83)"; // remove transparency from symbol color for a more "intense" color
624
625
  const SERIES_2_SYMBOL = "kW";

626
627
628
629
630
  Highcharts.chart("chart-scatter-plot", {
    chart: {
      type: "scatter",
      zoomType: "xy",
    },
631

632
    title: {
633
      text: CHART_TITLE,
634
    },
635

636
    subtitle: {
637
      text: CHART_SUBTITLE,
638
    },
639

640
641
642
    xAxis: {
      title: {
        enabled: true,
643
        text: X_AXIS_TITLE,
644
      },
645
      type: "datetime",
646
647
648
649
      startOnTick: true,
      endOnTick: true,
      showLastLabel: true,
    },
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665

    yAxis: [
      {
        // Primary yAxis
        labels: {
          format: `{value} ${SERIES_1_SYMBOL}`,
          style: {
            color: SERIES_1_TEXT_COLOR,
          },
        },
        title: {
          text: SERIES_1_NAME,
          style: {
            color: SERIES_1_TEXT_COLOR,
          },
        },
666
      },
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
      {
        // Secondary yAxis
        title: {
          text: SERIES_2_NAME,
          style: {
            color: SERIES_2_TEXT_COLOR,
          },
        },
        labels: {
          format: `{value} ${SERIES_2_SYMBOL}`,
          style: {
            color: SERIES_2_TEXT_COLOR,
          },
        },
        opposite: true,
      },
    ],

685
686
687
688
689
690
691
692
693
694
    legend: {
      layout: "vertical",
      align: "left",
      verticalAlign: "top",
      x: 100,
      y: 70,
      floating: true,
      backgroundColor: Highcharts.defaultOptions.chart.backgroundColor,
      borderWidth: 1,
    },
695

696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
    plotOptions: {
      scatter: {
        marker: {
          radius: 5,
          states: {
            hover: {
              enabled: true,
              lineColor: "rgb(100,100,100)",
            },
          },
        },
        states: {
          hover: {
            marker: {
              enabled: false,
            },
          },
        },
        tooltip: {
Pithon Kabiro's avatar
Pithon Kabiro committed
715
          headerFormat: "{point.x:%e %b, %Y %H:%M:%S}: <b>{point.y}</b>",
716
          pointFormat: "{point.x} cm, {point.y} kg",
Pithon Kabiro's avatar
Pithon Kabiro committed
717
          valueDecimals: 2,
718
719
720
        },
      },
    },
721

722
723
    series: [
      {
724
725
        name: SERIES_1_NAME,
        color: SERIES_1_SYMBOL_COLOR,
726
        data: formattedObsArrayForSeriesOne,
Pithon Kabiro's avatar
Pithon Kabiro committed
727
728
729
        tooltip: {
          valueSuffix: ` ${SERIES_1_SYMBOL}`,
        },
730
731
      },
      {
732
733
        name: SERIES_2_NAME,
        color: SERIES_2_SYMBOL_COLOR,
734
        data: formattedObsArrayForSeriesTwo,
Pithon Kabiro's avatar
Pithon Kabiro committed
735
736
737
738
739
740
        // need this property for the dual y-axes to work
        // defines the y-axis that this series refers to
        yAxis: 1,
        tooltip: {
          valueSuffix: ` ${SERIES_2_SYMBOL}`,
        },
741
742
743
744
745
      },
    ],
  });
};

746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
(async () => {
  const DATASTREAM_ID_SERIES_1 = getDatastreamIdFromBuildingNumber(
    "weather_station_521",
    "outside_temp",
    "60min"
  );
  const URL_DATASTREAM_SERIES_1 = getDatastreamUrl(
    BASE_URL,
    DATASTREAM_ID_SERIES_1
  );
  const URL_OBSERVATIONS_SERIES_1 = getObservationsUrl(
    BASE_URL,
    DATASTREAM_ID_SERIES_1
  );

  const DATASTREAM_ID_SERIES_2 = getDatastreamIdFromBuildingNumber(
    "225",
    "vl",
    "60min"
  );
  const URL_DATASTREAM_SERIES_2 = getDatastreamUrl(
    BASE_URL,
    DATASTREAM_ID_SERIES_2
  );
  const URL_OBSERVATIONS_SERIES_2 = getObservationsUrl(
    BASE_URL,
    DATASTREAM_ID_SERIES_2
  );

  // Create promises
776
  const promiseDatastreamMetadataSeries1 = getMetadataFromSingleDatastream(
777
778
779
780
781
782
783
    URL_DATASTREAM_SERIES_1
  );
  const promiseCombinedObservationsSeries1 =
    getCombinedObservationsFromAllNextLinks(
      axiosGetRequest(URL_OBSERVATIONS_SERIES_1, QUERY_PARAMS_COMBINED)
    );

784
  const promiseDatastreamMetadataSeries2 = getMetadataFromSingleDatastream(
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
    URL_DATASTREAM_SERIES_2
  );
  const promiseCombinedObservationsSeries2 =
    getCombinedObservationsFromAllNextLinks(
      axiosGetRequest(URL_OBSERVATIONS_SERIES_2, QUERY_PARAMS_COMBINED)
    );

  // Pass promises to our async function
  const metadataPlusObservationsSeries1 =
    await getMetadataPlusObservationsForChart([
      promiseCombinedObservationsSeries1,
      promiseDatastreamMetadataSeries1,
    ]);
  const metadataPlusObservationsSeries2 =
    await getMetadataPlusObservationsForChart([
      promiseCombinedObservationsSeries2,
      promiseDatastreamMetadataSeries2,
    ]);

  // Extract the metadata and the observations from resulting arrays
  const combinedObsSeries1 = metadataPlusObservationsSeries1[0];
  const datastreamMetadataSeries1 = metadataPlusObservationsSeries1[1];

  const combinedObsSeries2 = metadataPlusObservationsSeries2[0];
  const datastreamMetadataSeries2 = metadataPlusObservationsSeries2[1];

  drawScatterPlotHC(
    formatSTAResponseForLineChartOrScatterPlot(combinedObsSeries1),
    formatDatastreamMetadataForChart(datastreamMetadataSeries1),
    formatSTAResponseForLineChartOrScatterPlot(combinedObsSeries2),
    formatDatastreamMetadataForChart(datastreamMetadataSeries2)
  );
})();
818

819
820
821
822
823
824
825
826
export {
  BASE_URL,
  QUERY_PARAMS_COMBINED,
  getDatastreamIdFromBuildingNumber,
  getDatastreamUrl,
  getObservationsUrl,
  createTemporalFilterString,
  axiosGetRequest,
827
  getMetadataFromSingleDatastream,
828
829
830
  formatDatastreamMetadataForChart,
  formatSTAResponseForHeatMap,
  drawHeatMapHC,
831
  formatSTAResponseForLineChartOrScatterPlot,
832
833
834
835
  drawLineChartHC,
  getCombinedObservationsFromAllNextLinks,
  getMetadataPlusObservationsForChart,
};