@iotready/nextjs-components-library 1.0.0-preview2 → 1.0.0-preview21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -18,6 +18,10 @@ import 'moment/locale/it';
18
18
  // import 'moment/locale/en-gb'; // TODO set locale based on browser
19
19
  import { AdapterMoment } from '@mui/x-date-pickers/AdapterMoment';
20
20
  import { ThemeProvider } from '@mui/material/styles';
21
+ import TimelineIcon from '@mui/icons-material/Timeline';
22
+ import MuiTooltip from '@mui/material/Tooltip';
23
+ import EditNoteIcon from '@mui/icons-material/EditNote';
24
+ import { FilterTagMode } from '../../server-actions/types';
21
25
  const lineOptions = {
22
26
  parsing: false,
23
27
  normalized: true,
@@ -35,7 +39,7 @@ const lineOptions = {
35
39
  tooltip: {
36
40
  callbacks: {
37
41
  label: (context) => {
38
- return `${context.dataset.label}: ${context.parsed.y.toFixed(2)} ${context.dataset.unit}`;
42
+ return `${context.dataset.label}: ${context.parsed.y.toFixed(3)} ${context.dataset.unit}`;
39
43
  }
40
44
  },
41
45
  },
@@ -62,7 +66,19 @@ const lineOptions = {
62
66
  day: 'DD MMM YY',
63
67
  hour: 'DD MMM HH:mm',
64
68
  minute: 'HH:mm'
65
- }
69
+ },
70
+ y1: {
71
+ type: 'linear',
72
+ position: 'left',
73
+ title: { display: false, text: 'Primary Axis' },
74
+ display: false
75
+ },
76
+ y2: {
77
+ type: 'linear',
78
+ position: 'right',
79
+ title: { display: false, text: 'Secondary Axis' },
80
+ display: false
81
+ },
66
82
  },
67
83
  title: {
68
84
  display: false,
@@ -74,15 +90,6 @@ const lineOptions = {
74
90
  drawOnChartArea: false,
75
91
  }
76
92
  },
77
- y: {
78
- ticks: {
79
- callback: function (value) {
80
- if (Math.floor(value) === value) {
81
- return value;
82
- }
83
- }
84
- }
85
- },
86
93
  },
87
94
  };
88
95
  const chartConfigByPeriod = {
@@ -119,15 +126,15 @@ const chartConfigByPeriod = {
119
126
  scaleUnit: 'year',
120
127
  }
121
128
  };
122
- function GetPoints(data) {
123
- const points = data.results[0].series[0].values.map((row) => {
124
- return {
125
- x: moment.unix(row[0]),
126
- y: row[1]
127
- };
128
- });
129
- return points;
130
- }
129
+ // function getChartPoints(data: any): Point[] {
130
+ // const points: Point[] = data.results[0].series[0].values.map((row: any) => {
131
+ // return {
132
+ // x: moment.unix(row[0]),
133
+ // y: row[1]
134
+ // }
135
+ // });
136
+ // return points;
137
+ // }
131
138
  function getPollTime(intervalInSeconds, pollTime) {
132
139
  const CalculatedPollTime = Math.round(intervalInSeconds / 2880);
133
140
  if (CalculatedPollTime <= pollTime) {
@@ -138,59 +145,76 @@ function getPollTime(intervalInSeconds, pollTime) {
138
145
  }
139
146
  }
140
147
  function getCsvData(data, measures) {
141
- // Initialize the header with timestamp and measure names
142
- const headers = ["timestamp", ...measures.map(measure => measure.name)];
143
- const csvData = [];
144
- // Add the header to the csvData
145
- csvData.push(headers);
146
- // Collect all timestamps and values for each measure
148
+ // Intestazioni CSV
149
+ const headers = [
150
+ 'timestamp',
151
+ ...measures.map(m => m.description || m.name)
152
+ ];
153
+ const csvRows = [headers];
154
+ // Mappa temporale: ISO string → { timestamp, [measureName]: value, … }
147
155
  const timestampMap = {};
148
- // Populate timestampMap with data for each timestamp
149
- data.forEach((obj) => {
150
- obj.results[0].series.forEach((series) => {
151
- series.values.forEach((value) => {
152
- const timestamp = moment.unix(value[0]).toISOString();
153
- if (!timestampMap[timestamp]) {
154
- timestampMap[timestamp] = { timestamp: timestamp };
155
- }
156
- const measureName = series.name; // Assuming the series name corresponds to the measure name
157
- if (measures.some(measure => measure.name === measureName)) {
158
- timestampMap[timestamp][measureName] = value[1]; // Assign the value to the measure
159
- }
160
- });
156
+ // Ogni serie corrisponde a measures[index]
157
+ data.forEach((series, index) => {
158
+ const measure = measures[index];
159
+ if (!measure)
160
+ return; // difesa su mismatch lunghezze
161
+ series.forEach(point => {
162
+ // usa moment per coerenza con prima versione
163
+ const ts = moment(point.x).toISOString();
164
+ if (!timestampMap[ts]) {
165
+ timestampMap[ts] = { timestamp: ts };
166
+ }
167
+ timestampMap[ts][measure.name] = point.y;
161
168
  });
162
169
  });
163
- // Create rows from timestampMap
164
- Object.values(timestampMap).forEach((entry) => {
170
+ // Ordina i timestamp
171
+ const sortedTs = Object.keys(timestampMap)
172
+ .sort((a, b) => new Date(a).getTime() - new Date(b).getTime());
173
+ // Costruisci le righe CSV
174
+ sortedTs.forEach(ts => {
175
+ const entry = timestampMap[ts];
176
+ // prima colonna timestamp, poi i valori nelle colonne in ordine measures[]
165
177
  const row = [entry.timestamp];
166
- measures.forEach(measure => {
167
- // Push the corresponding value or an empty string if undefined
168
- row.push(entry[measure.name] !== undefined ? entry[measure.name] : "");
178
+ measures.forEach(m => {
179
+ row.push(entry[m.name] !== undefined
180
+ ? entry[m.name]
181
+ : '');
169
182
  });
170
- // Check if the row contains only empty values (besides the timestamp)
171
- const hasNonEmptyValues = row.slice(1).some(value => value !== null && value !== '' && value !== undefined);
172
- // If the row has at least one non-empty value or just the timestamp, add it to csvData
173
- if (hasNonEmptyValues || row.length === 1) {
174
- csvData.push(row);
183
+ // optional: filtra le righe che non hanno alcun valore utile
184
+ const hasValue = row.slice(1).some(v => v !== '' && v !== null);
185
+ if (hasValue) {
186
+ csvRows.push(row);
175
187
  }
176
188
  });
177
- // Join rows into a CSV string
178
- return csvData.map(row => row.join(',')).join('\n');
189
+ // Unisci in stringa
190
+ return csvRows
191
+ .map(r => r.join(','))
192
+ .join('\n');
179
193
  }
180
194
  // eslint-disable-next-line no-unused-vars
181
- const TrendChart = ({ deviceId, measures, enableExportData, enableDatePicker, handleGetInfluxData, handleGetFirstTimestamp, theme, ...props }) => {
195
+ const TrendChart = ({ deviceId, measures1, annotationsDataFn, measures2, enableDatePicker, handleGetInfluxData, handleGetDwSlotsCB, handleExportDataCB, theme, initialTimeStart, initialTimeEnd }) => {
182
196
  const [chartJsLoaded, setChartJsLoaded] = useState(false);
197
+ // Dichiarazione di annotationsData come funzione che ritorna una Promise<any>
198
+ const [annotationsData, setAnnotationsData] = useState([]);
199
+ const [annotentionsEnabled, setAnnotationsEnabled] = useState(true);
183
200
  const [dataMeasures, setDataMeasures] = useState(null);
184
201
  const [chartPeriod, setChartPeriod] = useState('1D');
185
202
  const [chartPeriodConfig, setChartPeriodConfig] = useState(chartConfigByPeriod['1D']);
186
203
  const [chartLoading, setChartLoading] = useState(false);
187
- const [timeStart, setTimeStart] = useState(moment().subtract(1, 'day').unix());
188
- const [timeEnd, setTimeEnd] = useState(moment().unix());
189
- const [firstTimestamp, setFirstTimestamp] = useState();
190
- const [datePickerUsed, setDatePickerUsed] = useState(false);
204
+ const [timeStartPicker, setTimeStartPicker] = useState(initialTimeStart || moment().subtract(1, 'day').unix());
205
+ const [timeStart, setTimeStart] = useState(initialTimeStart || moment().subtract(1, 'day').unix());
206
+ const [timeEnd, setTimeEnd] = useState(initialTimeEnd || moment().unix());
207
+ const [datePickerUsed, setDatePickerUsed] = useState(initialTimeStart || initialTimeEnd ? true : false);
208
+ const [pickerTimeStart, setPickerTimeStart] = useState(initialTimeStart || moment().subtract(1, 'day').unix());
209
+ const [pickerTimeEnd, setPickerTimeEnd] = useState(initialTimeEnd || moment().unix());
191
210
  const [loadingButton, setLoadingButton] = useState(false);
192
211
  const csvLinkRef = useRef(null);
193
212
  const [csvData, setCsvData] = useState('');
213
+ const [spanGapsOption, setSpanGapsOption] = useState(true);
214
+ const enableExportData = handleExportDataCB ? true : false;
215
+ const measures = measures1 && measures2 ? [...measures1, ...measures2] :
216
+ measures1 ? [...measures1] :
217
+ measures2 ? [...measures2] : [];
194
218
  const [options, setOptions] = useState({
195
219
  ...lineOptions,
196
220
  plugins: {
@@ -214,13 +238,19 @@ const TrendChart = ({ deviceId, measures, enableExportData, enableDatePicker, ha
214
238
  }
215
239
  });
216
240
  const [zoomed, setZoomed] = useState(false);
241
+ const prevMeasures = useRef();
217
242
  const resetChart = () => {
218
243
  setOptions({
219
244
  ...options,
220
245
  scales: {
221
246
  ...options.scales,
222
- y: {
223
- ...options.scales.y,
247
+ y1: {
248
+ ...options.scales.y1,
249
+ min: 0,
250
+ max: 1
251
+ },
252
+ y2: {
253
+ ...options.scales.y2,
224
254
  min: 0,
225
255
  max: 1
226
256
  }
@@ -229,115 +259,292 @@ const TrendChart = ({ deviceId, measures, enableExportData, enableDatePicker, ha
229
259
  setDataMeasures([...[]]);
230
260
  setCsvData("");
231
261
  };
262
+ const handleSpanGaps = (spanG) => {
263
+ setOptions({
264
+ ...options,
265
+ spanGaps: spanG,
266
+ });
267
+ setSpanGapsOption(spanG);
268
+ };
232
269
  const handleChange = (event, newPeriod) => {
233
- setZoomed(false);
234
- setDatePickerUsed(false);
235
- setCsvData('');
236
- if (newPeriod === "ALL") {
237
- setTimeStart(firstTimestamp - 86400);
238
- setTimeEnd(moment().unix());
239
- setChartPeriod(newPeriod);
270
+ if (newPeriod == null) {
240
271
  return;
241
272
  }
242
- const periodConfig = chartConfigByPeriod[newPeriod];
243
- if (periodConfig) {
273
+ if (chartPeriod === newPeriod) {
274
+ setChartLoading(true);
275
+ setZoomed(false);
276
+ setDatePickerUsed(false);
277
+ setCsvData('');
278
+ const periodConfig = chartConfigByPeriod[chartPeriod];
244
279
  setTimeStart(Math.round(moment().subtract(periodConfig.from).valueOf() / 1000));
245
280
  setTimeEnd(Math.round(moment().valueOf() / 1000));
246
- setChartPeriod(newPeriod);
281
+ setChartPeriod(chartPeriod);
247
282
  setChartPeriodConfig(periodConfig);
248
283
  }
284
+ if (newPeriod && newPeriod !== chartPeriod) {
285
+ setChartLoading(true);
286
+ setZoomed(false);
287
+ setDatePickerUsed(false);
288
+ setCsvData('');
289
+ if (newPeriod === "ALL") {
290
+ setTimeStart(1577854800);
291
+ setTimeEnd(moment().unix());
292
+ setChartPeriod(newPeriod);
293
+ return;
294
+ }
295
+ const periodConfig = chartConfigByPeriod[newPeriod];
296
+ if (periodConfig) {
297
+ setTimeStart(Math.round(moment().subtract(periodConfig.from).valueOf() / 1000));
298
+ setTimeEnd(Math.round(moment().valueOf() / 1000));
299
+ setChartPeriod(newPeriod);
300
+ setChartPeriodConfig(periodConfig);
301
+ }
302
+ }
249
303
  };
250
304
  const handleExportData = async () => {
251
305
  setLoadingButton(true);
252
- const intervalInSeconds = timeEnd - timeStart;
253
- const data = await Promise.all(measures.map(async (measure) => {
254
- const polltime = getPollTime(intervalInSeconds, measure.polltime);
255
- return await handleGetInfluxData(measure.name, timeStart, timeEnd, deviceId, polltime, true);
256
- }));
257
- const csvData = getCsvData(data, measures);
258
- setCsvData(csvData);
259
- setLoadingButton(false);
306
+ if (handleExportDataCB) {
307
+ const data = await Promise.all(measures.map(async (measure) => {
308
+ return await handleExportDataCB(measure.name, timeStart, timeEnd, deviceId);
309
+ }));
310
+ const csvData = getCsvData(data, measures);
311
+ setCsvData(csvData);
312
+ setLoadingButton(false);
313
+ }
260
314
  };
261
315
  useEffect(() => {
262
316
  if (csvData.length > 0) {
263
317
  csvLinkRef.current?.link.click();
264
318
  }
265
319
  }, [csvData]);
320
+ useEffect(() => {
321
+ if (annotationsDataFn && annotentionsEnabled) {
322
+ (async () => {
323
+ const resp = await annotationsDataFn();
324
+ setAnnotationsData(resp);
325
+ })();
326
+ }
327
+ }, [annotationsDataFn, annotentionsEnabled]);
328
+ function getDwSlotsFromValues(dwValues) {
329
+ const slots = [];
330
+ let start = null;
331
+ for (const point of dwValues) {
332
+ if (point.y === 1 && start === null) {
333
+ start = point.x;
334
+ }
335
+ if (point.y === 0 && start !== null) {
336
+ slots.push({ start, end: point.x });
337
+ start = null;
338
+ }
339
+ }
340
+ return slots;
341
+ }
266
342
  const loadDatasets = async (chartPeriod) => {
267
- setChartLoading(true);
268
- const intervalInSeconds = timeEnd - timeStart;
269
- // Inizializza un array di promesse per ottenere i dati per ciascuna misura
270
- const datasetsPromises = measures.map(async (measure) => {
271
- const polltime = getPollTime(intervalInSeconds, measure.polltime);
272
- const influxData = await handleGetInfluxData(measure.name, timeStart, timeEnd, deviceId, polltime, false);
273
- const points = GetPoints(influxData);
343
+ let intervalInSeconds = chartPeriod === "ALL" && !datePickerUsed && !zoomed ? 31536000 : timeEnd - timeStart;
344
+ const rawQuery = intervalInSeconds < 86400;
345
+ let tempSlots = [];
346
+ if (handleGetDwSlotsCB) {
347
+ const dwValues = await handleGetDwSlotsCB(timeStart, timeEnd, deviceId);
348
+ tempSlots = getDwSlotsFromValues(dwValues);
349
+ }
350
+ // Combine measures and track their source
351
+ const allMeasures = [
352
+ ...(measures1?.map(m => ({ ...m, source: 'measures1' })) || []),
353
+ ...(measures2?.map(m => ({ ...m, source: 'measures2' })) || [])
354
+ ];
355
+ const datasetsPromises = allMeasures.map(async (measure) => {
356
+ const polltime = getPollTime(intervalInSeconds, measure.polltime || 30);
357
+ let dwPoints = [];
358
+ let dwSlots = [];
359
+ if (measure.dwPolltime) {
360
+ const dwPolltime = getPollTime(intervalInSeconds, measure.dwPolltime);
361
+ // Query DW solo tag=100
362
+ const rawDwPoints = await handleGetInfluxData(measure.name, timeStart, timeEnd, deviceId, dwPolltime, false, "previous", 100, FilterTagMode.DW);
363
+ dwSlots = tempSlots;
364
+ if (dwSlots.length > 0) {
365
+ dwPoints = rawDwPoints.filter((p) => dwSlots.some(slot => p.x >= slot.start && p.x <= slot.end));
366
+ }
367
+ else {
368
+ dwPoints = rawDwPoints;
369
+ }
370
+ }
371
+ const stdRaw = await handleGetInfluxData(measure.name, timeStart, timeEnd, deviceId, polltime, !measure.polltime && rawQuery, !measure.polltime ? "none" : "null", 100, FilterTagMode.Exclude);
372
+ // Mappa sul formato Point
373
+ const stdPoints = stdRaw
374
+ .map((r) => ({ x: Number(r.x), y: r.y }))
375
+ .filter((p) => p.y !== null);
376
+ const filtered = dwSlots.length
377
+ ? stdPoints.filter((p) => !dwSlots.some(slot => p.x >= slot.start && p.x <= slot.end))
378
+ : stdPoints;
379
+ let combined;
380
+ // 6) Unisco e ordino
381
+ if (dwPoints.length > 2 && dwSlots.length > 0) {
382
+ combined = [...dwPoints, ...filtered]
383
+ .sort((a, b) => a.x - b.x);
384
+ }
385
+ else if (dwPoints.length > 2 && dwSlots.length <= 0) {
386
+ combined = [...dwPoints];
387
+ }
388
+ else {
389
+ combined = [...filtered];
390
+ }
274
391
  return {
275
- label: measure.name,
276
- data: points,
392
+ label: measure.description || measure.name,
393
+ data: combined,
277
394
  unit: measure.unit,
278
- // borderColor: `hsl(${index * 50}, 70%, 50%)`, // Colore unico per ogni dataset
279
395
  borderWidth: 2,
280
396
  pointRadius: 1,
281
397
  pointHoverRadius: 5,
282
398
  pointHoverBackgroundColor: 'rgba(52, 125, 236, 0.5)',
283
- spanGaps: false,
284
- fill: false
399
+ change: !measure.polltime,
400
+ stepped: measure.stepped,
401
+ yAxisID: measure.source === 'measures1' ? 'y1' : 'y2'
285
402
  };
286
403
  });
287
- // Risolvi tutte le promesse per popolare i dataset
288
404
  const datasets = await Promise.all(datasetsPromises);
289
- let min = null;
290
- let max = null;
291
- let values = [];
405
+ let min1 = null;
406
+ let max1 = null;
407
+ let min2 = null;
408
+ let max2 = null;
409
+ let time;
410
+ let minTime = null;
292
411
  datasets.forEach(dataset => {
293
- values = [...values, ...dataset.data.map((point) => point.y).filter((data) => data !== null)];
412
+ const values = dataset.data.map((point) => point.y).filter((data) => data !== null);
413
+ if (chartPeriod === "ALL" && !datePickerUsed && !zoomed && values.length) {
414
+ time = dataset.data.filter((data) => data.y !== null).map((data) => data.x)[0];
415
+ }
294
416
  const datasetMin = Math.min(...values);
295
417
  const datasetMax = Math.max(...values);
296
- if (min === null || datasetMin < min)
297
- min = datasetMin;
298
- if (max === null || datasetMax > max)
299
- max = datasetMax;
418
+ if (dataset.yAxisID === 'y1') {
419
+ if (min1 === null || datasetMin < min1)
420
+ min1 = datasetMin;
421
+ if (max1 === null || datasetMax > max1)
422
+ max1 = datasetMax;
423
+ }
424
+ else {
425
+ if (min2 === null || datasetMin < min2)
426
+ min2 = datasetMin;
427
+ if (max2 === null || datasetMax > max2)
428
+ max2 = datasetMax;
429
+ }
430
+ if (time && (minTime === null || time < minTime))
431
+ minTime = Math.floor(time / 1000) - 86400;
300
432
  });
301
- let paddedMin = null;
302
- let paddedMax = null;
303
- if (min !== null && max !== null) {
304
- const diff = Math.round((max - min) / 3) || 1;
305
- paddedMin = min - diff;
306
- paddedMax = max + diff;
433
+ const getPaddedMinMax = (min, max) => {
434
+ if (min === null || max === null)
435
+ return { paddedMin: null, paddedMax: null };
436
+ const diff = ((max - min) * 0.2) < 0.1 ? 0.1 : (max - min) * 0.2;
437
+ return {
438
+ paddedMin: Math.floor((min - diff) * 10) / 10,
439
+ paddedMax: Math.ceil((max + diff) * 10) / 10
440
+ };
441
+ };
442
+ const { paddedMin: paddedMin1, paddedMax: paddedMax1 } = getPaddedMinMax(min1, max1);
443
+ const { paddedMin: paddedMin2, paddedMax: paddedMax2 } = getPaddedMinMax(min2, max2);
444
+ // Handle undefined/null annotationsData
445
+ let dynamicAnnotations = {};
446
+ if (Array.isArray(annotationsData) && annotationsData.length > 0 && annotentionsEnabled) {
447
+ dynamicAnnotations = annotationsData.reduce((acc, [timestamp, label], index) => {
448
+ const yVal = paddedMin1 !== null && paddedMin1 !== undefined
449
+ ? paddedMin1 + 0.01 * (paddedMax1 - paddedMin1)
450
+ : paddedMin2 !== null && paddedMin2 !== undefined
451
+ ? paddedMin2 + 0.01 * (paddedMax2 - paddedMin2)
452
+ : 0;
453
+ acc[`line${index}`] = {
454
+ type: 'line',
455
+ xMin: timestamp,
456
+ xMax: timestamp,
457
+ borderColor: 'rgba(255, 225, 0, 0.8)',
458
+ borderWidth: 2,
459
+ drawTime: 'afterDatasetsDraw',
460
+ label: {
461
+ content: label,
462
+ enabled: false
463
+ }
464
+ };
465
+ acc[`triangle${index}`] = {
466
+ type: 'point',
467
+ xValue: timestamp,
468
+ yValue: yVal,
469
+ backgroundColor: 'rgba(255, 225, 0, 0.8)',
470
+ pointStyle: 'triangle',
471
+ radius: 6,
472
+ rotation: 0,
473
+ tooltip: {
474
+ enabled: true,
475
+ callbacks: {
476
+ label: () => label
477
+ }
478
+ }
479
+ };
480
+ acc[`label${index}`] = {
481
+ type: 'label',
482
+ xValue: timestamp,
483
+ yValue: yVal + 0.1 * (((paddedMax1 ?? paddedMax2 ?? 0) - (paddedMax1 ?? paddedMax2 ?? 0))),
484
+ xAdjust: 0,
485
+ yAdjust: -20,
486
+ backgroundColor: 'rgba(245,245,245)',
487
+ borderColor: 'rgba(255, 225, 0, 0.8)',
488
+ borderWidth: 1,
489
+ content: [label],
490
+ textAlign: 'start',
491
+ font: {
492
+ size: 10,
493
+ weight: 'bold'
494
+ },
495
+ callout: {
496
+ display: false,
497
+ }
498
+ };
499
+ return acc;
500
+ }, {});
307
501
  }
308
- setDataMeasures([...datasets]);
502
+ // 👇 Combina statiche + dinamiche
503
+ const annotations = {
504
+ // ...staticAnnotations,
505
+ ...dynamicAnnotations
506
+ };
507
+ // 👇 Configurazione completa
508
+ setDataMeasures(datasets);
509
+ setTimeStartPicker(minTime || timeStart);
309
510
  setOptions({
310
511
  ...options,
311
512
  scales: {
312
513
  ...options.scales,
313
- y: {
314
- ...options.scales.y,
315
- min: paddedMin,
316
- max: paddedMax,
514
+ y1: {
515
+ ...options.scales.y1,
516
+ min: paddedMin1,
517
+ max: paddedMax1,
518
+ position: "left",
519
+ display: "auto"
520
+ },
521
+ y2: {
522
+ ...options.scales.y2,
523
+ min: paddedMin2,
524
+ max: paddedMax2,
525
+ position: "right",
526
+ display: "auto"
317
527
  },
318
528
  x: {
319
529
  ...options.scales.x,
320
- min: moment.unix(timeStart).toString(),
321
- max: moment.unix(timeEnd).toString(),
322
- time: {
323
- ...options.scales.x.time,
324
- unit: chartPeriod.scaleUnit
325
- }
530
+ min: moment.unix(minTime || timeStart).toString(),
531
+ max: moment.unix(timeEnd).toString()
532
+ }
533
+ },
534
+ plugins: {
535
+ ...options.plugins,
536
+ annotation: {
537
+ interaction: {
538
+ mode: 'nearest',
539
+ intersect: true
540
+ },
541
+ annotations
326
542
  }
327
543
  }
328
544
  });
329
545
  };
330
546
  useEffect(() => {
331
- const fetchFirstTimestamp = async () => {
332
- const response = await handleGetFirstTimestamp(deviceId);
333
- if (response) {
334
- setFirstTimestamp(response);
335
- }
336
- };
337
- fetchFirstTimestamp();
338
- }, []);
339
- useEffect(() => {
340
- const timeDifference = Math.abs(moment(timeStart).valueOf() - moment(timeEnd).valueOf()); // Convert milliseconds to seconds
547
+ const timeDifference = Math.abs(moment(timeEnd).valueOf() - moment(timeStart).valueOf()); // Convert milliseconds to seconds
341
548
  let newChartPeriod = '1D'; // Default to 1 day
342
549
  if (timeDifference < 86400) { // Less than 1 day
343
550
  newChartPeriod = '1H'; // Set to 1 day
@@ -345,26 +552,34 @@ const TrendChart = ({ deviceId, measures, enableExportData, enableDatePicker, ha
345
552
  else if (timeDifference < 604800) { // Less than 1 week
346
553
  newChartPeriod = '1D'; // Set to 1 day
347
554
  }
348
- else if (timeDifference < 2629800) { // Less than 1 month
555
+ else if (timeDifference < 2678400) { // Less than 1 month
349
556
  newChartPeriod = '1W'; // Set to 1 week
350
557
  }
351
- else if (timeDifference < 7889400) { // Less than 3 months
558
+ else if (timeDifference < 8035200) { // Less than 3 months
352
559
  newChartPeriod = '1M'; // Set to 1 month
353
560
  }
354
- else if (timeDifference < 15778440) { // Less than 6 months
561
+ else if (timeDifference < 16070400) { // Less than 6 months
355
562
  newChartPeriod = '3M'; // Set to 3 months
356
563
  }
357
- else if (timeDifference < 31556926) { // Less than 1 year
564
+ else if (timeDifference < 31536000) { // Less than 1 year
358
565
  newChartPeriod = '6M'; // Set to 6 months
359
566
  }
360
567
  else {
361
568
  newChartPeriod = '1Y'; // Set to 1 year
362
569
  }
363
570
  setChartPeriodConfig(chartConfigByPeriod[newChartPeriod]);
571
+ // check prev measures value in order to show the loader
572
+ // hide the loader if measure is the same (for interval get measure value)
573
+ const prevMeasuresValue = prevMeasures.current || [];
574
+ //@ts-ignore
575
+ prevMeasures.current = measures;
576
+ if (!prevMeasuresValue || prevMeasuresValue.length !== measures?.length || (prevMeasuresValue[0] && prevMeasuresValue[0].name !== measures[0].name)) {
577
+ setChartLoading(true);
578
+ }
364
579
  loadDatasets(chartPeriod).then(() => {
365
580
  setChartLoading(false);
366
581
  });
367
- }, [timeEnd, timeStart]);
582
+ }, [measures1, timeEnd, timeStart, measures2, annotentionsEnabled, annotationsData]);
368
583
  useEffect(() => {
369
584
  const loadZoomPlugin = async () => {
370
585
  const zoomPlugin = (await import('chartjs-plugin-zoom')).default;
@@ -375,8 +590,24 @@ const TrendChart = ({ deviceId, measures, enableExportData, enableDatePicker, ha
375
590
  const activePoint = chart.tooltip._active[0];
376
591
  const ctx = chart.ctx;
377
592
  const x = activePoint.element.x;
378
- const topY = chart.scales.y.top;
379
- const bottomY = chart.scales.y.bottom;
593
+ let topY1 = 0;
594
+ let topY2 = 0;
595
+ let bottomY1 = 0;
596
+ let bottomY2 = 0;
597
+ if (chart.scales.y1?.top) {
598
+ topY1 = chart.scales.y1.top;
599
+ }
600
+ if (chart.scales.y2?.top) {
601
+ topY2 = chart.scales.y2.top;
602
+ }
603
+ if (chart.scales.y1?.bottom) {
604
+ bottomY1 = chart.scales.y1.bottom;
605
+ }
606
+ if (chart.scales.y2?.bottom) {
607
+ bottomY2 = chart.scales.y2.bottom;
608
+ }
609
+ const topY = Math.max(topY1, topY2);
610
+ const bottomY = Math.min(bottomY1, bottomY2);
380
611
  ctx.save();
381
612
  ctx.beginPath();
382
613
  ctx.moveTo(x, topY);
@@ -393,34 +624,41 @@ const TrendChart = ({ deviceId, measures, enableExportData, enableDatePicker, ha
393
624
  loadZoomPlugin();
394
625
  resetChart();
395
626
  }, []);
396
- const datePicker = _jsx(Box, { sx: { display: 'flex', alignItems: 'center', mr: { xs: 0, lg: 2 } }, children: _jsxs(LocalizationProvider, { dateAdapter: AdapterMoment, adapterLocale: "it", children: [_jsx(DateTimePicker, { value: moment(timeStart * 1000),
397
- // format="DD/MM/YY hh:mm A"
398
- onChange: (newValue) => {
399
- setTimeStart((moment(newValue).unix()));
400
- }, maxDate: moment(timeEnd * 1000), slotProps: {
627
+ const datePicker = _jsx(Box, { sx: { display: 'flex', alignItems: 'center', mr: { xs: 0, lg: 2 } }, children: _jsxs(LocalizationProvider, { dateAdapter: AdapterMoment, adapterLocale: "it", children: [_jsx(DateTimePicker, { value: moment(timeStartPicker * 1000), onChange: (newValue) => {
628
+ setChartLoading(true);
629
+ setDatePickerUsed(true);
630
+ setZoomed(false);
631
+ setPickerTimeStart(moment(newValue).unix());
632
+ setTimeStart(moment(newValue).unix());
633
+ }, maxDateTime: moment(timeEnd * 1000), slotProps: {
401
634
  textField: { size: 'small', sx: { width: { sm: 210 } } }
402
- } }), " \u00A0\u00A0 ", _jsx(Box, { children: "\u2013" }), " \u00A0\u00A0", _jsx(DateTimePicker, { value: moment(timeEnd * 1000),
403
- // format="DD/MM/YY hh:mm A"
404
- onChange: (newValue) => {
405
- setTimeEnd((moment(newValue).unix()));
406
- }, minDate: moment(timeStart * 1000), slotProps: {
635
+ } }), " \u00A0\u00A0 ", _jsx(Box, { children: "\u2013" }), " \u00A0\u00A0", _jsx(DateTimePicker, { value: moment(timeEnd * 1000), onChange: (newValue) => {
636
+ setChartLoading(true);
637
+ setDatePickerUsed(true);
638
+ setZoomed(false);
639
+ setPickerTimeEnd(moment(newValue).unix());
640
+ setTimeEnd(moment(newValue).unix());
641
+ }, minDateTime: moment(timeStart * 1000), slotProps: {
407
642
  textField: { size: 'small', sx: { width: { sm: 210 } } }
408
643
  } })] }) });
409
- return (_jsxs(ThemeProvider, { theme: theme, children: [enableDatePicker && _jsx(Box, { sx: { display: { xs: 'flex', lg: 'none', justifyContent: 'flex-end' }, mb: 2 }, children: datePicker }), _jsxs(Box, { sx: { display: 'flex', alignItems: 'center', justifyContent: 'space-between' }, children: [_jsxs(Box, { children: [enableExportData && (_jsxs(_Fragment, { children: [_jsxs(LoadingButton, { sx: { minWidth: '40px !important', mr: 1, px: 1 }, loading: loadingButton, type: "submit", variant: "text", color: "primary", onClick: handleExportData, disabled: !dataMeasures || !dataMeasures.length, size: 'small', children: [_jsx(LoginIcon, { fontSize: 'small', style: { transform: "rotate(90deg)" } }), _jsx(Box, { sx: { display: { xs: 'none', xl: 'block' }, ml: { md: 1 } }, children: "Export" })] }), _jsx(CSVLink
644
+ return (_jsxs(ThemeProvider, { theme: theme, children: [enableDatePicker && _jsx(Box, { sx: { display: { xs: 'flex', lg: 'none', justifyContent: 'flex-end' }, mb: 2 }, children: datePicker }), _jsxs(Box, { sx: { display: 'flex', alignItems: 'center', justifyContent: 'space-between' }, children: [_jsxs(Box, { sx: { display: 'flex', alignItems: 'center' }, children: [!enableExportData && measures && measures.length === 1 ? _jsx(Typography, { variant: 'body1', sx: { mr: 2 }, children: measures[0].description || measures[0].name }) : '', enableExportData && (_jsxs(_Fragment, { children: [_jsxs(LoadingButton, { sx: { minWidth: '40px !important', mr: 1, px: 1 }, loading: loadingButton, type: "submit", variant: "text", color: "primary", onClick: handleExportData, disabled: !dataMeasures || !dataMeasures.length, size: 'small', children: [_jsx(LoginIcon, { fontSize: 'small', style: { transform: "rotate(90deg)" } }), _jsx(Box, { sx: { display: { xs: 'none', xl: 'block' }, ml: { md: 1 } }, children: "Export" })] }), _jsx(CSVLink
410
645
  //@ts-ignore
411
646
  , {
412
647
  //@ts-ignore
413
- ref: csvLinkRef, data: csvData, filename: `export_${moment().toISOString()}.csv`, separator: ';' })] })), zoomed && (_jsxs(Button, { sx: { minWidth: '40px !important', boxShadow: 1, px: 1 }, variant: "contained", color: "secondary", size: 'small', onClick: () => {
414
- if (datePickerUsed) {
415
- setZoomed(false);
416
- }
417
- else if (chartPeriod === "ALL") {
418
- setZoomed(false);
648
+ ref: csvLinkRef, data: csvData, filename: `export_${moment().toISOString()}.csv`, separator: ';' })] })), zoomed && measures.length > 0 && (_jsxs(Button, { sx: { minWidth: '40px !important', boxShadow: 1, px: 1 }, variant: "contained", color: "secondary", size: 'small', onClick: () => {
649
+ setChartLoading(true);
650
+ setZoomed(false);
651
+ if (chartPeriod === "ALL") {
652
+ setDatePickerUsed(false);
653
+ setTimeStart(1577854800);
419
654
  setTimeEnd(moment().unix());
420
- setTimeStart(firstTimestamp - 86400);
655
+ }
656
+ else if (datePickerUsed) {
657
+ setTimeStart(pickerTimeStart);
658
+ setTimeEnd(pickerTimeEnd);
421
659
  }
422
660
  else {
423
- setZoomed(false);
661
+ setDatePickerUsed(false);
424
662
  setChartPeriodConfig(chartConfigByPeriod[chartPeriod]);
425
663
  setTimeStart(chartPeriodConfig.from?.days
426
664
  ? moment()
@@ -429,12 +667,15 @@ const TrendChart = ({ deviceId, measures, enableExportData, enableDatePicker, ha
429
667
  : moment().subtract(1, 'day').unix());
430
668
  setTimeEnd(moment().unix());
431
669
  }
432
- }, children: [_jsx(ZoomOut, { fontSize: 'small' }), _jsx(Box, { sx: { display: { xs: 'none', xl: 'block' }, ml: { md: 1 } }, children: "Reset" })] }))] }), _jsxs(Box, { sx: { display: 'flex', justifyContent: 'flex-end' }, children: [enableDatePicker && _jsx(Box, { sx: { display: { xs: 'none', lg: 'flex' } }, children: datePicker }), _jsxs(ToggleButtonGroup, { color: "primary", value: chartPeriod, exclusive: true, onChange: handleChange, size: "small",
433
- // sx={{ boxShadow: 1 }}
434
- disabled: chartLoading, children: [_jsx(ToggleButton, { value: "1D", sx: { px: 1 }, children: "1d" }), _jsx(ToggleButton, { value: "1W", sx: { px: 1 }, children: "1w" }), _jsx(ToggleButton, { value: "1M", sx: { px: 1 }, children: "1M" }), _jsx(ToggleButton, { value: "3M", sx: { px: 1 }, children: "3M" }), _jsx(ToggleButton, { value: "6M", sx: { px: 1 }, children: "6M" }), _jsx(ToggleButton, { value: "1Y", sx: { px: 1 }, children: "1Y" }), _jsx(ToggleButton, { value: "ALL", sx: { px: 1 }, children: "ALL" })] })] })] }), _jsx(Box, { component: 'div', className: "chart-container", sx: { height: { xs: enableDatePicker && props.height ? `calc(${props.height} - 50px)` : props.height, lg: props.height }, minHeight: 300, mt: 2 }, children: chartJsLoaded && !chartLoading && typeof window !== 'undefined' ?
435
- _jsx(_Fragment, { children: dataMeasures && dataMeasures[0]?.data?.length ?
670
+ }, children: [_jsx(ZoomOut, { fontSize: 'small' }), _jsx(Box, { sx: { display: { xs: 'none', lg: 'block' }, ml: { md: 1 } }, children: "Reset" })] }))] }), _jsxs(Box, { sx: { display: 'flex', justifyContent: 'flex-end' }, children: [enableDatePicker && _jsx(Box, { sx: { display: { xs: 'none', lg: 'flex' } }, children: datePicker }), _jsxs(ToggleButtonGroup, { color: "primary", value: !datePickerUsed && !zoomed ? chartPeriod : null, exclusive: true, onChange: handleChange, size: "small", sx: {
671
+ '& .MuiToggleButton-root': {
672
+ color: 'text.primary', fontSize: '0.95rem', fontWeight: 'normal', paddingTop: '6px', paddingBottom: '6px'
673
+ }
674
+ }, disabled: chartLoading, children: [_jsx(ToggleButton, { value: "1D", sx: { px: 1 }, children: "1d" }), _jsx(ToggleButton, { value: "1W", sx: { px: 1 }, children: "1w" }), _jsx(ToggleButton, { value: "1M", sx: { px: 1 }, children: "1M" }), _jsx(ToggleButton, { value: "3M", sx: { px: 1 }, children: "3M" }), _jsx(ToggleButton, { value: "6M", sx: { px: 1 }, children: "6M" }), _jsx(ToggleButton, { value: "1Y", sx: { px: 1 }, children: "1Y" }), _jsx(ToggleButton, { value: "ALL", sx: { px: 1 }, children: "ALL" })] }), _jsx(MuiTooltip, { placement: "top", arrow: true, title: "Connect point values", children: _jsx(ToggleButton, { value: "check", color: "primary", size: "small", selected: spanGapsOption, disabled: chartLoading, onChange: () => handleSpanGaps(!spanGapsOption), sx: { ml: 1 }, children: _jsx(TimelineIcon, {}) }) }), annotationsData !== undefined && (_jsx(MuiTooltip, { placement: "top", arrow: true, title: "Show annotations", children: _jsx(ToggleButton, { value: "check", color: "primary", size: "small", selected: annotentionsEnabled, disabled: chartLoading, onChange: () => setAnnotationsEnabled(!annotentionsEnabled), sx: { ml: 1 }, children: _jsx(EditNoteIcon, {}) }) }))] })] }), _jsx(Box, { component: 'div', className: "chart-container", sx: { mt: 2, height: '100%' }, children: chartJsLoaded && !chartLoading && typeof window !== 'undefined' ?
675
+ _jsx(_Fragment, { children: dataMeasures && (dataMeasures.length > 1 || (dataMeasures.length === 1 && dataMeasures[0].data?.length)) ?
436
676
  (_jsx(Line, { options: options, data: {
437
- datasets: dataMeasures || [{ data: [] }]
677
+ // datasets: dataMeasures || [{ data: [] }]
678
+ datasets: dataMeasures.map(d => ({ ...d, showLine: spanGapsOption || !d.change })) || [{ data: [] }]
438
679
  } })) : _jsxs(Box, { sx: {
439
680
  display: 'flex',
440
681
  flexDirection: 'column',
@@ -448,7 +689,7 @@ const TrendChart = ({ deviceId, measures, enableExportData, enableDatePicker, ha
448
689
  alignItems: 'center',
449
690
  justifyContent: 'center',
450
691
  textAlign: 'center',
451
- height: '100%'
692
+ height: 'calc(100% - 50px)'
452
693
  }, children: _jsx(CircularProgress, {}) })) })] }));
453
694
  };
454
695
  export default TrendChart;