Browse Source
Added support for dynamic y axis limits determination for time series chartspull/14573/head
committed by
GitHub
20 changed files with 783 additions and 47 deletions
@ -0,0 +1,71 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
Copyright © 2016-2025 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]="limitForm" class="tb-form-table-row overflow-hidden items-start"> |
||||
|
<div class="flex-shrink capitalize" [class]="isPanelView ? 'basis-10' : 'basis-1/5'"> |
||||
|
{{ labelKey | translate}} |
||||
|
</div> |
||||
|
<div class="flex-grow flex-shrink flex flex-col gap-2 gt-sm:flex-row gt-sm:gap-3" [class]="isPanelView ? 'basis-1/2' : 'basis-40%'"> |
||||
|
<mat-form-field class="tb-inline-field flex-1" appearance="outline" subscriptSizing="dynamic"> |
||||
|
<mat-select formControlName="type"> |
||||
|
<mat-option *ngFor="let type of ValueSourceTypes" [value]="type"> |
||||
|
{{ ValueSourceTypeTranslation.get(type) | translate }} |
||||
|
</mat-option> |
||||
|
</mat-select> |
||||
|
</mat-form-field> |
||||
|
<tb-entity-alias-input |
||||
|
required |
||||
|
class="flex-1" |
||||
|
*ngIf="limitForm.get('type').value === ValueSourceType.entity" |
||||
|
[aliasController]="aliasController" |
||||
|
formControlName="entityAlias"> |
||||
|
</tb-entity-alias-input> |
||||
|
</div> |
||||
|
<div class="flex-grow flex-shrink max-w-5/12" [class]="isPanelView ? 'basis-1/2' : 'basis-40%'"> |
||||
|
<mat-form-field *ngIf="limitForm.get('type').value === ValueSourceType.constant" |
||||
|
appearance="outline" class="tb-inline-field !w-full number" subscriptSizing="dynamic" |
||||
|
[class.tb-suffix-absolute]="!limitForm.get('value').value"> |
||||
|
<input matInput formControlName="value" type="number" placeholder="{{ 'widgets.chart.chart-axis.scale-auto' | translate }}"> |
||||
|
</mat-form-field> |
||||
|
<tb-data-key-input |
||||
|
required |
||||
|
*ngIf="limitForm.get('type').value === ValueSourceType.latestKey" |
||||
|
requiredText="widgets.chart.chart-axis.key-required" |
||||
|
[datasourceType]="datasource?.type" |
||||
|
[entityAliasId]="datasource?.entityAliasId" |
||||
|
[deviceId]="datasource?.deviceId" |
||||
|
[aliasController]="aliasController" |
||||
|
[dataKeyType]="datasource?.type === DatasourceType.function ? DataKeyType.function : null" |
||||
|
[dataKeyTypes]="[DataKeyType.attribute, DataKeyType.timeseries]" |
||||
|
[callbacks]="callbacks" |
||||
|
[editable]="false" |
||||
|
[formControl]="latestKeyFormControl"> |
||||
|
</tb-data-key-input> |
||||
|
<tb-data-key-input |
||||
|
required |
||||
|
*ngIf="limitForm.get('type').value === ValueSourceType.entity" |
||||
|
requiredText="widgets.chart.chart-axis.entity-key-required" |
||||
|
[datasourceType]="DatasourceType.entity" |
||||
|
[aliasController]="aliasController" |
||||
|
[entityAlias]="limitForm.get('entityAlias').value" |
||||
|
[dataKeyTypes]="[DataKeyType.attribute, DataKeyType.timeseries]" |
||||
|
[callbacks]="callbacks" |
||||
|
[editable]="false" |
||||
|
[formControl]="entityKeyFormControl"> |
||||
|
</tb-data-key-input> |
||||
|
</div> |
||||
|
</div> |
||||
@ -0,0 +1,209 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2025 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, DestroyRef, forwardRef, Input, OnInit } from '@angular/core'; |
||||
|
import { |
||||
|
ControlValueAccessor, NG_VALIDATORS, |
||||
|
NG_VALUE_ACCESSOR, |
||||
|
UntypedFormBuilder, UntypedFormControl, |
||||
|
UntypedFormGroup, ValidationErrors, Validator, |
||||
|
Validators, |
||||
|
} from '@angular/forms'; |
||||
|
import { |
||||
|
ValueSourceConfig, |
||||
|
ValueSourceType, |
||||
|
ValueSourceTypes, |
||||
|
ValueSourceTypeTranslation |
||||
|
} from '@shared/models/widget-settings.models'; |
||||
|
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; |
||||
|
import { DataKey, Datasource, DatasourceType, } from '@shared/models/widget.models'; |
||||
|
import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; |
||||
|
import { IAliasController } from '@core/api/widget-api.models'; |
||||
|
import { DataKeysCallbacks } from '@home/components/widget/lib/settings/common/key/data-keys.component.models'; |
||||
|
import { merge } from 'rxjs'; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-axis-scale-row', |
||||
|
templateUrl: './axis-scale-row.component.html', |
||||
|
providers: [ |
||||
|
{ |
||||
|
provide: NG_VALUE_ACCESSOR, |
||||
|
useExisting: forwardRef(() => AxisScaleRowComponent), |
||||
|
multi: true |
||||
|
}, |
||||
|
{ |
||||
|
provide: NG_VALIDATORS, |
||||
|
useExisting: forwardRef(() => AxisScaleRowComponent), |
||||
|
multi: true |
||||
|
}, |
||||
|
] |
||||
|
}) |
||||
|
export class AxisScaleRowComponent implements ControlValueAccessor, OnInit, Validator { |
||||
|
|
||||
|
@Input() |
||||
|
isPanelView = false; |
||||
|
|
||||
|
@Input() |
||||
|
aliasController: IAliasController; |
||||
|
|
||||
|
@Input() |
||||
|
callbacks: DataKeysCallbacks; |
||||
|
|
||||
|
@Input() |
||||
|
datasource: Datasource; |
||||
|
|
||||
|
@Input() |
||||
|
labelKey: string; |
||||
|
|
||||
|
ValueSourceType = ValueSourceType; |
||||
|
|
||||
|
DataKeyType = DataKeyType; |
||||
|
|
||||
|
DatasourceType = DatasourceType; |
||||
|
|
||||
|
ValueSourceTypeTranslation = ValueSourceTypeTranslation; |
||||
|
|
||||
|
ValueSourceTypes = ValueSourceTypes; |
||||
|
|
||||
|
limitForm: UntypedFormGroup; |
||||
|
|
||||
|
latestKeyFormControl: UntypedFormControl; |
||||
|
|
||||
|
entityKeyFormControl: UntypedFormControl; |
||||
|
|
||||
|
private propagateChanges: (value: any) => void = () => {}; |
||||
|
|
||||
|
private modelValue: ValueSourceConfig | null = null; |
||||
|
|
||||
|
constructor(private fb: UntypedFormBuilder, |
||||
|
private destroyRef: DestroyRef) { |
||||
|
} |
||||
|
|
||||
|
ngOnInit() { |
||||
|
this.limitForm = this.fb.group({ |
||||
|
type: [ValueSourceType.constant], |
||||
|
value: [null], |
||||
|
entityAlias: [null] |
||||
|
}); |
||||
|
this.latestKeyFormControl = this.fb.control(null, [Validators.required]); |
||||
|
this.entityKeyFormControl = this.fb.control(null, [Validators.required]); |
||||
|
merge( |
||||
|
this.latestKeyFormControl.valueChanges, |
||||
|
this.entityKeyFormControl.valueChanges, |
||||
|
this.limitForm.valueChanges |
||||
|
).pipe(takeUntilDestroyed(this.destroyRef)) |
||||
|
.subscribe(() => { |
||||
|
this.updateValidators(); |
||||
|
this.updateModel(); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
writeValue(value: ValueSourceConfig) { |
||||
|
this.modelValue = value; |
||||
|
this.limitForm.patchValue( |
||||
|
{ |
||||
|
type: value.type || ValueSourceType.constant, |
||||
|
value: value.value, |
||||
|
entityAlias: value.entityAlias, |
||||
|
}, {emitEvent: false} |
||||
|
); |
||||
|
if (value.type === ValueSourceType.latestKey) { |
||||
|
this.latestKeyFormControl.patchValue({ |
||||
|
type: value.latestKeyType, |
||||
|
name: value.latestKey |
||||
|
}, {emitEvent: false}); |
||||
|
} else if (value.type === ValueSourceType.entity) { |
||||
|
this.entityKeyFormControl.patchValue({ |
||||
|
type: value.entityKeyType, |
||||
|
name: value.entityKey |
||||
|
}, {emitEvent: false}); |
||||
|
} |
||||
|
|
||||
|
this.updateValidators(); |
||||
|
this.limitForm.markAllAsTouched(); |
||||
|
} |
||||
|
|
||||
|
registerOnChange(fn: any) { |
||||
|
this.propagateChanges = fn; |
||||
|
} |
||||
|
|
||||
|
registerOnTouched(fn: any) { |
||||
|
} |
||||
|
|
||||
|
validate(): ValidationErrors | null { |
||||
|
const type = this.limitForm.get('type')?.value; |
||||
|
const errors: any = {}; |
||||
|
|
||||
|
if (this.limitForm.invalid) { |
||||
|
errors.form = false; |
||||
|
} |
||||
|
|
||||
|
if (type === ValueSourceType.latestKey) { |
||||
|
if (!this.latestKeyFormControl.value || this.latestKeyFormControl.invalid) { |
||||
|
errors.latestKey = false; |
||||
|
} |
||||
|
} else if (type === ValueSourceType.entity) { |
||||
|
if (!this.limitForm.get('entityAlias')?.value) { |
||||
|
errors.entityAlias = false; |
||||
|
} |
||||
|
if (!this.entityKeyFormControl.value || this.entityKeyFormControl.invalid) { |
||||
|
errors.entityKey = false; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return Object.keys(errors).length ? { axisLimitForm: errors } : null; |
||||
|
} |
||||
|
|
||||
|
private updateValidators() { |
||||
|
const axisTypeControl = this.limitForm.get('type'); |
||||
|
if (axisTypeControl && this.entityKeyFormControl && this.latestKeyFormControl) { |
||||
|
const type = axisTypeControl.value; |
||||
|
if (type === ValueSourceType.latestKey) { |
||||
|
this.latestKeyFormControl.setValidators([Validators.required]); |
||||
|
this.entityKeyFormControl.clearValidators(); |
||||
|
} else if (type === ValueSourceType.entity) { |
||||
|
this.latestKeyFormControl.clearValidators(); |
||||
|
this.limitForm.get('entityAlias').setValidators([Validators.required]); |
||||
|
this.entityKeyFormControl.setValidators([Validators.required]); |
||||
|
} else { |
||||
|
this.latestKeyFormControl.clearValidators(); |
||||
|
this.entityKeyFormControl.clearValidators(); |
||||
|
} |
||||
|
this.latestKeyFormControl.updateValueAndValidity({ emitEvent: false }); |
||||
|
this.entityKeyFormControl.updateValueAndValidity({ emitEvent: false }); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private updateModel() { |
||||
|
const value = this.limitForm.value; |
||||
|
const type = value.type; |
||||
|
let updates: Partial<ValueSourceConfig> = { type }; |
||||
|
if (type === ValueSourceType.latestKey) { |
||||
|
const latestKey: DataKey = this.latestKeyFormControl.value; |
||||
|
updates.latestKey = latestKey?.name; |
||||
|
updates.latestKeyType = latestKey?.type as any; |
||||
|
} else if (type === ValueSourceType.entity) { |
||||
|
const entityKey: DataKey = this.entityKeyFormControl.value; |
||||
|
updates.entityKey = entityKey?.name; |
||||
|
updates.entityKeyType = entityKey?.type as any; |
||||
|
updates.entityAlias = value?.entityAlias; |
||||
|
} else { |
||||
|
updates.value = value?.value; |
||||
|
} |
||||
|
this.modelValue = updates as ValueSourceConfig; |
||||
|
this.propagateChanges(this.modelValue); |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue