@c8y/ngx-components
Version:
Angular modules for Cumulocity IoT applications
857 lines • 132 kB
JavaScript
/* eslint-disable @typescript-eslint/no-explicit-any */
import { Injectable } from '@angular/core';
import { DatePipe } 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 * 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 "@angular/router";
const INDEX_HTML = '/index.html';
export class EchartsOptionsService {
constructor(datePipe, yAxisService, chartTypesService, severityIconPipe, severityLabelPipe, router) {
this.datePipe = datePipe;
this.yAxisService = yAxisService;
this.chartTypesService = chartTypesService;
this.severityIconPipe = severityIconPipe;
this.severityLabelPipe = severityLabelPipe;
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);
// 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);
}
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) => {
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) {
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">Last update</label><span class="m-l-auto">${this.datePipe.transform(event['lastUpdated'])}<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}">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) {
return items.reduce((acc, item) => {
if (!item.creationTime) {
return acc;
}
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.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.Router }] });
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZWNoYXJ0cy1vcHRpb25zLnNlcnZpY2UuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi8uLi9lY2hhcnQvc2VydmljZXMvZWNoYXJ0cy1vcHRpb25zLnNlcnZpY2UudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsdURBQXVEO0FBQ3ZELE9BQU8sRUFBRSxVQUFVLEVBQUUsTUFBTSxlQUFlLENBQUM7QUFDM0MsT0FBTyxFQUFFLFFBQVEsRUFBRSxNQUFNLHFCQUFxQixDQUFDO0FBYS9DLE9BQU8sRUFBRSxZQUFZLEVBQUUsTUFBTSxrQkFBa0IsQ0FBQztBQUNoRCxPQUFPLEVBQUUsaUJBQWlCLEVBQUUsTUFBTSx1QkFBdUIsQ0FBQztBQUcxRCxPQUFPLEVBQUUsV0FBVyxFQUFnQyxNQUFNLGFBQWEsQ0FBQztBQUN4RSxPQUFPLEVBQUUsU0FBUyxFQUFFLE1BQU0sMkJBQTJCLENBQUM7QUFFdEQsT0FBTyxFQUFFLE1BQU0sRUFBRSxNQUFNLGlCQUFpQixDQUFDO0FBQ3pDLE9BQU8sRUFBRSx1QkFBdUIsRUFBRSx3QkFBd0IsRUFBRSxNQUFNLDRCQUE0QixDQUFDO0FBQy9GLE9BQU8sRUFBWSxTQUFTLEVBQUUsTUFBTSxxQ0FBcUMsQ0FBQzs7Ozs7OztBQWExRSxNQUFNLFVBQVUsR0FBRyxhQUFhLENBQUM7QUFHakMsTUFBTSxPQUFPLHFCQUFxQjtJQUtoQyxZQUNVLFFBQWtCLEVBQ2xCLFlBQTBCLEVBQzFCLGlCQUFvQyxFQUNwQyxnQkFBeUMsRUFDekMsaUJBQTJDLEVBQzNDLE1BQWM7UUFMZCxhQUFRLEdBQVIsUUFBUSxDQUFVO1FBQ2xCLGlCQUFZLEdBQVosWUFBWSxDQUFjO1FBQzFCLHNCQUFpQixHQUFqQixpQkFBaUIsQ0FBbUI7UUFDcEMscUJBQWdCLEdBQWhCLGdCQUFnQixDQUF5QjtRQUN6QyxzQkFBaUIsR0FBakIsaUJBQWlCLENBQTBCO1FBQzNDLFdBQU0sR0FBTixNQUFNLENBQVE7UUFUaEIsa0JBQWEsR0FBRyxH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