committed by
GitHub
49 changed files with 2624 additions and 750 deletions
@ -0,0 +1,26 @@ |
|||||
|
{ |
||||
|
"fqn": "pie", |
||||
|
"name": "Pie", |
||||
|
"deprecated": false, |
||||
|
"image": "tb-image:cGllX2NoYXJ0LnN2Zw==:IlBpZSIgc3lzdGVtIHdpZGdldCBpbWFnZQ==;data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAxIiBoZWlnaHQ9IjE2MCIgdmlld0JveD0iMCAwIDIwMSAxNjAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxnIGZpbHRlcj0idXJsKCNmaWx0ZXIwX2RfNDUzOF8zNTMwNikiPgo8cmVjdCB3aWR0aD0iMjAwIiBoZWlnaHQ9IjE2MCIgcng9IjIuMzg4MDYiIGZpbGw9IndoaXRlIiBzaGFwZS1yZW5kZXJpbmc9ImNyaXNwRWRnZXMiLz4KPHBhdGggZD0iTTE0OC45NzUgMTI5LjQ5N0MxNTYuNzc0IDEyMS42OTggMTYyLjYyIDExMi4xNjYgMTY2LjAzNiAxMDEuNjc4TDk5LjQ3NzUgODBMMTQ4Ljk3NSAxMjkuNDk3WiIgZmlsbD0iI0ZGREUzMCIgc3Ryb2tlPSJ3aGl0ZSIvPgo8cGF0aCBkPSJNOTkuNDc3NSAxMEM4Ny45NjYgMTAgNzYuNjMyMSAxMi44MzkgNjYuNDc5OCAxOC4yNjU1QzU2LjMyNzUgMjMuNjkyIDQ3LjY3MDIgMzEuNTM4NiA0MS4yNzQ3IDQxLjExMDFDMzQuODc5MiA1MC42ODE2IDMwLjk0MjkgNjEuNjgyNyAyOS44MTQ2IDczLjEzODhDMjguNjg2MyA4NC41OTQ5IDMwLjQwMDcgOTYuMTUyNSAzNC44MDYgMTA2Ljc4OEMzOS4yMTEzIDExNy40MjMgNDYuMTcxNCAxMjYuODA4IDU1LjA3IDEzNC4xMTFDNjMuOTY4NiAxNDEuNDE0IDc0LjUzMDggMTQ2LjQwOSA4NS44MjEyIDE0OC42NTVDOTcuMTExNiAxNTAuOTAxIDEwOC43ODIgMTUwLjMyNyAxMTkuNzk3IDE0Ni45ODZDMTMwLjgxMyAxNDMuNjQ0IDE0MC44MzUgMTM3LjYzNyAxNDguOTc1IDEyOS40OTdMOTkuNDc3NSA4MFYxMFoiIGZpbGw9IiMwODg3MkIiIHN0cm9rZT0id2hpdGUiLz4KPGcgZmlsdGVyPSJ1cmwoI2ZpbHRlcjFfZF80NTM4XzM1MzA2KSI+CjxwYXRoIGQ9Ik0xNjcuNTYyIDEwMi4wNTdDMTcxLjAxNCA5MS4yNTc1IDE3MS44ODIgNzkuNzg5OCAxNzAuMDk2IDY4LjU4ODVDMTY4LjMxMSA1Ny4zODcxIDE2My45MjIgNDYuNzY5MiAxNTcuMjg4IDM3LjYwMDJDMTUwLjY1MyAyOC40MzEyIDE0MS45NjEgMjAuOTcwNiAxMzEuOTIgMTUuODI2NkMxMjEuODc5IDEwLjY4MjYgMTEwLjc3MiA4LjAwMDc4IDk5LjUwNjQgNy45OTk3Nkw5OS41IDc5Ljk5OThMMTY3LjU2MiAxMDIuMDU3WiIgZmlsbD0iI0ZGNEQ1QSIvPgo8cGF0aCBkPSJNMTY3LjU2MiAxMDIuMDU3QzE3MS4wMTQgOTEuMjU3NSAxNzEuODgyIDc5Ljc4OTggMTcwLjA5NiA2OC41ODg1QzE2OC4zMTEgNTcuMzg3MSAxNjMuOTIyIDQ2Ljc2OTIgMTU3LjI4OCAzNy42MDAyQzE1MC42NTMgMjguNDMxMiAxNDEuOTYxIDIwLjk3MDYgMTMxLjkyIDE1LjgyNjZDMTIxLjg3OSAxMC42ODI2IDExMC43NzIgOC4wMDA3OCA5OS41MDY0IDcuOTk5NzZMOTkuNSA3OS45OTk4TDE2Ny41NjIgMTAyLjA1N1oiIHN0cm9rZT0id2hpdGUiLz4KPC9nPgo8L2c+CjxkZWZzPgo8ZmlsdGVyIGlkPSJmaWx0ZXIwX2RfNDUzOF8zNTMwNiIgeD0iLTcuMTY0MTgiIHk9Ii00Ljc3NjEyIiB3aWR0aD0iMjE0LjMyOCIgaGVpZ2h0PSIxNzQuMzI4IiBmaWx0ZXJVbml0cz0idXNlclNwYWNlT25Vc2UiIGNvbG9yLWludGVycG9sYXRpb24tZmlsdGVycz0ic1JHQiI+CjxmZUZsb29kIGZsb29kLW9wYWNpdHk9IjAiIHJlc3VsdD0iQmFja2dyb3VuZEltYWdlRml4Ii8+CjxmZUNvbG9yTWF0cml4IGluPSJTb3VyY2VBbHBoYSIgdHlwZT0ibWF0cml4IiB2YWx1ZXM9IjAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDEyNyAwIiByZXN1bHQ9ImhhcmRBbHBoYSIvPgo8ZmVPZmZzZXQgZHk9IjIuMzg4MDYiLz4KPGZlR2F1c3NpYW5CbHVyIHN0ZERldmlhdGlvbj0iMy41ODIwOSIvPgo8ZmVDb21wb3NpdGUgaW4yPSJoYXJkQWxwaGEiIG9wZXJhdG9yPSJvdXQiLz4KPGZlQ29sb3JNYXRyaXggdHlwZT0ibWF0cml4IiB2YWx1ZXM9IjAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAuMDYgMCIvPgo8ZmVCbGVuZCBtb2RlPSJub3JtYWwiIGluMj0iQmFja2dyb3VuZEltYWdlRml4IiByZXN1bHQ9ImVmZmVjdDFfZHJvcFNoYWRvd180NTM4XzM1MzA2Ii8+CjxmZUJsZW5kIG1vZGU9Im5vcm1hbCIgaW49IlNvdXJjZUdyYXBoaWMiIGluMj0iZWZmZWN0MV9kcm9wU2hhZG93XzQ1MzhfMzUzMDYiIHJlc3VsdD0ic2hhcGUiLz4KPC9maWx0ZXI+CjxmaWx0ZXIgaWQ9ImZpbHRlcjFfZF80NTM4XzM1MzA2IiB4PSI4NyIgeT0iLTQuNTAwMjQiIHdpZHRoPSI5Ni41IiBoZWlnaHQ9IjExOS4xODciIGZpbHRlclVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgY29sb3ItaW50ZXJwb2xhdGlvbi1maWx0ZXJzPSJzUkdCIj4KPGZlRmxvb2QgZmxvb2Qtb3BhY2l0eT0iMCIgcmVzdWx0PSJCYWNrZ3JvdW5kSW1hZ2VGaXgiLz4KPGZlQ29sb3JNYXRyaXggaW49IlNvdXJjZUFscGhhIiB0eXBlPSJtYXRyaXgiIHZhbHVlcz0iMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMTI3IDAiIHJlc3VsdD0iaGFyZEFscGhhIi8+CjxmZU9mZnNldC8+CjxmZUdhdXNzaWFuQmx1ciBzdGREZXZpYXRpb249IjYiLz4KPGZlQ29tcG9zaXRlIGluMj0iaGFyZEFscGhhIiBvcGVyYXRvcj0ib3V0Ii8+CjxmZUNvbG9yTWF0cml4IHR5cGU9Im1hdHJpeCIgdmFsdWVzPSIwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwLjI1IDAiLz4KPGZlQmxlbmQgbW9kZT0ibm9ybWFsIiBpbjI9IkJhY2tncm91bmRJbWFnZUZpeCIgcmVzdWx0PSJlZmZlY3QxX2Ryb3BTaGFkb3dfNDUzOF8zNTMwNiIvPgo8ZmVCbGVuZCBtb2RlPSJub3JtYWwiIGluPSJTb3VyY2VHcmFwaGljIiBpbjI9ImVmZmVjdDFfZHJvcFNoYWRvd180NTM4XzM1MzA2IiByZXN1bHQ9InNoYXBlIi8+CjwvZmlsdGVyPgo8L2RlZnM+Cjwvc3ZnPgo=", |
||||
|
"description": "Displays the latest values of the attributes or time-series data in a pie chart. Supports numeric values only.", |
||||
|
"descriptor": { |
||||
|
"type": "latest", |
||||
|
"sizeX": 5, |
||||
|
"sizeY": 4, |
||||
|
"resources": [], |
||||
|
"templateHtml": "<tb-pie-chart-widget \n [ctx]=\"ctx\"\n [widgetTitlePanel]=\"widgetTitlePanel\">\n</tb-pie-chart-widget>", |
||||
|
"templateCss": "", |
||||
|
"controllerScript": "self.onInit = function() {\n self.ctx.$scope.pieChartWidget.onInit();\n};\n\nself.onDataUpdated = function() {\n self.ctx.$scope.pieChartWidget.onDataUpdated();\n};\n\nself.typeParameters = function() {\n return {\n previewWidth: '500px',\n previewHeight: '380px',\n embedTitlePanel: true,\n defaultDataKeysFunction: function() {\n return [{ name: 'windPower', label: 'Wind', type: 'timeseries', color: '#08872B' },\n { name: 'solarPower', label: 'Solar', type: 'timeseries', color: '#FF4D5A' },\n { name: 'hydroelectricPower', label: 'Hydroelectric', type: 'timeseries', color: '#FFDE30' }];\n }\n };\n};\n\nself.onDestroy = function() {\n};\n\nself.actionSources = function() {\n return {\n 'sliceClick': {\n name: 'widget-action.pie-slice-click',\n multiple: false\n }\n };\n}\n", |
||||
|
"settingsSchema": "", |
||||
|
"dataKeySettingsSchema": "", |
||||
|
"settingsDirective": "tb-pie-chart-widget-settings", |
||||
|
"hasBasicMode": true, |
||||
|
"basicModeDirective": "tb-pie-chart-basic-config", |
||||
|
"defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Wind\",\"color\":\"#08872B\",\"settings\":{},\"_hash\":0.7227918773301678,\"funcBody\":\"if (prevValue === 0) {\\n prevValue = Math.random() * 200;\\n}\\nvar value = prevValue + Math.random() * 20 - 10;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 200) {\\n\\tvalue = 200;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Solar\",\"color\":\"#FF4D5A\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"if (prevValue === 0) {\\n prevValue = Math.random() * 200;\\n}\\nvar value = prevValue + Math.random() * 20 - 10;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 200) {\\n\\tvalue = 200;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Hydroelectric\",\"color\":\"#FFDE30\",\"settings\":{},\"_hash\":0.7051898468567794,\"funcBody\":\"if (prevValue === 0) {\\n prevValue = Math.random() * 200;\\n}\\nvar value = prevValue + Math.random() * 20 - 10;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 200) {\\n\\tvalue = 200;\\n}\\nreturn value;\",\"decimals\":0,\"aggregationType\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0px\",\"settings\":{},\"title\":\"Pie\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":null,\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{\"headerButton\":[]},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"titleIcon\":\"pie_chart\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":null,\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true}}" |
||||
|
}, |
||||
|
"tags": [ |
||||
|
"pie chart", |
||||
|
"pie" |
||||
|
] |
||||
|
} |
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -0,0 +1,276 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
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. |
||||
|
|
||||
|
--> |
||||
|
<ng-container [formGroup]="pieChartWidgetConfigForm"> |
||||
|
<tb-timewindow-config-panel *ngIf="displayTimewindowConfig" |
||||
|
[onlyHistoryTimewindow]="onlyHistoryTimewindow()" |
||||
|
formControlName="timewindowConfig"> |
||||
|
</tb-timewindow-config-panel> |
||||
|
<tb-datasources |
||||
|
[configMode]="basicMode" |
||||
|
hideDatasourceLabel |
||||
|
hideDataKeys |
||||
|
forceSingleDatasource |
||||
|
formControlName="datasources"> |
||||
|
</tb-datasources> |
||||
|
<tb-data-keys-panel |
||||
|
panelTitle="{{ 'widgets.chart.series' | translate }}" |
||||
|
addKeyTitle="{{ 'widgets.chart.add-series' | translate }}" |
||||
|
keySettingsTitle="{{ 'widgets.chart.series-settings' | translate }}" |
||||
|
removeKeyTitle="{{ 'widgets.chart.remove-series' | translate }}" |
||||
|
noKeysText="{{ 'widgets.chart.no-series' | translate }}" |
||||
|
requiredKeysText="{{ 'widgets.chart.no-series-error' | translate }}" |
||||
|
hideUnits |
||||
|
hideDecimals |
||||
|
hideDataKeyUnits |
||||
|
hideDataKeyDecimals |
||||
|
[datasourceType]="datasource?.type" |
||||
|
[deviceId]="datasource?.deviceId" |
||||
|
[entityAliasId]="datasource?.entityAliasId" |
||||
|
formControlName="series"> |
||||
|
</tb-data-keys-panel> |
||||
|
<div class="tb-form-panel"> |
||||
|
<div class="tb-form-panel-title" translate>widget-config.card-appearance</div> |
||||
|
<div class="tb-form-row space-between column-xs"> |
||||
|
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="showLabel"> |
||||
|
{{ 'widgets.pie-chart.label' | translate }} |
||||
|
</mat-slide-toggle> |
||||
|
<div fxFlex fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px"> |
||||
|
<mat-form-field fxFlex appearance="outline" subscriptSizing="dynamic"> |
||||
|
<mat-select formControlName="labelPosition"> |
||||
|
<mat-option *ngFor="let position of pieChartLabelPositions" [value]="position"> |
||||
|
{{ pieChartLabelPositionTranslationMap.get(position) | translate }} |
||||
|
</mat-option> |
||||
|
</mat-select> |
||||
|
</mat-form-field> |
||||
|
<tb-font-settings formControlName="labelFont" |
||||
|
disabledLineHeight |
||||
|
forceSizeUnit="px" |
||||
|
[previewText]="'Wind<br/>55%'"> |
||||
|
</tb-font-settings> |
||||
|
<tb-color-input asBoxInput |
||||
|
colorClearButton |
||||
|
formControlName="labelColor"> |
||||
|
</tb-color-input> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="tb-form-row space-between"> |
||||
|
<div>{{ 'widgets.pie-chart.border' | translate }}</div> |
||||
|
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px"> |
||||
|
<mat-form-field appearance="outline" class="number" subscriptSizing="dynamic"> |
||||
|
<input matInput formControlName="borderWidth" type="number" min="0" step="1" placeholder="{{ 'widget-config.set' | translate }}"> |
||||
|
<div matSuffix>px</div> |
||||
|
</mat-form-field> |
||||
|
<tb-color-input asBoxInput |
||||
|
colorClearButton |
||||
|
formControlName="borderColor"> |
||||
|
</tb-color-input> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="tb-form-row space-between"> |
||||
|
<div>{{ 'widgets.pie-chart.radius' | translate }}</div> |
||||
|
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px"> |
||||
|
<mat-form-field appearance="outline" class="number" subscriptSizing="dynamic"> |
||||
|
<input matInput formControlName="radius" type="number" min="0" max="100" step="1" placeholder="{{ 'widget-config.set' | translate }}"> |
||||
|
<div matSuffix>%</div> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="tb-form-row column-xs"> |
||||
|
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="showTitle"> |
||||
|
{{ 'widget-config.card-title' | translate }} |
||||
|
</mat-slide-toggle> |
||||
|
<div fxFlex fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px"> |
||||
|
<mat-form-field fxFlex appearance="outline" subscriptSizing="dynamic"> |
||||
|
<input matInput formControlName="title" placeholder="{{ 'widget-config.set' | translate }}"> |
||||
|
</mat-form-field> |
||||
|
<tb-font-settings formControlName="titleFont" |
||||
|
clearButton |
||||
|
[previewText]="pieChartWidgetConfigForm.get('title').value" |
||||
|
[initialPreviewStyle]="widgetConfig.config.titleStyle"> |
||||
|
</tb-font-settings> |
||||
|
<tb-color-input asBoxInput |
||||
|
colorClearButton |
||||
|
formControlName="titleColor"> |
||||
|
</tb-color-input> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="tb-form-row space-between"> |
||||
|
<mat-slide-toggle class="mat-slide" formControlName="showTitleIcon"> |
||||
|
{{ 'widget-config.card-icon' | translate }} |
||||
|
</mat-slide-toggle> |
||||
|
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px"> |
||||
|
<tb-material-icon-select asBoxInput |
||||
|
iconClearButton |
||||
|
[color]="pieChartWidgetConfigForm.get('iconColor').value" |
||||
|
formControlName="titleIcon"> |
||||
|
</tb-material-icon-select> |
||||
|
<tb-color-input asBoxInput |
||||
|
colorClearButton |
||||
|
formControlName="iconColor"> |
||||
|
</tb-color-input> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="tb-form-row"> |
||||
|
<mat-slide-toggle class="mat-slide" formControlName="clockwise"> |
||||
|
{{ 'widgets.latest-chart.clockwise-layout' | translate }} |
||||
|
</mat-slide-toggle> |
||||
|
</div> |
||||
|
<div class="tb-form-row"> |
||||
|
<mat-slide-toggle class="mat-slide" formControlName="sortSeries"> |
||||
|
{{ 'widgets.latest-chart.sort-series' | translate }} |
||||
|
</mat-slide-toggle> |
||||
|
</div> |
||||
|
<div class="tb-form-row space-between"> |
||||
|
<div translate>widget-config.units-short</div> |
||||
|
<tb-unit-input |
||||
|
formControlName="units"> |
||||
|
</tb-unit-input> |
||||
|
</div> |
||||
|
<div class="tb-form-row space-between"> |
||||
|
<div translate>widget-config.decimals-short</div> |
||||
|
<mat-form-field appearance="outline" class="number" subscriptSizing="dynamic"> |
||||
|
<input matInput formControlName="decimals" type="number" min="0" max="15" step="1" placeholder="{{ 'widget-config.set' | translate }}"> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="tb-form-panel tb-slide-toggle"> |
||||
|
<mat-expansion-panel class="tb-settings" [expanded]="pieChartWidgetConfigForm.get('showLegend').value" [disabled]="!pieChartWidgetConfigForm.get('showLegend').value"> |
||||
|
<mat-expansion-panel-header fxLayout="row wrap"> |
||||
|
<mat-panel-title> |
||||
|
<mat-slide-toggle class="mat-slide" formControlName="showLegend" (click)="$event.stopPropagation()" |
||||
|
fxLayoutAlign="center"> |
||||
|
{{ 'widget-config.legend' | translate }} |
||||
|
</mat-slide-toggle> |
||||
|
</mat-panel-title> |
||||
|
</mat-expansion-panel-header> |
||||
|
<ng-template matExpansionPanelContent> |
||||
|
<div class="tb-form-row space-between"> |
||||
|
<div>{{ 'legend.position' | translate }}</div> |
||||
|
<mat-form-field class="medium-width" appearance="outline" subscriptSizing="dynamic"> |
||||
|
<mat-select formControlName="legendPosition"> |
||||
|
<mat-option *ngFor="let pos of legendPositions" [value]="pos"> |
||||
|
{{ legendPositionTranslationMap.get(pos) | translate }} |
||||
|
</mat-option> |
||||
|
</mat-select> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
<div class="tb-form-row space-between"> |
||||
|
<div>{{ 'legend.label' | translate }}</div> |
||||
|
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px"> |
||||
|
<tb-font-settings formControlName="legendLabelFont" |
||||
|
previewText="Wind"> |
||||
|
</tb-font-settings> |
||||
|
<tb-color-input asBoxInput |
||||
|
colorClearButton |
||||
|
formControlName="legendLabelColor"> |
||||
|
</tb-color-input> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="tb-form-row space-between"> |
||||
|
<div>{{ 'legend.value' | translate }}</div> |
||||
|
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px"> |
||||
|
<tb-font-settings formControlName="legendValueFont" |
||||
|
[previewText]="valuePreviewFn"> |
||||
|
</tb-font-settings> |
||||
|
<tb-color-input asBoxInput |
||||
|
colorClearButton |
||||
|
formControlName="legendValueColor"> |
||||
|
</tb-color-input> |
||||
|
</div> |
||||
|
</div> |
||||
|
</ng-template> |
||||
|
</mat-expansion-panel> |
||||
|
</div> |
||||
|
<div class="tb-form-panel tb-slide-toggle"> |
||||
|
<mat-expansion-panel class="tb-settings" [expanded]="pieChartWidgetConfigForm.get('showTooltip').value" [disabled]="!pieChartWidgetConfigForm.get('showTooltip').value"> |
||||
|
<mat-expansion-panel-header fxLayout="row wrap"> |
||||
|
<mat-panel-title> |
||||
|
<mat-slide-toggle class="mat-slide" formControlName="showTooltip" (click)="$event.stopPropagation()" |
||||
|
fxLayoutAlign="center"> |
||||
|
{{ 'widget-config.tooltip' | translate }} |
||||
|
</mat-slide-toggle> |
||||
|
</mat-panel-title> |
||||
|
</mat-expansion-panel-header> |
||||
|
<ng-template matExpansionPanelContent> |
||||
|
<div class="tb-form-row space-between column-xs"> |
||||
|
<div>{{ 'tooltip.value' | translate }}</div> |
||||
|
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px"> |
||||
|
<mat-form-field class="medium-width" appearance="outline" subscriptSizing="dynamic"> |
||||
|
<mat-select formControlName="tooltipValueType"> |
||||
|
<mat-option *ngFor="let type of latestChartTooltipValueTypes" [value]="type"> |
||||
|
{{ latestChartTooltipValueTypeTranslationMap.get(type) | translate }} |
||||
|
</mat-option> |
||||
|
</mat-select> |
||||
|
</mat-form-field> |
||||
|
<mat-form-field appearance="outline" class="number" subscriptSizing="dynamic"> |
||||
|
<input matInput formControlName="tooltipValueDecimals" type="number" min="0" max="15" step="1" placeholder="{{ 'widget-config.set' | translate }}"> |
||||
|
<div matSuffix fxHide.lt-md translate>widget-config.decimals-suffix</div> |
||||
|
</mat-form-field> |
||||
|
<tb-font-settings formControlName="tooltipValueFont" |
||||
|
[previewText]="tooltipValuePreviewFn"> |
||||
|
</tb-font-settings> |
||||
|
<tb-color-input asBoxInput |
||||
|
colorClearButton |
||||
|
formControlName="tooltipValueColor"> |
||||
|
</tb-color-input> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="tb-form-row space-between"> |
||||
|
<div>{{ 'tooltip.background-color' | translate }}</div> |
||||
|
<tb-color-input asBoxInput |
||||
|
colorClearButton |
||||
|
formControlName="tooltipBackgroundColor"> |
||||
|
</tb-color-input> |
||||
|
</div> |
||||
|
<div class="tb-form-row space-between"> |
||||
|
<div>{{ 'tooltip.background-blur' | translate }}</div> |
||||
|
<mat-form-field appearance="outline" class="number" subscriptSizing="dynamic"> |
||||
|
<input matInput formControlName="tooltipBackgroundBlur" type="number" min="0" step="1" placeholder="{{ 'widget-config.set' | translate }}"> |
||||
|
<div matSuffix>px</div> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
</ng-template> |
||||
|
</mat-expansion-panel> |
||||
|
</div> |
||||
|
<tb-echarts-animation-settings |
||||
|
formControlName="animation"> |
||||
|
</tb-echarts-animation-settings> |
||||
|
<div class="tb-form-panel"> |
||||
|
<div class="tb-form-panel-title" translate>widget-config.card-appearance</div> |
||||
|
<div class="tb-form-row space-between"> |
||||
|
<div>{{ 'widgets.background.background' | translate }}</div> |
||||
|
<tb-background-settings formControlName="background"> |
||||
|
</tb-background-settings> |
||||
|
</div> |
||||
|
<div class="tb-form-row space-between column-lt-md"> |
||||
|
<div translate>widget-config.show-card-buttons</div> |
||||
|
<mat-chip-listbox multiple formControlName="cardButtons"> |
||||
|
<mat-chip-option value="fullscreen">{{ 'fullscreen.fullscreen' | translate }}</mat-chip-option> |
||||
|
</mat-chip-listbox> |
||||
|
</div> |
||||
|
<div class="tb-form-row space-between"> |
||||
|
<div>{{ 'widget-config.card-border-radius' | translate }}</div> |
||||
|
<mat-form-field appearance="outline" subscriptSizing="dynamic"> |
||||
|
<input matInput formControlName="borderRadius" placeholder="{{ 'widget-config.set' | translate }}"> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
</div> |
||||
|
<tb-widget-actions-panel |
||||
|
formControlName="actions"> |
||||
|
</tb-widget-actions-panel> |
||||
|
</ng-container> |
||||
@ -0,0 +1,343 @@ |
|||||
|
///
|
||||
|
/// 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 } from '@angular/core'; |
||||
|
import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; |
||||
|
import { Store } from '@ngrx/store'; |
||||
|
import { AppState } from '@core/core.state'; |
||||
|
import { BasicWidgetConfigComponent } from '@home/components/widget/config/widget-config.component.models'; |
||||
|
import { WidgetConfigComponentData } from '@home/models/widget-component.models'; |
||||
|
import { |
||||
|
DataKey, |
||||
|
Datasource, |
||||
|
datasourcesHasAggregation, |
||||
|
datasourcesHasOnlyComparisonAggregation, |
||||
|
legendPositions, |
||||
|
legendPositionTranslationMap, |
||||
|
WidgetConfig |
||||
|
} from '@shared/models/widget.models'; |
||||
|
import { WidgetConfigComponent } from '@home/components/widget/widget-config.component'; |
||||
|
import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; |
||||
|
import { formatValue, isUndefined, mergeDeep } from '@core/utils'; |
||||
|
import { |
||||
|
getTimewindowConfig, |
||||
|
setTimewindowConfig |
||||
|
} from '@home/components/widget/config/timewindow-config-panel.component'; |
||||
|
import { |
||||
|
LatestChartTooltipValueType, |
||||
|
latestChartTooltipValueTypes, |
||||
|
latestChartTooltipValueTypeTranslations |
||||
|
} from '@home/components/widget/lib/chart/latest-chart.models'; |
||||
|
import { |
||||
|
pieChartWidgetDefaultSettings, |
||||
|
PieChartWidgetSettings |
||||
|
} from '@home/components/widget/lib/chart/pie-chart-widget.models'; |
||||
|
import { |
||||
|
pieChartLabelPositions, |
||||
|
pieChartLabelPositionTranslations |
||||
|
} from '@home/components/widget/lib/chart/pie-chart.models'; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-pie-chart-basic-config', |
||||
|
templateUrl: './pie-chart-basic-config.component.html', |
||||
|
styleUrls: ['../basic-config.scss'] |
||||
|
}) |
||||
|
export class PieChartBasicConfigComponent extends BasicWidgetConfigComponent { |
||||
|
|
||||
|
public get datasource(): Datasource { |
||||
|
const datasources: Datasource[] = this.pieChartWidgetConfigForm.get('datasources').value; |
||||
|
if (datasources && datasources.length) { |
||||
|
return datasources[0]; |
||||
|
} else { |
||||
|
return null; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public get displayTimewindowConfig(): boolean { |
||||
|
const datasources = this.pieChartWidgetConfigForm.get('datasources').value; |
||||
|
return datasourcesHasAggregation(datasources); |
||||
|
} |
||||
|
|
||||
|
public onlyHistoryTimewindow(): boolean { |
||||
|
const datasources = this.pieChartWidgetConfigForm.get('datasources').value; |
||||
|
return datasourcesHasOnlyComparisonAggregation(datasources); |
||||
|
} |
||||
|
|
||||
|
pieChartLabelPositions = pieChartLabelPositions; |
||||
|
|
||||
|
pieChartLabelPositionTranslationMap = pieChartLabelPositionTranslations; |
||||
|
|
||||
|
legendPositions = legendPositions; |
||||
|
|
||||
|
legendPositionTranslationMap = legendPositionTranslationMap; |
||||
|
|
||||
|
latestChartTooltipValueTypes = latestChartTooltipValueTypes; |
||||
|
|
||||
|
latestChartTooltipValueTypeTranslationMap = latestChartTooltipValueTypeTranslations; |
||||
|
|
||||
|
pieChartWidgetConfigForm: UntypedFormGroup; |
||||
|
|
||||
|
valuePreviewFn = this._valuePreviewFn.bind(this); |
||||
|
|
||||
|
tooltipValuePreviewFn = this._tooltipValuePreviewFn.bind(this); |
||||
|
|
||||
|
constructor(protected store: Store<AppState>, |
||||
|
protected widgetConfigComponent: WidgetConfigComponent, |
||||
|
private fb: UntypedFormBuilder) { |
||||
|
super(store, widgetConfigComponent); |
||||
|
} |
||||
|
|
||||
|
protected configForm(): UntypedFormGroup { |
||||
|
return this.pieChartWidgetConfigForm; |
||||
|
} |
||||
|
|
||||
|
protected defaultDataKeys(configData: WidgetConfigComponentData): DataKey[] { |
||||
|
return [{ name: 'windPower', label: 'Wind', type: DataKeyType.timeseries, units: '', decimals: 0, color: '#08872B' }, |
||||
|
{ name: 'solarPower', label: 'Solar', type: DataKeyType.timeseries, units: '', decimals: 0, color: '#FF4D5A' }, |
||||
|
{ name: 'hydroelectricPower', label: 'Hydroelectric', type: DataKeyType.timeseries, units: '', decimals: 0, color: '#FFDE30' }]; |
||||
|
} |
||||
|
|
||||
|
protected onConfigSet(configData: WidgetConfigComponentData) { |
||||
|
const settings: PieChartWidgetSettings = mergeDeep<PieChartWidgetSettings>({} as PieChartWidgetSettings, |
||||
|
pieChartWidgetDefaultSettings, configData.config.settings as PieChartWidgetSettings); |
||||
|
this.pieChartWidgetConfigForm = this.fb.group({ |
||||
|
timewindowConfig: [getTimewindowConfig(configData.config), []], |
||||
|
datasources: [configData.config.datasources, []], |
||||
|
|
||||
|
series: [this.getSeries(configData.config.datasources), []], |
||||
|
|
||||
|
showLabel: [settings.showLabel, []], |
||||
|
labelPosition: [settings.labelPosition, []], |
||||
|
labelFont: [settings.labelFont, []], |
||||
|
labelColor: [settings.labelColor, []], |
||||
|
|
||||
|
borderWidth: [settings.borderWidth, [Validators.min(0)]], |
||||
|
borderColor: [settings.borderColor, []], |
||||
|
|
||||
|
radius: [settings.radius, [Validators.min(0), Validators.max(100)]], |
||||
|
|
||||
|
clockwise: [settings.clockwise, []], |
||||
|
sortSeries: [settings.sortSeries, []], |
||||
|
|
||||
|
showTitle: [configData.config.showTitle, []], |
||||
|
title: [configData.config.title, []], |
||||
|
titleFont: [configData.config.titleFont, []], |
||||
|
titleColor: [configData.config.titleColor, []], |
||||
|
showTitleIcon: [configData.config.showTitleIcon, []], |
||||
|
titleIcon: [configData.config.titleIcon, []], |
||||
|
iconColor: [configData.config.iconColor, []], |
||||
|
|
||||
|
units: [configData.config.units, []], |
||||
|
decimals: [configData.config.decimals, []], |
||||
|
|
||||
|
animation: [settings.animation, []], |
||||
|
|
||||
|
showLegend: [settings.showLegend, []], |
||||
|
legendPosition: [settings.legendPosition, []], |
||||
|
legendLabelFont: [settings.legendLabelFont, []], |
||||
|
legendLabelColor: [settings.legendLabelColor, []], |
||||
|
legendValueFont: [settings.legendValueFont, []], |
||||
|
legendValueColor: [settings.legendValueColor, []], |
||||
|
|
||||
|
showTooltip: [settings.showTooltip, []], |
||||
|
tooltipValueType: [settings.tooltipValueType, []], |
||||
|
tooltipValueDecimals: [settings.tooltipValueDecimals, []], |
||||
|
tooltipValueFont: [settings.tooltipValueFont, []], |
||||
|
tooltipValueColor: [settings.tooltipValueColor, []], |
||||
|
tooltipBackgroundColor: [settings.tooltipBackgroundColor, []], |
||||
|
tooltipBackgroundBlur: [settings.tooltipBackgroundBlur, []], |
||||
|
|
||||
|
background: [settings.background, []], |
||||
|
|
||||
|
cardButtons: [this.getCardButtons(configData.config), []], |
||||
|
borderRadius: [configData.config.borderRadius, []], |
||||
|
|
||||
|
actions: [configData.config.actions || {}, []] |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
protected prepareOutputConfig(config: any): WidgetConfigComponentData { |
||||
|
setTimewindowConfig(this.widgetConfig.config, config.timewindowConfig); |
||||
|
this.widgetConfig.config.datasources = config.datasources; |
||||
|
this.setSeries(config.series, this.widgetConfig.config.datasources); |
||||
|
|
||||
|
this.widgetConfig.config.showTitle = config.showTitle; |
||||
|
this.widgetConfig.config.title = config.title; |
||||
|
this.widgetConfig.config.titleFont = config.titleFont; |
||||
|
this.widgetConfig.config.titleColor = config.titleColor; |
||||
|
|
||||
|
this.widgetConfig.config.showTitleIcon = config.showTitleIcon; |
||||
|
this.widgetConfig.config.titleIcon = config.titleIcon; |
||||
|
this.widgetConfig.config.iconColor = config.iconColor; |
||||
|
|
||||
|
this.widgetConfig.config.settings = this.widgetConfig.config.settings || {}; |
||||
|
|
||||
|
this.widgetConfig.config.settings.showLabel = config.showLabel; |
||||
|
this.widgetConfig.config.settings.labelPosition = config.labelPosition; |
||||
|
this.widgetConfig.config.settings.labelFont = config.labelFont; |
||||
|
this.widgetConfig.config.settings.labelColor = config.labelColor; |
||||
|
|
||||
|
this.widgetConfig.config.settings.borderWidth = config.borderWidth; |
||||
|
this.widgetConfig.config.settings.borderColor = config.borderColor; |
||||
|
|
||||
|
this.widgetConfig.config.settings.radius = config.radius; |
||||
|
|
||||
|
this.widgetConfig.config.settings.clockwise = config.clockwise; |
||||
|
this.widgetConfig.config.settings.sortSeries = config.sortSeries; |
||||
|
|
||||
|
this.widgetConfig.config.units = config.units; |
||||
|
this.widgetConfig.config.decimals = config.decimals; |
||||
|
|
||||
|
this.widgetConfig.config.settings.animation = config.animation; |
||||
|
|
||||
|
this.widgetConfig.config.settings.showLegend = config.showLegend; |
||||
|
this.widgetConfig.config.settings.legendPosition = config.legendPosition; |
||||
|
this.widgetConfig.config.settings.legendLabelFont = config.legendLabelFont; |
||||
|
this.widgetConfig.config.settings.legendLabelColor = config.legendLabelColor; |
||||
|
this.widgetConfig.config.settings.legendValueFont = config.legendValueFont; |
||||
|
this.widgetConfig.config.settings.legendValueColor = config.legendValueColor; |
||||
|
|
||||
|
this.widgetConfig.config.settings.showTooltip = config.showTooltip; |
||||
|
this.widgetConfig.config.settings.tooltipValueType = config.tooltipValueType; |
||||
|
this.widgetConfig.config.settings.tooltipValueDecimals = config.tooltipValueDecimals; |
||||
|
this.widgetConfig.config.settings.tooltipValueFont = config.tooltipValueFont; |
||||
|
this.widgetConfig.config.settings.tooltipValueColor = config.tooltipValueColor; |
||||
|
this.widgetConfig.config.settings.tooltipBackgroundColor = config.tooltipBackgroundColor; |
||||
|
this.widgetConfig.config.settings.tooltipBackgroundBlur = config.tooltipBackgroundBlur; |
||||
|
|
||||
|
this.widgetConfig.config.settings.background = config.background; |
||||
|
|
||||
|
this.setCardButtons(config.cardButtons, this.widgetConfig.config); |
||||
|
this.widgetConfig.config.borderRadius = config.borderRadius; |
||||
|
|
||||
|
this.widgetConfig.config.actions = config.actions; |
||||
|
|
||||
|
return this.widgetConfig; |
||||
|
} |
||||
|
|
||||
|
protected validatorTriggers(): string[] { |
||||
|
return ['showLabel', 'showTitle', 'showTitleIcon', 'showLegend', 'showTooltip']; |
||||
|
} |
||||
|
|
||||
|
protected updateValidators(emitEvent: boolean, trigger?: string) { |
||||
|
const showLabel: boolean = this.pieChartWidgetConfigForm.get('showLabel').value; |
||||
|
const showTitle: boolean = this.pieChartWidgetConfigForm.get('showTitle').value; |
||||
|
const showTitleIcon: boolean = this.pieChartWidgetConfigForm.get('showTitleIcon').value; |
||||
|
const showLegend: boolean = this.pieChartWidgetConfigForm.get('showLegend').value; |
||||
|
const showTooltip: boolean = this.pieChartWidgetConfigForm.get('showTooltip').value; |
||||
|
|
||||
|
if (showLabel) { |
||||
|
this.pieChartWidgetConfigForm.get('labelPosition').enable(); |
||||
|
this.pieChartWidgetConfigForm.get('labelFont').enable(); |
||||
|
this.pieChartWidgetConfigForm.get('labelColor').enable(); |
||||
|
} else { |
||||
|
this.pieChartWidgetConfigForm.get('labelPosition').disable(); |
||||
|
this.pieChartWidgetConfigForm.get('labelFont').disable(); |
||||
|
this.pieChartWidgetConfigForm.get('labelColor').disable(); |
||||
|
} |
||||
|
|
||||
|
if (showTitle) { |
||||
|
this.pieChartWidgetConfigForm.get('title').enable(); |
||||
|
this.pieChartWidgetConfigForm.get('titleFont').enable(); |
||||
|
this.pieChartWidgetConfigForm.get('titleColor').enable(); |
||||
|
this.pieChartWidgetConfigForm.get('showTitleIcon').enable({emitEvent: false}); |
||||
|
if (showTitleIcon) { |
||||
|
this.pieChartWidgetConfigForm.get('titleIcon').enable(); |
||||
|
this.pieChartWidgetConfigForm.get('iconColor').enable(); |
||||
|
} else { |
||||
|
this.pieChartWidgetConfigForm.get('titleIcon').disable(); |
||||
|
this.pieChartWidgetConfigForm.get('iconColor').disable(); |
||||
|
} |
||||
|
} else { |
||||
|
this.pieChartWidgetConfigForm.get('title').disable(); |
||||
|
this.pieChartWidgetConfigForm.get('titleFont').disable(); |
||||
|
this.pieChartWidgetConfigForm.get('titleColor').disable(); |
||||
|
this.pieChartWidgetConfigForm.get('showTitleIcon').disable({emitEvent: false}); |
||||
|
this.pieChartWidgetConfigForm.get('titleIcon').disable(); |
||||
|
this.pieChartWidgetConfigForm.get('iconColor').disable(); |
||||
|
} |
||||
|
if (showLegend) { |
||||
|
this.pieChartWidgetConfigForm.get('legendPosition').enable(); |
||||
|
this.pieChartWidgetConfigForm.get('legendLabelFont').enable(); |
||||
|
this.pieChartWidgetConfigForm.get('legendLabelColor').enable(); |
||||
|
this.pieChartWidgetConfigForm.get('legendValueFont').enable(); |
||||
|
this.pieChartWidgetConfigForm.get('legendValueColor').enable(); |
||||
|
} else { |
||||
|
this.pieChartWidgetConfigForm.get('legendPosition').disable(); |
||||
|
this.pieChartWidgetConfigForm.get('legendLabelFont').disable(); |
||||
|
this.pieChartWidgetConfigForm.get('legendLabelColor').disable(); |
||||
|
this.pieChartWidgetConfigForm.get('legendValueFont').disable(); |
||||
|
this.pieChartWidgetConfigForm.get('legendValueColor').disable(); |
||||
|
} |
||||
|
if (showTooltip) { |
||||
|
this.pieChartWidgetConfigForm.get('tooltipValueType').enable(); |
||||
|
this.pieChartWidgetConfigForm.get('tooltipValueDecimals').enable(); |
||||
|
this.pieChartWidgetConfigForm.get('tooltipValueFont').enable(); |
||||
|
this.pieChartWidgetConfigForm.get('tooltipValueColor').enable(); |
||||
|
this.pieChartWidgetConfigForm.get('tooltipBackgroundColor').enable(); |
||||
|
this.pieChartWidgetConfigForm.get('tooltipBackgroundBlur').enable(); |
||||
|
} else { |
||||
|
this.pieChartWidgetConfigForm.get('tooltipValueType').disable(); |
||||
|
this.pieChartWidgetConfigForm.get('tooltipValueDecimals').disable(); |
||||
|
this.pieChartWidgetConfigForm.get('tooltipValueFont').disable(); |
||||
|
this.pieChartWidgetConfigForm.get('tooltipValueColor').disable(); |
||||
|
this.pieChartWidgetConfigForm.get('tooltipBackgroundColor').disable(); |
||||
|
this.pieChartWidgetConfigForm.get('tooltipBackgroundBlur').disable(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private getSeries(datasources?: Datasource[]): DataKey[] { |
||||
|
if (datasources && datasources.length) { |
||||
|
return datasources[0].dataKeys || []; |
||||
|
} |
||||
|
return []; |
||||
|
} |
||||
|
|
||||
|
private setSeries(series: DataKey[], datasources?: Datasource[]) { |
||||
|
if (datasources && datasources.length) { |
||||
|
datasources[0].dataKeys = series; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private getCardButtons(config: WidgetConfig): string[] { |
||||
|
const buttons: string[] = []; |
||||
|
if (isUndefined(config.enableFullscreen) || config.enableFullscreen) { |
||||
|
buttons.push('fullscreen'); |
||||
|
} |
||||
|
return buttons; |
||||
|
} |
||||
|
|
||||
|
private setCardButtons(buttons: string[], config: WidgetConfig) { |
||||
|
config.enableFullscreen = buttons.includes('fullscreen'); |
||||
|
} |
||||
|
|
||||
|
private _valuePreviewFn(): string { |
||||
|
const units: string = this.pieChartWidgetConfigForm.get('units').value; |
||||
|
const decimals: number = this.pieChartWidgetConfigForm.get('decimals').value; |
||||
|
return formatValue(110, decimals, units, false); |
||||
|
} |
||||
|
|
||||
|
private _tooltipValuePreviewFn(): string { |
||||
|
const tooltipValueType: LatestChartTooltipValueType = this.pieChartWidgetConfigForm.get('tooltipValueType').value; |
||||
|
const decimals: number = this.pieChartWidgetConfigForm.get('tooltipValueDecimals').value; |
||||
|
if (tooltipValueType === LatestChartTooltipValueType.percentage) { |
||||
|
return formatValue(35, decimals, '%', false); |
||||
|
} else { |
||||
|
const units: string = this.pieChartWidgetConfigForm.get('units').value; |
||||
|
return formatValue(110, decimals, units, false); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -1,37 +0,0 @@ |
|||||
<!-- |
|
||||
|
|
||||
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. |
|
||||
|
|
||||
--> |
|
||||
<div class="tb-doughnut-panel" [style]="backgroundStyle$ | async"> |
|
||||
<div class="tb-doughnut-overlay" [style]="overlayStyle"></div> |
|
||||
<ng-container *ngTemplateOutlet="widgetTitlePanel"></ng-container> |
|
||||
<div #doughnutContent class="tb-doughnut-content" [class]="legendClass"> |
|
||||
<div #doughnutShape class="tb-doughnut-shape"> |
|
||||
</div> |
|
||||
<div *ngIf="showLegend" #doughnutLegend class="tb-doughnut-legend"> |
|
||||
<div class="tb-doughnut-legend-item" *ngFor="let legendItem of legendItems" [class]="{'pointer': !legendItem.total && legendItem.hasValue}" |
|
||||
(mouseenter)="onLegendItemEnter(legendItem)" |
|
||||
(mouseleave)="onLegendItemLeave(legendItem)" |
|
||||
(click)="toggleLegendItem(legendItem)"> |
|
||||
<div class="tb-doughnut-legend-item-label"> |
|
||||
<div class="tb-doughnut-legend-item-label-circle" [style]="{background: (legendItem.enabled && legendItem.hasValue) ? legendItem.color : null}"></div> |
|
||||
<div [style]="(legendItem.enabled && legendItem.hasValue) ? legendLabelStyle : disabledLegendLabelStyle">{{ legendItem.label }}</div> |
|
||||
</div> |
|
||||
<div [style]="(legendItem.enabled && legendItem.hasValue) ? legendValueStyle : disabledLegendValueStyle" class="tb-doughnut-legend-item-value">{{ legendItem.value }}</div> |
|
||||
</div> |
|
||||
</div> |
|
||||
</div> |
|
||||
</div> |
|
||||
@ -0,0 +1,24 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
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. |
||||
|
|
||||
|
--> |
||||
|
<tb-latest-chart |
||||
|
#latestChart |
||||
|
[ctx]="ctx" |
||||
|
[settings]="settings" |
||||
|
[widgetTitlePanel]="widgetTitlePanel" |
||||
|
[callbacks]="callbacks" |
||||
|
></tb-latest-chart> |
||||
@ -0,0 +1,37 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
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. |
||||
|
|
||||
|
--> |
||||
|
<div class="tb-latest-chart-panel" [style]="backgroundStyle$ | async"> |
||||
|
<div class="tb-latest-chart-overlay" [style]="overlayStyle"></div> |
||||
|
<ng-container *ngTemplateOutlet="widgetTitlePanel"></ng-container> |
||||
|
<div #chartContent class="tb-latest-chart-content" [class]="legendClass"> |
||||
|
<div #chartShape class="tb-latest-chart-shape"> |
||||
|
</div> |
||||
|
<div *ngIf="showLegend" #chartLegend class="tb-latest-chart-legend"> |
||||
|
<div class="tb-latest-chart-legend-item" *ngFor="let legendItem of legendItems" [class]="{'pointer': !legendItem.total && legendItem.hasValue}" |
||||
|
(mouseenter)="onLegendItemEnter(legendItem)" |
||||
|
(mouseleave)="onLegendItemLeave(legendItem)" |
||||
|
(click)="toggleLegendItem(legendItem)"> |
||||
|
<div class="tb-latest-chart-legend-item-label"> |
||||
|
<div class="tb-latest-chart-legend-item-label-circle" [style]="{background: (!legendItem.dataKey?.hidden && legendItem.hasValue) ? legendItem.color : null}"></div> |
||||
|
<div [style]="(!legendItem.dataKey?.hidden && legendItem.hasValue) ? legendLabelStyle : disabledLegendLabelStyle">{{ legendItem.label }}</div> |
||||
|
</div> |
||||
|
<div [style]="(!legendItem.dataKey?.hidden && legendItem.hasValue) ? legendValueStyle : disabledLegendValueStyle" class="tb-latest-chart-legend-item-value">{{ legendItem.value }}</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
@ -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<HTMLElement>, renderer: Renderer2) => TbLatestChart<LatestChartSettings>; |
||||
|
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<HTMLElement>; |
||||
|
|
||||
|
@ViewChild('chartShape', {static: false}) |
||||
|
chartShape: ElementRef<HTMLElement>; |
||||
|
|
||||
|
@ViewChild('chartLegend', {static: false}) |
||||
|
chartLegend: ElementRef<HTMLElement>; |
||||
|
|
||||
|
@Input() |
||||
|
ctx: WidgetContext; |
||||
|
|
||||
|
@Input() |
||||
|
widgetTitlePanel: TemplateRef<any>; |
||||
|
|
||||
|
@Input() |
||||
|
callbacks: LatestChartComponentCallbacks; |
||||
|
|
||||
|
@Input() |
||||
|
settings: LatestChartWidgetSettings; |
||||
|
|
||||
|
showLegend: boolean; |
||||
|
legendClass: string; |
||||
|
|
||||
|
backgroundStyle$: Observable<ComponentStyle>; |
||||
|
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<LatestChartSettings>; |
||||
|
|
||||
|
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'); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -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, string>( |
||||
|
[ |
||||
|
[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; |
||||
|
}; |
||||
@ -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<S extends LatestChartSettings> { |
||||
|
|
||||
|
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<S>, |
||||
|
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; |
||||
|
|
||||
|
} |
||||
@ -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<any>; |
||||
|
|
||||
|
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(); |
||||
|
} |
||||
|
} |
||||
@ -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<PieChartSettings> => ({ |
||||
|
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 |
||||
|
}); |
||||
@ -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, string>( |
||||
|
[ |
||||
|
[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 |
||||
|
}; |
||||
|
|
||||
@ -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<PieChartSettings> { |
||||
|
|
||||
|
private totalValueColor: ColorProcessor; |
||||
|
private totalTextNode: Text; |
||||
|
|
||||
|
constructor(ctx: WidgetContext, |
||||
|
inputSettings: DeepPartial<PieChartSettings>, |
||||
|
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); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,184 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
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. |
||||
|
|
||||
|
--> |
||||
|
<ng-container [formGroup]="pieChartWidgetSettingsForm"> |
||||
|
<div class="tb-form-panel"> |
||||
|
<div class="tb-form-panel-title" translate>widgets.pie-chart.pie-chart-card-style</div> |
||||
|
<div class="tb-form-row space-between column-xs"> |
||||
|
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="showLabel"> |
||||
|
{{ 'widgets.pie-chart.label' | translate }} |
||||
|
</mat-slide-toggle> |
||||
|
<div fxFlex fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px"> |
||||
|
<mat-form-field fxFlex appearance="outline" subscriptSizing="dynamic"> |
||||
|
<mat-select formControlName="labelPosition"> |
||||
|
<mat-option *ngFor="let position of pieChartLabelPositions" [value]="position"> |
||||
|
{{ pieChartLabelPositionTranslationMap.get(position) | translate }} |
||||
|
</mat-option> |
||||
|
</mat-select> |
||||
|
</mat-form-field> |
||||
|
<tb-font-settings formControlName="labelFont" |
||||
|
disabledLineHeight |
||||
|
forceSizeUnit="px" |
||||
|
[previewText]="'Wind<br/>55%'"> |
||||
|
</tb-font-settings> |
||||
|
<tb-color-input asBoxInput |
||||
|
colorClearButton |
||||
|
formControlName="labelColor"> |
||||
|
</tb-color-input> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="tb-form-row space-between"> |
||||
|
<div>{{ 'widgets.pie-chart.border' | translate }}</div> |
||||
|
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px"> |
||||
|
<mat-form-field appearance="outline" class="number" subscriptSizing="dynamic"> |
||||
|
<input matInput formControlName="borderWidth" type="number" min="0" step="1" placeholder="{{ 'widget-config.set' | translate }}"> |
||||
|
<div matSuffix>px</div> |
||||
|
</mat-form-field> |
||||
|
<tb-color-input asBoxInput |
||||
|
colorClearButton |
||||
|
formControlName="borderColor"> |
||||
|
</tb-color-input> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="tb-form-row space-between"> |
||||
|
<div>{{ 'widgets.pie-chart.radius' | translate }}</div> |
||||
|
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px"> |
||||
|
<mat-form-field appearance="outline" class="number" subscriptSizing="dynamic"> |
||||
|
<input matInput formControlName="radius" type="number" min="0" max="100" step="1" placeholder="{{ 'widget-config.set' | translate }}"> |
||||
|
<div matSuffix>%</div> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="tb-form-row"> |
||||
|
<mat-slide-toggle class="mat-slide" formControlName="clockwise"> |
||||
|
{{ 'widgets.latest-chart.clockwise-layout' | translate }} |
||||
|
</mat-slide-toggle> |
||||
|
</div> |
||||
|
<div class="tb-form-row"> |
||||
|
<mat-slide-toggle class="mat-slide" formControlName="sortSeries"> |
||||
|
{{ 'widgets.latest-chart.sort-series' | translate }} |
||||
|
</mat-slide-toggle> |
||||
|
</div> |
||||
|
<div class="tb-form-panel tb-slide-toggle"> |
||||
|
<mat-expansion-panel class="tb-settings" [expanded]="pieChartWidgetSettingsForm.get('showLegend').value" [disabled]="!pieChartWidgetSettingsForm.get('showLegend').value"> |
||||
|
<mat-expansion-panel-header fxLayout="row wrap"> |
||||
|
<mat-panel-title> |
||||
|
<mat-slide-toggle class="mat-slide" formControlName="showLegend" (click)="$event.stopPropagation()" |
||||
|
fxLayoutAlign="center"> |
||||
|
{{ 'widget-config.legend' | translate }} |
||||
|
</mat-slide-toggle> |
||||
|
</mat-panel-title> |
||||
|
</mat-expansion-panel-header> |
||||
|
<ng-template matExpansionPanelContent> |
||||
|
<div class="tb-form-row space-between"> |
||||
|
<div>{{ 'legend.position' | translate }}</div> |
||||
|
<mat-form-field class="medium-width" appearance="outline" subscriptSizing="dynamic"> |
||||
|
<mat-select formControlName="legendPosition"> |
||||
|
<mat-option *ngFor="let pos of legendPositions" [value]="pos"> |
||||
|
{{ legendPositionTranslationMap.get(pos) | translate }} |
||||
|
</mat-option> |
||||
|
</mat-select> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
<div class="tb-form-row space-between"> |
||||
|
<div>{{ 'legend.label' | translate }}</div> |
||||
|
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px"> |
||||
|
<tb-font-settings formControlName="legendLabelFont" |
||||
|
previewText="Wind power"> |
||||
|
</tb-font-settings> |
||||
|
<tb-color-input asBoxInput |
||||
|
colorClearButton |
||||
|
formControlName="legendLabelColor"> |
||||
|
</tb-color-input> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="tb-form-row space-between"> |
||||
|
<div>{{ 'legend.value' | translate }}</div> |
||||
|
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px"> |
||||
|
<tb-font-settings formControlName="legendValueFont" |
||||
|
[previewText]="valuePreviewFn"> |
||||
|
</tb-font-settings> |
||||
|
<tb-color-input asBoxInput |
||||
|
colorClearButton |
||||
|
formControlName="legendValueColor"> |
||||
|
</tb-color-input> |
||||
|
</div> |
||||
|
</div> |
||||
|
</ng-template> |
||||
|
</mat-expansion-panel> |
||||
|
</div> |
||||
|
<div class="tb-form-panel tb-slide-toggle"> |
||||
|
<mat-expansion-panel class="tb-settings" [expanded]="pieChartWidgetSettingsForm.get('showTooltip').value" [disabled]="!pieChartWidgetSettingsForm.get('showTooltip').value"> |
||||
|
<mat-expansion-panel-header fxLayout="row wrap"> |
||||
|
<mat-panel-title> |
||||
|
<mat-slide-toggle class="mat-slide" formControlName="showTooltip" (click)="$event.stopPropagation()" |
||||
|
fxLayoutAlign="center"> |
||||
|
{{ 'widget-config.tooltip' | translate }} |
||||
|
</mat-slide-toggle> |
||||
|
</mat-panel-title> |
||||
|
</mat-expansion-panel-header> |
||||
|
<ng-template matExpansionPanelContent> |
||||
|
<div class="tb-form-row space-between column-xs"> |
||||
|
<div>{{ 'tooltip.value' | translate }}</div> |
||||
|
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px"> |
||||
|
<mat-form-field class="medium-width" appearance="outline" subscriptSizing="dynamic"> |
||||
|
<mat-select formControlName="tooltipValueType"> |
||||
|
<mat-option *ngFor="let type of latestChartTooltipValueTypes" [value]="type"> |
||||
|
{{ latestChartTooltipValueTypeTranslationMap.get(type) | translate }} |
||||
|
</mat-option> |
||||
|
</mat-select> |
||||
|
</mat-form-field> |
||||
|
<mat-form-field appearance="outline" class="number" subscriptSizing="dynamic"> |
||||
|
<input matInput formControlName="tooltipValueDecimals" type="number" min="0" max="15" step="1" placeholder="{{ 'widget-config.set' | translate }}"> |
||||
|
<div matSuffix fxHide.lt-md translate>widget-config.decimals-suffix</div> |
||||
|
</mat-form-field> |
||||
|
<tb-font-settings formControlName="tooltipValueFont" |
||||
|
[previewText]="tooltipValuePreviewFn"> |
||||
|
</tb-font-settings> |
||||
|
<tb-color-input asBoxInput |
||||
|
colorClearButton |
||||
|
formControlName="tooltipValueColor"> |
||||
|
</tb-color-input> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="tb-form-row space-between"> |
||||
|
<div>{{ 'tooltip.background-color' | translate }}</div> |
||||
|
<tb-color-input asBoxInput |
||||
|
colorClearButton |
||||
|
formControlName="tooltipBackgroundColor"> |
||||
|
</tb-color-input> |
||||
|
</div> |
||||
|
<div class="tb-form-row space-between"> |
||||
|
<div>{{ 'tooltip.background-blur' | translate }}</div> |
||||
|
<mat-form-field appearance="outline" class="number" subscriptSizing="dynamic"> |
||||
|
<input matInput formControlName="tooltipBackgroundBlur" type="number" min="0" step="1" placeholder="{{ 'widget-config.set' | translate }}"> |
||||
|
<div matSuffix>px</div> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
</ng-template> |
||||
|
</mat-expansion-panel> |
||||
|
</div> |
||||
|
<tb-echarts-animation-settings |
||||
|
formControlName="animation"> |
||||
|
</tb-echarts-animation-settings> |
||||
|
<div class="tb-form-row space-between"> |
||||
|
<div>{{ 'widgets.background.background' | translate }}</div> |
||||
|
<tb-background-settings formControlName="background"> |
||||
|
</tb-background-settings> |
||||
|
</div> |
||||
|
</div> |
||||
|
</ng-container> |
||||
@ -0,0 +1,182 @@ |
|||||
|
///
|
||||
|
/// 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 } from '@angular/core'; |
||||
|
import { |
||||
|
legendPositions, |
||||
|
legendPositionTranslationMap, |
||||
|
WidgetSettings, |
||||
|
WidgetSettingsComponent |
||||
|
} from '@shared/models/widget.models'; |
||||
|
import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; |
||||
|
import { Store } from '@ngrx/store'; |
||||
|
import { AppState } from '@core/core.state'; |
||||
|
import { formatValue, mergeDeep } from '@core/utils'; |
||||
|
import { |
||||
|
LatestChartTooltipValueType, |
||||
|
latestChartTooltipValueTypes, |
||||
|
latestChartTooltipValueTypeTranslations |
||||
|
} from '@home/components/widget/lib/chart/latest-chart.models'; |
||||
|
import { |
||||
|
pieChartLabelPositions, |
||||
|
pieChartLabelPositionTranslations |
||||
|
} from '@home/components/widget/lib/chart/pie-chart.models'; |
||||
|
import { |
||||
|
pieChartWidgetDefaultSettings, |
||||
|
PieChartWidgetSettings |
||||
|
} from '@home/components/widget/lib/chart/pie-chart-widget.models'; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-pie-chart-widget-settings', |
||||
|
templateUrl: './pie-chart-widget-settings.component.html', |
||||
|
styleUrls: [] |
||||
|
}) |
||||
|
export class PieChartWidgetSettingsComponent extends WidgetSettingsComponent { |
||||
|
|
||||
|
pieChartLabelPositions = pieChartLabelPositions; |
||||
|
|
||||
|
pieChartLabelPositionTranslationMap = pieChartLabelPositionTranslations; |
||||
|
|
||||
|
legendPositions = legendPositions; |
||||
|
|
||||
|
legendPositionTranslationMap = legendPositionTranslationMap; |
||||
|
|
||||
|
latestChartTooltipValueTypes = latestChartTooltipValueTypes; |
||||
|
|
||||
|
latestChartTooltipValueTypeTranslationMap = latestChartTooltipValueTypeTranslations; |
||||
|
|
||||
|
pieChartWidgetSettingsForm: UntypedFormGroup; |
||||
|
|
||||
|
valuePreviewFn = this._valuePreviewFn.bind(this); |
||||
|
|
||||
|
tooltipValuePreviewFn = this._tooltipValuePreviewFn.bind(this); |
||||
|
|
||||
|
constructor(protected store: Store<AppState>, |
||||
|
private fb: UntypedFormBuilder) { |
||||
|
super(store); |
||||
|
} |
||||
|
|
||||
|
protected settingsForm(): UntypedFormGroup { |
||||
|
return this.pieChartWidgetSettingsForm; |
||||
|
} |
||||
|
|
||||
|
protected defaultSettings(): WidgetSettings { |
||||
|
return mergeDeep<PieChartWidgetSettings>({} as PieChartWidgetSettings, pieChartWidgetDefaultSettings); |
||||
|
} |
||||
|
|
||||
|
protected onSettingsSet(settings: WidgetSettings) { |
||||
|
this.pieChartWidgetSettingsForm = this.fb.group({ |
||||
|
showLabel: [settings.showLabel, []], |
||||
|
labelPosition: [settings.labelPosition, []], |
||||
|
labelFont: [settings.labelFont, []], |
||||
|
labelColor: [settings.labelColor, []], |
||||
|
|
||||
|
borderWidth: [settings.borderWidth, [Validators.min(0)]], |
||||
|
borderColor: [settings.borderColor, []], |
||||
|
|
||||
|
radius: [settings.radius, [Validators.min(0), Validators.max(100)]], |
||||
|
|
||||
|
clockwise: [settings.clockwise, []], |
||||
|
sortSeries: [settings.sortSeries, []], |
||||
|
|
||||
|
animation: [settings.animation, []], |
||||
|
|
||||
|
showLegend: [settings.showLegend, []], |
||||
|
legendPosition: [settings.legendPosition, []], |
||||
|
legendLabelFont: [settings.legendLabelFont, []], |
||||
|
legendLabelColor: [settings.legendLabelColor, []], |
||||
|
legendValueFont: [settings.legendValueFont, []], |
||||
|
legendValueColor: [settings.legendValueColor, []], |
||||
|
|
||||
|
showTooltip: [settings.showTooltip, []], |
||||
|
tooltipValueType: [settings.tooltipValueType, []], |
||||
|
tooltipValueDecimals: [settings.tooltipValueDecimals, []], |
||||
|
tooltipValueFont: [settings.tooltipValueFont, []], |
||||
|
tooltipValueColor: [settings.tooltipValueColor, []], |
||||
|
tooltipBackgroundColor: [settings.tooltipBackgroundColor, []], |
||||
|
tooltipBackgroundBlur: [settings.tooltipBackgroundBlur, []], |
||||
|
|
||||
|
background: [settings.background, []] |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
protected validatorTriggers(): string[] { |
||||
|
return ['showLabel', 'showLegend', 'showTooltip']; |
||||
|
} |
||||
|
|
||||
|
protected updateValidators(emitEvent: boolean) { |
||||
|
const showLabel: boolean = this.pieChartWidgetSettingsForm.get('showLabel').value; |
||||
|
const showLegend: boolean = this.pieChartWidgetSettingsForm.get('showLegend').value; |
||||
|
const showTooltip: boolean = this.pieChartWidgetSettingsForm.get('showTooltip').value; |
||||
|
|
||||
|
if (showLabel) { |
||||
|
this.pieChartWidgetSettingsForm.get('labelPosition').enable(); |
||||
|
this.pieChartWidgetSettingsForm.get('labelFont').enable(); |
||||
|
this.pieChartWidgetSettingsForm.get('labelColor').enable(); |
||||
|
} else { |
||||
|
this.pieChartWidgetSettingsForm.get('labelPosition').disable(); |
||||
|
this.pieChartWidgetSettingsForm.get('labelFont').disable(); |
||||
|
this.pieChartWidgetSettingsForm.get('labelColor').disable(); |
||||
|
} |
||||
|
|
||||
|
if (showLegend) { |
||||
|
this.pieChartWidgetSettingsForm.get('legendPosition').enable(); |
||||
|
this.pieChartWidgetSettingsForm.get('legendLabelFont').enable(); |
||||
|
this.pieChartWidgetSettingsForm.get('legendLabelColor').enable(); |
||||
|
this.pieChartWidgetSettingsForm.get('legendValueFont').enable(); |
||||
|
this.pieChartWidgetSettingsForm.get('legendValueColor').enable(); |
||||
|
} else { |
||||
|
this.pieChartWidgetSettingsForm.get('legendPosition').disable(); |
||||
|
this.pieChartWidgetSettingsForm.get('legendLabelFont').disable(); |
||||
|
this.pieChartWidgetSettingsForm.get('legendLabelColor').disable(); |
||||
|
this.pieChartWidgetSettingsForm.get('legendValueFont').disable(); |
||||
|
this.pieChartWidgetSettingsForm.get('legendValueColor').disable(); |
||||
|
} |
||||
|
if (showTooltip) { |
||||
|
this.pieChartWidgetSettingsForm.get('tooltipValueType').enable(); |
||||
|
this.pieChartWidgetSettingsForm.get('tooltipValueDecimals').enable(); |
||||
|
this.pieChartWidgetSettingsForm.get('tooltipValueFont').enable(); |
||||
|
this.pieChartWidgetSettingsForm.get('tooltipValueColor').enable(); |
||||
|
this.pieChartWidgetSettingsForm.get('tooltipBackgroundColor').enable(); |
||||
|
this.pieChartWidgetSettingsForm.get('tooltipBackgroundBlur').enable(); |
||||
|
} else { |
||||
|
this.pieChartWidgetSettingsForm.get('tooltipValueType').disable(); |
||||
|
this.pieChartWidgetSettingsForm.get('tooltipValueDecimals').disable(); |
||||
|
this.pieChartWidgetSettingsForm.get('tooltipValueFont').disable(); |
||||
|
this.pieChartWidgetSettingsForm.get('tooltipValueColor').disable(); |
||||
|
this.pieChartWidgetSettingsForm.get('tooltipBackgroundColor').disable(); |
||||
|
this.pieChartWidgetSettingsForm.get('tooltipBackgroundBlur').disable(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private _valuePreviewFn(): string { |
||||
|
const units: string = this.widgetConfig.config.units; |
||||
|
const decimals: number = this.widgetConfig.config.decimals; |
||||
|
return formatValue(110, decimals, units, false); |
||||
|
} |
||||
|
|
||||
|
private _tooltipValuePreviewFn(): string { |
||||
|
const tooltipValueType: LatestChartTooltipValueType = this.pieChartWidgetSettingsForm.get('tooltipValueType').value; |
||||
|
const decimals: number = this.pieChartWidgetSettingsForm.get('tooltipValueDecimals').value; |
||||
|
if (tooltipValueType === LatestChartTooltipValueType.percentage) { |
||||
|
return formatValue(35, decimals, '%', false); |
||||
|
} else { |
||||
|
const units: string = this.widgetConfig.config.units; |
||||
|
return formatValue(110, decimals, units, false); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
} |
||||
Loading…
Reference in new issue