diff --git a/src/app/datapoints-graph/alarm-event-selector/alarm-event-attributes-form/alarm-event-attributes-form.component.html b/src/app/datapoints-graph/alarm-event-selector/alarm-event-attributes-form/alarm-event-attributes-form.component.html index 73460735..64341b13 100644 --- a/src/app/datapoints-graph/alarm-event-selector/alarm-event-attributes-form/alarm-event-attributes-form.component.html +++ b/src/app/datapoints-graph/alarm-event-selector/alarm-event-attributes-form/alarm-event-attributes-form.component.html @@ -38,4 +38,35 @@ /> + + + +
+ + +
+
diff --git a/src/app/datapoints-graph/alarm-event-selector/alarm-event-attributes-form/alarm-event-attributes-form.component.ts b/src/app/datapoints-graph/alarm-event-selector/alarm-event-attributes-form/alarm-event-attributes-form.component.ts index 539d9da4..e07ceb2d 100644 --- a/src/app/datapoints-graph/alarm-event-selector/alarm-event-attributes-form/alarm-event-attributes-form.component.ts +++ b/src/app/datapoints-graph/alarm-event-selector/alarm-event-attributes-form/alarm-event-attributes-form.component.ts @@ -11,7 +11,9 @@ import { Validators, } from '@angular/forms'; import { take } from 'rxjs/operators'; -import { TimelineType } from '../alarm-event-selector.model'; +import { SelectedDatapoint, TimelineType } from '../alarm-event-selector.model'; +import { IIdentified } from '@c8y/client'; +import { KPIDetails } from '@c8y/ngx-components/datapoint-selector'; @Component({ selector: 'c8y-alarm-event-attributes-form', @@ -33,6 +35,8 @@ export class AlarmEventAttributesFormComponent implements ControlValueAccessor, Validator, OnInit { @Input() timelineType: TimelineType; + @Input() target: IIdentified; + @Input() datapoints: KPIDetails[] = []; formGroup: FormGroup; constructor(private formBuilder: FormBuilder) {} @@ -42,7 +46,9 @@ export class AlarmEventAttributesFormComponent label: ['', [Validators.required]], filters: this.formBuilder.group({ type: ['', [Validators.required]] }), timelineType: '', + selectedDatapoint: [{}, []], }); + this.listKpis(); } validate(_control: AbstractControl): ValidationErrors { @@ -64,4 +70,20 @@ export class AlarmEventAttributesFormComponent setDisabledState(isDisabled: boolean): void { isDisabled ? this.formGroup.disable() : this.formGroup.enable(); } + + changeDatapointSelection(selectedDatapoint: SelectedDatapoint) { + this.formGroup.patchValue({ selectedDatapoint }); + } + + trackByFn(_index: number, item: KPIDetails) { + return `${item.fragment}-${item.series}-${item.__target?.id}`; + } + + private listKpis() { + if (this.target && this.target.id) { + this.datapoints = this.datapoints.filter( + (dp) => dp.__target?.id === this.target.id + ); + } + } } diff --git a/src/app/datapoints-graph/alarm-event-selector/alarm-event-selection-list/alarm-event-selection-list.component.html b/src/app/datapoints-graph/alarm-event-selector/alarm-event-selection-list/alarm-event-selection-list.component.html index 8c5313ef..d947075d 100644 --- a/src/app/datapoints-graph/alarm-event-selector/alarm-event-selection-list/alarm-event-selection-list.component.html +++ b/src/app/datapoints-graph/alarm-event-selector/alarm-event-selection-list/alarm-event-selection-list.component.html @@ -28,6 +28,7 @@ cdkDrag formControlName="details" [showAddRemoveButton]="false" + [datapoints]="config?.datapoints" [optionToRemove]="true" [showActiveToggle]="true" [timelineType]="timelineType" diff --git a/src/app/datapoints-graph/alarm-event-selector/alarm-event-selector-list-item/alarm-event-selector-list-item.component.html b/src/app/datapoints-graph/alarm-event-selector/alarm-event-selector-list-item/alarm-event-selector-list-item.component.html index 9f7b2577..3b2410cd 100644 --- a/src/app/datapoints-graph/alarm-event-selector/alarm-event-selector-list-item/alarm-event-selector-list-item.component.html +++ b/src/app/datapoints-graph/alarm-event-selector/alarm-event-selector-list-item/alarm-event-selector-list-item.component.html @@ -112,6 +112,8 @@ diff --git a/src/app/datapoints-graph/alarm-event-selector/alarm-event-selector-list-item/alarm-event-selector-list-item.component.ts b/src/app/datapoints-graph/alarm-event-selector/alarm-event-selector-list-item/alarm-event-selector-list-item.component.ts index 5fdaaceb..86bd487d 100644 --- a/src/app/datapoints-graph/alarm-event-selector/alarm-event-selector-list-item/alarm-event-selector-list-item.component.ts +++ b/src/app/datapoints-graph/alarm-event-selector/alarm-event-selector-list-item/alarm-event-selector-list-item.component.ts @@ -20,6 +20,7 @@ import { AlarmOrEvent, TimelineType } from '../alarm-event-selector.model'; import { map, take, takeUntil } from 'rxjs/operators'; import { Observable, Subject } from 'rxjs'; import { gettext } from '@c8y/ngx-components'; +import { KPIDetails } from '@c8y/ngx-components/datapoint-selector'; @Component({ selector: 'c8y-alarm-event-selector-list-item', @@ -40,6 +41,7 @@ import { gettext } from '@c8y/ngx-components'; export class AlarmEventSelectorListItemComponent implements ControlValueAccessor, Validator, OnDestroy { + @Input() datapoints: KPIDetails | undefined; @Input() timelineType: TimelineType | undefined; @Input() highlightText: string | undefined; @Input() showAddRemoveButton = true; diff --git a/src/app/datapoints-graph/alarm-event-selector/alarm-event-selector.model.ts b/src/app/datapoints-graph/alarm-event-selector/alarm-event-selector.model.ts index f8fa9110..d4cd2dfc 100644 --- a/src/app/datapoints-graph/alarm-event-selector/alarm-event-selector.model.ts +++ b/src/app/datapoints-graph/alarm-event-selector/alarm-event-selector.model.ts @@ -2,6 +2,7 @@ import { AlarmStatusType, IIdentified } from '@c8y/client'; import { AlarmEventSelectorModalComponent } from './alarm-event-selector-modal/alarm-event-selector-modal.component'; import { SeverityType } from '@c8y/client/lib/src/core/Severity'; import { gettext } from '@c8y/ngx-components'; +import { KPIDetails } from '@c8y/ngx-components/datapoint-selector'; export type TimelineType = 'ALARM' | 'EVENT'; @@ -32,6 +33,7 @@ export type AlarmDetails = AlarmOrEventBase & { filters: { type: string; }; + selectedDatapoint?: SelectedDatapoint; __hidden?: boolean; __severity?: SeverityType[]; __status?: AlarmStatusType[]; @@ -42,9 +44,16 @@ export type EventDetails = AlarmOrEventBase & { filters: { type: string; }; + selectedDatapoint?: SelectedDatapoint; __hidden?: boolean; }; +export type SelectedDatapoint = { + target?: string; + series?: string; + fragment?: string; +}; + export type AlarmOrEvent = AlarmDetails | EventDetails; export type TimelineTypeTexts = { diff --git a/src/app/datapoints-graph/charts/chart-realtime.service.ts b/src/app/datapoints-graph/charts/chart-realtime.service.ts index d95197a5..f07fa07a 100644 --- a/src/app/datapoints-graph/charts/chart-realtime.service.ts +++ b/src/app/datapoints-graph/charts/chart-realtime.service.ts @@ -26,11 +26,7 @@ import { import type { ECharts, SeriesOption } from 'echarts'; import { EchartsOptionsService } from './echarts-options.service'; import { AlarmOrEvent } from '../alarm-event-selector'; -import { - customSeriesMarkLineData, - customSeriesMarkPointData, - CustomSeriesOptions, -} from './chart.model'; +import { CustomSeriesOptions } from './chart.model'; type Milliseconds = number; @@ -224,7 +220,7 @@ export class ChartRealtimeService { seriesDataToUpdate.get(datapoint)?.push(measurement); }); - const allDataSeries = this.echartsInstance?.getOption()[ + let allDataSeries = this.echartsInstance?.getOption()[ 'series' ] as CustomSeriesOptions[]; @@ -301,29 +297,24 @@ export class ChartRealtimeService { ); } ); - // update the last value of the markline to the new value - const markLine = alarmSeries.find((series) => series['markLine']); - const alarmSeriesMarkLine = markLine![ - 'markLine' - ] as customSeriesMarkLineData; - alarmSeriesMarkLine.data[1].xAxis = (alarmOrEvent as IAlarm)[ - 'lastUpdated' - ]; - // update the last value of the markpoint to the new value - const markPoint = alarmSeries.find((series) => series['markPoint']); - const alarmSeriesMarkPoint = markPoint![ - 'markPoint' - ] as customSeriesMarkPointData; + // remove all matching alarm series which are in the array + alarmSeries.forEach((series) => { + allDataSeries = allDataSeries.filter( + (s) => s['id'] !== series['id'] + ); + }); - // the if block is needed in case an alarm has occured, of that type, but for a different target device. - if (alarmSeriesMarkPoint.data?.length > 2) { - alarmSeriesMarkPoint.data[2].coord[0] = (alarmOrEvent as IAlarm)[ - 'lastUpdated' - ]; - alarmSeriesMarkPoint.data[3].coord[0] = (alarmOrEvent as IAlarm)[ - 'lastUpdated' - ]; - } + const newAlarmSeries = + this.echartsOptionsService.getAlarmOrEventSeries( + dp, + renderType, + false, + [alarmOrEvent], + 'alarm', + displayOptions, + (alarmOrEvent as IAlarm).creationTime + ); + allDataSeries.push(...newAlarmSeries); } else { const newAlarmSeries = this.echartsOptionsService.getAlarmOrEventSeries( diff --git a/src/app/datapoints-graph/charts/chart.model.ts b/src/app/datapoints-graph/charts/chart.model.ts index 6b3aaca1..4f990423 100644 --- a/src/app/datapoints-graph/charts/chart.model.ts +++ b/src/app/datapoints-graph/charts/chart.model.ts @@ -8,6 +8,8 @@ import { TopLevelFormatterParams } from 'echarts/types/src/component/tooltip/Too interface ModifiedCustomSeriesOptions extends echarts.EChartsOption { // typeOfSeries is used for formatter to distinguish between events/alarms series typeOfSeries?: 'alarm' | 'event' | null; + // isDpTemplateSelected is used to distinguish if the series have a specific dp template selected. E.g. for case when a device has 2 measurements + isDpTemplateSelected?: boolean; id: string; data: SeriesValue[]; itemStyle: { color: string }; diff --git a/src/app/datapoints-graph/charts/charts.component.ts b/src/app/datapoints-graph/charts/charts.component.ts index f8f0850a..386c0e4d 100644 --- a/src/app/datapoints-graph/charts/charts.component.ts +++ b/src/app/datapoints-graph/charts/charts.component.ts @@ -558,6 +558,7 @@ export class ChartsComponent implements OnChanges, OnInit, OnDestroy { { displayMarkedLine: this.config.displayMarkedLine || false, displayMarkedPoint: this.config.displayMarkedPoint || false, + mergeMatchingDatapoints: this.config.mergeMatchingDatapoints || false, } ); } diff --git a/src/app/datapoints-graph/charts/echarts-options.service.spec.ts b/src/app/datapoints-graph/charts/echarts-options.service.spec.ts index 0b7b51f7..56088ea2 100644 --- a/src/app/datapoints-graph/charts/echarts-options.service.spec.ts +++ b/src/app/datapoints-graph/charts/echarts-options.service.spec.ts @@ -102,7 +102,11 @@ describe('EchartsOptionsService', () => { }, [], [], - { displayMarkedLine: true, displayMarkedPoint: true } + { + displayMarkedLine: true, + displayMarkedPoint: true, + mergeMatchingDatapoints: true, + } ); // then expect(JSON.stringify(options)).toBe( @@ -188,7 +192,11 @@ describe('EchartsOptionsService', () => { }, [], [], - { displayMarkedLine: true, displayMarkedPoint: true } + { + displayMarkedLine: true, + displayMarkedPoint: true, + mergeMatchingDatapoints: true, + } ); // then expect(options.grid).toEqual({ @@ -215,7 +223,11 @@ describe('EchartsOptionsService', () => { }, [], [], - { displayMarkedLine: true, displayMarkedPoint: true } + { + displayMarkedLine: true, + displayMarkedPoint: true, + mergeMatchingDatapoints: true, + } ); // then expect(options.grid).toEqual({ @@ -248,7 +260,11 @@ describe('EchartsOptionsService', () => { }, [], [], - { displayMarkedLine: true, displayMarkedPoint: true } + { + displayMarkedLine: true, + displayMarkedPoint: true, + mergeMatchingDatapoints: true, + } ); // then expect(options.grid).toEqual({ @@ -273,7 +289,11 @@ describe('EchartsOptionsService', () => { }, [], [], - { displayMarkedLine: true, displayMarkedPoint: true } + { + displayMarkedLine: true, + displayMarkedPoint: true, + mergeMatchingDatapoints: true, + } ).dataZoom as DataZoomComponentOption; // then expect(dataZoomOptions.filterMode).toBe('filter'); @@ -290,7 +310,11 @@ describe('EchartsOptionsService', () => { }, [], [], - { displayMarkedLine: true, displayMarkedPoint: true } + { + displayMarkedLine: true, + displayMarkedPoint: true, + mergeMatchingDatapoints: true, + } ).dataZoom as DataZoomComponentOption; // then expect(dataZoomOptions.filterMode).toBe('none'); @@ -307,7 +331,11 @@ describe('EchartsOptionsService', () => { }, [], [], - { displayMarkedLine: true, displayMarkedPoint: true } + { + displayMarkedLine: true, + displayMarkedPoint: true, + mergeMatchingDatapoints: true, + } ).xAxis as XAXisOption; // then expect(xAxisOptions.axisLine?.onZeroAxisIndex).toBe(-1); @@ -324,7 +352,11 @@ describe('EchartsOptionsService', () => { }, [], [], - { displayMarkedLine: true, displayMarkedPoint: true } + { + displayMarkedLine: true, + displayMarkedPoint: true, + mergeMatchingDatapoints: true, + } ).xAxis as XAXisOption; // then expect(xAxisOptions.axisLine?.onZeroAxisIndex).toBe(1); @@ -357,7 +389,11 @@ describe('EchartsOptionsService', () => { }, [], [], - { displayMarkedLine: true, displayMarkedPoint: true } + { + displayMarkedLine: true, + displayMarkedPoint: true, + mergeMatchingDatapoints: true, + } ).tooltip as TooltipOption ).formatter as TooltipFormatterCallback; // when @@ -404,7 +440,11 @@ describe('EchartsOptionsService', () => { }, [], [], - { displayMarkedLine: true, displayMarkedPoint: true } + { + displayMarkedLine: true, + displayMarkedPoint: true, + mergeMatchingDatapoints: true, + } ).tooltip as TooltipOption ).formatter as TooltipFormatterCallback; // when @@ -465,7 +505,11 @@ describe('EchartsOptionsService', () => { }, [], [], - { displayMarkedLine: true, displayMarkedPoint: true } + { + displayMarkedLine: true, + displayMarkedPoint: true, + mergeMatchingDatapoints: true, + } ).tooltip as TooltipOption ).formatter as TooltipFormatterCallback; // when @@ -509,7 +553,11 @@ describe('EchartsOptionsService', () => { }, [], [], - { displayMarkedLine: true, displayMarkedPoint: true } + { + displayMarkedLine: true, + displayMarkedPoint: true, + mergeMatchingDatapoints: true, + } ).tooltip as TooltipOption ).formatter as TooltipFormatterCallback; // when @@ -539,7 +587,11 @@ describe('EchartsOptionsService', () => { }, [], [], - { displayMarkedLine: true, displayMarkedPoint: true } + { + displayMarkedLine: true, + displayMarkedPoint: true, + mergeMatchingDatapoints: true, + } ).series as SeriesOption[]; // then expect(series).toHaveLength(1); @@ -570,7 +622,11 @@ describe('EchartsOptionsService', () => { }, [], [], - { displayMarkedLine: true, displayMarkedPoint: true } + { + displayMarkedLine: true, + displayMarkedPoint: true, + mergeMatchingDatapoints: true, + } ).series as SeriesOption[]; // then expect(series).toHaveLength(2); @@ -609,7 +665,11 @@ describe('EchartsOptionsService', () => { }, [], [], - { displayMarkedLine: true, displayMarkedPoint: true } + { + displayMarkedLine: true, + displayMarkedPoint: true, + mergeMatchingDatapoints: true, + } ).series as SeriesOption[]; // then expect(series).toHaveLength(2); diff --git a/src/app/datapoints-graph/charts/echarts-options.service.ts b/src/app/datapoints-graph/charts/echarts-options.service.ts index ddb7b590..ca0ad4bc 100644 --- a/src/app/datapoints-graph/charts/echarts-options.service.ts +++ b/src/app/datapoints-graph/charts/echarts-options.service.ts @@ -25,6 +25,7 @@ import { CustomSeriesOptions } from './chart.model'; import { AlarmSeverityToIconPipe } from '../alarms-filtering/alarm-severity-to-icon.pipe'; import { AlarmSeverityToLabelPipe } from '../alarms-filtering/alarm-severity-to-label.pipe'; import { Router } from '@angular/router'; +import { max } from 'rxjs'; type TooltipPositionCallback = ( point: [number, number], // position of mouse in chart [X, Y]; 0,0 is top left corner @@ -58,10 +59,15 @@ export class EchartsOptionsService { showSplitLines: { YAxis: boolean; XAxis: boolean }, events: IEvent[], alarms: IAlarm[], - displayOptions: { displayMarkedLine: boolean; displayMarkedPoint: boolean } + displayOptions: { + displayMarkedLine: boolean; + displayMarkedPoint: boolean; + mergeMatchingDatapoints: boolean; + } ): EChartsOption { const yAxis = this.yAxisService.getYAxis(datapointsWithValues, { showSplitLines: showSplitLines.YAxis, + mergeMatchingDatapoints: displayOptions.mergeMatchingDatapoints, }); const leftAxis = yAxis.filter((yx) => yx.position === 'left'); const gridLeft = leftAxis.length @@ -193,9 +199,18 @@ export class EchartsOptionsService { 'markLineFlag', ]); + // Is a specific datapoint template selected for this alarm/event type? + let isDpTemplateSelected = false; + // MarkPoint data const markPointData = itemsOfType.reduce( (acc, item) => { + isDpTemplateSelected = + item['selectedDatapoint'] && + dp['__target'] && + item['selectedDatapoint']['fragment'] === dp['fragment'] && + item['selectedDatapoint']['series'] === dp['series'] && + item['selectedDatapoint']['target'] === dp['__target']['id']; if (dp.__target?.id === item.source.id) { const isCleared = isAlarm && item.status === AlarmStatus.CLEARED; const isEvent = !isAlarm; @@ -225,7 +240,9 @@ export class EchartsOptionsService { name: `${type}-markPoint`, typeOfSeries: itemType, data: mainData, + isDpTemplateSelected, silent: true, + position: 'bottom', markPoint: { showSymbol: true, symbolKeepAspect: true, @@ -238,16 +255,19 @@ export class EchartsOptionsService { ), }; + const markLineData = this.createMarkLine(itemsOfType); + // Construct series with markLine const seriesWithMarkLine = { id: `${type}/${dp.__target?.id}+${id ? id : ''}-markLine`, name: `${type}-markLine`, typeOfSeries: itemType, data: mainData, + isDpTemplateSelected, markLine: { showSymbol: false, symbol: ['none', 'none'], // no symbol at the start/end of the line - data: this.createMarkLine(itemsOfType), + data: markLineData, }, ...this.chartTypesService.getSeriesOptions( dp, @@ -549,10 +569,10 @@ export class EchartsOptionsService { events: IEvent[], alarms: IAlarm[], displayOptions: { displayMarkedLine: boolean; displayMarkedPoint: boolean } - ): SeriesOption[] { + ): CustomSeriesOptions[] | SeriesOption[] { const series: SeriesOption[] = []; - let eventSeries: SeriesOption[] = []; - let alarmSeries: SeriesOption[] = []; + let eventSeries: CustomSeriesOptions[] = []; + let alarmSeries: CustomSeriesOptions[] = []; datapointsWithValues.forEach((dp, idx) => { const renderType: DatapointChartRenderType = dp.renderType || 'min'; if (renderType === 'area') { @@ -582,11 +602,20 @@ export class EchartsOptionsService { alarmSeries = [...alarmSeries, ...newAlarmSeries]; }); const deduplicateFilterCallback = ( - obj1: SeriesOption, + obj1: CustomSeriesOptions, i: number, - arr: SeriesOption[] - ): obj1 is SeriesOption => - arr.findIndex((obj2) => obj2['id'] === obj1['id']) === i; + arr: CustomSeriesOptions[] + ): obj1 is CustomSeriesOptions => { + const duplicates = arr.filter( + (obj2) => obj1['id'] === obj2['id'] && i !== arr.indexOf(obj2) + ); + + if (duplicates.length > 0) { + return obj1['isDpTemplateSelected'] as boolean; + } + + return true; + }; const deduplicatedEvents = eventSeries.filter(deduplicateFilterCallback); const deduplicatedAlarms = alarmSeries.filter(deduplicateFilterCallback); return [...series, ...deduplicatedEvents, ...deduplicatedAlarms]; @@ -643,6 +672,19 @@ export class EchartsOptionsService { }); } + private getClosestDpValueToTargetTime( + dpValuesArray: DpValuesItem[], + targetTime: number + ): DpValuesItem { + return dpValuesArray.reduce((prev, curr) => + //should take the value closest to the target time, for realtime the current time would always change + Math.abs(new Date(curr.time).getTime() - targetTime) < + Math.abs(new Date(prev.time).getTime() - targetTime) + ? curr + : prev + ); + } + /** * This method creates a markPoint on the chart which represents the icon of the alarm or event. * @param item Single alarm or event @@ -657,7 +699,8 @@ export class EchartsOptionsService { isCleared: boolean, isEvent: boolean ): MarkPointData[] { - if (!item.creationTime) { + // check if dp.values object is empty + if (!item.creationTime || Object.keys(dp.values).length === 0) { return []; } @@ -672,18 +715,30 @@ export class EchartsOptionsService { dpValuesArray, creationTime ); + const dpValuesForNewAlarms = this.getClosestDpValueToTargetTime( + dpValuesArray, + creationTime + ); const lastUpdatedTime = new Date(item['lastUpdated']).getTime(); const closestDpValueLastUpdated = this.interpolateBetweenTwoDps( dpValuesArray, lastUpdatedTime ); + const dpValuesForNewAlarmsLastUpdated = this.getClosestDpValueToTargetTime( + dpValuesArray, + lastUpdatedTime + ); if (isEvent) { return [ { coord: [ item.creationTime, - closestDpValue?.values[0]?.min ?? closestDpValue?.values[1] ?? null, + closestDpValue?.values[0]?.min ?? + closestDpValue?.values[1] ?? + dpValuesForNewAlarms?.values[0]?.min ?? + dpValuesForNewAlarms?.values[1] ?? + null, ], name: item.type, itemType: item.type, @@ -696,7 +751,11 @@ export class EchartsOptionsService { { coord: [ item.creationTime, - closestDpValue?.values[0]?.min ?? closestDpValue?.values[1] ?? null, + closestDpValue?.values[0]?.min ?? + closestDpValue?.values[1] ?? + dpValuesForNewAlarms?.values[0]?.min ?? + dpValuesForNewAlarms?.values[1] ?? + null, ], name: item.type, itemType: item.type, @@ -714,6 +773,8 @@ export class EchartsOptionsService { item.creationTime, closestDpValue?.values[0]?.min ?? closestDpValue?.values[1] ?? + dpValuesForNewAlarms?.values[0]?.min ?? + dpValuesForNewAlarms?.values[1] ?? null, ], name: item.type, @@ -729,6 +790,8 @@ export class EchartsOptionsService { item.creationTime, closestDpValue?.values[0]?.min ?? closestDpValue?.values[1] ?? + dpValuesForNewAlarms?.values[0]?.min ?? + dpValuesForNewAlarms?.values[1] ?? null, ], name: item.type, @@ -742,6 +805,8 @@ export class EchartsOptionsService { item['lastUpdated'], closestDpValueLastUpdated?.values[0]?.min ?? closestDpValueLastUpdated?.values[1] ?? + dpValuesForNewAlarmsLastUpdated?.values[0]?.min ?? + dpValuesForNewAlarmsLastUpdated?.values[1] ?? null, ], name: item.type, @@ -757,6 +822,8 @@ export class EchartsOptionsService { item['lastUpdated'], closestDpValueLastUpdated?.values[0]?.min ?? closestDpValueLastUpdated?.values[1] ?? + dpValuesForNewAlarmsLastUpdated?.values[0]?.min ?? + dpValuesForNewAlarmsLastUpdated?.values[1] ?? null, ], name: item.type, @@ -772,6 +839,8 @@ export class EchartsOptionsService { item.creationTime, closestDpValue?.values[0]?.min ?? closestDpValue?.values[1] ?? + dpValuesForNewAlarms?.values[0]?.min ?? + dpValuesForNewAlarms?.values[1] ?? null, ], name: item.type, @@ -787,6 +856,8 @@ export class EchartsOptionsService { item.creationTime, closestDpValue?.values[0]?.min ?? closestDpValue?.values[1] ?? + dpValuesForNewAlarms?.values[0]?.min ?? + dpValuesForNewAlarms?.values[1] ?? null, ], name: item.type, @@ -800,6 +871,8 @@ export class EchartsOptionsService { item['lastUpdated'], closestDpValueLastUpdated?.values[0]?.min ?? closestDpValueLastUpdated?.values[1] ?? + dpValuesForNewAlarmsLastUpdated?.values[0]?.min ?? + dpValuesForNewAlarmsLastUpdated?.values[1] ?? null, ], name: item.type, @@ -815,6 +888,8 @@ export class EchartsOptionsService { item['lastUpdated'], closestDpValueLastUpdated?.values[0]?.min ?? closestDpValueLastUpdated?.values[1] ?? + dpValuesForNewAlarmsLastUpdated?.values[0]?.min ?? + dpValuesForNewAlarmsLastUpdated?.values[1] ?? null, ], name: item.type, @@ -827,13 +902,11 @@ export class EchartsOptionsService { } /** - * This method creates a markLine on the chart which represents the line between every alarm or event on the chart. + * This method creates a markLine on the chart which represents the line between every alarm or eventon the chart. * @param items Array of alarms or events * @returns MarkLineDataItemOptionBase[] */ - private createMarkLine( - items: T[] - ): MarkLineData[] { + private createMarkLine(items: T[]): any[] { return items.reduce((acc, item) => { if (!item.creationTime) { return acc; diff --git a/src/app/datapoints-graph/charts/y-axis.service.spec.ts b/src/app/datapoints-graph/charts/y-axis.service.spec.ts index 33fc7768..9ca12617 100644 --- a/src/app/datapoints-graph/charts/y-axis.service.spec.ts +++ b/src/app/datapoints-graph/charts/y-axis.service.spec.ts @@ -15,6 +15,8 @@ describe('YAxisService', () => { __target: { id: 1 }, color: 'blue', values: {}, + label: 'DP1', + unit: 'C', }; const dp2: DatapointWithValues = { lineType: 'line', @@ -54,6 +56,7 @@ describe('YAxisService', () => { // given const YAxis: YAXisOption = service.getYAxis([dp1], { showSplitLines: false, + mergeMatchingDatapoints: true, })[0]; // when const YAxisValue = (YAxis.axisLabel as any).formatter(1_400_000_000); @@ -63,10 +66,19 @@ describe('YAxisService', () => { it('should return Y axis option with generic values', () => { // when - const YAxis = service.getYAxis([dp1], { showSplitLines: false })[0]; + const YAxis = service.getYAxis([dp1], { + showSplitLines: false, + mergeMatchingDatapoints: false, + })[0]; // then expect(JSON.stringify(YAxis)).toEqual( JSON.stringify({ + name: 'DP1 [C]', + nameLocation: 'middle', + nameGap: 20, + nameTextStyle: { + rich: {}, + }, type: 'value', animation: true, axisLine: { @@ -78,6 +90,7 @@ describe('YAxisService', () => { }, axisLabel: { fontSize: 10, + show: true, formatter: (val) => new Intl.NumberFormat('en-GB', { notation: 'compact', @@ -107,6 +120,7 @@ describe('YAxisService', () => { // when const YAxis = service.getYAxis([{ ...dp1, min: -100, max: 100 }], { showSplitLines: false, + mergeMatchingDatapoints: true, })[0]; // then expect(YAxis.min).toEqual(-100); @@ -118,6 +132,7 @@ describe('YAxisService', () => { // when const YAxis = service.getYAxis([dp1, dp2, dp3], { showSplitLines: false, + mergeMatchingDatapoints: true, }); // then expect(YAxis[0].position).toBe('left'); @@ -132,7 +147,7 @@ describe('YAxisService', () => { // when const YAxis = service.getYAxis( [{ ...dp1, yAxisType: 'left' }, { ...dp2, yAxisType: 'left' }, dp3], - { showSplitLines: false } + { showSplitLines: false, mergeMatchingDatapoints: true } ); // then expect(YAxis[0].position).toBe('left'); @@ -151,7 +166,7 @@ describe('YAxisService', () => { { ...dp2, yAxisType: 'right' }, { ...dp3, yAxisType: 'left' }, ], - { showSplitLines: false } + { showSplitLines: false, mergeMatchingDatapoints: true } ); // then expect(YAxis[0].position).toBe('left'); diff --git a/src/app/datapoints-graph/charts/y-axis.service.ts b/src/app/datapoints-graph/charts/y-axis.service.ts index 5999f88b..35e96769 100644 --- a/src/app/datapoints-graph/charts/y-axis.service.ts +++ b/src/app/datapoints-graph/charts/y-axis.service.ts @@ -22,42 +22,101 @@ export class YAxisService { { position: DatapointAxisType; offset: number } > = this.getYAxisPlacement(datapointsWithValues); - return datapointsWithValues.map((dp) => ({ - type: 'value', - animation: true, - axisLine: { - show: true, - lineStyle: { - color: dp.color, + const matchingDpSet = new Set(); + const firstOccurrence = new Set(); + + return datapointsWithValues.map((dp, index) => { + const matchingDpRange = datapointsWithValues.some( + (dp2, index2) => + dp2.min === dp.min && dp2.max === dp.max && index2 < index + ); + + const anyMatchingDp = datapointsWithValues.some( + (dp2, index2) => + dp2.min === dp.min && dp2.max === dp.max && index2 !== index + ); + + if ( + anyMatchingDp && + !matchingDpRange && + YAxisOptions.mergeMatchingDatapoints && + !firstOccurrence.has(dp) + ) { + firstOccurrence.add(dp); + } + + if (firstOccurrence.has(dp)) { + datapointsWithValues.forEach((dp2) => { + if (dp2.min === dp.min && dp2.max === dp.max) { + matchingDpSet.add(dp2); + } + }); + } + + return { + name: YAxisOptions.mergeMatchingDatapoints + ? firstOccurrence.has(dp) + ? Array.from(matchingDpSet) + .map((dp) => `{${dp.__target?.id}${dp.unit}|${dp.unit}}`) + .join(' /') + : matchingDpRange + ? '' + : `${dp.label} [${dp.unit}]` + : `${dp.label} [${dp.unit}]`, + nameLocation: 'middle', + nameGap: 20, + nameTextStyle: { + // add rich text to support multiple colors for different dp units + rich: { + ...Array.from(matchingDpSet).reduce((acc, dp) => { + const accKey = `${dp.__target?.id}${dp.unit}`; + acc[accKey] = { + color: dp.color, + }; + return acc; + }, {}), + }, }, - onZero: false, - }, - axisLabel: { - fontSize: 10, - formatter: (val) => - new Intl.NumberFormat(this.intlNumberFormatCompliantLocale, { - notation: 'compact', - compactDisplay: 'short', - }).format(val), - }, - splitLine: { - show: YAxisOptions.showSplitLines, - lineStyle: { color: dp.color, opacity: 0.4, type: 'dashed' }, - }, - position: YAxisPlacement.get(dp).position, - offset: YAxisPlacement.get(dp).offset, - axisTick: { - show: true, - }, - axisPointer: { - show: false, - label: { + type: 'value', + animation: true, + axisLine: { + show: + matchingDpRange && YAxisOptions.mergeMatchingDatapoints + ? false + : true, + lineStyle: { + color: firstOccurrence.has(dp) ? 'black' : dp.color, + }, + onZero: false, + }, + axisLabel: { + fontSize: 10, + show: !matchingDpRange, + formatter: (val) => + new Intl.NumberFormat(this.intlNumberFormatCompliantLocale, { + notation: 'compact', + compactDisplay: 'short', + }).format(val), + }, + splitLine: { + show: YAxisOptions.showSplitLines && !matchingDpRange, + lineStyle: { color: dp.color, opacity: 0.4, type: 'dashed' }, + }, + position: YAxisPlacement.get(dp)?.position, + offset: YAxisPlacement.get(dp)?.offset, + axisTick: { + show: !matchingDpRange, + }, + axisPointer: { show: false, + label: { + show: false, + }, }, - }, - ...(dp.min && { min: dp.min }), - ...(dp.max && { max: dp.max }), - })); + ...(dp.min && { min: dp.min }), + ...(dp.max && { max: dp.max }), + }; + }); } private getYAxisPlacement( diff --git a/src/app/datapoints-graph/datapoints-graph-config/datapoints-graph-widget-config.component.html b/src/app/datapoints-graph/datapoints-graph-config/datapoints-graph-widget-config.component.html index c255007c..33a9a38e 100644 --- a/src/app/datapoints-graph/datapoints-graph-config/datapoints-graph-widget-config.component.html +++ b/src/app/datapoints-graph/datapoints-graph-config/datapoints-graph-widget-config.component.html @@ -48,7 +48,7 @@
+
@@ -134,6 +143,7 @@ name="alarms" class="bg-level-1 separator-bottom d-block" [timelineType]="'ALARM'" + [config]="config" > diff --git a/src/app/datapoints-graph/datapoints-graph-config/datapoints-graph-widget-config.component.spec.ts b/src/app/datapoints-graph/datapoints-graph-config/datapoints-graph-widget-config.component.spec.ts index 65c96bf8..93b898be 100644 --- a/src/app/datapoints-graph/datapoints-graph-config/datapoints-graph-widget-config.component.spec.ts +++ b/src/app/datapoints-graph/datapoints-graph-config/datapoints-graph-widget-config.component.spec.ts @@ -137,6 +137,7 @@ describe('DatapointsGraphWidgetConfigComponent', () => { displayDateSelection: false, displayMarkedLine: true, displayMarkedPoint: true, + mergeMatchingDatapoints: true, interval: 'hours', realtime: false, widgetInstanceGlobalTimeContext: false, @@ -235,6 +236,7 @@ describe('DatapointsGraphWidgetConfigComponent', () => { displayMarkedLine: true, displayMarkedPoint: true, interval: 'hours', + mergeMatchingDatapoints: true, realtime: false, widgetInstanceGlobalTimeContext: false, canDecoupleGlobalTimeContext: false, diff --git a/src/app/datapoints-graph/datapoints-graph-config/datapoints-graph-widget-config.component.ts b/src/app/datapoints-graph/datapoints-graph-config/datapoints-graph-widget-config.component.ts index 24d9851b..dd28c323 100644 --- a/src/app/datapoints-graph/datapoints-graph-config/datapoints-graph-widget-config.component.ts +++ b/src/app/datapoints-graph/datapoints-graph-config/datapoints-graph-widget-config.component.ts @@ -199,6 +199,7 @@ export class DatapointsGraphWidgetConfigComponent events: [[] as EventDetails[]], displayMarkedLine: [true, []], displayMarkedPoint: [true, []], + mergeMatchingDatapoints: [true, []], displayDateSelection: [false, []], displayAggregationSelection: [false, []], widgetInstanceGlobalTimeContext: [false, []], diff --git a/src/app/datapoints-graph/datapoints-graph-view/chart-alarms.service.ts b/src/app/datapoints-graph/datapoints-graph-view/chart-alarms.service.ts index 9a0585c3..8ba8cad3 100644 --- a/src/app/datapoints-graph/datapoints-graph-view/chart-alarms.service.ts +++ b/src/app/datapoints-graph/datapoints-graph-view/chart-alarms.service.ts @@ -41,6 +41,7 @@ export class ChartAlarmsService { return this.alarmService.list(fetchOptions).then((result) => { result.data.forEach((iAlarm) => { iAlarm['color'] = alarm.color; + iAlarm['selectedDatapoint'] = alarm.selectedDatapoint; }); return result.data; }); diff --git a/src/app/datapoints-graph/datapoints-graph-view/chart-events.service.ts b/src/app/datapoints-graph/datapoints-graph-view/chart-events.service.ts index b03d64ee..02dc45a7 100644 --- a/src/app/datapoints-graph/datapoints-graph-view/chart-events.service.ts +++ b/src/app/datapoints-graph/datapoints-graph-view/chart-events.service.ts @@ -27,6 +27,7 @@ export class ChartEventsService { return this.eventService.list(fetchOptions).then((result) => { result.data.forEach((iEvent) => { iEvent['color'] = event.color; + iEvent['selectedDatapoint'] = event.selectedDatapoint; }); return result.data; }); diff --git a/src/app/datapoints-graph/datapoints-graph-view/datapoints-graph-widget-view.component.html b/src/app/datapoints-graph/datapoints-graph-view/datapoints-graph-widget-view.component.html index f7ff8bb3..58f69d1c 100644 --- a/src/app/datapoints-graph/datapoints-graph-view/datapoints-graph-widget-view.component.html +++ b/src/app/datapoints-graph/datapoints-graph-view/datapoints-graph-widget-view.component.html @@ -105,7 +105,7 @@ title="{{ datapoint.label }} - {{ datapoint.__target.name }}" > dp.__active).length === 1 + this.displayConfig?.datapoints?.filter((dp) => dp.__active).length === + 1 && + datapoint.__active ) { // at least 1 datapoint should be active this.hasAtleastOneDatapointActive = false; return; } datapoint.__active = !datapoint.__active; + this.hasAtleastOneDatapointActive = true; this.displayConfig = { ...this.displayConfig }; } diff --git a/src/app/datapoints-graph/datapoints-graph-widget.module.ts b/src/app/datapoints-graph/datapoints-graph-widget.module.ts index 7181fef9..4a95c8e0 100644 --- a/src/app/datapoints-graph/datapoints-graph-widget.module.ts +++ b/src/app/datapoints-graph/datapoints-graph-widget.module.ts @@ -48,7 +48,7 @@ async function loadConfigComponent() { settings: { noNewWidgets: false, widgetDefaults: { - _width: 4, + _width: 8, _height: 4, }, noDeviceTarget: true, diff --git a/src/app/datapoints-graph/model/datapoints-graph-widget.model.ts b/src/app/datapoints-graph/model/datapoints-graph-widget.model.ts index 008bc9db..bb7c719f 100644 --- a/src/app/datapoints-graph/model/datapoints-graph-widget.model.ts +++ b/src/app/datapoints-graph/model/datapoints-graph-widget.model.ts @@ -29,6 +29,7 @@ export type DatapointsGraphWidgetConfig = { widgetInstanceGlobalTimeContext?: boolean | null; displayMarkedLine?: boolean; displayMarkedPoint?: boolean; + mergeMatchingDatapoints?: boolean; dateFrom?: Date | null; dateTo?: Date | null; activeAlarmTypesOutOfRange?: string[]; @@ -188,6 +189,7 @@ export type DatapointRealtimeMeasurements = { export type YAxisOptions = { showSplitLines: boolean; + mergeMatchingDatapoints: boolean; }; export interface SeriesDatapointInfo {