@c8y/ngx-components 1024.19.10 → 1024.20.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/fesm2022/c8y-ngx-components-alarm-event-selector.mjs +2 -2
- package/fesm2022/c8y-ngx-components-alarm-event-selector.mjs.map +1 -1
- package/fesm2022/c8y-ngx-components-context-dashboard.mjs +6 -4
- package/fesm2022/c8y-ngx-components-context-dashboard.mjs.map +1 -1
- package/fesm2022/c8y-ngx-components-echart-models.mjs.map +1 -1
- package/fesm2022/c8y-ngx-components-echart.mjs +529 -323
- package/fesm2022/c8y-ngx-components-echart.mjs.map +1 -1
- package/fesm2022/c8y-ngx-components-widgets-implementations-datapoints-graph.mjs +13 -5
- package/fesm2022/c8y-ngx-components-widgets-implementations-datapoints-graph.mjs.map +1 -1
- package/fesm2022/c8y-ngx-components-widgets-implementations-info-gauge.mjs +13 -11
- package/fesm2022/c8y-ngx-components-widgets-implementations-info-gauge.mjs.map +1 -1
- package/fesm2022/c8y-ngx-components-widgets-implementations-linear-gauge.mjs +1 -1
- package/fesm2022/c8y-ngx-components-widgets-implementations-linear-gauge.mjs.map +1 -1
- package/fesm2022/c8y-ngx-components.mjs +10 -8
- package/fesm2022/c8y-ngx-components.mjs.map +1 -1
- package/locales/de.po +6 -0
- package/locales/es.po +6 -0
- package/locales/fr.po +6 -0
- package/locales/ja_JP.po +6 -0
- package/locales/ko.po +6 -0
- package/locales/locales.pot +6 -0
- package/locales/nl.po +6 -0
- package/locales/pl.po +6 -0
- package/locales/pt_BR.po +6 -0
- package/locales/zh_CN.po +6 -0
- package/locales/zh_TW.po +6 -0
- package/package.json +1 -1
- package/types/c8y-ngx-components-context-dashboard.d.ts.map +1 -1
- package/types/c8y-ngx-components-echart-models.d.ts +2 -0
- package/types/c8y-ngx-components-echart-models.d.ts.map +1 -1
- package/types/c8y-ngx-components-echart.d.ts +70 -1
- package/types/c8y-ngx-components-echart.d.ts.map +1 -1
- package/types/c8y-ngx-components-widgets-implementations-datapoints-graph.d.ts.map +1 -1
- package/types/c8y-ngx-components.d.ts +1 -1
- package/types/c8y-ngx-components.d.ts.map +1 -1
|
@@ -11,7 +11,7 @@ import * as echarts from 'echarts';
|
|
|
11
11
|
import * as i1 from '@ngx-translate/core';
|
|
12
12
|
import { TranslateService } from '@ngx-translate/core';
|
|
13
13
|
import * as i1$3 from '@c8y/client';
|
|
14
|
-
import {
|
|
14
|
+
import { aggregationType, AlarmStatus, MeasurementService } from '@c8y/client';
|
|
15
15
|
import * as i6 from '@angular/router';
|
|
16
16
|
import * as i4 from '@c8y/ngx-components/alarms';
|
|
17
17
|
import { AlarmsModule, AlarmSeverityToIconPipe, AlarmSeverityToLabelPipe } from '@c8y/ngx-components/alarms';
|
|
@@ -482,10 +482,252 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImpo
|
|
|
482
482
|
type: Injectable
|
|
483
483
|
}] });
|
|
484
484
|
|
|
485
|
+
class ChartHelpersService {
|
|
486
|
+
constructor() {
|
|
487
|
+
/** Vertical pixel distance between adjacent alarm/event rows in the carrier render path. */
|
|
488
|
+
this.ALARM_EVENT_ROW_STEP = 28;
|
|
489
|
+
/** Radius in pixels of an alarm/event mark (a 24 px circle), used to keep rows clear of the grid edges. */
|
|
490
|
+
this.ALARM_EVENT_MARK_RADIUS = 12;
|
|
491
|
+
/** Clearance in pixels kept at each grid edge when only the chart height (not the grid) can be measured. */
|
|
492
|
+
this.ALARM_EVENT_ROW_EDGE_PADDING = 12;
|
|
493
|
+
/** Row span assumed before the first layout, when neither the grid nor the chart height is measurable yet. */
|
|
494
|
+
this.ALARM_EVENT_ROW_FALLBACK_SPAN = 200;
|
|
495
|
+
/** Top data value of the fixed [0, 2] carrier y-axis, used to measure the usable row span in pixels. */
|
|
496
|
+
this.ALARM_EVENT_CARRIER_AXIS_MAX = 2;
|
|
497
|
+
}
|
|
498
|
+
/**
|
|
499
|
+
* Places each alarm/event type on its own row in the carrier render path by applying a per-type
|
|
500
|
+
* vertical `symbolOffset` to its markPoints. Without a datapoint line to anchor them the marks all
|
|
501
|
+
* resolve to the single carrier y-coordinate, so two types raised seconds apart would otherwise be
|
|
502
|
+
* drawn on top of each other. Rows are centered around the carrier line and the step is capped so
|
|
503
|
+
* they all fit inside the plot area at the current height.
|
|
504
|
+
* @param series - The carrier series to mutate in place.
|
|
505
|
+
* @param echartsInstance - The chart instance, used to measure the available row span.
|
|
506
|
+
*/
|
|
507
|
+
assignAlarmEventRowOffsets(series, echartsInstance) {
|
|
508
|
+
const markPointSeries = series.filter(s => s.markPoint);
|
|
509
|
+
// Distinct types in stable order — one row per type, shared by all devices of that type.
|
|
510
|
+
const types = [];
|
|
511
|
+
for (const s of markPointSeries) {
|
|
512
|
+
const [type] = s.id.split('/');
|
|
513
|
+
if (!types.includes(type)) {
|
|
514
|
+
types.push(type);
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
const centerIndex = (types.length - 1) / 2;
|
|
518
|
+
const rowStep = this.getAlarmEventRowStep(types.length, echartsInstance);
|
|
519
|
+
markPointSeries.forEach(s => {
|
|
520
|
+
const withMarkPoint = s;
|
|
521
|
+
const data = withMarkPoint.markPoint?.data;
|
|
522
|
+
if (!data) {
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
const [type] = s.id.split('/');
|
|
526
|
+
const offsetY = (types.indexOf(type) - centerIndex) * rowStep;
|
|
527
|
+
withMarkPoint.markPoint.data = data.map(point => ({
|
|
528
|
+
...point,
|
|
529
|
+
symbolOffset: [0, offsetY]
|
|
530
|
+
}));
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
/**
|
|
534
|
+
* Reapplies the per-type row offsets to the current carrier series and pushes only the markPoint
|
|
535
|
+
* data back to the chart. Called on resize so the rows are re-spaced for the new height instead of
|
|
536
|
+
* keeping the spacing computed at build time.
|
|
537
|
+
* @param echartsInstance - The chart instance.
|
|
538
|
+
* @param isAlarmEventOnlyChart - Whether the alarms/events-only (carrier) chart is currently shown;
|
|
539
|
+
* the datapoint path also has markPoint series and must never be touched.
|
|
540
|
+
*/
|
|
541
|
+
reapplyAlarmEventRowOffsets(echartsInstance, isAlarmEventOnlyChart) {
|
|
542
|
+
if (!isAlarmEventOnlyChart || !echartsInstance) {
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
const series = (echartsInstance.getOption()?.['series'] ?? []);
|
|
546
|
+
const markPointSeries = series.filter(s => s.markPoint);
|
|
547
|
+
if (!markPointSeries.length) {
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
this.assignAlarmEventRowOffsets(markPointSeries, echartsInstance);
|
|
551
|
+
echartsInstance.setOption({
|
|
552
|
+
series: markPointSeries.map(s => ({
|
|
553
|
+
id: s.id,
|
|
554
|
+
markPoint: { data: s.markPoint.data }
|
|
555
|
+
}))
|
|
556
|
+
});
|
|
557
|
+
}
|
|
558
|
+
/**
|
|
559
|
+
* Cleans the widget configuration which will be sent to Gainsight by removing unnecessary properties.
|
|
560
|
+
* @param config The widget configuration to clean.
|
|
561
|
+
* @returns The cleaned widget configuration.
|
|
562
|
+
*/
|
|
563
|
+
getConfigSummaryForGainsight(config) {
|
|
564
|
+
return {
|
|
565
|
+
datapointCount: config.datapoints?.length ?? 0,
|
|
566
|
+
alarmsEventsCount: config.alarmsEventsConfigs?.length ?? 0,
|
|
567
|
+
interval: config.interval,
|
|
568
|
+
forceMergeDatapoints: config.forceMergeDatapoints,
|
|
569
|
+
mergeMatchingDatapoints: config.mergeMatchingDatapoints,
|
|
570
|
+
showSlider: config.showSlider,
|
|
571
|
+
dateFrom: config.dateFrom,
|
|
572
|
+
dateTo: config.dateTo
|
|
573
|
+
};
|
|
574
|
+
}
|
|
575
|
+
findMatchingDatapoint(datapoints, selected) {
|
|
576
|
+
if (!selected)
|
|
577
|
+
return undefined;
|
|
578
|
+
return datapoints.find(dp => this.getTargetId(dp) === this.getTargetId(selected) &&
|
|
579
|
+
dp.fragment === selected.fragment &&
|
|
580
|
+
dp.series === selected.series);
|
|
581
|
+
}
|
|
582
|
+
/**
|
|
583
|
+
* Maps a computed auto-aggregation interval string (e.g. `'5m'`, `'3h'`, `'7d'`) to
|
|
584
|
+
* the nearest standard `aggregationType` for non-time-series tenants.
|
|
585
|
+
* - seconds / minutes → MINUTELY (finest supported granularity)
|
|
586
|
+
* - hours → HOURLY
|
|
587
|
+
* - days → DAILY
|
|
588
|
+
*/
|
|
589
|
+
mapIntervalToAggregationType(interval) {
|
|
590
|
+
if (interval.endsWith('d'))
|
|
591
|
+
return aggregationType.DAILY;
|
|
592
|
+
if (interval.endsWith('h'))
|
|
593
|
+
return aggregationType.HOURLY;
|
|
594
|
+
if (interval.endsWith('m') || interval.endsWith('s'))
|
|
595
|
+
return aggregationType.MINUTELY;
|
|
596
|
+
return null;
|
|
597
|
+
}
|
|
598
|
+
/**
|
|
599
|
+
* Computes the aggregation interval string (e.g. `30s`, `5m`, `24h`, `7d`) that
|
|
600
|
+
* targets approximately `desiredDatapoints` buckets within the given time range.
|
|
601
|
+
* The integer part is capped at 999 to satisfy the API constraint.
|
|
602
|
+
*/
|
|
603
|
+
calculateAutoAggregationInterval(dateFrom, dateTo, desiredDatapoints) {
|
|
604
|
+
const rangeMs = new Date(dateTo).valueOf() - new Date(dateFrom).valueOf();
|
|
605
|
+
const intervalMs = rangeMs / Math.max(1, desiredDatapoints);
|
|
606
|
+
// If each bucket would be sub-second, the zoom is fine enough to show raw data
|
|
607
|
+
if (intervalMs <= 1_000)
|
|
608
|
+
return null;
|
|
609
|
+
// Use the most natural human-readable unit.
|
|
610
|
+
// e.g. 24h / 300 dp → 288 s → 5 m (not 288s)
|
|
611
|
+
const totalSeconds = Math.max(1, Math.round(intervalMs / 1_000));
|
|
612
|
+
if (totalSeconds < 60)
|
|
613
|
+
return `${Math.min(999, totalSeconds)}s`;
|
|
614
|
+
const totalMinutes = Math.round(totalSeconds / 60);
|
|
615
|
+
if (totalMinutes < 60)
|
|
616
|
+
return `${Math.min(999, totalMinutes)}m`;
|
|
617
|
+
const totalHours = Math.round(totalSeconds / 3_600);
|
|
618
|
+
if (totalHours < 24)
|
|
619
|
+
return `${Math.min(999, totalHours)}h`;
|
|
620
|
+
const totalDays = Math.max(1, Math.round(totalSeconds / 86_400));
|
|
621
|
+
return `${Math.min(999, totalDays)}d`;
|
|
622
|
+
}
|
|
623
|
+
/**
|
|
624
|
+
* Scans raw DpWithValues to determine which datapoints should be collapsed to a single
|
|
625
|
+
* series — either because all per-function values are identical at every timestamp (the
|
|
626
|
+
* aggregation interval is too fine — each interval holds a single measurement — to
|
|
627
|
+
* differentiate functions).
|
|
628
|
+
*/
|
|
629
|
+
detectCollapsedDatapoints(dps) {
|
|
630
|
+
const collapsed = new Set();
|
|
631
|
+
for (const dp of dps) {
|
|
632
|
+
const dpId = `${dp.__target?.id}${dp.fragment}${dp.series}`;
|
|
633
|
+
const valueEntries = Object.values(dp.values ?? {});
|
|
634
|
+
if (!valueEntries.length)
|
|
635
|
+
continue;
|
|
636
|
+
const renderTypeConfigs = dp.renderTypeConfigs;
|
|
637
|
+
const renderAsBand = !!dp.renderAsBand;
|
|
638
|
+
let fnsToCompare = [];
|
|
639
|
+
let isBand = false;
|
|
640
|
+
if (renderTypeConfigs) {
|
|
641
|
+
const enabled = Object.keys(renderTypeConfigs).filter(k => renderTypeConfigs[k]?.enabled);
|
|
642
|
+
const hasMinMax = enabled.includes('min') && enabled.includes('max');
|
|
643
|
+
if (renderAsBand && hasMinMax) {
|
|
644
|
+
isBand = true;
|
|
645
|
+
}
|
|
646
|
+
else if (enabled.length >= 2) {
|
|
647
|
+
fnsToCompare = enabled;
|
|
648
|
+
}
|
|
649
|
+
else {
|
|
650
|
+
continue;
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
else if (dp.renderTypes && dp.renderTypes.length > 1) {
|
|
654
|
+
const fns = dp.renderTypes.filter(rt => rt !== 'area');
|
|
655
|
+
if (fns.length >= 2) {
|
|
656
|
+
fnsToCompare = fns;
|
|
657
|
+
}
|
|
658
|
+
else {
|
|
659
|
+
continue;
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
else {
|
|
663
|
+
continue;
|
|
664
|
+
}
|
|
665
|
+
if (isBand) {
|
|
666
|
+
const allSame = valueEntries.every(vals => vals[0]?.min === vals[0]?.max);
|
|
667
|
+
if (allSame)
|
|
668
|
+
collapsed.add(dpId);
|
|
669
|
+
}
|
|
670
|
+
else {
|
|
671
|
+
const allSame = valueEntries.every(vals => {
|
|
672
|
+
const first = vals[0]?.[fnsToCompare[0]];
|
|
673
|
+
if (first === undefined || first === null)
|
|
674
|
+
return false;
|
|
675
|
+
return fnsToCompare.slice(1).every(fn => vals[0]?.[fn] === first);
|
|
676
|
+
});
|
|
677
|
+
if (allSame)
|
|
678
|
+
collapsed.add(dpId);
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
return collapsed;
|
|
682
|
+
}
|
|
683
|
+
getTargetId(obj) {
|
|
684
|
+
return obj?.__target?.id ?? obj?.target;
|
|
685
|
+
}
|
|
686
|
+
/**
|
|
687
|
+
* Vertical distance between adjacent alarm/event rows, capped so all rows fit inside the plot area.
|
|
688
|
+
* Uses the default step until the rows would overflow, then compresses to the measured span.
|
|
689
|
+
* @param rowCount - The number of type rows to lay out.
|
|
690
|
+
* @param echartsInstance - The chart instance, used to measure the available span.
|
|
691
|
+
* @returns The per-row step in pixels.
|
|
692
|
+
*/
|
|
693
|
+
getAlarmEventRowStep(rowCount, echartsInstance) {
|
|
694
|
+
if (rowCount < 2) {
|
|
695
|
+
return this.ALARM_EVENT_ROW_STEP;
|
|
696
|
+
}
|
|
697
|
+
return Math.min(this.ALARM_EVENT_ROW_STEP, Math.max(this.getAlarmEventRowSpan(echartsInstance), 0) / (rowCount - 1));
|
|
698
|
+
}
|
|
699
|
+
/**
|
|
700
|
+
* Pixel height available for the centered rows, measured from the plot grid (the marks live in the
|
|
701
|
+
* grid, which is shorter than the chart by the axis-label band) minus one mark diameter of
|
|
702
|
+
* clearance. Falls back to the chart height, then a fixed span, before the first layout exists.
|
|
703
|
+
* @param echartsInstance - The chart instance.
|
|
704
|
+
* @returns The usable vertical span in pixels.
|
|
705
|
+
*/
|
|
706
|
+
getAlarmEventRowSpan(echartsInstance) {
|
|
707
|
+
if (typeof echartsInstance?.convertToPixel === 'function') {
|
|
708
|
+
const top = echartsInstance.convertToPixel({ yAxisIndex: 0 }, this.ALARM_EVENT_CARRIER_AXIS_MAX);
|
|
709
|
+
const bottom = echartsInstance.convertToPixel({ yAxisIndex: 0 }, 0);
|
|
710
|
+
if (Number.isFinite(top) && Number.isFinite(bottom) && bottom > top) {
|
|
711
|
+
return bottom - top - this.ALARM_EVENT_MARK_RADIUS * 2;
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
// Before the first layout there is no grid to measure, so fall back to the chart's own height.
|
|
715
|
+
const chartHeight = typeof echartsInstance?.getHeight === 'function' ? echartsInstance.getHeight() : 0;
|
|
716
|
+
return chartHeight > 0
|
|
717
|
+
? chartHeight - this.ALARM_EVENT_ROW_EDGE_PADDING * 2
|
|
718
|
+
: this.ALARM_EVENT_ROW_FALLBACK_SPAN;
|
|
719
|
+
}
|
|
720
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: ChartHelpersService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
721
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: ChartHelpersService }); }
|
|
722
|
+
}
|
|
723
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: ChartHelpersService, decorators: [{
|
|
724
|
+
type: Injectable
|
|
725
|
+
}] });
|
|
726
|
+
|
|
485
727
|
const INDEX_HTML = '/index.html';
|
|
486
728
|
const DEFAULT_DATAPOINT_COLOR = '#1776bf';
|
|
487
729
|
class EchartsOptionsService {
|
|
488
|
-
constructor(datePipe, yAxisService, chartTypesService, severityIconPipe, severityLabelPipe, translate, router) {
|
|
730
|
+
constructor(datePipe, yAxisService, chartTypesService, severityIconPipe, severityLabelPipe, translate, router, chartHelpersService) {
|
|
489
731
|
this.datePipe = datePipe;
|
|
490
732
|
this.yAxisService = yAxisService;
|
|
491
733
|
this.chartTypesService = chartTypesService;
|
|
@@ -493,11 +735,25 @@ class EchartsOptionsService {
|
|
|
493
735
|
this.severityLabelPipe = severityLabelPipe;
|
|
494
736
|
this.translate = translate;
|
|
495
737
|
this.router = router;
|
|
738
|
+
this.chartHelpersService = chartHelpersService;
|
|
496
739
|
this.highlightedSeriesName = null;
|
|
497
740
|
this.AGGREGATED_SERIES_TYPE = 'aggr';
|
|
498
741
|
this.TOOLTIP_WIDTH = 300;
|
|
499
742
|
this.COMPACT_TOOLTIP_WIDTH = 360;
|
|
500
743
|
this.COMPACT_TOOLTIP_THRESHOLD = 5;
|
|
744
|
+
/**
|
|
745
|
+
* Fixed y-coordinate the alarm/event marks are anchored to when no datapoint line exists to
|
|
746
|
+
* host them (the "carrier" render path). It sits in the middle of the hidden [0, 2] y-axis.
|
|
747
|
+
*/
|
|
748
|
+
this.ALARM_EVENT_CARRIER_Y = 1;
|
|
749
|
+
/**
|
|
750
|
+
* Vertical pixel offset applied to event marks so that an event and an alarm occurring at the
|
|
751
|
+
* same time no longer paint on top of each other. Alarms stay on the anchor line, events float
|
|
752
|
+
* just above it. Applied in both render paths (with and without a datapoint line).
|
|
753
|
+
*/
|
|
754
|
+
this.EVENT_MARK_VERTICAL_OFFSET = -18;
|
|
755
|
+
/** True while the chart shows only alarms/events (carrier path); gates the resize-driven row reflow. */
|
|
756
|
+
this.isAlarmEventOnlyChart = false;
|
|
501
757
|
}
|
|
502
758
|
getChartOptions(datapointsWithValues, timeRange, showSplitLines, events, alarms, displayOptions, selectedTimeRange, aggregatedDatapoint, sliderZoomUsed = false) {
|
|
503
759
|
displayOptions = {
|
|
@@ -526,23 +782,9 @@ class EchartsOptionsService {
|
|
|
526
782
|
if (sliderZoomUsed) {
|
|
527
783
|
intervalInMs = this.calculateExtendedIntervalInMs(timeRange.interval || 'hours', timeRange);
|
|
528
784
|
}
|
|
785
|
+
this.isAlarmEventOnlyChart = !!displayOptions.showOnlyAlarmsOrEvents;
|
|
529
786
|
if (displayOptions.showOnlyAlarmsOrEvents) {
|
|
530
|
-
return
|
|
531
|
-
xAxis: {
|
|
532
|
-
min: timeRange.dateFrom,
|
|
533
|
-
max: timeRange.dateTo,
|
|
534
|
-
type: 'time'
|
|
535
|
-
},
|
|
536
|
-
yAxis: {
|
|
537
|
-
show: false
|
|
538
|
-
},
|
|
539
|
-
tooltip: {
|
|
540
|
-
trigger: 'axis',
|
|
541
|
-
// intended empty for now. Later we can add a tooltip for more info on the hovered alarm/event
|
|
542
|
-
formatter: () => ''
|
|
543
|
-
},
|
|
544
|
-
series: this.createAlarmEventTypeSeries(alarms, events)
|
|
545
|
-
};
|
|
787
|
+
return this.getAlarmEventOnlyChartOptions(alarms, events, timeRange, displayOptions);
|
|
546
788
|
}
|
|
547
789
|
return {
|
|
548
790
|
grid: {
|
|
@@ -889,141 +1131,138 @@ class EchartsOptionsService {
|
|
|
889
1131
|
series['typeOfSeries'] !== 'event' &&
|
|
890
1132
|
legendSelected?.[series['name']] !== false);
|
|
891
1133
|
this.processSeries(allDataPointSeries, XAxisValue, YAxisReadings, displayOptions);
|
|
892
|
-
|
|
893
|
-
const
|
|
894
|
-
const
|
|
895
|
-
|
|
1134
|
+
const { itemType, value: markValue, coord } = params.data;
|
|
1135
|
+
const markTimestamp = markValue ?? coord?.[0];
|
|
1136
|
+
const event = allEvents.find(e => e.type === itemType && markTimestamp === e.time) ??
|
|
1137
|
+
allEvents.find(e => e.type === itemType);
|
|
1138
|
+
const alarm = allAlarms.find(a => a.type === itemType && markTimestamp === a.lastUpdated) ??
|
|
1139
|
+
allAlarms.find(a => a.type === itemType && markTimestamp === a.creationTime) ?? // cleared alarms
|
|
1140
|
+
allAlarms.find(a => a.type === itemType);
|
|
896
1141
|
let value = '';
|
|
897
1142
|
if (event) {
|
|
898
1143
|
value = this.processEvent(event);
|
|
899
1144
|
}
|
|
900
|
-
if (alarm) {
|
|
901
|
-
this.processAlarm(alarm)
|
|
902
|
-
value = alarmVal;
|
|
903
|
-
YAxisReadings.push(value);
|
|
904
|
-
const options = this.echartsInstance?.getOption();
|
|
905
|
-
if (!options.tooltip || !Array.isArray(options.tooltip)) {
|
|
906
|
-
return;
|
|
907
|
-
}
|
|
908
|
-
const updatedOptions = {
|
|
909
|
-
tooltip: options['tooltip'][0]
|
|
910
|
-
};
|
|
911
|
-
if (!updatedOptions.tooltip) {
|
|
912
|
-
return;
|
|
913
|
-
}
|
|
914
|
-
updatedOptions.tooltip.formatter = `<div style="width: ${this.TOOLTIP_WIDTH}px">${YAxisReadings.join('')}</div>`;
|
|
915
|
-
updatedOptions.tooltip.transitionDuration = 0;
|
|
916
|
-
updatedOptions.tooltip.position = this.tooltipPosition();
|
|
917
|
-
this.echartsInstance?.setOption(updatedOptions);
|
|
918
|
-
return;
|
|
919
|
-
});
|
|
1145
|
+
else if (alarm) {
|
|
1146
|
+
value = this.processAlarm(alarm);
|
|
920
1147
|
}
|
|
921
1148
|
YAxisReadings.push(value);
|
|
922
|
-
return `<div style="width:
|
|
1149
|
+
return `<div style="width: ${this.TOOLTIP_WIDTH}px">${YAxisReadings.join('')}</div>`;
|
|
923
1150
|
}
|
|
924
1151
|
/**
|
|
925
|
-
*
|
|
926
|
-
* @param alarms - The list of alarms.
|
|
927
|
-
* @param events - The list of events.
|
|
928
|
-
* @
|
|
1152
|
+
* Builds alarm/event markLine + markPoint series when there is no datapoint line to anchor them.
|
|
1153
|
+
* @param alarms - The list of alarms to render.
|
|
1154
|
+
* @param events - The list of events to render.
|
|
1155
|
+
* @param timeRange - The visible time range (used to span the flat carrier values).
|
|
1156
|
+
* @param displayOptions - The display options forwarded to `getAlarmOrEventSeries`.
|
|
1157
|
+
* @returns The markLine/markPoint series for the alarms/events-only view.
|
|
929
1158
|
*/
|
|
930
|
-
|
|
1159
|
+
getAlarmEventCarrierSeries(alarms, events, timeRange, displayOptions) {
|
|
1160
|
+
const deviceIds = Array.from(new Set([...alarms, ...events].map(item => item.source?.id).filter(Boolean)));
|
|
1161
|
+
const flatValues = {
|
|
1162
|
+
[new Date(timeRange.dateFrom).toISOString()]: [
|
|
1163
|
+
{ min: this.ALARM_EVENT_CARRIER_Y, max: this.ALARM_EVENT_CARRIER_Y }
|
|
1164
|
+
],
|
|
1165
|
+
[new Date(timeRange.dateTo).toISOString()]: [
|
|
1166
|
+
{ min: this.ALARM_EVENT_CARRIER_Y, max: this.ALARM_EVENT_CARRIER_Y }
|
|
1167
|
+
]
|
|
1168
|
+
};
|
|
931
1169
|
const series = [];
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
1170
|
+
for (const deviceId of deviceIds) {
|
|
1171
|
+
const carrierDp = {
|
|
1172
|
+
__target: { id: deviceId },
|
|
1173
|
+
values: flatValues,
|
|
1174
|
+
renderType: 'avg',
|
|
1175
|
+
lineType: 'line',
|
|
1176
|
+
color: 'transparent'
|
|
1177
|
+
};
|
|
1178
|
+
const deviceEvents = events.filter(e => e.source?.id === deviceId);
|
|
1179
|
+
const deviceAlarms = alarms.filter(a => a.source?.id === deviceId);
|
|
1180
|
+
series.push(...this.getAlarmOrEventSeries(carrierDp, 'avg', false, deviceEvents, 'event', displayOptions, null, 0), ...this.getAlarmOrEventSeries(carrierDp, 'avg', false, deviceAlarms, 'alarm', displayOptions, null, 0));
|
|
1181
|
+
}
|
|
1182
|
+
// markLines are full-height vertical lines keyed only by x, so copies for the same type across
|
|
1183
|
+
// devices are redundant; keep one per type (mirrors the dedup in getChartSeries).
|
|
1184
|
+
const seenMarkLineTypes = new Set();
|
|
1185
|
+
const deduped = series.filter(s => {
|
|
1186
|
+
const id = s.id;
|
|
1187
|
+
if (!id?.endsWith('-markLine')) {
|
|
1188
|
+
return true;
|
|
1189
|
+
}
|
|
1190
|
+
const isFirst = !seenMarkLineTypes.has(id);
|
|
1191
|
+
seenMarkLineTypes.add(id);
|
|
1192
|
+
return isFirst;
|
|
1193
|
+
});
|
|
1194
|
+
this.chartHelpersService.assignAlarmEventRowOffsets(deduped, this.echartsInstance);
|
|
1195
|
+
deduped.forEach(s => {
|
|
1196
|
+
const withMarks = s;
|
|
1197
|
+
if (withMarks.markLine) {
|
|
1198
|
+
withMarks.markLine.label = { show: false };
|
|
1199
|
+
withMarks.markLine.emphasis = { label: { show: false } };
|
|
1200
|
+
}
|
|
1201
|
+
if (withMarks.markPoint) {
|
|
1202
|
+
withMarks.markPoint.label = { show: false };
|
|
1203
|
+
withMarks.markPoint.emphasis = { label: { show: false } };
|
|
1204
|
+
}
|
|
1205
|
+
});
|
|
1206
|
+
return deduped;
|
|
1207
|
+
}
|
|
1208
|
+
/**
|
|
1209
|
+
* Exposes the default markLine data (creation time, plus lastUpdated when it differs) for a set of
|
|
1210
|
+
* alarms or events.
|
|
1211
|
+
* @param items - The alarms or events to build lines for.
|
|
1212
|
+
* @param itemType - Whether the items are alarms or events.
|
|
1213
|
+
* @returns The default markLine data.
|
|
1214
|
+
*/
|
|
1215
|
+
getDefaultMarkLineData(items, itemType) {
|
|
1216
|
+
return this.createMarkLine(items, itemType);
|
|
1217
|
+
}
|
|
1218
|
+
/**
|
|
1219
|
+
* Builds the full chart options for the "alarms/events only" case (no active datapoint).
|
|
1220
|
+
* @param alarms - The list of alarms to render.
|
|
1221
|
+
* @param events - The list of events to render.
|
|
1222
|
+
* @param timeRange - The visible time range.
|
|
1223
|
+
* @param displayOptions - The display options (marked line/point flags, axis color, …).
|
|
1224
|
+
* @returns The chart options for the alarms/events-only view.
|
|
1225
|
+
*/
|
|
1226
|
+
getAlarmEventOnlyChartOptions(alarms, events, timeRange, displayOptions) {
|
|
1227
|
+
return {
|
|
1228
|
+
grid: { containLabel: true, left: 32, top: 16, right: 32, bottom: 8 },
|
|
1229
|
+
dataZoom: [
|
|
1230
|
+
{
|
|
1231
|
+
type: 'inside',
|
|
1232
|
+
filterMode: 'none',
|
|
1233
|
+
zoomOnMouseWheel: true,
|
|
1234
|
+
startValue: new Date(timeRange.dateFrom).valueOf(),
|
|
1235
|
+
endValue: new Date(timeRange.dateTo).valueOf()
|
|
993
1236
|
}
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
});
|
|
1024
|
-
yValue += 10;
|
|
1025
|
-
}
|
|
1026
|
-
return series;
|
|
1237
|
+
],
|
|
1238
|
+
animation: false,
|
|
1239
|
+
tooltip: {
|
|
1240
|
+
trigger: 'axis',
|
|
1241
|
+
formatter: () => '',
|
|
1242
|
+
backgroundColor: 'var(--c8y-root-component-background-default)',
|
|
1243
|
+
borderColor: 'var(--c8y-root-component-border-color)',
|
|
1244
|
+
padding: 0,
|
|
1245
|
+
appendToBody: true,
|
|
1246
|
+
position: this.tooltipPosition(),
|
|
1247
|
+
transitionDuration: 0
|
|
1248
|
+
},
|
|
1249
|
+
legend: { show: false },
|
|
1250
|
+
xAxis: {
|
|
1251
|
+
min: new Date(timeRange.dateFrom).valueOf(),
|
|
1252
|
+
max: new Date(timeRange.dateTo).valueOf(),
|
|
1253
|
+
type: 'time',
|
|
1254
|
+
axisLine: { lineStyle: { color: displayOptions.axisColor } },
|
|
1255
|
+
axisLabel: { hideOverlap: true, color: displayOptions.axisColor }
|
|
1256
|
+
},
|
|
1257
|
+
// Hidden fixed-range axis so the carrier marks always render at a stable vertical position.
|
|
1258
|
+
yAxis: {
|
|
1259
|
+
show: false,
|
|
1260
|
+
type: 'value',
|
|
1261
|
+
min: 0,
|
|
1262
|
+
max: 2
|
|
1263
|
+
},
|
|
1264
|
+
series: this.getAlarmEventCarrierSeries(alarms, events, timeRange, displayOptions)
|
|
1265
|
+
};
|
|
1027
1266
|
}
|
|
1028
1267
|
tooltipPosition() {
|
|
1029
1268
|
let lastPositionOfTooltip = {};
|
|
@@ -1179,14 +1418,14 @@ class EchartsOptionsService {
|
|
|
1179
1418
|
* @param alarm - The alarm object.
|
|
1180
1419
|
* @returns The processed value.
|
|
1181
1420
|
*/
|
|
1182
|
-
|
|
1421
|
+
processAlarm(alarm) {
|
|
1183
1422
|
let value = `<ul class="list-unstyled small separator-top text-default m-0 p-l-8 p-r-8">`;
|
|
1184
1423
|
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>`;
|
|
1185
1424
|
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>`;
|
|
1186
1425
|
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>`;
|
|
1187
1426
|
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('Message')))}</label><span class="small m-l-auto" style="display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 5; overflow: hidden; text-overflow: ellipsis; word-break: break-word; white-space: normal; max-width: 200px;" title="${echarts.format.encodeHTML(this.translate.instant(alarm.text))}">${echarts.format.encodeHTML(this.translate.instant(alarm.text))}</span></li>`;
|
|
1188
1427
|
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('Last updated')))}</label><span class="small m-l-auto">${echarts.format.encodeHTML(this.datePipe.transform(alarm['lastUpdated']))}</span></li>`;
|
|
1189
|
-
const exists =
|
|
1428
|
+
const exists = this.alarmRouteExists();
|
|
1190
1429
|
if (exists) {
|
|
1191
1430
|
const currentUrl = window.location.href;
|
|
1192
1431
|
const baseUrlIndex = currentUrl.indexOf(INDEX_HTML);
|
|
@@ -1197,11 +1436,8 @@ class EchartsOptionsService {
|
|
|
1197
1436
|
value += `</ul>`;
|
|
1198
1437
|
return value;
|
|
1199
1438
|
}
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
return `${route.path}` === 'alarms';
|
|
1203
|
-
});
|
|
1204
|
-
return exists;
|
|
1439
|
+
alarmRouteExists() {
|
|
1440
|
+
return this.router.config.some(route => `${route.path}` === 'alarms');
|
|
1205
1441
|
}
|
|
1206
1442
|
getChartSeries(datapointsWithValues, events, alarms, displayOptions) {
|
|
1207
1443
|
const series = [];
|
|
@@ -1485,6 +1721,8 @@ class EchartsOptionsService {
|
|
|
1485
1721
|
];
|
|
1486
1722
|
}
|
|
1487
1723
|
if (isEvent) {
|
|
1724
|
+
// Offset events vertically so an event and an alarm at the same time don't obscure each other.
|
|
1725
|
+
const symbolOffset = [0, this.EVENT_MARK_VERTICAL_OFFSET];
|
|
1488
1726
|
return [
|
|
1489
1727
|
{
|
|
1490
1728
|
coord: coordCreationTime,
|
|
@@ -1494,7 +1732,8 @@ class EchartsOptionsService {
|
|
|
1494
1732
|
color: item['color']
|
|
1495
1733
|
},
|
|
1496
1734
|
symbol: 'circle',
|
|
1497
|
-
symbolSize: 24
|
|
1735
|
+
symbolSize: 24,
|
|
1736
|
+
symbolOffset
|
|
1498
1737
|
},
|
|
1499
1738
|
{
|
|
1500
1739
|
coord: coordCreationTime,
|
|
@@ -1502,7 +1741,8 @@ class EchartsOptionsService {
|
|
|
1502
1741
|
itemType: item.type,
|
|
1503
1742
|
itemStyle: { color: 'white' },
|
|
1504
1743
|
symbol: ICONS_MAP.EVENT,
|
|
1505
|
-
symbolSize: 16
|
|
1744
|
+
symbolSize: 16,
|
|
1745
|
+
symbolOffset
|
|
1506
1746
|
}
|
|
1507
1747
|
];
|
|
1508
1748
|
}
|
|
@@ -2014,12 +2254,12 @@ class EchartsOptionsService {
|
|
|
2014
2254
|
return baseColor;
|
|
2015
2255
|
}
|
|
2016
2256
|
}
|
|
2017
|
-
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", 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 }); }
|
|
2257
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", 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 }, { token: ChartHelpersService }], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
2018
2258
|
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: EchartsOptionsService }); }
|
|
2019
2259
|
}
|
|
2020
2260
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: EchartsOptionsService, decorators: [{
|
|
2021
2261
|
type: Injectable
|
|
2022
|
-
}], ctorParameters: () => [{ type: i1$1.DatePipe }, { type: YAxisService }, { type: ChartTypesService }, { type: i4.AlarmSeverityToIconPipe }, { type: i4.AlarmSeverityToLabelPipe }, { type: i1.TranslateService }, { type: i6.Router }] });
|
|
2262
|
+
}], ctorParameters: () => [{ type: i1$1.DatePipe }, { type: YAxisService }, { type: ChartTypesService }, { type: i4.AlarmSeverityToIconPipe }, { type: i4.AlarmSeverityToLabelPipe }, { type: i1.TranslateService }, { type: i6.Router }, { type: ChartHelpersService }] });
|
|
2023
2263
|
|
|
2024
2264
|
class ChartRealtimeService {
|
|
2025
2265
|
constructor(measurementRealtime, alarmRealtimeService, eventRealtimeService, echartsOptionsService) {
|
|
@@ -2049,6 +2289,7 @@ class ChartRealtimeService {
|
|
|
2049
2289
|
if (displayOptions.showOnlyAlarmsOrEvents) {
|
|
2050
2290
|
this.currentAlarms = alarms ? [...alarms] : [];
|
|
2051
2291
|
this.currentEvents = events ? [...events] : [];
|
|
2292
|
+
this.currentDisplayOptions = displayOptions;
|
|
2052
2293
|
// Subscribe to realtime alarms/events
|
|
2053
2294
|
const allAlarmsAndEvents$ = from(uniqueAlarmOrEventTargets).pipe(mergeMap(targetId => {
|
|
2054
2295
|
const alarmsRealtime$ = this.alarmRealtimeService.onAll$(targetId);
|
|
@@ -2058,9 +2299,18 @@ class ChartRealtimeService {
|
|
|
2058
2299
|
this.realtimeSubscriptionAlarmsEvents = allAlarmsAndEvents$.subscribe(alarmOrEvent => {
|
|
2059
2300
|
if (!alarmOrEvent)
|
|
2060
2301
|
return;
|
|
2302
|
+
const matchingConfig = activeAlarmsOrEvents.find(aOrE => aOrE.filters.type === alarmOrEvent.type && aOrE.__target?.id === alarmOrEvent.source?.id);
|
|
2303
|
+
if (matchingConfig) {
|
|
2304
|
+
alarmOrEvent['color'] = matchingConfig.color;
|
|
2305
|
+
alarmOrEvent['selectedDatapoint'] = matchingConfig.selectedDatapoint;
|
|
2306
|
+
}
|
|
2061
2307
|
if ('severity' in alarmOrEvent) {
|
|
2062
2308
|
// Alarm
|
|
2063
|
-
|
|
2309
|
+
const existingIdx = this.currentAlarms.findIndex(a => a.id === alarmOrEvent.id);
|
|
2310
|
+
if (existingIdx !== -1) {
|
|
2311
|
+
this.currentAlarms[existingIdx] = alarmOrEvent;
|
|
2312
|
+
}
|
|
2313
|
+
else {
|
|
2064
2314
|
this.currentAlarms.push(alarmOrEvent);
|
|
2065
2315
|
}
|
|
2066
2316
|
}
|
|
@@ -2070,6 +2320,7 @@ class ChartRealtimeService {
|
|
|
2070
2320
|
this.currentEvents.push(alarmOrEvent);
|
|
2071
2321
|
}
|
|
2072
2322
|
}
|
|
2323
|
+
alarmOrEventReceivedCallback?.(alarmOrEvent);
|
|
2073
2324
|
this.updateAlarmsEventsChartRealtime();
|
|
2074
2325
|
});
|
|
2075
2326
|
const updateThrottleTime = this.getRealtimeUpdateThrottleTime({
|
|
@@ -2167,7 +2418,18 @@ class ChartRealtimeService {
|
|
|
2167
2418
|
* Updates the chart with the current alarms and events data. Only used when no active datapoints are selected.
|
|
2168
2419
|
*/
|
|
2169
2420
|
updateAlarmsEventsChartRealtime() {
|
|
2170
|
-
const series = this.echartsOptionsService.
|
|
2421
|
+
const series = this.echartsOptionsService.getAlarmEventCarrierSeries(this.currentAlarms, this.currentEvents, {
|
|
2422
|
+
dateFrom: (this.currentTimeRange?.dateFrom ?? new Date()).toISOString(),
|
|
2423
|
+
dateTo: (this.currentTimeRange?.dateTo ?? new Date()).toISOString()
|
|
2424
|
+
}, this.currentDisplayOptions ?? {
|
|
2425
|
+
displayMarkedLine: true,
|
|
2426
|
+
displayMarkedPoint: true,
|
|
2427
|
+
mergeMatchingDatapoints: false,
|
|
2428
|
+
forceMergeDatapoints: false,
|
|
2429
|
+
setYaxisStartToZero: false,
|
|
2430
|
+
showLabelAndUnit: false,
|
|
2431
|
+
showSlider: false
|
|
2432
|
+
});
|
|
2171
2433
|
this.echartsInstance?.setOption({
|
|
2172
2434
|
series,
|
|
2173
2435
|
xAxis: {
|
|
@@ -2496,142 +2758,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImpo
|
|
|
2496
2758
|
type: Injectable
|
|
2497
2759
|
}], ctorParameters: () => [{ type: i1$3.AlarmService }] });
|
|
2498
2760
|
|
|
2499
|
-
class ChartHelpersService {
|
|
2500
|
-
/**
|
|
2501
|
-
* Cleans the widget configuration which will be sent to Gainsight by removing unnecessary properties.
|
|
2502
|
-
* @param config The widget configuration to clean.
|
|
2503
|
-
* @returns The cleaned widget configuration.
|
|
2504
|
-
*/
|
|
2505
|
-
getConfigSummaryForGainsight(config) {
|
|
2506
|
-
return {
|
|
2507
|
-
datapointCount: config.datapoints?.length ?? 0,
|
|
2508
|
-
alarmsEventsCount: config.alarmsEventsConfigs?.length ?? 0,
|
|
2509
|
-
interval: config.interval,
|
|
2510
|
-
forceMergeDatapoints: config.forceMergeDatapoints,
|
|
2511
|
-
mergeMatchingDatapoints: config.mergeMatchingDatapoints,
|
|
2512
|
-
showSlider: config.showSlider,
|
|
2513
|
-
dateFrom: config.dateFrom,
|
|
2514
|
-
dateTo: config.dateTo
|
|
2515
|
-
};
|
|
2516
|
-
}
|
|
2517
|
-
findMatchingDatapoint(datapoints, selected) {
|
|
2518
|
-
if (!selected)
|
|
2519
|
-
return undefined;
|
|
2520
|
-
return datapoints.find(dp => this.getTargetId(dp) === this.getTargetId(selected) &&
|
|
2521
|
-
dp.fragment === selected.fragment &&
|
|
2522
|
-
dp.series === selected.series);
|
|
2523
|
-
}
|
|
2524
|
-
/**
|
|
2525
|
-
* Maps a computed auto-aggregation interval string (e.g. `'5m'`, `'3h'`, `'7d'`) to
|
|
2526
|
-
* the nearest standard `aggregationType` for non-time-series tenants.
|
|
2527
|
-
* - seconds / minutes → MINUTELY (finest supported granularity)
|
|
2528
|
-
* - hours → HOURLY
|
|
2529
|
-
* - days → DAILY
|
|
2530
|
-
*/
|
|
2531
|
-
mapIntervalToAggregationType(interval) {
|
|
2532
|
-
if (interval.endsWith('d'))
|
|
2533
|
-
return aggregationType.DAILY;
|
|
2534
|
-
if (interval.endsWith('h'))
|
|
2535
|
-
return aggregationType.HOURLY;
|
|
2536
|
-
if (interval.endsWith('m') || interval.endsWith('s'))
|
|
2537
|
-
return aggregationType.MINUTELY;
|
|
2538
|
-
return null;
|
|
2539
|
-
}
|
|
2540
|
-
/**
|
|
2541
|
-
* Computes the aggregation interval string (e.g. `30s`, `5m`, `24h`, `7d`) that
|
|
2542
|
-
* targets approximately `desiredDatapoints` buckets within the given time range.
|
|
2543
|
-
* The integer part is capped at 999 to satisfy the API constraint.
|
|
2544
|
-
*/
|
|
2545
|
-
calculateAutoAggregationInterval(dateFrom, dateTo, desiredDatapoints) {
|
|
2546
|
-
const rangeMs = new Date(dateTo).valueOf() - new Date(dateFrom).valueOf();
|
|
2547
|
-
const intervalMs = rangeMs / Math.max(1, desiredDatapoints);
|
|
2548
|
-
// If each bucket would be sub-second, the zoom is fine enough to show raw data
|
|
2549
|
-
if (intervalMs <= 1_000)
|
|
2550
|
-
return null;
|
|
2551
|
-
// Use the most natural human-readable unit.
|
|
2552
|
-
// e.g. 24h / 300 dp → 288 s → 5 m (not 288s)
|
|
2553
|
-
const totalSeconds = Math.max(1, Math.round(intervalMs / 1_000));
|
|
2554
|
-
if (totalSeconds < 60)
|
|
2555
|
-
return `${Math.min(999, totalSeconds)}s`;
|
|
2556
|
-
const totalMinutes = Math.round(totalSeconds / 60);
|
|
2557
|
-
if (totalMinutes < 60)
|
|
2558
|
-
return `${Math.min(999, totalMinutes)}m`;
|
|
2559
|
-
const totalHours = Math.round(totalSeconds / 3_600);
|
|
2560
|
-
if (totalHours < 24)
|
|
2561
|
-
return `${Math.min(999, totalHours)}h`;
|
|
2562
|
-
const totalDays = Math.max(1, Math.round(totalSeconds / 86_400));
|
|
2563
|
-
return `${Math.min(999, totalDays)}d`;
|
|
2564
|
-
}
|
|
2565
|
-
/**
|
|
2566
|
-
* Scans raw DpWithValues to determine which datapoints should be collapsed to a single
|
|
2567
|
-
* series — either because all per-function values are identical at every timestamp (the
|
|
2568
|
-
* aggregation interval is too fine — each interval holds a single measurement — to
|
|
2569
|
-
* differentiate functions).
|
|
2570
|
-
*/
|
|
2571
|
-
detectCollapsedDatapoints(dps) {
|
|
2572
|
-
const collapsed = new Set();
|
|
2573
|
-
for (const dp of dps) {
|
|
2574
|
-
const dpId = `${dp.__target?.id}${dp.fragment}${dp.series}`;
|
|
2575
|
-
const valueEntries = Object.values(dp.values ?? {});
|
|
2576
|
-
if (!valueEntries.length)
|
|
2577
|
-
continue;
|
|
2578
|
-
const renderTypeConfigs = dp.renderTypeConfigs;
|
|
2579
|
-
const renderAsBand = !!dp.renderAsBand;
|
|
2580
|
-
let fnsToCompare = [];
|
|
2581
|
-
let isBand = false;
|
|
2582
|
-
if (renderTypeConfigs) {
|
|
2583
|
-
const enabled = Object.keys(renderTypeConfigs).filter(k => renderTypeConfigs[k]?.enabled);
|
|
2584
|
-
const hasMinMax = enabled.includes('min') && enabled.includes('max');
|
|
2585
|
-
if (renderAsBand && hasMinMax) {
|
|
2586
|
-
isBand = true;
|
|
2587
|
-
}
|
|
2588
|
-
else if (enabled.length >= 2) {
|
|
2589
|
-
fnsToCompare = enabled;
|
|
2590
|
-
}
|
|
2591
|
-
else {
|
|
2592
|
-
continue;
|
|
2593
|
-
}
|
|
2594
|
-
}
|
|
2595
|
-
else if (dp.renderTypes && dp.renderTypes.length > 1) {
|
|
2596
|
-
const fns = dp.renderTypes.filter(rt => rt !== 'area');
|
|
2597
|
-
if (fns.length >= 2) {
|
|
2598
|
-
fnsToCompare = fns;
|
|
2599
|
-
}
|
|
2600
|
-
else {
|
|
2601
|
-
continue;
|
|
2602
|
-
}
|
|
2603
|
-
}
|
|
2604
|
-
else {
|
|
2605
|
-
continue;
|
|
2606
|
-
}
|
|
2607
|
-
if (isBand) {
|
|
2608
|
-
const allSame = valueEntries.every(vals => vals[0]?.min === vals[0]?.max);
|
|
2609
|
-
if (allSame)
|
|
2610
|
-
collapsed.add(dpId);
|
|
2611
|
-
}
|
|
2612
|
-
else {
|
|
2613
|
-
const allSame = valueEntries.every(vals => {
|
|
2614
|
-
const first = vals[0]?.[fnsToCompare[0]];
|
|
2615
|
-
if (first === undefined || first === null)
|
|
2616
|
-
return false;
|
|
2617
|
-
return fnsToCompare.slice(1).every(fn => vals[0]?.[fn] === first);
|
|
2618
|
-
});
|
|
2619
|
-
if (allSame)
|
|
2620
|
-
collapsed.add(dpId);
|
|
2621
|
-
}
|
|
2622
|
-
}
|
|
2623
|
-
return collapsed;
|
|
2624
|
-
}
|
|
2625
|
-
getTargetId(obj) {
|
|
2626
|
-
return obj?.__target?.id ?? obj?.target;
|
|
2627
|
-
}
|
|
2628
|
-
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: ChartHelpersService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
2629
|
-
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: ChartHelpersService }); }
|
|
2630
|
-
}
|
|
2631
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: ChartHelpersService, decorators: [{
|
|
2632
|
-
type: Injectable
|
|
2633
|
-
}] });
|
|
2634
|
-
|
|
2635
2761
|
class SelectAggregatedDatapointComponent {
|
|
2636
2762
|
constructor() {
|
|
2637
2763
|
this.activeDatapoints = [];
|
|
@@ -2973,6 +3099,16 @@ class ChartsComponent {
|
|
|
2973
3099
|
}
|
|
2974
3100
|
});
|
|
2975
3101
|
this.echartsInstance.on('click', this.onChartClick.bind(this));
|
|
3102
|
+
// ZRender fires for every click, including on blank canvas — where echarts' own 'click' event
|
|
3103
|
+
// does not. A click on empty space (no target) therefore never reached onChartClick, so after
|
|
3104
|
+
// clicking an event/alarm (which switches the tooltip to triggerOn: 'click') hovering could no
|
|
3105
|
+
// longer reopen a tooltip. Routing blank clicks through the deselect path restores hover mode
|
|
3106
|
+
// (triggerOn back to 'mousemove') and clears any active alarm area, mirroring a click on the area.
|
|
3107
|
+
this.echartsInstance.getZr().on('click', (event) => {
|
|
3108
|
+
if (!event.target) {
|
|
3109
|
+
this.onChartClick({ data: { itemType: '' } });
|
|
3110
|
+
}
|
|
3111
|
+
});
|
|
2976
3112
|
let originalFormatter = null;
|
|
2977
3113
|
this.echartsInstance.on('mouseover', (params) => {
|
|
2978
3114
|
if (params?.componentType !== 'markLine' && params?.componentType !== 'markPoint') {
|
|
@@ -2999,13 +3135,14 @@ class ChartsComponent {
|
|
|
2999
3135
|
this.echartsInstance.setOption(options);
|
|
3000
3136
|
}
|
|
3001
3137
|
});
|
|
3138
|
+
this.observeChartResize();
|
|
3002
3139
|
}
|
|
3003
3140
|
onChartClick(params) {
|
|
3004
|
-
const
|
|
3141
|
+
const clearActiveArea = this.getActiveAlarmAreaClearPatch();
|
|
3005
3142
|
if (!this.isAlarmOrEventClick(params)) {
|
|
3006
3143
|
this.echartsInstance.setOption({
|
|
3007
3144
|
tooltip: { triggerOn: 'mousemove' },
|
|
3008
|
-
|
|
3145
|
+
...(clearActiveArea ? { series: [clearActiveArea] } : {})
|
|
3009
3146
|
});
|
|
3010
3147
|
if (params.componentType === 'series' && params.seriesName) {
|
|
3011
3148
|
this.toggleStickyHighlight(params.seriesName);
|
|
@@ -3031,44 +3168,49 @@ class ChartsComponent {
|
|
|
3031
3168
|
}
|
|
3032
3169
|
const clickedAlarms = this.alarms.filter(alarm => alarm.type === params.data.itemType);
|
|
3033
3170
|
const clickedEvents = this.events.filter(event => event.type === params.data.itemType);
|
|
3171
|
+
const clickedTime = params.data.xAxis;
|
|
3172
|
+
const selectedAlarms = clickedAlarms.filter(alarm => alarm.creationTime === clickedTime || alarm['lastUpdated'] === clickedTime);
|
|
3173
|
+
const alarmsToMark = selectedAlarms.length ? selectedAlarms : clickedAlarms;
|
|
3034
3174
|
if (clickedEvents.length > 0) {
|
|
3035
|
-
|
|
3036
|
-
tooltip: {
|
|
3037
|
-
enterable: true,
|
|
3038
|
-
triggerOn: 'click'
|
|
3039
|
-
}
|
|
3040
|
-
};
|
|
3041
|
-
this.echartsInstance.setOption(updatedOptions);
|
|
3175
|
+
this.echartsInstance.setOption({ tooltip: { enterable: true, triggerOn: 'click' } });
|
|
3042
3176
|
return;
|
|
3043
3177
|
}
|
|
3044
|
-
|
|
3045
|
-
const
|
|
3046
|
-
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
3178
|
+
const markLineSeriesId = params.seriesName?.replace('-markPoint', '-markLine');
|
|
3179
|
+
const isReclickOfActiveArea = !!clearActiveArea && clearActiveArea.id === markLineSeriesId;
|
|
3180
|
+
this.isMarkedAreaEnabled.emit(isReclickOfActiveArea);
|
|
3181
|
+
if (isReclickOfActiveArea) {
|
|
3182
|
+
// Clicking the active alarm again just removes its area.
|
|
3183
|
+
this.echartsInstance.setOption({
|
|
3184
|
+
tooltip: { triggerOn: 'mousemove' },
|
|
3185
|
+
series: [clearActiveArea]
|
|
3186
|
+
});
|
|
3187
|
+
return;
|
|
3188
|
+
}
|
|
3189
|
+
// Activate this alarm's area, first removing any other active area (mutual exclusivity).
|
|
3190
|
+
this.echartsInstance.setOption({
|
|
3191
|
+
tooltip: { enterable: true, triggerOn: 'click' },
|
|
3192
|
+
series: [
|
|
3193
|
+
...(clearActiveArea ? [clearActiveArea] : []),
|
|
3060
3194
|
{
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
3195
|
+
id: markLineSeriesId,
|
|
3196
|
+
markArea: { label: { show: false }, data: this.getMarkedAreaData(alarmsToMark) },
|
|
3197
|
+
markLine: {
|
|
3198
|
+
showSymbol: true,
|
|
3199
|
+
symbol: ['none', 'none'],
|
|
3200
|
+
data: this.getMarkedLineData(clickedAlarms)
|
|
3201
|
+
}
|
|
3202
|
+
}
|
|
3203
|
+
]
|
|
3204
|
+
});
|
|
3065
3205
|
}
|
|
3066
3206
|
isAlarmOrEventClick(params) {
|
|
3067
3207
|
return (this.alarms.some(alarm => alarm.type === params.data?.itemType) ||
|
|
3068
3208
|
this.events.some(event => event.type === params.data?.itemType));
|
|
3069
3209
|
}
|
|
3070
|
-
hasMarkArea(options) {
|
|
3071
|
-
|
|
3210
|
+
hasMarkArea(options, seriesId) {
|
|
3211
|
+
const series = (options?.series ?? []);
|
|
3212
|
+
const target = seriesId ? series.find(s => s.id === seriesId) : series[0];
|
|
3213
|
+
return (target?.markArea?.data?.length ?? 0) > 0;
|
|
3072
3214
|
}
|
|
3073
3215
|
saveAsImage() {
|
|
3074
3216
|
const dataZoom = this.echartsInstance.getOption().dataZoom;
|
|
@@ -3160,6 +3302,21 @@ class ChartsComponent {
|
|
|
3160
3302
|
}
|
|
3161
3303
|
});
|
|
3162
3304
|
}
|
|
3305
|
+
/**
|
|
3306
|
+
* Whether the given datapoint currently has at least one series rendered in the chart.
|
|
3307
|
+
* A datapoint can be `__active` in the config yet absent from the chart if a full rebuild ran
|
|
3308
|
+
* while it was hidden (the fetch pipeline only builds series for active datapoints). In that case
|
|
3309
|
+
* a legend toggle has nothing to act on, so the caller must trigger a reload instead.
|
|
3310
|
+
* @param datapoint - The datapoint to look for.
|
|
3311
|
+
*/
|
|
3312
|
+
hasRenderedDatapointSeries(datapoint) {
|
|
3313
|
+
if (!this.echartsInstance) {
|
|
3314
|
+
return false;
|
|
3315
|
+
}
|
|
3316
|
+
const dpId = `${datapoint.__target?.id}${datapoint.fragment}${datapoint.series}`;
|
|
3317
|
+
const allSeries = this.echartsInstance.getOption().series;
|
|
3318
|
+
return allSeries?.some(s => s.datapointId === dpId) ?? false;
|
|
3319
|
+
}
|
|
3163
3320
|
/** Toggles a single chart series by its exact name (used for per-function legend entries). */
|
|
3164
3321
|
toggleSeriesByName(seriesName, visible) {
|
|
3165
3322
|
if (!this.echartsInstance) {
|
|
@@ -3321,6 +3478,54 @@ class ChartsComponent {
|
|
|
3321
3478
|
series: []
|
|
3322
3479
|
};
|
|
3323
3480
|
}
|
|
3481
|
+
/**
|
|
3482
|
+
* Re-spaces the alarm/event rows whenever the chart height changes, so the rows stay inside the
|
|
3483
|
+
* plot area on a short chart and after a resize (the pixel offsets are otherwise fixed at build
|
|
3484
|
+
* time). Starting at -1 makes the observer's initial callback also correct the estimated first
|
|
3485
|
+
* build, once a real grid exists to measure. No-op outside the alarms/events-only chart.
|
|
3486
|
+
*/
|
|
3487
|
+
observeChartResize() {
|
|
3488
|
+
const chartDom = typeof this.echartsInstance.getDom === 'function' ? this.echartsInstance.getDom() : null;
|
|
3489
|
+
if (!chartDom || typeof ResizeObserver === 'undefined') {
|
|
3490
|
+
return;
|
|
3491
|
+
}
|
|
3492
|
+
let lastHeight = -1;
|
|
3493
|
+
const resizeObserver = new ResizeObserver(() => {
|
|
3494
|
+
const height = chartDom.clientHeight;
|
|
3495
|
+
if (height === lastHeight) {
|
|
3496
|
+
return;
|
|
3497
|
+
}
|
|
3498
|
+
lastHeight = height;
|
|
3499
|
+
this.echartsInstance.resize();
|
|
3500
|
+
this.chartHelpersService.reapplyAlarmEventRowOffsets(this.echartsInstance, this.echartsOptionsService.isAlarmEventOnlyChart);
|
|
3501
|
+
});
|
|
3502
|
+
resizeObserver.observe(chartDom);
|
|
3503
|
+
this.destroyRef.onDestroy(() => resizeObserver.disconnect());
|
|
3504
|
+
}
|
|
3505
|
+
/**
|
|
3506
|
+
* Finds the one alarm series that currently shows a markArea and builds the `setOption` patch that
|
|
3507
|
+
* removes it, restoring that series' default mark-line. Only one area is ever active, so there is
|
|
3508
|
+
* nothing to iterate; the event lines live on their own series and are left untouched.
|
|
3509
|
+
* @returns The clear patch, or null when no alarm area is active.
|
|
3510
|
+
*/
|
|
3511
|
+
getActiveAlarmAreaClearPatch() {
|
|
3512
|
+
const series = (this.echartsInstance.getOption().series ?? []);
|
|
3513
|
+
const activeArea = series.find(s => s.typeOfSeries === 'alarm' && (s.markArea?.data?.length ?? 0) > 0);
|
|
3514
|
+
if (!activeArea) {
|
|
3515
|
+
return null;
|
|
3516
|
+
}
|
|
3517
|
+
const type = String(activeArea.id).split('/')[0];
|
|
3518
|
+
const alarmsOfType = this.alarms.filter(alarm => alarm.type === type);
|
|
3519
|
+
return {
|
|
3520
|
+
id: activeArea.id,
|
|
3521
|
+
markArea: { data: [] },
|
|
3522
|
+
markLine: {
|
|
3523
|
+
showSymbol: false,
|
|
3524
|
+
symbol: ['none', 'none'],
|
|
3525
|
+
data: this.echartsOptionsService.getDefaultMarkLineData(alarmsOfType, 'alarm')
|
|
3526
|
+
}
|
|
3527
|
+
};
|
|
3528
|
+
}
|
|
3324
3529
|
getMarkedAreaData(clickedAlarms) {
|
|
3325
3530
|
const timeRange = this.getTimeRange();
|
|
3326
3531
|
const clearedAlarmColor = 'rgba(221,255,221,1.00)';
|
|
@@ -3401,7 +3606,8 @@ class ChartsComponent {
|
|
|
3401
3606
|
if ((this.activeDatapoints || this.config.alarmsEventsConfigs?.length > 0) &&
|
|
3402
3607
|
this.config.realtime &&
|
|
3403
3608
|
this.echartsInstance) {
|
|
3404
|
-
const showOnlyAlarmsOrEvents = this.config.alarmsEventsConfigs?.length > 0 &&
|
|
3609
|
+
const showOnlyAlarmsOrEvents = this.config.alarmsEventsConfigs?.length > 0 &&
|
|
3610
|
+
(!this.activeDatapoints || this.activeDatapoints.length === 0);
|
|
3405
3611
|
this.chartRealtimeService.startRealtime(this.echartsInstance, this.activeDatapoints, this.getTimeRange(), dp => this.datapointOutOfSync.emit(dp), dp => this.datapointBackInSync.emit(dp), timeRange => this.timeRangeChangeOnRealtime.emit(timeRange), this.config.alarmsEventsConfigs, {
|
|
3406
3612
|
...this.displayOptions,
|
|
3407
3613
|
showOnlyAlarmsOrEvents
|