diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/bar-chart-with-labels-basic-config.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/bar-chart-with-labels-basic-config.component.html index d468585ed2..01e967a8f1 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/bar-chart-with-labels-basic-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/bar-chart-with-labels-basic-config.component.html @@ -180,6 +180,9 @@
widgets.time-series-chart.axis.y-axis
diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/range-chart-basic-config.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/range-chart-basic-config.component.html index 66939ce4f4..56e40845f7 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/range-chart-basic-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/range-chart-basic-config.component.html @@ -242,6 +242,9 @@
widgets.time-series-chart.axis.y-axis
diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/time-series-chart-basic-config.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/time-series-chart-basic-config.component.html index b358ab5087..fa1e8c432f 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/time-series-chart-basic-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/time-series-chart-basic-config.component.html @@ -103,6 +103,9 @@ formControlName="states"> diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.models.ts index 301698cd21..0ce6a4b290 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.models.ts @@ -381,8 +381,8 @@ export interface TimeSeriesChartYAxisSettings extends TimeSeriesChartAxisSetting decimals?: number; interval?: number; splitNumber?: number; - min?: number | string; - max?: number | string; + min?: number | string | ValueSourceConfig; + max?: number | string | ValueSourceConfig; ticksGenerator?: TimeSeriesChartTicksGenerator | string; ticksFormatter?: TimeSeriesChartTicksFormatter | string; } @@ -867,6 +867,9 @@ export interface TimeSeriesChartAxis { id: string; settings: TimeSeriesChartAxisSettings; option: CartesianAxisOption; + minLatestDataKey?: DataKey; + maxLatestDataKey?: DataKey; + unitConvertor?: (value: number) => number; } export interface TimeSeriesChartYAxis extends TimeSeriesChartAxis { @@ -926,6 +929,29 @@ export const createTimeSeriesYAxis = (units: string, return ticks?.filter(tick => tick.value >= extent[0] && tick.value <= extent[1]); }; } + + let initialMin: number | string | undefined; + if (isDefinedAndNotNull(settings.min)) { + if (typeof settings.min === 'object' && 'type' in settings.min) { + initialMin = undefined; + } else if (typeof settings.min === 'number') { + initialMin = unitConvertor ? unitConvertor(settings.min) : settings.min; + } else if (typeof settings.min === 'string') { + initialMin = settings.min; + } + } + + let initialMax: number | string | undefined; + if (isDefinedAndNotNull(settings.max)) { + if (typeof settings.max === 'object' && 'type' in settings.max) { + initialMax = undefined; + } else if (typeof settings.max === 'number') { + initialMax = unitConvertor ? unitConvertor(settings.max) : settings.max; + } else if (typeof settings.max === 'string') { + initialMax = settings.max; + } + } + return { id: settings.id, decimals, @@ -939,8 +965,8 @@ export const createTimeSeriesYAxis = (units: string, offset: 0, alignTicks: true, scale: true, - min: isDefinedAndNotNull(settings.min) ? unitConvertor(Number(settings.min)) : settings.min, - max: isDefinedAndNotNull(settings.max) ? unitConvertor(Number(settings.max)) : settings.max, + min: initialMin, + max: initialMax, minInterval, splitNumber, interval, diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.ts index d447b51b37..8568e4b13b 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.ts @@ -54,13 +54,15 @@ import { getFocusedSeriesIndex, measureAxisNameSize } from '@home/components/widget/lib/chart/echarts-widget.models'; -import { DateFormatProcessor, ValueSourceType } from '@shared/models/widget-settings.models'; +import { DateFormatProcessor, ValueSourceConfig, ValueSourceType } from '@shared/models/widget-settings.models'; import { formattedDataFormDatasourceData, formatValue, isDefined, isDefinedAndNotNull, isEqual, + isNumber, + isString, mergeDeep } from '@core/utils'; import { DataKey, Datasource, DatasourceType, FormattedData, widgetType } from '@shared/models/widget.models'; @@ -286,9 +288,36 @@ export class TbTimeSeriesChart { } } } + + for (const yAxis of this.yAxisList) { + const minType = (yAxis.settings.min as ValueSourceConfig).type; + const maxType = (yAxis.settings.max as ValueSourceConfig).type; + if (minType === ValueSourceType.latestKey && yAxis.minLatestDataKey) { + const data = this.ctx.latestData.find(d => d.dataKey === yAxis.minLatestDataKey); + if (data?.data[0]) { + const value = this.parseAxisLimitData(data.data[0][1], yAxis.unitConvertor); + if (yAxis.option.min !== value) { + yAxis.option.min = value; + update = true; + } + } + } + + if (maxType === ValueSourceType.latestKey && yAxis.maxLatestDataKey) { + const data = this.ctx.latestData.find(d => d.dataKey === yAxis.maxLatestDataKey); + if (data?.data[0]) { + const value = this.parseAxisLimitData(data.data[0][1], yAxis.unitConvertor); + if (yAxis.option.max !== value) { + yAxis.option.max = value; + update = true; + } + } + } + } } if (this.timeSeriesChart && update) { this.updateSeriesData(); + this.updateAxisLimits(); } } @@ -550,6 +579,7 @@ export class TbTimeSeriesChart { private setupYAxes(): void { const yAxisSettingsList = Object.values(this.settings.yAxes); yAxisSettingsList.sort((a1, a2) => a1.order - a2.order); + const axisLimitDatasources: Datasource[] = []; for (const yAxisSettings of yAxisSettingsList) { const axisSettings = mergeDeep({} as TimeSeriesChartYAxisSettings, defaultTimeSeriesChartYAxisSettings, yAxisSettings); @@ -563,8 +593,81 @@ export class TbTimeSeriesChart { axisSettings.ticksFormatter = this.stateValueConverter.ticksFormatter; } const yAxis = createTimeSeriesYAxis(unitSymbol, decimals, axisSettings, this.ctx.utilsService, this.darkMode, unitConvertor); + if (isDefinedAndNotNull(axisSettings.min)) { + this.processYAxisLimit(axisSettings.min as ValueSourceConfig, 'min', yAxis, axisLimitDatasources, unitConvertor); + } + if (isDefinedAndNotNull(axisSettings.max)) { + this.processYAxisLimit(axisSettings.max as ValueSourceConfig, 'max', yAxis, axisLimitDatasources, unitConvertor); + } this.yAxisList.push(yAxis); } + this.subscribeForAxisLimits(axisLimitDatasources); + } + + private processYAxisLimit( + limit: ValueSourceConfig, + limitType: 'min' | 'max', + yAxis: TimeSeriesChartYAxis, + axisLimitDatasources: Datasource[], + unitConvertor?: (value: number) => number + ): void { + if (limit && typeof limit === 'object' && 'type' in limit) { + if (limit.type === ValueSourceType.latestKey) { + let latestDataKey: DataKey = null; + if (this.ctx.datasources.length) { + for (const datasource of this.ctx.datasources) { + latestDataKey = datasource.latestDataKeys?.find(d => + (d.type === DataKeyType.function && d.label === limit.latestKey) || + (d.type !== DataKeyType.function && d.name === limit.latestKey && + d.type === limit.latestKeyType)); + if (latestDataKey) { + break; + } + } + } + if (latestDataKey) { + if (limitType === 'min') { + yAxis.minLatestDataKey = latestDataKey; + } else { + yAxis.maxLatestDataKey = latestDataKey; + } + } + } else if (limit.type === ValueSourceType.entity) { + const entityAliasId = this.ctx.aliasController.getEntityAliasId(limit.entityAlias); + if (entityAliasId) { + let datasource = axisLimitDatasources.find(d => d.entityAliasId === entityAliasId); + const entityDataKey: DataKey = { + type: limit.entityKeyType, + name: limit.entityKey, + label: limit.entityKey, + settings: { + yAxisId: yAxis.id, + axisLimit: limitType + } + }; + if (datasource) { + datasource.dataKeys.push(entityDataKey); + } else { + datasource = { + type: DatasourceType.entity, + name: limit.entityAlias, + aliasName: limit.entityAlias, + entityAliasId, + dataKeys: [entityDataKey] + }; + axisLimitDatasources.push(datasource); + } + } + } else if (limit.type === ValueSourceType.constant) { + const value = unitConvertor ? unitConvertor(limit.value) : limit.value; + if (limitType === 'min') { + yAxis.option.min = value; + } else { + yAxis.option.max = value; + } + } + return; + } } private setupVisualMap(): void { @@ -622,6 +725,50 @@ export class TbTimeSeriesChart { } } + private subscribeForAxisLimits(datasources: Datasource[]) { + if (datasources.length) { + const axisLimitsSubscriptionOptions: WidgetSubscriptionOptions = { + datasources, + useDashboardTimewindow: false, + type: widgetType.latest, + callbacks: { + onDataUpdated: (subscription) => { + let update = false; + if (subscription.data) { + for (const yAxis of this.yAxisList) { + for (const data of subscription.data) { + if (data.dataKey.settings?.yAxisId === yAxis.id) { + const limitType = data.dataKey.settings.axisLimit as ('min' | 'max'); + if (data.data[0]) { + const value = this.parseAxisLimitData(data.data[0][1], yAxis.unitConvertor); + if (isDefinedAndNotNull(value)) { + if (limitType === 'min') { + if (yAxis.option.min !== value) { + yAxis.option.min = value; + update = true; + } + } else { + if (yAxis.option.max !== value) { + yAxis.option.max = value; + update = true; + } + } + } + } + } + } + } + } + if (this.timeSeriesChart && update) { + this.updateAxisLimits(); + } + } + } + }; + this.ctx.subscriptionApi.createSubscription(axisLimitsSubscriptionOptions, true).subscribe(); + } + } + private drawChart() { echartsModule.init(); this.renderer.setStyle(this.chartElement, 'letterSpacing', 'normal'); @@ -714,6 +861,24 @@ export class TbTimeSeriesChart { } } + private parseAxisLimitData = (data: any, unitConvertor?: (value: number) => number): number => { + let value: number; + if (isDefinedAndNotNull(data)) { + if (isNumber(data)) { + value = data; + } else if (isString(data)) { + value = Number(data); + } + } + if (isDefinedAndNotNull(value) && !isNaN(value)) { + if (unitConvertor) { + return unitConvertor(value); + } + return value; + } + return null; + }; + private updateSeries(): void { this.timeSeriesChartOptions.series = generateChartData(this.dataItems, this.thresholdItems, this.stackMode, @@ -836,6 +1001,16 @@ export class TbTimeSeriesChart { return changed; } + private updateAxisLimits(): void { + if (this.timeSeriesChart && !this.timeSeriesChart.isDisposed()) { + this.timeSeriesChartOptions.yAxis = this.yAxisList.map(axis => axis.option); + this.timeSeriesChart.setOption(this.timeSeriesChartOptions, { + replaceMerge: ['yAxis'] + }); + this.updateAxes(); + } + } + private scaleYAxis(yAxis: TimeSeriesChartYAxis): boolean { if (!this.stateData) { const axisBarDataItems = this.dataItems.filter(d => d.yAxisId === yAxis.id && d.enabled && diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/bar-chart-with-labels-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/bar-chart-with-labels-widget-settings.component.html index 488a08e161..08dd087cdb 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/bar-chart-with-labels-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/bar-chart-with-labels-widget-settings.component.html @@ -91,6 +91,9 @@
widgets.time-series-chart.axis.y-axis
widgets.time-series-chart.axis.y-axis diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-widget-settings.component.html index 9a80362c8d..afa3c774ec 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-widget-settings.component.html @@ -62,6 +62,9 @@ diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/axis-scale-row.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/axis-scale-row.component.html new file mode 100644 index 0000000000..0129bdc490 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/axis-scale-row.component.html @@ -0,0 +1,71 @@ + +
+
+ {{ labelKey | translate}} +
+
+ + + + {{ ValueSourceTypeTranslation.get(type) | translate }} + + + + + +
+
+ + + + + + + +
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/axis-scale-row.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/axis-scale-row.component.ts new file mode 100644 index 0000000000..f01336d22e --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/axis-scale-row.component.ts @@ -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 = { 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); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-axis-settings-panel.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-axis-settings-panel.component.html index a462c92c03..bd6e8e888a 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-axis-settings-panel.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-axis-settings-panel.component.html @@ -19,6 +19,9 @@
{{ panelTitle }}
+
+
+
widgets.chart.chart-axis.scale-limits
+
+
+
widgets.chart.chart-axis.limit
+
widgets.chart.chart-axis.source
+
widgets.chart.chart-axis.key-value
+
+
+ + + + + +
+
+
+
-
-
-
widgets.chart.chart-axis.scale
-
-
widgets.chart.chart-axis.scale-min
- - - -
widgets.chart.chart-axis.scale-max
- - - -
-
-
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-axis-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-axis-settings.component.ts index 40f2a96c69..9bed5d6e7c 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-axis-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-axis-settings.component.ts @@ -17,9 +17,11 @@ import { Component, DestroyRef, forwardRef, Input, OnInit } from '@angular/core'; import { ControlValueAccessor, + NG_VALIDATORS, NG_VALUE_ACCESSOR, UntypedFormBuilder, UntypedFormGroup, + ValidationErrors, Validator, Validators } from '@angular/forms'; import { @@ -32,6 +34,9 @@ import { merge } from 'rxjs'; import { coerceBoolean } from '@shared/decorators/coercion'; import { WidgetService } from '@core/http/widget.service'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { IAliasController } from '@app/core/public-api'; +import { Datasource } from '@app/shared/public-api'; +import { DataKeysCallbacks } from '@home/components/widget/lib/settings/common/key/data-keys.component.models'; @Component({ selector: 'tb-time-series-chart-axis-settings', @@ -42,10 +47,15 @@ import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => TimeSeriesChartAxisSettingsComponent), multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => TimeSeriesChartAxisSettingsComponent), + multi: true } ] }) -export class TimeSeriesChartAxisSettingsComponent implements OnInit, ControlValueAccessor { +export class TimeSeriesChartAxisSettingsComponent implements OnInit, ControlValueAccessor, Validator { @Input() @coerceBoolean() @@ -61,6 +71,15 @@ export class TimeSeriesChartAxisSettingsComponent implements OnInit, ControlValu defaultXAxisTicksFormat = defaultXAxisTicksFormat; + @Input() + aliasController: IAliasController; + + @Input() + dataKeyCallbacks: DataKeysCallbacks; + + @Input() + datasource: Datasource; + @Input() disabled: boolean; @@ -147,6 +166,12 @@ export class TimeSeriesChartAxisSettingsComponent implements OnInit, ControlValu registerOnTouched(_fn: any): void { } + validate(): ValidationErrors | null { + return this.axisSettingsFormGroup.valid ? null : { + axisSettings: false + }; + } + setDisabledState(isDisabled: boolean): void { this.disabled = isDisabled; if (isDisabled) { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axes-panel.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axes-panel.component.ts index 12ac24f9e3..4d710a6ff4 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axes-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axes-panel.component.ts @@ -38,7 +38,8 @@ import { import { defaultTimeSeriesChartYAxisSettings, getNextTimeSeriesYAxisId, - TimeSeriesChartYAxes, TimeSeriesChartYAxisId, + TimeSeriesChartYAxes, + TimeSeriesChartYAxisId, TimeSeriesChartYAxisSettings, timeSeriesChartYAxisValid, timeSeriesChartYAxisValidator @@ -47,6 +48,9 @@ import { mergeDeep } from '@core/utils'; import { CdkDragDrop } from '@angular/cdk/drag-drop'; import { coerceBoolean } from '@shared/decorators/coercion'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { IAliasController } from '@app/core/public-api'; +import { DataKeysCallbacks } from '@home/components/widget/lib/settings/common/key/data-keys.component.models'; +import { DataKey, DataKeyType, Datasource, ValueSourceConfig, ValueSourceType } from '@app/shared/public-api'; @Component({ selector: 'tb-time-series-chart-y-axes-panel', @@ -68,6 +72,15 @@ import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; }) export class TimeSeriesChartYAxesPanelComponent implements ControlValueAccessor, OnInit, Validator { + @Input() + aliasController: IAliasController; + + @Input() + dataKeyCallbacks: DataKeysCallbacks; + + @Input() + datasource: Datasource; + @Input() disabled: boolean; @@ -113,6 +126,7 @@ export class TimeSeriesChartYAxesPanelComponent implements ControlValueAccessor, for (const axis of axes) { yAxes[axis.id] = axis; } + this.updateLatestDataKeys(Object.values(yAxes)); this.propagateChange(yAxes); } ); @@ -135,7 +149,7 @@ export class TimeSeriesChartYAxesPanelComponent implements ControlValueAccessor, } writeValue(value: TimeSeriesChartYAxes | undefined): void { - const yAxes: TimeSeriesChartYAxes = value || {}; + const yAxes: TimeSeriesChartYAxes = this.checkLatestDataKeys(value || {}); if (!yAxes.default) { yAxes.default = mergeDeep({} as TimeSeriesChartYAxisSettings, defaultTimeSeriesChartYAxisSettings, {id: 'default', order: 0} as TimeSeriesChartYAxisSettings); @@ -182,6 +196,8 @@ export class TimeSeriesChartYAxesPanelComponent implements ControlValueAccessor, const axes: TimeSeriesChartYAxisSettings[] = this.yAxesFormGroup.get('axes').value; axis.id = getNextTimeSeriesYAxisId(axes); axis.order = axes.length; + axis.min = this.normalizeAxisLimit(axis.min); + axis.max = this.normalizeAxisLimit(axis.max); const axesArray = this.yAxesFormGroup.get('axes') as UntypedFormArray; const axisControl = this.fb.control(axis, [timeSeriesChartYAxisValidator]); axesArray.push(axisControl); @@ -194,4 +210,101 @@ export class TimeSeriesChartYAxesPanelComponent implements ControlValueAccessor, }); return this.fb.array(axesControls); } + + private checkLatestDataKeys(yAxes: TimeSeriesChartYAxes): TimeSeriesChartYAxes { + const latestKeys = this.datasource?.latestDataKeys || []; + const result: TimeSeriesChartYAxes = {}; + + for (const [id, axis] of Object.entries(yAxes)) { + axis.min = this.normalizeAxisLimit(axis.min); + axis.max = this.normalizeAxisLimit(axis.max); + const minCfg = axis.min; + const maxCfg = axis.max; + + const minValid = !!minCfg && ( + minCfg.type !== ValueSourceType.latestKey || + latestKeys.some(k => this.isYAxisKey(k, minCfg)) + ); + + const maxValid = !!maxCfg && ( + maxCfg.type !== ValueSourceType.latestKey || + latestKeys.some(k => this.isYAxisKey(k, maxCfg)) + ); + + if (minValid && maxValid) { + result[id] = axis; + } + } + + return result; + } + + private updateLatestDataKeys(yAxes: TimeSeriesChartYAxisSettings[]) { + if (this.datasource) { + let latestKeys = this.datasource.latestDataKeys; + if (!latestKeys) { + latestKeys = []; + this.datasource.latestDataKeys = latestKeys; + } + const existingYAxisKeys = latestKeys.filter(k => k.settings?.__yAxisMinKey === true || k.settings?.__yAxisMaxKey === true); + const foundYAxisKeys: DataKey[] = []; + + for(const yAxis of yAxes) { + const min = yAxis.min as ValueSourceConfig; + const max = yAxis.max as ValueSourceConfig; + if (min.type === ValueSourceType.latestKey) { + const found = existingYAxisKeys.find(k => this.isYAxisKey(k, min)); + if (!found) { + const newKey = this.dataKeyCallbacks.generateDataKey(min.latestKey, min.latestKeyType, + null, true, null); + newKey.settings.__yAxisMinKey = true; + latestKeys.push(newKey); + } else if (foundYAxisKeys.indexOf(found) === -1) { + foundYAxisKeys.push(found); + } + } + if (max.type === ValueSourceType.latestKey) { + const found = existingYAxisKeys.find(k => this.isYAxisKey(k, max)); + if (!found) { + const newKey = this.dataKeyCallbacks.generateDataKey(max.latestKey, max.latestKeyType, + null, true, null); + newKey.settings.__yAxisMaxKey = true; + latestKeys.push(newKey); + } else if (foundYAxisKeys.indexOf(found) === -1) { + foundYAxisKeys.push(found); + } + } + } + const toRemove = existingYAxisKeys.filter(k => foundYAxisKeys.indexOf(k) === -1); + for (const key of toRemove) { + const index = latestKeys.indexOf(key); + if (index > -1) { + latestKeys.splice(index, 1); + } + } + } + } + + private isYAxisKey(d: DataKey, limit: ValueSourceConfig): boolean { + return (d.type === DataKeyType.function && d.label === limit.latestKey) || + (d.type !== DataKeyType.function && d.name === limit.latestKey && + d.type === limit.latestKeyType); + } + + private normalizeAxisLimit(limit: string | number | ValueSourceConfig): ValueSourceConfig { + if (!limit) { + return { + type: ValueSourceType.constant, + value: null, + entityAlias: null + }; + } else if (typeof limit === 'number' || typeof limit === 'string') { + return { + type: ValueSourceType.constant, + value: Number(limit), + entityAlias: null + }; + } + return limit; + } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-row.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-row.component.html index 420e834106..22d3fab103 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-row.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-row.component.html @@ -30,14 +30,14 @@ -
+
- +
-
+
- +
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-row.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-row.component.ts index ebc24ddd22..83f1b6623a 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-row.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-row.component.ts @@ -42,6 +42,8 @@ import { import { deepClone } from '@core/utils'; import { TranslateService } from '@ngx-translate/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { TimeSeriesChartYAxesPanelComponent } from '@home/components/widget/lib/settings/common/chart/time-series-chart-y-axes-panel.component'; +import { ValueSourceType } from '@shared/models/widget-settings.models'; @Component({ selector: 'tb-time-series-chart-y-axis-row', @@ -85,6 +87,7 @@ export class TimeSeriesChartYAxisRowComponent implements ControlValueAccessor, O constructor(private fb: UntypedFormBuilder, private translate: TranslateService, private popoverService: TbPopoverService, + private timeSeriesChartYAxesPanel: TimeSeriesChartYAxesPanelComponent, private renderer: Renderer2, private viewContainerRef: ViewContainerRef, private cd: ChangeDetectorRef, @@ -97,8 +100,8 @@ export class TimeSeriesChartYAxisRowComponent implements ControlValueAccessor, O position: [null, []], units: [null, []], decimals: [null, []], - min: [null, []], - max: [null, []], + min: this.createLimitFormGroup(), + max: this.createLimitFormGroup(), show: [null, []] }); this.axisFormGroup.valueChanges.pipe( @@ -132,17 +135,19 @@ export class TimeSeriesChartYAxisRowComponent implements ControlValueAccessor, O writeValue(value: TimeSeriesChartYAxisSettings): void { this.modelValue = value; - this.axisFormGroup.patchValue( - { - label: value.label, - position: value.position, - units: value.units, - decimals: value.decimals, - min: value.min, - max: value.max, - show: value.show, - }, {emitEvent: false} - ); + const min = this.normalizeLimit(value.min); + const max = this.normalizeLimit(value.max); + + this.axisFormGroup.patchValue({ + label: value.label, + position: value.position, + units: value.units, + decimals: value.decimals, + min, + max, + show: value.show, + }, { emitEvent: false }); + this.updateValidators(); this.cd.markForCheck(); } @@ -165,7 +170,10 @@ export class TimeSeriesChartYAxisRowComponent implements ControlValueAccessor, O axisType: 'yAxis', panelTitle: this.translate.instant('widgets.time-series-chart.axis.y-axis-settings'), axisSettings: deepClone(this.modelValue), - advanced: this.advanced + advanced: this.advanced, + aliasController: this.timeSeriesChartYAxesPanel.aliasController, + dataKeyCallbacks: this.timeSeriesChartYAxesPanel.dataKeyCallbacks, + datasource: this.timeSeriesChartYAxesPanel.datasource }, isModal: true }); @@ -190,6 +198,10 @@ export class TimeSeriesChartYAxisRowComponent implements ControlValueAccessor, O } } + checkIsConstantLimit(limit: 'min' | 'max') { + return this.axisFormGroup.get(`${limit}.type`)?.value === ValueSourceType.constant; + } + private updateValidators() { const show: boolean = this.axisFormGroup.get('show').value; if (show) { @@ -203,10 +215,21 @@ export class TimeSeriesChartYAxisRowComponent implements ControlValueAccessor, O this.axisFormGroup.get('units').disable({emitEvent: false}); this.axisFormGroup.get('decimals').disable({emitEvent: false}); } + if(!this.checkIsConstantLimit('min')){ + this.axisFormGroup.get('min').disable({emitEvent: false}); + } else { + this.axisFormGroup.get('min').enable({emitEvent: false}); + } + if(!this.checkIsConstantLimit('max')){ + this.axisFormGroup.get('max').disable({emitEvent: false}); + } else { + this.axisFormGroup.get('max').enable({emitEvent: false}); + } + } private updateModel() { - const value = this.axisFormGroup.value; + const value = this.axisFormGroup.getRawValue(); this.modelValue.label = value.label; this.modelValue.position = value.position; this.modelValue.units = value.units; @@ -216,4 +239,39 @@ export class TimeSeriesChartYAxisRowComponent implements ControlValueAccessor, O this.modelValue.show = value.show; this.propagateChange(this.modelValue); } + + private createLimitFormGroup() { + return this.fb.group({ + type: [ValueSourceType.constant, []], + value: [null, []], + latestKey: [null, []], + latestKeyType: [null, []], + entityAlias: [null, []], + entityKey: [null, []], + entityKeyType: [null, []] + }); + } + + private normalizeLimit(limit: any) { + const base = { + type: ValueSourceType.constant, + value: null, + latestKey: null, + latestKeyType: null, + entityAlias: null, + entityKey: null, + entityKeyType: null + }; + + if (limit == null) return base; + + if (typeof limit === 'number' || typeof limit === 'string') { + return { ...base, type: ValueSourceType.constant, value: Number(limit) }; + } + + return { + ...base, + ...limit, + }; + } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget-settings-common.module.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget-settings-common.module.ts index 233b202684..385bc55495 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget-settings-common.module.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget-settings-common.module.ts @@ -267,6 +267,7 @@ import { import { ShapeFillStripeSettingsPanelComponent } from '@home/components/widget/lib/settings/common/map/shape-fill-stripe-settings-panel.component'; +import { AxisScaleRowComponent } from './axis-scale-row.component'; @NgModule({ declarations: [ @@ -372,7 +373,8 @@ import { DataKeysComponent, DataKeyConfigDialogComponent, DataKeyConfigComponent, - WidgetSettingsComponent + WidgetSettingsComponent, + AxisScaleRowComponent ], imports: [ CommonModule, @@ -453,7 +455,8 @@ import { DataKeysComponent, DataKeyConfigDialogComponent, DataKeyConfigComponent, - WidgetSettingsComponent + WidgetSettingsComponent, + AxisScaleRowComponent ], providers: [ ColorSettingsComponentService, diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index 54492e06b5..5a786e28a8 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -8050,6 +8050,14 @@ "update-animation-delay": "Update animation delay" }, "chart-axis": { + "limit":"Limit", + "source": "Source", + "key-value": "Key / Value", + "value-required": "Value is required.", + "entity-key-required": "Entity key is required.", + "key-required": "Key is required.", + "scale-limits": "Scale limits", + "scale-appearance": "Scale appearance", "scale": "Scale", "scale-min": "min", "scale-max": "max", diff --git a/ui-ngx/tailwind.config.js b/ui-ngx/tailwind.config.js index d675da4cda..bc516ddf40 100644 --- a/ui-ngx/tailwind.config.js +++ b/ui-ngx/tailwind.config.js @@ -84,7 +84,8 @@ module.exports = { '25': '6.25rem', '37.5': '9.375rem', '62.5': '15.625rem', - '72.5': '18.125rem' + '72.5': '18.125rem', + '40%': '40%' }, flex: { full: '1 1 100%'