Browse Source

Merge pull request #10562 from thingsboard/feature/time-series-comparison

Comparison support for new time series charts
pull/10579/head
Igor Kulikov 2 years ago
committed by GitHub
parent
commit
b453bc6ced
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 9
      ui-ngx/src/app/core/api/widget-api.models.ts
  2. 14
      ui-ngx/src/app/core/api/widget-subscription.ts
  3. 8
      ui-ngx/src/app/modules/home/components/widget/config/basic/basic-widget-config.module.ts
  4. 37
      ui-ngx/src/app/modules/home/components/widget/config/basic/chart/comparison-key-row.component.html
  5. 54
      ui-ngx/src/app/modules/home/components/widget/config/basic/chart/comparison-key-row.component.scss
  6. 131
      ui-ngx/src/app/modules/home/components/widget/config/basic/chart/comparison-key-row.component.ts
  7. 38
      ui-ngx/src/app/modules/home/components/widget/config/basic/chart/comparison-keys-table.component.html
  8. 53
      ui-ngx/src/app/modules/home/components/widget/config/basic/chart/comparison-keys-table.component.scss
  9. 110
      ui-ngx/src/app/modules/home/components/widget/config/basic/chart/comparison-keys-table.component.ts
  10. 87
      ui-ngx/src/app/modules/home/components/widget/config/basic/chart/time-series-chart-basic-config.component.html
  11. 48
      ui-ngx/src/app/modules/home/components/widget/config/basic/chart/time-series-chart-basic-config.component.ts
  12. 6
      ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-keys-panel.component.html
  13. 4
      ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-keys-panel.component.ts
  14. 2
      ui-ngx/src/app/modules/home/components/widget/dynamic-widget.component.ts
  15. 219
      ui-ngx/src/app/modules/home/components/widget/lib/chart/echarts-widget.models.ts
  16. 203
      ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.models.ts
  17. 146
      ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.ts
  18. 12
      ui-ngx/src/app/modules/home/components/widget/lib/flot-widget.models.ts
  19. 29
      ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-key-settings.component.html
  20. 24
      ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-key-settings.component.ts
  21. 40
      ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-widget-settings.component.html
  22. 20
      ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-widget-settings.component.ts
  23. 28
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-axis-settings-button.component.html
  24. 108
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-axis-settings-button.component.ts
  25. 16
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-axis-settings-panel.component.html
  26. 8
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-axis-settings-panel.component.scss
  27. 37
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-axis-settings-panel.component.ts
  28. 14
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-row.component.ts
  29. 4
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/data-key-input.component.html
  30. 7
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/data-key-input.component.scss
  31. 4
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/data-key-input.component.ts
  32. 13
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget-settings-common.module.ts
  33. 2
      ui-ngx/src/app/modules/home/models/widget-component.models.ts
  34. 16
      ui-ngx/src/app/shared/models/widget.models.ts
  35. 10
      ui-ngx/src/assets/locale/locale.constant-en_US.json
  36. 12
      ui-ngx/src/form.scss

9
ui-ngx/src/app/core/api/widget-api.models.ts

@ -30,7 +30,12 @@ import {
import { TimeService } from '../services/time.service'; import { TimeService } from '../services/time.service';
import { DeviceService } from '../http/device.service'; import { DeviceService } from '../http/device.service';
import { UtilsService } from '@core/services/utils.service'; import { UtilsService } from '@core/services/utils.service';
import { SubscriptionTimewindow, Timewindow, WidgetTimewindow } from '@shared/models/time/time.models'; import {
ComparisonDuration,
SubscriptionTimewindow,
Timewindow,
WidgetTimewindow
} from '@shared/models/time/time.models';
import { EntityType } from '@shared/models/entity-type.models'; import { EntityType } from '@shared/models/entity-type.models';
import { HttpErrorResponse } from '@angular/common/http'; import { HttpErrorResponse } from '@angular/common/http';
import { RafService } from '@core/services/raf.service'; import { RafService } from '@core/services/raf.service';
@ -265,7 +270,7 @@ export interface WidgetSubscriptionOptions {
onTimewindowChangeFunction?: (timewindow: Timewindow) => Timewindow; onTimewindowChangeFunction?: (timewindow: Timewindow) => Timewindow;
legendConfig?: LegendConfig; legendConfig?: LegendConfig;
comparisonEnabled?: boolean; comparisonEnabled?: boolean;
timeForComparison?: moment_.unitOfTime.DurationConstructor; timeForComparison?: ComparisonDuration;
comparisonCustomIntervalValue?: number; comparisonCustomIntervalValue?: number;
decimals?: number; decimals?: number;
units?: string; units?: string;

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

@ -23,13 +23,13 @@ import {
WidgetSubscriptionOptions WidgetSubscriptionOptions
} from '@core/api/widget-api.models'; } from '@core/api/widget-api.models';
import { import {
DataKey, DataKey, DataKeySettingsWithComparison,
DataSet, DataSet,
DataSetHolder, DataSetHolder,
Datasource, Datasource,
DatasourceData, DatasourceData,
datasourcesHasAggregation, datasourcesHasAggregation,
DatasourceType, DatasourceType, isDataKeySettingsWithComparison,
LegendConfig, LegendConfig,
LegendData, LegendData,
LegendKey, LegendKey,
@ -513,7 +513,7 @@ export class WidgetSubscription implements IWidgetSubscription {
this.configuredDatasources.forEach((datasource, datasourceIndex) => { this.configuredDatasources.forEach((datasource, datasourceIndex) => {
const additionalDataKeys: DataKey[] = []; const additionalDataKeys: DataKey[] = [];
datasource.dataKeys.forEach((dataKey, dataKeyIndex) => { datasource.dataKeys.forEach((dataKey, dataKeyIndex) => {
if (dataKey.settings.comparisonSettings && dataKey.settings.comparisonSettings.showValuesForComparison) { if (isDataKeySettingsWithComparison(dataKey.settings) && dataKey.settings.comparisonSettings.showValuesForComparison) {
const additionalDataKey = deepClone(dataKey); const additionalDataKey = deepClone(dataKey);
additionalDataKey.isAdditional = true; additionalDataKey.isAdditional = true;
additionalDataKey.origDataKeyIndex = dataKeyIndex; additionalDataKey.origDataKeyIndex = dataKeyIndex;
@ -1468,11 +1468,12 @@ export class WidgetSubscription implements IWidgetSubscription {
if (datasource.isAdditional) { if (datasource.isAdditional) {
const origDatasource = this.datasourcePages[datasource.origDatasourceIndex].data[dIndex]; const origDatasource = this.datasourcePages[datasource.origDatasourceIndex].data[dIndex];
datasource.dataKeys.forEach((dataKey) => { datasource.dataKeys.forEach((dataKey) => {
if (dataKey.settings.comparisonSettings.color) { const settings: DataKeySettingsWithComparison = dataKey.settings;
if (settings.comparisonSettings.color) {
dataKey.color = dataKey.settings.comparisonSettings.color; dataKey.color = dataKey.settings.comparisonSettings.color;
} }
const origDataKey = origDatasource.dataKeys[dataKey.origDataKeyIndex]; const origDataKey = origDatasource.dataKeys[dataKey.origDataKeyIndex];
origDataKey.settings.comparisonSettings.color = dataKey.color; (origDataKey.settings as DataKeySettingsWithComparison).comparisonSettings.color = dataKey.color;
}); });
} }
}); });
@ -1523,7 +1524,8 @@ export class WidgetSubscription implements IWidgetSubscription {
const formattedData = flatFormattedData(formattedDataArray); const formattedData = flatFormattedData(formattedDataArray);
datasource.dataKeys.forEach((dataKey) => { datasource.dataKeys.forEach((dataKey) => {
if (this.comparisonEnabled && dataKey.isAdditional && dataKey.settings.comparisonSettings.comparisonValuesLabel) { if (this.comparisonEnabled && dataKey.isAdditional && isDataKeySettingsWithComparison(dataKey.settings) &&
dataKey.settings.comparisonSettings.comparisonValuesLabel) {
dataKey.label = createLabelFromPattern(dataKey.settings.comparisonSettings.comparisonValuesLabel, formattedData); dataKey.label = createLabelFromPattern(dataKey.settings.comparisonSettings.comparisonValuesLabel, formattedData);
} else { } else {
if (this.comparisonEnabled && dataKey.isAdditional) { if (this.comparisonEnabled && dataKey.isAdditional) {

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

@ -110,6 +110,10 @@ import {
import { import {
TimeSeriesChartBasicConfigComponent TimeSeriesChartBasicConfigComponent
} from '@home/components/widget/config/basic/chart/time-series-chart-basic-config.component'; } from '@home/components/widget/config/basic/chart/time-series-chart-basic-config.component';
import { ComparisonKeyRowComponent } from '@home/components/widget/config/basic/chart/comparison-key-row.component';
import {
ComparisonKeysTableComponent
} from '@home/components/widget/config/basic/chart/comparison-keys-table.component';
@NgModule({ @NgModule({
declarations: [ declarations: [
@ -145,7 +149,9 @@ import {
PowerButtonBasicConfigComponent, PowerButtonBasicConfigComponent,
SliderBasicConfigComponent, SliderBasicConfigComponent,
ToggleButtonBasicConfigComponent, ToggleButtonBasicConfigComponent,
TimeSeriesChartBasicConfigComponent TimeSeriesChartBasicConfigComponent,
ComparisonKeyRowComponent,
ComparisonKeysTableComponent
], ],
imports: [ imports: [
CommonModule, CommonModule,

37
ui-ngx/src/app/modules/home/components/widget/config/basic/chart/comparison-key-row.component.html

@ -0,0 +1,37 @@
<!--
Copyright © 2016-2024 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<div [formGroup]="keyRowFormGroup" class="tb-form-table-row tb-comparison-key-row">
<mat-checkbox class="tb-show-field" formControlName="showValuesForComparison"></mat-checkbox>
<tb-data-key-input
[editable]="false"
[removable]="false"
[datasourceType]="datasourceType"
[formControl]="keyFormControl">
</tb-data-key-input>
<div class="tb-label-field">
<mat-form-field class="tb-inline-field" appearance="outline" subscriptSizing="dynamic">
<input matInput formControlName="comparisonValuesLabel" placeholder="{{ 'widgets.time-series-chart.comparison.comparison-values-label-auto' | translate }}">
</mat-form-field>
</div>
<div class="tb-color-field">
<tb-color-input asBoxInput
colorClearButton
formControlName="color">
</tb-color-input>
</div>
</div>

54
ui-ngx/src/app/modules/home/components/widget/config/basic/chart/comparison-key-row.component.scss

@ -0,0 +1,54 @@
/**
* Copyright © 2016-2024 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@import '../../../../../../../../scss/constants';
.tb-comparison-key-row {
.tb-show-field {
width: 40px;
min-width: 40px;
}
.tb-data-key-input {
flex: 1;
@media #{$mat-gt-xs} {
min-width: 100px;
flex: 1 1 40%;
}
}
.tb-label-field, .tb-color-field {
display: flex;
flex-direction: row;
place-content: center;
align-items: center;
.tb-inline-field {
flex: 1;
}
}
.tb-label-field {
flex: 1;
@media #{$mat-gt-xs} {
min-width: 150px;
flex: 1 1 60%;
}
}
.tb-color-field {
width: 40px;
min-width: 40px;
}
}

131
ui-ngx/src/app/modules/home/components/widget/config/basic/chart/comparison-key-row.component.ts

@ -0,0 +1,131 @@
///
/// Copyright © 2016-2024 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { ChangeDetectorRef, Component, forwardRef, Input, OnInit, ViewEncapsulation } from '@angular/core';
import {
ControlValueAccessor,
NG_VALUE_ACCESSOR,
UntypedFormBuilder,
UntypedFormControl,
UntypedFormGroup
} from '@angular/forms';
import {
DataKey,
DataKeyComparisonSettings,
DataKeySettingsWithComparison,
DatasourceType
} from '@shared/models/widget.models';
import { deepClone } from '@core/utils';
@Component({
selector: 'tb-comparison-key-row',
templateUrl: './comparison-key-row.component.html',
styleUrls: ['./comparison-key-row.component.scss', '../../data-keys.component.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => ComparisonKeyRowComponent),
multi: true
}
],
encapsulation: ViewEncapsulation.None
})
export class ComparisonKeyRowComponent implements ControlValueAccessor, OnInit {
@Input()
disabled: boolean;
@Input()
datasourceType: DatasourceType;
keyFormControl: UntypedFormControl;
keyRowFormGroup: UntypedFormGroup;
modelValue: DataKey;
private propagateChange = (_val: any) => {};
constructor(private fb: UntypedFormBuilder,
private cd: ChangeDetectorRef) {
}
ngOnInit() {
this.keyFormControl = this.fb.control(null, []);
this.keyRowFormGroup = this.fb.group({
showValuesForComparison: [null, []],
comparisonValuesLabel: [null, []],
color: [null, []]
});
this.keyRowFormGroup.valueChanges.subscribe(
() => this.updateModel()
);
this.keyRowFormGroup.get('showValuesForComparison').valueChanges.subscribe(() => this.updateValidators());
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(_fn: any): void {
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
if (isDisabled) {
this.keyFormControl.disable({emitEvent: false});
this.keyRowFormGroup.disable({emitEvent: false});
} else {
this.keyFormControl.enable({emitEvent: false});
this.keyRowFormGroup.enable({emitEvent: false});
this.updateValidators();
}
}
writeValue(value: DataKey): void {
this.modelValue = value;
const comparisonSettings = (value?.settings as DataKeySettingsWithComparison)?.comparisonSettings;
this.keyRowFormGroup.patchValue(
comparisonSettings, {emitEvent: false}
);
this.keyFormControl.patchValue(deepClone(this.modelValue), {emitEvent: false});
this.updateValidators();
this.cd.markForCheck();
}
private updateValidators() {
const showValuesForComparison: boolean = this.keyRowFormGroup.get('showValuesForComparison').value;
if (showValuesForComparison) {
this.keyFormControl.enable({emitEvent: false});
this.keyRowFormGroup.get('comparisonValuesLabel').enable({emitEvent: false});
this.keyRowFormGroup.get('color').enable({emitEvent: false});
} else {
this.keyFormControl.disable({emitEvent: false});
this.keyRowFormGroup.get('comparisonValuesLabel').disable({emitEvent: false});
this.keyRowFormGroup.get('color').disable({emitEvent: false});
}
}
private updateModel() {
const comparisonSettings: DataKeyComparisonSettings = this.keyRowFormGroup.value;
if (!this.modelValue.settings) {
this.modelValue.settings = {};
}
this.modelValue.settings.comparisonSettings = comparisonSettings;
this.propagateChange(this.modelValue);
}
}

38
ui-ngx/src/app/modules/home/components/widget/config/basic/chart/comparison-keys-table.component.html

@ -0,0 +1,38 @@
<!--
Copyright © 2016-2024 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<div class="tb-comparison-keys-table tb-form-table">
<div class="tb-form-table-header">
<div class="tb-form-table-header-cell tb-show-header" translate>widgets.time-series-chart.comparison.show</div>
<div class="tb-form-table-header-cell tb-key-header" translate>datakey.key</div>
<div class="tb-form-table-header-cell tb-label-header" translate>datakey.label</div>
<div class="tb-form-table-header-cell tb-color-header" translate>datakey.color</div>
</div>
<div *ngIf="keysFormArray().controls.length; else noKeys" class="tb-form-table-body">
<div *ngFor="let keyControl of keysFormArray().controls; trackBy: trackByKey; let $index = index;">
<tb-comparison-key-row
fxFlex
[datasourceType]="datasourceType"
[formControl]="keyControl">
</tb-comparison-key-row>
</div>
</div>
</div>
<ng-template #noKeys>
<span fxLayoutAlign="center center"
class="tb-prompt">{{ 'widgets.chart.no-series' | translate }}</span>
</ng-template>

53
ui-ngx/src/app/modules/home/components/widget/config/basic/chart/comparison-keys-table.component.scss

@ -0,0 +1,53 @@
/**
* Copyright © 2016-2024 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@import '../../../../../../../../scss/constants';
.tb-comparison-keys-table {
.tb-form-table-header-cell {
&.tb-show-header {
width: 40px;
min-width: 40px;
}
&.tb-key-header {
flex: 1;
@media #{$mat-gt-xs} {
min-width: 100px;
flex: 1 1 40%;
}
}
&.tb-label-header {
flex: 1;
@media #{$mat-gt-xs} {
min-width: 150px;
flex: 1 1 60%;
}
}
&.tb-color-header {
width: 40px;
min-width: 40px;
}
}
.tb-form-table-body {
tb-comparison-key-row {
overflow: hidden;
}
}
}

110
ui-ngx/src/app/modules/home/components/widget/config/basic/chart/comparison-keys-table.component.ts

@ -0,0 +1,110 @@
///
/// Copyright © 2016-2024 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Component, forwardRef, Input, OnInit, ViewEncapsulation } from '@angular/core';
import {
AbstractControl,
ControlValueAccessor,
NG_VALUE_ACCESSOR,
UntypedFormArray,
UntypedFormBuilder,
UntypedFormGroup
} from '@angular/forms';
import { DataKey, DatasourceType } from '@shared/models/widget.models';
@Component({
selector: 'tb-comparison-keys-table',
templateUrl: './comparison-keys-table.component.html',
styleUrls: ['./comparison-keys-table.component.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => ComparisonKeysTableComponent),
multi: true
}
],
encapsulation: ViewEncapsulation.None
})
export class ComparisonKeysTableComponent implements ControlValueAccessor, OnInit {
@Input()
disabled: boolean;
@Input()
datasourceType: DatasourceType;
keysListFormGroup: UntypedFormGroup;
get noKeys(): boolean {
const keys: DataKey[] = this.keysListFormGroup.get('keys').value;
return keys.length === 0;
}
private propagateChange = (_val: any) => {};
constructor(private fb: UntypedFormBuilder) {
}
ngOnInit() {
this.keysListFormGroup = this.fb.group({
keys: [this.fb.array([]), []]
});
this.keysListFormGroup.valueChanges.subscribe(
() => {
const keys: DataKey[] = this.keysListFormGroup.get('keys').value;
this.propagateChange(keys);
}
);
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(_fn: any): void {
}
setDisabledState(isDisabled: boolean): void {
if (isDisabled) {
this.keysListFormGroup.disable({emitEvent: false});
} else {
this.keysListFormGroup.enable({emitEvent: false});
}
}
writeValue(value: DataKey[] | undefined): void {
this.keysListFormGroup.setControl('keys', this.prepareKeysFormArray(value), {emitEvent: false});
}
keysFormArray(): UntypedFormArray {
return this.keysListFormGroup.get('keys') as UntypedFormArray;
}
trackByKey(_index: number, keyControl: AbstractControl): any {
return keyControl;
}
private prepareKeysFormArray(keys: DataKey[] | undefined): UntypedFormArray {
const keysControls: Array<AbstractControl> = [];
if (keys) {
keys.forEach((key) => {
keysControls.push(this.fb.control(key, []));
});
}
return this.fb.array(keysControls);
}
}

87
ui-ngx/src/app/modules/home/components/widget/config/basic/chart/time-series-chart-basic-config.component.html

@ -25,22 +25,77 @@
forceSingleDatasource forceSingleDatasource
formControlName="datasources"> formControlName="datasources">
</tb-datasources> </tb-datasources>
<tb-data-keys-panel <div class="tb-form-panel">
panelTitle="{{ 'widgets.chart.series' | translate }}" <div fxLayout="row" fxLayoutAlign="space-between center">
addKeyTitle="{{ 'widgets.chart.add-series' | translate }}" <div class="tb-form-panel-title">{{ 'widgets.chart.series' | translate }}</div>
keySettingsTitle="{{ 'widgets.chart.series-settings' | translate }}" <tb-toggle-select [ngModel]="seriesMode" (ngModelChange)="seriesModeChange($event)"
removeKeyTitle="{{ 'widgets.chart.remove-series' | translate }}" [ngModelOptions]="{ standalone: true }">
noKeysText="{{ 'widgets.chart.no-series' | translate }}" <tb-toggle-option value="series">{{ 'widgets.chart.series' | translate }}</tb-toggle-option>
requiredKeysText="{{ 'widgets.chart.no-series-error' | translate }}" <tb-toggle-option value="comparison">{{ 'widgets.time-series-chart.comparison.comparison' | translate }}</tb-toggle-option>
timeSeriesChart </tb-toggle-select>
[yAxisIds]="yAxisIds" </div>
[showTimeSeriesType]="chartType === TimeSeriesChartType.default" <tb-data-keys-panel
hideSourceSelection *ngIf="seriesMode === 'series'"
[datasourceType]="datasource?.type" hidePanel
[deviceId]="datasource?.deviceId" panelTitle="{{ 'widgets.chart.series' | translate }}"
[entityAliasId]="datasource?.entityAliasId" addKeyTitle="{{ 'widgets.chart.add-series' | translate }}"
formControlName="series"> keySettingsTitle="{{ 'widgets.chart.series-settings' | translate }}"
</tb-data-keys-panel> removeKeyTitle="{{ 'widgets.chart.remove-series' | translate }}"
noKeysText="{{ 'widgets.chart.no-series' | translate }}"
requiredKeysText="{{ 'widgets.chart.no-series-error' | translate }}"
timeSeriesChart
[yAxisIds]="yAxisIds"
[showTimeSeriesType]="chartType === TimeSeriesChartType.default"
hideSourceSelection
[datasourceType]="datasource?.type"
[deviceId]="datasource?.deviceId"
[entityAliasId]="datasource?.entityAliasId"
formControlName="series">
</tb-data-keys-panel>
<div *ngIf="seriesMode === 'comparison'" class="tb-form-row no-border no-padding column-xs">
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="comparisonEnabled">
{{ 'widgets.time-series-chart.comparison.comparison' | translate }}
</mat-slide-toggle>
<div fxFlex fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<mat-form-field class="flex" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="timeForComparison">
<mat-option [value]="'previousInterval'">
{{ 'widgets.chart.time-for-comparison-previous-interval' | translate }}
</mat-option>
<mat-option [value]="'days'">
{{ 'widgets.chart.time-for-comparison-days' | translate }}
</mat-option>
<mat-option [value]="'weeks'">
{{ 'widgets.chart.time-for-comparison-weeks' | translate }}
</mat-option>
<mat-option [value]="'months'">
{{ 'widgets.chart.time-for-comparison-months' | translate }}
</mat-option>
<mat-option [value]="'years'">
{{ 'widgets.chart.time-for-comparison-years' | translate }}
</mat-option>
<mat-option [value]="'customInterval'">
{{ 'widgets.chart.time-for-comparison-custom-interval' | translate }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field [fxShow]="timeSeriesChartWidgetConfigForm.get('timeForComparison').value === 'customInterval'"
appearance="outline" class="number flex-lt-md" subscriptSizing="dynamic">
<input matInput formControlName="comparisonCustomIntervalValue" type="number" min="0" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
<tb-time-series-chart-axis-settings-button
axisType="xAxis"
panelTitle="{{ 'widgets.time-series-chart.axis.comparison-x-axis-settings' | translate }}"
formControlName="comparisonXAxis">
</tb-time-series-chart-axis-settings-button>
</div>
</div>
<tb-comparison-keys-table
*ngIf="seriesMode === 'comparison'"
[datasourceType]="datasource?.type"
formControlName="series">
</tb-comparison-keys-table>
</div>
<tb-time-series-chart-states-panel <tb-time-series-chart-states-panel
*ngIf="chartType === TimeSeriesChartType.state" *ngIf="chartType === TimeSeriesChartType.state"
formControlName="states"> formControlName="states">

48
ui-ngx/src/app/modules/home/components/widget/config/basic/chart/time-series-chart-basic-config.component.ts

@ -23,6 +23,7 @@ import { WidgetConfigComponentData } from '@home/models/widget-component.models'
import { import {
DataKey, DataKey,
Datasource, Datasource,
DatasourceType,
legendPositions, legendPositions,
legendPositionTranslationMap, legendPositionTranslationMap,
WidgetConfig, WidgetConfig,
@ -89,6 +90,8 @@ export class TimeSeriesChartBasicConfigComponent extends BasicWidgetConfigCompon
chartType: TimeSeriesChartType = TimeSeriesChartType.default; chartType: TimeSeriesChartType = TimeSeriesChartType.default;
seriesMode = 'series';
constructor(protected store: Store<AppState>, constructor(protected store: Store<AppState>,
protected widgetConfigComponent: WidgetConfigComponent, protected widgetConfigComponent: WidgetConfigComponent,
private $injector: Injector, private $injector: Injector,
@ -105,6 +108,11 @@ export class TimeSeriesChartBasicConfigComponent extends BasicWidgetConfigCompon
} }
} }
seriesModeChange(seriesMode: string) {
this.seriesMode = seriesMode;
this.updateSeriesState();
}
protected configForm(): UntypedFormGroup { protected configForm(): UntypedFormGroup {
return this.timeSeriesChartWidgetConfigForm; return this.timeSeriesChartWidgetConfigForm;
} }
@ -131,6 +139,12 @@ export class TimeSeriesChartBasicConfigComponent extends BasicWidgetConfigCompon
yAxes: [settings.yAxes, []], yAxes: [settings.yAxes, []],
series: [this.getSeries(configData.config.datasources), []], series: [this.getSeries(configData.config.datasources), []],
comparisonEnabled: [settings.comparisonEnabled, []],
timeForComparison: [settings.timeForComparison, []],
comparisonCustomIntervalValue: [settings.comparisonCustomIntervalValue, [Validators.min(0)]],
comparisonXAxis: [settings.comparisonXAxis, []],
thresholds: [settings.thresholds, []], thresholds: [settings.thresholds, []],
showTitle: [configData.config.showTitle, []], showTitle: [configData.config.showTitle, []],
@ -182,6 +196,7 @@ export class TimeSeriesChartBasicConfigComponent extends BasicWidgetConfigCompon
if (this.chartType === TimeSeriesChartType.state) { if (this.chartType === TimeSeriesChartType.state) {
this.timeSeriesChartWidgetConfigForm.addControl('states', this.fb.control(settings.states, [])); this.timeSeriesChartWidgetConfigForm.addControl('states', this.fb.control(settings.states, []));
} }
this.timeSeriesChartWidgetConfigForm.get('comparisonEnabled').valueChanges.subscribe(() => this.updateSeriesState());
} }
protected prepareOutputConfig(config: any): WidgetConfigComponentData { protected prepareOutputConfig(config: any): WidgetConfigComponentData {
@ -201,6 +216,11 @@ export class TimeSeriesChartBasicConfigComponent extends BasicWidgetConfigCompon
this.widgetConfig.config.settings = this.widgetConfig.config.settings || {}; this.widgetConfig.config.settings = this.widgetConfig.config.settings || {};
this.widgetConfig.config.settings.comparisonEnabled = config.comparisonEnabled;
this.widgetConfig.config.settings.timeForComparison = config.timeForComparison;
this.widgetConfig.config.settings.comparisonCustomIntervalValue = config.comparisonCustomIntervalValue;
this.widgetConfig.config.settings.comparisonXAxis = config.comparisonXAxis;
this.widgetConfig.config.settings.thresholds = config.thresholds; this.widgetConfig.config.settings.thresholds = config.thresholds;
this.widgetConfig.config.settings.dataZoom = config.dataZoom; this.widgetConfig.config.settings.dataZoom = config.dataZoom;
@ -245,16 +265,27 @@ export class TimeSeriesChartBasicConfigComponent extends BasicWidgetConfigCompon
} }
protected validatorTriggers(): string[] { protected validatorTriggers(): string[] {
return ['showTitle', 'showIcon', 'showLegend', 'showTooltip', 'tooltipShowDate']; return ['comparisonEnabled', 'showTitle', 'showIcon', 'showLegend', 'showTooltip', 'tooltipShowDate'];
} }
protected updateValidators(emitEvent: boolean, trigger?: string) { protected updateValidators(emitEvent: boolean, trigger?: string) {
const comparisonEnabled: boolean = this.timeSeriesChartWidgetConfigForm.get('comparisonEnabled').value;
const showTitle: boolean = this.timeSeriesChartWidgetConfigForm.get('showTitle').value; const showTitle: boolean = this.timeSeriesChartWidgetConfigForm.get('showTitle').value;
const showIcon: boolean = this.timeSeriesChartWidgetConfigForm.get('showIcon').value; const showIcon: boolean = this.timeSeriesChartWidgetConfigForm.get('showIcon').value;
const showLegend: boolean = this.timeSeriesChartWidgetConfigForm.get('showLegend').value; const showLegend: boolean = this.timeSeriesChartWidgetConfigForm.get('showLegend').value;
const showTooltip: boolean = this.timeSeriesChartWidgetConfigForm.get('showTooltip').value; const showTooltip: boolean = this.timeSeriesChartWidgetConfigForm.get('showTooltip').value;
const tooltipShowDate: boolean = this.timeSeriesChartWidgetConfigForm.get('tooltipShowDate').value; const tooltipShowDate: boolean = this.timeSeriesChartWidgetConfigForm.get('tooltipShowDate').value;
if (comparisonEnabled) {
this.timeSeriesChartWidgetConfigForm.get('timeForComparison').enable();
this.timeSeriesChartWidgetConfigForm.get('comparisonCustomIntervalValue').enable();
this.timeSeriesChartWidgetConfigForm.get('comparisonXAxis').enable();
} else {
this.timeSeriesChartWidgetConfigForm.get('timeForComparison').disable();
this.timeSeriesChartWidgetConfigForm.get('comparisonCustomIntervalValue').disable();
this.timeSeriesChartWidgetConfigForm.get('comparisonXAxis').disable();
}
if (showTitle) { if (showTitle) {
this.timeSeriesChartWidgetConfigForm.get('title').enable(); this.timeSeriesChartWidgetConfigForm.get('title').enable();
this.timeSeriesChartWidgetConfigForm.get('titleFont').enable(); this.timeSeriesChartWidgetConfigForm.get('titleFont').enable();
@ -336,6 +367,19 @@ export class TimeSeriesChartBasicConfigComponent extends BasicWidgetConfigCompon
} }
} }
private updateSeriesState() {
if (this.seriesMode === 'series') {
this.timeSeriesChartWidgetConfigForm.get('series').enable({emitEvent: false});
} else {
const comparisonEnabled = this.timeSeriesChartWidgetConfigForm.get('comparisonEnabled').value;
if (comparisonEnabled) {
this.timeSeriesChartWidgetConfigForm.get('series').enable({emitEvent: false});
} else {
this.timeSeriesChartWidgetConfigForm.get('series').disable({emitEvent: false});
}
}
}
private removeYaxisId(series: DataKey[], yAxisId: TimeSeriesChartYAxisId): boolean { private removeYaxisId(series: DataKey[], yAxisId: TimeSeriesChartYAxisId): boolean {
let changed = false; let changed = false;
if (series) { if (series) {
@ -372,4 +416,6 @@ export class TimeSeriesChartBasicConfigComponent extends BasicWidgetConfigCompon
processor.update(Date.now()); processor.update(Date.now());
return processor.formatted; return processor.formatted;
} }
protected readonly DatasourceType = DatasourceType;
} }

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

@ -15,8 +15,10 @@
limitations under the License. limitations under the License.
--> -->
<div class="tb-form-panel tb-data-keys-panel"> <div class="tb-form-panel tb-data-keys-panel"
<div class="tb-form-panel-title">{{ panelTitle }}</div> [class.no-border]="hidePanel"
[class.no-padding]="hidePanel">
<div *ngIf="!hidePanel" class="tb-form-panel-title">{{ panelTitle }}</div>
<div class="tb-form-table"> <div class="tb-form-table">
<div class="tb-form-table-header no-padding-right"> <div class="tb-form-table-header no-padding-right">
<div *ngIf="hasAdditionalLatestDataKeys" class="tb-form-table-header-cell tb-source-header" translate>datakey.source</div> <div *ngIf="hasAdditionalLatestDataKeys" class="tb-form-table-header-cell tb-source-header" translate>datakey.source</div>

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

@ -96,6 +96,10 @@ export class DataKeysPanelComponent implements ControlValueAccessor, OnInit, OnC
@Input() @Input()
deviceId: string; deviceId: string;
@Input()
@coerceBoolean()
hidePanel = false;
@Input() @Input()
@coerceBoolean() @coerceBoolean()
hideDataKeyColor = false; hideDataKeyColor = false;

2
ui-ngx/src/app/modules/home/components/widget/dynamic-widget.component.ts

@ -49,6 +49,7 @@ import { TbInject } from '@shared/decorators/tb-inject';
import { MillisecondsToTimeStringPipe } from '@shared/pipe/milliseconds-to-time-string.pipe'; import { MillisecondsToTimeStringPipe } from '@shared/pipe/milliseconds-to-time-string.pipe';
import { UserSettingsService } from '@core/http/user-settings.service'; import { UserSettingsService } from '@core/http/user-settings.service';
import { ImagePipe } from '@shared/pipe/image.pipe'; import { ImagePipe } from '@shared/pipe/image.pipe';
import { UtilsService } from '@core/services/utils.service';
@Directive() @Directive()
// eslint-disable-next-line @angular-eslint/directive-class-suffix // eslint-disable-next-line @angular-eslint/directive-class-suffix
@ -86,6 +87,7 @@ export class DynamicWidgetComponent extends PageComponent implements IDynamicWid
this.ctx.customDialog = $injector.get(CustomDialogService); this.ctx.customDialog = $injector.get(CustomDialogService);
this.ctx.resourceService = $injector.get(ResourceService); this.ctx.resourceService = $injector.get(ResourceService);
this.ctx.userSettingsService = $injector.get(UserSettingsService); this.ctx.userSettingsService = $injector.get(UserSettingsService);
this.ctx.utilsService = $injector.get(UtilsService);
this.ctx.telemetryWsService = $injector.get(TelemetryWebsocketService); this.ctx.telemetryWsService = $injector.get(TelemetryWebsocketService);
this.ctx.date = $injector.get(DatePipe); this.ctx.date = $injector.get(DatePipe);
this.ctx.imagePipe = $injector.get(ImagePipe); this.ctx.imagePipe = $injector.get(ImagePipe);

219
ui-ngx/src/app/modules/home/components/widget/lib/chart/echarts-widget.models.ts

@ -115,6 +115,7 @@ export type EChartsSeriesItem = {
decimals?: number; decimals?: number;
latestData?: FormattedData; latestData?: FormattedData;
tooltipValueFormatFunction?: EChartsTooltipValueFormatFunction; tooltipValueFormatFunction?: EChartsTooltipValueFormatFunction;
comparisonItem?: boolean;
}; };
export enum EChartsShape { export enum EChartsShape {
@ -200,9 +201,9 @@ export const timeAxisBandWidthCalculator: TimeAxisBandWidthCalculator = (model)
} }
}; };
export const getXAxis = (chart: ECharts): Axis2D => { export const getAxis = (chart: ECharts, mainType: string, axisId: string): Axis2D => {
const model: GlobalModel = (chart as any).getModel(); const model: GlobalModel = (chart as any).getModel();
const models = model.queryComponents({mainType: 'xAxis'}); const models = model.queryComponents({mainType, id: axisId});
if (models?.length) { if (models?.length) {
const axisModel = models[0] as AxisModel; const axisModel = models[0] as AxisModel;
return axisModel.axis; return axisModel.axis;
@ -210,27 +211,20 @@ export const getXAxis = (chart: ECharts): Axis2D => {
return null; return null;
}; };
export const getYAxis = (chart: ECharts, axisId: string): Axis2D => { export const calculateAxisSize = (chart: ECharts, mainType: string, axisId: string): number => {
const model: GlobalModel = (chart as any).getModel(); const axis = getAxis(chart, mainType, axisId);
const models = model.queryComponents({mainType: 'yAxis', id: axisId}); return _calculateAxisSize(axis);
if (models?.length) {
const axisModel = models[0] as AxisModel;
return axisModel.axis;
}
return null;
};
export const calculateYAxisWidth = (chart: ECharts, axisId: string): number => {
const axis = getYAxis(chart, axisId);
return calculateAxisSize(axis);
}; };
export const calculateXAxisHeight = (chart: ECharts): number => { export const measureAxisNameSize = (chart: ECharts, mainType: string, axisId: string, name: string): number => {
const axis = getXAxis(chart); const axis = getAxis(chart, mainType, axisId);
return calculateAxisSize(axis); if (axis) {
return axis.model.getModel('nameTextStyle').getTextRect(name).height;
}
return 0;
}; };
const calculateAxisSize = (axis: Axis2D): number => { const _calculateAxisSize = (axis: Axis2D): number => {
let size = 0; let size = 0;
if (axis && axis.model.option.show) { if (axis && axis.model.option.show) {
const labelUnionRect = estimateLabelUnionRect(axis); const labelUnionRect = estimateLabelUnionRect(axis);
@ -247,22 +241,6 @@ const calculateAxisSize = (axis: Axis2D): number => {
return size; return size;
}; };
export const measureYAxisNameWidth = (chart: ECharts, axisId: string, name: string): number => {
const axis = getYAxis(chart, axisId);
if (axis) {
return axis.model.getModel('nameTextStyle').getTextRect(name).height;
}
return 0;
};
export const measureXAxisNameHeight = (chart: ECharts, name: string): number => {
const axis = getXAxis(chart);
if (axis) {
return axis.model.getModel('nameTextStyle').getTextRect(name).height;
}
return 0;
};
const measureSymbolOffset = (symbol: string, symbolSize: any): number => { const measureSymbolOffset = (symbol: string, symbolSize: any): number => {
if (isNumber(symbolSize)) { if (isNumber(symbolSize)) {
if (symbol) { if (symbol) {
@ -280,7 +258,7 @@ const measureSymbolOffset = (symbol: string, symbolSize: any): number => {
export const measureThresholdOffset = (chart: ECharts, axisId: string, thresholdId: string, value: any): [number, number] => { export const measureThresholdOffset = (chart: ECharts, axisId: string, thresholdId: string, value: any): [number, number] => {
const offset: [number, number] = [0,0]; const offset: [number, number] = [0,0];
const axis = getYAxis(chart, axisId); const axis = getAxis(chart, 'yAxis', axisId);
if (axis && !axis.scale.isBlank()) { if (axis && !axis.scale.isBlank()) {
const extent = axis.scale.getExtent(); const extent = axis.scale.getExtent();
const model: GlobalModel = (chart as any).getModel(); const model: GlobalModel = (chart as any).getModel();
@ -352,7 +330,7 @@ export const measureThresholdOffset = (chart: ECharts, axisId: string, threshold
}; };
export const getAxisExtent = (chart: ECharts, axisId: string): [number, number] => { export const getAxisExtent = (chart: ECharts, axisId: string): [number, number] => {
const axis = getYAxis(chart, axisId); const axis = getAxis(chart, 'yAxis', axisId);
if (axis) { if (axis) {
return axis.scale.getExtent(); return axis.scale.getExtent();
} }
@ -503,43 +481,72 @@ export const echartsTooltipFormatter = (renderer: Renderer2,
focusedSeriesIndex: number, focusedSeriesIndex: number,
series?: EChartsSeriesItem[], series?: EChartsSeriesItem[],
interval?: Interval): null | HTMLElement => { interval?: Interval): null | HTMLElement => {
if (!params || Array.isArray(params) && !params[0]) {
return null; const tooltipParams = mapTooltipParams(params, series, focusedSeriesIndex);
} if (!tooltipParams.items.length && !tooltipParams.comparisonItems.length) {
const firstParam = Array.isArray(params) ? params[0] : params;
if (!firstParam.value) {
return null; return null;
} }
const tooltipElement: HTMLElement = renderer.createElement('div'); const tooltipElement: HTMLElement = renderer.createElement('div');
renderer.setStyle(tooltipElement, 'display', 'flex'); renderer.setStyle(tooltipElement, 'display', 'flex');
renderer.setStyle(tooltipElement, 'flex-direction', 'column'); renderer.setStyle(tooltipElement, 'flex-direction', 'column');
renderer.setStyle(tooltipElement, 'align-items', 'flex-start'); renderer.setStyle(tooltipElement, 'align-items', 'flex-start');
renderer.setStyle(tooltipElement, 'gap', '4px'); renderer.setStyle(tooltipElement, 'gap', '16px');
if (settings.tooltipShowDate) {
const dateElement: HTMLElement = renderer.createElement('div'); buildItemsTooltip(tooltipElement, tooltipParams.items, renderer, tooltipDateFormat, settings, valueFormatFunction, interval);
let dateText: string; buildItemsTooltip(tooltipElement, tooltipParams.comparisonItems, renderer, tooltipDateFormat, settings, valueFormatFunction, interval);
const startTs = firstParam.value[2];
const endTs = firstParam.value[3]; return tooltipElement;
if (settings.tooltipDateInterval && startTs && endTs && (endTs - 1) > startTs) { };
const startDateText = tooltipDateFormat.update(startTs, interval);
const endDateText = tooltipDateFormat.update(endTs - 1, interval); interface TooltipItem {
if (startDateText === endDateText) { param: CallbackDataParams;
dateText = startDateText; dataItem: EChartsSeriesItem;
} else { }
dateText = startDateText + ' - ' + endDateText;
} interface TooltipParams {
} else { items: TooltipItem[];
const ts = firstParam.value[0]; comparisonItems: TooltipItem[];
dateText = tooltipDateFormat.update(ts, interval); }
const buildItemsTooltip = (tooltipElement: HTMLElement,
items: TooltipItem[],
renderer: Renderer2,
tooltipDateFormat: DateFormatProcessor,
settings: EChartsTooltipWidgetSettings,
valueFormatFunction: EChartsTooltipValueFormatFunction,
interval?: Interval) => {
if (items.length) {
const tooltipItemsElement: HTMLElement = renderer.createElement('div');
renderer.setStyle(tooltipItemsElement, 'display', 'flex');
renderer.setStyle(tooltipItemsElement, 'flex-direction', 'column');
renderer.setStyle(tooltipItemsElement, 'align-items', 'flex-start');
renderer.setStyle(tooltipItemsElement, 'gap', '4px');
renderer.appendChild(tooltipElement, tooltipItemsElement);
if (settings.tooltipShowDate) {
renderer.appendChild(tooltipItemsElement,
constructEchartsTooltipDateElement(renderer, tooltipDateFormat, settings, items[0].param, interval));
}
for (const item of items) {
renderer.appendChild(tooltipItemsElement,
constructEchartsTooltipSeriesElement(renderer, settings, item, valueFormatFunction));
} }
renderer.appendChild(dateElement, renderer.createText(dateText)); }
renderer.setStyle(dateElement, 'font-family', settings.tooltipDateFont.family); };
renderer.setStyle(dateElement, 'font-size', settings.tooltipDateFont.size + settings.tooltipDateFont.sizeUnit);
renderer.setStyle(dateElement, 'font-style', settings.tooltipDateFont.style); const mapTooltipParams = (params: CallbackDataParams[] | CallbackDataParams,
renderer.setStyle(dateElement, 'font-weight', settings.tooltipDateFont.weight); series?: EChartsSeriesItem[],
renderer.setStyle(dateElement, 'line-height', settings.tooltipDateFont.lineHeight); focusedSeriesIndex?: number): TooltipParams => {
renderer.setStyle(dateElement, 'color', settings.tooltipDateColor); const result: TooltipParams = {
renderer.appendChild(tooltipElement, dateElement); items: [],
comparisonItems: []
};
if (!params || Array.isArray(params) && !params[0]) {
return result;
}
const firstParam = Array.isArray(params) ? params[0] : params;
if (!firstParam.value) {
return result;
} }
let seriesParams: CallbackDataParams = null; let seriesParams: CallbackDataParams = null;
if (Array.isArray(params) && focusedSeriesIndex > -1) { if (Array.isArray(params) && focusedSeriesIndex > -1) {
@ -548,22 +555,63 @@ export const echartsTooltipFormatter = (renderer: Renderer2,
seriesParams = params; seriesParams = params;
} }
if (seriesParams) { if (seriesParams) {
renderer.appendChild(tooltipElement, appendTooltipItem(result, seriesParams, series);
constructEchartsTooltipSeriesElement(renderer, settings, seriesParams, valueFormatFunction, series));
} else if (Array.isArray(params)) { } else if (Array.isArray(params)) {
for (seriesParams of params) { for (seriesParams of params) {
renderer.appendChild(tooltipElement, appendTooltipItem(result, seriesParams, series);
constructEchartsTooltipSeriesElement(renderer, settings, seriesParams, valueFormatFunction, series));
} }
} }
return tooltipElement; return result;
};
const appendTooltipItem = (tooltipParams: TooltipParams, seriesParams: CallbackDataParams, series?: EChartsSeriesItem[]) => {
const dataItem = series?.find(s => s.id === seriesParams.seriesId);
const tooltipItem: TooltipItem = {
param: seriesParams,
dataItem
};
if (dataItem?.comparisonItem) {
tooltipParams.comparisonItems.push(tooltipItem);
} else {
tooltipParams.items.push(tooltipItem);
}
};
const constructEchartsTooltipDateElement = (renderer: Renderer2,
tooltipDateFormat: DateFormatProcessor,
settings: EChartsTooltipWidgetSettings,
param: CallbackDataParams,
interval?: Interval): HTMLElement => {
const dateElement: HTMLElement = renderer.createElement('div');
let dateText: string;
const startTs = param.value[2];
const endTs = param.value[3];
if (settings.tooltipDateInterval && startTs && endTs && (endTs - 1) > startTs) {
const startDateText = tooltipDateFormat.update(startTs, interval);
const endDateText = tooltipDateFormat.update(endTs - 1, interval);
if (startDateText === endDateText) {
dateText = startDateText;
} else {
dateText = startDateText + ' - ' + endDateText;
}
} else {
const ts = param.value[0];
dateText = tooltipDateFormat.update(ts, interval);
}
renderer.appendChild(dateElement, renderer.createText(dateText));
renderer.setStyle(dateElement, 'font-family', settings.tooltipDateFont.family);
renderer.setStyle(dateElement, 'font-size', settings.tooltipDateFont.size + settings.tooltipDateFont.sizeUnit);
renderer.setStyle(dateElement, 'font-style', settings.tooltipDateFont.style);
renderer.setStyle(dateElement, 'font-weight', settings.tooltipDateFont.weight);
renderer.setStyle(dateElement, 'line-height', settings.tooltipDateFont.lineHeight);
renderer.setStyle(dateElement, 'color', settings.tooltipDateColor);
return dateElement;
}; };
const constructEchartsTooltipSeriesElement = (renderer: Renderer2, const constructEchartsTooltipSeriesElement = (renderer: Renderer2,
settings: EChartsTooltipWidgetSettings, settings: EChartsTooltipWidgetSettings,
seriesParams: CallbackDataParams, item: TooltipItem,
valueFormatFunction: EChartsTooltipValueFormatFunction, valueFormatFunction: EChartsTooltipValueFormatFunction): HTMLElement => {
series?: EChartsSeriesItem[]): HTMLElement => {
const labelValueElement: HTMLElement = renderer.createElement('div'); const labelValueElement: HTMLElement = renderer.createElement('div');
renderer.setStyle(labelValueElement, 'display', 'flex'); renderer.setStyle(labelValueElement, 'display', 'flex');
renderer.setStyle(labelValueElement, 'flex-direction', 'row'); renderer.setStyle(labelValueElement, 'flex-direction', 'row');
@ -579,10 +627,10 @@ const constructEchartsTooltipSeriesElement = (renderer: Renderer2,
renderer.setStyle(circleElement, 'width', '8px'); renderer.setStyle(circleElement, 'width', '8px');
renderer.setStyle(circleElement, 'height', '8px'); renderer.setStyle(circleElement, 'height', '8px');
renderer.setStyle(circleElement, 'border-radius', '50%'); renderer.setStyle(circleElement, 'border-radius', '50%');
renderer.setStyle(circleElement, 'background', seriesParams.color); renderer.setStyle(circleElement, 'background', item.param.color);
renderer.appendChild(labelElement, circleElement); renderer.appendChild(labelElement, circleElement);
const labelTextElement: HTMLElement = renderer.createElement('div'); const labelTextElement: HTMLElement = renderer.createElement('div');
renderer.appendChild(labelTextElement, renderer.createText(seriesParams.seriesName)); renderer.appendChild(labelTextElement, renderer.createText(item.param.seriesName));
renderer.setStyle(labelTextElement, 'font-family', 'Roboto'); renderer.setStyle(labelTextElement, 'font-family', 'Roboto');
renderer.setStyle(labelTextElement, 'font-size', '12px'); renderer.setStyle(labelTextElement, 'font-size', '12px');
renderer.setStyle(labelTextElement, 'font-style', 'normal'); renderer.setStyle(labelTextElement, 'font-style', 'normal');
@ -596,21 +644,18 @@ const constructEchartsTooltipSeriesElement = (renderer: Renderer2,
let latestData: FormattedData; let latestData: FormattedData;
let units = ''; let units = '';
let decimals = 0; let decimals = 0;
if (series) { if (item.dataItem) {
const item = series.find(s => s.id === seriesParams.seriesId); if (item.dataItem.tooltipValueFormatFunction) {
if (item) { formatFunction = item.dataItem.tooltipValueFormatFunction;
if (item.tooltipValueFormatFunction) {
formatFunction = item.tooltipValueFormatFunction;
}
latestData = item.latestData;
units = item.units;
decimals = item.decimals;
} }
latestData = item.dataItem.latestData;
units = item.dataItem.units;
decimals = item.dataItem.decimals;
} }
if (!latestData) { if (!latestData) {
latestData = {} as FormattedData; latestData = {} as FormattedData;
} }
const value = formatFunction(seriesParams.value[1], latestData, units, decimals); const value = formatFunction(item.param.value[1], latestData, units, decimals);
renderer.appendChild(valueElement, renderer.createText(value)); renderer.appendChild(valueElement, renderer.createText(value));
renderer.setStyle(valueElement, 'flex', '1'); renderer.setStyle(valueElement, 'flex', '1');
renderer.setStyle(valueElement, 'text-align', 'end'); renderer.setStyle(valueElement, 'text-align', 'end');

203
ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.models.ts

@ -62,13 +62,16 @@ import {
BarVisualSettings, BarVisualSettings,
renderTimeSeriesBar renderTimeSeriesBar
} from '@home/components/widget/lib/chart/time-series-chart-bar.models'; } from '@home/components/widget/lib/chart/time-series-chart-bar.models';
import { DataKey } from '@shared/models/widget.models'; import { DataKey, DataKeySettingsWithComparison, WidgetComparisonSettings } from '@shared/models/widget.models';
import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
import { TbColorScheme } from '@shared/models/color.models'; import { TbColorScheme } from '@shared/models/color.models';
import { AbstractControl, ValidationErrors } from '@angular/forms'; import { AbstractControl, ValidationErrors } from '@angular/forms';
import { MarkLine2DDataItemOption } from 'echarts/types/src/component/marker/MarkLineModel'; import { MarkLine2DDataItemOption } from 'echarts/types/src/component/marker/MarkLineModel';
import { DatePipe } from '@angular/common'; import { DatePipe } from '@angular/common';
import { BuiltinTextPosition } from 'zrender/src/core/types'; import { BuiltinTextPosition } from 'zrender/src/core/types';
import { CartesianAxisOption } from 'echarts/types/src/coord/cartesian/AxisModel';
import { WidgetTimewindow } from '@shared/models/time/time.models';
import { UtilsService } from '@core/services/utils.service';
export enum TimeSeriesChartType { export enum TimeSeriesChartType {
default = 'default', default = 'default',
@ -692,7 +695,11 @@ export const timeSeriesChartStateValidator = (control: AbstractControl): Validat
return null; return null;
}; };
export interface TimeSeriesChartSettings extends EChartsTooltipWidgetSettings { export interface TimeSeriesChartComparisonSettings extends WidgetComparisonSettings {
comparisonXAxis?: TimeSeriesChartXAxisSettings;
}
export interface TimeSeriesChartSettings extends EChartsTooltipWidgetSettings, TimeSeriesChartComparisonSettings {
thresholds: TimeSeriesChartThreshold[]; thresholds: TimeSeriesChartThreshold[];
darkMode: boolean; darkMode: boolean;
dataZoom: boolean; dataZoom: boolean;
@ -750,7 +757,13 @@ export const timeSeriesChartDefaultSettings: TimeSeriesChartSettings = {
tooltipDateColor: 'rgba(0, 0, 0, 0.76)', tooltipDateColor: 'rgba(0, 0, 0, 0.76)',
tooltipDateInterval: true, tooltipDateInterval: true,
tooltipBackgroundColor: 'rgba(255, 255, 255, 0.76)', tooltipBackgroundColor: 'rgba(255, 255, 255, 0.76)',
tooltipBackgroundBlur: 4 tooltipBackgroundBlur: 4,
comparisonEnabled: false,
timeForComparison: 'previousInterval',
comparisonCustomIntervalValue: 7200000,
comparisonXAxis: mergeDeep({} as TimeSeriesChartXAxisSettings,
defaultTimeSeriesChartXAxisSettings,
{ position: AxisPosition.top } as TimeSeriesChartXAxisSettings)
}; };
export interface SeriesFillSettings { export interface SeriesFillSettings {
@ -798,7 +811,7 @@ export interface BarSeriesSettings {
backgroundSettings: SeriesFillSettings; backgroundSettings: SeriesFillSettings;
} }
export interface TimeSeriesChartKeySettings { export interface TimeSeriesChartKeySettings extends DataKeySettingsWithComparison {
yAxisId: TimeSeriesChartYAxisId; yAxisId: TimeSeriesChartYAxisId;
showInLegend: boolean; showInLegend: boolean;
dataHiddenByDefault: boolean; dataHiddenByDefault: boolean;
@ -870,10 +883,16 @@ export const timeSeriesChartKeyDefaultSettings: TimeSeriesChartKeySettings = {
end: 0 end: 0
} }
} }
},
comparisonSettings: {
showValuesForComparison: false,
comparisonValuesLabel: '',
color: ''
} }
}; };
export interface TimeSeriesChartDataItem extends EChartsSeriesItem { export interface TimeSeriesChartDataItem extends EChartsSeriesItem {
xAxisIndex: number;
yAxisId: TimeSeriesChartYAxisId; yAxisId: TimeSeriesChartYAxisId;
yAxisIndex: number; yAxisIndex: number;
option?: LineSeriesOption | CustomSeriesOption; option?: LineSeriesOption | CustomSeriesOption;
@ -894,16 +913,27 @@ export interface TimeSeriesChartThresholdItem {
option?: LineSeriesOption; option?: LineSeriesOption;
} }
export interface TimeSeriesChartYAxis { export interface TimeSeriesChartAxis {
id: string; id: string;
settings: TimeSeriesChartAxisSettings;
option: CartesianAxisOption;
}
export interface TimeSeriesChartYAxis extends TimeSeriesChartAxis {
decimals: number; decimals: number;
settings: TimeSeriesChartYAxisSettings; settings: TimeSeriesChartYAxisSettings;
option: YAXisOption & ValueAxisBaseOption; option: YAXisOption & ValueAxisBaseOption;
} }
export interface TimeSeriesChartXAxis extends TimeSeriesChartAxis {
settings: TimeSeriesChartXAxisSettings;
option: XAXisOption;
}
export const createTimeSeriesYAxis = (units: string, export const createTimeSeriesYAxis = (units: string,
decimals: number, decimals: number,
settings: TimeSeriesChartYAxisSettings, settings: TimeSeriesChartYAxisSettings,
utils: UtilsService,
darkMode: boolean): TimeSeriesChartYAxis => { darkMode: boolean): TimeSeriesChartYAxis => {
const yAxisTickLabelStyle = createChartTextStyle(settings.tickLabelFont, const yAxisTickLabelStyle = createChartTextStyle(settings.tickLabelFont,
settings.tickLabelColor, darkMode, 'axis.tickLabel'); settings.tickLabelColor, darkMode, 'axis.tickLabel');
@ -944,6 +974,7 @@ export const createTimeSeriesYAxis = (units: string,
decimals, decimals,
settings, settings,
option: { option: {
mainType: 'yAxis',
show: settings.show, show: settings.show,
type: 'value', type: 'value',
position: settings.position, position: settings.position,
@ -957,7 +988,7 @@ export const createTimeSeriesYAxis = (units: string,
splitNumber, splitNumber,
interval, interval,
ticksGenerator, ticksGenerator,
name: settings.label, name: utils.customTranslation(settings.label, settings.label),
nameLocation: 'middle', nameLocation: 'middle',
nameRotate: settings.position === AxisPosition.left ? 90 : -90, nameRotate: settings.position === AxisPosition.left ? 90 : -90,
nameTextStyle: { nameTextStyle: {
@ -1011,75 +1042,88 @@ export const createTimeSeriesYAxis = (units: string,
}; };
}; };
export const createTimeSeriesXAxisOption = (settings: TimeSeriesChartXAxisSettings, export const createTimeSeriesXAxis = (id: string,
min: number, max: number, settings: TimeSeriesChartXAxisSettings,
datePipe: DatePipe, min: number, max: number,
darkMode: boolean): XAXisOption => { datePipe: DatePipe,
utils: UtilsService,
darkMode: boolean): TimeSeriesChartXAxis => {
const xAxisTickLabelStyle = createChartTextStyle(settings.tickLabelFont, const xAxisTickLabelStyle = createChartTextStyle(settings.tickLabelFont,
settings.tickLabelColor, darkMode, 'axis.tickLabel'); settings.tickLabelColor, darkMode, 'axis.tickLabel');
const xAxisNameStyle = createChartTextStyle(settings.labelFont, const xAxisNameStyle = createChartTextStyle(settings.labelFont,
settings.labelColor, darkMode, 'axis.label'); settings.labelColor, darkMode, 'axis.label');
const ticksFormat = mergeDeep({}, defaultXAxisTicksFormat, settings.ticksFormat); const ticksFormat = mergeDeep({}, defaultXAxisTicksFormat, settings.ticksFormat);
return { return {
show: settings.show, id,
type: 'time', settings,
scale: true, option: {
position: settings.position, mainType: 'xAxis',
name: settings.label, show: settings.show,
nameLocation: 'middle', type: 'time',
nameTextStyle: { scale: true,
color: xAxisNameStyle.color, position: settings.position,
fontStyle: xAxisNameStyle.fontStyle, id,
fontWeight: xAxisNameStyle.fontWeight, name: utils.customTranslation(settings.label, settings.label),
fontFamily: xAxisNameStyle.fontFamily, nameLocation: 'middle',
fontSize: xAxisNameStyle.fontSize nameTextStyle: {
}, color: xAxisNameStyle.color,
axisTick: { fontStyle: xAxisNameStyle.fontStyle,
show: settings.showTicks, fontWeight: xAxisNameStyle.fontWeight,
lineStyle: { fontFamily: xAxisNameStyle.fontFamily,
color: prepareChartThemeColor(settings.ticksColor, darkMode, 'axis.ticks') fontSize: xAxisNameStyle.fontSize
} },
}, axisPointer: {
axisLabel: { shadowStyle: {
show: settings.showTickLabels, color: id === 'main' ? 'rgba(210,219,238,0.2)' : 'rgba(150,150,150,0.1)'
color: xAxisTickLabelStyle.color,
fontStyle: xAxisTickLabelStyle.fontStyle,
fontWeight: xAxisTickLabelStyle.fontWeight,
fontFamily: xAxisTickLabelStyle.fontFamily,
fontSize: xAxisTickLabelStyle.fontSize,
hideOverlap: true,
/** Min/Max time label always visible **/
/* alignMinLabel: 'left',
alignMaxLabel: 'right',
showMinLabel: true,
showMaxLabel: true, */
formatter: (value: number, _index: number, extra: {level: number}) => {
const unit = tsToFormatTimeUnit(value);
const format = ticksFormat[unit];
const formatted = datePipe.transform(value, format);
if (extra.level > 0) {
return `{primary|${formatted}}`;
} else {
return formatted;
} }
} },
}, axisTick: {
axisLine: { show: settings.showTicks,
show: settings.showLine, lineStyle: {
onZero: false, color: prepareChartThemeColor(settings.ticksColor, darkMode, 'axis.ticks')
lineStyle: { }
color: prepareChartThemeColor(settings.lineColor, darkMode, 'axis.line') },
} axisLabel: {
}, show: settings.showTickLabels,
splitLine: { color: xAxisTickLabelStyle.color,
show: settings.showSplitLines, fontStyle: xAxisTickLabelStyle.fontStyle,
lineStyle: { fontWeight: xAxisTickLabelStyle.fontWeight,
color: prepareChartThemeColor(settings.splitLinesColor, darkMode, 'axis.splitLine') fontFamily: xAxisTickLabelStyle.fontFamily,
} fontSize: xAxisTickLabelStyle.fontSize,
}, hideOverlap: true,
min, /** Min/Max time label always visible **/
max, /* alignMinLabel: 'left',
bandWidthCalculator: timeAxisBandWidthCalculator alignMaxLabel: 'right',
showMinLabel: true,
showMaxLabel: true, */
formatter: (value: number, _index: number, extra: {level: number}) => {
const unit = tsToFormatTimeUnit(value);
const format = ticksFormat[unit];
const formatted = datePipe.transform(value, format);
if (extra.level > 0) {
return `{primary|${formatted}}`;
} else {
return formatted;
}
}
},
axisLine: {
show: settings.showLine,
onZero: false,
lineStyle: {
color: prepareChartThemeColor(settings.lineColor, darkMode, 'axis.line')
}
},
splitLine: {
show: settings.showSplitLines,
lineStyle: {
color: prepareChartThemeColor(settings.splitLinesColor, darkMode, 'axis.splitLine')
}
},
min,
max,
bandWidthCalculator: timeAxisBandWidthCalculator
}
}; };
}; };
@ -1098,6 +1142,13 @@ export const createTimeSeriesVisualMapOption = (settings: TimeSeriesChartVisualM
} : undefined } : undefined
}); });
export const updateXAxisTimeWindow = (option: XAXisOption,
timeWindow: WidgetTimewindow) => {
option.min = timeWindow.minTime;
option.max = timeWindow.maxTime;
(option as any).tbTimeWindow = timeWindow;
};
export const generateChartData = (dataItems: TimeSeriesChartDataItem[], export const generateChartData = (dataItems: TimeSeriesChartDataItem[],
thresholdItems: TimeSeriesChartThresholdItem[], thresholdItems: TimeSeriesChartThresholdItem[],
stack: boolean, stack: boolean,
@ -1262,6 +1313,7 @@ const generateChartSeries = (dataItems: TimeSeriesChartDataItem[],
}; };
export const updateDarkMode = (options: EChartsOption, settings: TimeSeriesChartSettings, export const updateDarkMode = (options: EChartsOption, settings: TimeSeriesChartSettings,
xAxisList: TimeSeriesChartXAxis[],
yAxisList: TimeSeriesChartYAxis[], yAxisList: TimeSeriesChartYAxis[],
dataItems: TimeSeriesChartDataItem[], dataItems: TimeSeriesChartDataItem[],
darkMode: boolean): EChartsOption => { darkMode: boolean): EChartsOption => {
@ -1278,12 +1330,14 @@ export const updateDarkMode = (options: EChartsOption, settings: TimeSeriesChart
} }
} }
if (Array.isArray(options.xAxis)) { if (Array.isArray(options.xAxis)) {
for (const xAxis of options.xAxis) { for (let i = 0; i < options.xAxis.length; i++) {
xAxis.nameTextStyle.color = prepareChartThemeColor(settings.xAxis.labelColor, darkMode, 'axis.label'); const xAxis = options.xAxis[i];
xAxis.axisLabel.color = prepareChartThemeColor(settings.xAxis.tickLabelColor, darkMode, 'axis.tickLabel'); const xAxisSettings = xAxisList[i].settings;
xAxis.axisLine.lineStyle.color = prepareChartThemeColor(settings.xAxis.lineColor, darkMode, 'axis.line'); xAxis.nameTextStyle.color = prepareChartThemeColor(xAxisSettings.labelColor, darkMode, 'axis.label');
xAxis.axisTick.lineStyle.color = prepareChartThemeColor(settings.xAxis.ticksColor, darkMode, 'axis.ticks'); xAxis.axisLabel.color = prepareChartThemeColor(xAxisSettings.tickLabelColor, darkMode, 'axis.tickLabel');
xAxis.splitLine.lineStyle.color = prepareChartThemeColor(settings.xAxis.splitLinesColor, darkMode, 'axis.splitLine'); xAxis.axisLine.lineStyle.color = prepareChartThemeColor(xAxisSettings.lineColor, darkMode, 'axis.line');
xAxis.axisTick.lineStyle.color = prepareChartThemeColor(xAxisSettings.ticksColor, darkMode, 'axis.ticks');
xAxis.splitLine.lineStyle.color = prepareChartThemeColor(xAxisSettings.splitLinesColor, darkMode, 'axis.splitLine');
} }
} }
for (const item of dataItems) { for (const item of dataItems) {
@ -1322,6 +1376,7 @@ const createTimeSeriesChartSeries = (item: TimeSeriesChartDataItem,
seriesOption = { seriesOption = {
id: item.id, id: item.id,
dataGroupId: item.id, dataGroupId: item.id,
xAxisIndex: item.xAxisIndex,
yAxisIndex: item.yAxisIndex, yAxisIndex: item.yAxisIndex,
name: item.dataKey.label, name: item.dataKey.label,
color: seriesColor, color: seriesColor,

146
ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.ts

@ -16,16 +16,16 @@
import { WidgetContext } from '@home/models/widget-component.models'; import { WidgetContext } from '@home/models/widget-component.models';
import { import {
AxisPosition,
calculateThresholdsOffset, calculateThresholdsOffset,
createTimeSeriesVisualMapOption, createTimeSeriesVisualMapOption,
createTimeSeriesXAxisOption, createTimeSeriesXAxis,
createTimeSeriesYAxis, createTimeSeriesYAxis,
defaultTimeSeriesChartYAxisSettings, defaultTimeSeriesChartYAxisSettings,
generateChartData, generateChartData,
LineSeriesStepType, LineSeriesStepType,
parseThresholdData, parseThresholdData,
SeriesLabelPosition, SeriesLabelPosition,
TimeSeriesChartAxis,
TimeSeriesChartDataItem, TimeSeriesChartDataItem,
timeSeriesChartDefaultSettings, timeSeriesChartDefaultSettings,
timeSeriesChartKeyDefaultSettings, timeSeriesChartKeyDefaultSettings,
@ -38,16 +38,17 @@ import {
TimeSeriesChartThresholdItem, TimeSeriesChartThresholdItem,
TimeSeriesChartThresholdType, TimeSeriesChartThresholdType,
TimeSeriesChartType, TimeSeriesChartType,
TimeSeriesChartXAxis,
TimeSeriesChartYAxis, TimeSeriesChartYAxis,
TimeSeriesChartYAxisId, TimeSeriesChartYAxisId,
TimeSeriesChartYAxisSettings, TimeSeriesChartYAxisSettings,
updateDarkMode updateDarkMode,
updateXAxisTimeWindow
} from '@home/components/widget/lib/chart/time-series-chart.models'; } from '@home/components/widget/lib/chart/time-series-chart.models';
import { ResizeObserver } from '@juggle/resize-observer'; import { ResizeObserver } from '@juggle/resize-observer';
import { import {
adjustTimeAxisExtentToData, adjustTimeAxisExtentToData,
calculateXAxisHeight, calculateAxisSize,
calculateYAxisWidth,
createTooltipValueFormatFunction, createTooltipValueFormatFunction,
ECharts, ECharts,
echartsModule, echartsModule,
@ -58,8 +59,7 @@ import {
EChartsTooltipValueFormatFunction, EChartsTooltipValueFormatFunction,
getAxisExtent, getAxisExtent,
getFocusedSeriesIndex, getFocusedSeriesIndex,
measureXAxisNameHeight, measureAxisNameSize,
measureYAxisNameWidth,
toNamedData toNamedData
} from '@home/components/widget/lib/chart/echarts-widget.models'; } from '@home/components/widget/lib/chart/echarts-widget.models';
import { DateFormatProcessor } from '@shared/models/widget-settings.models'; import { DateFormatProcessor } from '@shared/models/widget-settings.models';
@ -120,6 +120,10 @@ export class TbTimeSeriesChart {
private readonly settings: TimeSeriesChartSettings; private readonly settings: TimeSeriesChartSettings;
private readonly comparisonEnabled: boolean;
private readonly stackMode: boolean;
private xAxisList: TimeSeriesChartXAxis[] = [];
private yAxisList: TimeSeriesChartYAxis[] = []; private yAxisList: TimeSeriesChartYAxis[] = [];
private dataItems: TimeSeriesChartDataItem[] = []; private dataItems: TimeSeriesChartDataItem[] = [];
private thresholdItems: TimeSeriesChartThresholdItem[] = []; private thresholdItems: TimeSeriesChartThresholdItem[] = [];
@ -163,13 +167,16 @@ export class TbTimeSeriesChart {
this.settings = mergeDeep({} as TimeSeriesChartSettings, this.settings = mergeDeep({} as TimeSeriesChartSettings,
timeSeriesChartDefaultSettings, timeSeriesChartDefaultSettings,
this.inputSettings as TimeSeriesChartSettings); this.inputSettings as TimeSeriesChartSettings);
this.comparisonEnabled = !!this.ctx.defaultSubscription.comparisonEnabled;
this.stackMode = !this.comparisonEnabled && this.settings.stack;
if (this.settings.states && this.settings.states.length) { if (this.settings.states && this.settings.states.length) {
this.stateValueConverter = new TimeSeriesChartStateValueConverter(this.ctx.dashboard.utils, this.settings.states); this.stateValueConverter = new TimeSeriesChartStateValueConverter(this.ctx.utilsService, this.settings.states);
this.tooltipValueFormatFunction = this.stateValueConverter.tooltipFormatter; this.tooltipValueFormatFunction = this.stateValueConverter.tooltipFormatter;
} }
const $dashboardPageElement = this.ctx.$containerParent.parents('.tb-dashboard-page'); const $dashboardPageElement = this.ctx.$containerParent.parents('.tb-dashboard-page');
const dashboardPageElement = $dashboardPageElement.length ? $($dashboardPageElement[$dashboardPageElement.length-1]) : null; const dashboardPageElement = $dashboardPageElement.length ? $($dashboardPageElement[$dashboardPageElement.length-1]) : null;
this.darkMode = this.settings.darkMode || dashboardPageElement?.hasClass('dark'); this.darkMode = this.settings.darkMode || dashboardPageElement?.hasClass('dark');
this.setupXAxes();
this.setupYAxes(); this.setupYAxes();
this.setupData(); this.setupData();
this.setupThresholds(); this.setupThresholds();
@ -216,14 +223,16 @@ export class TbTimeSeriesChart {
} }
this.onResize(); this.onResize();
if (this.timeSeriesChart) { if (this.timeSeriesChart) {
this.timeSeriesChartOptions.xAxis[0].min = this.ctx.defaultSubscription.timeWindow.minTime; updateXAxisTimeWindow(this.xAxisList[0].option, this.ctx.defaultSubscription.timeWindow);
this.timeSeriesChartOptions.xAxis[0].max = this.ctx.defaultSubscription.timeWindow.maxTime;
this.timeSeriesChartOptions.xAxis[0].tbTimeWindow = this.ctx.defaultSubscription.timeWindow;
if (this.noAggregation) { if (this.noAggregation) {
this.timeSeriesChartOptions.tooltip[0].axisPointer.type = 'line'; this.timeSeriesChartOptions.tooltip[0].axisPointer.type = 'line';
} else { } else {
this.timeSeriesChartOptions.tooltip[0].axisPointer.type = 'shadow'; this.timeSeriesChartOptions.tooltip[0].axisPointer.type = 'shadow';
} }
if (this.comparisonEnabled) {
updateXAxisTimeWindow(this.xAxisList[1].option, this.ctx.defaultSubscription.comparisonTimeWindow);
}
this.timeSeriesChartOptions.xAxis = this.xAxisList.map(axis => axis.option);
if (this.hasVisualMap) { if (this.hasVisualMap) {
(this.timeSeriesChartOptions.visualMap as PiecewiseVisualMapOption).selected = this.visualMapSelectedRanges; (this.timeSeriesChartOptions.visualMap as PiecewiseVisualMapOption).selected = this.visualMapSelectedRanges;
} }
@ -300,7 +309,7 @@ export class TbTimeSeriesChart {
this.timeSeriesChartOptions.yAxis = this.yAxisList.map(axis => axis.option); this.timeSeriesChartOptions.yAxis = this.yAxisList.map(axis => axis.option);
mergeList.push('yAxis'); mergeList.push('yAxis');
} }
this.timeSeriesChart.setOption(this.timeSeriesChartOptions, this.settings.stack ? {notMerge: true} : {replaceMerge: mergeList}); this.timeSeriesChart.setOption(this.timeSeriesChartOptions, this.stackMode ? {notMerge: true} : {replaceMerge: mergeList});
this.updateAxes(); this.updateAxes();
dataKey.hidden = !enable; dataKey.hidden = !enable;
if (enable) { if (enable) {
@ -343,7 +352,7 @@ export class TbTimeSeriesChart {
this.darkMode = darkMode; this.darkMode = darkMode;
if (this.timeSeriesChart) { if (this.timeSeriesChart) {
this.timeSeriesChartOptions = updateDarkMode(this.timeSeriesChartOptions, this.timeSeriesChartOptions = updateDarkMode(this.timeSeriesChartOptions,
this.settings, this.yAxisList, this.dataItems, darkMode); this.settings, this.xAxisList, this.yAxisList, this.dataItems, darkMode);
this.timeSeriesChart.setOption(this.timeSeriesChartOptions); this.timeSeriesChart.setOption(this.timeSeriesChartOptions);
} }
} }
@ -392,12 +401,16 @@ export class TbTimeSeriesChart {
if (!Object.keys(this.settings.yAxes).includes(yAxisId)) { if (!Object.keys(this.settings.yAxes).includes(yAxisId)) {
yAxisId = 'default'; yAxisId = 'default';
} }
const comparisonItem = this.comparisonEnabled && dataKey.isAdditional;
const xAxisIndex = comparisonItem ? 1 : 0;
this.dataItems.push({ this.dataItems.push({
id: this.nextComponentId(), id: this.nextComponentId(),
units, units,
decimals, decimals,
xAxisIndex,
yAxisId, yAxisId,
yAxisIndex: this.getYAxisIndex(yAxisId), yAxisIndex: this.getYAxisIndex(yAxisId),
comparisonItem,
datasource, datasource,
dataKey, dataKey,
data: namedData, data: namedData,
@ -488,6 +501,18 @@ export class TbTimeSeriesChart {
this.subscribeForEntityThresholds(thresholdDatasources); this.subscribeForEntityThresholds(thresholdDatasources);
} }
private setupXAxes(): void {
const mainXAxis = createTimeSeriesXAxis('main', this.settings.xAxis, this.ctx.defaultSubscription.timeWindow.minTime,
this.ctx.defaultSubscription.timeWindow.maxTime, this.ctx.date, this.ctx.utilsService, this.darkMode);
this.xAxisList.push(mainXAxis);
if (this.comparisonEnabled) {
const comparisonXAxis = createTimeSeriesXAxis('comparison', this.settings.comparisonXAxis,
this.ctx.defaultSubscription.comparisonTimeWindow.minTime, this.ctx.defaultSubscription.comparisonTimeWindow.maxTime,
this.ctx.date, this.ctx.utilsService, this.darkMode);
this.xAxisList.push(comparisonXAxis);
}
}
private setupYAxes(): void { private setupYAxes(): void {
const yAxisSettingsList = Object.values(this.settings.yAxes); const yAxisSettingsList = Object.values(this.settings.yAxes);
yAxisSettingsList.sort((a1, a2) => a1.order - a2.order); yAxisSettingsList.sort((a1, a2) => a1.order - a2.order);
@ -501,7 +526,7 @@ export class TbTimeSeriesChart {
axisSettings.ticksGenerator = this.stateValueConverter.ticksGenerator; axisSettings.ticksGenerator = this.stateValueConverter.ticksGenerator;
axisSettings.ticksFormatter = this.stateValueConverter.ticksFormatter; axisSettings.ticksFormatter = this.stateValueConverter.ticksFormatter;
} }
const yAxis = createTimeSeriesYAxis(units, decimals, axisSettings, this.darkMode); const yAxis = createTimeSeriesYAxis(units, decimals, axisSettings, this.ctx.utilsService, this.darkMode);
this.yAxisList.push(yAxis); this.yAxisList.push(yAxis);
} }
} }
@ -592,10 +617,7 @@ export class TbTimeSeriesChart {
right: this.settings.dataZoom ? 5 : 0, right: this.settings.dataZoom ? 5 : 0,
bottom: this.minBottomOffset() bottom: this.minBottomOffset()
}], }],
xAxis: [ xAxis: this.xAxisList.map(axis => axis.option),
createTimeSeriesXAxisOption(this.settings.xAxis, this.ctx.defaultSubscription.timeWindow.minTime,
this.ctx.defaultSubscription.timeWindow.maxTime, this.ctx.date, this.darkMode)
],
yAxis: this.yAxisList.map(axis => axis.option), yAxis: this.yAxisList.map(axis => axis.option),
dataZoom: [ dataZoom: [
{ {
@ -655,10 +677,10 @@ export class TbTimeSeriesChart {
private updateSeries(): void { private updateSeries(): void {
this.timeSeriesChartOptions.series = generateChartData(this.dataItems, this.thresholdItems, this.timeSeriesChartOptions.series = generateChartData(this.dataItems, this.thresholdItems,
this.settings.stack, this.stackMode,
this.noAggregation, this.noAggregation,
this.barRenderSharedContext, this.darkMode); this.barRenderSharedContext, this.darkMode);
if (this.stateData) { if (this.stateData && !this.comparisonEnabled) {
adjustTimeAxisExtentToData(this.timeSeriesChartOptions.xAxis[0], this.dataItems, adjustTimeAxisExtentToData(this.timeSeriesChartOptions.xAxis[0], this.dataItems,
this.ctx.defaultSubscription.timeWindow.minTime, this.ctx.defaultSubscription.timeWindow.minTime,
this.ctx.defaultSubscription.timeWindow.maxTime); this.ctx.defaultSubscription.timeWindow.maxTime);
@ -667,37 +689,26 @@ export class TbTimeSeriesChart {
private updateAxes(lazy = true) { private updateAxes(lazy = true) {
const leftAxisList = this.yAxisList.filter(axis => axis.option.position === 'left'); const leftAxisList = this.yAxisList.filter(axis => axis.option.position === 'left');
let res = this.updateYAxisOffset(leftAxisList); let res = this.updateAxisOffset(leftAxisList);
let leftOffset = res.offset + (!res.offset && this.settings.dataZoom ? 5 : 0); let leftOffset = res.offset + (!res.offset && this.settings.dataZoom ? 5 : 0);
let changed = res.changed; let changed = res.changed;
const rightAxisList = this.yAxisList.filter(axis => axis.option.position === 'right'); const rightAxisList = this.yAxisList.filter(axis => axis.option.position === 'right');
res = this.updateYAxisOffset(rightAxisList); res = this.updateAxisOffset(rightAxisList);
let rightOffset = res.offset + (!res.offset && this.settings.dataZoom ? 5 : 0); let rightOffset = res.offset + (!res.offset && this.settings.dataZoom ? 5 : 0);
changed = changed || res.changed; changed = changed || res.changed;
let bottomOffset = this.minBottomOffset(); let bottomOffset = this.minBottomOffset();
const minTopOffset = this.minTopOffset(); const minTopOffset = this.minTopOffset();
let topOffset = minTopOffset;
if (this.timeSeriesChartOptions.xAxis[0].show) { const topAxisList = this.xAxisList.filter(axis => axis.option.position === 'top');
const xAxisHeight = calculateXAxisHeight(this.timeSeriesChart); res = this.updateAxisOffset(topAxisList);
if (this.timeSeriesChartOptions.xAxis[0].position === AxisPosition.bottom) { const topOffset = Math.max(res.offset, minTopOffset);
bottomOffset += xAxisHeight; changed = changed || res.changed;
} else {
topOffset = Math.max(minTopOffset, xAxisHeight); const bottomAxisList = this.xAxisList.filter(axis => axis.option.position === 'bottom');
} res = this.updateAxisOffset(bottomAxisList);
if (this.settings.xAxis.label) { bottomOffset += res.offset;
const nameHeight = measureXAxisNameHeight(this.timeSeriesChart, this.timeSeriesChartOptions.xAxis[0].name); changed = changed || res.changed;
if (this.timeSeriesChartOptions.xAxis[0].position === AxisPosition.bottom) {
bottomOffset += nameHeight;
} else {
topOffset = Math.max(minTopOffset, xAxisHeight + nameHeight);
}
const nameGap = xAxisHeight;
if (this.timeSeriesChartOptions.xAxis[0].nameGap !== nameGap) {
this.timeSeriesChartOptions.xAxis[0].nameGap = nameGap;
changed = true;
}
}
}
const thresholdsOffset = calculateThresholdsOffset(this.timeSeriesChart, this.thresholdItems, this.yAxisList); const thresholdsOffset = calculateThresholdsOffset(this.timeSeriesChart, this.thresholdItems, this.yAxisList);
leftOffset = Math.max(leftOffset, thresholdsOffset[0]); leftOffset = Math.max(leftOffset, thresholdsOffset[0]);
@ -715,6 +726,7 @@ export class TbTimeSeriesChart {
} }
if (changed) { if (changed) {
this.timeSeriesChartOptions.yAxis = this.yAxisList.map(axis => axis.option); this.timeSeriesChartOptions.yAxis = this.yAxisList.map(axis => axis.option);
this.timeSeriesChartOptions.xAxis = this.xAxisList.map(axis => axis.option);
this.timeSeriesChart.setOption(this.timeSeriesChartOptions, {replaceMerge: ['yAxis', 'xAxis', 'grid'], lazyUpdate: lazy}); this.timeSeriesChart.setOption(this.timeSeriesChartOptions, {replaceMerge: ['yAxis', 'xAxis', 'grid'], lazyUpdate: lazy});
} }
if (this.yAxisList.length) { if (this.yAxisList.length) {
@ -730,45 +742,45 @@ export class TbTimeSeriesChart {
} }
} }
private updateYAxisOffset(axisList: TimeSeriesChartYAxis[]): {offset: number; changed: boolean} { private updateAxisOffset(axisList: TimeSeriesChartAxis[]): {offset: number; changed: boolean} {
const result = {offset: 0, changed: false}; const result = {offset: 0, changed: false};
let width = 0; let size = 0;
for (const yAxis of axisList) { for (const axis of axisList) {
const newWidth = calculateYAxisWidth(this.timeSeriesChart, yAxis.id); const newSize = calculateAxisSize(this.timeSeriesChart, axis.option.mainType, axis.id);
if (width && newWidth) { if (size && newSize) {
result.offset += 5; result.offset += 5;
} }
width = newWidth; size = newSize;
const showLine = !!width && yAxis.settings.showLine; const showLine = !!size && axis.settings.showLine;
if (yAxis.option.axisLine.show !== showLine) { if (axis.option.axisLine.show !== showLine) {
yAxis.option.axisLine.show = showLine; axis.option.axisLine.show = showLine;
result.changed = true; result.changed = true;
} }
if (yAxis.option.offset !== result.offset) { if (axis.option.offset !== result.offset) {
yAxis.option.offset = result.offset; axis.option.offset = result.offset;
result.changed = true; result.changed = true;
} }
if (yAxis.settings.label) { if (axis.settings.label) {
if (!width) { if (!size) {
if (yAxis.option.name) { if (axis.option.name) {
yAxis.option.name = null; axis.option.name = null;
result.changed = true; result.changed = true;
} }
} else { } else {
if (!yAxis.option.name) { if (!axis.option.name) {
yAxis.option.name = yAxis.settings.label; axis.option.name = axis.settings.label;
result.changed = true; result.changed = true;
} }
const nameGap = width; const nameGap = size;
if (yAxis.option.nameGap !== nameGap) { if (axis.option.nameGap !== nameGap) {
yAxis.option.nameGap = nameGap; axis.option.nameGap = nameGap;
result.changed = true; result.changed = true;
} }
const nameWidth = measureYAxisNameWidth(this.timeSeriesChart, yAxis.id, yAxis.settings.label); const nameSize = measureAxisNameSize(this.timeSeriesChart, axis.option.mainType, axis.id, axis.settings.label);
result.offset += nameWidth; result.offset += nameSize;
} }
} }
result.offset += width; result.offset += size;
} }
return result; return result;
} }

12
ui-ngx/src/app/modules/home/components/widget/lib/flot-widget.models.ts

@ -18,11 +18,10 @@
/// <reference path="../../../../../../../src/typings/jquery.flot.typings.d.ts" /> /// <reference path="../../../../../../../src/typings/jquery.flot.typings.d.ts" />
import { import {
DataKey, DataKey, DataKeySettingsWithComparison,
Datasource, Datasource,
DatasourceData, DatasourceData,
FormattedData, FormattedData,
JsonSettingsSchema,
LegendConfig LegendConfig
} from '@shared/models/widget.models'; } from '@shared/models/widget.models';
import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
@ -219,13 +218,7 @@ export interface TbFlotKeyThreshold {
color: string; color: string;
} }
export interface TbFlotKeyComparisonSettings { export interface TbFlotKeySettings extends DataKeySettingsWithComparison {
showValuesForComparison: boolean;
comparisonValuesLabel: string;
color: string;
}
export interface TbFlotKeySettings {
excludeFromStacking: boolean; excludeFromStacking: boolean;
hideDataByDefault: boolean; hideDataByDefault: boolean;
disableDataHiding: boolean; disableDataHiding: boolean;
@ -249,7 +242,6 @@ export interface TbFlotKeySettings {
axisPosition: TbFlotYAxisPosition; axisPosition: TbFlotYAxisPosition;
axisTicksFormatter: string; axisTicksFormatter: string;
thresholds: TbFlotKeyThreshold[]; thresholds: TbFlotKeyThreshold[];
comparisonSettings: TbFlotKeyComparisonSettings;
} }
export interface TbFlotLatestKeySettings { export interface TbFlotLatestKeySettings {

29
ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-key-settings.component.html

@ -66,6 +66,35 @@
helpId="widget/lib/flot/tooltip_value_format_fn"> helpId="widget/lib/flot/tooltip_value_format_fn">
</tb-js-func> </tb-js-func>
</div> </div>
<div *ngIf="comparisonEnabled" class="tb-form-panel tb-slide-toggle" formGroupName="comparisonSettings">
<div class="tb-form-panel-title" translate>widgets.time-series-chart.comparison.settings</div>
<mat-expansion-panel class="tb-settings" [expanded]="timeSeriesChartKeySettingsForm.get('comparisonSettings.showValuesForComparison').value"
[disabled]="!timeSeriesChartKeySettingsForm.get('comparisonSettings.showValuesForComparison').value">
<mat-expansion-panel-header fxLayout="row wrap">
<mat-panel-title>
<mat-slide-toggle class="mat-slide" formControlName="showValuesForComparison" (click)="$event.stopPropagation()"
fxLayoutAlign="center">
{{ 'widgets.time-series-chart.comparison.show-values-for-comparison' | translate }}
</mat-slide-toggle>
</mat-panel-title>
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
<div class="tb-form-row">
<div class="fixed-title-width" translate>widgets.time-series-chart.comparison.comparison-values-label</div>
<mat-form-field fxFlex appearance="outline" subscriptSizing="dynamic">
<input matInput formControlName="comparisonValuesLabel" placeholder="{{ 'widgets.time-series-chart.comparison.comparison-values-label-auto' | translate }}">
</mat-form-field>
</div>
<div class="tb-form-row space-between">
<div>{{ 'widgets.time-series-chart.comparison.comparison-data-color' | translate }}</div>
<tb-color-input asBoxInput
colorClearButton
formControlName="color">
</tb-color-input>
</div>
</ng-template>
</mat-expansion-panel>
</div>
</ng-container> </ng-container>
<ng-template #chartTypeTitle> <ng-template #chartTypeTitle>
<div class="tb-form-panel-title">{{ timeSeriesChartTypeTranslations.get(chartType) | translate }}</div> <div class="tb-form-panel-title">{{ timeSeriesChartTypeTranslations.get(chartType) | translate }}</div>

24
ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-key-settings.component.ts

@ -54,6 +54,8 @@ export class TimeSeriesChartKeySettingsComponent extends WidgetSettingsComponent
yAxisIds: TimeSeriesChartYAxisId[]; yAxisIds: TimeSeriesChartYAxisId[];
comparisonEnabled: boolean;
functionScopeVariables = this.widgetService.getWidgetScopeVariables(); functionScopeVariables = this.widgetService.getWidgetScopeVariables();
constructor(protected store: Store<AppState>, constructor(protected store: Store<AppState>,
@ -73,6 +75,7 @@ export class TimeSeriesChartKeySettingsComponent extends WidgetSettingsComponent
} }
const widgetSettings = (widgetConfig.config?.settings || {}) as TimeSeriesChartWidgetSettings; const widgetSettings = (widgetConfig.config?.settings || {}) as TimeSeriesChartWidgetSettings;
this.yAxisIds = widgetSettings.yAxes ? Object.keys(widgetSettings.yAxes) : ['default']; this.yAxisIds = widgetSettings.yAxes ? Object.keys(widgetSettings.yAxes) : ['default'];
this.comparisonEnabled = !!widgetSettings.comparisonEnabled;
} }
protected defaultSettings(): WidgetSettings { protected defaultSettings(): WidgetSettings {
@ -94,16 +97,23 @@ export class TimeSeriesChartKeySettingsComponent extends WidgetSettingsComponent
lineSettings: [seriesSettings.lineSettings, []], lineSettings: [seriesSettings.lineSettings, []],
barSettings: [seriesSettings.barSettings, []], barSettings: [seriesSettings.barSettings, []],
tooltipValueFormatter: [seriesSettings.tooltipValueFormatter, []], tooltipValueFormatter: [seriesSettings.tooltipValueFormatter, []],
comparisonSettings: this.fb.group({
showValuesForComparison: [seriesSettings.comparisonSettings?.showValuesForComparison, []],
comparisonValuesLabel: [seriesSettings.comparisonSettings?.comparisonValuesLabel, []],
color: [seriesSettings.comparisonSettings?.color, []]
})
}); });
} }
protected validatorTriggers(): string[] { protected validatorTriggers(): string[] {
return ['showInLegend', 'type']; return ['showInLegend', 'type', 'comparisonSettings.showValuesForComparison'];
} }
protected updateValidators(_emitEvent: boolean) { protected updateValidators(_emitEvent: boolean) {
const showInLegend: boolean = this.timeSeriesChartKeySettingsForm.get('showInLegend').value; const showInLegend: boolean = this.timeSeriesChartKeySettingsForm.get('showInLegend').value;
const type: TimeSeriesChartSeriesType = this.timeSeriesChartKeySettingsForm.get('type').value; const type: TimeSeriesChartSeriesType = this.timeSeriesChartKeySettingsForm.get('type').value;
const showValuesForComparison: boolean =
this.timeSeriesChartKeySettingsForm.get('comparisonSettings').get('showValuesForComparison').value;
if (showInLegend) { if (showInLegend) {
this.timeSeriesChartKeySettingsForm.get('dataHiddenByDefault').enable(); this.timeSeriesChartKeySettingsForm.get('dataHiddenByDefault').enable();
} else { } else {
@ -117,5 +127,17 @@ export class TimeSeriesChartKeySettingsComponent extends WidgetSettingsComponent
this.timeSeriesChartKeySettingsForm.get('lineSettings').disable(); this.timeSeriesChartKeySettingsForm.get('lineSettings').disable();
this.timeSeriesChartKeySettingsForm.get('barSettings').enable(); this.timeSeriesChartKeySettingsForm.get('barSettings').enable();
} }
if (this.comparisonEnabled) {
this.timeSeriesChartKeySettingsForm.get('comparisonSettings').enable({emitEvent: false});
if (showValuesForComparison) {
this.timeSeriesChartKeySettingsForm.get('comparisonSettings').get('comparisonValuesLabel').enable();
this.timeSeriesChartKeySettingsForm.get('comparisonSettings').get('color').enable();
} else {
this.timeSeriesChartKeySettingsForm.get('comparisonSettings').get('comparisonValuesLabel').disable();
this.timeSeriesChartKeySettingsForm.get('comparisonSettings').get('color').disable();
}
} else {
this.timeSeriesChartKeySettingsForm.get('comparisonSettings').disable({emitEvent: false});
}
} }
} }

40
ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-widget-settings.component.html

@ -16,6 +16,46 @@
--> -->
<ng-container [formGroup]="timeSeriesChartWidgetSettingsForm"> <ng-container [formGroup]="timeSeriesChartWidgetSettingsForm">
<div class="tb-form-panel">
<div class="tb-form-row no-border no-padding column-xs">
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="comparisonEnabled">
{{ 'widgets.time-series-chart.comparison.comparison' | translate }}
</mat-slide-toggle>
<div fxFlex fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<mat-form-field class="flex" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="timeForComparison">
<mat-option [value]="'previousInterval'">
{{ 'widgets.chart.time-for-comparison-previous-interval' | translate }}
</mat-option>
<mat-option [value]="'days'">
{{ 'widgets.chart.time-for-comparison-days' | translate }}
</mat-option>
<mat-option [value]="'weeks'">
{{ 'widgets.chart.time-for-comparison-weeks' | translate }}
</mat-option>
<mat-option [value]="'months'">
{{ 'widgets.chart.time-for-comparison-months' | translate }}
</mat-option>
<mat-option [value]="'years'">
{{ 'widgets.chart.time-for-comparison-years' | translate }}
</mat-option>
<mat-option [value]="'customInterval'">
{{ 'widgets.chart.time-for-comparison-custom-interval' | translate }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field [fxShow]="timeSeriesChartWidgetSettingsForm.get('timeForComparison').value === 'customInterval'"
appearance="outline" class="number flex-lt-md" subscriptSizing="dynamic">
<input matInput formControlName="comparisonCustomIntervalValue" type="number" min="0" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
<tb-time-series-chart-axis-settings-button
axisType="xAxis"
panelTitle="{{ 'widgets.time-series-chart.axis.comparison-x-axis-settings' | translate }}"
formControlName="comparisonXAxis">
</tb-time-series-chart-axis-settings-button>
</div>
</div>
</div>
<tb-time-series-chart-states-panel <tb-time-series-chart-states-panel
*ngIf="chartType === TimeSeriesChartType.state" *ngIf="chartType === TimeSeriesChartType.state"
formControlName="states"> formControlName="states">

20
ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-widget-settings.component.ts

@ -23,7 +23,7 @@ import {
WidgetSettings, WidgetSettings,
WidgetSettingsComponent WidgetSettingsComponent
} from '@shared/models/widget.models'; } from '@shared/models/widget.models';
import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
import { formatValue, isDefinedAndNotNull, mergeDeep } from '@core/utils'; import { formatValue, isDefinedAndNotNull, mergeDeep } from '@core/utils';
@ -114,6 +114,11 @@ export class TimeSeriesChartWidgetSettingsComponent extends WidgetSettingsCompon
protected onSettingsSet(settings: WidgetSettings) { protected onSettingsSet(settings: WidgetSettings) {
this.timeSeriesChartWidgetSettingsForm = this.fb.group({ this.timeSeriesChartWidgetSettingsForm = this.fb.group({
comparisonEnabled: [settings.comparisonEnabled, []],
timeForComparison: [settings.timeForComparison, []],
comparisonCustomIntervalValue: [settings.comparisonCustomIntervalValue, [Validators.min(0)]],
comparisonXAxis: [settings.comparisonXAxis, []],
yAxes: [settings.yAxes, []], yAxes: [settings.yAxes, []],
thresholds: [settings.thresholds, []], thresholds: [settings.thresholds, []],
@ -154,14 +159,25 @@ export class TimeSeriesChartWidgetSettingsComponent extends WidgetSettingsCompon
} }
protected validatorTriggers(): string[] { protected validatorTriggers(): string[] {
return ['showLegend', 'showTooltip', 'tooltipShowDate']; return ['comparisonEnabled', 'showLegend', 'showTooltip', 'tooltipShowDate'];
} }
protected updateValidators(emitEvent: boolean) { protected updateValidators(emitEvent: boolean) {
const comparisonEnabled: boolean = this.timeSeriesChartWidgetSettingsForm.get('comparisonEnabled').value;
const showLegend: boolean = this.timeSeriesChartWidgetSettingsForm.get('showLegend').value; const showLegend: boolean = this.timeSeriesChartWidgetSettingsForm.get('showLegend').value;
const showTooltip: boolean = this.timeSeriesChartWidgetSettingsForm.get('showTooltip').value; const showTooltip: boolean = this.timeSeriesChartWidgetSettingsForm.get('showTooltip').value;
const tooltipShowDate: boolean = this.timeSeriesChartWidgetSettingsForm.get('tooltipShowDate').value; const tooltipShowDate: boolean = this.timeSeriesChartWidgetSettingsForm.get('tooltipShowDate').value;
if (comparisonEnabled) {
this.timeSeriesChartWidgetSettingsForm.get('timeForComparison').enable();
this.timeSeriesChartWidgetSettingsForm.get('comparisonCustomIntervalValue').enable();
this.timeSeriesChartWidgetSettingsForm.get('comparisonXAxis').enable();
} else {
this.timeSeriesChartWidgetSettingsForm.get('timeForComparison').disable();
this.timeSeriesChartWidgetSettingsForm.get('comparisonCustomIntervalValue').disable();
this.timeSeriesChartWidgetSettingsForm.get('comparisonXAxis').disable();
}
if (showLegend) { if (showLegend) {
this.timeSeriesChartWidgetSettingsForm.get('legendLabelFont').enable(); this.timeSeriesChartWidgetSettingsForm.get('legendLabelFont').enable();
this.timeSeriesChartWidgetSettingsForm.get('legendLabelColor').enable(); this.timeSeriesChartWidgetSettingsForm.get('legendLabelColor').enable();

28
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-axis-settings-button.component.html

@ -0,0 +1,28 @@
<!--
Copyright © 2016-2024 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<button type="button"
mat-stroked-button
color="primary"
[disabled]="disabled"
#matButton
(click)="openAxisSettingsPopup($event, matButton)"
matTooltip="{{ panelTitle }}"
matTooltipPosition="above">
{{ (axisType === 'xAxis' ? 'widgets.time-series-chart.axis.x-axis' : 'widgets.time-series-chart.axis.y-axis') | translate }}
<tb-icon matButtonIcon>settings</tb-icon>
</button>

108
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-axis-settings-button.component.ts

@ -0,0 +1,108 @@
///
/// Copyright © 2016-2024 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Component, forwardRef, Input, OnInit, Renderer2, ViewContainerRef } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { MatButton } from '@angular/material/button';
import { TbPopoverService } from '@shared/components/popover.service';
import { coerceBoolean } from '@shared/decorators/coercion';
import { TimeSeriesChartAxisSettings } from '@home/components/widget/lib/chart/time-series-chart.models';
import {
TimeSeriesChartAxisSettingsPanelComponent
} from '@home/components/widget/lib/settings/common/chart/time-series-chart-axis-settings-panel.component';
@Component({
selector: 'tb-time-series-chart-axis-settings-button',
templateUrl: './time-series-chart-axis-settings-button.component.html',
styleUrls: [],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => TimeSeriesChartAxisSettingsButtonComponent),
multi: true
}
]
})
export class TimeSeriesChartAxisSettingsButtonComponent implements OnInit, ControlValueAccessor {
@Input()
disabled: boolean;
@Input()
axisType: 'xAxis' | 'yAxis' = 'xAxis';
@Input()
panelTitle: string;
@Input()
@coerceBoolean()
advanced = false;
private modelValue: TimeSeriesChartAxisSettings;
private propagateChange = null;
constructor(private popoverService: TbPopoverService,
private renderer: Renderer2,
private viewContainerRef: ViewContainerRef) {}
ngOnInit(): void {
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(_fn: any): void {
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
}
writeValue(value: TimeSeriesChartAxisSettings): void {
this.modelValue = value;
}
openAxisSettingsPopup($event: Event, matButton: MatButton) {
if ($event) {
$event.stopPropagation();
}
const trigger = matButton._elementRef.nativeElement;
if (this.popoverService.hasPopover(trigger)) {
this.popoverService.hidePopover(trigger);
} else {
const ctx: any = {
axisSettings: this.modelValue,
axisType: this.axisType,
panelTitle: this.panelTitle,
advanced: this.advanced
};
const axisSettingsPanelPopover = this.popoverService.displayPopover(trigger, this.renderer,
this.viewContainerRef, TimeSeriesChartAxisSettingsPanelComponent, ['leftOnly', 'leftTopOnly', 'leftBottomOnly'], true, null,
ctx,
{},
{}, {}, true);
axisSettingsPanelPopover.tbComponentRef.instance.popover = axisSettingsPanelPopover;
axisSettingsPanelPopover.tbComponentRef.instance.axisSettingsApplied.subscribe((axisSettings) => {
axisSettingsPanelPopover.hide();
this.modelValue = axisSettings;
this.propagateChange(this.modelValue);
});
}
}
}

16
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-settings-panel.component.html → ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-axis-settings-panel.component.html

@ -15,17 +15,17 @@
limitations under the License. limitations under the License.
--> -->
<div class="tb-y-axis-settings-panel" [formGroup]="yAxisSettingsFormGroup"> <div class="tb-axis-settings-panel" [formGroup]="axisSettingsFormGroup">
<div class="tb-y-axis-settings-title">{{ 'widgets.time-series-chart.axis.y-axis-settings' | translate }}</div> <div class="tb-axis-settings-title">{{ panelTitle }}</div>
<div class="tb-y-axis-settings-panel-content"> <div class="tb-axis-settings-panel-content">
<tb-time-series-chart-axis-settings <tb-time-series-chart-axis-settings
formControlName="yAxis" formControlName="axis"
alwaysExpanded alwaysExpanded
[advanced]="advanced" [advanced]="advanced"
axisType="yAxis"> [axisType]="axisType">
</tb-time-series-chart-axis-settings> </tb-time-series-chart-axis-settings>
</div> </div>
<div class="tb-y-axis-settings-panel-buttons"> <div class="tb-axis-settings-panel-buttons">
<button mat-button <button mat-button
color="primary" color="primary"
type="button" type="button"
@ -35,8 +35,8 @@
<button mat-raised-button <button mat-raised-button
color="primary" color="primary"
type="button" type="button"
(click)="applyYAxisSettings()" (click)="applyAxisSettings()"
[disabled]="yAxisSettingsFormGroup.invalid || !yAxisSettingsFormGroup.dirty"> [disabled]="axisSettingsFormGroup.invalid || !axisSettingsFormGroup.dirty">
{{ 'action.apply' | translate }} {{ 'action.apply' | translate }}
</button> </button>
</div> </div>

8
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-settings-panel.component.scss → ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-axis-settings-panel.component.scss

@ -15,7 +15,7 @@
*/ */
@import '../../../../../../../../../scss/constants'; @import '../../../../../../../../../scss/constants';
.tb-y-axis-settings-panel { .tb-axis-settings-panel {
width: 530px; width: 530px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -23,7 +23,7 @@
@media #{$mat-lt-md} { @media #{$mat-lt-md} {
width: 90vw; width: 90vw;
} }
.tb-y-axis-settings-panel-content { .tb-axis-settings-panel-content {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 16px; gap: 16px;
@ -31,14 +31,14 @@
margin: -10px; margin: -10px;
padding: 10px; padding: 10px;
} }
.tb-y-axis-settings-title { .tb-axis-settings-title {
font-size: 16px; font-size: 16px;
font-weight: 500; font-weight: 500;
line-height: 24px; line-height: 24px;
letter-spacing: 0.25px; letter-spacing: 0.25px;
color: rgba(0, 0, 0, 0.87); color: rgba(0, 0, 0, 0.87);
} }
.tb-y-axis-settings-panel-buttons { .tb-axis-settings-panel-buttons {
height: 40px; height: 40px;
display: flex; display: flex;
flex-direction: row; flex-direction: row;

37
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-settings-panel.component.ts → ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-axis-settings-panel.component.ts

@ -17,40 +17,49 @@
import { Component, EventEmitter, Input, OnInit, Output, ViewEncapsulation } from '@angular/core'; import { Component, EventEmitter, Input, OnInit, Output, ViewEncapsulation } from '@angular/core';
import { TbPopoverComponent } from '@shared/components/popover.component'; import { TbPopoverComponent } from '@shared/components/popover.component';
import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
import { TimeSeriesChartYAxisSettings } from '@home/components/widget/lib/chart/time-series-chart.models'; import {
TimeSeriesChartAxisSettings,
TimeSeriesChartYAxisSettings
} from '@home/components/widget/lib/chart/time-series-chart.models';
import { coerceBoolean } from '@shared/decorators/coercion'; import { coerceBoolean } from '@shared/decorators/coercion';
@Component({ @Component({
selector: 'tb-time-series-chart-y-axis-settings-panel', selector: 'tb-time-series-chart-axis-settings-panel',
templateUrl: './time-series-chart-y-axis-settings-panel.component.html', templateUrl: './time-series-chart-axis-settings-panel.component.html',
providers: [], providers: [],
styleUrls: ['./time-series-chart-y-axis-settings-panel.component.scss'], styleUrls: ['./time-series-chart-axis-settings-panel.component.scss'],
encapsulation: ViewEncapsulation.None encapsulation: ViewEncapsulation.None
}) })
export class TimeSeriesChartYAxisSettingsPanelComponent implements OnInit { export class TimeSeriesChartAxisSettingsPanelComponent implements OnInit {
@Input() @Input()
yAxisSettings: TimeSeriesChartYAxisSettings; axisType: 'xAxis' | 'yAxis' = 'xAxis';
@Input()
panelTitle: string;
@Input()
axisSettings: TimeSeriesChartAxisSettings;
@Input() @Input()
@coerceBoolean() @coerceBoolean()
advanced = false; advanced = false;
@Input() @Input()
popover: TbPopoverComponent<TimeSeriesChartYAxisSettingsPanelComponent>; popover: TbPopoverComponent<TimeSeriesChartAxisSettingsPanelComponent>;
@Output() @Output()
yAxisSettingsApplied = new EventEmitter<TimeSeriesChartYAxisSettings>(); axisSettingsApplied = new EventEmitter<TimeSeriesChartAxisSettings>();
yAxisSettingsFormGroup: UntypedFormGroup; axisSettingsFormGroup: UntypedFormGroup;
constructor(private fb: UntypedFormBuilder) { constructor(private fb: UntypedFormBuilder) {
} }
ngOnInit(): void { ngOnInit(): void {
this.yAxisSettingsFormGroup = this.fb.group( this.axisSettingsFormGroup = this.fb.group(
{ {
yAxis: [this.yAxisSettings, []] axis: [this.axisSettings, []]
} }
); );
} }
@ -59,8 +68,8 @@ export class TimeSeriesChartYAxisSettingsPanelComponent implements OnInit {
this.popover?.hide(); this.popover?.hide();
} }
applyYAxisSettings() { applyAxisSettings() {
const yAxisSettings = this.yAxisSettingsFormGroup.get('yAxis').getRawValue(); const axisSettings = this.axisSettingsFormGroup.get('axis').getRawValue();
this.yAxisSettingsApplied.emit(yAxisSettings); this.axisSettingsApplied.emit(axisSettings);
} }
} }

14
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-row.component.ts

@ -36,9 +36,10 @@ import { MatButton } from '@angular/material/button';
import { TbPopoverService } from '@shared/components/popover.service'; import { TbPopoverService } from '@shared/components/popover.service';
import { coerceBoolean } from '@shared/decorators/coercion'; import { coerceBoolean } from '@shared/decorators/coercion';
import { import {
TimeSeriesChartYAxisSettingsPanelComponent TimeSeriesChartAxisSettingsPanelComponent
} from '@home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-settings-panel.component'; } from '@home/components/widget/lib/settings/common/chart/time-series-chart-axis-settings-panel.component';
import { deepClone } from '@core/utils'; import { deepClone } from '@core/utils';
import { TranslateService } from '@ngx-translate/core';
@Component({ @Component({
selector: 'tb-time-series-chart-y-axis-row', selector: 'tb-time-series-chart-y-axis-row',
@ -76,6 +77,7 @@ export class TimeSeriesChartYAxisRowComponent implements ControlValueAccessor, O
private propagateChange = (_val: any) => {}; private propagateChange = (_val: any) => {};
constructor(private fb: UntypedFormBuilder, constructor(private fb: UntypedFormBuilder,
private translate: TranslateService,
private popoverService: TbPopoverService, private popoverService: TbPopoverService,
private renderer: Renderer2, private renderer: Renderer2,
private viewContainerRef: ViewContainerRef, private viewContainerRef: ViewContainerRef,
@ -143,16 +145,18 @@ export class TimeSeriesChartYAxisRowComponent implements ControlValueAccessor, O
this.popoverService.hidePopover(trigger); this.popoverService.hidePopover(trigger);
} else { } else {
const ctx: any = { const ctx: any = {
yAxisSettings: deepClone(this.modelValue), axisType: 'yAxis',
panelTitle: this.translate.instant('widgets.time-series-chart.axis.y-axis-settings'),
axisSettings: deepClone(this.modelValue),
advanced: this.advanced advanced: this.advanced
}; };
const yAxisSettingsPanelPopover = this.popoverService.displayPopover(trigger, this.renderer, const yAxisSettingsPanelPopover = this.popoverService.displayPopover(trigger, this.renderer,
this.viewContainerRef, TimeSeriesChartYAxisSettingsPanelComponent, ['leftOnly', 'leftTopOnly', 'leftBottomOnly'], true, null, this.viewContainerRef, TimeSeriesChartAxisSettingsPanelComponent, ['leftOnly', 'leftTopOnly', 'leftBottomOnly'], true, null,
ctx, ctx,
{}, {},
{}, {}, true); {}, {}, true);
yAxisSettingsPanelPopover.tbComponentRef.instance.popover = yAxisSettingsPanelPopover; yAxisSettingsPanelPopover.tbComponentRef.instance.popover = yAxisSettingsPanelPopover;
yAxisSettingsPanelPopover.tbComponentRef.instance.yAxisSettingsApplied.subscribe((yAxisSettings) => { yAxisSettingsPanelPopover.tbComponentRef.instance.axisSettingsApplied.subscribe((yAxisSettings) => {
yAxisSettingsPanelPopover.hide(); yAxisSettingsPanelPopover.hide();
this.modelValue = {...this.modelValue, ...yAxisSettings}; this.modelValue = {...this.modelValue, ...yAxisSettings};
this.axisFormGroup.patchValue( this.axisFormGroup.patchValue(

4
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/data-key-input.component.html

@ -19,6 +19,7 @@
[class.tb-suffix-absolute]="!keysFormControl.value?.length"> [class.tb-suffix-absolute]="!keysFormControl.value?.length">
<mat-chip-grid #chipList [formControl]="keysFormControl"> <mat-chip-grid #chipList [formControl]="keysFormControl">
<mat-chip-row class="tb-datakey-chip" *ngIf="modelValue?.type" <mat-chip-row class="tb-datakey-chip" *ngIf="modelValue?.type"
[removable]="removable"
(removed)="removeKey()"> (removed)="removeKey()">
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="4px" class="tb-attribute-chip"> <div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="4px" class="tb-attribute-chip">
<div class="tb-chip-labels"> <div class="tb-chip-labels">
@ -49,7 +50,8 @@
(click)="editKey()" mat-icon-button class="tb-mat-24"> (click)="editKey()" mat-icon-button class="tb-mat-24">
<mat-icon class="tb-mat-18">edit</mat-icon> <mat-icon class="tb-mat-18">edit</mat-icon>
</button> </button>
<button matChipRemove <button *ngIf="removable"
matChipRemove
type="button" type="button"
mat-icon-button class="tb-mat-24"> mat-icon-button class="tb-mat-24">
<mat-icon class="tb-mat-18">close</mat-icon> <mat-icon class="tb-mat-18">close</mat-icon>

7
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/data-key-input.component.scss

@ -16,6 +16,13 @@
.tb-data-key-input { .tb-data-key-input {
.mat-mdc-form-field.tb-inline-field.tb-key-field { .mat-mdc-form-field.tb-inline-field.tb-key-field {
width: 100%; width: 100%;
&.mat-form-field-appearance-fill {
.mdc-text-field--filled.mdc-text-field--disabled {
&:before {
opacity: 0;
}
}
}
.mat-mdc-text-field-wrapper:not(.mdc-text-field--outlined) { .mat-mdc-text-field-wrapper:not(.mdc-text-field--outlined) {
padding-left: 8px; padding-left: 8px;
padding-right: 0; padding-right: 0;

4
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/data-key-input.component.ts

@ -98,6 +98,10 @@ export class DataKeyInputComponent implements ControlValueAccessor, OnInit, OnCh
@coerceBoolean() @coerceBoolean()
editable = true; editable = true;
@Input()
@coerceBoolean()
removable = true;
@Input() @Input()
datasourceType: DatasourceType; datasourceType: DatasourceType;

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

@ -116,8 +116,8 @@ import {
TimeSeriesChartYAxisRowComponent TimeSeriesChartYAxisRowComponent
} from '@home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-row.component'; } from '@home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-row.component';
import { import {
TimeSeriesChartYAxisSettingsPanelComponent TimeSeriesChartAxisSettingsPanelComponent
} from '@home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-settings-panel.component'; } from '@home/components/widget/lib/settings/common/chart/time-series-chart-axis-settings-panel.component';
import { import {
TimeSeriesChartAnimationSettingsComponent TimeSeriesChartAnimationSettingsComponent
} from '@home/components/widget/lib/settings/common/chart/time-series-chart-animation-settings.component'; } from '@home/components/widget/lib/settings/common/chart/time-series-chart-animation-settings.component';
@ -139,6 +139,9 @@ import {
import { import {
TimeSeriesChartStatesPanelComponent TimeSeriesChartStatesPanelComponent
} from '@home/components/widget/lib/settings/common/chart/time-series-chart-states-panel.component'; } from '@home/components/widget/lib/settings/common/chart/time-series-chart-states-panel.component';
import {
TimeSeriesChartAxisSettingsButtonComponent
} from '@home/components/widget/lib/settings/common/chart/time-series-chart-axis-settings-button.component';
@NgModule({ @NgModule({
declarations: [ declarations: [
@ -184,7 +187,8 @@ import {
TimeSeriesNoAggregationBarWidthSettingsComponent, TimeSeriesNoAggregationBarWidthSettingsComponent,
TimeSeriesChartYAxesPanelComponent, TimeSeriesChartYAxesPanelComponent,
TimeSeriesChartYAxisRowComponent, TimeSeriesChartYAxisRowComponent,
TimeSeriesChartYAxisSettingsPanelComponent, TimeSeriesChartAxisSettingsPanelComponent,
TimeSeriesChartAxisSettingsButtonComponent,
TimeSeriesChartAnimationSettingsComponent, TimeSeriesChartAnimationSettingsComponent,
TimeSeriesChartFillSettingsComponent, TimeSeriesChartFillSettingsComponent,
TimeSeriesChartThresholdSettingsComponent, TimeSeriesChartThresholdSettingsComponent,
@ -241,7 +245,8 @@ import {
TimeSeriesNoAggregationBarWidthSettingsComponent, TimeSeriesNoAggregationBarWidthSettingsComponent,
TimeSeriesChartYAxesPanelComponent, TimeSeriesChartYAxesPanelComponent,
TimeSeriesChartYAxisRowComponent, TimeSeriesChartYAxisRowComponent,
TimeSeriesChartYAxisSettingsPanelComponent, TimeSeriesChartAxisSettingsPanelComponent,
TimeSeriesChartAxisSettingsButtonComponent,
TimeSeriesChartAnimationSettingsComponent, TimeSeriesChartAnimationSettingsComponent,
TimeSeriesChartFillSettingsComponent, TimeSeriesChartFillSettingsComponent,
TimeSeriesChartThresholdSettingsComponent, TimeSeriesChartThresholdSettingsComponent,

2
ui-ngx/src/app/modules/home/models/widget-component.models.ts

@ -103,6 +103,7 @@ import { UserId } from '@shared/models/id/user-id';
import { UserSettingsService } from '@core/http/user-settings.service'; import { UserSettingsService } from '@core/http/user-settings.service';
import { DynamicComponentModule } from '@core/services/dynamic-component-factory.service'; import { DynamicComponentModule } from '@core/services/dynamic-component-factory.service';
import { DataKeySettingsFunction } from '@home/components/widget/config/data-keys.component.models'; import { DataKeySettingsFunction } from '@home/components/widget/config/data-keys.component.models';
import { UtilsService } from '@core/services/utils.service';
export interface IWidgetAction { export interface IWidgetAction {
name: string; name: string;
@ -194,6 +195,7 @@ export class WidgetContext {
customDialog: CustomDialogService; customDialog: CustomDialogService;
resourceService: ResourceService; resourceService: ResourceService;
userSettingsService: UserSettingsService; userSettingsService: UserSettingsService;
utilsService: UtilsService;
telemetryWsService: TelemetryWebsocketService; telemetryWsService: TelemetryWebsocketService;
telemetrySubscribers?: TelemetrySubscriber[]; telemetrySubscribers?: TelemetrySubscriber[];
date: DatePipe; date: DatePipe;

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

@ -21,7 +21,6 @@ import { AggregationType, ComparisonDuration, Timewindow } from '@shared/models/
import { EntityType } from '@shared/models/entity-type.models'; import { EntityType } from '@shared/models/entity-type.models';
import { DataKeyType } from './telemetry/telemetry.models'; import { DataKeyType } from './telemetry/telemetry.models';
import { EntityId } from '@shared/models/id/entity-id'; import { EntityId } from '@shared/models/id/entity-id';
import * as moment_ from 'moment';
import { import {
AlarmFilter, AlarmFilter,
AlarmFilterConfig, AlarmFilterConfig,
@ -710,10 +709,23 @@ export const defaultWidgetAction = (setEntityId = true): WidgetAction => ({
export interface WidgetComparisonSettings { export interface WidgetComparisonSettings {
comparisonEnabled?: boolean; comparisonEnabled?: boolean;
timeForComparison?: moment_.unitOfTime.DurationConstructor; timeForComparison?: ComparisonDuration;
comparisonCustomIntervalValue?: number; comparisonCustomIntervalValue?: number;
} }
export interface DataKeyComparisonSettings {
showValuesForComparison: boolean;
comparisonValuesLabel: string;
color: string;
}
export interface DataKeySettingsWithComparison {
comparisonSettings?: DataKeyComparisonSettings;
}
export const isDataKeySettingsWithComparison = (settings: any): settings is DataKeySettingsWithComparison =>
'comparisonSettings' in settings;
export interface WidgetSettings { export interface WidgetSettings {
[key: string]: any; [key: string]: any;
} }

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

@ -6749,6 +6749,15 @@
"bar-width": "Bar width", "bar-width": "Bar width",
"bar-width-relative": "Percentage of time window", "bar-width-relative": "Percentage of time window",
"bar-width-absolute": "Absolute (ms)", "bar-width-absolute": "Absolute (ms)",
"comparison": {
"comparison": "Comparison",
"show": "Show",
"settings": "Comparison settings",
"show-values-for-comparison": "Show historical data for comparison",
"comparison-values-label": "Comparison key label",
"comparison-values-label-auto": "Auto",
"comparison-data-color": "Comparison data color"
},
"threshold": { "threshold": {
"thresholds": "Thresholds", "thresholds": "Thresholds",
"source": "Source", "source": "Source",
@ -6802,6 +6811,7 @@
"x-axis": "X axis", "x-axis": "X axis",
"y-axis": "Y axis", "y-axis": "Y axis",
"y-axis-settings": "Y axis settings", "y-axis-settings": "Y axis settings",
"comparison-x-axis-settings": "Comparison X axis settings",
"remove-y-axis": "Remove Y axis", "remove-y-axis": "Remove Y axis",
"id": "Id", "id": "Id",
"label": "Label", "label": "Label",

12
ui-ngx/src/form.scss

@ -216,6 +216,18 @@
flex: 1; flex: 1;
width: auto; width: auto;
} }
&.flex-xs {
@media #{$mat-xs} {
width: auto;
flex: 1;
}
}
&.flex-lt-md {
@media #{$mat-lt-md} {
width: auto;
flex: 1;
}
}
} }
.fixed-title-width { .fixed-title-width {
min-width: 200px; min-width: 200px;

Loading…
Cancel
Save