diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/bar-chart-with-labels-widget.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/bar-chart-with-labels-widget.models.ts
index 3b6333bbc3..5351b219a3 100644
--- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/bar-chart-with-labels-widget.models.ts
+++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/bar-chart-with-labels-widget.models.ts
@@ -23,15 +23,17 @@ import {
textStyle
} from '@shared/models/widget-settings.models';
import { LegendPosition } from '@shared/models/widget.models';
-import { EChartsTooltipWidgetSettings } from '@home/components/widget/lib/chart/echarts-widget.models';
+import {
+ echartsAnimationDefaultSettings,
+ EChartsAnimationSettings,
+ EChartsTooltipWidgetSettings
+} from '@home/components/widget/lib/chart/echarts-widget.models';
import { DeepPartial } from '@shared/models/common';
import {
defaultTimeSeriesChartXAxisSettings,
defaultTimeSeriesChartYAxisSettings,
SeriesFillSettings,
SeriesFillType,
- timeSeriesChartAnimationDefaultSettings,
- TimeSeriesChartAnimationSettings,
timeSeriesChartGridDefaultSettings,
TimeSeriesChartGridSettings,
TimeSeriesChartKeySettings,
@@ -63,7 +65,7 @@ export interface BarChartWithLabelsWidgetSettings extends EChartsTooltipWidgetSe
grid: TimeSeriesChartGridSettings;
yAxis: TimeSeriesChartYAxisSettings;
xAxis: TimeSeriesChartXAxisSettings;
- animation: TimeSeriesChartAnimationSettings;
+ animation: EChartsAnimationSettings;
thresholds: TimeSeriesChartThreshold[];
showLegend: boolean;
legendPosition: LegendPosition;
@@ -116,8 +118,8 @@ export const barChartWithLabelsDefaultSettings: BarChartWithLabelsWidgetSettings
xAxis: mergeDeep({} as TimeSeriesChartXAxisSettings,
defaultTimeSeriesChartXAxisSettings,
{showTicks: false, showSplitLines: false} as TimeSeriesChartXAxisSettings),
- animation: mergeDeep({} as TimeSeriesChartAnimationSettings,
- timeSeriesChartAnimationDefaultSettings),
+ animation: mergeDeep({} as EChartsAnimationSettings,
+ echartsAnimationDefaultSettings),
thresholds: [],
showLegend: true,
legendPosition: LegendPosition.top,
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/doughnut-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/chart/doughnut-widget.component.html
deleted file mode 100644
index 3465e80c15..0000000000
--- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/doughnut-widget.component.html
+++ /dev/null
@@ -1,37 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
{{ legendItem.label }}
-
-
{{ legendItem.value }}
-
-
-
-
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/doughnut-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/doughnut-widget.component.ts
index d215f37a32..4b2eca3728 100644
--- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/doughnut-widget.component.ts
+++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/doughnut-widget.component.ts
@@ -14,85 +14,32 @@
/// limitations under the License.
///
-import {
- AfterViewInit,
- ChangeDetectorRef,
- Component,
- ElementRef,
- Input,
- OnDestroy,
- OnInit,
- Renderer2,
- TemplateRef,
- ViewChild,
- ViewEncapsulation
-} from '@angular/core';
+import { Component, Input, OnInit, TemplateRef, ViewChild, ViewEncapsulation } from '@angular/core';
import {
doughnutDefaultSettings,
- DoughnutLayout,
- DoughnutTooltipValueType,
+ doughnutPieChartSettings,
DoughnutWidgetSettings
} from '@home/components/widget/lib/chart/doughnut-widget.models';
import { WidgetContext } from '@home/models/widget-component.models';
-import {
- backgroundStyle,
- ColorProcessor,
- ComponentStyle,
- overlayStyle,
- textStyle
-} from '@shared/models/widget-settings.models';
-import { ResizeObserver } from '@juggle/resize-observer';
import { WidgetComponent } from '@home/components/widget/widget.component';
-import * as echarts from 'echarts/core';
import { TranslateService } from '@ngx-translate/core';
-import { PieDataItemOption } from 'echarts/types/src/chart/pie/PieSeries';
-import { formatValue, isDefinedAndNotNull, isNumeric } from '@core/utils';
-import { SVG, Svg, Text } from '@svgdotjs/svg.js';
-import { DataKey, LegendPosition } from '@shared/models/widget.models';
-import { Observable } from 'rxjs';
-import { ImagePipe } from '@shared/pipe/image.pipe';
-import { DomSanitizer } from '@angular/platform-browser';
-import { ECharts, echartsModule, EChartsOption } from '@home/components/widget/lib/chart/echarts-widget.models';
-
-const shapeSize = 134;
-const shapeSegmentWidth = 13.4;
-
-interface DoughnutDataItem {
- id: number;
- dataKey: DataKey;
- value: number;
- hasValue: boolean;
- enabled: boolean;
-}
-
-interface DoughnutLegendItem {
- id: number;
- color: string;
- label: string;
- value: string;
- hasValue: boolean;
- enabled: boolean;
- total?: boolean;
-}
+import { isDefinedAndNotNull } from '@core/utils';
+import { TbPieChart } from '@home/components/widget/lib/chart/pie-chart';
+import {
+ LatestChartComponent,
+ LatestChartComponentCallbacks
+} from '@home/components/widget/lib/chart/latest-chart.component';
@Component({
selector: 'tb-doughnut-widget',
- templateUrl: './doughnut-widget.component.html',
- styleUrls: ['./doughnut-widget.component.scss'],
+ templateUrl: './latest-chart-widget.component.html',
+ styleUrls: [],
encapsulation: ViewEncapsulation.None
})
-export class DoughnutWidgetComponent implements OnInit, OnDestroy, AfterViewInit {
+export class DoughnutWidgetComponent implements OnInit {
- @ViewChild('doughnutContent', {static: false})
- doughnutContent: ElementRef
;
-
- @ViewChild('doughnutShape', {static: false})
- doughnutShape: ElementRef;
-
- @ViewChild('doughnutLegend', {static: false})
- doughnutLegend: ElementRef;
-
- settings: DoughnutWidgetSettings;
+ @ViewChild('latestChart')
+ latestChart: LatestChartComponent;
@Input()
ctx: WidgetContext;
@@ -100,45 +47,12 @@ export class DoughnutWidgetComponent implements OnInit, OnDestroy, AfterViewInit
@Input()
widgetTitlePanel: TemplateRef;
- showLegend: boolean;
- legendClass: string;
-
- totalValueColor: ColorProcessor;
-
- backgroundStyle$: Observable;
- overlayStyle: ComponentStyle = {};
-
- legendItems: DoughnutLegendItem[];
- legendLabelStyle: ComponentStyle;
- legendValueStyle: ComponentStyle;
- disabledLegendLabelStyle: ComponentStyle;
- disabledLegendValueStyle: ComponentStyle;
-
- private shapeResize$: ResizeObserver;
- private legendHorizontal: boolean;
-
- private decimals = 0;
- private units = '';
-
- private total = 0;
- private totalText = 'N/A';
- private scale = 1;
-
- private dataItems: DoughnutDataItem[] = [];
+ settings: DoughnutWidgetSettings;
- private drawDoughnutPending = false;
- private showTotal = false;
- private doughnutChart: ECharts;
- private doughnutOptions: EChartsOption;
- private svgShape: Svg;
- private totalTextNode: Text;
+ callbacks: LatestChartComponentCallbacks;
- constructor(private imagePipe: ImagePipe,
- private sanitizer: DomSanitizer,
- private widgetComponent: WidgetComponent,
- private renderer: Renderer2,
- private translate: TranslateService,
- private cd: ChangeDetectorRef) {
+ constructor(private widgetComponent: WidgetComponent,
+ private translate: TranslateService) {
}
ngOnInit(): void {
@@ -146,390 +60,19 @@ export class DoughnutWidgetComponent implements OnInit, OnDestroy, AfterViewInit
const horizontal = isDefinedAndNotNull(params.horizontal) ? params.horizontal : false;
this.ctx.$scope.doughnutWidget = this;
this.settings = {...doughnutDefaultSettings(horizontal), ...this.ctx.settings};
-
- this.decimals = this.ctx.decimals;
- this.units = this.ctx.units;
-
- this.showLegend = this.settings.showLegend;
- this.showTotal = this.settings.layout === DoughnutLayout.with_total;
-
- if (this.showTotal) {
- this.totalValueColor = ColorProcessor.fromSettings(this.settings.totalValueColor);
- }
-
- this.backgroundStyle$ = backgroundStyle(this.settings.background, this.imagePipe, this.sanitizer);
- this.overlayStyle = overlayStyle(this.settings.background.overlay);
-
- if (this.showLegend) {
- this.legendItems = [];
- this.legendClass = `legend-${this.settings.legendPosition}`;
- this.legendHorizontal = [LegendPosition.left, LegendPosition.right].includes(this.settings.legendPosition);
- this.legendLabelStyle = textStyle(this.settings.legendLabelFont);
- this.disabledLegendLabelStyle = textStyle(this.settings.legendLabelFont);
- this.legendLabelStyle.color = this.settings.legendLabelColor;
- this.legendValueStyle = textStyle(this.settings.legendValueFont);
- this.disabledLegendValueStyle = textStyle(this.settings.legendValueFont);
- this.legendValueStyle.color = this.settings.legendValueColor;
- }
- let counter = 0;
- if (this.ctx.datasources.length) {
- for (const datasource of this.ctx.datasources) {
- const dataKeys = datasource.dataKeys;
- for (const dataKey of dataKeys) {
- const id = counter++;
- this.dataItems.push({
- id,
- dataKey,
- value: 0,
- hasValue: false,
- enabled: true
- });
- if (this.showLegend) {
- this.legendItems.push(
- {
- id,
- value: '--',
- label: dataKey.label,
- color: dataKey.color,
- enabled: true,
- hasValue: false
- }
- );
- }
- }
+ this.callbacks = {
+ createChart: (chartShape, renderer) => {
+ const settings = doughnutPieChartSettings(this.settings);
+ return new TbPieChart(this.ctx, settings, chartShape.nativeElement, renderer, this.translate, true);
}
- }
- if (this.settings.sortSeries) {
- this.dataItems.sort((a, b) => a.dataKey.label.localeCompare(b.dataKey.label));
- if (this.showLegend) {
- this.legendItems.sort((a, b) => a.label.localeCompare(b.label));
- }
- }
- if (this.showLegend && !this.showTotal) {
- this.legendItems.push(
- {
- id: null,
- value: '--',
- label: this.translate.instant('widgets.doughnut.total'),
- color: 'rgba(0, 0, 0, 0.06)',
- enabled: true,
- hasValue: false,
- total: true
- }
- );
- }
- }
-
- ngAfterViewInit() {
- if (this.drawDoughnutPending) {
- this.drawDoughnut();
- }
- }
-
- ngOnDestroy() {
- if (this.shapeResize$) {
- this.shapeResize$.disconnect();
- }
- if (this.doughnutChart) {
- this.doughnutChart.dispose();
- }
+ };
}
public onInit() {
- const borderRadius = this.ctx.$widgetElement.css('borderRadius');
- this.overlayStyle = {...this.overlayStyle, ...{borderRadius}};
- if (this.doughnutShape) {
- this.drawDoughnut();
- } else {
- this.drawDoughnutPending = true;
- }
- this.cd.detectChanges();
+ this.latestChart?.onInit();
}
public onDataUpdated() {
- for (const dsData of this.ctx.data) {
- let value = 0;
- const tsValue = dsData.data[0];
- const dataItem = this.dataItems.find(item => item.dataKey === dsData.dataKey);
- if (tsValue && isDefinedAndNotNull(tsValue[1]) && isNumeric(tsValue[1])) {
- value = tsValue[1];
- dataItem.hasValue = true;
- dataItem.value = Number(value);
- } else {
- dataItem.hasValue = false;
- dataItem.value = 0;
- }
- }
- this.updateSeriesData();
- if (this.showLegend) {
- this.cd.detectChanges();
- if (this.legendHorizontal) {
- setTimeout(() => {
- this.onResize();
- });
- }
- }
- }
-
- private updateSeriesData(renderTotal = true) {
- this.total = 0;
- this.totalText = 'N/A';
- let hasValue = false;
- const seriesData: PieDataItemOption[] = [];
- const enabledDataItems = this.dataItems.filter(item => item.enabled && item.hasValue);
- for (const dataItem of this.dataItems) {
- if (dataItem.enabled && dataItem.hasValue) {
- hasValue = true;
- this.total += dataItem.value;
- seriesData.push(
- {id: dataItem.id, value: dataItem.value, name: dataItem.dataKey.label, itemStyle: {color: dataItem.dataKey.color}}
- );
- if (enabledDataItems.length > 1) {
- seriesData.push({
- value: 0, name: '', itemStyle: {color: 'transparent'}, emphasis: {disabled: true}
- });
- }
- }
- if (this.showLegend) {
- const legendItem = this.legendItems.find(item => item.id === dataItem.id);
- if (dataItem.hasValue) {
- legendItem.hasValue = true;
- legendItem.value = formatValue(dataItem.value, this.decimals, this.units, false);
- } else {
- legendItem.hasValue = false;
- legendItem.value = '--';
- }
- }
- }
- for (let i= 1; i < seriesData.length; i+=2) {
- seriesData[i].value = this.total / 100;
- }
- if (this.showTotal || this.showLegend) {
- if (hasValue) {
- this.totalText = formatValue(this.total, this.decimals, this.units, false);
- if (this.showLegend && !this.showTotal) {
- this.legendItems[this.legendItems.length - 1].hasValue = true;
- this.legendItems[this.legendItems.length - 1].value = this.totalText;
- }
- } else if (this.showLegend && !this.showTotal) {
- this.legendItems[this.legendItems.length - 1].hasValue = false;
- this.legendItems[this.legendItems.length - 1].value = '--';
- }
- }
- this.doughnutOptions.series[0].data = seriesData;
- this.doughnutChart.setOption(this.doughnutOptions);
- if (this.showTotal) {
- this.totalValueColor.update(this.total);
- if (renderTotal) {
- this.renderTotal();
- }
- }
- }
-
- public onLegendItemEnter(item: DoughnutLegendItem) {
- if (!item.total && item.enabled && item.hasValue) {
- const dataIndex = this.doughnutOptions.series[0].data.findIndex(d => d.id === item.id);
- if (dataIndex > -1) {
- this.doughnutChart.dispatchAction({
- type: 'highlight',
- dataIndex
- });
- }
- }
- }
-
- public onLegendItemLeave(item: DoughnutLegendItem) {
- if (!item.total && item.enabled && item.hasValue) {
- const dataIndex = this.doughnutOptions.series[0].data.findIndex(d => d.id === item.id);
- if (dataIndex > -1) {
- this.doughnutChart.dispatchAction({
- type: 'downplay',
- dataIndex
- });
- }
- }
+ this.latestChart?.onDataUpdated();
}
-
- public toggleLegendItem(item: DoughnutLegendItem) {
- if (!item.total && item.hasValue) {
- const enable = !item.enabled;
- const dataItem = this.dataItems.find(d => d.id === item.id);
- if (dataItem) {
- dataItem.enabled = enable;
- this.updateSeriesData();
- item.enabled = enable;
- if (enable) {
- const dataIndex = this.doughnutOptions.series[0].data.findIndex(d => d.id === item.id);
- if (dataIndex > -1) {
- this.doughnutChart.dispatchAction({
- type: 'highlight',
- dataIndex
- });
- }
- }
- }
- }
- }
-
- private drawDoughnut() {
- echartsModule.init();
- const shapeWidth = this.doughnutShape.nativeElement.getBoundingClientRect().width;
- const shapeHeight = this.doughnutShape.nativeElement.getBoundingClientRect().height;
- const size = this.settings.autoScale ? shapeSize : Math.min(shapeWidth, shapeHeight);
- const innerRadius = size / 2 - shapeSegmentWidth;
- const outerRadius = size / 2;
- this.doughnutChart = echarts.init(this.doughnutShape.nativeElement, null, {
- renderer: 'svg',
- width: this.settings.autoScale ? shapeSize : undefined,
- height: this.settings.autoScale ? shapeSize : undefined,
- });
- this.doughnutOptions = {
- tooltip: {
- trigger: this.settings.showTooltip ? 'item' : 'none',
- confine: false,
- appendTo: 'body',
- },
- series: [
- {
- type: 'pie',
- clockwise: this.settings.clockwise,
- radius: [innerRadius, outerRadius],
- avoidLabelOverlap: false,
- itemStyle: {
- borderRadius: '50%',
- borderWidth: 0,
- borderColor: '#fff'
- },
- label: {
- show: false
- },
- emphasis: {
- scale: false,
- itemStyle: {
- borderColor: '#fff',
- borderWidth: 2,
- shadowColor: 'rgba(0, 0, 0, 0.24)',
- shadowBlur: 8
- },
- label: {
- show: false
- }
- }
- }
- ]
- };
- if (this.settings.showTooltip) {
- this.doughnutOptions.series[0].tooltip = {
- formatter: (params) => {
- if (!params.name) {
- return null;
- }
- let value: string;
- if (this.settings.tooltipValueType === DoughnutTooltipValueType.percentage) {
- const percents = params.value / this.total * 100;
- value = formatValue(percents, this.settings.tooltipValueDecimals, '%', false);
- } else {
- value = formatValue(params.value, this.settings.tooltipValueDecimals, this.units, false);
- }
- const textElement: HTMLElement = this.renderer.createElement('div');
- this.renderer.setStyle(textElement, 'display', 'inline-flex');
- this.renderer.setStyle(textElement, 'align-items', 'center');
- this.renderer.setStyle(textElement, 'gap', '8px');
- const labelElement: HTMLElement = this.renderer.createElement('div');
- this.renderer.appendChild(labelElement, this.renderer.createText(params.name));
- this.renderer.setStyle(labelElement, 'font-family', 'Roboto');
- this.renderer.setStyle(labelElement, 'font-size', '11px');
- this.renderer.setStyle(labelElement, 'font-style', 'normal');
- this.renderer.setStyle(labelElement, 'font-weight', '400');
- this.renderer.setStyle(labelElement, 'line-height', '16px');
- this.renderer.setStyle(labelElement, 'letter-spacing', '0.25px');
- this.renderer.setStyle(labelElement, 'color', 'rgba(0, 0, 0, 0.38)');
- const valueElement: HTMLElement = this.renderer.createElement('div');
- this.renderer.appendChild(valueElement, this.renderer.createText(value));
- this.renderer.setStyle(valueElement, 'font-family', this.settings.tooltipValueFont.family);
- this.renderer.setStyle(valueElement, 'font-size', this.settings.tooltipValueFont.size + this.settings.tooltipValueFont.sizeUnit);
- this.renderer.setStyle(valueElement, 'font-style', this.settings.tooltipValueFont.style);
- this.renderer.setStyle(valueElement, 'font-weight', this.settings.tooltipValueFont.weight);
- this.renderer.setStyle(valueElement, 'line-height', this.settings.tooltipValueFont.lineHeight);
- this.renderer.setStyle(valueElement, 'color', this.settings.tooltipValueColor);
- this.renderer.appendChild(textElement, labelElement);
- this.renderer.appendChild(textElement, valueElement);
- return textElement;
- },
- padding: [4, 8],
- backgroundColor: this.settings.tooltipBackgroundColor,
- extraCssText: `line-height: 1; backdrop-filter: blur(${this.settings.tooltipBackgroundBlur}px);`
- };
- this.doughnutOptions.series[0].tooltip.position = (pos) => [pos[0] + 10, pos[1] + 10];
- }
- this.updateSeriesData(false);
-
- this.renderer.setStyle(this.doughnutChart.getDom().firstChild, 'overflow', 'visible');
- if (this.settings.autoScale) {
- this.renderer.setStyle(this.doughnutChart.getDom().firstChild, 'position', 'absolute');
- }
- this.renderer.setStyle(this.doughnutChart.getDom().firstChild.firstChild, 'overflow', 'visible');
-
- this.svgShape = SVG(this.doughnutChart.getDom().firstChild.firstChild).toRoot();
-
- if (this.showTotal) {
- this.totalTextNode = this.svgShape.text('').font({
- family: 'Roboto',
- leading: 1
- }).attr({'text-anchor': 'middle'});
- this.renderTotal();
- }
-
- this.shapeResize$ = new ResizeObserver(() => {
- this.onResize();
- });
- this.shapeResize$.observe(this.doughnutContent.nativeElement);
- this.onResize();
- }
-
- private renderTotal() {
- this.totalTextNode.text(add => {
- add.tspan(this.translate.instant('widgets.doughnut.total')).font({size: '12px', weight: 400}).fill('rgba(0, 0, 0, 0.38)');
- add.tspan('').newLine().font({size: '4px'});
- add.tspan(this.totalText).newLine().font(
- {family: this.settings.totalValueFont.family,
- size: this.settings.totalValueFont.size + this.settings.totalValueFont.sizeUnit,
- weight: this.settings.totalValueFont.weight,
- style: this.settings.totalValueFont.style}
- ).fill(this.totalValueColor.color);
- }).center(this.svgShape.bbox().width / 2, this.svgShape.bbox().height / 2);
- }
-
- private onResize() {
- if (this.legendHorizontal) {
- this.renderer.setStyle(this.doughnutShape.nativeElement, 'max-width', null);
- this.renderer.setStyle(this.doughnutShape.nativeElement, 'min-width', null);
- this.renderer.setStyle(this.doughnutLegend.nativeElement, 'flex', null);
- }
- const shapeWidth = this.doughnutShape.nativeElement.getBoundingClientRect().width;
- const shapeHeight = this.doughnutShape.nativeElement.getBoundingClientRect().height;
- const size = Math.min(shapeWidth, shapeHeight);
- if (this.legendHorizontal) {
- this.renderer.setStyle(this.doughnutShape.nativeElement, 'max-width', `${size}px`);
- this.renderer.setStyle(this.doughnutShape.nativeElement, 'min-width', `${size}px`);
- this.renderer.setStyle(this.doughnutLegend.nativeElement, 'flex', '1');
- }
- if (!this.settings.autoScale) {
- const innerRadius = size / 2 - shapeSegmentWidth;
- const outerRadius = size / 2;
- this.doughnutOptions.series[0].radius = [innerRadius, outerRadius];
- this.doughnutChart.setOption(this.doughnutOptions);
- } else {
- this.scale = size / shapeSize;
- this.renderer.setStyle(this.doughnutChart.getDom().firstChild, 'transform', `scale(${this.scale})`);
- }
- if (!this.settings.autoScale) {
- this.doughnutChart.resize();
- }
- if (this.showTotal) {
- this.totalTextNode.center((this.settings.autoScale ? shapeSize : shapeWidth) / 2,
- (this.settings.autoScale ? shapeSize : shapeHeight) / 2);
- }
- }
-
}
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/doughnut-widget.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/doughnut-widget.models.ts
index 9110cfcd4d..d059e0186c 100644
--- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/doughnut-widget.models.ts
+++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/doughnut-widget.models.ts
@@ -14,14 +14,16 @@
/// limitations under the License.
///
-import {
- BackgroundSettings,
- BackgroundType,
- ColorSettings,
- constantColor,
- Font
-} from '@shared/models/widget-settings.models';
+import { BackgroundType, ColorSettings, constantColor, Font } from '@shared/models/widget-settings.models';
import { LegendPosition } from '@shared/models/widget.models';
+import { pieChartAnimationDefaultSettings, PieChartSettings } from '@home/components/widget/lib/chart/pie-chart.models';
+import { DeepPartial } from '@shared/models/common';
+import {
+ LatestChartTooltipValueType,
+ LatestChartWidgetSettings
+} from '@home/components/widget/lib/chart/latest-chart.models';
+import { mergeDeep } from '@core/utils';
+import { EChartsAnimationSettings } from '@home/components/widget/lib/chart/echarts-widget.models';
export enum DoughnutLayout {
default = 'default',
@@ -51,41 +53,11 @@ export const horizontalDoughnutLayoutImages = new Map(
]
);
-export enum DoughnutTooltipValueType {
- absolute = 'absolute',
- percentage = 'percentage'
-}
-
-export const doughnutTooltipValueTypes = Object.keys(DoughnutTooltipValueType) as DoughnutTooltipValueType[];
-
-export const doughnutTooltipValueTypeTranslations = new Map(
- [
- [DoughnutTooltipValueType.absolute, 'widgets.doughnut.tooltip-value-type-absolute'],
- [DoughnutTooltipValueType.percentage, 'widgets.doughnut.tooltip-value-type-percentage']
- ]
-);
-
-export interface DoughnutWidgetSettings {
+export interface DoughnutWidgetSettings extends LatestChartWidgetSettings {
layout: DoughnutLayout;
- autoScale: boolean;
clockwise: boolean;
- sortSeries: boolean;
totalValueFont: Font;
totalValueColor: ColorSettings;
- showLegend: boolean;
- legendPosition: LegendPosition;
- legendLabelFont: Font;
- legendLabelColor: string;
- legendValueFont: Font;
- legendValueColor: string;
- showTooltip: boolean;
- tooltipValueType: DoughnutTooltipValueType;
- tooltipValueDecimals: number;
- tooltipValueFont: Font;
- tooltipValueColor: string;
- tooltipBackgroundColor: string;
- tooltipBackgroundBlur: number;
- background: BackgroundSettings;
}
export const doughnutDefaultSettings = (horizontal: boolean): DoughnutWidgetSettings => ({
@@ -102,6 +74,8 @@ export const doughnutDefaultSettings = (horizontal: boolean): DoughnutWidgetSett
lineHeight: '1'
},
totalValueColor: constantColor('rgba(0, 0, 0, 0.87)'),
+ animation: mergeDeep({} as EChartsAnimationSettings,
+ pieChartAnimationDefaultSettings),
showLegend: true,
legendPosition: horizontal ? LegendPosition.right : LegendPosition.bottom,
legendLabelFont: {
@@ -123,7 +97,7 @@ export const doughnutDefaultSettings = (horizontal: boolean): DoughnutWidgetSett
},
legendValueColor: 'rgba(0, 0, 0, 0.87)',
showTooltip: true,
- tooltipValueType: DoughnutTooltipValueType.percentage,
+ tooltipValueType: LatestChartTooltipValueType.percentage,
tooltipValueDecimals: 0,
tooltipValueFont: {
family: 'Roboto',
@@ -146,3 +120,31 @@ export const doughnutDefaultSettings = (horizontal: boolean): DoughnutWidgetSett
}
}
});
+
+export const doughnutPieChartSettings = (settings: DoughnutWidgetSettings): DeepPartial => ({
+ autoScale: settings.autoScale,
+ doughnut: true,
+ clockwise: settings.clockwise,
+ sortSeries: settings.sortSeries,
+ showTotal: settings.layout === DoughnutLayout.with_total,
+ animation: settings.animation,
+ showLegend: settings.showLegend,
+ totalValueFont: settings.totalValueFont,
+ totalValueColor: settings.totalValueColor,
+ showLabel: false,
+ borderWidth: 0,
+ borderColor: '#fff',
+ borderRadius: '50%',
+ emphasisScale: false,
+ emphasisBorderWidth: 2,
+ emphasisBorderColor: '#fff',
+ emphasisShadowColor: 'rgba(0, 0, 0, 0.24)',
+ emphasisShadowBlur: 8,
+ showTooltip: settings.showTooltip,
+ tooltipValueType: settings.tooltipValueType,
+ tooltipValueDecimals: settings.tooltipValueDecimals,
+ tooltipValueFont: settings.tooltipValueFont,
+ tooltipValueColor: settings.tooltipValueColor,
+ tooltipBackgroundColor: settings.tooltipBackgroundColor,
+ tooltipBackgroundBlur: settings.tooltipBackgroundBlur
+});
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/echarts-widget.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/echarts-widget.models.ts
index 4d188e0a74..a225973b82 100644
--- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/echarts-widget.models.ts
+++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/echarts-widget.models.ts
@@ -162,6 +162,63 @@ export const timeSeriesChartShapeOffsetFunctions = new Map {
let interval: number;
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/latest-chart-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/chart/latest-chart-widget.component.html
new file mode 100644
index 0000000000..436687b23a
--- /dev/null
+++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/latest-chart-widget.component.html
@@ -0,0 +1,24 @@
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/latest-chart.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/chart/latest-chart.component.html
new file mode 100644
index 0000000000..bd85f19d81
--- /dev/null
+++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/latest-chart.component.html
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+
+
+
+
+
+
{{ legendItem.label }}
+
+
{{ legendItem.value }}
+
+
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/doughnut-widget.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/chart/latest-chart.component.scss
similarity index 82%
rename from ui-ngx/src/app/modules/home/components/widget/lib/chart/doughnut-widget.component.scss
rename to ui-ngx/src/app/modules/home/components/widget/lib/chart/latest-chart.component.scss
index 3ed90e1c2c..77b4f0fc08 100644
--- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/doughnut-widget.component.scss
+++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/latest-chart.component.scss
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-.tb-doughnut-panel {
+.tb-latest-chart-panel {
width: 100%;
height: 100%;
position: relative;
@@ -21,10 +21,10 @@
flex-direction: column;
gap: 16px;
padding: 20px 24px 24px 24px;
- > div:not(.tb-doughnut-overlay) {
+ > div:not(.tb-latest-chart-overlay) {
z-index: 1;
}
- .tb-doughnut-overlay {
+ .tb-latest-chart-overlay {
position: absolute;
top: 12px;
left: 12px;
@@ -34,7 +34,7 @@
div.tb-widget-title {
padding: 0;
}
- .tb-doughnut-content {
+ .tb-latest-chart-content {
flex: 1;
min-width: 0;
min-height: 0;
@@ -50,22 +50,22 @@
&.legend-left {
flex-direction: row-reverse;
}
- .tb-doughnut-shape {
+ .tb-latest-chart-shape {
flex: 1;
- min-width: 0;
- min-height: 0;
+ min-width: 10px;
+ min-height: 10px;
display: flex;
align-items: center;
justify-content: center;
}
- .tb-doughnut-legend {
+ .tb-latest-chart-legend {
display: flex;
justify-content: space-around;
align-items: center;
align-self: stretch;
flex-wrap: wrap;
gap: 8px;
- .tb-doughnut-legend-item {
+ .tb-latest-chart-legend-item {
display: flex;
flex-direction: column;
justify-content: center;
@@ -74,19 +74,19 @@
&.pointer {
cursor: pointer;
}
- .tb-doughnut-legend-item-label {
+ .tb-latest-chart-legend-item-label {
display: flex;
align-items: center;
gap: 4px;
color: #ccc;
- .tb-doughnut-legend-item-label-circle {
+ .tb-latest-chart-legend-item-label-circle {
width: 8px;
height: 8px;
border-radius: 50%;
background-color: #ccc;
}
}
- .tb-doughnut-legend-item-value {
+ .tb-latest-chart-legend-item-value {
padding-left: 12px;
color: #ccc;
}
@@ -94,11 +94,11 @@
}
&.legend-right, &.legend-left {
gap: 24px;
- .tb-doughnut-legend {
+ .tb-latest-chart-legend {
flex-direction: column;
justify-content: center;
align-items: stretch;
- .tb-doughnut-legend-item {
+ .tb-latest-chart-legend-item {
flex-direction: row;
justify-content: space-between;
align-items: center;
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/latest-chart.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/latest-chart.component.ts
new file mode 100644
index 0000000000..21599dc94b
--- /dev/null
+++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/latest-chart.component.ts
@@ -0,0 +1,200 @@
+///
+/// Copyright © 2016-2024 The Thingsboard Authors
+///
+/// Licensed under the Apache License, Version 2.0 (the "License");
+/// you may not use this file except in compliance with the License.
+/// You may obtain a copy of the License at
+///
+/// http://www.apache.org/licenses/LICENSE-2.0
+///
+/// Unless required by applicable law or agreed to in writing, software
+/// distributed under the License is distributed on an "AS IS" BASIS,
+/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+/// See the License for the specific language governing permissions and
+/// limitations under the License.
+///
+
+import {
+ AfterViewInit,
+ ChangeDetectorRef,
+ Component,
+ ElementRef,
+ Input,
+ OnDestroy,
+ OnInit,
+ Renderer2,
+ TemplateRef,
+ ViewChild,
+ ViewEncapsulation
+} from '@angular/core';
+import {
+ LatestChartDataItem,
+ LatestChartLegendItem,
+ LatestChartSettings,
+ LatestChartWidgetSettings
+} from '@home/components/widget/lib/chart/latest-chart.models';
+import { WidgetContext } from '@home/models/widget-component.models';
+import { Observable } from 'rxjs';
+import { backgroundStyle, ComponentStyle, overlayStyle, textStyle } from '@shared/models/widget-settings.models';
+import { TbLatestChart } from '@home/components/widget/lib/chart/latest-chart';
+import { ResizeObserver } from '@juggle/resize-observer';
+import { ImagePipe } from '@shared/pipe/image.pipe';
+import { DomSanitizer } from '@angular/platform-browser';
+import { WidgetComponent } from '@home/components/widget/widget.component';
+import { TranslateService } from '@ngx-translate/core';
+import { LegendPosition } from '@shared/models/widget.models';
+
+export interface LatestChartComponentCallbacks {
+ createChart: (chartShape: ElementRef, renderer: Renderer2) => TbLatestChart;
+ onItemClick?: ($event: Event, item: LatestChartDataItem) => void;
+}
+
+@Component({
+ selector: 'tb-latest-chart',
+ templateUrl: './latest-chart.component.html',
+ styleUrls: ['./latest-chart.component.scss'],
+ encapsulation: ViewEncapsulation.None
+})
+export class LatestChartComponent implements OnInit, OnDestroy, AfterViewInit {
+
+ @ViewChild('chartContent', {static: false})
+ chartContent: ElementRef;
+
+ @ViewChild('chartShape', {static: false})
+ chartShape: ElementRef;
+
+ @ViewChild('chartLegend', {static: false})
+ chartLegend: ElementRef;
+
+ @Input()
+ ctx: WidgetContext;
+
+ @Input()
+ widgetTitlePanel: TemplateRef;
+
+ @Input()
+ callbacks: LatestChartComponentCallbacks;
+
+ @Input()
+ settings: LatestChartWidgetSettings;
+
+ showLegend: boolean;
+ legendClass: string;
+
+ backgroundStyle$: Observable;
+ overlayStyle: ComponentStyle = {};
+
+ get legendItems(): LatestChartLegendItem[] {
+ return this.latestChart ? this.latestChart.getLegendItems() : [];
+ }
+
+ legendLabelStyle: ComponentStyle;
+ legendValueStyle: ComponentStyle;
+ disabledLegendLabelStyle: ComponentStyle;
+ disabledLegendValueStyle: ComponentStyle;
+
+ private shapeResize$: ResizeObserver;
+ private legendHorizontal: boolean;
+
+ private latestChart: TbLatestChart;
+
+ constructor(private imagePipe: ImagePipe,
+ private sanitizer: DomSanitizer,
+ private widgetComponent: WidgetComponent,
+ private renderer: Renderer2,
+ private translate: TranslateService,
+ private cd: ChangeDetectorRef) {
+ }
+
+ ngOnInit(): void {
+ this.showLegend = this.settings.showLegend;
+
+ this.backgroundStyle$ = backgroundStyle(this.settings.background, this.imagePipe, this.sanitizer);
+ this.overlayStyle = overlayStyle(this.settings.background.overlay);
+
+ if (this.showLegend) {
+ this.legendClass = `legend-${this.settings.legendPosition}`;
+ this.legendHorizontal = [LegendPosition.left, LegendPosition.right].includes(this.settings.legendPosition);
+ this.legendLabelStyle = textStyle(this.settings.legendLabelFont);
+ this.disabledLegendLabelStyle = textStyle(this.settings.legendLabelFont);
+ this.legendLabelStyle.color = this.settings.legendLabelColor;
+ this.legendValueStyle = textStyle(this.settings.legendValueFont);
+ this.disabledLegendValueStyle = textStyle(this.settings.legendValueFont);
+ this.legendValueStyle.color = this.settings.legendValueColor;
+ }
+ }
+
+ ngAfterViewInit() {
+ this.latestChart = this.callbacks.createChart(this.chartShape, this.renderer);
+ this.latestChart.onItemClick(this.callbacks.onItemClick);
+ this.shapeResize$ = new ResizeObserver(() => {
+ this.onResize();
+ });
+ this.shapeResize$.observe(this.chartContent.nativeElement);
+ this.onResize();
+ }
+
+ ngOnDestroy() {
+ if (this.shapeResize$) {
+ this.shapeResize$.disconnect();
+ }
+ if (this.latestChart) {
+ this.latestChart.destroy();
+ }
+ }
+
+ public onInit() {
+ const borderRadius = this.ctx.$widgetElement.css('borderRadius');
+ this.overlayStyle = {...this.overlayStyle, ...{borderRadius}};
+ this.cd.detectChanges();
+ }
+
+ public onDataUpdated() {
+ if (this.latestChart) {
+ this.latestChart.update();
+ }
+ if (this.showLegend) {
+ this.cd.detectChanges();
+ if (this.legendHorizontal) {
+ setTimeout(() => {
+ this.onResize();
+ });
+ }
+ }
+ }
+
+ public onLegendItemEnter(item: LatestChartLegendItem) {
+ if (!item.total && item.hasValue) {
+ this.latestChart.keyEnter(item.dataKey);
+ }
+ }
+
+ public onLegendItemLeave(item: LatestChartLegendItem) {
+ if (!item.total && item.hasValue) {
+ this.latestChart.keyLeave(item.dataKey);
+ }
+ }
+
+ public toggleLegendItem(item: LatestChartLegendItem) {
+ if (!item.total && item.hasValue) {
+ this.latestChart.toggleKey(item.dataKey);
+ }
+ }
+
+ private onResize() {
+ if (this.legendHorizontal) {
+ this.renderer.setStyle(this.chartShape.nativeElement, 'max-width', null);
+ this.renderer.setStyle(this.chartShape.nativeElement, 'min-width', null);
+ this.renderer.setStyle(this.chartLegend.nativeElement, 'flex', null);
+ }
+ const shapeWidth = this.chartShape.nativeElement.getBoundingClientRect().width;
+ const shapeHeight = this.chartShape.nativeElement.getBoundingClientRect().height;
+ const size = Math.min(shapeWidth, shapeHeight);
+ if (this.legendHorizontal) {
+ this.renderer.setStyle(this.chartShape.nativeElement, 'max-width', `${size}px`);
+ this.renderer.setStyle(this.chartShape.nativeElement, 'min-width', `${size}px`);
+ this.renderer.setStyle(this.chartLegend.nativeElement, 'flex', '1');
+ }
+ }
+
+}
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/latest-chart.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/latest-chart.models.ts
new file mode 100644
index 0000000000..c99cd5e773
--- /dev/null
+++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/latest-chart.models.ts
@@ -0,0 +1,122 @@
+///
+/// Copyright © 2016-2024 The Thingsboard Authors
+///
+/// Licensed under the Apache License, Version 2.0 (the "License");
+/// you may not use this file except in compliance with the License.
+/// You may obtain a copy of the License at
+///
+/// http://www.apache.org/licenses/LICENSE-2.0
+///
+/// Unless required by applicable law or agreed to in writing, software
+/// distributed under the License is distributed on an "AS IS" BASIS,
+/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+/// See the License for the specific language governing permissions and
+/// limitations under the License.
+///
+
+import { DataKey, Datasource, LegendPosition } from '@shared/models/widget.models';
+import { BackgroundSettings, Font } from '@shared/models/widget-settings.models';
+import { Renderer2 } from '@angular/core';
+import { CallbackDataParams } from 'echarts/types/dist/shared';
+import { formatValue, isDefinedAndNotNull } from '@core/utils';
+import { EChartsAnimationSettings } from '@home/components/widget/lib/chart/echarts-widget.models';
+
+export interface LatestChartDataItem {
+ id: number;
+ datasource: Datasource;
+ dataKey: DataKey;
+ value: number;
+ hasValue: boolean;
+ enabled: boolean;
+}
+
+export interface LatestChartLegendItem {
+ dataKey?: DataKey;
+ color: string;
+ label: string;
+ value: string;
+ hasValue: boolean;
+ total?: boolean;
+}
+
+export enum LatestChartTooltipValueType {
+ absolute = 'absolute',
+ percentage = 'percentage'
+}
+
+export const latestChartTooltipValueTypes = Object.keys(LatestChartTooltipValueType) as LatestChartTooltipValueType[];
+
+export const latestChartTooltipValueTypeTranslations = new Map(
+ [
+ [LatestChartTooltipValueType.absolute, 'widgets.latest-chart.tooltip-value-type-absolute'],
+ [LatestChartTooltipValueType.percentage, 'widgets.latest-chart.tooltip-value-type-percentage']
+ ]
+);
+
+export interface LatestChartTooltipSettings {
+ showTooltip: boolean;
+ tooltipValueType: LatestChartTooltipValueType;
+ tooltipValueDecimals: number;
+ tooltipValueFont: Font;
+ tooltipValueColor: string;
+ tooltipBackgroundColor: string;
+ tooltipBackgroundBlur: number;
+}
+
+export interface LatestChartSettings extends LatestChartTooltipSettings {
+ autoScale?: boolean;
+ sortSeries: boolean;
+ showTotal?: boolean;
+ showLegend: boolean;
+ animation: EChartsAnimationSettings;
+}
+
+export interface LatestChartWidgetSettings extends LatestChartSettings {
+ legendPosition: LegendPosition;
+ legendLabelFont: Font;
+ legendLabelColor: string;
+ legendValueFont: Font;
+ legendValueColor: string;
+ background: BackgroundSettings;
+}
+
+export const latestChartTooltipFormatter = (renderer: Renderer2,
+ settings: LatestChartTooltipSettings,
+ params: CallbackDataParams,
+ units: string,
+ total: number): null | HTMLElement => {
+ if (!params.name) {
+ return null;
+ }
+ let value: string;
+ if (settings.tooltipValueType === LatestChartTooltipValueType.percentage) {
+ const percents = isDefinedAndNotNull(params.percent) ? params.percent : (params.value as number) / total * 100;
+ value = formatValue(percents, settings.tooltipValueDecimals, '%', false);
+ } else {
+ value = formatValue(params.value, settings.tooltipValueDecimals, units, false);
+ }
+ const textElement: HTMLElement = renderer.createElement('div');
+ renderer.setStyle(textElement, 'display', 'inline-flex');
+ renderer.setStyle(textElement, 'align-items', 'center');
+ renderer.setStyle(textElement, 'gap', '8px');
+ const labelElement: HTMLElement = renderer.createElement('div');
+ renderer.appendChild(labelElement, renderer.createText(params.name));
+ renderer.setStyle(labelElement, 'font-family', 'Roboto');
+ renderer.setStyle(labelElement, 'font-size', '11px');
+ renderer.setStyle(labelElement, 'font-style', 'normal');
+ renderer.setStyle(labelElement, 'font-weight', '400');
+ renderer.setStyle(labelElement, 'line-height', '16px');
+ renderer.setStyle(labelElement, 'letter-spacing', '0.25px');
+ renderer.setStyle(labelElement, 'color', 'rgba(0, 0, 0, 0.38)');
+ const valueElement: HTMLElement = renderer.createElement('div');
+ renderer.appendChild(valueElement, renderer.createText(value));
+ renderer.setStyle(valueElement, 'font-family', settings.tooltipValueFont.family);
+ renderer.setStyle(valueElement, 'font-size', settings.tooltipValueFont.size + settings.tooltipValueFont.sizeUnit);
+ renderer.setStyle(valueElement, 'font-style', settings.tooltipValueFont.style);
+ renderer.setStyle(valueElement, 'font-weight', settings.tooltipValueFont.weight);
+ renderer.setStyle(valueElement, 'line-height', settings.tooltipValueFont.lineHeight);
+ renderer.setStyle(valueElement, 'color', settings.tooltipValueColor);
+ renderer.appendChild(textElement, labelElement);
+ renderer.appendChild(textElement, valueElement);
+ return textElement;
+};
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/latest-chart.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/latest-chart.ts
new file mode 100644
index 0000000000..ddb6e47560
--- /dev/null
+++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/latest-chart.ts
@@ -0,0 +1,374 @@
+///
+/// Copyright © 2016-2024 The Thingsboard Authors
+///
+/// Licensed under the Apache License, Version 2.0 (the "License");
+/// you may not use this file except in compliance with the License.
+/// You may obtain a copy of the License at
+///
+/// http://www.apache.org/licenses/LICENSE-2.0
+///
+/// Unless required by applicable law or agreed to in writing, software
+/// distributed under the License is distributed on an "AS IS" BASIS,
+/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+/// See the License for the specific language governing permissions and
+/// limitations under the License.
+///
+
+import { ResizeObserver } from '@juggle/resize-observer';
+import { ECharts, echartsModule, EChartsOption } from '@home/components/widget/lib/chart/echarts-widget.models';
+import {
+ LatestChartDataItem,
+ LatestChartLegendItem,
+ LatestChartSettings,
+ latestChartTooltipFormatter
+} from '@home/components/widget/lib/chart/latest-chart.models';
+import { WidgetContext } from '@home/models/widget-component.models';
+import { DeepPartial } from '@shared/models/common';
+import { Renderer2 } from '@angular/core';
+import { TranslateService } from '@ngx-translate/core';
+import { formatValue, isDefinedAndNotNull, isNumeric, mergeDeep } from '@core/utils';
+import { DataKey } from '@shared/models/widget.models';
+import * as echarts from 'echarts/core';
+import { CallbackDataParams } from 'echarts/types/dist/shared';
+import { SVG, Svg } from '@svgdotjs/svg.js';
+
+export abstract class TbLatestChart {
+
+ private readonly shapeResize$: ResizeObserver;
+
+ protected readonly settings: S;
+
+ protected readonly decimals: number;
+ protected readonly units: string;
+
+ protected total = 0;
+ protected totalText = 'N/A';
+
+ protected latestChart: ECharts;
+ protected latestChartOption: EChartsOption;
+
+ protected svgShape: Svg;
+
+ protected dataItems: LatestChartDataItem[] = [];
+ private legendItems: LatestChartLegendItem[] = [];
+
+ private itemClick: ($event: Event, item: LatestChartDataItem) => void;
+
+ protected constructor(private ctx: WidgetContext,
+ private readonly inputSettings: DeepPartial,
+ protected chartElement: HTMLElement,
+ private renderer: Renderer2,
+ protected translate: TranslateService,
+ private autoResize = true) {
+ this.settings = mergeDeep({} as S,
+ this.defaultSettings(),
+ this.inputSettings as S);
+
+ this.decimals = this.ctx.decimals;
+ this.units = this.ctx.units;
+
+ this.initSettings();
+
+ this.setupData();
+
+ this.onResize();
+ if (this.autoResize) {
+ this.shapeResize$ = new ResizeObserver(() => {
+ this.onResize();
+ });
+ this.shapeResize$.observe(this.chartElement);
+ }
+ }
+
+ private setupData(): void {
+ let counter = 0;
+ if (this.ctx.datasources.length) {
+ for (const datasource of this.ctx.datasources) {
+ const dataKeys = datasource.dataKeys;
+ for (const dataKey of dataKeys) {
+ const id = counter++;
+ this.dataItems.push({
+ id,
+ datasource,
+ dataKey,
+ value: 0,
+ hasValue: false,
+ enabled: true
+ });
+ if (this.settings.showLegend) {
+ this.legendItems.push(
+ {
+ dataKey,
+ value: '--',
+ label: dataKey.label,
+ color: dataKey.color,
+ hasValue: false
+ }
+ );
+ }
+ }
+ }
+ }
+ if (this.settings.sortSeries) {
+ this.dataItems.sort((a, b) => a.dataKey.label.localeCompare(b.dataKey.label));
+ if (this.settings.showLegend) {
+ this.legendItems.sort((a, b) => a.label.localeCompare(b.label));
+ }
+ }
+ if (this.settings.showLegend && !this.settings.showTotal) {
+ this.legendItems.push(
+ {
+ value: '--',
+ label: this.translate.instant('widgets.latest-chart.total'),
+ color: 'rgba(0, 0, 0, 0.06)',
+ hasValue: false,
+ total: true
+ }
+ );
+ }
+ }
+
+ public getLegendItems(): LatestChartLegendItem[] {
+ return this.legendItems;
+ }
+
+ public update(): void {
+ for (const dsData of this.ctx.data) {
+ let value = 0;
+ const tsValue = dsData.data[0];
+ const dataItem = this.dataItems.find(item => item.dataKey === dsData.dataKey);
+ if (tsValue && isDefinedAndNotNull(tsValue[1]) && isNumeric(tsValue[1])) {
+ value = tsValue[1];
+ dataItem.hasValue = true;
+ dataItem.value = Number(value);
+ } else {
+ dataItem.hasValue = false;
+ dataItem.value = 0;
+ }
+ }
+ this.updateSeriesData();
+ }
+
+ public keyEnter(dataKey: DataKey): void {
+ const item = this.dataItems.find(d => d.dataKey === dataKey);
+ if (item) {
+ const dataIndex = this.latestChartOption.series[0].data.findIndex(
+ (d: any) => d.id === item.id);
+ if (dataIndex > -1) {
+ this.latestChart.dispatchAction({
+ type: 'highlight',
+ dataIndex
+ });
+ }
+ }
+ }
+
+ public keyLeave(dataKey: DataKey): void {
+ const item = this.dataItems.find(d => d.dataKey === dataKey);
+ if (item) {
+ const dataIndex = this.latestChartOption.series[0].data.findIndex(
+ (d: any) => d.id === item.id);
+ if (dataIndex > -1) {
+ this.latestChart.dispatchAction({
+ type: 'downplay',
+ dataIndex
+ });
+ }
+ }
+ }
+
+ public toggleKey(dataKey: DataKey): void {
+ const enable = dataKey.hidden;
+ const dataItem = this.dataItems.find(d => d.dataKey === dataKey);
+ if (dataItem) {
+ let dataIndex = this.latestChartOption.series[0].data.findIndex(
+ (d: any) => d.id === dataItem.id);
+ dataItem.enabled = enable;
+ if (!enable && dataIndex > -1) {
+ this.latestChart.dispatchAction({
+ type: 'downplay',
+ dataIndex
+ });
+ }
+ this.updateSeriesData();
+ dataKey.hidden = !enable;
+ if (enable) {
+ dataIndex = this.latestChartOption.series[0].data.findIndex(
+ (d: any) => d.id === dataItem.id);
+ if (dataIndex > -1) {
+ this.latestChart.dispatchAction({
+ type: 'highlight',
+ dataIndex
+ });
+ }
+ }
+ }
+ }
+
+ public destroy(): void {
+ if (this.shapeResize$) {
+ this.shapeResize$.disconnect();
+ }
+ if (this.latestChart) {
+ this.latestChart.dispose();
+ }
+ }
+
+ public resize(): void {
+ this.onResize();
+ }
+
+ public onItemClick(itemClick: ($event: Event, item: LatestChartDataItem) => void) {
+ this.itemClick = itemClick;
+ }
+
+ private updateSeriesData(initial = false) {
+ this.total = 0;
+ this.totalText = 'N/A';
+ let hasValue = false;
+ for (const dataItem of this.dataItems) {
+ if (dataItem.enabled && dataItem.hasValue) {
+ hasValue = true;
+ this.total += dataItem.value;
+ }
+ if (this.settings.showLegend) {
+ const legendItem = this.legendItems.find(item => item.dataKey === dataItem.dataKey);
+ if (dataItem.hasValue) {
+ legendItem.hasValue = true;
+ legendItem.value = formatValue(dataItem.value, this.decimals, this.units, false);
+ } else {
+ legendItem.hasValue = false;
+ legendItem.value = '--';
+ }
+ }
+ }
+ if (this.settings.showTotal || this.settings.showLegend) {
+ if (hasValue) {
+ this.totalText = formatValue(this.total, this.decimals, this.units, false);
+ if (this.settings.showLegend && !this.settings.showTotal) {
+ this.legendItems[this.legendItems.length - 1].hasValue = true;
+ this.legendItems[this.legendItems.length - 1].value = this.totalText;
+ }
+ } else if (this.settings.showLegend && !this.settings.showTotal) {
+ this.legendItems[this.legendItems.length - 1].hasValue = false;
+ this.legendItems[this.legendItems.length - 1].value = '--';
+ }
+ }
+ this.doUpdateSeriesData();
+ this.latestChart.setOption(this.latestChartOption);
+ this.afterUpdateSeriesData(initial);
+ }
+
+ private drawChart() {
+ echartsModule.init();
+ this.renderer.setStyle(this.chartElement, 'letterSpacing', 'normal');
+ this.latestChart = echarts.init(this.chartElement, null, {
+ renderer: 'svg',
+ width: this.settings.autoScale ? this.initialShapeWidth() : undefined,
+ height: this.settings.autoScale ? this.initialShapeHeight() : undefined,
+ });
+ this.latestChartOption = {
+ tooltip: {
+ trigger: this.settings.showTooltip ? 'item' : 'none',
+ confine: false,
+ appendTo: 'body',
+ formatter: (params: CallbackDataParams) =>
+ this.settings.showTooltip
+ ? latestChartTooltipFormatter(this.renderer, this.settings, params, this.units, this.total)
+ : undefined,
+ padding: [4, 8],
+ backgroundColor: this.settings.tooltipBackgroundColor,
+ extraCssText: `line-height: 1; backdrop-filter: blur(${this.settings.tooltipBackgroundBlur}px);`,
+ position: (pos) => [pos[0] + 10, pos[1] + 10]
+ },
+ animation: this.settings.animation.animation,
+ animationThreshold: this.settings.animation.animationThreshold,
+ animationDuration: this.settings.animation.animationDuration,
+ animationEasing: this.settings.animation.animationEasing,
+ animationDelay: this.settings.animation.animationDelay,
+ animationDurationUpdate: this.settings.animation.animationDurationUpdate,
+ animationEasingUpdate: this.settings.animation.animationEasingUpdate,
+ animationDelayUpdate: this.settings.animation.animationDelayUpdate
+ };
+ this.prepareLatestChartOption();
+ this.updateSeriesData(true);
+ this.renderer.setStyle(this.latestChart.getDom().firstChild, 'overflow', 'visible');
+ if (this.settings.autoScale) {
+ this.renderer.setStyle(this.latestChart.getDom().firstChild, 'position', 'absolute');
+ }
+ this.renderer.setStyle(this.latestChart.getDom().firstChild.firstChild, 'overflow', 'visible');
+ this.svgShape = SVG(this.latestChart.getDom().firstChild.firstChild).toRoot();
+ this.afterDrawChart();
+ this.latestChart.on('click', (params) => {
+ this.onChartClick(params);
+ });
+ }
+
+ protected onChartClick(params: echarts.ECElementEvent) {
+ if (params.componentType === 'series') {
+ if (params.data) {
+ const data = params.data as any;
+ if (isDefinedAndNotNull(data?.id)) {
+ const item = this.dataItems.find(d => d.id === data.id);
+ if (item && this.itemClick) {
+ this.itemClick(params.event.event, item);
+ }
+ }
+ }
+ }
+ };
+
+ private onResize() {
+ const shapeWidth = this.chartElement.offsetWidth;
+ const shapeHeight = this.chartElement.offsetHeight;
+ if (shapeWidth && shapeHeight) {
+ if (!this.latestChart) {
+ this.drawChart();
+ } else {
+ const width = this.latestChart.getWidth();
+ const height = this.latestChart.getHeight();
+ if (width !== shapeWidth || height !== shapeHeight) {
+ this.beforeResize(shapeWidth, shapeHeight);
+ if (!this.settings.autoScale) {
+ this.latestChart.resize();
+ } else {
+ let scale: number;
+ if (shapeWidth < shapeHeight) {
+ scale = shapeWidth / this.initialShapeWidth();
+ } else {
+ scale = shapeHeight / this.initialShapeHeight();
+ }
+ this.renderer.setStyle(this.latestChart.getDom().firstChild, 'transform', `scale(${scale})`);
+ }
+ this.afterResize(shapeWidth, shapeHeight);
+ }
+ }
+ }
+ }
+
+ protected initSettings() {
+ }
+
+ protected initialShapeWidth(): number {
+ return 100;
+ }
+
+ protected initialShapeHeight(): number {
+ return 100;
+ }
+
+ protected beforeResize(_shapeWidth: number, _shapeHeight: number) {};
+
+ protected afterResize(_shapeWidth: number, _shapeHeight: number) {};
+
+ protected afterDrawChart() {};
+
+ protected afterUpdateSeriesData(_initial: boolean) {};
+
+ protected abstract defaultSettings(): S;
+
+ protected abstract prepareLatestChartOption(): void;
+
+ protected abstract doUpdateSeriesData(): void;
+
+}
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/pie-chart-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/pie-chart-widget.component.ts
new file mode 100644
index 0000000000..41f36a43a5
--- /dev/null
+++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/pie-chart-widget.component.ts
@@ -0,0 +1,86 @@
+///
+/// Copyright © 2016-2024 The Thingsboard Authors
+///
+/// Licensed under the Apache License, Version 2.0 (the "License");
+/// you may not use this file except in compliance with the License.
+/// You may obtain a copy of the License at
+///
+/// http://www.apache.org/licenses/LICENSE-2.0
+///
+/// Unless required by applicable law or agreed to in writing, software
+/// distributed under the License is distributed on an "AS IS" BASIS,
+/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+/// See the License for the specific language governing permissions and
+/// limitations under the License.
+///
+
+import { Component, Input, OnInit, TemplateRef, ViewChild, ViewEncapsulation } from '@angular/core';
+import { WidgetContext } from '@home/models/widget-component.models';
+import { WidgetComponent } from '@home/components/widget/widget.component';
+import { TranslateService } from '@ngx-translate/core';
+import { TbPieChart } from '@home/components/widget/lib/chart/pie-chart';
+import {
+ LatestChartComponent,
+ LatestChartComponentCallbacks
+} from '@home/components/widget/lib/chart/latest-chart.component';
+import {
+ pieChartWidgetDefaultSettings,
+ pieChartWidgetPieChartSettings,
+ PieChartWidgetSettings
+} from '@home/components/widget/lib/chart/pie-chart-widget.models';
+
+@Component({
+ selector: 'tb-pie-chart-widget',
+ templateUrl: './latest-chart-widget.component.html',
+ styleUrls: [],
+ encapsulation: ViewEncapsulation.None
+})
+export class PieChartWidgetComponent implements OnInit {
+
+ @ViewChild('latestChart')
+ latestChart: LatestChartComponent;
+
+ @Input()
+ ctx: WidgetContext;
+
+ @Input()
+ widgetTitlePanel: TemplateRef;
+
+ settings: PieChartWidgetSettings;
+
+ callbacks: LatestChartComponentCallbacks;
+
+ constructor(private widgetComponent: WidgetComponent,
+ private translate: TranslateService) {
+ }
+
+ ngOnInit(): void {
+ this.ctx.$scope.pieChartWidget = this;
+ this.settings = {...pieChartWidgetDefaultSettings, ...this.ctx.settings};
+ this.callbacks = {
+ createChart: (chartShape, renderer) => {
+ const settings = pieChartWidgetPieChartSettings(this.settings);
+ return new TbPieChart(this.ctx, settings, chartShape.nativeElement, renderer, this.translate, true);
+ },
+ onItemClick: ($event: Event, item) => {
+ const descriptors = this.ctx.actionsApi.getActionDescriptors('sliceClick');
+ if ($event && descriptors.length) {
+ $event.stopPropagation();
+ const datasource = item.datasource;
+ const entityId = datasource ? datasource.entity?.id : null;
+ const entityName = datasource ? datasource.entityName : null;
+ const entityLabel = datasource ? datasource.entityLabel : null;
+ this.ctx.actionsApi.handleWidgetAction($event, descriptors[0], entityId, entityName, item, entityLabel);
+ }
+ }
+ };
+ }
+
+ public onInit() {
+ this.latestChart?.onInit();
+ }
+
+ public onDataUpdated() {
+ this.latestChart?.onDataUpdated();
+ }
+}
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/pie-chart-widget.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/pie-chart-widget.models.ts
new file mode 100644
index 0000000000..05dbef0959
--- /dev/null
+++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/pie-chart-widget.models.ts
@@ -0,0 +1,131 @@
+///
+/// Copyright © 2016-2024 The Thingsboard Authors
+///
+/// Licensed under the Apache License, Version 2.0 (the "License");
+/// you may not use this file except in compliance with the License.
+/// You may obtain a copy of the License at
+///
+/// http://www.apache.org/licenses/LICENSE-2.0
+///
+/// Unless required by applicable law or agreed to in writing, software
+/// distributed under the License is distributed on an "AS IS" BASIS,
+/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+/// See the License for the specific language governing permissions and
+/// limitations under the License.
+///
+
+import {
+ LatestChartTooltipValueType,
+ LatestChartWidgetSettings
+} from '@home/components/widget/lib/chart/latest-chart.models';
+import { BackgroundType, Font } from '@shared/models/widget-settings.models';
+import { LegendPosition } from '@shared/models/widget.models';
+import { DeepPartial } from '@shared/models/common';
+import {
+ pieChartAnimationDefaultSettings,
+ PieChartLabelPosition,
+ PieChartSettings
+} from '@home/components/widget/lib/chart/pie-chart.models';
+import { isDefinedAndNotNull, mergeDeep } from '@core/utils';
+import { EChartsAnimationSettings } from '@home/components/widget/lib/chart/echarts-widget.models';
+
+export interface PieChartWidgetSettings extends LatestChartWidgetSettings {
+ showLabel: boolean;
+ labelPosition: PieChartLabelPosition;
+ labelFont: Font;
+ labelColor: string;
+ borderWidth: number;
+ borderColor: string;
+ radius: number;
+ clockwise: boolean;
+}
+
+export const pieChartWidgetDefaultSettings: PieChartWidgetSettings = {
+ showLabel: true,
+ labelPosition: PieChartLabelPosition.outside,
+ labelFont: {
+ family: 'Roboto',
+ size: 12,
+ sizeUnit: 'px',
+ style: 'normal',
+ weight: 'normal',
+ lineHeight: '1.2'
+ },
+ labelColor: '#000',
+ borderWidth: 0,
+ borderColor: '#000',
+ radius: 80,
+ clockwise: false,
+ sortSeries: false,
+ animation: mergeDeep({} as EChartsAnimationSettings,
+ pieChartAnimationDefaultSettings),
+ showLegend: true,
+ legendPosition: LegendPosition.bottom,
+ legendLabelFont: {
+ family: 'Roboto',
+ size: 12,
+ sizeUnit: 'px',
+ style: 'normal',
+ weight: '400',
+ lineHeight: '16px'
+ },
+ legendLabelColor: 'rgba(0, 0, 0, 0.38)',
+ legendValueFont: {
+ family: 'Roboto',
+ size: 14,
+ sizeUnit: 'px',
+ style: 'normal',
+ weight: '500',
+ lineHeight: '20px'
+ },
+ legendValueColor: 'rgba(0, 0, 0, 0.87)',
+ showTooltip: true,
+ tooltipValueType: LatestChartTooltipValueType.percentage,
+ tooltipValueDecimals: 0,
+ tooltipValueFont: {
+ family: 'Roboto',
+ size: 13,
+ sizeUnit: 'px',
+ style: 'normal',
+ weight: '500',
+ lineHeight: '16px'
+ },
+ tooltipValueColor: 'rgba(0, 0, 0, 0.76)',
+ tooltipBackgroundColor: 'rgba(255, 255, 255, 0.76)',
+ tooltipBackgroundBlur: 4,
+ background: {
+ type: BackgroundType.color,
+ color: '#fff',
+ overlay: {
+ enabled: false,
+ color: 'rgba(255,255,255,0.72)',
+ blur: 3
+ }
+ }
+};
+
+export const pieChartWidgetPieChartSettings = (settings: PieChartWidgetSettings): DeepPartial => ({
+ autoScale: false,
+ doughnut: false,
+ clockwise: settings.clockwise,
+ sortSeries: settings.sortSeries,
+ showTotal: false,
+ animation: settings.animation,
+ showLegend: settings.showLegend,
+ showLabel: settings.showLabel,
+ labelPosition: settings.labelPosition,
+ labelFont: settings.labelFont,
+ labelColor: settings.labelColor,
+ borderWidth: settings.borderWidth,
+ borderColor: settings.borderColor,
+ radius: isDefinedAndNotNull(settings.radius) ? settings.radius + '%' : undefined,
+ emphasisBorderWidth: settings.borderWidth,
+ emphasisBorderColor: settings.borderColor,
+ showTooltip: settings.showTooltip,
+ tooltipValueType: settings.tooltipValueType,
+ tooltipValueDecimals: settings.tooltipValueDecimals,
+ tooltipValueFont: settings.tooltipValueFont,
+ tooltipValueColor: settings.tooltipValueColor,
+ tooltipBackgroundColor: settings.tooltipBackgroundColor,
+ tooltipBackgroundBlur: settings.tooltipBackgroundBlur
+});
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/pie-chart.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/pie-chart.models.ts
new file mode 100644
index 0000000000..419c4b61bb
--- /dev/null
+++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/pie-chart.models.ts
@@ -0,0 +1,126 @@
+///
+/// Copyright © 2016-2024 The Thingsboard Authors
+///
+/// Licensed under the Apache License, Version 2.0 (the "License");
+/// you may not use this file except in compliance with the License.
+/// You may obtain a copy of the License at
+///
+/// http://www.apache.org/licenses/LICENSE-2.0
+///
+/// Unless required by applicable law or agreed to in writing, software
+/// distributed under the License is distributed on an "AS IS" BASIS,
+/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+/// See the License for the specific language governing permissions and
+/// limitations under the License.
+///
+
+import { ColorSettings, constantColor, Font } from '@shared/models/widget-settings.models';
+import {
+ LatestChartSettings,
+ LatestChartTooltipValueType
+} from '@home/components/widget/lib/chart/latest-chart.models';
+import { mergeDeep } from '@core/utils';
+import {
+ EChartsAnimationEasing,
+ EChartsAnimationSettings
+} from '@home/components/widget/lib/chart/echarts-widget.models';
+
+export enum PieChartLabelPosition {
+ outside = 'outside',
+ inside = 'inside'
+}
+
+export const pieChartLabelPositions = Object.keys(PieChartLabelPosition) as PieChartLabelPosition[];
+
+export const pieChartLabelPositionTranslations = new Map(
+ [
+ [PieChartLabelPosition.outside, 'widgets.pie-chart.label-position-outside'],
+ [PieChartLabelPosition.inside, 'widgets.pie-chart.label-position-inside']
+ ]
+);
+
+export interface PieChartSettings extends LatestChartSettings {
+ doughnut: boolean;
+ radius: string;
+ clockwise: boolean;
+ totalValueFont: Font;
+ totalValueColor: ColorSettings;
+ showLabel: boolean;
+ labelPosition: PieChartLabelPosition;
+ labelFont: Font;
+ labelColor: string;
+ borderWidth: number;
+ borderColor: string;
+ borderRadius: string;
+ emphasisScale: boolean;
+ emphasisBorderWidth: number;
+ emphasisBorderColor: string;
+ emphasisShadowBlur: number;
+ emphasisShadowColor: string;
+}
+
+export const pieChartAnimationDefaultSettings: EChartsAnimationSettings = {
+ animation: true,
+ animationThreshold: 2000,
+ animationDuration: 1000,
+ animationEasing: EChartsAnimationEasing.cubicOut,
+ animationDelay: 0,
+ animationDurationUpdate: 500,
+ animationEasingUpdate: EChartsAnimationEasing.cubicOut,
+ animationDelayUpdate: 0
+};
+
+export const pieChartDefaultSettings: PieChartSettings = {
+ autoScale: false,
+ doughnut: false,
+ radius: '80%',
+ clockwise: false,
+ sortSeries: false,
+ showTotal: false,
+ animation: mergeDeep({} as EChartsAnimationSettings,
+ pieChartAnimationDefaultSettings),
+ showLegend: true,
+ totalValueFont: {
+ family: 'Roboto',
+ size: 24,
+ sizeUnit: 'px',
+ style: 'normal',
+ weight: '500',
+ lineHeight: '1'
+ },
+ totalValueColor: constantColor('rgba(0, 0, 0, 0.87)'),
+ showLabel: false,
+ labelPosition: PieChartLabelPosition.outside,
+ labelFont: {
+ family: 'Roboto',
+ size: 12,
+ sizeUnit: 'px',
+ style: 'normal',
+ weight: 'normal',
+ lineHeight: '1'
+ },
+ labelColor: '#000',
+ borderWidth: 0,
+ borderColor: '#000',
+ borderRadius: '0%',
+ emphasisScale: true,
+ emphasisBorderWidth: 0,
+ emphasisBorderColor: '#000',
+ emphasisShadowBlur: 10,
+ emphasisShadowColor: 'rgba(0, 0, 0, 0.5)',
+ showTooltip: true,
+ tooltipValueType: LatestChartTooltipValueType.percentage,
+ tooltipValueDecimals: 0,
+ tooltipValueFont: {
+ family: 'Roboto',
+ size: 13,
+ sizeUnit: 'px',
+ style: 'normal',
+ weight: '500',
+ lineHeight: '16px'
+ },
+ tooltipValueColor: 'rgba(0, 0, 0, 0.76)',
+ tooltipBackgroundColor: 'rgba(255, 255, 255, 0.76)',
+ tooltipBackgroundBlur: 4
+};
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/pie-chart.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/pie-chart.ts
new file mode 100644
index 0000000000..cfe003732e
--- /dev/null
+++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/pie-chart.ts
@@ -0,0 +1,194 @@
+///
+/// Copyright © 2016-2024 The Thingsboard Authors
+///
+/// Licensed under the Apache License, Version 2.0 (the "License");
+/// you may not use this file except in compliance with the License.
+/// You may obtain a copy of the License at
+///
+/// http://www.apache.org/licenses/LICENSE-2.0
+///
+/// Unless required by applicable law or agreed to in writing, software
+/// distributed under the License is distributed on an "AS IS" BASIS,
+/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+/// See the License for the specific language governing permissions and
+/// limitations under the License.
+///
+
+import { pieChartDefaultSettings, PieChartSettings } from '@home/components/widget/lib/chart/pie-chart.models';
+import { WidgetContext } from '@home/models/widget-component.models';
+import { DeepPartial } from '@shared/models/common';
+import { Renderer2 } from '@angular/core';
+import { ColorProcessor, textStyle } from '@shared/models/widget-settings.models';
+import { PieDataItemOption } from 'echarts/types/src/chart/pie/PieSeries';
+import { Text } from '@svgdotjs/svg.js';
+import { TranslateService } from '@ngx-translate/core';
+import { TbLatestChart } from '@home/components/widget/lib/chart/latest-chart';
+import { formatValue } from '@core/utils';
+
+const shapeSize = 134;
+const shapeSegmentWidth = 13.4;
+
+export class TbPieChart extends TbLatestChart {
+
+ private totalValueColor: ColorProcessor;
+ private totalTextNode: Text;
+
+ constructor(ctx: WidgetContext,
+ inputSettings: DeepPartial,
+ chartElement: HTMLElement,
+ renderer: Renderer2,
+ translate: TranslateService,
+ autoResize = true) {
+
+ super(ctx, inputSettings, chartElement, renderer, translate, autoResize);
+ }
+
+ protected defaultSettings(): PieChartSettings {
+ return pieChartDefaultSettings;
+ }
+
+ protected initSettings() {
+ if (this.settings.showTotal) {
+ this.totalValueColor = ColorProcessor.fromSettings(this.settings.totalValueColor);
+ }
+ }
+
+ protected prepareLatestChartOption() {
+ const shapeWidth = this.chartElement.offsetWidth;
+ const shapeHeight = this.chartElement.offsetHeight;
+ const size = this.settings.autoScale ? shapeSize : Math.min(shapeWidth, shapeHeight);
+ const innerRadius = size / 2 - shapeSegmentWidth;
+ const outerRadius = size / 2;
+ const labelStyle = textStyle(this.settings.labelFont);
+ labelStyle.fontSize = this.settings.labelFont.size;
+ labelStyle.lineHeight = labelStyle.fontSize * 1.2;
+ labelStyle.color = this.settings.labelColor;
+ this.latestChartOption.series = [
+ {
+ type: 'pie',
+ clockwise: this.settings.clockwise,
+ radius: this.settings.doughnut ? [innerRadius, outerRadius] : this.settings.radius,
+ avoidLabelOverlap: false,
+ itemStyle: {
+ borderRadius: this.settings.borderRadius,
+ borderWidth: this.settings.borderWidth,
+ borderColor: this.settings.borderColor
+ },
+ label: {
+ show: this.settings.showLabel,
+ position: this.settings.labelPosition,
+ formatter: (params) => {
+ const percents = params.percent;
+ const value = formatValue(percents, 0, '%', false);
+ return `{label|${params.name}\n${value}}`;
+ },
+ rich: {
+ label: labelStyle
+ }
+ },
+ emphasis: {
+ scale: this.settings.emphasisScale,
+ itemStyle: {
+ borderColor: this.settings.emphasisBorderColor,
+ borderWidth: this.settings.emphasisBorderWidth,
+ shadowColor: this.settings.emphasisShadowColor,
+ shadowBlur: this.settings.emphasisShadowBlur
+ },
+ label: {
+ show: this.settings.showLabel
+ }
+ },
+ animation: this.settings.animation.animation,
+ animationThreshold: this.settings.animation.animationThreshold,
+ animationDuration: this.settings.animation.animationDuration,
+ animationEasing: this.settings.animation.animationEasing,
+ animationDelay: this.settings.animation.animationDelay,
+ animationDurationUpdate: this.settings.animation.animationDurationUpdate,
+ animationEasingUpdate: this.settings.animation.animationEasingUpdate,
+ animationDelayUpdate: this.settings.animation.animationDelayUpdate
+ }
+ ];
+ }
+
+ protected afterDrawChart() {
+ if (this.settings.showTotal) {
+ this.totalTextNode = this.svgShape.text('').font({
+ family: 'Roboto',
+ leading: 1
+ }).attr({'text-anchor': 'middle'});
+ this.renderTotal();
+ }
+ };
+
+ protected doUpdateSeriesData() {
+ const seriesData: PieDataItemOption[] = [];
+ const enabledDataItems = this.dataItems.filter(item => item.enabled && item.hasValue);
+ for (const dataItem of this.dataItems) {
+ if (dataItem.enabled && dataItem.hasValue) {
+ seriesData.push(
+ {id: dataItem.id, value: dataItem.value, name: dataItem.dataKey.label, itemStyle: {color: dataItem.dataKey.color}}
+ );
+ if (this.settings.doughnut && enabledDataItems.length > 1) {
+ seriesData.push({
+ value: 0, name: '', itemStyle: {color: 'transparent'}, emphasis: {disabled: true}
+ });
+ }
+ }
+ }
+ if (this.settings.doughnut) {
+ for (let i = 1; i < seriesData.length; i += 2) {
+ seriesData[i].value = this.total / 100;
+ }
+ }
+ this.latestChartOption.series[0].data = seriesData;
+ }
+
+ protected afterUpdateSeriesData(initial: boolean) {
+ if (this.settings.showTotal) {
+ this.totalValueColor.update(this.total);
+ if (!initial) {
+ this.renderTotal();
+ }
+ }
+ };
+
+ protected initialShapeWidth(): number {
+ return shapeSize;
+ }
+
+ protected initialShapeHeight(): number {
+ return shapeSize;
+ }
+
+ protected beforeResize(shapeWidth: number, shapeHeight: number) {
+ if (!this.settings.autoScale) {
+ if (this.settings.doughnut) {
+ const size = Math.min(shapeWidth, shapeHeight);
+ const innerRadius = size / 2 - shapeSegmentWidth;
+ const outerRadius = size / 2;
+ this.latestChartOption.series[0].radius = [innerRadius, outerRadius];
+ this.latestChart.setOption(this.latestChartOption);
+ }
+ }
+ };
+
+ protected afterResize(shapeWidth: number, shapeHeight: number) {
+ if (this.settings.showTotal) {
+ this.totalTextNode.center((this.settings.autoScale ? shapeSize : shapeWidth) / 2,
+ (this.settings.autoScale ? shapeSize : shapeHeight) / 2);
+ }
+ };
+
+ private renderTotal() {
+ this.totalTextNode.text(add => {
+ add.tspan(this.translate.instant('widgets.latest-chart.total')).font({size: '12px', weight: 400}).fill('rgba(0, 0, 0, 0.38)');
+ add.tspan('').newLine().font({size: '4px'});
+ add.tspan(this.totalText).newLine().font(
+ {family: this.settings.totalValueFont.family,
+ size: this.settings.totalValueFont.size + this.settings.totalValueFont.sizeUnit,
+ weight: this.settings.totalValueFont.weight,
+ style: this.settings.totalValueFont.style}
+ ).fill(this.totalValueColor.color);
+ }).center(this.svgShape.bbox().width / 2, this.svgShape.bbox().height / 2);
+ }
+}
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.models.ts
index c1221536ee..c411737761 100644
--- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.models.ts
+++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.models.ts
@@ -24,15 +24,19 @@ import {
sortedColorRange
} from '@shared/models/widget-settings.models';
import { LegendPosition } from '@shared/models/widget.models';
-import { EChartsShape, EChartsTooltipWidgetSettings } from '@home/components/widget/lib/chart/echarts-widget.models';
+import {
+ echartsAnimationDefaultSettings,
+ EChartsAnimationSettings,
+ EChartsShape,
+ EChartsTooltipWidgetSettings
+} from '@home/components/widget/lib/chart/echarts-widget.models';
import {
createTimeSeriesChartVisualMapPiece,
defaultTimeSeriesChartXAxisSettings,
defaultTimeSeriesChartYAxisSettings,
LineSeriesStepType,
SeriesFillType,
- SeriesLabelPosition, ThresholdLabelPosition, timeSeriesChartAnimationDefaultSettings,
- TimeSeriesChartAnimationSettings,
+ SeriesLabelPosition, ThresholdLabelPosition,
timeSeriesChartColorScheme, timeSeriesChartGridDefaultSettings, TimeSeriesChartGridSettings,
TimeSeriesChartKeySettings,
TimeSeriesChartLineType,
@@ -84,7 +88,7 @@ export interface RangeChartWidgetSettings extends EChartsTooltipWidgetSettings {
grid: TimeSeriesChartGridSettings;
yAxis: TimeSeriesChartYAxisSettings;
xAxis: TimeSeriesChartXAxisSettings;
- animation: TimeSeriesChartAnimationSettings;
+ animation: EChartsAnimationSettings;
thresholds: TimeSeriesChartThreshold[];
showLegend: boolean;
legendPosition: LegendPosition;
@@ -150,8 +154,8 @@ export const rangeChartDefaultSettings: RangeChartWidgetSettings = {
xAxis: mergeDeep({} as TimeSeriesChartXAxisSettings,
defaultTimeSeriesChartXAxisSettings,
{showSplitLines: false} as TimeSeriesChartXAxisSettings),
- animation: mergeDeep({} as TimeSeriesChartAnimationSettings,
- timeSeriesChartAnimationDefaultSettings),
+ animation: mergeDeep({} as EChartsAnimationSettings,
+ echartsAnimationDefaultSettings),
thresholds: [],
showLegend: true,
legendPosition: LegendPosition.top,
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.models.ts
index e03c2a7158..0fd702a95f 100644
--- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.models.ts
+++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.models.ts
@@ -16,6 +16,8 @@
import {
ECharts,
+ echartsAnimationDefaultSettings,
+ EChartsAnimationSettings,
EChartsOption,
EChartsSeriesItem,
EChartsShape,
@@ -571,64 +573,6 @@ export interface TimeSeriesChartBarWidthSettings {
intervalGap: number;
}
-export enum TimeSeriesChartAnimationEasing {
- linear = 'linear',
- quadraticIn = 'quadraticIn',
- quadraticOut = 'quadraticOut',
- quadraticInOut = 'quadraticInOut',
- cubicIn = 'cubicIn',
- cubicOut = 'cubicOut',
- cubicInOut = 'cubicInOut',
- quarticIn = 'quarticIn',
- quarticOut = 'quarticOut',
- quarticInOut = 'quarticInOut',
- quinticIn = 'quinticIn',
- quinticOut = 'quinticOut',
- quinticInOut = 'quinticInOut',
- sinusoidalIn = 'sinusoidalIn',
- sinusoidalOut = 'sinusoidalOut',
- sinusoidalInOut = 'sinusoidalInOut',
- exponentialIn = 'exponentialIn',
- exponentialOut = 'exponentialOut',
- exponentialInOut = 'exponentialInOut',
- circularIn = 'circularIn',
- circularOut = 'circularOut',
- circularInOut = 'circularInOut',
- elasticIn = 'elasticIn',
- elasticOut = 'elasticOut',
- elasticInOut = 'elasticInOut',
- backIn = 'backIn',
- backOut = 'backOut',
- backInOut = 'backInOut',
- bounceIn = 'bounceIn',
- bounceOut = 'bounceOut',
- bounceInOut = 'bounceInOut'
-}
-
-export const timeSeriesChartAnimationEasings = Object.keys(TimeSeriesChartAnimationEasing) as TimeSeriesChartAnimationEasing[];
-
-export interface TimeSeriesChartAnimationSettings {
- animation: boolean;
- animationThreshold: number;
- animationDuration: number;
- animationEasing: TimeSeriesChartAnimationEasing;
- animationDelay: number;
- animationDurationUpdate: number;
- animationEasingUpdate: TimeSeriesChartAnimationEasing;
- animationDelayUpdate: number;
-}
-
-export const timeSeriesChartAnimationDefaultSettings: TimeSeriesChartAnimationSettings = {
- animation: true,
- animationThreshold: 2000,
- animationDuration: 500,
- animationEasing: TimeSeriesChartAnimationEasing.cubicOut,
- animationDelay: 0,
- animationDurationUpdate: 300,
- animationEasingUpdate: TimeSeriesChartAnimationEasing.cubicOut,
- animationDelayUpdate: 0
-};
-
export interface TimeSeriesChartVisualMapPiece {
lt?: number;
gt?: number;
@@ -721,7 +665,7 @@ export interface TimeSeriesChartSettings extends EChartsTooltipWidgetSettings, T
grid: TimeSeriesChartGridSettings;
yAxes: TimeSeriesChartYAxes;
xAxis: TimeSeriesChartXAxisSettings;
- animation: TimeSeriesChartAnimationSettings;
+ animation: EChartsAnimationSettings;
barWidthSettings: TimeSeriesChartBarWidthSettings;
noAggregationBarWidthSettings: TimeSeriesChartNoAggregationBarWidthSettings;
visualMapSettings?: TimeSeriesChartVisualMapSettings;
@@ -742,8 +686,8 @@ export const timeSeriesChartDefaultSettings: TimeSeriesChartSettings = {
},
xAxis: mergeDeep({} as TimeSeriesChartXAxisSettings,
defaultTimeSeriesChartXAxisSettings),
- animation: mergeDeep({} as TimeSeriesChartAnimationSettings,
- timeSeriesChartAnimationDefaultSettings),
+ animation: mergeDeep({} as EChartsAnimationSettings,
+ echartsAnimationDefaultSettings),
barWidthSettings: {
barGap: 0.3,
intervalGap: 0.6
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/bar-chart-with-labels-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/bar-chart-with-labels-widget-settings.component.html
index 04b2bff29c..920374ec00 100644
--- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/bar-chart-with-labels-widget-settings.component.html
+++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/bar-chart-with-labels-widget-settings.component.html
@@ -227,9 +227,9 @@