UNPKG

@c8y/ngx-components

Version:

Angular modules for Cumulocity IoT applications

873 lines 134 kB
/* 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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