@c8y/ngx-components 1024.1.6 → 1024.2.4

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.
Files changed (53) hide show
  1. package/fesm2022/c8y-ngx-components-auth-configuration.mjs +18 -16
  2. package/fesm2022/c8y-ngx-components-auth-configuration.mjs.map +1 -1
  3. package/fesm2022/c8y-ngx-components-datapoint-explorer-view.mjs +64 -25
  4. package/fesm2022/c8y-ngx-components-datapoint-explorer-view.mjs.map +1 -1
  5. package/fesm2022/c8y-ngx-components-datapoint-selector.mjs +363 -38
  6. package/fesm2022/c8y-ngx-components-datapoint-selector.mjs.map +1 -1
  7. package/fesm2022/c8y-ngx-components-echart-models.mjs.map +1 -1
  8. package/fesm2022/c8y-ngx-components-echart.mjs +796 -123
  9. package/fesm2022/c8y-ngx-components-echart.mjs.map +1 -1
  10. package/fesm2022/c8y-ngx-components-global-context.mjs +391 -54
  11. package/fesm2022/c8y-ngx-components-global-context.mjs.map +1 -1
  12. package/fesm2022/c8y-ngx-components-time-context.mjs +146 -20
  13. package/fesm2022/c8y-ngx-components-time-context.mjs.map +1 -1
  14. package/fesm2022/c8y-ngx-components-widgets-implementations-datapoints-graph.mjs +198 -24
  15. package/fesm2022/c8y-ngx-components-widgets-implementations-datapoints-graph.mjs.map +1 -1
  16. package/fesm2022/c8y-ngx-components.mjs +100 -32
  17. package/fesm2022/c8y-ngx-components.mjs.map +1 -1
  18. package/locales/de.po +80 -8
  19. package/locales/es.po +80 -8
  20. package/locales/fr.po +80 -8
  21. package/locales/ja_JP.po +80 -8
  22. package/locales/ko.po +80 -8
  23. package/locales/locales.pot +79 -7
  24. package/locales/nl.po +80 -8
  25. package/locales/pl.po +80 -8
  26. package/locales/pt_BR.po +80 -8
  27. package/locales/zh_CN.po +80 -8
  28. package/locales/zh_TW.po +80 -8
  29. package/package.json +1 -1
  30. package/types/c8y-ngx-components-auth-configuration.d.ts +3 -3
  31. package/types/c8y-ngx-components-auth-configuration.d.ts.map +1 -1
  32. package/types/c8y-ngx-components-datapoint-explorer-view.d.ts +4 -1
  33. package/types/c8y-ngx-components-datapoint-explorer-view.d.ts.map +1 -1
  34. package/types/c8y-ngx-components-datapoint-selector.d.ts +93 -5
  35. package/types/c8y-ngx-components-datapoint-selector.d.ts.map +1 -1
  36. package/types/c8y-ngx-components-echart-models.d.ts +25 -3
  37. package/types/c8y-ngx-components-echart-models.d.ts.map +1 -1
  38. package/types/c8y-ngx-components-echart.d.ts +65 -5
  39. package/types/c8y-ngx-components-echart.d.ts.map +1 -1
  40. package/types/c8y-ngx-components-global-context.d.ts +187 -7
  41. package/types/c8y-ngx-components-global-context.d.ts.map +1 -1
  42. package/types/c8y-ngx-components-time-context.d.ts +50 -8
  43. package/types/c8y-ngx-components-time-context.d.ts.map +1 -1
  44. package/types/c8y-ngx-components-widgets-implementations-alarms.d.ts +1 -0
  45. package/types/c8y-ngx-components-widgets-implementations-alarms.d.ts.map +1 -1
  46. package/types/c8y-ngx-components-widgets-implementations-datapoints-graph.d.ts +25 -0
  47. package/types/c8y-ngx-components-widgets-implementations-datapoints-graph.d.ts.map +1 -1
  48. package/types/c8y-ngx-components-widgets-implementations-datapoints-list.d.ts +1 -0
  49. package/types/c8y-ngx-components-widgets-implementations-datapoints-list.d.ts.map +1 -1
  50. package/types/c8y-ngx-components-widgets-implementations-scada.d.ts +2 -0
  51. package/types/c8y-ngx-components-widgets-implementations-scada.d.ts.map +1 -1
  52. package/types/c8y-ngx-components.d.ts +50 -11
  53. package/types/c8y-ngx-components.d.ts.map +1 -1
@@ -3,22 +3,23 @@ import { Injectable, inject, SecurityContext, ViewChild, Input, Component, Event
3
3
  import { gettext } from '@c8y/ngx-components/gettext';
4
4
  import { from, merge, interval, BehaviorSubject, of, lastValueFrom, defer, forkJoin } from 'rxjs';
5
5
  import { mergeMap, map, tap, throttleTime, buffer, switchMap } from 'rxjs/operators';
6
- import * as i1 from '@c8y/ngx-components';
6
+ import * as i1$1 from '@c8y/ngx-components';
7
7
  import { CommonModule, FormsModule, IconDirective, C8yTranslatePipe, WidgetTimeContextMediatorService, WidgetTimeContextDateRangeService, ThemeSwitcherService, DismissAlertStrategy, DynamicComponentAlert, CoreModule, MeasurementRealtimeService, AlarmRealtimeService, EventRealtimeService } from '@c8y/ngx-components';
8
+ import chroma from 'chroma-js';
8
9
  import * as echarts from 'echarts';
9
- import * as i1$2 from '@c8y/client';
10
+ import * as i1 from '@ngx-translate/core';
11
+ import * as i1$3 from '@c8y/client';
10
12
  import { AlarmStatus, aggregationType, MeasurementService } from '@c8y/client';
11
13
  import * as i6 from '@angular/router';
12
14
  import * as i4 from '@c8y/ngx-components/alarms';
13
15
  import { AlarmsModule, AlarmSeverityToIconPipe, AlarmSeverityToLabelPipe } from '@c8y/ngx-components/alarms';
14
16
  import { INTERVALS } from '@c8y/ngx-components/interval-picker';
15
- import * as i5 from '@ngx-translate/core';
16
- import * as i1$1 from '@angular/common';
17
+ import * as i1$2 from '@angular/common';
17
18
  import { NgIf, NgForOf, CommonModule as CommonModule$1 } from '@angular/common';
18
19
  import * as i2$1 from 'ngx-echarts';
19
20
  import { NgxEchartsModule, NGX_ECHARTS_CONFIG } from 'ngx-echarts';
20
21
  import { TooltipModule } from 'ngx-bootstrap/tooltip';
21
- import * as i1$3 from 'ngx-bootstrap/popover';
22
+ import * as i1$4 from 'ngx-bootstrap/popover';
22
23
  import { PopoverModule } from 'ngx-bootstrap/popover';
23
24
  import { DomSanitizer } from '@angular/platform-browser';
24
25
  import * as i2 from 'ngx-bootstrap/dropdown';
@@ -102,17 +103,18 @@ const ICONS_MAP = {
102
103
  };
103
104
 
104
105
  class YAxisService {
105
- constructor(appStateService) {
106
- this.appStateService = appStateService;
106
+ constructor(translate) {
107
+ this.translate = translate;
107
108
  this.Y_AXIS_OFFSET = 48;
108
109
  this.Y_AXIS_OFFSET_WITH_DP_VALUES = 22;
109
110
  this.Y_AXIS_OFFSET_WITHOUT_DP_VALUES = 15;
110
- this.intlNumberFormatCompliantLocale = this.appStateService.state.lang.replace('_', '-');
111
111
  }
112
112
  getYAxis(datapointsWithValues, YAxisOptions) {
113
113
  const YAxisPlacement = this.getYAxisPlacement(datapointsWithValues, YAxisOptions);
114
114
  const hasValues = dp => dp.values && Object.keys(dp.values).length > 0;
115
115
  const anyDpHasValues = datapointsWithValues.some(dp => hasValues(dp));
116
+ const selectedLanguage = this.translate.currentLang;
117
+ const locale = selectedLanguage?.replace('_', '-');
116
118
  if (YAxisOptions.forceMergeDatapoints) {
117
119
  return {
118
120
  name: Array.from(datapointsWithValues)
@@ -134,7 +136,12 @@ class YAxisService {
134
136
  axisLabel: {
135
137
  fontSize: 10,
136
138
  show: true,
137
- color: YAxisOptions.axisColor
139
+ color: YAxisOptions.axisColor,
140
+ formatter: (val) => new Intl.NumberFormat(locale, {
141
+ notation: 'compact',
142
+ compactDisplay: 'short',
143
+ maximumFractionDigits: 3
144
+ }).format(val)
138
145
  },
139
146
  axisLine: {
140
147
  show: true,
@@ -214,9 +221,14 @@ class YAxisService {
214
221
  if (this.isDefined(dp.min) && this.isDefined(dp.max)) {
215
222
  const range = dp.max - dp.min;
216
223
  const decimalPlaces = this.getDecimalPlaces(range);
217
- return val.toFixed(decimalPlaces);
224
+ return new Intl.NumberFormat(locale, {
225
+ notation: 'compact',
226
+ compactDisplay: 'short',
227
+ minimumFractionDigits: decimalPlaces,
228
+ maximumFractionDigits: decimalPlaces
229
+ }).format(val);
218
230
  }
219
- return new Intl.NumberFormat(this.intlNumberFormatCompliantLocale, {
231
+ return new Intl.NumberFormat(locale, {
220
232
  notation: 'compact',
221
233
  compactDisplay: 'short',
222
234
  maximumFractionDigits: 3
@@ -335,12 +347,12 @@ class YAxisService {
335
347
  }
336
348
  return !dps.some((dp2, index2) => this.isMatchingDp(dp, dp2) && index2 < index);
337
349
  }
338
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: YAxisService, deps: [{ token: i1.AppStateService }], target: i0.ɵɵFactoryTarget.Injectable }); }
350
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: YAxisService, deps: [{ token: i1.TranslateService }], target: i0.ɵɵFactoryTarget.Injectable }); }
339
351
  static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: YAxisService }); }
340
352
  }
341
353
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: YAxisService, decorators: [{
342
354
  type: Injectable
343
- }], ctorParameters: () => [{ type: i1.AppStateService }] });
355
+ }], ctorParameters: () => [{ type: i1.TranslateService }] });
344
356
 
345
357
  function assertUnreachable(_) {
346
358
  throw new Error('Case unreachable.');
@@ -367,6 +379,10 @@ class ChartTypesService {
367
379
  return this.stepBeforeSeriesOption(color, isMinMaxChart, renderType);
368
380
  case 'step-after':
369
381
  return this.stepAfterSeriesOption(color, isMinMaxChart, renderType);
382
+ case 'dashed':
383
+ return this.dashedLineSeriesOption(color, isMinMaxChart, renderType);
384
+ case 'dotted':
385
+ return this.dottedLineSeriesOption(color, isMinMaxChart, renderType);
370
386
  case 'line':
371
387
  case undefined:
372
388
  return this.lineSeriesOption(color, isMinMaxChart, renderType);
@@ -438,6 +454,20 @@ class ChartTypesService {
438
454
  const baseOption = this.lineSeriesOption(color, isMinMaxChart, renderType);
439
455
  return { ...baseOption, step: 'end' };
440
456
  }
457
+ dashedLineSeriesOption(color, isMinMaxChart, renderType) {
458
+ const baseOption = this.lineSeriesOption(color, isMinMaxChart, renderType);
459
+ return {
460
+ ...baseOption,
461
+ lineStyle: { ...baseOption.lineStyle, type: 'dashed' }
462
+ };
463
+ }
464
+ dottedLineSeriesOption(color, isMinMaxChart, renderType) {
465
+ const baseOption = this.lineSeriesOption(color, isMinMaxChart, renderType);
466
+ return {
467
+ ...baseOption,
468
+ lineStyle: { ...baseOption.lineStyle, type: 'dotted' }
469
+ };
470
+ }
441
471
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: ChartTypesService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
442
472
  static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: ChartTypesService }); }
443
473
  }
@@ -445,8 +475,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImpo
445
475
  type: Injectable
446
476
  }] });
447
477
 
448
- /* eslint-disable @typescript-eslint/no-explicit-any */
449
478
  const INDEX_HTML = '/index.html';
479
+ const DEFAULT_DATAPOINT_COLOR = '#1776bf';
450
480
  class EchartsOptionsService {
451
481
  constructor(datePipe, yAxisService, chartTypesService, severityIconPipe, severityLabelPipe, translate, router) {
452
482
  this.datePipe = datePipe;
@@ -467,7 +497,11 @@ class EchartsOptionsService {
467
497
  numberOfDecimalPlaces: CHART_DISPLAY_OPTION_DEFAULTS.numberOfDecimalPlaces,
468
498
  ...displayOptions
469
499
  };
470
- const yAxis = this.yAxisService.getYAxis(datapointsWithValues, {
500
+ // Expand datapoints so each enabled render type config becomes its own virtual dp,
501
+ // but only when aggregation is active — without aggregation the API returns raw
502
+ // min/max data and we must render a single series in the datapoint's primary color.
503
+ const expandedDatapoints = this.expandDatapointsForRenderTypeConfigs(datapointsWithValues, displayOptions.aggregationActive);
504
+ const yAxis = this.yAxisService.getYAxis(expandedDatapoints, {
471
505
  showSplitLines: showSplitLines.YAxis,
472
506
  mergeMatchingDatapoints: displayOptions.mergeMatchingDatapoints,
473
507
  forceMergeDatapoints: displayOptions.forceMergeDatapoints,
@@ -515,7 +549,7 @@ class EchartsOptionsService {
515
549
  {
516
550
  type: 'inside',
517
551
  // TODO: use 'none' only when this bug is fixed https://github.com/apache/echarts/issues/17858
518
- filterMode: datapointsWithValues.some(dp => dp.lineType === 'bars') ? 'filter' : 'none',
552
+ filterMode: expandedDatapoints.some(dp => dp.lineType === 'bars') ? 'filter' : 'none',
519
553
  zoomOnMouseWheel: true,
520
554
  startValue: selectedTimeRange
521
555
  ? new Date(selectedTimeRange.dateFrom).valueOf()
@@ -580,6 +614,7 @@ class EchartsOptionsService {
580
614
  },
581
615
  backgroundColor: 'var(--c8y-root-component-background-default)',
582
616
  borderColor: 'var(--c8y-root-component-border-color)',
617
+ padding: 0,
583
618
  formatter: this.getTooltipFormatter(displayOptions),
584
619
  appendToBody: true,
585
620
  position: this.tooltipPosition(),
@@ -606,7 +641,7 @@ class EchartsOptionsService {
606
641
  lineStyle: {
607
642
  color: displayOptions.axisColor
608
643
  },
609
- onZeroAxisIndex: datapointsWithValues.findIndex(dp => dp.lineType === 'bars') // align X axis to 0 of Y axis of datapoint with lineType 'bars'
644
+ onZeroAxisIndex: expandedDatapoints.findIndex(dp => dp.lineType === 'bars') // align X axis to 0 of Y axis of datapoint with lineType 'bars'
610
645
  },
611
646
  axisLabel: {
612
647
  hideOverlap: true,
@@ -651,7 +686,7 @@ class EchartsOptionsService {
651
686
  yAxis,
652
687
  series: [
653
688
  this.getAggregatedSeries(aggregatedDatapoint || null, displayOptions),
654
- ...this.getChartSeries(datapointsWithValues, events, alarms, displayOptions)
689
+ ...this.getChartSeries(expandedDatapoints, events, alarms, displayOptions)
655
690
  ]
656
691
  };
657
692
  }
@@ -680,7 +715,9 @@ class EchartsOptionsService {
680
715
  12;
681
716
  break;
682
717
  default:
683
- intervalInMs = INTERVALS.find(i => i.id === interval).timespanInMs;
718
+ intervalInMs = INTERVALS.find(
719
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
720
+ i => i.id === interval).timespanInMs;
684
721
  break;
685
722
  }
686
723
  return intervalInMs;
@@ -690,7 +727,7 @@ class EchartsOptionsService {
690
727
  return { type: 'line', data: [], id: 'aggregated-empty' };
691
728
  }
692
729
  const series = [];
693
- const renderType = aggregatedDatapoint?.renderType || 'min';
730
+ const renderType = aggregatedDatapoint?.renderType || 'avg';
694
731
  const idx = 0;
695
732
  if (renderType === 'area') {
696
733
  series.push(this.getSingleSeries(aggregatedDatapoint, 'min', idx, true, displayOptions, this.AGGREGATED_SERIES_TYPE));
@@ -838,6 +875,7 @@ class EchartsOptionsService {
838
875
  const XAxisValue = tooltipParams[0].data[0];
839
876
  const YAxisReadings = [];
840
877
  const allSeries = this.echartsInstance?.getOption()['series'];
878
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
841
879
  const legendSelected = this.echartsInstance?.getOption()['legend']?.[0]?.selected;
842
880
  // filter out alarm, event, and hidden series
843
881
  const allDataPointSeries = allSeries.filter(series => series['typeOfSeries'] !== 'alarm' &&
@@ -1059,8 +1097,8 @@ class EchartsOptionsService {
1059
1097
  YAxisReadings.push(this.buildCompactTooltipRow(color, label, value));
1060
1098
  }
1061
1099
  else {
1062
- YAxisReadings.push(`<div class="d-flex a-i-center p-b-8 text-default"><span class='dlt-c8y-icon-circle m-r-4' style='color: ${color};'></span>` + // color circle
1063
- `<strong class="text-truncate">${label}</strong></div>` + // name
1100
+ YAxisReadings.push(`<div class="d-flex a-i-center p-8 p-b-0 text-default"><span class='dlt-c8y-icon-circle m-r-4' style='color: ${color};'></span>` + // color circle
1101
+ `<span class="text-truncate text-medium">${label}</span></div>` + // name
1064
1102
  `${value}` // single value or min-max range
1065
1103
  );
1066
1104
  }
@@ -1089,7 +1127,7 @@ class EchartsOptionsService {
1089
1127
  if (displayOptions.showCompactTooltip) {
1090
1128
  return `${minStr} — ${maxStr}${unitStr}`;
1091
1129
  }
1092
- return (`<div class="d-flex a-i-center separator-top p-t-8 p-b-8 text-default"><label class="text-12 m-r-8 m-b-0">${echarts.format.encodeHTML(this.datePipe.transform(minValue[0]))}</label>` +
1130
+ return (`<div class="d-flex a-i-center separator-bottom p-8 p-b-0 text-default"><label class="text-12 m-r-8 m-b-0">${echarts.format.encodeHTML(this.datePipe.transform(minValue[0]))}</label>` +
1093
1131
  `<span class="m-l-auto text-12">${minStr} — ${maxStr}${unitStr}</span>` +
1094
1132
  `</div>`);
1095
1133
  }
@@ -1111,7 +1149,7 @@ class EchartsOptionsService {
1111
1149
  if (isCompact) {
1112
1150
  return `${valStr}${unitStr}`;
1113
1151
  }
1114
- return (`<div class="d-flex a-i-center p-t-8 p-b-8 separator-top text-default">` +
1152
+ return (`<div class="d-flex a-i-center separator-bottom p-8 p-b-0 text-default">` +
1115
1153
  `<label class="m-b-0 m-r-8 text-12">${echarts.format.encodeHTML(this.datePipe.transform(seriesValue[0]))}</label>` +
1116
1154
  `<span class="m-l-auto text-12">${valStr}${unitStr}</span>` +
1117
1155
  `</div>`);
@@ -1122,7 +1160,7 @@ class EchartsOptionsService {
1122
1160
  * @returns The processed value.
1123
1161
  */
1124
1162
  processEvent(event) {
1125
- let value = `<ul class="list-unstyled small separator-top text-default">`;
1163
+ let value = `<ul class="list-unstyled small separator-top text-default p-l-8 p-r-8">`;
1126
1164
  value += `<li class="p-t-4 p-b-4 d-flex separator-bottom text-truncate"><label class="text-label-small m-b-0 m-r-8">${echarts.format.encodeHTML(this.translate.instant(gettext('Event type')))}</label><span class="small m-l-auto"><code title="${echarts.format.encodeHTML(event.type)}">${echarts.format.encodeHTML(event.type)}</code></span></li>`;
1127
1165
  value += `<li class="p-t-4 p-b-4 d-flex separator-bottom text-truncate"><label class="text-label-small m-b-0 m-r-8">${echarts.format.encodeHTML(this.translate.instant(gettext('Event text')))}</label><span class="small m-l-auto" style="display: -webkit-box; -webkit-box-orient: vertical; overflow: hidden; text-overflow: ellipsis; word-break: break-word; white-space: normal; max-width: 200px; -webkit-line-clamp: 5;" title="${echarts.format.encodeHTML(event.text)}">${echarts.format.encodeHTML(event.text)}</span></li>`;
1128
1166
  value += `<li class="p-t-4 p-b-4 d-flex separator-bottom text-truncate"><label class="text-label-small m-b-0 m-r-8">${echarts.format.encodeHTML(this.translate.instant(gettext('Event time')))}</label><span class="small m-l-auto">${echarts.format.encodeHTML(this.datePipe.transform(event.time))}<span></li>`;
@@ -1135,7 +1173,7 @@ class EchartsOptionsService {
1135
1173
  * @returns The processed value.
1136
1174
  */
1137
1175
  async processAlarm(alarm) {
1138
- let value = `<ul class="list-unstyled small separator-top text-default m-0">`;
1176
+ let value = `<ul class="list-unstyled small separator-top text-default m-0 p-l-8 p-r-8">`;
1139
1177
  value += `<li class="p-t-4 p-b-4 d-flex a-i-center separator-bottom text-truncate"><label class="text-label-small m-b-0 m-r-8">${echarts.format.encodeHTML(this.translate.instant(gettext('Alarm severity')))}</label>`;
1140
1178
  value += `<span class="small d-inline-flex a-i-center gap-4 m-l-auto"><i class="stroked-icon icon-14 status dlt-c8y-icon-${echarts.format.encodeHTML(this.severityIconPipe.transform(alarm.severity))} ${alarm.severity.toLowerCase()}" > </i> ${this.severityLabelPipe.transform(alarm.severity)} </span></li>`;
1141
1179
  value += `<li class="p-t-4 p-b-4 d-flex separator-bottom text-truncate"><label class="text-label-small m-b-0 m-r-8">${echarts.format.encodeHTML(this.translate.instant(gettext('Alarm type')))}</label><span class="small m-l-auto"><code title="${echarts.format.encodeHTML(alarm.type)}">${echarts.format.encodeHTML(alarm.type)}</code></span></li>`;
@@ -1162,17 +1200,53 @@ class EchartsOptionsService {
1162
1200
  const series = [];
1163
1201
  let eventSeries = [];
1164
1202
  let alarmSeries = [];
1203
+ // When expandDatapointsForRenderTypeConfigs produces multiple virtual DPs from
1204
+ // the same source (e.g. separate min + max), they each get their own Y-axis entry
1205
+ // but the duplicate is hidden by YAxisService. To keep all series on the *same*
1206
+ // visible axis, map each expanded DP to the first expanded index that shares its
1207
+ // source identity and range.
1208
+ const sourceYAxisMap = new Map();
1209
+ const resolveYAxisIdx = (dp, idx) => {
1210
+ if (displayOptions.forceMergeDatapoints)
1211
+ return idx;
1212
+ const key = `${dp.__target?.id}|${dp.fragment}|${dp.series}|${dp.min ?? ''}|${dp.max ?? ''}`;
1213
+ if (!sourceYAxisMap.has(key)) {
1214
+ sourceYAxisMap.set(key, idx);
1215
+ }
1216
+ return sourceYAxisMap.get(key);
1217
+ };
1165
1218
  datapointsWithValues.forEach((dp, idx) => {
1166
- const renderType = dp.renderType || 'min';
1167
- if (renderType === 'area') {
1168
- series.push(this.getSingleSeries(dp, 'min', idx, true, displayOptions));
1169
- series.push(this.getSingleSeries(dp, 'max', idx, true, displayOptions));
1219
+ const yIdx = resolveYAxisIdx(dp, idx);
1220
+ const renderType = dp.renderType || 'avg';
1221
+ const renderTypes = !dp.renderTypeConfigs && dp.renderTypes?.length > 1 && displayOptions.aggregationActive
1222
+ ? dp.renderTypes
1223
+ : null;
1224
+ if (renderTypes) {
1225
+ // Legacy multi-render-type (no renderTypeConfigs): create one series per type
1226
+ for (const rt of renderTypes) {
1227
+ if (rt === 'area') {
1228
+ series.push(this.getSingleSeries(dp, 'min', yIdx, true, displayOptions));
1229
+ series.push(this.getSingleSeries(dp, 'max', yIdx, true, displayOptions));
1230
+ }
1231
+ else {
1232
+ series.push(this.getSingleSeries(dp, rt, yIdx, false, displayOptions, '', rt));
1233
+ }
1234
+ }
1235
+ }
1236
+ else if (renderType === 'area') {
1237
+ if (dp.renderAsBand) {
1238
+ series.push(...this.getBandSeries(dp, yIdx, displayOptions));
1239
+ }
1240
+ else {
1241
+ series.push(this.getSingleSeries(dp, 'min', yIdx, true, displayOptions));
1242
+ series.push(this.getSingleSeries(dp, 'max', yIdx, true, displayOptions));
1243
+ }
1170
1244
  }
1171
1245
  else {
1172
- series.push(this.getSingleSeries(dp, renderType, idx, false, displayOptions));
1246
+ series.push(this.getSingleSeries(dp, renderType, yIdx, false, displayOptions));
1173
1247
  }
1174
- const newEventSeries = this.getAlarmOrEventSeries(dp, renderType, false, events, 'event', displayOptions, null, idx);
1175
- const newAlarmSeries = this.getAlarmOrEventSeries(dp, renderType, false, alarms, 'alarm', displayOptions, null, idx);
1248
+ const newEventSeries = this.getAlarmOrEventSeries(dp, renderType, false, events, 'event', displayOptions, null, yIdx);
1249
+ const newAlarmSeries = this.getAlarmOrEventSeries(dp, renderType, false, alarms, 'alarm', displayOptions, null, yIdx);
1176
1250
  eventSeries = [...eventSeries, ...newEventSeries];
1177
1251
  alarmSeries = [...alarmSeries, ...newAlarmSeries];
1178
1252
  });
@@ -1222,6 +1296,7 @@ class EchartsOptionsService {
1222
1296
  return items.reduce((grouped, item) => {
1223
1297
  (grouped[item[typeField]] = grouped[item[typeField]] || []).push(item);
1224
1298
  return grouped;
1299
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1225
1300
  }, {});
1226
1301
  }
1227
1302
  calculateGridLeft(leftAxisCount, forceMergeDatapoints) {
@@ -1242,6 +1317,65 @@ class EchartsOptionsService {
1242
1317
  }
1243
1318
  return (rightAxisCount - 1) * this.yAxisService.Y_AXIS_OFFSET + RIGHT_AXIS_BASE_SPACE;
1244
1319
  }
1320
+ /**
1321
+ * Expands datapoints so that each enabled render type config becomes its own
1322
+ * virtual DpWithValues entry with overridden lineType, color, min, max, yAxisType.
1323
+ * Datapoints without renderTypeConfigs are passed through unchanged.
1324
+ * When `aggregationActive` is false the expansion is skipped so non-aggregated
1325
+ * raw data renders as a single series using the primary renderType and datapoint color.
1326
+ */
1327
+ expandDatapointsForRenderTypeConfigs(dps, aggregationActive = false) {
1328
+ const result = [];
1329
+ for (const dp of dps) {
1330
+ const configs = dp.renderTypeConfigs;
1331
+ if (!configs || !aggregationActive) {
1332
+ result.push(dp);
1333
+ continue;
1334
+ }
1335
+ const enabledKeys = Object.keys(configs).filter(k => configs[k]?.enabled);
1336
+ if (enabledKeys.length === 0) {
1337
+ result.push(dp);
1338
+ continue;
1339
+ }
1340
+ // When renderAsBand is true, replace separate min+max with a single 'area' render type
1341
+ const hasMinAndMax = enabledKeys.includes('min') && enabledKeys.includes('max');
1342
+ const renderAsBand = dp.renderAsBand && hasMinAndMax;
1343
+ for (const rt of enabledKeys) {
1344
+ if (renderAsBand && (rt === 'min' || rt === 'max')) {
1345
+ continue; // skip individual min/max; we'll add area below
1346
+ }
1347
+ const cfg = configs[rt];
1348
+ const expanded = {
1349
+ ...dp,
1350
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1351
+ renderType: rt,
1352
+ renderTypes: null,
1353
+ lineType: cfg.lineType || dp.lineType,
1354
+ color: cfg.color || this.getRenderTypeColorFromBase(dp.color, rt) || dp.color,
1355
+ min: cfg.rangeMin ?? dp.min,
1356
+ max: cfg.rangeMax ?? dp.max,
1357
+ yAxisType: cfg.yAxisType || dp.yAxisType
1358
+ };
1359
+ result.push(expanded);
1360
+ }
1361
+ if (renderAsBand) {
1362
+ // Add a single 'area' entry using min config's properties as fallback
1363
+ const minCfg = configs['min'];
1364
+ result.push({
1365
+ ...dp,
1366
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1367
+ renderType: 'area',
1368
+ renderTypes: null,
1369
+ lineType: minCfg?.lineType || dp.lineType,
1370
+ color: minCfg?.color || dp.color,
1371
+ min: minCfg?.rangeMin ?? dp.min,
1372
+ max: minCfg?.rangeMax ?? dp.max,
1373
+ yAxisType: minCfg?.yAxisType || dp.yAxisType
1374
+ });
1375
+ }
1376
+ }
1377
+ return result;
1378
+ }
1245
1379
  /**
1246
1380
  * This method interpolates between two data points. The goal is to place the markPoint on the chart in the right place.
1247
1381
  * @param dpValuesArray array of data points
@@ -1491,9 +1625,12 @@ class EchartsOptionsService {
1491
1625
  }
1492
1626
  }, []);
1493
1627
  }
1494
- getSingleSeries(dp, renderType, idx, isMinMaxChart = false, displayOptions, seriesType = '') {
1628
+ getSingleSeries(dp, renderType, idx, isMinMaxChart = false, displayOptions, seriesType = '', multiRenderTypeLabel) {
1495
1629
  const datapointId = dp.__target?.id + dp.fragment + dp.series;
1496
- const isLine = dp.lineType === 'line' || dp.lineType === 'linePoints';
1630
+ const isLine = dp.lineType === 'line' ||
1631
+ dp.lineType === 'linePoints' ||
1632
+ dp.lineType === 'dashed' ||
1633
+ dp.lineType === 'dotted';
1497
1634
  const isAggr = seriesType === this.AGGREGATED_SERIES_TYPE;
1498
1635
  const data = isAggr && Object.keys(dp.values).length <= 1
1499
1636
  ? []
@@ -1501,21 +1638,133 @@ class EchartsOptionsService {
1501
1638
  dateString,
1502
1639
  values[0][renderType] ?? values[0]['min']
1503
1640
  ]);
1641
+ const effectiveLabel = multiRenderTypeLabel ||
1642
+ (dp.renderTypeConfigs && displayOptions.aggregationActive ? renderType : undefined);
1643
+ const renderTypeSuffix = effectiveLabel ? `~${effectiveLabel}` : '';
1644
+ const colorOverride = multiRenderTypeLabel && dp.renderTypeColors?.[multiRenderTypeLabel]
1645
+ ? dp.renderTypeColors[multiRenderTypeLabel]
1646
+ : multiRenderTypeLabel && dp.color
1647
+ ? this.getRenderTypeColorFromBase(dp.color, multiRenderTypeLabel)
1648
+ : undefined;
1649
+ const dpForSeries = colorOverride ? { ...dp, color: colorOverride } : dp;
1650
+ const displayType = isMinMaxChart && renderType === 'min' ? 'min — max' : effectiveLabel;
1504
1651
  return {
1505
1652
  datapointId,
1506
1653
  datapointUnit: renderType !== 'count' ? dp.unit : '',
1507
1654
  // 'id' property is needed as 'seriesId' in tooltip formatter
1508
1655
  id: isMinMaxChart
1509
1656
  ? `${datapointId}/${renderType}${seriesType}`
1510
- : `${datapointId}${seriesType}`,
1511
- name: `${dp.label} (${dp.__target?.['name']})`,
1512
- datapointLabel: dp.label || '',
1657
+ : `${datapointId}${seriesType}${renderTypeSuffix}`,
1658
+ name: displayType
1659
+ ? `[${displayType}] ${dp.label} (${dp.__target?.['name']})`
1660
+ : `${dp.label} (${dp.__target?.['name']})`,
1661
+ datapointLabel: displayType ? `[${displayType}] ${dp.label || ''}` : dp.label || '',
1513
1662
  data,
1514
1663
  ...(displayOptions.forceMergeDatapoints ? {} : { yAxisIndex: idx }),
1515
- ...this.chartTypesService.getSeriesOptions(dp, isMinMaxChart, renderType),
1664
+ ...this.chartTypesService.getSeriesOptions(dpForSeries, isMinMaxChart, renderType),
1516
1665
  ...(isLine && { smooth: displayOptions.smoothLines ?? false, triggerLineEvent: true })
1517
1666
  };
1518
1667
  }
1668
+ /**
1669
+ * Creates a band (shaded area) between min and max values using the
1670
+ * stack + stackStrategy:'positive' technique.
1671
+ *
1672
+ * Three series are produced:
1673
+ * 1. max – visible line, stacked
1674
+ * 2. diff (min − max, negative) – invisible line stacked on max with
1675
+ * stackStrategy:'positive' so the area fills downward to min
1676
+ * 3. min – visible line, NOT stacked
1677
+ */
1678
+ getBandSeries(dp, idx, displayOptions) {
1679
+ const datapointId = dp.__target?.id + dp.fragment + dp.series;
1680
+ const configs = dp.renderTypeConfigs;
1681
+ const minColor = configs?.['min']?.color || dp.color || DEFAULT_DATAPOINT_COLOR;
1682
+ const maxColor = configs?.['max']?.color || dp.color || DEFAULT_DATAPOINT_COLOR;
1683
+ const bandColor = dp.bandColor || dp.color || DEFAULT_DATAPOINT_COLOR;
1684
+ const stackId = `band-${datapointId}`;
1685
+ const maxData = Object.entries(dp.values).map(([dateString, values]) => [
1686
+ dateString,
1687
+ values[0]['max'] ?? null
1688
+ ]);
1689
+ const minData = Object.entries(dp.values).map(([dateString, values]) => [
1690
+ dateString,
1691
+ values[0]['min'] ?? null
1692
+ ]);
1693
+ const yAxisIndex = displayOptions.forceMergeDatapoints ? {} : { yAxisIndex: idx };
1694
+ // ECharts cumulative line-stack technique for confidence bands.
1695
+ // diff = minVal − maxVal is always negative (min < max).
1696
+ // Stacked cumulative: maxVal + (minVal − maxVal) = minVal.
1697
+ // areaStyle on the diff series fills from minVal (current cumulative) back to
1698
+ // maxVal (previous cumulative) — the correct band — for both positive and negative ranges.
1699
+ // A separate invisible bandMaxSeries is used so getSeriesOptions never touches the stack.
1700
+ const diffData = Object.entries(dp.values).map(([dateString, values]) => {
1701
+ const minVal = values[0]['min'];
1702
+ const maxVal = values[0]['max'];
1703
+ if (minVal == null || maxVal == null)
1704
+ return [dateString, null];
1705
+ return [dateString, minVal - maxVal];
1706
+ });
1707
+ // Invisible stacked upper boundary at maxVal.
1708
+ const bandMaxSeries = {
1709
+ datapointId,
1710
+ datapointUnit: '',
1711
+ id: `${datapointId}/band-max`,
1712
+ name: `[band-max] ${dp.label} (${dp.__target?.['name']})`,
1713
+ datapointLabel: '',
1714
+ typeOfSeries: 'fake',
1715
+ type: 'line',
1716
+ stack: stackId,
1717
+ symbol: 'none',
1718
+ lineStyle: { opacity: 0 },
1719
+ tooltip: { show: false },
1720
+ data: maxData,
1721
+ ...yAxisIndex
1722
+ };
1723
+ // Invisible stacked diff series — cumulative lands at minVal, fills the band.
1724
+ // stackStrategy:'all' is required: the default 'samesign' would refuse to stack
1725
+ // a negative diff on a positive sum (or vice versa), leaving it at its raw value.
1726
+ const bandMinSeries = {
1727
+ datapointId,
1728
+ datapointUnit: dp.unit || '',
1729
+ id: `${datapointId}/band-area`,
1730
+ name: `[band] ${dp.label} (${dp.__target?.['name']})`,
1731
+ datapointLabel: `[band] ${dp.label || ''}`,
1732
+ typeOfSeries: 'fake',
1733
+ type: 'line',
1734
+ stack: stackId,
1735
+ stackStrategy: 'all',
1736
+ symbol: 'none',
1737
+ lineStyle: { opacity: 0 },
1738
+ areaStyle: { opacity: 0.2, color: bandColor },
1739
+ tooltip: { show: false },
1740
+ data: diffData,
1741
+ ...yAxisIndex
1742
+ };
1743
+ // Visible max line — not stacked, drawn at actual maxVal.
1744
+ const maxSeries = {
1745
+ datapointId,
1746
+ datapointUnit: dp.unit || '',
1747
+ id: `${datapointId}/max`,
1748
+ name: `[max] ${dp.label} (${dp.__target?.['name']})`,
1749
+ datapointLabel: `[max] ${dp.label || ''}`,
1750
+ data: maxData,
1751
+ ...this.chartTypesService.getSeriesOptions({ ...dp, color: maxColor }, true, 'max'),
1752
+ ...yAxisIndex
1753
+ };
1754
+ // Visible min line — not stacked, drawn at actual minVal. Tooltip shows combined [min — max].
1755
+ const minSeries = {
1756
+ datapointId,
1757
+ datapointUnit: dp.unit || '',
1758
+ id: `${datapointId}/min`,
1759
+ name: `[min — max] ${dp.label} (${dp.__target?.['name']})`,
1760
+ datapointLabel: `[min — max] ${dp.label || ''}`,
1761
+ tooltipColor: bandColor,
1762
+ data: minData,
1763
+ ...this.chartTypesService.getSeriesOptions({ ...dp, color: minColor }, true, 'min'),
1764
+ ...yAxisIndex
1765
+ };
1766
+ return [bandMaxSeries, bandMinSeries, maxSeries, minSeries];
1767
+ }
1519
1768
  /**
1520
1769
  * This method creates a general tooltip formatter for the chart.
1521
1770
  * @returns TooltipComponentFormatterCallback<TooltipComponentFormatterCallbackParams>
@@ -1529,11 +1778,36 @@ class EchartsOptionsService {
1529
1778
  const XAxisValue = data[0];
1530
1779
  const YAxisReadings = [];
1531
1780
  const allSeries = this.echartsInstance?.getOption()['series'];
1781
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1532
1782
  const legendSelected = this.echartsInstance?.getOption()['legend']?.[0]?.selected;
1533
1783
  const allDataPointSeries = allSeries.filter(series => series['typeOfSeries'] !== 'alarm' &&
1534
1784
  series['typeOfSeries'] !== 'event' &&
1535
1785
  series['typeOfSeries'] !== 'fake' &&
1536
1786
  legendSelected?.[series['name']] !== false);
1787
+ // Pre-compute which per-function series groups have identical values at this x-coordinate.
1788
+ // Per-function series are identified by having '~' in their id (e.g. datapointId~avg).
1789
+ const perFnGroupSeries = new Map();
1790
+ for (const s of allDataPointSeries) {
1791
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1792
+ const sId = s['id'];
1793
+ if (!sId?.includes('~'))
1794
+ continue;
1795
+ const sDpId = sId.split('~')[0];
1796
+ if (!perFnGroupSeries.has(sDpId))
1797
+ perFnGroupSeries.set(sDpId, []);
1798
+ perFnGroupSeries.get(sDpId).push(s);
1799
+ }
1800
+ const duplicateDpIds = new Set();
1801
+ perFnGroupSeries.forEach((group, sDpId) => {
1802
+ if (group.length < 2)
1803
+ return;
1804
+ const vals = group.map(s => this.findValueForExactOrEarlierTimestamp(s['data'], XAxisValue)?.[1] ??
1805
+ null);
1806
+ const first = vals[0];
1807
+ if (first !== null && vals.every(v => v === first)) {
1808
+ duplicateDpIds.add(sDpId);
1809
+ }
1810
+ });
1537
1811
  // Count only series that will actually produce a row at this timestamp
1538
1812
  const tooltipRowCount = allDataPointSeries.filter(series => {
1539
1813
  const id = series['id'];
@@ -1546,42 +1820,68 @@ class EchartsOptionsService {
1546
1820
  allDataPointSeries.forEach((series) => {
1547
1821
  const id = series['id'];
1548
1822
  const itemStyle = series['itemStyle'];
1549
- const color = echarts.format.encodeHTML(itemStyle?.color);
1823
+ const seriesColor = series['tooltipColor'] ?? itemStyle?.color;
1824
+ const color = echarts.format.encodeHTML(seriesColor);
1550
1825
  const label = echarts.format.encodeHTML(series['datapointLabel']);
1551
1826
  const unit = series['datapointUnit']
1552
1827
  ? ` ${echarts.format.encodeHTML(series['datapointUnit'])}`
1553
1828
  : '';
1829
+ const dpId = id?.includes('~') ? id.split('~')[0] : id?.split('/')[0];
1830
+ const isCollapsed = !!(dpId && displayOptions.collapsedDpIds?.has(dpId));
1831
+ const isDuplicate = !!(dpId && duplicateDpIds.has(dpId)) || isCollapsed;
1832
+ const isDimmed = this.highlightedSeriesName !== null && series['name'] !== this.highlightedSeriesName;
1554
1833
  if (id.endsWith('/min')) {
1555
1834
  const minValue = this.findValueForExactOrEarlierTimestamp(series['data'], XAxisValue);
1556
1835
  if (!minValue)
1557
1836
  return;
1558
1837
  const maxSeries = allDataPointSeries.find(s => s['id'] === id.replace('/min', '/max'));
1559
- if (!maxSeries)
1560
- return;
1561
- const maxValue = this.findValueForExactOrEarlierTimestamp(maxSeries['data'], XAxisValue);
1562
- if (maxValue === null)
1563
- return;
1564
- const minStr = echarts.format.encodeHTML(minValue[1].toFixed(displayOptions.numberOfDecimalPlaces) ?? '--');
1565
- const maxStr = echarts.format.encodeHTML(maxValue[1].toFixed(displayOptions.numberOfDecimalPlaces) ?? '--');
1566
- const valueStr = `${minStr} ${maxStr}${unit}`;
1838
+ const maxValue = maxSeries
1839
+ ? this.findValueForExactOrEarlierTimestamp(maxSeries['data'], XAxisValue)
1840
+ : null;
1841
+ let valueStr;
1842
+ if (maxSeries && maxValue !== null) {
1843
+ // Both min and max visible: show combined range.
1844
+ const minStr = echarts.format.encodeHTML(minValue[1].toFixed(displayOptions.numberOfDecimalPlaces ?? 2) ?? '--');
1845
+ const maxStr = echarts.format.encodeHTML(maxValue[1].toFixed(displayOptions.numberOfDecimalPlaces ?? 2) ?? '--');
1846
+ valueStr = `${minStr} — ${maxStr}`;
1847
+ }
1848
+ else {
1849
+ // Max is hidden: show min value only.
1850
+ const minStr = echarts.format.encodeHTML(minValue[1].toFixed(displayOptions.numberOfDecimalPlaces ?? 2) ?? '--');
1851
+ valueStr = minStr;
1852
+ }
1567
1853
  if (isCompact) {
1568
- YAxisReadings.push(this.buildCompactTooltipRow(color, label, valueStr));
1854
+ YAxisReadings.push(this.buildCompactTooltipRow(color, label, `${valueStr}${unit}`, isDimmed));
1569
1855
  }
1570
1856
  else {
1571
1857
  const date = echarts.format.encodeHTML(this.datePipe.transform(minValue[0]));
1572
- YAxisReadings.push(this.buildTooltipRow(color, label, date, valueStr));
1858
+ YAxisReadings.push(this.buildTooltipRow(color, label, date, valueStr, isDimmed, unit));
1573
1859
  }
1574
1860
  }
1575
1861
  else if (id.endsWith('/max')) {
1576
- // handled in '/min' case
1577
- return;
1862
+ const minSeries = allDataPointSeries.find(s => s['id'] === id.replace('/max', '/min'));
1863
+ if (minSeries) {
1864
+ // Min is visible and handles the tooltip for this pair: skip.
1865
+ return;
1866
+ }
1867
+ // Min is hidden: show max value only.
1868
+ const maxValue = this.findValueForExactOrEarlierTimestamp(series['data'], XAxisValue);
1869
+ if (!maxValue)
1870
+ return;
1871
+ const maxStr = echarts.format.encodeHTML(maxValue[1].toFixed(displayOptions.numberOfDecimalPlaces ?? 2) ?? '--');
1872
+ if (isCompact) {
1873
+ YAxisReadings.push(this.buildCompactTooltipRow(color, label, `${maxStr}${unit}`, isDimmed));
1874
+ }
1875
+ else {
1876
+ const date = echarts.format.encodeHTML(this.datePipe.transform(maxValue[0]));
1877
+ YAxisReadings.push(this.buildTooltipRow(color, label, date, maxStr, isDimmed, unit));
1878
+ }
1578
1879
  }
1579
1880
  else {
1580
- const isDimmed = this.highlightedSeriesName !== null && series['name'] !== this.highlightedSeriesName;
1581
1881
  const seriesValue = this.findValueForExactOrEarlierTimestamp(series['data'], XAxisValue);
1582
1882
  if (!seriesValue)
1583
1883
  return;
1584
- const valStr = echarts.format.encodeHTML(seriesValue[1]?.toFixed(displayOptions.numberOfDecimalPlaces) ?? '--');
1884
+ const valStr = echarts.format.encodeHTML(seriesValue[1]?.toFixed(displayOptions.numberOfDecimalPlaces ?? 2) ?? '--');
1585
1885
  const valueStr = `${valStr}${unit}`;
1586
1886
  if (isCompact) {
1587
1887
  YAxisReadings.push(this.buildCompactTooltipRow(color, label, valueStr, isDimmed));
@@ -1591,23 +1891,52 @@ class EchartsOptionsService {
1591
1891
  YAxisReadings.push(this.buildTooltipRow(color, label, date, valueStr, isDimmed));
1592
1892
  }
1593
1893
  }
1894
+ if (isDuplicate) {
1895
+ const group = perFnGroupSeries.get(dpId);
1896
+ // For per-fn groups: show warning after last series in group.
1897
+ // For collapsed single-series: always show warning (no group exists).
1898
+ const isLastInGroup = group ? group[group.length - 1]?.['id'] === id : true;
1899
+ if (isLastInGroup) {
1900
+ const dpLabel = group
1901
+ ? // eslint-disable-next-line @typescript-eslint/no-explicit-any
1902
+ group[0]['datapointLabel']
1903
+ : series['datapointLabel'];
1904
+ const baseName = dpLabel?.replace(/\s*\[.*?\]\s*$/g, '').trim() || dpLabel;
1905
+ const msg = this.translate.instant(gettext('The selected aggregation functions for {{dp}} return the same value because each interval contains a single measurement. Choose a coarser aggregation interval.'), { dp: baseName });
1906
+ YAxisReadings.push(`<div class="separator-bottom p-b-8 text-muted small bg-component text-12 text-break-word" style="white-space: wrap; margin-top:-2px">` +
1907
+ `<div class="icon-flex"><i class="dlt-c8y-icon-warning m-r-4" style="color: var(--c8y-palette-status-warning)"></i>` +
1908
+ `<span>${echarts.format.encodeHTML(msg)}</span>` +
1909
+ `</div></div>`);
1910
+ }
1911
+ }
1594
1912
  });
1595
1913
  const tooltipWidth = isCompact ? this.COMPACT_TOOLTIP_WIDTH : this.TOOLTIP_WIDTH;
1596
1914
  return `<div inner-e-cbg-component style="width: ${tooltipWidth}px">${YAxisReadings.join('')}</div>`;
1597
1915
  };
1598
1916
  }
1599
- buildTooltipRow(color, label, date, value, isDimmed) {
1917
+ buildTooltipRow(color, label, date, value, isDimmed, unit) {
1918
+ const header = `<div class="d-flex a-i-center p-8 p-b-0 text-default"><span class='dlt-c8y-icon-circle m-r-4' style='color: ${color}'></span>` +
1919
+ `<strong class="text-truncate">${label}</strong></div>`;
1920
+ if (unit !== undefined) {
1921
+ // min/max area row: unit sits after the text-truncate div, row uses p-8 p-b-0
1922
+ // The extra </div> preserves the output shape from the original code pattern.
1923
+ return (`<div style="opacity: ${isDimmed ? 0.4 : 1}">` +
1924
+ header +
1925
+ `<div class="d-flex a-i-center separator-bottom text-default p-8 p-b-0">` +
1926
+ `<label class="text-12 text-muted m-r-8 m-b-0">${date}</label>` +
1927
+ `<div class="m-l-auto text-12 text-truncate">${value}</div>${unit}` +
1928
+ `</div></div></div>`);
1929
+ }
1600
1930
  return (`<div style="opacity: ${isDimmed ? 0.4 : 1}">` +
1601
- `<div class="d-flex a-i-center p-b-8 text-default"><span class='dlt-c8y-icon-circle m-r-4' style='color: ${color}'></span>` +
1602
- `<strong class="text-truncate">${label}</strong></div>` +
1603
- `<div class="d-flex a-i-center separator-top text-default p-t-8 p-b-8">` +
1604
- `<label class="text-12 m-r-8 m-b-0">${date}</label>` +
1931
+ header +
1932
+ `<div class="d-flex a-i-center separator-bottom text-default p-b-8 p-l-24 p-r-8">` +
1933
+ `<label class="text-12 text-muted m-r-8 m-b-0">${date}</label>` +
1605
1934
  `<div class="m-l-auto text-12 text-truncate">${value}</div>` +
1606
1935
  `</div></div>`);
1607
1936
  }
1608
1937
  buildCompactTooltipRow(color, label, value, isDimmed) {
1609
1938
  return (`<div style="opacity: ${isDimmed ? 0.4 : 1}">` +
1610
- `<div class="d-flex a-i-center p-t-4 p-b-4 separator-bottom text-default">` +
1939
+ `<div class="d-flex a-i-center p-t-4 p-b-4 p-l-8 p-r-8 separator-bottom text-default">` +
1611
1940
  `<span class="dlt-c8y-icon-circle m-r-4 flex-no-shrink" style="color:${color}"></span>` +
1612
1941
  `<strong class="text-truncate m-r-4" style="max-width:120px">${label}</strong>` +
1613
1942
  `<span class="m-l-auto">${value}</span>` +
@@ -1626,12 +1955,53 @@ class EchartsOptionsService {
1626
1955
  return acc;
1627
1956
  }, null);
1628
1957
  }
1629
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: EchartsOptionsService, deps: [{ token: i1.DatePipe }, { token: YAxisService }, { token: ChartTypesService }, { token: i4.AlarmSeverityToIconPipe }, { token: i4.AlarmSeverityToLabelPipe }, { token: i5.TranslateService }, { token: i6.Router }], target: i0.ɵɵFactoryTarget.Injectable }); }
1958
+ /**
1959
+ * Generates a per-function color from the datapoint's base color using luminance-matched shades.
1960
+ * `avg` maps to the base color itself; `min`/`count` shift one/two steps darker;
1961
+ * `max`/`sum` shift one/two steps lighter.
1962
+ */
1963
+ getRenderTypeColorFromBase(baseColor, renderType) {
1964
+ const SHADE_OFFSETS = {
1965
+ count: -2,
1966
+ min: -1,
1967
+ avg: 0,
1968
+ max: 1,
1969
+ sum: 2
1970
+ };
1971
+ const offset = SHADE_OFFSETS[renderType];
1972
+ if (offset === undefined) {
1973
+ return undefined;
1974
+ }
1975
+ if (offset === 0) {
1976
+ return baseColor;
1977
+ }
1978
+ try {
1979
+ const referenceLuminances = [
1980
+ chroma('#134158').luminance(),
1981
+ chroma('#1C5569').luminance(),
1982
+ chroma('#058192').luminance(), // primary / avg
1983
+ chroma('#22A6AA').luminance(),
1984
+ chroma('#3CC1B7').luminance()
1985
+ ];
1986
+ const baseLuminance = chroma(baseColor).luminance();
1987
+ const targetIndex = 2 + offset;
1988
+ if (targetIndex < 0 || targetIndex >= referenceLuminances.length) {
1989
+ return baseColor;
1990
+ }
1991
+ const luminanceDelta = referenceLuminances[targetIndex] - referenceLuminances[2];
1992
+ const targetLuminance = Math.max(0, Math.min(1, baseLuminance + luminanceDelta));
1993
+ return chroma(baseColor).luminance(targetLuminance).hex();
1994
+ }
1995
+ catch {
1996
+ return baseColor;
1997
+ }
1998
+ }
1999
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: EchartsOptionsService, deps: [{ token: i1$1.DatePipe }, { token: YAxisService }, { token: ChartTypesService }, { token: i4.AlarmSeverityToIconPipe }, { token: i4.AlarmSeverityToLabelPipe }, { token: i1.TranslateService }, { token: i6.Router }], target: i0.ɵɵFactoryTarget.Injectable }); }
1630
2000
  static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: EchartsOptionsService }); }
1631
2001
  }
1632
2002
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: EchartsOptionsService, decorators: [{
1633
2003
  type: Injectable
1634
- }], ctorParameters: () => [{ type: i1.DatePipe }, { type: YAxisService }, { type: ChartTypesService }, { type: i4.AlarmSeverityToIconPipe }, { type: i4.AlarmSeverityToLabelPipe }, { type: i5.TranslateService }, { type: i6.Router }] });
2004
+ }], ctorParameters: () => [{ type: i1$1.DatePipe }, { type: YAxisService }, { type: ChartTypesService }, { type: i4.AlarmSeverityToIconPipe }, { type: i4.AlarmSeverityToLabelPipe }, { type: i1.TranslateService }, { type: i6.Router }] });
1635
2005
 
1636
2006
  class ChartRealtimeService {
1637
2007
  constructor(measurementRealtime, alarmRealtimeService, eventRealtimeService, echartsOptionsService) {
@@ -1757,9 +2127,23 @@ class ChartRealtimeService {
1757
2127
  .subscribe();
1758
2128
  }
1759
2129
  stopRealtime() {
1760
- this.realtimeSubscriptionMeasurements?.unsubscribe();
1761
- this.realtimeSubscriptionAlarmsEvents?.unsubscribe();
1762
- this.realtimeIntervalSubscription?.unsubscribe();
2130
+ // Tearing down a realtime subscription forwards to cometd's unsubscribe, which throws
2131
+ // 'Illegal state: disconnected' if the cometd client has already disconnected (e.g. when
2132
+ // switching config/history mode or saving the widget). Guard each teardown so a disconnected
2133
+ // channel cannot bubble up as an UnsubscriptionError.
2134
+ this.safeUnsubscribe(this.realtimeSubscriptionMeasurements);
2135
+ this.safeUnsubscribe(this.realtimeSubscriptionAlarmsEvents);
2136
+ this.safeUnsubscribe(this.realtimeIntervalSubscription);
2137
+ }
2138
+ safeUnsubscribe(subscription) {
2139
+ try {
2140
+ subscription?.unsubscribe();
2141
+ }
2142
+ catch (error) {
2143
+ // Expected when cometd has already disconnected; warn rather than error so this benign
2144
+ // race does not surface as a failure in error monitoring.
2145
+ console.warn('Failed to unsubscribe from realtime channel', error);
2146
+ }
1763
2147
  }
1764
2148
  /**
1765
2149
  * Updates the chart with the current alarms and events data. Only used when no active datapoints are selected.
@@ -1964,12 +2348,12 @@ class ChartRealtimeService {
1964
2348
  datapointBackInSyncCallback(datapoint);
1965
2349
  }
1966
2350
  }
1967
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: ChartRealtimeService, deps: [{ token: i1.MeasurementRealtimeService }, { token: i1.AlarmRealtimeService }, { token: i1.EventRealtimeService }, { token: EchartsOptionsService }], target: i0.ɵɵFactoryTarget.Injectable }); }
2351
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: ChartRealtimeService, deps: [{ token: i1$1.MeasurementRealtimeService }, { token: i1$1.AlarmRealtimeService }, { token: i1$1.EventRealtimeService }, { token: EchartsOptionsService }], target: i0.ɵɵFactoryTarget.Injectable }); }
1968
2352
  static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: ChartRealtimeService }); }
1969
2353
  }
1970
2354
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: ChartRealtimeService, decorators: [{
1971
2355
  type: Injectable
1972
- }], ctorParameters: () => [{ type: i1.MeasurementRealtimeService }, { type: i1.AlarmRealtimeService }, { type: i1.EventRealtimeService }, { type: EchartsOptionsService }] });
2356
+ }], ctorParameters: () => [{ type: i1$1.MeasurementRealtimeService }, { type: i1$1.AlarmRealtimeService }, { type: i1$1.EventRealtimeService }, { type: EchartsOptionsService }] });
1973
2357
 
1974
2358
  class ChartAlertsComponent {
1975
2359
  constructor() {
@@ -1982,7 +2366,7 @@ class ChartAlertsComponent {
1982
2366
  return text;
1983
2367
  }
1984
2368
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: ChartAlertsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
1985
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.17", type: ChartAlertsComponent, isStandalone: true, selector: "c8y-chart-alerts", inputs: { alerts: "alerts" }, viewQueries: [{ propertyName: "dismissPermanently", first: true, predicate: ["dismissPermanently"], descendants: true }], ngImport: i0, template: "@if (alerts?.anyAlertExists$ | async) {\n <div class=\"overlay-center-vertically d-col p-16 p-r-24 p-l-24\">\n @for (alertGroup of alerts?.alertGroups; track alertGroup) {\n @if (alertGroup.value.alerts.length) {\n <div\n class=\"alert\"\n role=\"alert\"\n [ngClass]=\"'alert-' + alertGroup.type\"\n >\n @if (alertGroup.value.alertDismissal.dismissStrategy !== 'none') {\n <button\n class=\"close\"\n type=\"button\"\n (click)=\"\n alerts.dismissAlertGroup(alertGroup.type, dismissPermanently.nativeElement.checked)\n \"\n >\n <span aria-hidden=\"true\">\u00D7</span>\n <span class=\"sr-only\">{{ 'Close' | translate }}</span>\n </button>\n }\n\n @for (alertItem of alertGroup.value.alerts; track alertItem) {\n <p [innerHTML]=\"sanitizeAlertText(alertItem.text)\"></p>\n }\n\n @if (alertGroup.value.alertDismissal.dismissStrategy !== 'none') {\n <label\n class=\"c8y-checkbox m-t-16\"\n title=\"{{ 'Don\\'t show again' | translate }}\"\n >\n <input\n type=\"checkbox\"\n #dismissPermanently\n />\n <span></span>\n <span>{{ \"Don't show again\" | translate }}</span>\n </label>\n }\n </div>\n }\n }\n </div>\n}\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "pipe", type: i1.C8yTranslatePipe, name: "translate" }, { kind: "pipe", type: i1$1.AsyncPipe, name: "async" }] }); }
2369
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.17", type: ChartAlertsComponent, isStandalone: true, selector: "c8y-chart-alerts", inputs: { alerts: "alerts" }, viewQueries: [{ propertyName: "dismissPermanently", first: true, predicate: ["dismissPermanently"], descendants: true }], ngImport: i0, template: "@if (alerts?.anyAlertExists$ | async) {\n <div class=\"overlay-center-vertically d-col p-16 p-r-24 p-l-24\">\n @for (alertGroup of alerts?.alertGroups; track alertGroup) {\n @if (alertGroup.value.alerts.length) {\n <div\n class=\"alert\"\n role=\"alert\"\n [ngClass]=\"'alert-' + alertGroup.type\"\n >\n @if (alertGroup.value.alertDismissal.dismissStrategy !== 'none') {\n <button\n class=\"close\"\n type=\"button\"\n (click)=\"\n alerts.dismissAlertGroup(alertGroup.type, dismissPermanently.nativeElement.checked)\n \"\n >\n <span aria-hidden=\"true\">\u00D7</span>\n <span class=\"sr-only\">{{ 'Close' | translate }}</span>\n </button>\n }\n\n @for (alertItem of alertGroup.value.alerts; track alertItem) {\n <p [innerHTML]=\"sanitizeAlertText(alertItem.text)\"></p>\n }\n\n @if (alertGroup.value.alertDismissal.dismissStrategy !== 'none') {\n <label\n class=\"c8y-checkbox m-t-16\"\n title=\"{{ 'Don\\'t show again' | translate }}\"\n >\n <input\n type=\"checkbox\"\n #dismissPermanently\n />\n <span></span>\n <span>{{ \"Don't show again\" | translate }}</span>\n </label>\n }\n </div>\n }\n }\n </div>\n}\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$2.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "pipe", type: i1$1.C8yTranslatePipe, name: "translate" }, { kind: "pipe", type: i1$2.AsyncPipe, name: "async" }] }); }
1986
2370
  }
1987
2371
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: ChartAlertsComponent, decorators: [{
1988
2372
  type: Component,
@@ -2027,12 +2411,12 @@ class ChartEventsService {
2027
2411
  const result = await Promise.all(promises);
2028
2412
  return result.flat();
2029
2413
  }
2030
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: ChartEventsService, deps: [{ token: i1$2.EventService }], target: i0.ɵɵFactoryTarget.Injectable }); }
2414
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: ChartEventsService, deps: [{ token: i1$3.EventService }], target: i0.ɵɵFactoryTarget.Injectable }); }
2031
2415
  static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: ChartEventsService }); }
2032
2416
  }
2033
2417
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: ChartEventsService, decorators: [{
2034
2418
  type: Injectable
2035
- }], ctorParameters: () => [{ type: i1$2.EventService }] });
2419
+ }], ctorParameters: () => [{ type: i1$3.EventService }] });
2036
2420
 
2037
2421
  class ChartAlarmsService {
2038
2422
  constructor(alarmService) {
@@ -2081,12 +2465,12 @@ class ChartAlarmsService {
2081
2465
  const result = await Promise.all(promises);
2082
2466
  return result.flat();
2083
2467
  }
2084
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: ChartAlarmsService, deps: [{ token: i1$2.AlarmService }], target: i0.ɵɵFactoryTarget.Injectable }); }
2468
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: ChartAlarmsService, deps: [{ token: i1$3.AlarmService }], target: i0.ɵɵFactoryTarget.Injectable }); }
2085
2469
  static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: ChartAlarmsService }); }
2086
2470
  }
2087
2471
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: ChartAlarmsService, decorators: [{
2088
2472
  type: Injectable
2089
- }], ctorParameters: () => [{ type: i1$2.AlarmService }] });
2473
+ }], ctorParameters: () => [{ type: i1$3.AlarmService }] });
2090
2474
 
2091
2475
  class ChartHelpersService {
2092
2476
  /**
@@ -2113,6 +2497,107 @@ class ChartHelpersService {
2113
2497
  dp.fragment === selected.fragment &&
2114
2498
  dp.series === selected.series);
2115
2499
  }
2500
+ /**
2501
+ * Maps a computed auto-aggregation interval string (e.g. `'5m'`, `'3h'`, `'7d'`) to
2502
+ * the nearest standard `aggregationType` for non-time-series tenants.
2503
+ * - seconds / minutes → MINUTELY (finest supported granularity)
2504
+ * - hours → HOURLY
2505
+ * - days → DAILY
2506
+ */
2507
+ mapIntervalToAggregationType(interval) {
2508
+ if (interval.endsWith('d'))
2509
+ return aggregationType.DAILY;
2510
+ if (interval.endsWith('h'))
2511
+ return aggregationType.HOURLY;
2512
+ if (interval.endsWith('m') || interval.endsWith('s'))
2513
+ return aggregationType.MINUTELY;
2514
+ return null;
2515
+ }
2516
+ /**
2517
+ * Computes the aggregation interval string (e.g. `30s`, `5m`, `24h`, `7d`) that
2518
+ * targets approximately `desiredDatapoints` buckets within the given time range.
2519
+ * The integer part is capped at 999 to satisfy the API constraint.
2520
+ */
2521
+ calculateAutoAggregationInterval(dateFrom, dateTo, desiredDatapoints) {
2522
+ const rangeMs = new Date(dateTo).valueOf() - new Date(dateFrom).valueOf();
2523
+ const intervalMs = rangeMs / Math.max(1, desiredDatapoints);
2524
+ // If each bucket would be sub-second, the zoom is fine enough to show raw data
2525
+ if (intervalMs <= 1_000)
2526
+ return null;
2527
+ // Use the most natural human-readable unit.
2528
+ // e.g. 24h / 300 dp → 288 s → 5 m (not 288s)
2529
+ const totalSeconds = Math.max(1, Math.round(intervalMs / 1_000));
2530
+ if (totalSeconds < 60)
2531
+ return `${Math.min(999, totalSeconds)}s`;
2532
+ const totalMinutes = Math.round(totalSeconds / 60);
2533
+ if (totalMinutes < 60)
2534
+ return `${Math.min(999, totalMinutes)}m`;
2535
+ const totalHours = Math.round(totalSeconds / 3_600);
2536
+ if (totalHours < 24)
2537
+ return `${Math.min(999, totalHours)}h`;
2538
+ const totalDays = Math.max(1, Math.round(totalSeconds / 86_400));
2539
+ return `${Math.min(999, totalDays)}d`;
2540
+ }
2541
+ /**
2542
+ * Scans raw DpWithValues to determine which datapoints should be collapsed to a single
2543
+ * series — either because all per-function values are identical at every timestamp (the
2544
+ * aggregation interval is too fine — each interval holds a single measurement — to
2545
+ * differentiate functions).
2546
+ */
2547
+ detectCollapsedDatapoints(dps) {
2548
+ const collapsed = new Set();
2549
+ for (const dp of dps) {
2550
+ const dpId = `${dp.__target?.id}${dp.fragment}${dp.series}`;
2551
+ const valueEntries = Object.values(dp.values ?? {});
2552
+ if (!valueEntries.length)
2553
+ continue;
2554
+ const renderTypeConfigs = dp.renderTypeConfigs;
2555
+ const renderAsBand = !!dp.renderAsBand;
2556
+ let fnsToCompare = [];
2557
+ let isBand = false;
2558
+ if (renderTypeConfigs) {
2559
+ const enabled = Object.keys(renderTypeConfigs).filter(k => renderTypeConfigs[k]?.enabled);
2560
+ const hasMinMax = enabled.includes('min') && enabled.includes('max');
2561
+ if (renderAsBand && hasMinMax) {
2562
+ isBand = true;
2563
+ }
2564
+ else if (enabled.length >= 2) {
2565
+ fnsToCompare = enabled;
2566
+ }
2567
+ else {
2568
+ continue;
2569
+ }
2570
+ }
2571
+ else if (dp.renderTypes && dp.renderTypes.length > 1) {
2572
+ const fns = dp.renderTypes.filter(rt => rt !== 'area');
2573
+ if (fns.length >= 2) {
2574
+ fnsToCompare = fns;
2575
+ }
2576
+ else {
2577
+ continue;
2578
+ }
2579
+ }
2580
+ else {
2581
+ continue;
2582
+ }
2583
+ if (isBand) {
2584
+ const allSame = valueEntries.every(vals => vals[0]?.min === vals[0]?.max);
2585
+ if (allSame)
2586
+ collapsed.add(dpId);
2587
+ }
2588
+ else {
2589
+ const allSame = valueEntries.every(vals => {
2590
+ const first = vals[0]?.[fnsToCompare[0]];
2591
+ if (first === undefined || first === null)
2592
+ return false;
2593
+ return fnsToCompare.slice(1).every(fn => vals[0]?.[fn] === first);
2594
+ });
2595
+ if (allSame)
2596
+ collapsed.add(dpId);
2597
+ }
2598
+ }
2599
+ return collapsed;
2600
+ }
2116
2601
  getTargetId(obj) {
2117
2602
  return obj?.__target?.id ?? obj?.target;
2118
2603
  }
@@ -2159,7 +2644,7 @@ class SelectAggregatedDatapointComponent {
2159
2644
  this.selectedDatapointColor = found?.color || '';
2160
2645
  }
2161
2646
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: SelectAggregatedDatapointComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
2162
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.2.17", type: SelectAggregatedDatapointComponent, isStandalone: true, selector: "c8y-select-aggregated-datapoint", inputs: { activeDatapoints: "activeDatapoints", value: "value" }, outputs: { valueChange: "valueChange" }, host: { classAttribute: "d-contents" }, usesOnChanges: true, ngImport: i0, template: "<div\n class=\"dropdown dropup\"\n dropdown\n #datapointDropdown=\"bs-dropdown\"\n [cdkTrapFocus]=\"datapointDropdown.isOpen\"\n *ngIf=\"activeDatapoints?.length > 0\"\n>\n <ng-template #selectedDatapoint>\n <div class=\"text-left\">\n <strong>{{ selectedDatapointLabel }}</strong>\n <br />\n {{ 'Click to change' | translate }}\n </div>\n </ng-template>\n <button\n class=\"btn btn-default d-flex a-i-center dropdown-toggle c8y-dropdown\"\n title=\"{{'Select a data point to view its aggregated values in the time slider.' | translate }}\"\n type=\"button\"\n dropdownToggle\n >\n <span\n [style.background-color]=\"selectedDatapointColor\"\n style=\"display: inline-block; width: 16px; height: 16px; border-radius: 50%\"\n ></span>\n <span class=\"caret\"></span>\n </button>\n <ul\n class=\"dropdown-menu dropdown-menu-right\"\n *dropdownMenu\n >\n <li *ngFor=\"let datapoint of activeDatapoints\">\n <button\n class=\"p-r-8\"\n type=\"button\"\n (click)=\"changeDatapointSelection(datapoint)\"\n >\n <span\n class=\"d-inline-block m-r-4\"\n [style.background-color]=\"datapoint?.color\"\n style=\"width: 16px; height: 16px; border-radius: 50%\"\n ></span>\n <span class=\"text-truncate\" style=\"max-width:220px!important\" title=\"{{ datapoint?.label || `${datapoint?.fragment}-${datapoint?.series}` }}\">\n {{ datapoint?.label || `${datapoint?.fragment}-${datapoint?.series}` }}\n </span>\n <span\n class=\"m-l-auto flex-no-shrink\"\n *ngIf=\"\n datapoint.__target.id === value?.__target.id &&\n datapoint.fragment === value?.fragment &&\n datapoint.series === value?.series\n \"\n >\n <i\n class=\"text-success h4\"\n [c8yIcon]=\"'check'\"\n ></i>\n </span>\n </button>\n </li>\n </ul>\n</div>\n<button\n class=\"btn-help\"\n [attr.aria-label]=\"'Help' | translate\"\n popover=\"{{\n 'Select a data point to view its aggregated values in the time slider.' | translate\n }}\"\n placement=\"left\"\n triggers=\"focus\"\n container=\"body\"\n></button>\n", dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "ngmodule", type: PopoverModule }, { kind: "directive", type: i1$3.PopoverDirective, selector: "[popover]", inputs: ["adaptivePosition", "boundariesElement", "popover", "popoverContext", "popoverTitle", "placement", "outsideClick", "triggers", "container", "containerClass", "isOpen", "delay"], outputs: ["onShown", "onHidden"], exportAs: ["bs-popover"] }, { kind: "ngmodule", type: TooltipModule }, { kind: "ngmodule", type: BsDropdownModule }, { kind: "directive", type: i2.BsDropdownMenuDirective, selector: "[bsDropdownMenu],[dropdownMenu]", exportAs: ["bs-dropdown-menu"] }, { kind: "directive", type: i2.BsDropdownToggleDirective, selector: "[bsDropdownToggle],[dropdownToggle]", exportAs: ["bs-dropdown-toggle"] }, { kind: "directive", type: i2.BsDropdownDirective, selector: "[bsDropdown], [dropdown]", inputs: ["placement", "triggers", "container", "dropup", "autoClose", "isAnimated", "insideClick", "isDisabled", "isOpen"], outputs: ["isOpenChange", "onShown", "onHidden"], exportAs: ["bs-dropdown"] }, { kind: "ngmodule", type: A11yModule }, { kind: "directive", type: i3.CdkTrapFocus, selector: "[cdkTrapFocus]", inputs: ["cdkTrapFocus", "cdkTrapFocusAutoCapture"], exportAs: ["cdkTrapFocus"] }, { kind: "directive", type: IconDirective, selector: "[c8yIcon]", inputs: ["c8yIcon"] }, { kind: "directive", type: NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "pipe", type: C8yTranslatePipe, name: "translate" }] }); }
2647
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.2.17", type: SelectAggregatedDatapointComponent, isStandalone: true, selector: "c8y-select-aggregated-datapoint", inputs: { activeDatapoints: "activeDatapoints", value: "value" }, outputs: { valueChange: "valueChange" }, host: { classAttribute: "d-contents" }, usesOnChanges: true, ngImport: i0, template: "<div\n class=\"dropdown dropup\"\n dropdown\n #datapointDropdown=\"bs-dropdown\"\n [cdkTrapFocus]=\"datapointDropdown.isOpen\"\n *ngIf=\"activeDatapoints?.length > 0\"\n>\n <ng-template #selectedDatapoint>\n <div class=\"text-left\">\n <strong>{{ selectedDatapointLabel }}</strong>\n <br />\n {{ 'Click to change' | translate }}\n </div>\n </ng-template>\n <button\n class=\"btn btn-default d-flex a-i-center dropdown-toggle c8y-dropdown\"\n title=\"{{'Select a data point to view its aggregated values in the time slider.' | translate }}\"\n type=\"button\"\n dropdownToggle\n >\n <span\n [style.background-color]=\"selectedDatapointColor\"\n style=\"display: inline-block; width: 16px; height: 16px; border-radius: 50%\"\n ></span>\n <span class=\"caret\"></span>\n </button>\n <ul\n class=\"dropdown-menu dropdown-menu-right\"\n *dropdownMenu\n >\n <li *ngFor=\"let datapoint of activeDatapoints\">\n <button\n class=\"p-r-8\"\n type=\"button\"\n (click)=\"changeDatapointSelection(datapoint)\"\n >\n <span\n class=\"d-inline-block m-r-4\"\n [style.background-color]=\"datapoint?.color\"\n style=\"width: 16px; height: 16px; border-radius: 50%\"\n ></span>\n <span class=\"text-truncate\" style=\"max-width:220px!important\" title=\"{{ datapoint?.label || `${datapoint?.fragment}-${datapoint?.series}` }}\">\n {{ datapoint?.label || `${datapoint?.fragment}-${datapoint?.series}` }}\n </span>\n <span\n class=\"m-l-auto flex-no-shrink\"\n *ngIf=\"\n datapoint.__target.id === value?.__target.id &&\n datapoint.fragment === value?.fragment &&\n datapoint.series === value?.series\n \"\n >\n <i\n class=\"text-success h4\"\n [c8yIcon]=\"'check'\"\n ></i>\n </span>\n </button>\n </li>\n </ul>\n</div>\n<button\n class=\"btn-help\"\n [attr.aria-label]=\"'Help' | translate\"\n popover=\"{{\n 'Select a data point to view its aggregated values in the time slider.' | translate\n }}\"\n placement=\"left\"\n triggers=\"focus\"\n container=\"body\"\n></button>\n", dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "ngmodule", type: PopoverModule }, { kind: "directive", type: i1$4.PopoverDirective, selector: "[popover]", inputs: ["adaptivePosition", "boundariesElement", "popover", "popoverContext", "popoverTitle", "placement", "outsideClick", "triggers", "container", "containerClass", "isOpen", "delay"], outputs: ["onShown", "onHidden"], exportAs: ["bs-popover"] }, { kind: "ngmodule", type: TooltipModule }, { kind: "ngmodule", type: BsDropdownModule }, { kind: "directive", type: i2.BsDropdownMenuDirective, selector: "[bsDropdownMenu],[dropdownMenu]", exportAs: ["bs-dropdown-menu"] }, { kind: "directive", type: i2.BsDropdownToggleDirective, selector: "[bsDropdownToggle],[dropdownToggle]", exportAs: ["bs-dropdown-toggle"] }, { kind: "directive", type: i2.BsDropdownDirective, selector: "[bsDropdown], [dropdown]", inputs: ["placement", "triggers", "container", "dropup", "autoClose", "isAnimated", "insideClick", "isDisabled", "isOpen"], outputs: ["isOpenChange", "onShown", "onHidden"], exportAs: ["bs-dropdown"] }, { kind: "ngmodule", type: A11yModule }, { kind: "directive", type: i3.CdkTrapFocus, selector: "[cdkTrapFocus]", inputs: ["cdkTrapFocus", "cdkTrapFocusAutoCapture"], exportAs: ["cdkTrapFocus"] }, { kind: "directive", type: IconDirective, selector: "[c8yIcon]", inputs: ["c8yIcon"] }, { kind: "directive", type: NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "pipe", type: C8yTranslatePipe, name: "translate" }] }); }
2163
2648
  }
2164
2649
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: SelectAggregatedDatapointComponent, decorators: [{
2165
2650
  type: Component,
@@ -2184,7 +2669,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImpo
2184
2669
  type: Output
2185
2670
  }] } });
2186
2671
 
2187
- /* eslint-disable @typescript-eslint/no-explicit-any */
2188
2672
  class ChartsComponent {
2189
2673
  get axisColor() {
2190
2674
  return (getComputedStyle(this.chart.nativeElement).getPropertyValue('--text-muted')?.trim() ||
@@ -2206,6 +2690,13 @@ class ChartsComponent {
2206
2690
  this.finishLoading = new EventEmitter(false);
2207
2691
  this.updateActiveDatapoints = new EventEmitter();
2208
2692
  this.updateAggregatedSliderDatapoint = new EventEmitter();
2693
+ /**
2694
+ * Emitted after each data load with the set of datapointIds (target+fragment+series)
2695
+ * whose per-function aggregation series all return identical values — indicating that
2696
+ * the current zoom/aggregation interval is too fine (each interval holds a single
2697
+ * measurement) to differentiate functions.
2698
+ */
2699
+ this.duplicateAggregationsDetected = new EventEmitter();
2209
2700
  this.CHART_VIEW_CONTEXT = CHART_VIEW_CONTEXT;
2210
2701
  this.AGGREGATION_INTERVAL_MAP = {
2211
2702
  [aggregationType.MINUTELY]: '1m',
@@ -2213,18 +2704,26 @@ class ChartsComponent {
2213
2704
  [aggregationType.DAILY]: '1d'
2214
2705
  };
2215
2706
  this.configChangedSubject = new BehaviorSubject(null);
2707
+ /** Series names hidden by the user via per-function legend toggles; re-applied after each setOption. */
2708
+ this.userHiddenSeriesNames = new Set();
2709
+ /** DpIds whose per-fn series were stripped to a single series on the last render. */
2710
+ this.lastCollapsedDpIds = new Set();
2216
2711
  this.destroyRef = inject(DestroyRef);
2217
2712
  this.measurementService = inject(MeasurementService);
2218
2713
  this.echartsOptionsService = inject(EchartsOptionsService);
2219
2714
  this.chartRealtimeService = inject(ChartRealtimeService);
2220
2715
  this.chartEventsService = inject(ChartEventsService);
2221
2716
  this.chartAlarmsService = inject(ChartAlarmsService);
2717
+ this.chartHelpersService = inject(ChartHelpersService);
2222
2718
  this.mediator = inject(WidgetTimeContextMediatorService);
2223
2719
  this.widgetTimeContextDateRangeService = inject(WidgetTimeContextDateRangeService);
2224
2720
  this.themeSwitcherService = inject(ThemeSwitcherService);
2225
2721
  this.loadedTimeRange = null;
2226
2722
  /** Tracks if the last data fetch returned truncated data */
2227
2723
  this.dataWasTruncated = false;
2724
+ /** Tracks the aggregation interval used in the last fetch so we can detect
2725
+ * whether a zoom-in would produce a finer interval (and thus warrants a refetch). */
2726
+ this.lastAutoAggregationInterval = null;
2228
2727
  /** Tracks if current change originated from zoom interaction */
2229
2728
  this.isChangeFromZoom = false;
2230
2729
  /** Tracks if the slider is being dragged — suppresses config updates mid-drag */
@@ -2239,7 +2738,27 @@ class ChartsComponent {
2239
2738
  }
2240
2739
  // Use padded range since data is fetched with 60s padding
2241
2740
  this.loadedTimeRange = { ...this.getTimeRange(60_000) };
2242
- return this.getChartOptions(datapointsWithValues);
2741
+ // Pre-render collapse detection from raw API values.
2742
+ // Emit early so the view legend/dropdown rebuilds BEFORE the chart renders.
2743
+ const collapsed = this.chartHelpersService.detectCollapsedDatapoints(datapointsWithValues);
2744
+ this.duplicateAggregationsDetected.emit(collapsed);
2745
+ this.lastCollapsedDpIds = collapsed;
2746
+ // Strip multi-fn rendering config from collapsed DPs so getChartOptions
2747
+ // produces a single series in dp.color instead of multiple per-fn series.
2748
+ const processedDps = datapointsWithValues.map(dp => {
2749
+ const dpId = `${dp.__target?.id}${dp.fragment}${dp.series}`;
2750
+ if (!collapsed.has(dpId))
2751
+ return dp;
2752
+ const stripped = {
2753
+ ...dp,
2754
+ renderTypes: null,
2755
+ renderTypeColors: null,
2756
+ renderTypeConfigs: null,
2757
+ renderAsBand: false
2758
+ };
2759
+ return stripped;
2760
+ });
2761
+ return this.getChartOptions(processedDps);
2243
2762
  }), tap(options => {
2244
2763
  this.isFetching = false;
2245
2764
  this.finishLoading.emit(false);
@@ -2249,6 +2768,14 @@ class ChartsComponent {
2249
2768
  this.chartRealtimeService.stopRealtime();
2250
2769
  this.startRealtimeIfPossible();
2251
2770
  if (this.echartsInstance) {
2771
+ // Apply user-hidden series into legend.selected so setOption initializes with the
2772
+ // correct state. Dispatching legendUnSelect after setOption races with lazyUpdate=true,
2773
+ // because ECharts defers the actual render and overwrites the legend state afterward.
2774
+ if (this.userHiddenSeriesNames.size > 0) {
2775
+ const selectedMap = {};
2776
+ this.userHiddenSeriesNames.forEach(name => (selectedMap[name] = false));
2777
+ options.legend = { ...(options.legend ?? {}), selected: selectedMap };
2778
+ }
2252
2779
  // If a series is highlighted, set the opacity into the chart options before
2253
2780
  // rendering so the lazy setOption already carries the correct visual state.
2254
2781
  // This avoids any async race — the render itself is the source of truth.
@@ -2290,7 +2817,7 @@ class ChartsComponent {
2290
2817
  const isWithinLoadedRange = this.loadedTimeRange &&
2291
2818
  new Date(currentRange.dateFrom) >= new Date(this.loadedTimeRange.dateFrom) &&
2292
2819
  new Date(currentRange.dateTo) <= new Date(this.loadedTimeRange.dateTo);
2293
- // Sync zoom level across multiple data point graph widgets on the same dashboard
2820
+ // Sync zoom level across multiple Data graph widgets on the same dashboard
2294
2821
  if (this.echartsInstance) {
2295
2822
  this.echartsInstance.dispatchAction({
2296
2823
  type: 'dataZoom',
@@ -2298,7 +2825,23 @@ class ChartsComponent {
2298
2825
  endValue: new Date(currentRange.dateTo).getTime()
2299
2826
  }, { silent: true });
2300
2827
  }
2301
- if (!isWithinLoadedRange || this.dataWasTruncated) {
2828
+ // Auto mode with "Keep loading data when zooming in" disabled: a zoom that stays
2829
+ // within the already loaded range must not trigger any request the chart just
2830
+ // zooms into the data it has. When enabled (the default), zooming in refetches at
2831
+ // a finer interval to maintain the desired number of aggregation points. Zooming
2832
+ // out beyond the loaded range or zooming into truncated data always refetches.
2833
+ if (isWithinLoadedRange &&
2834
+ !this.dataWasTruncated &&
2835
+ this.config.autoAggregation &&
2836
+ !(this.config.autoAggregation.keepDataOnZoomIn ?? true)) {
2837
+ return;
2838
+ }
2839
+ let needsAutoAggregationRefetch = false;
2840
+ if (this.config.autoAggregation) {
2841
+ const zoomedInterval = this.chartHelpersService.calculateAutoAggregationInterval(currentRange.dateFrom, currentRange.dateTo, this.config.autoAggregation.datapoints);
2842
+ needsAutoAggregationRefetch = zoomedInterval !== this.lastAutoAggregationInterval;
2843
+ }
2844
+ if (!isWithinLoadedRange || this.dataWasTruncated || needsAutoAggregationRefetch) {
2302
2845
  this.configChangedSubject.next();
2303
2846
  }
2304
2847
  }
@@ -2474,7 +3017,9 @@ class ChartsComponent {
2474
3017
  }
2475
3018
  loadMoreData() {
2476
3019
  const currentRange = this.widgetTimeContextDateRangeService.initialTimeRange();
2477
- const currentTimeRangeInMs = this.echartsOptionsService.calculateExtendedIntervalInMs(this.config.dateTimeContext?.interval || this.config.interval || 'custom', {
3020
+ const currentTimeRangeInMs = this.echartsOptionsService.calculateExtendedIntervalInMs(this.config.dateTimeContext?.interval ||
3021
+ this.config.interval || // eslint-disable-next-line @typescript-eslint/no-explicit-any
3022
+ 'custom', {
2478
3023
  dateTo: currentRange.dateTo,
2479
3024
  dateFrom: currentRange.dateFrom
2480
3025
  });
@@ -2508,13 +3053,15 @@ class ChartsComponent {
2508
3053
  */
2509
3054
  toggleDatapointSeriesVisibility(datapoint) {
2510
3055
  const allSeries = this.echartsInstance.getOption().series;
2511
- const seriesToUpdate = allSeries.find(s => s.datapointId === `${datapoint.__target?.id}${datapoint.fragment}${datapoint.series}`);
2512
- if (seriesToUpdate) {
3056
+ const dpId = `${datapoint.__target?.id}${datapoint.fragment}${datapoint.series}`;
3057
+ // Use filter (not find) so all matching series (e.g. multiple per-fn series) are toggled.
3058
+ const seriesToUpdate = allSeries.filter(s => s.datapointId === dpId);
3059
+ seriesToUpdate.forEach(s => {
2513
3060
  this.echartsInstance.dispatchAction({
2514
3061
  type: datapoint.__active ? 'legendSelect' : 'legendUnSelect',
2515
- name: seriesToUpdate.name
3062
+ name: s.name
2516
3063
  });
2517
- }
3064
+ });
2518
3065
  const deviceId = datapoint.__target?.id;
2519
3066
  const isHiding = !datapoint.__active;
2520
3067
  const visibleTypes = new Set(this.config.alarmsEventsConfigs
@@ -2536,6 +3083,22 @@ class ChartsComponent {
2536
3083
  }
2537
3084
  });
2538
3085
  }
3086
+ /** Toggles a single chart series by its exact name (used for per-function legend entries). */
3087
+ toggleSeriesByName(seriesName, visible) {
3088
+ if (!this.echartsInstance) {
3089
+ return;
3090
+ }
3091
+ if (visible) {
3092
+ this.userHiddenSeriesNames.delete(seriesName);
3093
+ }
3094
+ else {
3095
+ this.userHiddenSeriesNames.add(seriesName);
3096
+ }
3097
+ this.echartsInstance.dispatchAction({
3098
+ type: visible ? 'legendSelect' : 'legendUnSelect',
3099
+ name: seriesName
3100
+ });
3101
+ }
2539
3102
  /**
2540
3103
  * Toggles alarm/event series visibility via legendSelect/legendUnSelect.
2541
3104
  * Matches all mark-line and mark-point series for the alarm type across every axis,
@@ -2650,7 +3213,8 @@ class ChartsComponent {
2650
3213
  showSlider: this.config.showSlider ?? chartDefaults.showSlider,
2651
3214
  smoothLines: this.config.smoothLines ?? chartDefaults.smoothLines,
2652
3215
  numberOfDecimalPlaces: this.config.numberOfDecimalPlaces ?? chartDefaults.numberOfDecimalPlaces,
2653
- axisColor: this.axisColor
3216
+ axisColor: this.axisColor,
3217
+ aggregationActive: !!(this.config.aggregation || this.config.autoAggregation)
2654
3218
  };
2655
3219
  }
2656
3220
  getDefaultChartOptions() {
@@ -2818,6 +3382,8 @@ class ChartsComponent {
2818
3382
  smoothLines: false,
2819
3383
  showSlider: false,
2820
3384
  showOnlyAlarmsOrEvents: true,
3385
+ aggregationActive: !!(this.config.aggregation || this.config.autoAggregation),
3386
+ collapsedDpIds: this.lastCollapsedDpIds,
2821
3387
  axisColor: this.axisColor
2822
3388
  });
2823
3389
  }
@@ -2836,11 +3402,14 @@ class ChartsComponent {
2836
3402
  dateTo: timeRange.dateTo
2837
3403
  }, this.config.aggregatedDatapoint))
2838
3404
  : undefined;
2839
- return this.echartsOptionsService.getChartOptions(datapointsWithValues, timeRange, {
3405
+ return this.echartsOptionsService.getChartOptions(datapointsWithValues,
3406
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
3407
+ timeRange, {
2840
3408
  YAxis: this.config.yAxisSplitLines || false,
2841
3409
  XAxis: this.config.xAxisSplitLines || false
2842
3410
  }, this.events, this.alarms, {
2843
3411
  sliderPositionMode,
3412
+ collapsedDpIds: this.lastCollapsedDpIds,
2844
3413
  ...this.displayOptions
2845
3414
  }, undefined, aggregatedDatapoint);
2846
3415
  }
@@ -2875,8 +3444,11 @@ class ChartsComponent {
2875
3444
  XAxis: this.config.xAxisSplitLines || false
2876
3445
  }, this.events, this.alarms, {
2877
3446
  sliderPositionMode,
3447
+ collapsedDpIds: this.lastCollapsedDpIds,
2878
3448
  ...this.displayOptions
2879
- }, timeRange, aggregatedDatapoint, true);
3449
+ },
3450
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
3451
+ timeRange, aggregatedDatapoint, true);
2880
3452
  }
2881
3453
  }
2882
3454
  fetchSeriesForDatapoints$() {
@@ -2898,47 +3470,122 @@ class ChartsComponent {
2898
3470
  const datapointsWithValuesRequests = [];
2899
3471
  const timeRange = this.getTimeRange();
2900
3472
  for (const dp of this.activeDatapoints) {
2901
- const request = defer(() => this.measurementService.listSeries({
2902
- revert: true,
2903
- ...timeRange,
2904
- source: dp.__target?.id || '',
2905
- series: [`${dp.fragment}.${dp.series}`],
2906
- ...(this.config.aggregation &&
2907
- (!this.config.showAdvancedChartOptions || dp.renderType === 'area') && {
2908
- aggregationType: this.config.aggregation
2909
- }),
2910
- ...(this.config.aggregation &&
2911
- this.config.showAdvancedChartOptions &&
2912
- dp.renderType !== 'area' && {
2913
- aggregationFunction: dp.renderType,
2914
- aggregationInterval: this.AGGREGATION_INTERVAL_MAP[this.config.aggregation]
2915
- })
2916
- })).pipe(map(res => {
2917
- const values = res.data.values;
2918
- if (res.data.truncated && this.config.dateFrom) {
2919
- values[new Date(this.config.dateFrom).toISOString()] = [{ min: null, max: null }];
2920
- }
2921
- else {
2922
- if (this.alerts) {
3473
+ const renderTypes = dp.renderTypes?.length > 1 ? dp.renderTypes : null;
3474
+ if (renderTypes &&
3475
+ (this.config.autoAggregation ||
3476
+ (this.config.showAdvancedChartOptions && this.config.aggregation))) {
3477
+ const nonAreaRenderTypes = renderTypes.filter(rt => rt !== 'area');
3478
+ const singleRequest = defer(() => this.measurementService.listSeries({
3479
+ revert: true,
3480
+ ...timeRange,
3481
+ source: dp.__target?.id || '',
3482
+ series: [`${dp.fragment}.${dp.series}`],
3483
+ ...(this.config.autoAggregation &&
3484
+ (() => {
3485
+ const interval = this.chartHelpersService.calculateAutoAggregationInterval(timeRange.dateFrom, timeRange.dateTo, this.config.autoAggregation.datapoints);
3486
+ if (!interval)
3487
+ return {};
3488
+ if (this.config.showAdvancedChartOptions) {
3489
+ return { aggregationFunction: nonAreaRenderTypes, aggregationInterval: interval };
3490
+ }
3491
+ const aggType = this.chartHelpersService.mapIntervalToAggregationType(interval);
3492
+ return aggType ? { aggregationType: aggType } : {};
3493
+ })()),
3494
+ ...(!this.config.autoAggregation &&
3495
+ this.config.aggregation && {
3496
+ aggregationFunction: nonAreaRenderTypes,
3497
+ aggregationInterval: this.AGGREGATION_INTERVAL_MAP[this.config.aggregation]
3498
+ })
3499
+ })).pipe(map(res => {
3500
+ const values = res.data.values;
3501
+ if (res.data.truncated && this.config.dateFrom) {
3502
+ values[new Date(this.config.dateFrom).toISOString()] = [{ min: null, max: null }];
3503
+ }
3504
+ else if (this.alerts) {
2923
3505
  this.alerts.clear();
2924
3506
  }
2925
- }
2926
- // If the datapoint already has a unit (e.g. from config), keep it. Otherwise, try to get it from the series response.
2927
- if (!dp.unit) {
2928
- const unitFromSeries = res.data.series[0]?.unit;
2929
- if (unitFromSeries) {
2930
- dp.unit = unitFromSeries;
3507
+ return {
3508
+ ...dp,
3509
+ renderType: dp.renderType,
3510
+ values,
3511
+ truncated: res.data.truncated
3512
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
3513
+ };
3514
+ }));
3515
+ datapointsWithValuesRequests.push(singleRequest);
3516
+ }
3517
+ else {
3518
+ const request = defer(() => this.measurementService.listSeries({
3519
+ revert: true,
3520
+ ...timeRange,
3521
+ source: dp.__target?.id || '',
3522
+ series: [`${dp.fragment}.${dp.series}`],
3523
+ // Auto aggregation: compute interval from time range + desired datapoints.
3524
+ // Falls back to raw data when zoomed in fine enough (returns null).
3525
+ ...(this.config.autoAggregation &&
3526
+ (() => {
3527
+ const interval = this.chartHelpersService.calculateAutoAggregationInterval(timeRange.dateFrom, timeRange.dateTo, this.config.autoAggregation.datapoints);
3528
+ if (!interval)
3529
+ return {};
3530
+ if (this.config.showAdvancedChartOptions) {
3531
+ return {
3532
+ aggregationFunction: dp.renderType ?? 'avg',
3533
+ aggregationInterval: interval
3534
+ };
3535
+ }
3536
+ const aggType = this.chartHelpersService.mapIntervalToAggregationType(interval);
3537
+ return aggType ? { aggregationType: aggType } : {};
3538
+ })()),
3539
+ // Standard aggregation (only when auto aggregation is not active)
3540
+ ...(!this.config.autoAggregation &&
3541
+ this.config.aggregation &&
3542
+ (!this.config.showAdvancedChartOptions || dp.renderType === 'area') && {
3543
+ aggregationType: this.config.aggregation
3544
+ }),
3545
+ ...(!this.config.autoAggregation &&
3546
+ this.config.aggregation &&
3547
+ this.config.showAdvancedChartOptions &&
3548
+ dp.renderType !== 'area' && {
3549
+ aggregationFunction: dp.renderType,
3550
+ aggregationInterval: this.AGGREGATION_INTERVAL_MAP[this.config.aggregation]
3551
+ })
3552
+ })).pipe(map(res => {
3553
+ const values = res.data.values;
3554
+ if (res.data.truncated && this.config.dateFrom) {
3555
+ values[new Date(this.config.dateFrom).toISOString()] = [{ min: null, max: null }];
2931
3556
  }
2932
- }
2933
- return { ...dp, values, truncated: res.data.truncated };
2934
- }));
2935
- datapointsWithValuesRequests.push(request);
3557
+ else {
3558
+ if (this.alerts) {
3559
+ this.alerts.clear();
3560
+ }
3561
+ }
3562
+ // If the datapoint already has a unit (e.g. from config), keep it. Otherwise, try to get it from the series response.
3563
+ if (!dp.unit) {
3564
+ const unitFromSeries = res.data.series[0]?.unit;
3565
+ if (unitFromSeries) {
3566
+ dp.unit = unitFromSeries;
3567
+ }
3568
+ }
3569
+ return {
3570
+ ...dp,
3571
+ renderType: dp.renderType,
3572
+ values,
3573
+ truncated: res.data.truncated
3574
+ };
3575
+ }));
3576
+ datapointsWithValuesRequests.push(request);
3577
+ }
2936
3578
  }
2937
3579
  return forkJoin(datapointsWithValuesRequests).pipe(tap((dpsWithValues) => {
2938
3580
  this.dataWasTruncated = dpsWithValues.some(dp => dp['truncated']);
2939
3581
  if (this.dataWasTruncated) {
2940
3582
  this.addTruncatedDataAlert();
2941
3583
  }
3584
+ if (this.config.autoAggregation) {
3585
+ const timeRange = this.getTimeRange(60_000);
3586
+ this.lastAutoAggregationInterval =
3587
+ this.chartHelpersService.calculateAutoAggregationInterval(timeRange.dateFrom, timeRange.dateTo, this.config.autoAggregation.datapoints);
3588
+ }
2942
3589
  }));
2943
3590
  }
2944
3591
  fetchAggregatedSeriesForSelectedDatapoint$(customTimeRange, aggregatedDatapoint) {
@@ -2952,13 +3599,35 @@ class ChartsComponent {
2952
3599
  ...(customTimeRange ? customTimeRange : timeRange),
2953
3600
  source: aggregatedDatapoint.__target?.id || '',
2954
3601
  series: [`${aggregatedDatapoint.fragment}.${aggregatedDatapoint.series}`],
2955
- ...((this.config.aggregation || customTimeRange) &&
3602
+ // Auto aggregation: compute interval from time range + desired datapoints.
3603
+ // Falls back to raw data when the zoom level is fine enough that aggregation
3604
+ // would add no value (calculateAutoAggregationInterval returns null).
3605
+ // Non-migrated tenants don't support aggregationFunction/aggregationInterval,
3606
+ // so we map the computed interval to the nearest standard aggregationType.
3607
+ ...(this.config.autoAggregation &&
3608
+ (() => {
3609
+ const interval = this.chartHelpersService.calculateAutoAggregationInterval((customTimeRange ?? timeRange).dateFrom, (customTimeRange ?? timeRange).dateTo, this.config.autoAggregation.datapoints);
3610
+ if (!interval)
3611
+ return {};
3612
+ if (this.config.showAdvancedChartOptions) {
3613
+ return {
3614
+ aggregationFunction: aggregatedDatapoint.renderType ?? 'avg',
3615
+ aggregationInterval: interval
3616
+ };
3617
+ }
3618
+ const aggType = this.chartHelpersService.mapIntervalToAggregationType(interval);
3619
+ return aggType ? { aggregationType: aggType } : {};
3620
+ })()),
3621
+ // Standard aggregation (only when auto aggregation is not active)
3622
+ ...(!this.config.autoAggregation &&
3623
+ (this.config.aggregation || customTimeRange) &&
2956
3624
  (!this.config.showAdvancedChartOptions || aggregatedDatapoint.renderType === 'area') && {
2957
3625
  aggregationType: customTimeRange
2958
3626
  ? this.getAggregationTypeForTimeRange()
2959
3627
  : this.config.aggregation
2960
3628
  }),
2961
- ...((this.config.aggregation || customTimeRange) &&
3629
+ ...(!this.config.autoAggregation &&
3630
+ (this.config.aggregation || customTimeRange) &&
2962
3631
  this.config.showAdvancedChartOptions &&
2963
3632
  aggregatedDatapoint.renderType !== 'area' && {
2964
3633
  aggregationFunction: aggregatedDatapoint.renderType,
@@ -3042,7 +3711,7 @@ class ChartsComponent {
3042
3711
  return { dateFrom: timeRange.dateFrom.toISOString(), dateTo: timeRange.dateTo.toISOString() };
3043
3712
  }
3044
3713
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: ChartsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
3045
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.17", type: ChartsComponent, isStandalone: true, selector: "c8y-charts", inputs: { config: "config", alerts: "alerts", chartViewContext: "chartViewContext" }, outputs: { configChangeOnZoomOut: "configChangeOnZoomOut", timeRangeChangeOnRealtime: "timeRangeChangeOnRealtime", datapointOutOfSync: "datapointOutOfSync", datapointBackInSync: "datapointBackInSync", updateAlarmsAndEvents: "updateAlarmsAndEvents", isMarkedAreaEnabled: "isMarkedAreaEnabled", finishLoading: "finishLoading", updateActiveDatapoints: "updateActiveDatapoints", updateAggregatedSliderDatapoint: "updateAggregatedSliderDatapoint" }, providers: [
3714
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.17", type: ChartsComponent, isStandalone: true, selector: "c8y-charts", inputs: { config: "config", alerts: "alerts", chartViewContext: "chartViewContext" }, outputs: { configChangeOnZoomOut: "configChangeOnZoomOut", timeRangeChangeOnRealtime: "timeRangeChangeOnRealtime", datapointOutOfSync: "datapointOutOfSync", datapointBackInSync: "datapointBackInSync", updateAlarmsAndEvents: "updateAlarmsAndEvents", isMarkedAreaEnabled: "isMarkedAreaEnabled", finishLoading: "finishLoading", updateActiveDatapoints: "updateActiveDatapoints", updateAggregatedSliderDatapoint: "updateAggregatedSliderDatapoint", duplicateAggregationsDetected: "duplicateAggregationsDetected" }, providers: [
3046
3715
  { provide: NGX_ECHARTS_CONFIG, useFactory: () => ({ echarts: () => import('echarts') }) },
3047
3716
  ChartRealtimeService,
3048
3717
  MeasurementRealtimeService,
@@ -3052,8 +3721,9 @@ class ChartsComponent {
3052
3721
  EchartsOptionsService,
3053
3722
  YAxisService,
3054
3723
  AlarmSeverityToIconPipe,
3055
- AlarmSeverityToLabelPipe
3056
- ], viewQueries: [{ propertyName: "chart", first: true, predicate: ["chart"], descendants: true }], usesOnChanges: true, ngImport: i0, template: "<div\n class=\"p-relative fit-h chart-container\"\n [attr.data-cy]=\"\n config?.realtime ? 'c8y-charts--realtime-active' : 'c8y-charts--realtime-inactive'\n \"\n>\n <div\n class=\"fit-w fit-h\"\n #chart\n echarts\n [options]=\"chartOption$ | async\"\n (chartInit)=\"onChartInit($event)\"\n ></div>\n\n <c8y-chart-alerts [alerts]=\"alerts\"></c8y-chart-alerts>\n\n @if (showLoadMore) {\n <div class=\"chart-load-more\">\n <button\n class=\"btn btn-primary btn-block\"\n [disabled]=\"isFetching\"\n (click)=\"loadMoreData()\"\n >\n <i c8yIcon=\"restore\"></i>\n {{ 'Load more' | translate }}\n </button>\n </div>\n }\n\n @if (\n activeDatapoints.length > 1 &&\n config?.showSlider &&\n chartViewContext !== CHART_VIEW_CONTEXT.WIDGET_VIEW\n ) {\n <div class=\"e-charts--aggregated-datapoint\">\n <c8y-select-aggregated-datapoint\n [value]=\"config?.aggregatedDatapoint || activeDatapoints[0]\"\n (valueChange)=\"updateAggregatedSliderDatapoint.emit($event)\"\n [activeDatapoints]=\"activeDatapoints\"\n ></c8y-select-aggregated-datapoint>\n </div>\n }\n</div>\n", dependencies: [{ kind: "ngmodule", type: CommonModule$1 }, { kind: "ngmodule", type: CoreModule }, { kind: "directive", type: i1.IconDirective, selector: "[c8yIcon]", inputs: ["c8yIcon"] }, { kind: "ngmodule", type: NgxEchartsModule }, { kind: "directive", type: i2$1.NgxEchartsDirective, selector: "echarts, [echarts]", inputs: ["options", "theme", "initOpts", "merge", "autoResize", "loading", "loadingType", "loadingOpts"], outputs: ["chartInit", "optionsError", "chartClick", "chartDblClick", "chartMouseDown", "chartMouseMove", "chartMouseUp", "chartMouseOver", "chartMouseOut", "chartGlobalOut", "chartContextMenu", "chartHighlight", "chartDownplay", "chartSelectChanged", "chartLegendSelectChanged", "chartLegendSelected", "chartLegendUnselected", "chartLegendLegendSelectAll", "chartLegendLegendInverseSelect", "chartLegendScroll", "chartDataZoom", "chartDataRangeSelected", "chartGraphRoam", "chartGeoRoam", "chartTreeRoam", "chartTimelineChanged", "chartTimelinePlayChanged", "chartRestore", "chartDataViewChanged", "chartMagicTypeChanged", "chartGeoSelectChanged", "chartGeoSelected", "chartGeoUnselected", "chartAxisAreaSelected", "chartBrush", "chartBrushEnd", "chartBrushSelected", "chartGlobalCursorTaken", "chartRendered", "chartFinished"], exportAs: ["echarts"] }, { kind: "ngmodule", type: TooltipModule }, { kind: "ngmodule", type: PopoverModule }, { kind: "component", type: ChartAlertsComponent, selector: "c8y-chart-alerts", inputs: ["alerts"] }, { kind: "ngmodule", type: AlarmsModule }, { kind: "component", type: SelectAggregatedDatapointComponent, selector: "c8y-select-aggregated-datapoint", inputs: ["activeDatapoints", "value"], outputs: ["valueChange"] }, { kind: "pipe", type: i1$1.AsyncPipe, name: "async" }, { kind: "pipe", type: i1.C8yTranslatePipe, name: "translate" }] }); }
3724
+ AlarmSeverityToLabelPipe,
3725
+ ChartHelpersService
3726
+ ], viewQueries: [{ propertyName: "chart", first: true, predicate: ["chart"], descendants: true }], usesOnChanges: true, ngImport: i0, template: "<div\n class=\"p-relative fit-h chart-container\"\n [attr.data-cy]=\"\n config?.realtime ? 'c8y-charts--realtime-active' : 'c8y-charts--realtime-inactive'\n \"\n>\n <div\n class=\"fit-w fit-h\"\n #chart\n echarts\n [options]=\"chartOption$ | async\"\n (chartInit)=\"onChartInit($event)\"\n ></div>\n\n <c8y-chart-alerts [alerts]=\"alerts\"></c8y-chart-alerts>\n\n @if (showLoadMore) {\n <div class=\"chart-load-more\">\n <button\n class=\"btn btn-primary btn-block\"\n [disabled]=\"isFetching\"\n (click)=\"loadMoreData()\"\n >\n <i c8yIcon=\"restore\"></i>\n {{ 'Load more' | translate }}\n </button>\n </div>\n }\n\n @if (\n activeDatapoints.length > 1 &&\n config?.showSlider &&\n chartViewContext !== CHART_VIEW_CONTEXT.WIDGET_VIEW\n ) {\n <div class=\"e-charts--aggregated-datapoint\">\n <c8y-select-aggregated-datapoint\n [value]=\"config?.aggregatedDatapoint || activeDatapoints[0]\"\n (valueChange)=\"updateAggregatedSliderDatapoint.emit($event)\"\n [activeDatapoints]=\"activeDatapoints\"\n ></c8y-select-aggregated-datapoint>\n </div>\n }\n</div>\n", dependencies: [{ kind: "ngmodule", type: CommonModule$1 }, { kind: "ngmodule", type: CoreModule }, { kind: "directive", type: i1$1.IconDirective, selector: "[c8yIcon]", inputs: ["c8yIcon"] }, { kind: "ngmodule", type: NgxEchartsModule }, { kind: "directive", type: i2$1.NgxEchartsDirective, selector: "echarts, [echarts]", inputs: ["options", "theme", "initOpts", "merge", "autoResize", "loading", "loadingType", "loadingOpts"], outputs: ["chartInit", "optionsError", "chartClick", "chartDblClick", "chartMouseDown", "chartMouseMove", "chartMouseUp", "chartMouseOver", "chartMouseOut", "chartGlobalOut", "chartContextMenu", "chartHighlight", "chartDownplay", "chartSelectChanged", "chartLegendSelectChanged", "chartLegendSelected", "chartLegendUnselected", "chartLegendLegendSelectAll", "chartLegendLegendInverseSelect", "chartLegendScroll", "chartDataZoom", "chartDataRangeSelected", "chartGraphRoam", "chartGeoRoam", "chartTreeRoam", "chartTimelineChanged", "chartTimelinePlayChanged", "chartRestore", "chartDataViewChanged", "chartMagicTypeChanged", "chartGeoSelectChanged", "chartGeoSelected", "chartGeoUnselected", "chartAxisAreaSelected", "chartBrush", "chartBrushEnd", "chartBrushSelected", "chartGlobalCursorTaken", "chartRendered", "chartFinished"], exportAs: ["echarts"] }, { kind: "ngmodule", type: TooltipModule }, { kind: "ngmodule", type: PopoverModule }, { kind: "component", type: ChartAlertsComponent, selector: "c8y-chart-alerts", inputs: ["alerts"] }, { kind: "ngmodule", type: AlarmsModule }, { kind: "component", type: SelectAggregatedDatapointComponent, selector: "c8y-select-aggregated-datapoint", inputs: ["activeDatapoints", "value"], outputs: ["valueChange"] }, { kind: "pipe", type: i1$2.AsyncPipe, name: "async" }, { kind: "pipe", type: i1$1.C8yTranslatePipe, name: "translate" }] }); }
3057
3727
  }
3058
3728
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: ChartsComponent, decorators: [{
3059
3729
  type: Component,
@@ -3067,7 +3737,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImpo
3067
3737
  EchartsOptionsService,
3068
3738
  YAxisService,
3069
3739
  AlarmSeverityToIconPipe,
3070
- AlarmSeverityToLabelPipe
3740
+ AlarmSeverityToLabelPipe,
3741
+ ChartHelpersService
3071
3742
  ], standalone: true, imports: [
3072
3743
  CommonModule$1,
3073
3744
  CoreModule,
@@ -3102,6 +3773,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImpo
3102
3773
  type: Output
3103
3774
  }], updateAggregatedSliderDatapoint: [{
3104
3775
  type: Output
3776
+ }], duplicateAggregationsDetected: [{
3777
+ type: Output
3105
3778
  }], chart: [{
3106
3779
  type: ViewChild,
3107
3780
  args: ['chart']