@c8y/ngx-components 1024.20.0 → 1024.20.8
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-application-select.mjs +1 -1
- package/fesm2022/c8y-ngx-components-application-select.mjs.map +1 -1
- package/fesm2022/c8y-ngx-components-asset-properties.mjs +3 -2
- package/fesm2022/c8y-ngx-components-asset-properties.mjs.map +1 -1
- package/fesm2022/c8y-ngx-components-context-dashboard.mjs +78 -17
- package/fesm2022/c8y-ngx-components-context-dashboard.mjs.map +1 -1
- package/fesm2022/c8y-ngx-components-datapoint-selector.mjs +1 -1
- package/fesm2022/c8y-ngx-components-datapoint-selector.mjs.map +1 -1
- package/fesm2022/c8y-ngx-components-device-profile.mjs +1 -1
- package/fesm2022/c8y-ngx-components-device-profile.mjs.map +1 -1
- package/fesm2022/c8y-ngx-components-echart-models.mjs.map +1 -1
- package/fesm2022/c8y-ngx-components-echart.mjs +571 -401
- package/fesm2022/c8y-ngx-components-echart.mjs.map +1 -1
- package/fesm2022/c8y-ngx-components-ecosystem-shared.mjs +1 -1
- package/fesm2022/c8y-ngx-components-ecosystem-shared.mjs.map +1 -1
- package/fesm2022/c8y-ngx-components-ecosystem.mjs +1 -1
- package/fesm2022/c8y-ngx-components-ecosystem.mjs.map +1 -1
- package/fesm2022/c8y-ngx-components-icon-selector.mjs +4 -4
- package/fesm2022/c8y-ngx-components-icon-selector.mjs.map +1 -1
- package/fesm2022/c8y-ngx-components-operations-stepper-bulk-type-configuration.mjs +1 -1
- package/fesm2022/c8y-ngx-components-operations-stepper-bulk-type-configuration.mjs.map +1 -1
- package/fesm2022/c8y-ngx-components-operations-stepper-bulk-type-device-profile.mjs +1 -1
- package/fesm2022/c8y-ngx-components-operations-stepper-bulk-type-device-profile.mjs.map +1 -1
- package/fesm2022/c8y-ngx-components-operations-stepper-bulk-type-software.mjs +1 -1
- package/fesm2022/c8y-ngx-components-operations-stepper-bulk-type-software.mjs.map +1 -1
- package/fesm2022/c8y-ngx-components-protocol-lpwan.mjs +2 -2
- package/fesm2022/c8y-ngx-components-protocol-lpwan.mjs.map +1 -1
- package/fesm2022/c8y-ngx-components-repository-configuration.mjs +4 -4
- package/fesm2022/c8y-ngx-components-repository-configuration.mjs.map +1 -1
- package/fesm2022/c8y-ngx-components-repository-firmware.mjs +2 -2
- package/fesm2022/c8y-ngx-components-repository-firmware.mjs.map +1 -1
- package/fesm2022/c8y-ngx-components-repository-shared.mjs +2 -2
- package/fesm2022/c8y-ngx-components-repository-shared.mjs.map +1 -1
- package/fesm2022/c8y-ngx-components-repository-software.mjs +1 -1
- package/fesm2022/c8y-ngx-components-repository-software.mjs.map +1 -1
- package/fesm2022/c8y-ngx-components-widgets-definitions-html-widget.mjs +17 -1
- package/fesm2022/c8y-ngx-components-widgets-definitions-html-widget.mjs.map +1 -1
- package/fesm2022/c8y-ngx-components-widgets-definitions-scada.mjs +21 -1
- package/fesm2022/c8y-ngx-components-widgets-definitions-scada.mjs.map +1 -1
- package/fesm2022/c8y-ngx-components-widgets-implementations-asset-table.mjs +60 -31
- package/fesm2022/c8y-ngx-components-widgets-implementations-asset-table.mjs.map +1 -1
- package/fesm2022/c8y-ngx-components-widgets-implementations-datapoints-graph.mjs +15 -7
- package/fesm2022/c8y-ngx-components-widgets-implementations-datapoints-graph.mjs.map +1 -1
- package/fesm2022/c8y-ngx-components.mjs +199 -21
- 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/nl.po +6 -0
- package/locales/pl.po +6 -0
- package/locales/pt_BR.po +8 -2
- package/locales/zh_CN.po +6 -0
- package/locales/zh_TW.po +6 -0
- package/package.json +1 -1
- package/types/c8y-ngx-components-asset-properties.d.ts.map +1 -1
- package/types/c8y-ngx-components-context-dashboard.d.ts +37 -3
- 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-asset-table.d.ts +5 -0
- package/types/c8y-ngx-components-widgets-implementations-asset-table.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 +141 -4
- 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,87 +1741,51 @@ 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
|
}
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
{
|
|
1550
|
-
coord: coordCreationTime,
|
|
1551
|
-
name: item.type,
|
|
1552
|
-
itemType: item.type,
|
|
1553
|
-
itemStyle: {
|
|
1554
|
-
color: item['color']
|
|
1555
|
-
},
|
|
1556
|
-
symbol: 'circle',
|
|
1557
|
-
symbolSize: 24
|
|
1558
|
-
},
|
|
1559
|
-
{
|
|
1560
|
-
coord: coordCreationTime,
|
|
1561
|
-
name: item.type,
|
|
1562
|
-
itemType: item.type,
|
|
1563
|
-
itemStyle: { color: 'white' },
|
|
1564
|
-
symbol: ICONS_MAP[item.severity],
|
|
1565
|
-
symbolSize: 16
|
|
1566
|
-
},
|
|
1567
|
-
{
|
|
1568
|
-
coord: coordLastUpdatedTime,
|
|
1569
|
-
name: item.type,
|
|
1570
|
-
itemType: item.type,
|
|
1571
|
-
itemStyle: {
|
|
1572
|
-
color: item['color']
|
|
1573
|
-
},
|
|
1574
|
-
symbol: 'circle',
|
|
1575
|
-
symbolSize: 24
|
|
1576
|
-
},
|
|
1577
|
-
{
|
|
1578
|
-
coord: coordLastUpdatedTime,
|
|
1579
|
-
name: item.type,
|
|
1580
|
-
itemType: item.type,
|
|
1581
|
-
itemStyle: { color: 'white' },
|
|
1582
|
-
symbol: ICONS_MAP[item.severity],
|
|
1583
|
-
symbolSize: 16
|
|
1584
|
-
}
|
|
1585
|
-
];
|
|
1749
|
+
const creationMarks = [
|
|
1750
|
+
{
|
|
1751
|
+
coord: coordCreationTime,
|
|
1752
|
+
name: item.type,
|
|
1753
|
+
itemType: item.type,
|
|
1754
|
+
itemStyle: { color: item['color'] },
|
|
1755
|
+
symbol: 'circle',
|
|
1756
|
+
symbolSize: 24
|
|
1757
|
+
},
|
|
1758
|
+
{
|
|
1759
|
+
coord: coordCreationTime,
|
|
1760
|
+
name: item.type,
|
|
1761
|
+
itemType: item.type,
|
|
1762
|
+
itemStyle: { color: 'white' },
|
|
1763
|
+
symbol: ICONS_MAP[item.severity],
|
|
1764
|
+
symbolSize: 16
|
|
1765
|
+
}
|
|
1766
|
+
];
|
|
1767
|
+
if (!item['lastUpdated']) {
|
|
1768
|
+
return creationMarks;
|
|
1769
|
+
}
|
|
1770
|
+
return [
|
|
1771
|
+
...creationMarks,
|
|
1772
|
+
{
|
|
1773
|
+
coord: coordLastUpdatedTime,
|
|
1774
|
+
name: item.type,
|
|
1775
|
+
itemType: item.type,
|
|
1776
|
+
itemStyle: { color: item['color'] },
|
|
1777
|
+
symbol: 'circle',
|
|
1778
|
+
symbolSize: 24
|
|
1779
|
+
},
|
|
1780
|
+
{
|
|
1781
|
+
coord: coordLastUpdatedTime,
|
|
1782
|
+
name: item.type,
|
|
1783
|
+
itemType: item.type,
|
|
1784
|
+
itemStyle: { color: 'white' },
|
|
1785
|
+
symbol: isCleared ? ICONS_MAP.CLEARED : ICONS_MAP[item.severity],
|
|
1786
|
+
symbolSize: 16
|
|
1787
|
+
}
|
|
1788
|
+
];
|
|
1586
1789
|
}
|
|
1587
1790
|
/**
|
|
1588
1791
|
* This method creates a markLine on the chart which represents the line between every alarm or event on the chart.
|
|
@@ -1604,7 +1807,7 @@ class EchartsOptionsService {
|
|
|
1604
1807
|
}
|
|
1605
1808
|
]);
|
|
1606
1809
|
}
|
|
1607
|
-
if (item.creationTime === item['lastUpdated']) {
|
|
1810
|
+
if (!item['lastUpdated'] || item.creationTime === item['lastUpdated']) {
|
|
1608
1811
|
return acc.concat([
|
|
1609
1812
|
{
|
|
1610
1813
|
xAxis: item.creationTime,
|
|
@@ -2014,12 +2217,12 @@ class EchartsOptionsService {
|
|
|
2014
2217
|
return baseColor;
|
|
2015
2218
|
}
|
|
2016
2219
|
}
|
|
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 }); }
|
|
2220
|
+
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
2221
|
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: EchartsOptionsService }); }
|
|
2019
2222
|
}
|
|
2020
2223
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: EchartsOptionsService, decorators: [{
|
|
2021
2224
|
type: Injectable
|
|
2022
|
-
}], ctorParameters: () => [{ type: i1$1.DatePipe }, { type: YAxisService }, { type: ChartTypesService }, { type: i4.AlarmSeverityToIconPipe }, { type: i4.AlarmSeverityToLabelPipe }, { type: i1.TranslateService }, { type: i6.Router }] });
|
|
2225
|
+
}], ctorParameters: () => [{ type: i1$1.DatePipe }, { type: YAxisService }, { type: ChartTypesService }, { type: i4.AlarmSeverityToIconPipe }, { type: i4.AlarmSeverityToLabelPipe }, { type: i1.TranslateService }, { type: i6.Router }, { type: ChartHelpersService }] });
|
|
2023
2226
|
|
|
2024
2227
|
class ChartRealtimeService {
|
|
2025
2228
|
constructor(measurementRealtime, alarmRealtimeService, eventRealtimeService, echartsOptionsService) {
|
|
@@ -2049,6 +2252,7 @@ class ChartRealtimeService {
|
|
|
2049
2252
|
if (displayOptions.showOnlyAlarmsOrEvents) {
|
|
2050
2253
|
this.currentAlarms = alarms ? [...alarms] : [];
|
|
2051
2254
|
this.currentEvents = events ? [...events] : [];
|
|
2255
|
+
this.currentDisplayOptions = displayOptions;
|
|
2052
2256
|
// Subscribe to realtime alarms/events
|
|
2053
2257
|
const allAlarmsAndEvents$ = from(uniqueAlarmOrEventTargets).pipe(mergeMap(targetId => {
|
|
2054
2258
|
const alarmsRealtime$ = this.alarmRealtimeService.onAll$(targetId);
|
|
@@ -2058,9 +2262,19 @@ class ChartRealtimeService {
|
|
|
2058
2262
|
this.realtimeSubscriptionAlarmsEvents = allAlarmsAndEvents$.subscribe(alarmOrEvent => {
|
|
2059
2263
|
if (!alarmOrEvent)
|
|
2060
2264
|
return;
|
|
2265
|
+
const matchingConfig = activeAlarmsOrEvents.find(aOrE => aOrE.filters.type === alarmOrEvent.type && aOrE.__target?.id === alarmOrEvent.source?.id);
|
|
2266
|
+
if (!matchingConfig) {
|
|
2267
|
+
return;
|
|
2268
|
+
}
|
|
2269
|
+
alarmOrEvent['color'] = matchingConfig.color;
|
|
2270
|
+
alarmOrEvent['selectedDatapoint'] = matchingConfig.selectedDatapoint;
|
|
2061
2271
|
if ('severity' in alarmOrEvent) {
|
|
2062
2272
|
// Alarm
|
|
2063
|
-
|
|
2273
|
+
const existingIdx = this.currentAlarms.findIndex(a => a.id === alarmOrEvent.id);
|
|
2274
|
+
if (existingIdx !== -1) {
|
|
2275
|
+
this.currentAlarms[existingIdx] = alarmOrEvent;
|
|
2276
|
+
}
|
|
2277
|
+
else {
|
|
2064
2278
|
this.currentAlarms.push(alarmOrEvent);
|
|
2065
2279
|
}
|
|
2066
2280
|
}
|
|
@@ -2070,6 +2284,7 @@ class ChartRealtimeService {
|
|
|
2070
2284
|
this.currentEvents.push(alarmOrEvent);
|
|
2071
2285
|
}
|
|
2072
2286
|
}
|
|
2287
|
+
alarmOrEventReceivedCallback?.(alarmOrEvent);
|
|
2073
2288
|
this.updateAlarmsEventsChartRealtime();
|
|
2074
2289
|
});
|
|
2075
2290
|
const updateThrottleTime = this.getRealtimeUpdateThrottleTime({
|
|
@@ -2167,7 +2382,18 @@ class ChartRealtimeService {
|
|
|
2167
2382
|
* Updates the chart with the current alarms and events data. Only used when no active datapoints are selected.
|
|
2168
2383
|
*/
|
|
2169
2384
|
updateAlarmsEventsChartRealtime() {
|
|
2170
|
-
const series = this.echartsOptionsService.
|
|
2385
|
+
const series = this.echartsOptionsService.getAlarmEventCarrierSeries(this.currentAlarms, this.currentEvents, {
|
|
2386
|
+
dateFrom: (this.currentTimeRange?.dateFrom ?? new Date()).toISOString(),
|
|
2387
|
+
dateTo: (this.currentTimeRange?.dateTo ?? new Date()).toISOString()
|
|
2388
|
+
}, this.currentDisplayOptions ?? {
|
|
2389
|
+
displayMarkedLine: true,
|
|
2390
|
+
displayMarkedPoint: true,
|
|
2391
|
+
mergeMatchingDatapoints: false,
|
|
2392
|
+
forceMergeDatapoints: false,
|
|
2393
|
+
setYaxisStartToZero: false,
|
|
2394
|
+
showLabelAndUnit: false,
|
|
2395
|
+
showSlider: false
|
|
2396
|
+
});
|
|
2171
2397
|
this.echartsInstance?.setOption({
|
|
2172
2398
|
series,
|
|
2173
2399
|
xAxis: {
|
|
@@ -2496,142 +2722,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImpo
|
|
|
2496
2722
|
type: Injectable
|
|
2497
2723
|
}], ctorParameters: () => [{ type: i1$3.AlarmService }] });
|
|
2498
2724
|
|
|
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
2725
|
class SelectAggregatedDatapointComponent {
|
|
2636
2726
|
constructor() {
|
|
2637
2727
|
this.activeDatapoints = [];
|
|
@@ -2973,6 +3063,16 @@ class ChartsComponent {
|
|
|
2973
3063
|
}
|
|
2974
3064
|
});
|
|
2975
3065
|
this.echartsInstance.on('click', this.onChartClick.bind(this));
|
|
3066
|
+
// ZRender fires for every click, including on blank canvas — where echarts' own 'click' event
|
|
3067
|
+
// does not. A click on empty space (no target) therefore never reached onChartClick, so after
|
|
3068
|
+
// clicking an event/alarm (which switches the tooltip to triggerOn: 'click') hovering could no
|
|
3069
|
+
// longer reopen a tooltip. Routing blank clicks through the deselect path restores hover mode
|
|
3070
|
+
// (triggerOn back to 'mousemove') and clears any active alarm area, mirroring a click on the area.
|
|
3071
|
+
this.echartsInstance.getZr().on('click', (event) => {
|
|
3072
|
+
if (!event.target) {
|
|
3073
|
+
this.onChartClick({ data: { itemType: '' } });
|
|
3074
|
+
}
|
|
3075
|
+
});
|
|
2976
3076
|
let originalFormatter = null;
|
|
2977
3077
|
this.echartsInstance.on('mouseover', (params) => {
|
|
2978
3078
|
if (params?.componentType !== 'markLine' && params?.componentType !== 'markPoint') {
|
|
@@ -2999,13 +3099,14 @@ class ChartsComponent {
|
|
|
2999
3099
|
this.echartsInstance.setOption(options);
|
|
3000
3100
|
}
|
|
3001
3101
|
});
|
|
3102
|
+
this.observeChartResize();
|
|
3002
3103
|
}
|
|
3003
3104
|
onChartClick(params) {
|
|
3004
|
-
const
|
|
3105
|
+
const clearActiveArea = this.getActiveAlarmAreaClearPatch();
|
|
3005
3106
|
if (!this.isAlarmOrEventClick(params)) {
|
|
3006
3107
|
this.echartsInstance.setOption({
|
|
3007
3108
|
tooltip: { triggerOn: 'mousemove' },
|
|
3008
|
-
|
|
3109
|
+
...(clearActiveArea ? { series: [clearActiveArea] } : {})
|
|
3009
3110
|
});
|
|
3010
3111
|
if (params.componentType === 'series' && params.seriesName) {
|
|
3011
3112
|
this.toggleStickyHighlight(params.seriesName);
|
|
@@ -3031,44 +3132,49 @@ class ChartsComponent {
|
|
|
3031
3132
|
}
|
|
3032
3133
|
const clickedAlarms = this.alarms.filter(alarm => alarm.type === params.data.itemType);
|
|
3033
3134
|
const clickedEvents = this.events.filter(event => event.type === params.data.itemType);
|
|
3135
|
+
const clickedTime = params.data.xAxis;
|
|
3136
|
+
const selectedAlarms = clickedAlarms.filter(alarm => alarm.creationTime === clickedTime || alarm['lastUpdated'] === clickedTime);
|
|
3137
|
+
const alarmsToMark = selectedAlarms.length ? selectedAlarms : clickedAlarms;
|
|
3034
3138
|
if (clickedEvents.length > 0) {
|
|
3035
|
-
|
|
3036
|
-
tooltip: {
|
|
3037
|
-
enterable: true,
|
|
3038
|
-
triggerOn: 'click'
|
|
3039
|
-
}
|
|
3040
|
-
};
|
|
3041
|
-
this.echartsInstance.setOption(updatedOptions);
|
|
3139
|
+
this.echartsInstance.setOption({ tooltip: { enterable: true, triggerOn: 'click' } });
|
|
3042
3140
|
return;
|
|
3043
3141
|
}
|
|
3044
|
-
|
|
3045
|
-
const
|
|
3046
|
-
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
3142
|
+
const markLineSeriesId = params.seriesName?.replace('-markPoint', '-markLine');
|
|
3143
|
+
const isReclickOfActiveArea = !!clearActiveArea && clearActiveArea.id === markLineSeriesId;
|
|
3144
|
+
this.isMarkedAreaEnabled.emit(isReclickOfActiveArea);
|
|
3145
|
+
if (isReclickOfActiveArea) {
|
|
3146
|
+
// Clicking the active alarm again just removes its area.
|
|
3147
|
+
this.echartsInstance.setOption({
|
|
3148
|
+
tooltip: { triggerOn: 'mousemove' },
|
|
3149
|
+
series: [clearActiveArea]
|
|
3150
|
+
});
|
|
3151
|
+
return;
|
|
3152
|
+
}
|
|
3153
|
+
// Activate this alarm's area, first removing any other active area (mutual exclusivity).
|
|
3154
|
+
this.echartsInstance.setOption({
|
|
3155
|
+
tooltip: { enterable: true, triggerOn: 'click' },
|
|
3156
|
+
series: [
|
|
3157
|
+
...(clearActiveArea ? [clearActiveArea] : []),
|
|
3060
3158
|
{
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
3159
|
+
id: markLineSeriesId,
|
|
3160
|
+
markArea: { label: { show: false }, data: this.getMarkedAreaData(alarmsToMark) },
|
|
3161
|
+
markLine: {
|
|
3162
|
+
showSymbol: true,
|
|
3163
|
+
symbol: ['none', 'none'],
|
|
3164
|
+
data: this.getMarkedLineData(clickedAlarms)
|
|
3165
|
+
}
|
|
3166
|
+
}
|
|
3167
|
+
]
|
|
3168
|
+
});
|
|
3065
3169
|
}
|
|
3066
3170
|
isAlarmOrEventClick(params) {
|
|
3067
3171
|
return (this.alarms.some(alarm => alarm.type === params.data?.itemType) ||
|
|
3068
3172
|
this.events.some(event => event.type === params.data?.itemType));
|
|
3069
3173
|
}
|
|
3070
|
-
hasMarkArea(options) {
|
|
3071
|
-
|
|
3174
|
+
hasMarkArea(options, seriesId) {
|
|
3175
|
+
const series = (options?.series ?? []);
|
|
3176
|
+
const target = seriesId ? series.find(s => s.id === seriesId) : series[0];
|
|
3177
|
+
return (target?.markArea?.data?.length ?? 0) > 0;
|
|
3072
3178
|
}
|
|
3073
3179
|
saveAsImage() {
|
|
3074
3180
|
const dataZoom = this.echartsInstance.getOption().dataZoom;
|
|
@@ -3160,6 +3266,21 @@ class ChartsComponent {
|
|
|
3160
3266
|
}
|
|
3161
3267
|
});
|
|
3162
3268
|
}
|
|
3269
|
+
/**
|
|
3270
|
+
* Whether the given datapoint currently has at least one series rendered in the chart.
|
|
3271
|
+
* A datapoint can be `__active` in the config yet absent from the chart if a full rebuild ran
|
|
3272
|
+
* while it was hidden (the fetch pipeline only builds series for active datapoints). In that case
|
|
3273
|
+
* a legend toggle has nothing to act on, so the caller must trigger a reload instead.
|
|
3274
|
+
* @param datapoint - The datapoint to look for.
|
|
3275
|
+
*/
|
|
3276
|
+
hasRenderedDatapointSeries(datapoint) {
|
|
3277
|
+
if (!this.echartsInstance) {
|
|
3278
|
+
return false;
|
|
3279
|
+
}
|
|
3280
|
+
const dpId = `${datapoint.__target?.id}${datapoint.fragment}${datapoint.series}`;
|
|
3281
|
+
const allSeries = this.echartsInstance.getOption().series;
|
|
3282
|
+
return allSeries?.some(s => s.datapointId === dpId) ?? false;
|
|
3283
|
+
}
|
|
3163
3284
|
/** Toggles a single chart series by its exact name (used for per-function legend entries). */
|
|
3164
3285
|
toggleSeriesByName(seriesName, visible) {
|
|
3165
3286
|
if (!this.echartsInstance) {
|
|
@@ -3321,6 +3442,54 @@ class ChartsComponent {
|
|
|
3321
3442
|
series: []
|
|
3322
3443
|
};
|
|
3323
3444
|
}
|
|
3445
|
+
/**
|
|
3446
|
+
* Re-spaces the alarm/event rows whenever the chart height changes, so the rows stay inside the
|
|
3447
|
+
* plot area on a short chart and after a resize (the pixel offsets are otherwise fixed at build
|
|
3448
|
+
* time). Starting at -1 makes the observer's initial callback also correct the estimated first
|
|
3449
|
+
* build, once a real grid exists to measure. No-op outside the alarms/events-only chart.
|
|
3450
|
+
*/
|
|
3451
|
+
observeChartResize() {
|
|
3452
|
+
const chartDom = typeof this.echartsInstance.getDom === 'function' ? this.echartsInstance.getDom() : null;
|
|
3453
|
+
if (!chartDom || typeof ResizeObserver === 'undefined') {
|
|
3454
|
+
return;
|
|
3455
|
+
}
|
|
3456
|
+
let lastHeight = -1;
|
|
3457
|
+
const resizeObserver = new ResizeObserver(() => {
|
|
3458
|
+
const height = chartDom.clientHeight;
|
|
3459
|
+
if (height === lastHeight) {
|
|
3460
|
+
return;
|
|
3461
|
+
}
|
|
3462
|
+
lastHeight = height;
|
|
3463
|
+
this.echartsInstance.resize();
|
|
3464
|
+
this.chartHelpersService.reapplyAlarmEventRowOffsets(this.echartsInstance, this.echartsOptionsService.isAlarmEventOnlyChart);
|
|
3465
|
+
});
|
|
3466
|
+
resizeObserver.observe(chartDom);
|
|
3467
|
+
this.destroyRef.onDestroy(() => resizeObserver.disconnect());
|
|
3468
|
+
}
|
|
3469
|
+
/**
|
|
3470
|
+
* Finds the one alarm series that currently shows a markArea and builds the `setOption` patch that
|
|
3471
|
+
* removes it, restoring that series' default mark-line. Only one area is ever active, so there is
|
|
3472
|
+
* nothing to iterate; the event lines live on their own series and are left untouched.
|
|
3473
|
+
* @returns The clear patch, or null when no alarm area is active.
|
|
3474
|
+
*/
|
|
3475
|
+
getActiveAlarmAreaClearPatch() {
|
|
3476
|
+
const series = (this.echartsInstance.getOption().series ?? []);
|
|
3477
|
+
const activeArea = series.find(s => s.typeOfSeries === 'alarm' && (s.markArea?.data?.length ?? 0) > 0);
|
|
3478
|
+
if (!activeArea) {
|
|
3479
|
+
return null;
|
|
3480
|
+
}
|
|
3481
|
+
const type = String(activeArea.id).split('/')[0];
|
|
3482
|
+
const alarmsOfType = this.alarms.filter(alarm => alarm.type === type);
|
|
3483
|
+
return {
|
|
3484
|
+
id: activeArea.id,
|
|
3485
|
+
markArea: { data: [] },
|
|
3486
|
+
markLine: {
|
|
3487
|
+
showSymbol: false,
|
|
3488
|
+
symbol: ['none', 'none'],
|
|
3489
|
+
data: this.echartsOptionsService.getDefaultMarkLineData(alarmsOfType, 'alarm')
|
|
3490
|
+
}
|
|
3491
|
+
};
|
|
3492
|
+
}
|
|
3324
3493
|
getMarkedAreaData(clickedAlarms) {
|
|
3325
3494
|
const timeRange = this.getTimeRange();
|
|
3326
3495
|
const clearedAlarmColor = 'rgba(221,255,221,1.00)';
|
|
@@ -3401,7 +3570,8 @@ class ChartsComponent {
|
|
|
3401
3570
|
if ((this.activeDatapoints || this.config.alarmsEventsConfigs?.length > 0) &&
|
|
3402
3571
|
this.config.realtime &&
|
|
3403
3572
|
this.echartsInstance) {
|
|
3404
|
-
const showOnlyAlarmsOrEvents = this.config.alarmsEventsConfigs?.length > 0 &&
|
|
3573
|
+
const showOnlyAlarmsOrEvents = this.config.alarmsEventsConfigs?.length > 0 &&
|
|
3574
|
+
(!this.activeDatapoints || this.activeDatapoints.length === 0);
|
|
3405
3575
|
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
3576
|
...this.displayOptions,
|
|
3407
3577
|
showOnlyAlarmsOrEvents
|