@c8y/ngx-components
Version:
Angular modules for Cumulocity IoT applications
873 lines • 134 kB
JavaScript
/* eslint-disable @typescript-eslint/no-explicit-any */
import { Injectable } from '@angular/core';
import { DatePipe, gettext } from '@c8y/ngx-components';
import { YAxisService } from './y-axis.service';
import { ChartTypesService } from './chart-types.service';
import { AlarmStatus } from '@c8y/client';
import { ICONS_MAP } from '../models/svg-icons.model';
import { Router } from '@angular/router';
import { AlarmSeverityToIconPipe, AlarmSeverityToLabelPipe } from '@c8y/ngx-components/alarms';
import { INTERVALS } from '@c8y/ngx-components/interval-picker';
import { TranslateService } from '@ngx-translate/core';
import * as i0 from "@angular/core";
import * as i1 from "@c8y/ngx-components";
import * as i2 from "./y-axis.service";
import * as i3 from "./chart-types.service";
import * as i4 from "@c8y/ngx-components/alarms";
import * as i5 from "@ngx-translate/core";
import * as i6 from "@angular/router";
const INDEX_HTML = '/index.html';
export class EchartsOptionsService {
constructor(datePipe, yAxisService, chartTypesService, severityIconPipe, severityLabelPipe, translate, router) {
this.datePipe = datePipe;
this.yAxisService = yAxisService;
this.chartTypesService = chartTypesService;
this.severityIconPipe = severityIconPipe;
this.severityLabelPipe = severityLabelPipe;
this.translate = translate;
this.router = router;
this.TOOLTIP_WIDTH = 300;
}
getChartOptions(datapointsWithValues, timeRange, showSplitLines, events, alarms, displayOptions, selectedTimeRange, aggregatedDatapoints, sliderZoomUsed = false) {
const yAxis = this.yAxisService.getYAxis(datapointsWithValues, {
showSplitLines: showSplitLines.YAxis,
mergeMatchingDatapoints: displayOptions.mergeMatchingDatapoints,
showLabelAndUnit: displayOptions.showLabelAndUnit
});
const leftAxis = yAxis.filter(yx => yx.position === 'left');
const gridLeft = leftAxis.length ? leftAxis.length * this.yAxisService.Y_AXIS_OFFSET : 16;
const rightAxis = yAxis.filter(yx => yx.position === 'right');
const gridRight = rightAxis.length ? rightAxis.length * this.yAxisService.Y_AXIS_OFFSET : 16;
let intervalInMs = this.calculateExtendedIntervalInMs(selectedTimeRange?.interval || timeRange.interval || 'hours', selectedTimeRange || timeRange);
if (sliderZoomUsed) {
intervalInMs = this.calculateExtendedIntervalInMs(timeRange.interval || 'hours', timeRange);
}
return {
grid: {
containLabel: false, // axis labels are not taken into account to calculate graph grid
left: gridLeft,
top: 16,
right: gridRight,
bottom: 68
},
dataZoom: [
{
type: 'inside',
// TODO: use 'none' only when this bug is fixed https://github.com/apache/echarts/issues/17858
filterMode: datapointsWithValues.some(dp => dp.lineType === 'bars') ? 'filter' : 'none',
zoomOnMouseWheel: true,
startValue: selectedTimeRange
? selectedTimeRange.dateFrom.valueOf()
: timeRange.dateFrom.valueOf(),
endValue: selectedTimeRange
? selectedTimeRange.dateTo.valueOf()
: timeRange.dateTo.valueOf()
},
{
type: 'slider',
show: displayOptions.showSlider,
bottom: 8,
realtime: false
}
], // on realtime, 'none' will cause extending chart line to left edge of the chart
animation: false,
toolbox: {
show: true,
itemSize: 0, // toolbox is needed for zooming in action, but we provide our own buttons
feature: {
dataZoom: {
yAxisIndex: 'none'
}
}
},
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'cross',
snap: true
},
backgroundColor: 'rgba(255, 255, 255, 0.9)',
formatter: this.getTooltipFormatter(),
appendToBody: true,
position: this.tooltipPosition(),
transitionDuration: 0
},
legend: {
show: false
},
xAxis: {
min: new Date(timeRange.dateFrom).valueOf() - intervalInMs,
max: timeRange.dateTo,
type: 'time',
animation: false,
axisPointer: {
label: {
show: false
}
},
axisLine: {
// align X axis to 0 of Y axis of datapoint with lineType 'bars'
onZeroAxisIndex: datapointsWithValues.findIndex(dp => dp.lineType === 'bars')
},
axisLabel: {
hideOverlap: true,
borderWidth: 2, // as there is no margin for labels spacing, transparent border is a workaround
borderColor: 'transparent'
},
splitLine: {
show: showSplitLines.XAxis,
lineStyle: { opacity: 0.8, type: 'dashed', width: 2 }
}
},
yAxis,
series: [
...this.getAggregatedSeries(aggregatedDatapoints || []).filter(series => series !== undefined),
...this.getChartSeries(datapointsWithValues, events, alarms, displayOptions)
]
};
}
calculateExtendedIntervalInMs(interval, selectedTimeRange) {
let intervalInMs = INTERVALS.find(i => i.id === interval).timespanInMs;
switch (interval) {
case 'minutes':
intervalInMs = INTERVALS.find(i => i.id === interval).timespanInMs * 60;
break;
case 'hours':
intervalInMs = INTERVALS.find(i => i.id === interval).timespanInMs * 24;
break;
case 'days':
intervalInMs = INTERVALS.find(i => i.id === interval).timespanInMs * 28;
break;
case 'weeks':
intervalInMs = INTERVALS.find(i => i.id === interval).timespanInMs * 12;
break;
case 'months':
intervalInMs = INTERVALS.find(i => i.id === interval).timespanInMs * 12;
break;
case 'custom':
intervalInMs =
(new Date(selectedTimeRange.dateTo).valueOf() -
new Date(selectedTimeRange.dateFrom).valueOf()) *
12;
break;
default:
intervalInMs = INTERVALS.find(i => i.id === interval).timespanInMs;
break;
}
return intervalInMs;
}
getAggregatedSeries(aggregatedDatapoints) {
const series = [];
aggregatedDatapoints.forEach((dp, idx) => {
const renderType = dp.renderType || 'min';
if (renderType === 'area') {
series.push(this.getSingleSeries(dp, 'min', idx, true, 'aggr'));
series.push(this.getSingleSeries(dp, 'max', idx, true, 'aggr'));
}
else {
series.push(this.getSingleSeries(dp, renderType, idx, false, 'aggr'));
}
});
series.forEach((s) => {
s.datapointId = 'aggregated';
s.typeOfSeries = 'fake';
s.itemStyle = {
...s.itemStyle,
opacity: 0
};
s.lineStyle = {
...s.lineStyle,
opacity: 0
};
});
return [series[0]];
}
/**
* This method is used to get the series for alarms and events.
* @param dp - The data point.
* @param renderType - The render type.
* @param isMinMaxChart - If the chart is min max chart.
* @param items - All alarms or events which should be displayed on the chart.
* @param itemType - The item type.
* @param id - The id of the device
*/
getAlarmOrEventSeries(dp, renderType, isMinMaxChart = false, items = [], itemType = 'alarm', displayOptions = { displayMarkedLine: true, displayMarkedPoint: true }, id, idx, realtime) {
if (!items.length) {
return [];
}
if (!displayOptions.displayMarkedLine && !displayOptions.displayMarkedPoint) {
return [];
}
//filter items that are not __hidden
const filteredItems = items.filter(item => !item['__hidden']);
const itemsByType = this.groupByType(filteredItems, 'type');
const isAlarm = itemType === 'alarm';
return Object.entries(itemsByType).flatMap(([type, itemsOfType]) => {
// Main series data
const mainData = itemsOfType.map(item => [item.creationTime, null, '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;
return acc.concat(this.createMarkPoint(item, dp, isCleared, isEvent, realtime));
}
else {
if (!item.creationTime) {
return [];
}
return acc.concat([
{
coord: [item.creationTime, null],
name: item.type,
itemType: item.type,
itemStyle: { color: item['color'] }
}
]);
}
}, []);
// Construct series with markPoint
const seriesWithMarkPoint = {
id: `${type}/${dp.__target?.id}+${id ? id : ''}-markPoint`,
name: `${type}-markPoint`,
typeOfSeries: itemType,
data: mainData,
isDpTemplateSelected,
position: 'bottom',
silent: true,
markPoint: {
showSymbol: true,
symbolKeepAspect: true,
data: markPointData
},
yAxisIndex: idx,
...this.chartTypesService.getSeriesOptions(dp, isMinMaxChart, renderType)
};
const markLineData = this.createMarkLine(itemsOfType, itemType);
// Construct series with markLine
const seriesWithMarkLine = {
id: `${type}/${dp.__target?.id}+${id ? id : ''}-markLine`,
name: `${type}-markLine`,
typeOfSeries: itemType,
isDpTemplateSelected,
data: mainData,
markLine: {
showSymbol: false,
symbol: ['none', 'none'], // no symbol at the start/end of the line
data: markLineData
},
...this.chartTypesService.getSeriesOptions(dp, isMinMaxChart, renderType)
};
//depending on the options return only the required series
if (displayOptions.displayMarkedLine && displayOptions.displayMarkedPoint) {
return [seriesWithMarkLine, seriesWithMarkPoint];
}
else if (displayOptions.displayMarkedLine) {
return [seriesWithMarkLine];
}
else if (displayOptions.displayMarkedPoint) {
return [seriesWithMarkPoint];
}
else {
return null;
}
});
}
/**
* This method is used to get tooltip formatter for alarms and events.
* @param tooltipParams - The tooltip parameters.
* @param params - The parameters data.
* @param allEvents - All events.
* @param allAlarms - All alarms.
* @returns The formatted string for the tooltip.
*/
getTooltipFormatterForAlarmAndEvents(tooltipParams, params, allEvents, allAlarms) {
if (!Array.isArray(tooltipParams)) {
return '';
}
const XAxisValue = tooltipParams[0].data[0];
const YAxisReadings = [];
const allSeries = this.echartsInstance?.getOption()['series'];
// filter out alarm and event series
const allDataPointSeries = allSeries.filter(series => series['typeOfSeries'] !== 'alarm' && series['typeOfSeries'] !== 'event');
this.processSeries(allDataPointSeries, XAxisValue, YAxisReadings);
// find event and alarm of the same type as the hovered markedLine or markedPoint
const event = allEvents.find(e => e.type === params.data.itemType);
const alarm = allAlarms.find(a => a.type === params.data.itemType);
let value = '';
if (event) {
value = this.processEvent(event, XAxisValue);
}
if (alarm) {
this.processAlarm(alarm).then(alarmVal => {
value = alarmVal;
YAxisReadings.push(value);
const options = this.echartsInstance?.getOption();
if (!options.tooltip || !Array.isArray(options.tooltip)) {
return;
}
const updatedOptions = {
tooltip: options['tooltip'][0]
};
if (!updatedOptions.tooltip) {
return;
}
updatedOptions.tooltip.formatter = `<div style="width: ${this.TOOLTIP_WIDTH}px">${YAxisReadings.join('')}</div>`;
updatedOptions.tooltip.transitionDuration = 0;
updatedOptions.tooltip.position = this.tooltipPosition();
this.echartsInstance?.setOption(updatedOptions);
return;
});
}
YAxisReadings.push(value);
return `<div style="width: 300px">${YAxisReadings.join('')}</div>`;
}
tooltipPosition() {
let lastPositionOfTooltip = {};
if (this.tooltipPositionCallback) {
return this.tooltipPositionCallback;
}
this.tooltipPositionCallback = (point, // position of mouse in chart [X, Y]; 0,0 is top left corner
_, // tooltip data
dom, // tooltip element
__, size // size of chart
) => {
const offset = 10;
const [mouseX, mouseY] = point;
const chartWidth = size?.viewSize[0] || 0;
const chartHeight = size?.viewSize[1] || 0;
const tooltipWidth = size?.contentSize[0] || 0;
const tooltipHeight = size?.contentSize[1] || 0;
const tooltipRect = dom?.getBoundingClientRect();
const tooltipOverflowsBottomEdge = tooltipRect.bottom > window.innerHeight;
const tooltipOverflowsRightEdge = tooltipRect.right > window.innerWidth;
const tooltipWouldOverflowBottomEdgeOnPositionChange = !lastPositionOfTooltip.top &&
tooltipRect.bottom + 2 * offset + tooltipHeight > window.innerHeight;
const tooltipWouldOverflowRightEdgeOnPositionChange = !lastPositionOfTooltip.left &&
tooltipRect.right + 2 * offset + tooltipWidth > window.innerWidth;
let verticalPosition = {
top: mouseY + offset
};
let horizontalPosition = {
left: mouseX + offset
};
if (tooltipOverflowsBottomEdge || tooltipWouldOverflowBottomEdgeOnPositionChange) {
verticalPosition = {
bottom: chartHeight - mouseY + offset
};
}
if (tooltipOverflowsRightEdge || tooltipWouldOverflowRightEdgeOnPositionChange) {
horizontalPosition = {
right: chartWidth - mouseX + offset
};
}
lastPositionOfTooltip = {
...verticalPosition,
...horizontalPosition
};
return lastPositionOfTooltip;
};
return this.tooltipPositionCallback;
}
/**
* This method is used to add the data point info to the tooltip.
* @param allDataPointSeries - All the data point series.
* @param XAxisValue - The X Axis value.
* @param YAxisReadings - The Y Axis readings.
*/
processSeries(allDataPointSeries, XAxisValue, YAxisReadings) {
allDataPointSeries.forEach((series) => {
if (series.datapointId === 'aggregated') {
return;
}
let value = '';
if (series.id.endsWith('/min')) {
value = this.processMinSeries(series, allDataPointSeries, XAxisValue);
}
else if (!series.id.endsWith('/max')) {
value = this.processRegularSeries(series, XAxisValue);
}
if (value) {
YAxisReadings.push(`<div class="d-flex a-i-center p-b-8"><span class='dlt-c8y-icon-circle m-r-4' style='color: ${series.itemStyle.color};'></span>` + // color circle
`<strong>${series.datapointLabel}</strong></div>` + // name
`${value}` // single value or min-max range
);
}
});
}
/**
* This method is used to process the min series.
* @param series - The series.
* @param allDataPointSeries - All the data point series.
* @param XAxisValue - The X Axis value.
* @returns The processed value.
*/
processMinSeries(series, allDataPointSeries, XAxisValue) {
const minValue = this.findValueForExactOrEarlierTimestamp(series.data, XAxisValue);
if (!minValue) {
return '';
}
const maxSeries = allDataPointSeries.find(s => s['id'] === series.id.replace('/min', '/max'));
const maxValue = this.findValueForExactOrEarlierTimestamp(maxSeries?.['data'], XAxisValue);
return (`<div class="d-flex a-i-center separator-top p-t-8 p-b-8"><label class="text-12 m-r-8 m-b-0">${this.datePipe.transform(minValue[0])}</label>` +
`<span class="m-l-auto text-12">${minValue[1]} — ${maxValue?.[1]}` +
(series.datapointUnit ? ` ${series.datapointUnit}` : '') +
`</span></div>`);
}
/**
* This method is used to process the regular series.
* @param series - The series.
* @param XAxisValue - The X Axis value.
* @returns The processed value.
*/
processRegularSeries(series, XAxisValue) {
const seriesValue = this.findValueForExactOrEarlierTimestamp(series.data, XAxisValue);
if (!seriesValue) {
return '';
}
return (`<div class="d-flex a-i-center p-t-8 p-b-8 separator-top">` +
`<label class="m-b-0 m-r-8 text-12">${this.datePipe.transform(seriesValue[0])}</label><span class="m-l-auto text-12">` +
seriesValue[1]?.toString() +
(series.datapointUnit ? ` ${series.datapointUnit}` : '') +
`</span></div>`);
}
/**
* This method is used to process the event tooltip.
* @param event - The event object.
* @returns The processed value.
*/
processEvent(event, XAxisValue) {
let value = `<ul class="list-unstyled small separator-top">`;
value += `<li class="p-t-4 p-b-4 d-flex separator-bottom text-no-wrap"><label class="small m-b-0 m-r-8">Event type</label><code class="m-l-auto">${event.type}</code></li>`;
value += `<li class="p-t-4 p-b-4 d-flex separator-bottom text-no-wrap"><label class="small m-b-0 m-r-8">Event text</label><span class="m-l-auto">${event.text}<span></li>`;
value += `<li class="p-t-4 p-b-4 d-flex separator-bottom text-no-wrap"><label class="small m-b-0 m-r-8">Event time</label><span class="m-l-auto">${this.datePipe.transform(XAxisValue)}<span></li>`;
value += `</ul>`;
return value;
}
/**
* This method is used to process the alarm tooltip.
* @param alarm - The alarm object.
* @returns The processed value.
*/
async processAlarm(alarm) {
let value = `<ul class="list-unstyled small separator-top m-0">`;
value += `<li class="p-t-4 p-b-4 d-flex a-i-center separator-bottom text-no-wrap"><label class="text-label-small m-b-0 m-r-8">Alarm Severity</label>`;
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-${this.severityIconPipe.transform(alarm.severity)} ${alarm.severity.toLowerCase()}" > </i> ${this.severityLabelPipe.transform(alarm.severity)} </span></li>`;
value += `<li class="p-t-4 p-b-4 d-flex separator-bottom text-no-wrap"><label class="text-label-small m-b-0 m-r-8">Alarm Type</label><span class="small m-l-auto"><code>${alarm.type}</code></span></li>`;
value += `<li class="p-t-4 p-b-4 d-flex separator-bottom text-no-wrap"><label class="text-label-small m-b-0 m-r-8">Message</label><span class="small m-l-auto" style="overflow: hidden; text-overflow: ellipsis;" title="${alarm.text}">${alarm.text}</span></li>`;
value += `<li class="p-t-4 p-b-4 d-flex separator-bottom text-no-wrap"><label class="text-label-small m-b-0 m-r-8">Last Updated</label><span class="small m-l-auto">${this.datePipe.transform(alarm['lastUpdated'])}</span></li>`;
const exists = await this.alarmRouteExists();
if (exists) {
const currentUrl = window.location.href;
const baseUrlIndex = currentUrl.indexOf(INDEX_HTML);
const baseUrl = currentUrl.substring(0, baseUrlIndex + INDEX_HTML.length);
value += `<li class="p-t-4 p-b-4 d-flex separator-bottom text-no-wrap"><label class="text-label-small m-b-0 m-r-8">Link</label><span class="small m-l-auto"><a href="${baseUrl}#/alarms/${alarm.id}/details?showCleared=true">${this.translate.instant(gettext('Alarm details'))}</a></span></li>`;
}
value += `<li class="p-t-4 p-b-4 d-flex text-no-wrap"><label class="text-label-small m-b-0 m-r-8">Alarm count</label><span class="small m-l-auto"><span class="badge badge-info">${alarm.count}</span></span></li>`;
value += `</ul>`;
return value;
}
async alarmRouteExists() {
const exists = this.router.config.some(route => {
return `${route.path}` === 'alarms';
});
return exists;
}
getChartSeries(datapointsWithValues, events, alarms, displayOptions) {
const series = [];
let eventSeries = [];
let alarmSeries = [];
datapointsWithValues.forEach((dp, idx) => {
const renderType = dp.renderType || 'min';
if (renderType === 'area') {
series.push(this.getSingleSeries(dp, 'min', idx, true));
series.push(this.getSingleSeries(dp, 'max', idx, true));
}
else {
series.push(this.getSingleSeries(dp, renderType, idx, false));
}
const newEventSeries = this.getAlarmOrEventSeries(dp, renderType, false, events, 'event', displayOptions, null, idx);
const newAlarmSeries = this.getAlarmOrEventSeries(dp, renderType, false, alarms, 'alarm', displayOptions, null, idx);
eventSeries = [...eventSeries, ...newEventSeries];
alarmSeries = [...alarmSeries, ...newAlarmSeries];
});
const deduplicateFilterCallback = (obj1, i, arr) => {
const duplicates = arr.filter(obj2 => obj1['id'] === obj2['id'] && i !== arr.indexOf(obj2));
if (duplicates.length > 0) {
return obj1['isDpTemplateSelected'];
}
return true;
};
const deduplicateFilterCallbackFallback = (obj1, i, arr) => arr.findIndex(obj2 => obj2['id'] === obj1['id']) === i;
let deduplicatedEvents = eventSeries.filter(deduplicateFilterCallback);
let deduplicatedAlarms = alarmSeries.filter(deduplicateFilterCallback);
if (deduplicatedAlarms.length === 0) {
deduplicatedAlarms = alarmSeries.filter(deduplicateFilterCallbackFallback);
}
if (deduplicatedEvents.length === 0) {
deduplicatedEvents = eventSeries.filter(deduplicateFilterCallbackFallback);
}
return [...series, ...deduplicatedEvents, ...deduplicatedAlarms];
}
groupByType(items, typeField) {
return items.reduce((grouped, item) => {
(grouped[item[typeField]] = grouped[item[typeField]] || []).push(item);
return grouped;
}, {});
}
/**
* This method interpolates between two data points. The goal is to place the markPoint on the chart in the right place.
* @param dpValuesArray array of data points
* @param targetTime time of the alarm or event
* @returns interpolated data point
*/
interpolateBetweenTwoDps(dpValuesArray, targetTime) {
let maxValue;
let minValue;
return dpValuesArray.reduce((acc, curr, idx, arr) => {
if (new Date(curr.time).getTime() <= targetTime) {
if (idx === arr.length - 1) {
return {
time: targetTime,
values: [{ min: minValue, max: maxValue }]
};
}
const nextDp = arr[idx + 1];
if (new Date(nextDp.time).getTime() >= targetTime) {
const timeDiff = new Date(nextDp.time).getTime() - new Date(curr.time).getTime();
const targetTimeDiff = targetTime - new Date(curr.time).getTime();
const minValueDiff = nextDp.values[0]?.min - curr.values[0]?.min;
const maxValueDiff = nextDp.values[0]?.max - curr.values[0]?.max;
minValue = curr.values[0]?.min + (minValueDiff * targetTimeDiff) / timeDiff;
maxValue = curr.values[0]?.max + (maxValueDiff * targetTimeDiff) / timeDiff;
return {
time: targetTime,
values: [{ min: minValue, max: maxValue }]
};
}
}
return acc;
});
}
getClosestDpValueToTargetTime(dpValuesArray, targetTime) {
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
* @param dp Data point
* @param isCleared If the alarm is cleared in case of alarm
* @param isEvent If the item is an event
* @param realtime If the chart is in realtime mode
* @returns MarkPointDataItemOption[]
*/
createMarkPoint(item, dp, isCleared, isEvent, realtime) {
// check if dp.values object is empty
if (!item.creationTime || Object.keys(dp.values).length === 0) {
return [];
}
const dpValuesArray = Object.entries(dp.values).map(([time, values]) => ({
time: new Date(time).getTime(),
values
}));
const creationTime = new Date(item.creationTime).getTime();
const lastUpdatedTime = new Date(item['lastUpdated']).getTime();
let coordCreationTime;
let coordLastUpdatedTime;
if (realtime) {
const lastValue = dpValuesArray[dpValuesArray.length - 1];
coordCreationTime = [
item.creationTime,
lastValue?.values[0]?.min ?? lastValue?.values[1] ?? null
];
coordLastUpdatedTime = [
item['lastUpdated'],
lastValue?.values[0]?.min ?? lastValue?.values[1] ?? null
];
}
else {
const closestDpValue = this.interpolateBetweenTwoDps(dpValuesArray, creationTime);
const dpValuesForNewAlarms = this.getClosestDpValueToTargetTime(dpValuesArray, creationTime);
const closestDpValueLastUpdated = this.interpolateBetweenTwoDps(dpValuesArray, lastUpdatedTime);
const dpValuesForNewAlarmsLastUpdated = this.getClosestDpValueToTargetTime(dpValuesArray, lastUpdatedTime);
coordCreationTime = [
item.creationTime,
closestDpValue?.values[0]?.min ??
closestDpValue?.values[1] ??
dpValuesForNewAlarms?.values[0]?.min ??
dpValuesForNewAlarms?.values[1] ??
null
];
coordLastUpdatedTime = [
item['lastUpdated'],
closestDpValueLastUpdated?.values[0]?.min ??
closestDpValueLastUpdated?.values[1] ??
dpValuesForNewAlarmsLastUpdated?.values[0]?.min ??
dpValuesForNewAlarmsLastUpdated?.values[1] ??
null
];
}
if (isEvent) {
return [
{
coord: coordCreationTime,
name: item.type,
itemType: item.type,
itemStyle: {
color: item['color']
},
symbol: 'circle',
symbolSize: 24
},
{
coord: coordCreationTime,
name: item.type,
itemType: item.type,
itemStyle: { color: 'white' },
symbol: ICONS_MAP.EVENT,
symbolSize: 16
}
];
}
return isCleared
? [
{
coord: coordCreationTime,
name: item.type,
itemType: item.type,
itemStyle: {
color: item['color']
},
symbol: 'circle',
symbolSize: 24
},
{
coord: coordCreationTime,
name: item.type,
itemType: item.type,
itemStyle: { color: 'white' },
symbol: ICONS_MAP[item.severity],
symbolSize: 16
},
{
coord: coordLastUpdatedTime,
name: item.type,
itemType: item.type,
itemStyle: {
color: item['color']
},
symbol: 'circle',
symbolSize: 24
},
{
coord: coordLastUpdatedTime,
name: item.type,
itemType: item.type,
itemStyle: { color: 'white' },
symbol: ICONS_MAP.CLEARED,
symbolSize: 16
}
]
: [
{
coord: coordCreationTime,
name: item.type,
itemType: item.type,
itemStyle: {
color: item['color']
},
symbol: 'circle',
symbolSize: 24
},
{
coord: coordCreationTime,
name: item.type,
itemType: item.type,
itemStyle: { color: 'white' },
symbol: ICONS_MAP[item.severity],
symbolSize: 16
},
{
coord: coordLastUpdatedTime,
name: item.type,
itemType: item.type,
itemStyle: {
color: item['color']
},
symbol: 'circle',
symbolSize: 24
},
{
coord: coordLastUpdatedTime,
name: item.type,
itemType: item.type,
itemStyle: { color: 'white' },
symbol: ICONS_MAP[item.severity],
symbolSize: 16
}
];
}
/**
* This method creates a markLine on the chart which represents the line between every alarm or event on the chart.
* @param items Array of alarms or events
* @returns MarkLineDataItemOptionBase[]
*/
createMarkLine(items, itemType) {
return items.reduce((acc, item) => {
if (!item.creationTime) {
return acc;
}
if (itemType === 'event') {
return acc.concat([
{
xAxis: item.time,
itemType: item.type,
label: { show: false, formatter: () => item.type },
itemStyle: { color: item['color'] }
}
]);
}
if (item.creationTime === item['lastUpdated']) {
return acc.concat([
{
xAxis: item.creationTime,
itemType: item.type,
label: { show: false, formatter: () => item.type },
itemStyle: { color: item['color'] }
}
]);
}
else {
return acc.concat([
{
xAxis: item.creationTime,
itemType: item.type,
label: { show: false, formatter: () => item.type },
itemStyle: { color: item['color'] }
},
{
xAxis: item['lastUpdated'],
itemType: item.type,
label: { show: false, formatter: () => item.type },
itemStyle: { color: item['color'] }
}
]);
}
}, []);
}
getSingleSeries(dp, renderType, idx, isMinMaxChart = false, seriesType = '') {
const datapointId = dp.__target?.id + dp.fragment + dp.series;
return {
datapointId,
datapointUnit: dp.unit || '',
// 'id' property is needed as 'seriesId' in tooltip formatter
id: isMinMaxChart
? `${datapointId}/${renderType}${seriesType}`
: `${datapointId}${seriesType}`,
name: `${dp.label} (${dp.__target?.['name']})`,
// datapointLabel used to proper display of tooltip
datapointLabel: dp.label || '',
data: Object.entries(dp.values).map(([dateString, values]) => {
return [dateString, values[0][renderType]];
}),
yAxisIndex: idx,
...this.chartTypesService.getSeriesOptions(dp, isMinMaxChart, renderType)
};
}
/**
* This method creates a general tooltip formatter for the chart.
* @returns TooltipFormatterCallback<TopLevelFormatterParams>
*/
getTooltipFormatter() {
return params => {
if (!Array.isArray(params) || !params[0]?.data) {
return '';
}
const data = params[0].data;
const XAxisValue = data[0];
const YAxisReadings = [];
const allSeries = this.echartsInstance?.getOption()['series'];
const allDataPointSeries = allSeries.filter(series => series['typeOfSeries'] !== 'alarm' &&
series['typeOfSeries'] !== 'event' &&
series['typeOfSeries'] !== 'fake');
allDataPointSeries.forEach((series) => {
let value;
const id = series['id'];
if (id.endsWith('/min')) {
const minValue = this.findValueForExactOrEarlierTimestamp(series['data'], XAxisValue);
if (!minValue) {
return;
}
const maxSeries = allDataPointSeries.find(s => s['id'] === id.replace('/min', '/max'));
if (!maxSeries) {
return;
}
const maxValue = this.findValueForExactOrEarlierTimestamp(maxSeries['data'], XAxisValue);
if (maxValue === null) {
return;
}
value =
`<div class="d-flex a-i-center separator-top p-t-8 p-b-8">` +
`<label class="text-12 m-r-8 m-b-0">${this.datePipe.transform(minValue[0])}</label>` +
`<div class="m-l-auto text-12" >${minValue[1]} — ${maxValue[1]}` +
(series['datapointUnit'] ? ` ${series['datapointUnit']}` : '') +
`</div></div>`;
}
else if (id.endsWith('/max')) {
// do nothing, value is handled in 'min' case
return;
}
else {
const seriesValue = this.findValueForExactOrEarlierTimestamp(series['data'], XAxisValue);
if (!seriesValue) {
return;
}
value =
`<div class="d-flex a-i-center separator-top p-t-8 p-b-8">` +
`<label class="text-12 m-r-8 m-b-0">${this.datePipe.transform(seriesValue[0])}</label>` +
`<div class="m-l-auto text-12" >${seriesValue[1]?.toString()}` +
(series['datapointUnit'] ? ` ${series['datapointUnit']}` : '') +
`</div></div>`;
}
const itemStyle = series['itemStyle'];
YAxisReadings.push(`<div class="d-flex a-i-center p-b-8"><span class='dlt-c8y-icon-circle m-r-4' style='color: ${itemStyle.color}'></span>` + // color circle
`<strong>${series['datapointLabel']} </strong></div>` + // name
`${value}` // single value or min-max range
);
});
return `<div style="width: ${this.TOOLTIP_WIDTH}px">${YAxisReadings.join('')}</div>`;
};
}
findValueForExactOrEarlierTimestamp(values, timestampString) {
const timestamp = new Date(timestampString).valueOf();
return values.reduce((acc, curr) => {
if (new Date(curr[0]).valueOf() <= timestamp) {
if (acc === null ||
Math.abs(new Date(curr[0]).valueOf() - timestamp) <
Math.abs(new Date(acc[0]).valueOf() - timestamp)) {
return curr;
}
}
return acc;
}, null);
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: EchartsOptionsService, deps: [{ token: i1.DatePipe }, { token: i2.YAxisService }, { token: i3.ChartTypesService }, { token: i4.AlarmSeverityToIconPipe }, { token: i4.AlarmSeverityToLabelPipe }, { token: i5.TranslateService }, { token: i6.Router }], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: EchartsOptionsService }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: EchartsOptionsService, decorators: [{
type: Injectable
}], ctorParameters: () => [{ type: i1.DatePipe }, { type: i2.YAxisService }, { type: i3.ChartTypesService }, { type: i4.AlarmSeverityToIconPipe }, { type: i4.AlarmSeverityToLabelPipe }, { type: i5.TranslateService }, { type: i6.Router }] });
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZWNoYXJ0cy1vcHRpb25zLnNlcnZpY2UuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi8uLi9lY2hhcnQvc2VydmljZXMvZWNoYXJ0cy1vcHRpb25zLnNlcnZpY2UudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsdURBQXVEO0FBQ3ZELE9BQU8sRUFBRSxVQUFVLEVBQUUsTUFBTSxlQUFlLENBQUM7QUFDM0MsT0FBTyxFQUFFLFFBQVEsRUFBRSxPQUFPLEVBQUUsTUFBTSxxQkFBcUIsQ0FBQztBQWF4RCxPQUFPLEVBQUUsWUFBWSxFQUFFLE1BQU0sa0JBQWtCLENBQUM7QUFDaEQsT0FBTyxFQUFFLGlCQUFpQixFQUFFLE1BQU0sdUJBQXVCLENBQUM7QUFHMUQsT0FBTyxFQUFFLFdBQVcsRUFBZ0MsTUFBTSxhQUFhLENBQUM7QUFDeEUsT0FBTyxFQUFFLFNBQVMsRUFBRSxNQUFNLDJCQUEyQixDQUFDO0FBRXRELE9BQU8sRUFBRSxNQUFNLEVBQUUsTUFBTSxpQkFBaUIsQ0FBQztBQUN6QyxPQUFPLEVBQUUsdUJBQXVCLEVBQUUsd0JBQXdCLEVBQUUsTUFBTSw0QkFBNEIsQ0FBQztBQUMvRixPQUFPLEVBQVksU0FBUyxFQUFFLE1BQU0scUNBQXFDLENBQUM7QUFDMUUsT0FBTyxFQUFFLGdCQUFnQixFQUFFLE1BQU0scUJBQXFCLENBQUM7Ozs7Ozs7O0FBYXZELE1BQU0sVUFBVSxHQUFHLGFBQWEsQ0FBQztBQUdqQyxNQUFNLE9BQU8scUJBQXFCO0lBS2hDLFlBQ1UsUUFBa0IsRUFDbEIsWUFBMEIsRUFDMUIsaUJBQW9DLEVBQ3BDLGdCQUF5QyxFQUN6QyxpQkFBMkMsRUFDM0MsU0FBMkIsRUFDM0IsTUFBYztRQU5kLGFBQVEsR0FBUixRQUFRLENBQVU7UUFDbEIsaUJBQVksR0FBWixZQUFZLENBQWM7UUFDMUIsc0JBQWlCLEdBQWpCLGlCQUFpQixDQUFtQjtRQUNwQyxxQkFBZ0IsR0FBaEIsZ0JBQWdCLENBQXlCO1FBQ3pDLHNC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