Browse Source

Merge branch 'master' of github.com:thingsboard/thingsboard

pull/9583/head
Andrii Shvaika 3 years ago
parent
commit
12d7b6bb6d
  1. 6
      application/src/main/data/json/system/widget_bundles/charts.json
  2. 31
      application/src/main/data/json/system/widget_types/doughnut.json
  3. 30
      application/src/main/data/json/system/widget_types/doughnut_deprecated.json
  4. 29
      application/src/main/data/json/system/widget_types/horizontal_doughnut.json
  5. 1
      ui-ngx/package.json
  6. 4
      ui-ngx/src/app/core/api/widget-subscription.ts
  7. 4
      ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-widget-select.component.ts
  8. 2
      ui-ngx/src/app/modules/home/components/grid/scroll-grid.component.ts
  9. 12
      ui-ngx/src/app/modules/home/components/widget/config/basic/basic-widget-config.module.ts
  10. 5
      ui-ngx/src/app/modules/home/components/widget/config/basic/cards/progress-bar-basic-config.component.html
  11. 6
      ui-ngx/src/app/modules/home/components/widget/config/basic/cards/value-card-basic-config.component.html
  12. 5
      ui-ngx/src/app/modules/home/components/widget/config/basic/cards/value-chart-card-basic-config.component.html
  13. 244
      ui-ngx/src/app/modules/home/components/widget/config/basic/chart/doughnut-basic-config.component.html
  14. 339
      ui-ngx/src/app/modules/home/components/widget/config/basic/chart/doughnut-basic-config.component.ts
  15. 2
      ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-keys-panel.component.html
  16. 8
      ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-keys-panel.component.ts
  17. 6
      ui-ngx/src/app/modules/home/components/widget/config/basic/indicator/battery-level-basic-config.component.html
  18. 13
      ui-ngx/src/app/modules/home/components/widget/config/basic/indicator/liquid-level-card-basic-config.component.html
  19. 5
      ui-ngx/src/app/modules/home/components/widget/config/basic/indicator/signal-strength-basic-config.component.html
  20. 6
      ui-ngx/src/app/modules/home/components/widget/config/basic/weather/wind-speed-direction-basic-config.component.html
  21. 37
      ui-ngx/src/app/modules/home/components/widget/lib/chart/doughnut-widget.component.html
  22. 110
      ui-ngx/src/app/modules/home/components/widget/lib/chart/doughnut-widget.component.scss
  23. 535
      ui-ngx/src/app/modules/home/components/widget/lib/chart/doughnut-widget.component.ts
  24. 159
      ui-ngx/src/app/modules/home/components/widget/lib/chart/doughnut-widget.models.ts
  25. 5
      ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/progress-bar-widget-settings.component.html
  26. 6
      ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/value-card-widget-settings.component.html
  27. 5
      ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/value-chart-card-widget-settings.component.html
  28. 153
      ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/doughnut-widget-settings.component.html
  29. 204
      ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/doughnut-widget-settings.component.ts
  30. 8
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/count-widget-settings.component.html
  31. 75
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/image-cards-select.component.ts
  32. 6
      ui-ngx/src/app/modules/home/components/widget/lib/settings/indicator/battery-level-widget-settings.component.html
  33. 13
      ui-ngx/src/app/modules/home/components/widget/lib/settings/indicator/liquid-level-card-widget-settings.component.html
  34. 5
      ui-ngx/src/app/modules/home/components/widget/lib/settings/indicator/signal-strength-widget-settings.component.html
  35. 6
      ui-ngx/src/app/modules/home/components/widget/lib/settings/weather/wind-speed-direction-widget-settings.component.html
  36. 8
      ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts
  37. 7
      ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts
  38. 26
      ui-ngx/src/app/modules/home/models/datasource/scroll-grid-datasource.ts
  39. 7
      ui-ngx/src/app/shared/models/constants.ts
  40. 4
      ui-ngx/src/app/shared/models/widget.models.ts
  41. 21
      ui-ngx/src/assets/locale/locale.constant-en_US.json
  42. 29
      ui-ngx/src/assets/widget/doughnut/default-layout.svg
  43. 29
      ui-ngx/src/assets/widget/doughnut/horizontal-default-layout.svg
  44. 28
      ui-ngx/src/assets/widget/doughnut/horizontal-with-total-layout.svg
  45. 28
      ui-ngx/src/assets/widget/doughnut/with-total-layout.svg
  46. 20
      ui-ngx/yarn.lock

6
application/src/main/data/json/system/widget_bundles/charts.json

@ -16,8 +16,10 @@
"charts.bars",
"charts.pie",
"charts.pie_chart_js",
"charts.doughnut_chart_js",
"doughnut",
"horizontal_doughnut",
"charts.polar_area_chart_js",
"charts.radar_chart_js"
"charts.radar_chart_js",
"charts.doughnut_chart_js"
]
}

31
application/src/main/data/json/system/widget_types/doughnut.json

File diff suppressed because one or more lines are too long

30
application/src/main/data/json/system/widget_types/doughnut_deprecated.json

File diff suppressed because one or more lines are too long

29
application/src/main/data/json/system/widget_types/horizontal_doughnut.json

File diff suppressed because one or more lines are too long

1
ui-ngx/package.json

@ -54,6 +54,7 @@
"core-js": "^3.29.1",
"date-fns": "2.0.0-alpha.27",
"dayjs": "1.11.4",
"echarts": "^5.4.3",
"flot": "https://github.com/thingsboard/flot.git#0.9-work",
"flot.curvedlines": "https://github.com/MichaelZinsmaier/CurvedLines.git#master",
"font-awesome": "^4.7.0",

4
ui-ngx/src/app/core/api/widget-subscription.ts

@ -1537,7 +1537,7 @@ export class WidgetSubscription implements IWidgetSubscription {
}
if (this.type === widgetType.latest) {
const prevData = currentData.data;
if (!data.data.length) {
if (!data.data.length && !prevData.length) {
update = false;
} else if (prevData && prevData[0] && prevData[0].length > 1 && data.data.length > 0) {
const prevTs = prevData[0][0];
@ -1560,6 +1560,8 @@ export class WidgetSubscription implements IWidgetSubscription {
}
this.notifyDataLoaded();
this.onDataUpdated(detectChanges);
} else if (this.loadingData) {
this.notifyDataLoaded();
}
}

4
ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-widget-select.component.ts

@ -142,8 +142,8 @@ export class DashboardWidgetSelectComponent implements OnInit {
columns: 2,
breakpoints: {
'screen and (min-width: 2000px)': 5,
'screen and (min-width: 1280px)': 4,
'screen and (min-width: 600px)': 3
'gt-md': 4,
'gt-xs': 3
}
};

2
ui-ngx/src/app/modules/home/components/grid/scroll-grid.component.ts

@ -47,7 +47,7 @@ export class ScrollGridComponent<T, F> implements OnInit, AfterViewInit, OnChang
viewport: CdkVirtualScrollViewport;
@Input()
columns: ScrollGridColumns = {columns: 1};
columns: ScrollGridColumns | number = 1;
@Input()
fetchFunction: GridEntitiesFetchFunction<T, F>;

12
ui-ngx/src/app/modules/home/components/widget/config/basic/basic-widget-config.module.ts

@ -82,6 +82,9 @@ import {
import {
LiquidLevelCardBasicConfigComponent
} from '@home/components/widget/config/basic/indicator/liquid-level-card-basic-config.component';
import {
DoughnutBasicConfigComponent
} from '@home/components/widget/config/basic/chart/doughnut-basic-config.component';
@NgModule({
declarations: [
@ -107,7 +110,8 @@ import {
RadialGaugeBasicConfigComponent,
ThermometerScaleGaugeBasicConfigComponent,
CompassGaugeBasicConfigComponent,
LiquidLevelCardBasicConfigComponent
LiquidLevelCardBasicConfigComponent,
DoughnutBasicConfigComponent
],
imports: [
CommonModule,
@ -137,7 +141,8 @@ import {
RadialGaugeBasicConfigComponent,
ThermometerScaleGaugeBasicConfigComponent,
CompassGaugeBasicConfigComponent,
LiquidLevelCardBasicConfigComponent
LiquidLevelCardBasicConfigComponent,
DoughnutBasicConfigComponent
]
})
export class BasicWidgetConfigModule {
@ -161,5 +166,6 @@ export const basicWidgetConfigComponentsMap: {[key: string]: Type<IBasicWidgetCo
'tb-radial-gauge-basic-config': RadialGaugeBasicConfigComponent,
'tb-thermometer-scale-gauge-basic-config': ThermometerScaleGaugeBasicConfigComponent,
'tb-compass-gauge-basic-config': CompassGaugeBasicConfigComponent,
'tb-liquid-level-card-basic-config': LiquidLevelCardBasicConfigComponent
'tb-liquid-level-card-basic-config': LiquidLevelCardBasicConfigComponent,
'tb-doughnut-basic-config': DoughnutBasicConfigComponent
};

5
ui-ngx/src/app/modules/home/components/widget/config/basic/cards/progress-bar-basic-config.component.html

@ -31,7 +31,10 @@
<div class="tb-form-panel">
<div class="tb-form-panel-title" translate>widget-config.appearance</div>
<tb-image-cards-select rowHeight="2:1"
cols="2"
[cols]="{columns: 2,
breakpoints: {
'lt-sm': 1
}}"
label="{{ 'widgets.progress-bar.layout' | translate }}" formControlName="layout">
<tb-image-cards-select-option *ngFor="let layout of progressBarLayouts"
[value]="layout"

6
ui-ngx/src/app/modules/home/components/widget/config/basic/cards/value-card-basic-config.component.html

@ -31,8 +31,10 @@
<div class="tb-form-panel">
<div class="tb-form-panel-title" translate>widget-config.appearance</div>
<tb-image-cards-select rowHeight="{{ horizontal ? '3:1' : '7:5' }}"
[cols]="horizontal ? 2 : 4"
[colsLtMd]="horizontal ? 1 : 2"
[cols]="{columns: horizontal ? 2 : 4,
breakpoints: {
'lt-md': horizontal ? 1 : 2
}}"
label="{{ 'widgets.value-card.layout' | translate }}" formControlName="layout">
<tb-image-cards-select-option *ngFor="let layout of valueCardLayouts"
[value]="layout"

5
ui-ngx/src/app/modules/home/components/widget/config/basic/cards/value-chart-card-basic-config.component.html

@ -30,7 +30,10 @@
<div class="tb-form-panel">
<div class="tb-form-panel-title" translate>widget-config.appearance</div>
<tb-image-cards-select rowHeight="2:1"
cols="2"
[cols]="{columns: 2,
breakpoints: {
'lt-sm': 1
}}"
label="{{ 'widgets.value-chart-card.layout' | translate }}" formControlName="layout">
<tb-image-cards-select-option *ngFor="let layout of valueChartCardLayouts"
[value]="layout"

244
ui-ngx/src/app/modules/home/components/widget/config/basic/chart/doughnut-basic-config.component.html

@ -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>

339
ui-ngx/src/app/modules/home/components/widget/config/basic/chart/doughnut-basic-config.component.ts

@ -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);
}
}
}

2
ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-keys-panel.component.html

@ -44,6 +44,8 @@
[hideDataKeyColor]="hideDataKeyColor"
[hideDecimals]="hideDecimals"
[hideUnits]="hideUnits"
[hideDataKeyDecimals]="hideDataKeyDecimals"
[hideDataKeyUnits]="hideDataKeyUnits"
[dataKeyType]="dataKeyType"
[singleRow]="false"
[keySettingsTitle]="keySettingsTitle"

8
ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-keys-panel.component.ts

@ -107,6 +107,14 @@ export class DataKeysPanelComponent implements ControlValueAccessor, OnInit, OnC
@coerceBoolean()
hideDecimals = false;
@Input()
@coerceBoolean()
hideDataKeyUnits = false;
@Input()
@coerceBoolean()
hideDataKeyDecimals = false;
@Input()
@coerceBoolean()
hideSourceSelection = false;

6
ui-ngx/src/app/modules/home/components/widget/config/basic/indicator/battery-level-basic-config.component.html

@ -31,8 +31,10 @@
<div class="tb-form-panel">
<div class="tb-form-panel-title" translate>widget-config.appearance</div>
<tb-image-cards-select rowHeight="1:1"
cols="4"
colsLtMd="2"
[cols]="{columns: 4,
breakpoints: {
'lt-md': 2
}}"
label="{{ 'widgets.battery-level.layout' | translate }}" formControlName="layout">
<tb-image-cards-select-option *ngFor="let layout of batteryLevelLayouts"
[value]="layout"

13
ui-ngx/src/app/modules/home/components/widget/config/basic/indicator/liquid-level-card-basic-config.component.html

@ -74,9 +74,11 @@
</div>
<tb-image-cards-select #shapesImageCardsSelect
[fxShow]="levelCardWidgetConfigForm.get('tankSelectionType').value === DataSourceType.static"
rowHeight="{{ '1:1' }}"
[cols]="5"
[colsLtMd]="2"
rowHeight="1:1"
[cols]="{columns: 5,
breakpoints: {
'lt-md': 2
}}"
style="width: 100%;"
label="{{ 'widgets.liquid-level-card.shape-type' | translate }}" formControlName="selectedShape">
<tb-image-cards-select-option *ngFor="let shape of shapes"
@ -106,9 +108,8 @@
</div>
</ng-container>
</div>
<tb-image-cards-select #layoutsImageCardsSelect rowHeight="{{ '1:1' }}"
[cols]="3"
[colsLtMd]="3"
<tb-image-cards-select #layoutsImageCardsSelect rowHeight="1:1"
cols="3"
label="{{ 'widgets.liquid-level-card.layout' | translate }}" formControlName="layout">
<tb-image-cards-select-option
*ngFor="let layout of LevelCardLayouts"

5
ui-ngx/src/app/modules/home/components/widget/config/basic/indicator/signal-strength-basic-config.component.html

@ -31,7 +31,10 @@
<div class="tb-form-panel">
<div class="tb-form-panel-title" translate>widget-config.appearance</div>
<tb-image-cards-select rowHeight="2:1"
cols="2"
[cols]="{columns: 2,
breakpoints: {
'lt-sm': 1
}}"
label="{{ 'widgets.signal-strength.layout' | translate }}" formControlName="layout">
<tb-image-cards-select-option *ngFor="let layout of signalStrengthLayouts"
[value]="layout"

6
ui-ngx/src/app/modules/home/components/widget/config/basic/weather/wind-speed-direction-basic-config.component.html

@ -79,8 +79,10 @@
<div class="tb-form-panel">
<div class="tb-form-panel-title" translate>widget-config.appearance</div>
<tb-image-cards-select rowHeight="1:1"
cols="3"
colsLtMd="2"
[cols]="{columns: 3,
breakpoints: {
'lt-md': 2
}}"
label="{{ 'widgets.wind-speed-direction.layout' | translate }}" formControlName="layout">
<tb-image-cards-select-option *ngFor="let layout of windSpeedDirectionLayouts"
[value]="layout"

37
ui-ngx/src/app/modules/home/components/widget/lib/chart/doughnut-widget.component.html

@ -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>

110
ui-ngx/src/app/modules/home/components/widget/lib/chart/doughnut-widget.component.scss

@ -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;
}
}
}
}
}

535
ui-ngx/src/app/modules/home/components/widget/lib/chart/doughnut-widget.component.ts

@ -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);
}
}
}

159
ui-ngx/src/app/modules/home/components/widget/lib/chart/doughnut-widget.models.ts

@ -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
}
}
});

5
ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/progress-bar-widget-settings.component.html

@ -19,7 +19,10 @@
<div class="tb-form-panel">
<div class="tb-form-panel-title" translate>widgets.progress-bar.progress-bar-card-style</div>
<tb-image-cards-select rowHeight="2:1"
cols="2"
[cols]="{columns: 2,
breakpoints: {
'lt-sm': 1
}}"
label="{{ 'widgets.progress-bar.layout' | translate }}" formControlName="layout">
<tb-image-cards-select-option *ngFor="let layout of progressBarLayouts"
[value]="layout"

6
ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/value-card-widget-settings.component.html

@ -19,8 +19,10 @@
<div class="tb-form-panel">
<div class="tb-form-panel-title" translate>widgets.value-card.value-card-style</div>
<tb-image-cards-select rowHeight="{{ horizontal ? '3:1' : '7:5' }}"
[cols]="horizontal ? 2 : 4"
[colsLtMd]="horizontal ? 1 : 2"
[cols]="{columns: horizontal ? 2 : 4,
breakpoints: {
'lt-md': horizontal ? 1 : 2
}}"
label="{{ 'widgets.value-card.layout' | translate }}" formControlName="layout">
<tb-image-cards-select-option *ngFor="let layout of valueCardLayouts"
[value]="layout"

5
ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/value-chart-card-widget-settings.component.html

@ -19,7 +19,10 @@
<div class="tb-form-panel">
<div class="tb-form-panel-title" translate>widgets.value-chart-card.value-chart-card-style</div>
<tb-image-cards-select rowHeight="2:1"
cols="2"
[cols]="{columns: 2,
breakpoints: {
'lt-sm': 1
}}"
label="{{ 'widgets.value-chart-card.layout' | translate }}" formControlName="layout">
<tb-image-cards-select-option *ngFor="let layout of valueChartCardLayouts"
[value]="layout"

153
ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/doughnut-widget-settings.component.html

@ -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>

204
ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/doughnut-widget-settings.component.ts

@ -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);
}
}
}

8
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/count-widget-settings.component.html

@ -16,9 +16,11 @@
-->
<ng-container [formGroup]="countWidgetConfigForm">
<tb-image-cards-select rowHeight="{{ '3:1' }}"
[cols]="2"
[colsLtMd]="1"
<tb-image-cards-select rowHeight="3:1"
[cols]="{columns: 2,
breakpoints: {
'lt-md': 1
}}"
label="{{ 'widgets.count.layout' | translate }}" formControlName="layout">
<tb-image-cards-select-option *ngFor="let layout of countCardLayouts"
[value]="layout"

75
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/image-cards-select.component.ts

@ -21,18 +21,21 @@ import {
Directive,
ElementRef,
forwardRef,
Input, OnChanges,
OnDestroy, OnInit,
QueryList, SimpleChanges,
Input,
OnChanges,
OnDestroy,
OnInit,
QueryList,
SimpleChanges,
ViewEncapsulation
} from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR, UntypedFormControl } from '@angular/forms';
import { coerceBoolean } from '@shared/decorators/coercion';
import { BehaviorSubject, combineLatest, Observable, Subject } from 'rxjs';
import { EMPTY, Observable, Subject } from 'rxjs';
import { map, share, startWith, takeUntil } from 'rxjs/operators';
import { BreakpointObserver } from '@angular/cdk/layout';
import { MediaBreakpoints } from '@shared/models/constants';
import { BreakpointObserver, BreakpointState } from '@angular/cdk/layout';
import { SafeUrl } from '@angular/platform-browser';
import { resolveBreakpoint } from '@shared/models/constants';
export interface ImageCardsSelectOption {
name: string;
@ -40,6 +43,11 @@ export interface ImageCardsSelectOption {
image: string | SafeUrl;
}
export interface ImageCardsColumns {
columns: number;
breakpoints?: {[breakpoint: string]: number};
}
@Directive(
{
// eslint-disable-next-line @angular-eslint/directive-selector
@ -83,10 +91,7 @@ export class ImageCardsSelectComponent implements ControlValueAccessor, OnInit,
disabled: boolean;
@Input()
cols = 4;
@Input()
colsLtMd = 2;
cols: ImageCardsColumns | number = 4;
@Input()
rowHeight = '9:5';
@ -108,28 +113,20 @@ export class ImageCardsSelectComponent implements ControlValueAccessor, OnInit,
private _destroyed = new Subject<void>();
private _colsChanged = new BehaviorSubject<void>(null);
constructor(private breakpointObserver: BreakpointObserver) {
this.valueFormControl = new UntypedFormControl('');
}
ngOnInit(): void {
const gridColumns = this.breakpointObserver.isMatched(MediaBreakpoints['lt-md']) ? this.colsLtMd : this.cols;
this.cols$ = combineLatest({state: this.breakpointObserver
.observe(MediaBreakpoints['lt-md']), colsChanged: this._colsChanged.asObservable()}).pipe(
map((data) => data.state.matches ? this.colsLtMd : this.cols),
startWith(gridColumns),
share()
);
this._initCols();
}
ngOnChanges(changes: SimpleChanges): void {
for (const propName of Object.keys(changes)) {
const change = changes[propName];
if (!change.firstChange && change.currentValue !== change.previousValue) {
if (['cols', 'colsLtMd'].includes(propName)) {
this._colsChanged.next(null);
if (['cols'].includes(propName)) {
this._initCols();
}
}
}
@ -146,6 +143,40 @@ export class ImageCardsSelectComponent implements ControlValueAccessor, OnInit,
this._destroyed.complete();
}
private _initCols() {
const gridColumns = this._detectColumns();
let state: Observable<BreakpointState>;
if (typeof this.cols === 'object' && this.cols.breakpoints) {
const breakpoints = Object.keys(this.cols.breakpoints);
state = this.breakpointObserver.observe(breakpoints.map(breakpoint => resolveBreakpoint(breakpoint)));
} else {
state = EMPTY;
}
this.cols$ = state.pipe(
map(() => this._detectColumns()),
startWith(gridColumns),
share()
);
}
private _detectColumns(): number {
if (typeof this.cols !== 'object') {
return this.cols;
} else {
let columns = this.cols.columns;
if (this.cols.breakpoints) {
for (const breakpoint of Object.keys(this.cols.breakpoints)) {
const breakpointValue = resolveBreakpoint(breakpoint);
if (this.breakpointObserver.isMatched(breakpointValue)) {
columns = this.cols.breakpoints[breakpoint];
break;
}
}
}
return columns;
}
}
private syncImageCardsSelectOptions() {
if (this.imageCardsSelectOptions?.length) {
this.options.length = 0;
@ -165,7 +196,7 @@ export class ImageCardsSelectComponent implements ControlValueAccessor, OnInit,
this.propagateChange = fn;
}
registerOnTouched(fn: any): void {
registerOnTouched(_fn: any): void {
}
setDisabledState(isDisabled: boolean): void {

6
ui-ngx/src/app/modules/home/components/widget/lib/settings/indicator/battery-level-widget-settings.component.html

@ -19,8 +19,10 @@
<div class="tb-form-panel">
<div class="tb-form-panel-title" translate>widgets.battery-level.battery-level-card-style</div>
<tb-image-cards-select rowHeight="1:1"
cols="4"
colsLtMd="2"
[cols]="{columns: 4,
breakpoints: {
'lt-md': 2
}}"
label="{{ 'widgets.battery-level.layout' | translate }}" formControlName="layout">
<tb-image-cards-select-option *ngFor="let layout of batteryLevelLayouts"
[value]="layout"

13
ui-ngx/src/app/modules/home/components/widget/lib/settings/indicator/liquid-level-card-widget-settings.component.html

@ -29,9 +29,11 @@
</div>
<tb-image-cards-select #shapesImageCardsSelect
[fxShow]="levelCardWidgetSettingsForm.get('tankSelectionType').value === DataSourceType.static"
rowHeight="{{ '1:1' }}"
[cols]="5"
[colsLtMd]="2"
rowHeight="1:1"
[cols]="{columns: 5,
breakpoints: {
'lt-md': 2
}}"
style="width: 100%;"
label="{{ 'widgets.liquid-level-card.shape-type' | translate }}" formControlName="selectedShape">
<tb-image-cards-select-option *ngFor="let shape of shapes"
@ -65,9 +67,8 @@
<div class="tb-form-panel">
<div class="tb-form-panel-title" translate>widgets.liquid-level-card.layout</div>
<tb-image-cards-select #layoutsImageCardsSelect
rowHeight="{{ '1:1' }}"
[cols]="3"
[colsLtMd]="3"
rowHeight="1:1"
cols="3"
label="{{ 'widgets.liquid-level-card.layout' | translate }}" formControlName="layout">
<tb-image-cards-select-option
*ngFor="let layout of LevelCardLayouts"

5
ui-ngx/src/app/modules/home/components/widget/lib/settings/indicator/signal-strength-widget-settings.component.html

@ -19,7 +19,10 @@
<div class="tb-form-panel">
<div class="tb-form-panel-title" translate>widgets.signal-strength.signal-strength-card-style</div>
<tb-image-cards-select rowHeight="2:1"
cols="2"
[cols]="{columns: 2,
breakpoints: {
'lt-sm': 1
}}"
label="{{ 'widgets.signal-strength.layout' | translate }}" formControlName="layout">
<tb-image-cards-select-option *ngFor="let layout of signalStrengthLayouts"
[value]="layout"

6
ui-ngx/src/app/modules/home/components/widget/lib/settings/weather/wind-speed-direction-widget-settings.component.html

@ -19,8 +19,10 @@
<div class="tb-form-panel">
<div class="tb-form-panel-title" translate>widgets.wind-speed-direction.wind-speed-direction-card-style</div>
<tb-image-cards-select rowHeight="1:1"
cols="3"
colsLtMd="2"
[cols]="{columns: 3,
breakpoints: {
'lt-md': 2
}}"
label="{{ 'widgets.wind-speed-direction.layout' | translate }}" formControlName="layout">
<tb-image-cards-select-option *ngFor="let layout of windSpeedDirectionLayouts"
[value]="layout"

8
ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts

@ -302,6 +302,9 @@ import {
import {
LiquidLevelCardWidgetSettingsComponent
} from '@home/components/widget/lib/settings/indicator/liquid-level-card-widget-settings.component';
import {
DoughnutWidgetSettingsComponent
} from '@home/components/widget/lib/settings/chart/doughnut-widget-settings.component';
@NgModule({
declarations: [
@ -412,7 +415,8 @@ import {
SignalStrengthWidgetSettingsComponent,
ValueChartCardWidgetSettingsComponent,
ProgressBarWidgetSettingsComponent,
LiquidLevelCardWidgetSettingsComponent
LiquidLevelCardWidgetSettingsComponent,
DoughnutWidgetSettingsComponent
],
imports: [
CommonModule,
@ -529,6 +533,7 @@ import {
ValueChartCardWidgetSettingsComponent,
ProgressBarWidgetSettingsComponent,
LiquidLevelCardWidgetSettingsComponent,
DoughnutWidgetSettingsComponent
]
})
export class WidgetSettingsModule {
@ -611,4 +616,5 @@ export const widgetSettingsComponentsMap: {[key: string]: Type<IWidgetSettingsCo
'tb-value-chart-card-widget-settings': ValueChartCardWidgetSettingsComponent,
'tb-progress-bar-widget-settings': ProgressBarWidgetSettingsComponent,
'tb-liquid-level-card-widget-settings': LiquidLevelCardWidgetSettingsComponent,
'tb-doughnut-widget-settings': DoughnutWidgetSettingsComponent
};

7
ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts

@ -65,6 +65,7 @@ import { SignalStrengthWidgetComponent } from '@home/components/widget/lib/indic
import { ValueChartCardWidgetComponent } from '@home/components/widget/lib/cards/value-chart-card-widget.component';
import { ProgressBarWidgetComponent } from '@home/components/widget/lib/cards/progress-bar-widget.component';
import { LiquidLevelWidgetComponent } from '@home/components/widget/lib/indicator/liquid-level-widget.component';
import { DoughnutWidgetComponent } from '@home/components/widget/lib/chart/doughnut-widget.component';
@NgModule({
declarations:
@ -104,7 +105,8 @@ import { LiquidLevelWidgetComponent } from '@home/components/widget/lib/indicato
SignalStrengthWidgetComponent,
ValueChartCardWidgetComponent,
ProgressBarWidgetComponent,
LiquidLevelWidgetComponent
LiquidLevelWidgetComponent,
DoughnutWidgetComponent
],
imports: [
CommonModule,
@ -148,7 +150,8 @@ import { LiquidLevelWidgetComponent } from '@home/components/widget/lib/indicato
SignalStrengthWidgetComponent,
ValueChartCardWidgetComponent,
ProgressBarWidgetComponent,
LiquidLevelWidgetComponent
LiquidLevelWidgetComponent,
DoughnutWidgetComponent
],
providers: [
{provide: WIDGET_COMPONENTS_MODULE_TOKEN, useValue: WidgetComponentsModule }

26
ui-ngx/src/app/modules/home/models/datasource/scroll-grid-datasource.ts

@ -20,6 +20,7 @@ import { BehaviorSubject, Observable, of, Subscription } from 'rxjs';
import { catchError } from 'rxjs/operators';
import { emptyPageData, PageData } from '@shared/models/page/page-data';
import { BreakpointObserver } from '@angular/cdk/layout';
import { resolveBreakpoint } from '@shared/models/constants';
export type GridEntitiesFetchFunction<T, F> = (pageSize: number, page: number, filter: F) => Observable<PageData<T>>;
@ -48,7 +49,7 @@ export class ScrollGridDatasource<T, F> extends DataSource<(T | GridCellType)[]>
private _subscription: Subscription;
constructor(private breakpointObserver: BreakpointObserver,
private columns: ScrollGridColumns,
private columns: ScrollGridColumns | number,
private fetchFunction: GridEntitiesFetchFunction<T, F>,
private filter: F) {
super();
@ -58,9 +59,9 @@ export class ScrollGridDatasource<T, F> extends DataSource<(T | GridCellType)[]>
this._viewport = (collectionViewer as any)._viewport;
this._init();
if (this.columns.breakpoints) {
if (typeof this.columns === 'object' && this.columns.breakpoints) {
const breakpoints = Object.keys(this.columns.breakpoints);
this._subscription.add(this.breakpointObserver.observe(breakpoints).subscribe(
this._subscription.add(this.breakpointObserver.observe(breakpoints.map(breakpoint => resolveBreakpoint(breakpoint))).subscribe(
() => {
this._columnsChanged(this._detectColumns());
}
@ -119,16 +120,21 @@ export class ScrollGridDatasource<T, F> extends DataSource<(T | GridCellType)[]>
}
private _detectColumns(): number {
let columns = this.columns.columns;
if (this.columns.breakpoints) {
for (const breakpont of Object.keys(this.columns.breakpoints)) {
if (this.breakpointObserver.isMatched(breakpont)) {
columns = this.columns.breakpoints[breakpont];
break;
if (typeof this.columns !== 'object') {
return this.columns;
} else {
let columns = this.columns.columns;
if (this.columns.breakpoints) {
for (const breakpoint of Object.keys(this.columns.breakpoints)) {
const breakpointValue = resolveBreakpoint(breakpoint);
if (this.breakpointObserver.isMatched(breakpointValue)) {
columns = this.columns.breakpoints[breakpoint];
break;
}
}
}
return columns;
}
return columns;
}
private _init() {

7
ui-ngx/src/app/shared/models/constants.ts

@ -71,6 +71,13 @@ export const MediaBreakpoints = {
'md-lg': 'screen and (min-width: 960px) and (max-width: 1819px)'
};
export const resolveBreakpoint = (breakpoint: string): string => {
if (MediaBreakpoints[breakpoint]) {
return MediaBreakpoints[breakpoint];
}
return breakpoint;
};
export const helpBaseUrl = 'https://thingsboard.io';
export const HelpLinks = {

4
ui-ngx/src/app/shared/models/widget.models.ts

@ -409,8 +409,8 @@ export interface Datasource {
export const datasourcesHasAggregation = (datasources?: Array<Datasource>): boolean => {
if (datasources) {
const foundDatasource = datasources.find(datasource => {
const found = datasource.dataKeys && datasource.dataKeys.find(key => key.type === DataKeyType.timeseries &&
key.aggregationType && key.aggregationType !== AggregationType.NONE);
const found = datasource.dataKeys && datasource.dataKeys.find(key => key?.type === DataKeyType.timeseries &&
key?.aggregationType && key.aggregationType !== AggregationType.NONE);
return !!found;
});
if (foundDatasource) {

21
ui-ngx/src/assets/locale/locale.constant-en_US.json

@ -5338,6 +5338,27 @@
"Ok": "Ok"
}
},
"doughnut": {
"total": "Total",
"layout": "Layout",
"layout-default": "Default",
"layout-with-total": "With total",
"auto-scale": "Auto scale",
"central-total-value": "Central total value",
"legend-position-top": "Top",
"legend-position-bottom": "Bottom",
"legend-position-left": "Left",
"legend-position-right": "Right",
"legend-label": "Label",
"legend-value": "Value",
"tooltip": "Tooltip",
"tooltip-value": "Value",
"tooltip-value-type-absolute": "Absolute",
"tooltip-value-type-percentage": "Percentage",
"tooltip-background-color": "Background color",
"tooltip-background-blur": "Background blur",
"doughnut-card-style": "Doughnut card style"
},
"entities-hierarchy": {
"hierarchy-data-settings": "Hierarchy data settings",
"relations-query-function": "Node relations query function",

29
ui-ngx/src/assets/widget/doughnut/default-layout.svg

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 45 KiB

29
ui-ngx/src/assets/widget/doughnut/horizontal-default-layout.svg

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 46 KiB

28
ui-ngx/src/assets/widget/doughnut/horizontal-with-total-layout.svg

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 46 KiB

28
ui-ngx/src/assets/widget/doughnut/with-total-layout.svg

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 45 KiB

20
ui-ngx/yarn.lock

@ -5360,6 +5360,14 @@ ecc-jsbn@~0.1.1:
jsbn "~0.1.0"
safer-buffer "^2.1.0"
echarts@^5.4.3:
version "5.4.3"
resolved "https://registry.yarnpkg.com/echarts/-/echarts-5.4.3.tgz#f5522ef24419164903eedcfd2b506c6fc91fb20c"
integrity sha512-mYKxLxhzy6zyTi/FaEbJMOZU1ULGEQHaeIeuMR5L+JnJTpz+YR03mnnpBhbR4+UYJAgiXgpyTVLffPAjOTLkZA==
dependencies:
tslib "2.3.0"
zrender "5.4.4"
editorconfig@^0.15.3:
version "0.15.3"
resolved "https://registry.yarnpkg.com/editorconfig/-/editorconfig-0.15.3.tgz#bef84c4e75fb8dcb0ce5cee8efd51c15999befc5"
@ -10329,6 +10337,11 @@ tsconfig-paths@^4.1.0:
minimist "^1.2.6"
strip-bom "^3.0.0"
tslib@2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.0.tgz#803b8cdab3e12ba581a4ca41c8839bbb0dacb09e"
integrity sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==
tslib@2.5.0, tslib@^2.0.0, tslib@^2.0.3, tslib@^2.1.0, tslib@^2.2.0, tslib@^2.3.0, tslib@^2.3.1, tslib@^2.4.0, tslib@^2.4.1, tslib@^2.5.0:
version "2.5.0"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.5.0.tgz#42bfed86f5787aeb41d031866c8f402429e0fddf"
@ -11075,3 +11088,10 @@ zone.js@~0.13.0:
integrity sha512-7m3hNNyswsdoDobCkYNAy5WiUulkMd3+fWaGT9ij6iq3Zr/IwJo4RMCYPSDjT+r7tnPErmY9sZpKhWQ8S5k6XQ==
dependencies:
tslib "^2.3.0"
zrender@5.4.4:
version "5.4.4"
resolved "https://registry.yarnpkg.com/zrender/-/zrender-5.4.4.tgz#8854f1d95ecc82cf8912f5a11f86657cb8c9e261"
integrity sha512-0VxCNJ7AGOMCWeHVyTrGzUgrK4asT4ml9PEkeGirAkKNYXYzoPJCLvmyfdoOXcjTHPs10OZVMfD1Rwg16AZyYw==
dependencies:
tslib "2.3.0"

Loading…
Cancel
Save