@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.
- package/components/charts/TrendChart.d.ts +14 -5
- package/components/charts/TrendChart.js +397 -156
- package/components/groups/GroupUpdate.d.ts +1 -1
- package/components/groups/GroupUpdate.js +1 -1
- package/components/groups/GroupsDevices.d.ts +2 -2
- package/components/groups/GroupsDevices.js +27 -13
- package/package.json +3 -2
- package/server-actions/annotations.d.ts +4 -0
- package/server-actions/annotations.js +15 -0
- package/server-actions/groups.d.ts +1 -8
- package/server-actions/index.d.ts +1 -0
- package/server-actions/index.js +1 -0
- package/server-actions/influx.d.ts +5 -13
- package/server-actions/influx.js +202 -98
- package/server-actions/trackle.d.ts +3 -2
- package/server-actions/trackle.js +4 -17
- package/server-actions/types.d.ts +24 -0
- package/server-actions/types.js +6 -0
- package/types/user.d.ts +1 -0
@@ -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(
|
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
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
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
|
-
//
|
142
|
-
const headers = [
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
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
|
-
//
|
149
|
-
data.forEach((
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
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
|
-
//
|
164
|
-
Object.
|
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(
|
167
|
-
|
168
|
-
|
178
|
+
measures.forEach(m => {
|
179
|
+
row.push(entry[m.name] !== undefined
|
180
|
+
? entry[m.name]
|
181
|
+
: '');
|
169
182
|
});
|
170
|
-
//
|
171
|
-
const
|
172
|
-
|
173
|
-
|
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
|
-
//
|
178
|
-
return
|
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,
|
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 [
|
188
|
-
const [
|
189
|
-
const [
|
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
|
-
|
223
|
-
...options.scales.
|
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
|
-
|
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
|
-
|
243
|
-
|
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(
|
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
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
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
|
-
|
268
|
-
const
|
269
|
-
|
270
|
-
|
271
|
-
const
|
272
|
-
|
273
|
-
|
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:
|
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
|
-
|
284
|
-
|
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
|
290
|
-
let
|
291
|
-
let
|
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 =
|
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 (
|
297
|
-
|
298
|
-
|
299
|
-
|
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
|
-
|
302
|
-
|
303
|
-
|
304
|
-
const diff =
|
305
|
-
|
306
|
-
|
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
|
-
|
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
|
-
|
314
|
-
...options.scales.
|
315
|
-
min:
|
316
|
-
max:
|
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
|
-
|
323
|
-
|
324
|
-
|
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
|
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 <
|
555
|
+
else if (timeDifference < 2678400) { // Less than 1 month
|
349
556
|
newChartPeriod = '1W'; // Set to 1 week
|
350
557
|
}
|
351
|
-
else if (timeDifference <
|
558
|
+
else if (timeDifference < 8035200) { // Less than 3 months
|
352
559
|
newChartPeriod = '1M'; // Set to 1 month
|
353
560
|
}
|
354
|
-
else if (timeDifference <
|
561
|
+
else if (timeDifference < 16070400) { // Less than 6 months
|
355
562
|
newChartPeriod = '3M'; // Set to 3 months
|
356
563
|
}
|
357
|
-
else if (timeDifference <
|
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
|
-
|
379
|
-
|
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(
|
397
|
-
|
398
|
-
|
399
|
-
|
400
|
-
|
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
|
-
|
404
|
-
|
405
|
-
|
406
|
-
|
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
|
-
|
415
|
-
|
416
|
-
|
417
|
-
|
418
|
-
|
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
|
-
|
655
|
+
}
|
656
|
+
else if (datePickerUsed) {
|
657
|
+
setTimeStart(pickerTimeStart);
|
658
|
+
setTimeEnd(pickerTimeEnd);
|
421
659
|
}
|
422
660
|
else {
|
423
|
-
|
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',
|
433
|
-
|
434
|
-
|
435
|
-
|
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;
|