92 changed files with 3070 additions and 490 deletions
File diff suppressed because one or more lines are too long
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,58 @@ |
|||
<?xml version="1.0" encoding="UTF-8" ?> |
|||
<!-- |
|||
|
|||
Copyright © 2016-2023 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. |
|||
|
|||
--> |
|||
<!DOCTYPE configuration> |
|||
<configuration scan="true" scanPeriod="10 seconds"> |
|||
|
|||
<appender name="fileLogAppender" |
|||
class="ch.qos.logback.core.rolling.RollingFileAppender"> |
|||
<file>/var/log/thingsboard/${TB_SERVICE_ID}/thingsboard.log</file> |
|||
<rollingPolicy |
|||
class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy"> |
|||
<fileNamePattern>/var/log/thingsboard/${TB_SERVICE_ID}/thingsboard.%d{yyyy-MM-dd}.%i.log</fileNamePattern> |
|||
<maxFileSize>100MB</maxFileSize> |
|||
<maxHistory>30</maxHistory> |
|||
<totalSizeCap>3GB</totalSizeCap> |
|||
</rollingPolicy> |
|||
<encoder> |
|||
<pattern>%d{ISO8601} [%thread] %-5level %logger{36} - %msg%n</pattern> |
|||
</encoder> |
|||
</appender> |
|||
|
|||
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> |
|||
<encoder> |
|||
<pattern>%d{ISO8601} [%thread] %-5level %logger{36} - %msg%n</pattern> |
|||
</encoder> |
|||
</appender> |
|||
|
|||
<logger name="org.thingsboard.server" level="INFO" /> |
|||
<logger name="com.google.common.util.concurrent.AggregateFuture" level="OFF" /> |
|||
<logger name="org.apache.kafka.common.utils.AppInfoParser" level="WARN"/> |
|||
<logger name="org.apache.kafka.clients" level="WARN"/> |
|||
<logger name="com.microsoft.azure.servicebus.primitives.CoreMessageReceiver" level="OFF" /> |
|||
|
|||
|
|||
<logger name="org.thingsboard.server.service" level="DEBUG"/> |
|||
<logger name="org.thingsboard.server.service.subscription" level="TRACE"/> |
|||
|
|||
<root level="INFO"> |
|||
<appender-ref ref="fileLogAppender"/> |
|||
<appender-ref ref="STDOUT"/> |
|||
</root> |
|||
|
|||
</configuration> |
|||
@ -0,0 +1,57 @@ |
|||
<?xml version="1.0" encoding="UTF-8" ?> |
|||
<!-- |
|||
|
|||
Copyright © 2016-2023 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. |
|||
|
|||
--> |
|||
<!DOCTYPE configuration> |
|||
<configuration scan="true" scanPeriod="10 seconds"> |
|||
|
|||
<appender name="fileLogAppender" |
|||
class="ch.qos.logback.core.rolling.RollingFileAppender"> |
|||
<file>/var/log/tb-coap-transport/${TB_SERVICE_ID}/tb-coap-transport.log</file> |
|||
<rollingPolicy |
|||
class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy"> |
|||
<fileNamePattern>/var/log/tb-coap-transport/${TB_SERVICE_ID}/tb-coap-transport.%d{yyyy-MM-dd}.%i.log</fileNamePattern> |
|||
<maxFileSize>100MB</maxFileSize> |
|||
<maxHistory>30</maxHistory> |
|||
<totalSizeCap>3GB</totalSizeCap> |
|||
</rollingPolicy> |
|||
<encoder> |
|||
<pattern>%d{ISO8601} [%thread] %-5level %logger{36} - %msg%n</pattern> |
|||
</encoder> |
|||
</appender> |
|||
|
|||
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> |
|||
<encoder> |
|||
<pattern>%d{ISO8601} [%thread] %-5level %logger{36} - %msg%n</pattern> |
|||
</encoder> |
|||
</appender> |
|||
|
|||
<logger name="org.thingsboard.server" level="INFO" /> |
|||
|
|||
<logger name="com.microsoft.azure.servicebus.primitives.CoreMessageReceiver" level="OFF" /> |
|||
|
|||
<logger name="org.apache.kafka.common.utils.AppInfoParser" level="WARN"/> |
|||
<logger name="org.apache.kafka.clients" level="WARN"/> |
|||
|
|||
<logger name="org.thingsboard.server.transport.coap" level="TRACE"/> |
|||
|
|||
<root level="INFO"> |
|||
<appender-ref ref="fileLogAppender"/> |
|||
<appender-ref ref="STDOUT"/> |
|||
</root> |
|||
|
|||
</configuration> |
|||
@ -0,0 +1,57 @@ |
|||
<?xml version="1.0" encoding="UTF-8" ?> |
|||
<!-- |
|||
|
|||
Copyright © 2016-2023 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. |
|||
|
|||
--> |
|||
<!DOCTYPE configuration> |
|||
<configuration scan="true" scanPeriod="10 seconds"> |
|||
|
|||
<appender name="fileLogAppender" |
|||
class="ch.qos.logback.core.rolling.RollingFileAppender"> |
|||
<file>/var/log/tb-http-transport/${TB_SERVICE_ID}/tb-http-transport.log</file> |
|||
<rollingPolicy |
|||
class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy"> |
|||
<fileNamePattern>/var/log/tb-http-transport/${TB_SERVICE_ID}/tb-http-transport.%d{yyyy-MM-dd}.%i.log</fileNamePattern> |
|||
<maxFileSize>100MB</maxFileSize> |
|||
<maxHistory>30</maxHistory> |
|||
<totalSizeCap>3GB</totalSizeCap> |
|||
</rollingPolicy> |
|||
<encoder> |
|||
<pattern>%d{ISO8601} [%thread] %-5level %logger{36} - %msg%n</pattern> |
|||
</encoder> |
|||
</appender> |
|||
|
|||
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> |
|||
<encoder> |
|||
<pattern>%d{ISO8601} [%thread] %-5level %logger{36} - %msg%n</pattern> |
|||
</encoder> |
|||
</appender> |
|||
|
|||
<logger name="org.thingsboard.server" level="INFO" /> |
|||
|
|||
<logger name="com.microsoft.azure.servicebus.primitives.CoreMessageReceiver" level="OFF" /> |
|||
|
|||
<logger name="org.apache.kafka.common.utils.AppInfoParser" level="WARN"/> |
|||
<logger name="org.apache.kafka.clients" level="WARN"/> |
|||
|
|||
<logger name="org.thingsboard.server.transport.http" level="TRACE"/> |
|||
|
|||
<root level="INFO"> |
|||
<appender-ref ref="fileLogAppender"/> |
|||
<appender-ref ref="STDOUT"/> |
|||
</root> |
|||
|
|||
</configuration> |
|||
@ -0,0 +1,55 @@ |
|||
<?xml version="1.0" encoding="UTF-8" ?> |
|||
<!-- |
|||
|
|||
Copyright © 2016-2023 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. |
|||
|
|||
--> |
|||
<!DOCTYPE configuration> |
|||
<configuration scan="true" scanPeriod="10 seconds"> |
|||
|
|||
<appender name="fileLogAppender" |
|||
class="ch.qos.logback.core.rolling.RollingFileAppender"> |
|||
<file>/var/log/tb-mqtt-transport/${TB_SERVICE_ID}/tb-mqtt-transport.log</file> |
|||
<rollingPolicy |
|||
class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy"> |
|||
<fileNamePattern>/var/log/tb-mqtt-transport/${TB_SERVICE_ID}/tb-mqtt-transport.%d{yyyy-MM-dd}.%i.log</fileNamePattern> |
|||
<maxFileSize>100MB</maxFileSize> |
|||
<maxHistory>30</maxHistory> |
|||
<totalSizeCap>3GB</totalSizeCap> |
|||
</rollingPolicy> |
|||
<encoder> |
|||
<pattern>%d{ISO8601} [%thread] %-5level %logger{36} - %msg%n</pattern> |
|||
</encoder> |
|||
</appender> |
|||
|
|||
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> |
|||
<encoder> |
|||
<pattern>%d{ISO8601} [%thread] %-5level %logger{36} - %msg%n</pattern> |
|||
</encoder> |
|||
</appender> |
|||
|
|||
<logger name="org.thingsboard.server" level="INFO" /> |
|||
|
|||
<logger name="com.microsoft.azure.servicebus.primitives.CoreMessageReceiver" level="OFF" /> |
|||
<logger name="org.apache.kafka.common.utils.AppInfoParser" level="WARN"/> |
|||
<logger name="org.apache.kafka.clients" level="WARN"/> |
|||
<logger name="org.thingsboard.server.transport.mqtt" level="TRACE"/> |
|||
|
|||
<root level="INFO"> |
|||
<appender-ref ref="fileLogAppender"/> |
|||
<appender-ref ref="STDOUT"/> |
|||
</root> |
|||
|
|||
</configuration> |
|||
@ -0,0 +1,244 @@ |
|||
<!-- |
|||
|
|||
Copyright © 2016-2023 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]="doughnutWidgetConfigForm"> |
|||
<tb-timewindow-config-panel *ngIf="displayTimewindowConfig" |
|||
[onlyHistoryTimewindow]="onlyHistoryTimewindow()" |
|||
formControlName="timewindowConfig"> |
|||
</tb-timewindow-config-panel> |
|||
<tb-datasources |
|||
[configMode]="basicMode" |
|||
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> |
|||
<tb-image-cards-select rowHeight="{{ horizontal ? '8:5' : '5:4' }}" |
|||
[cols]="{columns: 2, |
|||
breakpoints: { |
|||
'lt-sm': 1 |
|||
}}" |
|||
label="{{ 'widgets.doughnut.layout' | translate }}" formControlName="layout"> |
|||
<tb-image-cards-select-option *ngFor="let layout of doughnutLayouts" |
|||
[value]="layout" |
|||
[image]="doughnutLayoutImageMap.get(layout)"> |
|||
{{ doughnutLayoutTranslationMap.get(layout) | translate }} |
|||
</tb-image-cards-select-option> |
|||
</tb-image-cards-select> |
|||
<div class="tb-form-row"> |
|||
<mat-slide-toggle class="mat-slide" formControlName="autoScale"> |
|||
{{ 'widgets.value-card.auto-scale' | translate }} |
|||
</mat-slide-toggle> |
|||
</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]="doughnutWidgetConfigForm.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]="doughnutWidgetConfigForm.get('iconColor').value" |
|||
formControlName="titleIcon"> |
|||
</tb-material-icon-select> |
|||
<tb-color-input asBoxInput |
|||
colorClearButton |
|||
formControlName="iconColor"> |
|||
</tb-color-input> |
|||
</div> |
|||
</div> |
|||
<div [fxShow]="totalEnabled" class="tb-form-row space-between"> |
|||
<div>{{ 'widgets.doughnut.central-total-value' | translate }}</div> |
|||
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px"> |
|||
<tb-font-settings formControlName="totalValueFont" |
|||
[previewText]="valuePreviewFn"> |
|||
</tb-font-settings> |
|||
<tb-color-settings formControlName="totalValueColor" settingsKey="{{'widgets.doughnut.central-total-value' | translate }}"> |
|||
</tb-color-settings> |
|||
</div> |
|||
</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]="doughnutWidgetConfigForm.get('showLegend').value" [disabled]="!doughnutWidgetConfigForm.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 doughnutLegendPositions" [value]="pos"> |
|||
{{ doughnutLegendPositionTranslationMap.get(pos) | translate }} |
|||
</mat-option> |
|||
</mat-select> |
|||
</mat-form-field> |
|||
</div> |
|||
<div class="tb-form-row space-between"> |
|||
<div>{{ 'widgets.doughnut.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>{{ 'widgets.doughnut.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]="doughnutWidgetConfigForm.get('showTooltip').value" [disabled]="!doughnutWidgetConfigForm.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"> |
|||
{{ 'widgets.doughnut.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>{{ 'widgets.doughnut.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 doughnutTooltipValueTypes" [value]="type"> |
|||
{{ doughnutTooltipValueTypeTranslationMap.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>{{ 'widgets.doughnut.tooltip-background-color' | translate }}</div> |
|||
<tb-color-input asBoxInput |
|||
colorClearButton |
|||
formControlName="tooltipBackgroundColor"> |
|||
</tb-color-input> |
|||
</div> |
|||
<div class="tb-form-row space-between"> |
|||
<div>{{ 'widgets.doughnut.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> |
|||
<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,339 @@ |
|||
///
|
|||
/// Copyright © 2016-2023 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 } 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, |
|||
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, isDefinedAndNotNull, isUndefined } from '@core/utils'; |
|||
import { |
|||
getTimewindowConfig, |
|||
setTimewindowConfig |
|||
} from '@home/components/widget/config/timewindow-config-panel.component'; |
|||
import { |
|||
doughnutDefaultSettings, |
|||
DoughnutLayout, |
|||
doughnutLayoutImages, |
|||
doughnutLayouts, |
|||
doughnutLayoutTranslations, |
|||
DoughnutLegendPosition, |
|||
doughnutLegendPositionTranslations, |
|||
DoughnutTooltipValueType, |
|||
doughnutTooltipValueTypes, |
|||
doughnutTooltipValueTypeTranslations, |
|||
DoughnutWidgetSettings, |
|||
horizontalDoughnutLayoutImages |
|||
} from '@home/components/widget/lib/chart/doughnut-widget.models'; |
|||
|
|||
@Component({ |
|||
selector: 'tb-doughnut-basic-config', |
|||
templateUrl: './doughnut-basic-config.component.html', |
|||
styleUrls: ['../basic-config.scss'] |
|||
}) |
|||
export class DoughnutBasicConfigComponent extends BasicWidgetConfigComponent { |
|||
|
|||
public get datasource(): Datasource { |
|||
const datasources: Datasource[] = this.doughnutWidgetConfigForm.get('datasources').value; |
|||
if (datasources && datasources.length) { |
|||
return datasources[0]; |
|||
} else { |
|||
return null; |
|||
} |
|||
} |
|||
|
|||
public get displayTimewindowConfig(): boolean { |
|||
const datasources = this.doughnutWidgetConfigForm.get('datasources').value; |
|||
return datasourcesHasAggregation(datasources); |
|||
} |
|||
|
|||
public onlyHistoryTimewindow(): boolean { |
|||
const datasources = this.doughnutWidgetConfigForm.get('datasources').value; |
|||
return datasourcesHasOnlyComparisonAggregation(datasources); |
|||
} |
|||
|
|||
doughnutLayouts = doughnutLayouts; |
|||
|
|||
doughnutLayoutTranslationMap = doughnutLayoutTranslations; |
|||
|
|||
horizontal = false; |
|||
|
|||
doughnutLayoutImageMap: Map<DoughnutLayout, string>; |
|||
|
|||
doughnutLegendPositions: DoughnutLegendPosition[]; |
|||
|
|||
doughnutLegendPositionTranslationMap = doughnutLegendPositionTranslations; |
|||
|
|||
doughnutTooltipValueTypes = doughnutTooltipValueTypes; |
|||
|
|||
doughnutTooltipValueTypeTranslationMap = doughnutTooltipValueTypeTranslations; |
|||
|
|||
doughnutWidgetConfigForm: UntypedFormGroup; |
|||
|
|||
valuePreviewFn = this._valuePreviewFn.bind(this); |
|||
|
|||
tooltipValuePreviewFn = this._tooltipValuePreviewFn.bind(this); |
|||
|
|||
get totalEnabled(): boolean { |
|||
const layout: DoughnutLayout = this.doughnutWidgetConfigForm.get('layout').value; |
|||
return layout === DoughnutLayout.with_total; |
|||
} |
|||
|
|||
constructor(protected store: Store<AppState>, |
|||
protected widgetConfigComponent: WidgetConfigComponent, |
|||
private fb: UntypedFormBuilder) { |
|||
super(store, widgetConfigComponent); |
|||
} |
|||
|
|||
protected configForm(): UntypedFormGroup { |
|||
return this.doughnutWidgetConfigForm; |
|||
} |
|||
|
|||
protected setupConfig(widgetConfig: WidgetConfigComponentData) { |
|||
const params = widgetConfig.typeParameters as any; |
|||
this.horizontal = isDefinedAndNotNull(params.horizontal) ? params.horizontal : false; |
|||
this.doughnutLayoutImageMap = this.horizontal ? horizontalDoughnutLayoutImages : doughnutLayoutImages; |
|||
this.doughnutLegendPositions = this.horizontal ? [DoughnutLegendPosition.left, DoughnutLegendPosition.right] : |
|||
[DoughnutLegendPosition.top, DoughnutLegendPosition.bottom]; |
|||
super.setupConfig(widgetConfig); |
|||
} |
|||
|
|||
protected defaultDataKeys(configData: WidgetConfigComponentData): DataKey[] { |
|||
return [{ name: 'windPower', label: 'Wind power', type: DataKeyType.timeseries, units: '', decimals: 0, color: '#08872B' }, |
|||
{ name: 'solarPower', label: 'Solar power', type: DataKeyType.timeseries, units: '', decimals: 0, color: '#FF4D5A' }]; |
|||
} |
|||
|
|||
protected onConfigSet(configData: WidgetConfigComponentData) { |
|||
const settings: DoughnutWidgetSettings = {...doughnutDefaultSettings(this.horizontal), ...(configData.config.settings || {})}; |
|||
this.doughnutWidgetConfigForm = this.fb.group({ |
|||
timewindowConfig: [getTimewindowConfig(configData.config), []], |
|||
datasources: [configData.config.datasources, []], |
|||
|
|||
series: [this.getSeries(configData.config.datasources), []], |
|||
|
|||
layout: [settings.layout, []], |
|||
autoScale: [settings.autoScale, []], |
|||
|
|||
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, []], |
|||
|
|||
totalValueFont: [settings.totalValueFont, []], |
|||
totalValueColor: [settings.totalValueColor, []], |
|||
|
|||
units: [configData.config.units, []], |
|||
decimals: [configData.config.decimals, []], |
|||
|
|||
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.layout = config.layout; |
|||
this.widgetConfig.config.settings.autoScale = config.autoScale; |
|||
|
|||
this.widgetConfig.config.settings.totalValueFont = config.totalValueFont; |
|||
this.widgetConfig.config.settings.totalValueColor = config.totalValueColor; |
|||
|
|||
this.widgetConfig.config.units = config.units; |
|||
this.widgetConfig.config.decimals = config.decimals; |
|||
|
|||
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 ['layout', 'showTitle', 'showTitleIcon', 'showLegend', 'showTooltip']; |
|||
} |
|||
|
|||
protected updateValidators(emitEvent: boolean, trigger?: string) { |
|||
const layout: DoughnutLayout = this.doughnutWidgetConfigForm.get('layout').value; |
|||
const showTitle: boolean = this.doughnutWidgetConfigForm.get('showTitle').value; |
|||
const showTitleIcon: boolean = this.doughnutWidgetConfigForm.get('showTitleIcon').value; |
|||
const showLegend: boolean = this.doughnutWidgetConfigForm.get('showLegend').value; |
|||
const showTooltip: boolean = this.doughnutWidgetConfigForm.get('showTooltip').value; |
|||
|
|||
const totalEnabled = layout === DoughnutLayout.with_total; |
|||
|
|||
if (showTitle) { |
|||
this.doughnutWidgetConfigForm.get('title').enable(); |
|||
this.doughnutWidgetConfigForm.get('titleFont').enable(); |
|||
this.doughnutWidgetConfigForm.get('titleColor').enable(); |
|||
this.doughnutWidgetConfigForm.get('showTitleIcon').enable({emitEvent: false}); |
|||
if (showTitleIcon) { |
|||
this.doughnutWidgetConfigForm.get('titleIcon').enable(); |
|||
this.doughnutWidgetConfigForm.get('iconColor').enable(); |
|||
} else { |
|||
this.doughnutWidgetConfigForm.get('titleIcon').disable(); |
|||
this.doughnutWidgetConfigForm.get('iconColor').disable(); |
|||
} |
|||
} else { |
|||
this.doughnutWidgetConfigForm.get('title').disable(); |
|||
this.doughnutWidgetConfigForm.get('titleFont').disable(); |
|||
this.doughnutWidgetConfigForm.get('titleColor').disable(); |
|||
this.doughnutWidgetConfigForm.get('showTitleIcon').disable({emitEvent: false}); |
|||
this.doughnutWidgetConfigForm.get('titleIcon').disable(); |
|||
this.doughnutWidgetConfigForm.get('iconColor').disable(); |
|||
} |
|||
if (showLegend) { |
|||
this.doughnutWidgetConfigForm.get('legendPosition').enable(); |
|||
this.doughnutWidgetConfigForm.get('legendLabelFont').enable(); |
|||
this.doughnutWidgetConfigForm.get('legendLabelColor').enable(); |
|||
this.doughnutWidgetConfigForm.get('legendValueFont').enable(); |
|||
this.doughnutWidgetConfigForm.get('legendValueColor').enable(); |
|||
} else { |
|||
this.doughnutWidgetConfigForm.get('legendPosition').disable(); |
|||
this.doughnutWidgetConfigForm.get('legendLabelFont').disable(); |
|||
this.doughnutWidgetConfigForm.get('legendLabelColor').disable(); |
|||
this.doughnutWidgetConfigForm.get('legendValueFont').disable(); |
|||
this.doughnutWidgetConfigForm.get('legendValueColor').disable(); |
|||
} |
|||
if (showTooltip) { |
|||
this.doughnutWidgetConfigForm.get('tooltipValueType').enable(); |
|||
this.doughnutWidgetConfigForm.get('tooltipValueDecimals').enable(); |
|||
this.doughnutWidgetConfigForm.get('tooltipValueFont').enable(); |
|||
this.doughnutWidgetConfigForm.get('tooltipValueColor').enable(); |
|||
this.doughnutWidgetConfigForm.get('tooltipBackgroundColor').enable(); |
|||
this.doughnutWidgetConfigForm.get('tooltipBackgroundBlur').enable(); |
|||
} else { |
|||
this.doughnutWidgetConfigForm.get('tooltipValueType').disable(); |
|||
this.doughnutWidgetConfigForm.get('tooltipValueDecimals').disable(); |
|||
this.doughnutWidgetConfigForm.get('tooltipValueFont').disable(); |
|||
this.doughnutWidgetConfigForm.get('tooltipValueColor').disable(); |
|||
this.doughnutWidgetConfigForm.get('tooltipBackgroundColor').disable(); |
|||
this.doughnutWidgetConfigForm.get('tooltipBackgroundBlur').disable(); |
|||
} |
|||
if (totalEnabled) { |
|||
this.doughnutWidgetConfigForm.get('totalValueFont').enable(); |
|||
this.doughnutWidgetConfigForm.get('totalValueColor').enable(); |
|||
} else { |
|||
this.doughnutWidgetConfigForm.get('totalValueFont').disable(); |
|||
this.doughnutWidgetConfigForm.get('totalValueColor').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.doughnutWidgetConfigForm.get('units').value; |
|||
const decimals: number = this.doughnutWidgetConfigForm.get('decimals').value; |
|||
return formatValue(110, decimals, units, false); |
|||
} |
|||
|
|||
private _tooltipValuePreviewFn(): string { |
|||
const tooltipValueType: DoughnutTooltipValueType = this.doughnutWidgetConfigForm.get('tooltipValueType').value; |
|||
const decimals: number = this.doughnutWidgetConfigForm.get('tooltipValueDecimals').value; |
|||
if (tooltipValueType === DoughnutTooltipValueType.percentage) { |
|||
return formatValue(35, decimals, '%', false); |
|||
} else { |
|||
const units: string = this.doughnutWidgetConfigForm.get('units').value; |
|||
return formatValue(110, decimals, units, false); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,37 @@ |
|||
<!-- |
|||
|
|||
Copyright © 2016-2023 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"> |
|||
<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,110 @@ |
|||
/** |
|||
* Copyright © 2016-2023 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-doughnut-panel { |
|||
width: 100%; |
|||
height: 100%; |
|||
position: relative; |
|||
display: flex; |
|||
flex-direction: column; |
|||
gap: 16px; |
|||
padding: 20px 24px 24px 24px; |
|||
> div:not(.tb-doughnut-overlay) { |
|||
z-index: 1; |
|||
} |
|||
.tb-doughnut-overlay { |
|||
position: absolute; |
|||
top: 12px; |
|||
left: 12px; |
|||
bottom: 12px; |
|||
right: 12px; |
|||
} |
|||
div.tb-widget-title { |
|||
padding: 0; |
|||
} |
|||
.tb-doughnut-content { |
|||
flex: 1; |
|||
min-width: 0; |
|||
min-height: 0; |
|||
display: flex; |
|||
flex-direction: column; |
|||
gap: 16px; |
|||
&.legend-top { |
|||
flex-direction: column-reverse; |
|||
} |
|||
&.legend-right { |
|||
flex-direction: row; |
|||
} |
|||
&.legend-left { |
|||
flex-direction: row-reverse; |
|||
} |
|||
.tb-doughnut-shape { |
|||
flex: 1; |
|||
min-width: 0; |
|||
min-height: 0; |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
} |
|||
.tb-doughnut-legend { |
|||
display: flex; |
|||
justify-content: space-around; |
|||
align-items: center; |
|||
align-self: stretch; |
|||
flex-wrap: wrap; |
|||
gap: 8px; |
|||
.tb-doughnut-legend-item { |
|||
display: flex; |
|||
flex-direction: column; |
|||
justify-content: center; |
|||
align-items: flex-start; |
|||
user-select: none; |
|||
&.pointer { |
|||
cursor: pointer; |
|||
} |
|||
.tb-doughnut-legend-item-label { |
|||
display: flex; |
|||
align-items: center; |
|||
gap: 4px; |
|||
color: #ccc; |
|||
.tb-doughnut-legend-item-label-circle { |
|||
width: 8px; |
|||
height: 8px; |
|||
border-radius: 50%; |
|||
background-color: #ccc; |
|||
} |
|||
} |
|||
.tb-doughnut-legend-item-value { |
|||
padding-left: 12px; |
|||
color: #ccc; |
|||
} |
|||
} |
|||
} |
|||
&.legend-right, &.legend-left { |
|||
gap: 24px; |
|||
.tb-doughnut-legend { |
|||
flex-direction: column; |
|||
justify-content: center; |
|||
align-items: stretch; |
|||
.tb-doughnut-legend-item { |
|||
flex-direction: row; |
|||
justify-content: space-between; |
|||
align-items: center; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,535 @@ |
|||
///
|
|||
/// Copyright © 2016-2023 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 { |
|||
doughnutDefaultSettings, |
|||
DoughnutLayout, |
|||
DoughnutLegendPosition, |
|||
DoughnutTooltipValueType, |
|||
DoughnutWidgetSettings |
|||
} from '@home/components/widget/lib/chart/doughnut-widget.models'; |
|||
import { WidgetContext } from '@home/models/widget-component.models'; |
|||
import { |
|||
backgroundStyle, |
|||
ColorProcessor, |
|||
ComponentStyle, |
|||
overlayStyle, |
|||
textStyle |
|||
} from '@shared/models/widget-settings.models'; |
|||
import { ResizeObserver } from '@juggle/resize-observer'; |
|||
import { WidgetComponent } from '@home/components/widget/widget.component'; |
|||
import * as echarts from 'echarts/core'; |
|||
import { TranslateService } from '@ngx-translate/core'; |
|||
import { PieDataItemOption } from 'echarts/types/src/chart/pie/PieSeries'; |
|||
import { formatValue, isDefinedAndNotNull, isNumeric } from '@core/utils'; |
|||
import { SVG, Svg, Text } from '@svgdotjs/svg.js'; |
|||
import { DataKey } from '@shared/models/widget.models'; |
|||
import { TooltipComponent, TooltipComponentOption } from 'echarts/components'; |
|||
import { PieChart, PieSeriesOption } from 'echarts/charts'; |
|||
import { SVGRenderer } from 'echarts/renderers'; |
|||
|
|||
echarts.use([ |
|||
TooltipComponent, |
|||
PieChart, |
|||
SVGRenderer |
|||
]); |
|||
|
|||
type EChartsOption = echarts.ComposeOption< |
|||
| TooltipComponentOption |
|||
| PieSeriesOption |
|||
>; |
|||
type ECharts = echarts.ECharts; |
|||
|
|||
const shapeSize = 134; |
|||
const shapeSegmentWidth = 13.4; |
|||
|
|||
interface DoughnutDataItem { |
|||
id: number; |
|||
dataKey: DataKey; |
|||
value: number; |
|||
hasValue: boolean; |
|||
enabled: boolean; |
|||
} |
|||
|
|||
interface DoughnutLegendItem { |
|||
id: number; |
|||
color: string; |
|||
label: string; |
|||
value: string; |
|||
hasValue: boolean; |
|||
enabled: boolean; |
|||
total?: boolean; |
|||
} |
|||
|
|||
@Component({ |
|||
selector: 'tb-doughnut-widget', |
|||
templateUrl: './doughnut-widget.component.html', |
|||
styleUrls: ['./doughnut-widget.component.scss'], |
|||
encapsulation: ViewEncapsulation.None |
|||
}) |
|||
export class DoughnutWidgetComponent implements OnInit, OnDestroy, AfterViewInit { |
|||
|
|||
@ViewChild('doughnutContent', {static: false}) |
|||
doughnutContent: ElementRef<HTMLElement>; |
|||
|
|||
@ViewChild('doughnutShape', {static: false}) |
|||
doughnutShape: ElementRef<HTMLElement>; |
|||
|
|||
@ViewChild('doughnutLegend', {static: false}) |
|||
doughnutLegend: ElementRef<HTMLElement>; |
|||
|
|||
settings: DoughnutWidgetSettings; |
|||
|
|||
@Input() |
|||
ctx: WidgetContext; |
|||
|
|||
@Input() |
|||
widgetTitlePanel: TemplateRef<any>; |
|||
|
|||
showLegend: boolean; |
|||
legendClass: string; |
|||
|
|||
totalValueColor: ColorProcessor; |
|||
|
|||
backgroundStyle: ComponentStyle = {}; |
|||
overlayStyle: ComponentStyle = {}; |
|||
|
|||
legendItems: DoughnutLegendItem[]; |
|||
legendLabelStyle: ComponentStyle; |
|||
legendValueStyle: ComponentStyle; |
|||
disabledLegendLabelStyle: ComponentStyle; |
|||
disabledLegendValueStyle: ComponentStyle; |
|||
|
|||
private shapeResize$: ResizeObserver; |
|||
private legendHorizontal: boolean; |
|||
|
|||
private decimals = 0; |
|||
private units = ''; |
|||
|
|||
private total = 0; |
|||
private totalText = 'N/A'; |
|||
private scale = 1; |
|||
|
|||
private dataItems: DoughnutDataItem[] = []; |
|||
|
|||
private drawDoughnutPending = false; |
|||
private showTotal = false; |
|||
private doughnutChart: ECharts; |
|||
private doughnutOptions: EChartsOption; |
|||
private svgShape: Svg; |
|||
private totalTextNode: Text; |
|||
|
|||
constructor(private widgetComponent: WidgetComponent, |
|||
private renderer: Renderer2, |
|||
private translate: TranslateService, |
|||
private cd: ChangeDetectorRef) { |
|||
} |
|||
|
|||
ngOnInit(): void { |
|||
const params = this.widgetComponent.typeParameters as any; |
|||
const horizontal = isDefinedAndNotNull(params.horizontal) ? params.horizontal : false; |
|||
this.ctx.$scope.doughnutWidget = this; |
|||
this.settings = {...doughnutDefaultSettings(horizontal), ...this.ctx.settings}; |
|||
|
|||
this.decimals = this.ctx.decimals; |
|||
this.units = this.ctx.units; |
|||
|
|||
this.showLegend = this.settings.showLegend; |
|||
this.showTotal = this.settings.layout === DoughnutLayout.with_total; |
|||
|
|||
if (this.showTotal) { |
|||
this.totalValueColor = ColorProcessor.fromSettings(this.settings.totalValueColor); |
|||
} |
|||
|
|||
this.backgroundStyle = backgroundStyle(this.settings.background); |
|||
this.overlayStyle = overlayStyle(this.settings.background.overlay); |
|||
|
|||
if (this.showLegend) { |
|||
this.legendItems = []; |
|||
this.legendClass = `legend-${this.settings.legendPosition}`; |
|||
this.legendHorizontal = [DoughnutLegendPosition.left, DoughnutLegendPosition.right].includes(this.settings.legendPosition); |
|||
this.legendLabelStyle = textStyle(this.settings.legendLabelFont); |
|||
this.disabledLegendLabelStyle = textStyle(this.settings.legendLabelFont); |
|||
this.legendLabelStyle.color = this.settings.legendLabelColor; |
|||
this.legendValueStyle = textStyle(this.settings.legendValueFont); |
|||
this.disabledLegendValueStyle = textStyle(this.settings.legendValueFont); |
|||
this.legendValueStyle.color = this.settings.legendValueColor; |
|||
} |
|||
let counter = 0; |
|||
if (this.ctx.datasources.length) { |
|||
for (const datasource of this.ctx.datasources) { |
|||
const dataKeys = datasource.dataKeys; |
|||
for (const dataKey of dataKeys) { |
|||
const id = counter++; |
|||
this.dataItems.push({ |
|||
id, |
|||
dataKey, |
|||
value: 0, |
|||
hasValue: false, |
|||
enabled: true |
|||
}); |
|||
if (this.showLegend) { |
|||
this.legendItems.push( |
|||
{ |
|||
id, |
|||
value: '--', |
|||
label: dataKey.label, |
|||
color: dataKey.color, |
|||
enabled: true, |
|||
hasValue: false |
|||
} |
|||
); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
if (!this.showTotal && this.showLegend) { |
|||
this.legendItems.push( |
|||
{ |
|||
id: null, |
|||
value: '--', |
|||
label: this.translate.instant('widgets.doughnut.total'), |
|||
color: 'rgba(0, 0, 0, 0.06)', |
|||
enabled: true, |
|||
hasValue: false, |
|||
total: true |
|||
} |
|||
); |
|||
} |
|||
} |
|||
|
|||
ngAfterViewInit() { |
|||
if (this.drawDoughnutPending) { |
|||
this.drawDoughnut(); |
|||
} |
|||
} |
|||
|
|||
ngOnDestroy() { |
|||
if (this.shapeResize$) { |
|||
this.shapeResize$.disconnect(); |
|||
} |
|||
if (this.doughnutChart) { |
|||
this.doughnutChart.dispose(); |
|||
} |
|||
} |
|||
|
|||
public onInit() { |
|||
const borderRadius = this.ctx.$widgetElement.css('borderRadius'); |
|||
this.overlayStyle = {...this.overlayStyle, ...{borderRadius}}; |
|||
if (this.doughnutShape) { |
|||
this.drawDoughnut(); |
|||
} else { |
|||
this.drawDoughnutPending = true; |
|||
} |
|||
this.cd.detectChanges(); |
|||
} |
|||
|
|||
public onDataUpdated() { |
|||
for (let i=0; i < this.ctx.data.length; i++) { |
|||
const dsData = this.ctx.data[i]; |
|||
let value = 0; |
|||
const tsValue = dsData.data[0]; |
|||
if (tsValue && isDefinedAndNotNull(tsValue[1]) && isNumeric(tsValue[1])) { |
|||
value = tsValue[1]; |
|||
this.dataItems[i].hasValue = true; |
|||
this.dataItems[i].value = value; |
|||
} else { |
|||
this.dataItems[i].hasValue = false; |
|||
this.dataItems[i].value = 0; |
|||
} |
|||
} |
|||
this.updateSeriesData(); |
|||
if (this.showLegend) { |
|||
this.cd.detectChanges(); |
|||
if (this.legendHorizontal) { |
|||
setTimeout(() => { |
|||
this.onResize(); |
|||
}); |
|||
} |
|||
} |
|||
} |
|||
|
|||
private updateSeriesData(renderTotal = true) { |
|||
this.total = 0; |
|||
this.totalText = 'N/A'; |
|||
let hasValue = false; |
|||
const seriesData: PieDataItemOption[] = []; |
|||
const enabledDataItems = this.dataItems.filter(item => item.enabled && item.hasValue); |
|||
for (let i=0; i < this.dataItems.length; i++) { |
|||
const dataItem = this.dataItems[i]; |
|||
if (dataItem.enabled && dataItem.hasValue) { |
|||
hasValue = true; |
|||
this.total += dataItem.value; |
|||
seriesData.push( |
|||
{id: dataItem.id, value: dataItem.value, name: dataItem.dataKey.label, itemStyle: {color: dataItem.dataKey.color}} |
|||
); |
|||
if (enabledDataItems.length > 1) { |
|||
seriesData.push({ |
|||
value: 0, name: '', itemStyle: {color: 'transparent'}, emphasis: {disabled: true} |
|||
}); |
|||
} |
|||
} |
|||
if (this.showLegend) { |
|||
if (dataItem.hasValue) { |
|||
this.legendItems[i].hasValue = true; |
|||
this.legendItems[i].value = formatValue(dataItem.value, this.decimals, this.units, false); |
|||
} else { |
|||
this.legendItems[i].hasValue = false; |
|||
this.legendItems[i].value = '--'; |
|||
} |
|||
} |
|||
} |
|||
for (let i= 1; i < seriesData.length; i+=2) { |
|||
seriesData[i].value = this.total / 100; |
|||
} |
|||
if (this.showTotal || this.showLegend) { |
|||
if (hasValue) { |
|||
this.totalText = formatValue(this.total, this.decimals, this.units, false); |
|||
if (this.showLegend && !this.showTotal) { |
|||
this.legendItems[this.legendItems.length - 1].hasValue = true; |
|||
this.legendItems[this.legendItems.length - 1].value = this.totalText; |
|||
} |
|||
} else if (this.showLegend && !this.showTotal) { |
|||
this.legendItems[this.legendItems.length - 1].hasValue = false; |
|||
this.legendItems[this.legendItems.length - 1].value = '--'; |
|||
} |
|||
} |
|||
this.doughnutOptions.series[0].data = seriesData; |
|||
this.doughnutChart.setOption(this.doughnutOptions); |
|||
if (this.showTotal) { |
|||
this.totalValueColor.update(this.total); |
|||
if (renderTotal) { |
|||
this.renderTotal(); |
|||
} |
|||
} |
|||
} |
|||
|
|||
public onLegendItemEnter(item: DoughnutLegendItem) { |
|||
if (!item.total && item.enabled && item.hasValue) { |
|||
const dataIndex = this.doughnutOptions.series[0].data.findIndex(d => d.id === item.id); |
|||
if (dataIndex > -1) { |
|||
this.doughnutChart.dispatchAction({ |
|||
type: 'highlight', |
|||
dataIndex |
|||
}); |
|||
} |
|||
} |
|||
} |
|||
|
|||
public onLegendItemLeave(item: DoughnutLegendItem) { |
|||
if (!item.total && item.enabled && item.hasValue) { |
|||
const dataIndex = this.doughnutOptions.series[0].data.findIndex(d => d.id === item.id); |
|||
if (dataIndex > -1) { |
|||
this.doughnutChart.dispatchAction({ |
|||
type: 'downplay', |
|||
dataIndex |
|||
}); |
|||
} |
|||
} |
|||
} |
|||
|
|||
public toggleLegendItem(item: DoughnutLegendItem) { |
|||
if (!item.total && item.hasValue) { |
|||
const enable = !item.enabled; |
|||
const dataItem = this.dataItems.find(d => d.id === item.id); |
|||
if (dataItem) { |
|||
dataItem.enabled = enable; |
|||
this.updateSeriesData(); |
|||
item.enabled = enable; |
|||
if (enable) { |
|||
const dataIndex = this.doughnutOptions.series[0].data.findIndex(d => d.id === item.id); |
|||
if (dataIndex > -1) { |
|||
this.doughnutChart.dispatchAction({ |
|||
type: 'highlight', |
|||
dataIndex |
|||
}); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
private drawDoughnut() { |
|||
const shapeWidth = this.doughnutShape.nativeElement.getBoundingClientRect().width; |
|||
const shapeHeight = this.doughnutShape.nativeElement.getBoundingClientRect().height; |
|||
const size = this.settings.autoScale ? shapeSize : Math.min(shapeWidth, shapeHeight); |
|||
const innerRadius = size / 2 - shapeSegmentWidth; |
|||
const outerRadius = size / 2; |
|||
this.doughnutChart = echarts.init(this.doughnutShape.nativeElement, null, { |
|||
renderer: 'svg', |
|||
width: this.settings.autoScale ? shapeSize : undefined, |
|||
height: this.settings.autoScale ? shapeSize : undefined, |
|||
}); |
|||
this.doughnutOptions = { |
|||
tooltip: { |
|||
trigger: this.settings.showTooltip ? 'item' : 'none', |
|||
confine: false, |
|||
appendToBody: true |
|||
}, |
|||
series: [ |
|||
{ |
|||
type: 'pie', |
|||
clockwise: false, |
|||
radius: [innerRadius, outerRadius], |
|||
avoidLabelOverlap: false, |
|||
itemStyle: { |
|||
borderRadius: '50%', |
|||
borderWidth: 0, |
|||
borderColor: '#fff' |
|||
}, |
|||
label: { |
|||
show: false |
|||
}, |
|||
emphasis: { |
|||
scale: false, |
|||
itemStyle: { |
|||
borderColor: '#fff', |
|||
borderWidth: 2, |
|||
shadowColor: 'rgba(0, 0, 0, 0.24)', |
|||
shadowBlur: 8 |
|||
}, |
|||
label: { |
|||
show: false |
|||
} |
|||
} |
|||
} |
|||
] |
|||
}; |
|||
if (this.settings.showTooltip) { |
|||
this.doughnutOptions.series[0].tooltip = { |
|||
formatter: (params) => { |
|||
let value: string; |
|||
if (this.settings.tooltipValueType === DoughnutTooltipValueType.percentage) { |
|||
const percents = params.value / this.total * 100; |
|||
value = formatValue(percents, this.settings.tooltipValueDecimals, '%', false); |
|||
} else { |
|||
value = formatValue(params.value, this.settings.tooltipValueDecimals, this.units, false); |
|||
} |
|||
const textElement: HTMLElement = this.renderer.createElement('div'); |
|||
this.renderer.setStyle(textElement, 'display', 'inline-flex'); |
|||
this.renderer.setStyle(textElement, 'align-items', 'center'); |
|||
this.renderer.setStyle(textElement, 'gap', '8px'); |
|||
const labelElement: HTMLElement = this.renderer.createElement('div'); |
|||
this.renderer.appendChild(labelElement, this.renderer.createText(params.name)); |
|||
this.renderer.setStyle(labelElement, 'font-family', 'Roboto'); |
|||
this.renderer.setStyle(labelElement, 'font-size', '11px'); |
|||
this.renderer.setStyle(labelElement, 'font-style', 'normal'); |
|||
this.renderer.setStyle(labelElement, 'font-weight', '400'); |
|||
this.renderer.setStyle(labelElement, 'line-height', '16px'); |
|||
this.renderer.setStyle(labelElement, 'letter-spacing', '0.25px'); |
|||
this.renderer.setStyle(labelElement, 'color', 'rgba(0, 0, 0, 0.38)'); |
|||
const valueElement: HTMLElement = this.renderer.createElement('div'); |
|||
this.renderer.appendChild(valueElement, this.renderer.createText(value)); |
|||
this.renderer.setStyle(valueElement, 'font-family', this.settings.tooltipValueFont.family); |
|||
this.renderer.setStyle(valueElement, 'font-size', this.settings.tooltipValueFont.size + this.settings.tooltipValueFont.sizeUnit); |
|||
this.renderer.setStyle(valueElement, 'font-style', this.settings.tooltipValueFont.style); |
|||
this.renderer.setStyle(valueElement, 'font-weight', this.settings.tooltipValueFont.weight); |
|||
this.renderer.setStyle(valueElement, 'line-height', this.settings.tooltipValueFont.lineHeight); |
|||
this.renderer.setStyle(valueElement, 'color', this.settings.tooltipValueColor); |
|||
this.renderer.appendChild(textElement, labelElement); |
|||
this.renderer.appendChild(textElement, valueElement); |
|||
return textElement; |
|||
}, |
|||
padding: [4, 8], |
|||
backgroundColor: this.settings.tooltipBackgroundColor, |
|||
extraCssText: `line-height: 1; backdrop-filter: blur(${this.settings.tooltipBackgroundBlur}px);` |
|||
}; |
|||
this.doughnutOptions.series[0].tooltip.position = (pos) => [pos[0] + 10, pos[1] + 10]; |
|||
} |
|||
this.updateSeriesData(false); |
|||
|
|||
this.renderer.setStyle(this.doughnutChart.getDom().firstChild, 'overflow', 'visible'); |
|||
if (this.settings.autoScale) { |
|||
this.renderer.setStyle(this.doughnutChart.getDom().firstChild, 'position', 'absolute'); |
|||
} |
|||
this.renderer.setStyle(this.doughnutChart.getDom().firstChild.firstChild, 'overflow', 'visible'); |
|||
|
|||
this.svgShape = SVG(this.doughnutChart.getDom().firstChild.firstChild).toRoot(); |
|||
|
|||
if (this.showTotal) { |
|||
this.totalTextNode = this.svgShape.text('').font({ |
|||
family: 'Roboto', |
|||
leading: 1 |
|||
}).attr({'text-anchor': 'middle'}); |
|||
this.renderTotal(); |
|||
} |
|||
|
|||
this.shapeResize$ = new ResizeObserver(() => { |
|||
this.onResize(); |
|||
}); |
|||
this.shapeResize$.observe(this.doughnutContent.nativeElement); |
|||
this.onResize(); |
|||
} |
|||
|
|||
private renderTotal() { |
|||
this.totalTextNode.text(add => { |
|||
add.tspan(this.translate.instant('widgets.doughnut.total')).font({size: '12px', weight: 400}).fill('rgba(0, 0, 0, 0.38)'); |
|||
add.tspan('').newLine().font({size: '4px'}); |
|||
add.tspan(this.totalText).newLine().font( |
|||
{family: this.settings.totalValueFont.family, |
|||
size: this.settings.totalValueFont.size + this.settings.totalValueFont.sizeUnit, |
|||
weight: this.settings.totalValueFont.weight, |
|||
style: this.settings.totalValueFont.style} |
|||
).fill(this.totalValueColor.color); |
|||
}).center(this.svgShape.bbox().width / 2, this.svgShape.bbox().height / 2); |
|||
} |
|||
|
|||
private onResize() { |
|||
if (this.legendHorizontal) { |
|||
this.renderer.setStyle(this.doughnutShape.nativeElement, 'max-width', null); |
|||
this.renderer.setStyle(this.doughnutShape.nativeElement, 'min-width', null); |
|||
this.renderer.setStyle(this.doughnutLegend.nativeElement, 'flex', null); |
|||
} |
|||
const shapeWidth = this.doughnutShape.nativeElement.getBoundingClientRect().width; |
|||
const shapeHeight = this.doughnutShape.nativeElement.getBoundingClientRect().height; |
|||
const size = Math.min(shapeWidth, shapeHeight); |
|||
if (this.legendHorizontal) { |
|||
this.renderer.setStyle(this.doughnutShape.nativeElement, 'max-width', `${size}px`); |
|||
this.renderer.setStyle(this.doughnutShape.nativeElement, 'min-width', `${size}px`); |
|||
this.renderer.setStyle(this.doughnutLegend.nativeElement, 'flex', '1'); |
|||
} |
|||
if (!this.settings.autoScale) { |
|||
const innerRadius = size / 2 - shapeSegmentWidth; |
|||
const outerRadius = size / 2; |
|||
this.doughnutOptions.series[0].radius = [innerRadius, outerRadius]; |
|||
this.doughnutChart.setOption(this.doughnutOptions); |
|||
} else { |
|||
this.scale = size / shapeSize; |
|||
this.renderer.setStyle(this.doughnutChart.getDom().firstChild, 'transform', `scale(${this.scale})`); |
|||
} |
|||
if (!this.settings.autoScale) { |
|||
this.doughnutChart.resize(); |
|||
} |
|||
if (this.showTotal) { |
|||
this.totalTextNode.center((this.settings.autoScale ? shapeSize : shapeWidth) / 2, |
|||
(this.settings.autoScale ? shapeSize : shapeHeight) / 2); |
|||
} |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,159 @@ |
|||
///
|
|||
/// Copyright © 2016-2023 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 { |
|||
BackgroundSettings, |
|||
BackgroundType, |
|||
ColorSettings, |
|||
constantColor, |
|||
Font |
|||
} from '@shared/models/widget-settings.models'; |
|||
|
|||
export enum DoughnutLayout { |
|||
default = 'default', |
|||
with_total = 'with_total' |
|||
} |
|||
|
|||
export const doughnutLayouts = Object.keys(DoughnutLayout) as DoughnutLayout[]; |
|||
|
|||
export const doughnutLayoutTranslations = new Map<DoughnutLayout, string>( |
|||
[ |
|||
[DoughnutLayout.default, 'widgets.doughnut.layout-default'], |
|||
[DoughnutLayout.with_total, 'widgets.doughnut.layout-with-total'] |
|||
] |
|||
); |
|||
|
|||
export const doughnutLayoutImages = new Map<DoughnutLayout, string>( |
|||
[ |
|||
[DoughnutLayout.default, 'assets/widget/doughnut/default-layout.svg'], |
|||
[DoughnutLayout.with_total, 'assets/widget/doughnut/with-total-layout.svg'] |
|||
] |
|||
); |
|||
|
|||
export const horizontalDoughnutLayoutImages = new Map<DoughnutLayout, string>( |
|||
[ |
|||
[DoughnutLayout.default, 'assets/widget/doughnut/horizontal-default-layout.svg'], |
|||
[DoughnutLayout.with_total, 'assets/widget/doughnut/horizontal-with-total-layout.svg'] |
|||
] |
|||
); |
|||
|
|||
export enum DoughnutLegendPosition { |
|||
top = 'top', |
|||
bottom = 'bottom', |
|||
left = 'left', |
|||
right = 'right' |
|||
} |
|||
|
|||
export const doughnutLegendPositionTranslations = new Map<DoughnutLegendPosition, string>( |
|||
[ |
|||
[DoughnutLegendPosition.top, 'widgets.doughnut.legend-position-top'], |
|||
[DoughnutLegendPosition.bottom, 'widgets.doughnut.legend-position-bottom'], |
|||
[DoughnutLegendPosition.left, 'widgets.doughnut.legend-position-left'], |
|||
[DoughnutLegendPosition.right, 'widgets.doughnut.legend-position-right'] |
|||
] |
|||
); |
|||
|
|||
export enum DoughnutTooltipValueType { |
|||
absolute = 'absolute', |
|||
percentage = 'percentage' |
|||
} |
|||
|
|||
export const doughnutTooltipValueTypes = Object.keys(DoughnutTooltipValueType) as DoughnutTooltipValueType[]; |
|||
|
|||
export const doughnutTooltipValueTypeTranslations = new Map<DoughnutTooltipValueType, string>( |
|||
[ |
|||
[DoughnutTooltipValueType.absolute, 'widgets.doughnut.tooltip-value-type-absolute'], |
|||
[DoughnutTooltipValueType.percentage, 'widgets.doughnut.tooltip-value-type-percentage'] |
|||
] |
|||
); |
|||
|
|||
export interface DoughnutWidgetSettings { |
|||
layout: DoughnutLayout; |
|||
autoScale: boolean; |
|||
totalValueFont: Font; |
|||
totalValueColor: ColorSettings; |
|||
showLegend: boolean; |
|||
legendPosition: DoughnutLegendPosition; |
|||
legendLabelFont: Font; |
|||
legendLabelColor: string; |
|||
legendValueFont: Font; |
|||
legendValueColor: string; |
|||
showTooltip: boolean; |
|||
tooltipValueType: DoughnutTooltipValueType; |
|||
tooltipValueDecimals: number; |
|||
tooltipValueFont: Font; |
|||
tooltipValueColor: string; |
|||
tooltipBackgroundColor: string; |
|||
tooltipBackgroundBlur: number; |
|||
background: BackgroundSettings; |
|||
} |
|||
|
|||
export const doughnutDefaultSettings = (horizontal: boolean): DoughnutWidgetSettings => ({ |
|||
layout: DoughnutLayout.default, |
|||
autoScale: true, |
|||
totalValueFont: { |
|||
family: 'Roboto', |
|||
size: 24, |
|||
sizeUnit: 'px', |
|||
style: 'normal', |
|||
weight: '500', |
|||
lineHeight: '1' |
|||
}, |
|||
totalValueColor: constantColor('rgba(0, 0, 0, 0.87)'), |
|||
showLegend: true, |
|||
legendPosition: horizontal ? DoughnutLegendPosition.right : DoughnutLegendPosition.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: DoughnutTooltipValueType.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 |
|||
} |
|||
} |
|||
}); |
|||
@ -0,0 +1,153 @@ |
|||
<!-- |
|||
|
|||
Copyright © 2016-2023 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]="doughnutWidgetSettingsForm"> |
|||
<div class="tb-form-panel"> |
|||
<div class="tb-form-panel-title" translate>widgets.doughnut.doughnut-card-style</div> |
|||
<tb-image-cards-select rowHeight="{{ horizontal ? '8:5' : '5:4' }}" |
|||
[cols]="{columns: 2, |
|||
breakpoints: { |
|||
'lt-sm': 1 |
|||
}}" |
|||
label="{{ 'widgets.doughnut.layout' | translate }}" formControlName="layout"> |
|||
<tb-image-cards-select-option *ngFor="let layout of doughnutLayouts" |
|||
[value]="layout" |
|||
[image]="doughnutLayoutImageMap.get(layout)"> |
|||
{{ doughnutLayoutTranslationMap.get(layout) | translate }} |
|||
</tb-image-cards-select-option> |
|||
</tb-image-cards-select> |
|||
<div class="tb-form-row"> |
|||
<mat-slide-toggle class="mat-slide" formControlName="autoScale"> |
|||
{{ 'widgets.value-card.auto-scale' | translate }} |
|||
</mat-slide-toggle> |
|||
</div> |
|||
<div [fxShow]="totalEnabled" class="tb-form-row space-between"> |
|||
<div>{{ 'widgets.doughnut.central-total-value' | translate }}</div> |
|||
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px"> |
|||
<tb-font-settings formControlName="totalValueFont" |
|||
[previewText]="valuePreviewFn"> |
|||
</tb-font-settings> |
|||
<tb-color-settings formControlName="totalValueColor" settingsKey="{{'widgets.doughnut.central-total-value' | translate }}"> |
|||
</tb-color-settings> |
|||
</div> |
|||
</div> |
|||
<div class="tb-form-panel tb-slide-toggle"> |
|||
<mat-expansion-panel class="tb-settings" [expanded]="doughnutWidgetSettingsForm.get('showLegend').value" [disabled]="!doughnutWidgetSettingsForm.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 doughnutLegendPositions" [value]="pos"> |
|||
{{ doughnutLegendPositionTranslationMap.get(pos) | translate }} |
|||
</mat-option> |
|||
</mat-select> |
|||
</mat-form-field> |
|||
</div> |
|||
<div class="tb-form-row space-between"> |
|||
<div>{{ 'widgets.doughnut.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>{{ 'widgets.doughnut.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]="doughnutWidgetSettingsForm.get('showTooltip').value" [disabled]="!doughnutWidgetSettingsForm.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"> |
|||
{{ 'widgets.doughnut.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>{{ 'widgets.doughnut.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 doughnutTooltipValueTypes" [value]="type"> |
|||
{{ doughnutTooltipValueTypeTranslationMap.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>{{ 'widgets.doughnut.tooltip-background-color' | translate }}</div> |
|||
<tb-color-input asBoxInput |
|||
colorClearButton |
|||
formControlName="tooltipBackgroundColor"> |
|||
</tb-color-input> |
|||
</div> |
|||
<div class="tb-form-row space-between"> |
|||
<div>{{ 'widgets.doughnut.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> |
|||
<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,204 @@ |
|||
///
|
|||
/// Copyright © 2016-2023 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, Injector } from '@angular/core'; |
|||
import { WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models'; |
|||
import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; |
|||
import { Store } from '@ngrx/store'; |
|||
import { AppState } from '@core/core.state'; |
|||
import { formatValue, isDefinedAndNotNull } from '@core/utils'; |
|||
import { getDataKey } from '@shared/models/widget-settings.models'; |
|||
import { |
|||
doughnutDefaultSettings, |
|||
DoughnutLayout, |
|||
doughnutLayoutImages, |
|||
doughnutLayouts, |
|||
doughnutLayoutTranslations, |
|||
DoughnutLegendPosition, |
|||
doughnutLegendPositionTranslations, |
|||
DoughnutTooltipValueType, |
|||
doughnutTooltipValueTypes, |
|||
doughnutTooltipValueTypeTranslations, |
|||
horizontalDoughnutLayoutImages |
|||
} from '@home/components/widget/lib/chart/doughnut-widget.models'; |
|||
import { WidgetConfigComponentData } from '@home/models/widget-component.models'; |
|||
|
|||
@Component({ |
|||
selector: 'tb-doughnut-widget-settings', |
|||
templateUrl: './doughnut-widget-settings.component.html', |
|||
styleUrls: [] |
|||
}) |
|||
export class DoughnutWidgetSettingsComponent extends WidgetSettingsComponent { |
|||
|
|||
get totalEnabled(): boolean { |
|||
const layout: DoughnutLayout = this.doughnutWidgetSettingsForm.get('layout').value; |
|||
return layout === DoughnutLayout.with_total; |
|||
} |
|||
|
|||
doughnutLayouts = doughnutLayouts; |
|||
|
|||
doughnutLayoutTranslationMap = doughnutLayoutTranslations; |
|||
|
|||
horizontal = false; |
|||
|
|||
doughnutLayoutImageMap: Map<DoughnutLayout, string>; |
|||
|
|||
doughnutLegendPositions: DoughnutLegendPosition[]; |
|||
|
|||
doughnutLegendPositionTranslationMap = doughnutLegendPositionTranslations; |
|||
|
|||
doughnutTooltipValueTypes = doughnutTooltipValueTypes; |
|||
|
|||
doughnutTooltipValueTypeTranslationMap = doughnutTooltipValueTypeTranslations; |
|||
|
|||
doughnutWidgetSettingsForm: UntypedFormGroup; |
|||
|
|||
valuePreviewFn = this._valuePreviewFn.bind(this); |
|||
|
|||
tooltipValuePreviewFn = this._tooltipValuePreviewFn.bind(this); |
|||
|
|||
constructor(protected store: Store<AppState>, |
|||
private $injector: Injector, |
|||
private fb: UntypedFormBuilder) { |
|||
super(store); |
|||
} |
|||
|
|||
protected settingsForm(): UntypedFormGroup { |
|||
return this.doughnutWidgetSettingsForm; |
|||
} |
|||
|
|||
protected onWidgetConfigSet(widgetConfig: WidgetConfigComponentData) { |
|||
const params = widgetConfig.typeParameters as any; |
|||
this.horizontal = isDefinedAndNotNull(params.horizontal) ? params.horizontal : false; |
|||
this.doughnutLayoutImageMap = this.horizontal ? horizontalDoughnutLayoutImages : doughnutLayoutImages; |
|||
this.doughnutLegendPositions = this.horizontal ? [DoughnutLegendPosition.left, DoughnutLegendPosition.right] : |
|||
[DoughnutLegendPosition.top, DoughnutLegendPosition.bottom]; |
|||
} |
|||
|
|||
protected defaultSettings(): WidgetSettings { |
|||
return doughnutDefaultSettings(this.horizontal); |
|||
} |
|||
|
|||
protected onSettingsSet(settings: WidgetSettings) { |
|||
this.doughnutWidgetSettingsForm = this.fb.group({ |
|||
layout: [settings.layout, []], |
|||
autoScale: [settings.autoScale, []], |
|||
|
|||
totalValueFont: [settings.totalValueFont, []], |
|||
totalValueColor: [settings.totalValueColor, []], |
|||
|
|||
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 ['layout', 'showLegend', 'showTooltip']; |
|||
} |
|||
|
|||
protected updateValidators(emitEvent: boolean) { |
|||
const layout: DoughnutLayout = this.doughnutWidgetSettingsForm.get('layout').value; |
|||
const showLegend: boolean = this.doughnutWidgetSettingsForm.get('showLegend').value; |
|||
const showTooltip: boolean = this.doughnutWidgetSettingsForm.get('showTooltip').value; |
|||
|
|||
const totalEnabled = layout === DoughnutLayout.with_total; |
|||
|
|||
if (showLegend) { |
|||
this.doughnutWidgetSettingsForm.get('legendPosition').enable(); |
|||
this.doughnutWidgetSettingsForm.get('legendLabelFont').enable(); |
|||
this.doughnutWidgetSettingsForm.get('legendLabelColor').enable(); |
|||
this.doughnutWidgetSettingsForm.get('legendValueFont').enable(); |
|||
this.doughnutWidgetSettingsForm.get('legendValueColor').enable(); |
|||
} else { |
|||
this.doughnutWidgetSettingsForm.get('legendPosition').disable(); |
|||
this.doughnutWidgetSettingsForm.get('legendLabelFont').disable(); |
|||
this.doughnutWidgetSettingsForm.get('legendLabelColor').disable(); |
|||
this.doughnutWidgetSettingsForm.get('legendValueFont').disable(); |
|||
this.doughnutWidgetSettingsForm.get('legendValueColor').disable(); |
|||
} |
|||
if (showTooltip) { |
|||
this.doughnutWidgetSettingsForm.get('tooltipValueType').enable(); |
|||
this.doughnutWidgetSettingsForm.get('tooltipValueDecimals').enable(); |
|||
this.doughnutWidgetSettingsForm.get('tooltipValueFont').enable(); |
|||
this.doughnutWidgetSettingsForm.get('tooltipValueColor').enable(); |
|||
this.doughnutWidgetSettingsForm.get('tooltipBackgroundColor').enable(); |
|||
this.doughnutWidgetSettingsForm.get('tooltipBackgroundBlur').enable(); |
|||
} else { |
|||
this.doughnutWidgetSettingsForm.get('tooltipValueType').disable(); |
|||
this.doughnutWidgetSettingsForm.get('tooltipValueDecimals').disable(); |
|||
this.doughnutWidgetSettingsForm.get('tooltipValueFont').disable(); |
|||
this.doughnutWidgetSettingsForm.get('tooltipValueColor').disable(); |
|||
this.doughnutWidgetSettingsForm.get('tooltipBackgroundColor').disable(); |
|||
this.doughnutWidgetSettingsForm.get('tooltipBackgroundBlur').disable(); |
|||
} |
|||
if (totalEnabled) { |
|||
this.doughnutWidgetSettingsForm.get('totalValueFont').enable(); |
|||
this.doughnutWidgetSettingsForm.get('totalValueColor').enable(); |
|||
} else { |
|||
this.doughnutWidgetSettingsForm.get('totalValueFont').disable(); |
|||
this.doughnutWidgetSettingsForm.get('totalValueColor').disable(); |
|||
} |
|||
} |
|||
|
|||
private _centerValuePreviewFn(): string { |
|||
const centerValueDataKey = getDataKey(this.widgetConfig.config.datasources, 1); |
|||
if (centerValueDataKey) { |
|||
let units: string = this.widgetConfig.config.units; |
|||
let decimals: number = this.widgetConfig.config.decimals; |
|||
if (isDefinedAndNotNull(centerValueDataKey?.decimals)) { |
|||
decimals = centerValueDataKey.decimals; |
|||
} |
|||
if (centerValueDataKey?.units) { |
|||
units = centerValueDataKey.units; |
|||
} |
|||
return formatValue(25, decimals, units, true); |
|||
} else { |
|||
return '225°'; |
|||
} |
|||
} |
|||
|
|||
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: DoughnutTooltipValueType = this.doughnutWidgetSettingsForm.get('tooltipValueType').value; |
|||
const decimals: number = this.doughnutWidgetSettingsForm.get('tooltipValueDecimals').value; |
|||
if (tooltipValueType === DoughnutTooltipValueType.percentage) { |
|||
return formatValue(35, decimals, '%', false); |
|||
} else { |
|||
const units: string = this.widgetConfig.config.units; |
|||
return formatValue(110, decimals, units, false); |
|||
} |
|||
} |
|||
|
|||
} |
|||
|
After Width: | Height: | Size: 45 KiB |
|
After Width: | Height: | Size: 46 KiB |
|
After Width: | Height: | Size: 46 KiB |
|
After Width: | Height: | Size: 45 KiB |
Loading…
Reference in new issue