UNPKG

@c8y/ngx-components

Version:

Angular modules for Cumulocity IoT applications

857 lines 132 kB
/* 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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