Browse Source

Merge pull request #14488 from mtsymbarov-del/feat/chart-dynamic-y-axis-configuration

Added support for dynamic y axis limits determination for time series charts
pull/14573/head
Vladyslav Prykhodko 10 months ago
committed by GitHub
parent
commit
6da390f34f
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 3
      ui-ngx/src/app/modules/home/components/widget/config/basic/chart/bar-chart-with-labels-basic-config.component.html
  2. 3
      ui-ngx/src/app/modules/home/components/widget/config/basic/chart/range-chart-basic-config.component.html
  3. 3
      ui-ngx/src/app/modules/home/components/widget/config/basic/chart/time-series-chart-basic-config.component.html
  4. 34
      ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.models.ts
  5. 177
      ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.ts
  6. 3
      ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/bar-chart-with-labels-widget-settings.component.html
  7. 3
      ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/range-chart-widget-settings.component.html
  8. 3
      ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-widget-settings.component.html
  9. 71
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/axis-scale-row.component.html
  10. 209
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/axis-scale-row.component.ts
  11. 3
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-axis-settings-panel.component.html
  12. 12
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-axis-settings-panel.component.ts
  13. 48
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-axis-settings.component.html
  14. 27
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-axis-settings.component.ts
  15. 117
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axes-panel.component.ts
  16. 8
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-row.component.html
  17. 88
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-row.component.ts
  18. 7
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget-settings-common.module.ts
  19. 8
      ui-ngx/src/assets/locale/locale.constant-en_US.json
  20. 3
      ui-ngx/tailwind.config.js

3
ui-ngx/src/app/modules/home/components/widget/config/basic/chart/bar-chart-with-labels-basic-config.component.html

@ -180,6 +180,9 @@
<div class="tb-form-panel-title" translate>widgets.time-series-chart.axis.y-axis</div> <div class="tb-form-panel-title" translate>widgets.time-series-chart.axis.y-axis</div>
<tb-time-series-chart-axis-settings <tb-time-series-chart-axis-settings
formControlName="yAxis" formControlName="yAxis"
[aliasController]="aliasController"
[dataKeyCallbacks]="callbacks"
[datasource]="datasource"
axisType="yAxis" axisType="yAxis"
hideUnits hideUnits
hideDecimals> hideDecimals>

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

@ -242,6 +242,9 @@
<div class="tb-form-panel-title" translate>widgets.time-series-chart.axis.y-axis</div> <div class="tb-form-panel-title" translate>widgets.time-series-chart.axis.y-axis</div>
<tb-time-series-chart-axis-settings <tb-time-series-chart-axis-settings
formControlName="yAxis" formControlName="yAxis"
[aliasController]="aliasController"
[dataKeyCallbacks]="callbacks"
[datasource]="datasource"
axisType="yAxis" axisType="yAxis"
hideUnits hideUnits
hideDecimals> hideDecimals>

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

@ -103,6 +103,9 @@
formControlName="states"> formControlName="states">
</tb-time-series-chart-states-panel> </tb-time-series-chart-states-panel>
<tb-time-series-chart-y-axes-panel <tb-time-series-chart-y-axes-panel
[aliasController]="aliasController"
[dataKeyCallbacks]="callbacks"
[datasource]="datasource"
formControlName="yAxes" formControlName="yAxes"
[supportsUnitConversion]="widgetConfig.typeParameters.supportsUnitConversion" [supportsUnitConversion]="widgetConfig.typeParameters.supportsUnitConversion"
(axisRemoved)="yAxisRemoved($event)"> (axisRemoved)="yAxisRemoved($event)">

34
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; decimals?: number;
interval?: number; interval?: number;
splitNumber?: number; splitNumber?: number;
min?: number | string; min?: number | string | ValueSourceConfig;
max?: number | string; max?: number | string | ValueSourceConfig;
ticksGenerator?: TimeSeriesChartTicksGenerator | string; ticksGenerator?: TimeSeriesChartTicksGenerator | string;
ticksFormatter?: TimeSeriesChartTicksFormatter | string; ticksFormatter?: TimeSeriesChartTicksFormatter | string;
} }
@ -867,6 +867,9 @@ export interface TimeSeriesChartAxis {
id: string; id: string;
settings: TimeSeriesChartAxisSettings; settings: TimeSeriesChartAxisSettings;
option: CartesianAxisOption; option: CartesianAxisOption;
minLatestDataKey?: DataKey;
maxLatestDataKey?: DataKey;
unitConvertor?: (value: number) => number;
} }
export interface TimeSeriesChartYAxis extends TimeSeriesChartAxis { 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]); 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 { return {
id: settings.id, id: settings.id,
decimals, decimals,
@ -939,8 +965,8 @@ export const createTimeSeriesYAxis = (units: string,
offset: 0, offset: 0,
alignTicks: true, alignTicks: true,
scale: true, scale: true,
min: isDefinedAndNotNull(settings.min) ? unitConvertor(Number(settings.min)) : settings.min, min: initialMin,
max: isDefinedAndNotNull(settings.max) ? unitConvertor(Number(settings.max)) : settings.max, max: initialMax,
minInterval, minInterval,
splitNumber, splitNumber,
interval, interval,

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

@ -54,13 +54,15 @@ import {
getFocusedSeriesIndex, getFocusedSeriesIndex,
measureAxisNameSize measureAxisNameSize
} from '@home/components/widget/lib/chart/echarts-widget.models'; } 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 { import {
formattedDataFormDatasourceData, formattedDataFormDatasourceData,
formatValue, formatValue,
isDefined, isDefined,
isDefinedAndNotNull, isDefinedAndNotNull,
isEqual, isEqual,
isNumber,
isString,
mergeDeep mergeDeep
} from '@core/utils'; } from '@core/utils';
import { DataKey, Datasource, DatasourceType, FormattedData, widgetType } from '@shared/models/widget.models'; 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) { if (this.timeSeriesChart && update) {
this.updateSeriesData(); this.updateSeriesData();
this.updateAxisLimits();
} }
} }
@ -550,6 +579,7 @@ export class TbTimeSeriesChart {
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);
const axisLimitDatasources: Datasource[] = [];
for (const yAxisSettings of yAxisSettingsList) { for (const yAxisSettings of yAxisSettingsList) {
const axisSettings = mergeDeep<TimeSeriesChartYAxisSettings>({} as TimeSeriesChartYAxisSettings, const axisSettings = mergeDeep<TimeSeriesChartYAxisSettings>({} as TimeSeriesChartYAxisSettings,
defaultTimeSeriesChartYAxisSettings, yAxisSettings); defaultTimeSeriesChartYAxisSettings, yAxisSettings);
@ -563,8 +593,81 @@ export class TbTimeSeriesChart {
axisSettings.ticksFormatter = this.stateValueConverter.ticksFormatter; axisSettings.ticksFormatter = this.stateValueConverter.ticksFormatter;
} }
const yAxis = createTimeSeriesYAxis(unitSymbol, decimals, axisSettings, this.ctx.utilsService, this.darkMode, unitConvertor); 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.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 { 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() { private drawChart() {
echartsModule.init(); echartsModule.init();
this.renderer.setStyle(this.chartElement, 'letterSpacing', 'normal'); 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 { private updateSeries(): void {
this.timeSeriesChartOptions.series = generateChartData(this.dataItems, this.thresholdItems, this.timeSeriesChartOptions.series = generateChartData(this.dataItems, this.thresholdItems,
this.stackMode, this.stackMode,
@ -836,6 +1001,16 @@ export class TbTimeSeriesChart {
return changed; 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 { private scaleYAxis(yAxis: TimeSeriesChartYAxis): boolean {
if (!this.stateData) { if (!this.stateData) {
const axisBarDataItems = this.dataItems.filter(d => d.yAxisId === yAxis.id && d.enabled && const axisBarDataItems = this.dataItems.filter(d => d.yAxisId === yAxis.id && d.enabled &&

3
ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/bar-chart-with-labels-widget-settings.component.html

@ -91,6 +91,9 @@
<div class="tb-form-panel-title" translate>widgets.time-series-chart.axis.y-axis</div> <div class="tb-form-panel-title" translate>widgets.time-series-chart.axis.y-axis</div>
<tb-time-series-chart-axis-settings <tb-time-series-chart-axis-settings
formControlName="yAxis" formControlName="yAxis"
[aliasController]="aliasController"
[dataKeyCallbacks]="callbacks"
[datasource]="datasource"
axisType="yAxis" axisType="yAxis"
advanced advanced
hideUnits hideUnits

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

@ -169,6 +169,9 @@
<div class="tb-form-panel-title" translate>widgets.time-series-chart.axis.y-axis</div> <div class="tb-form-panel-title" translate>widgets.time-series-chart.axis.y-axis</div>
<tb-time-series-chart-axis-settings <tb-time-series-chart-axis-settings
formControlName="yAxis" formControlName="yAxis"
[aliasController]="aliasController"
[dataKeyCallbacks]="callbacks"
[datasource]="datasource"
axisType="yAxis" axisType="yAxis"
hideUnits hideUnits
hideDecimals> hideDecimals>

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

@ -62,6 +62,9 @@
</tb-time-series-chart-states-panel> </tb-time-series-chart-states-panel>
<tb-time-series-chart-y-axes-panel <tb-time-series-chart-y-axes-panel
formControlName="yAxes" formControlName="yAxes"
[aliasController]="aliasController"
[dataKeyCallbacks]="dataKeyCallbacks"
[datasource]="datasource"
(axisRemoved)="yAxisRemoved($event)" (axisRemoved)="yAxisRemoved($event)"
[supportsUnitConversion]="widgetConfig.typeParameters.supportsUnitConversion" [supportsUnitConversion]="widgetConfig.typeParameters.supportsUnitConversion"
advanced> advanced>

71
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/axis-scale-row.component.html

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

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

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

@ -19,6 +19,9 @@
<div class="tb-axis-settings-title">{{ panelTitle }}</div> <div class="tb-axis-settings-title">{{ panelTitle }}</div>
<div class="tb-axis-settings-panel-content"> <div class="tb-axis-settings-panel-content">
<tb-time-series-chart-axis-settings <tb-time-series-chart-axis-settings
[aliasController]="aliasController"
[dataKeyCallbacks]="dataKeyCallbacks"
[datasource]="datasource"
formControlName="axis" formControlName="axis"
alwaysExpanded alwaysExpanded
[advanced]="advanced" [advanced]="advanced"

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

@ -22,6 +22,9 @@ import {
TimeSeriesChartYAxisSettings TimeSeriesChartYAxisSettings
} from '@home/components/widget/lib/chart/time-series-chart.models'; } from '@home/components/widget/lib/chart/time-series-chart.models';
import { coerceBoolean } from '@shared/decorators/coercion'; import { coerceBoolean } from '@shared/decorators/coercion';
import { IAliasController } from '@core/api/widget-api.models';
import { DataKeysCallbacks } from '@home/components/widget/lib/settings/common/key/data-keys.component.models';
import { Datasource } from '@shared/models/widget.models';
@Component({ @Component({
selector: 'tb-time-series-chart-axis-settings-panel', selector: 'tb-time-series-chart-axis-settings-panel',
@ -32,6 +35,15 @@ import { coerceBoolean } from '@shared/decorators/coercion';
}) })
export class TimeSeriesChartAxisSettingsPanelComponent implements OnInit { export class TimeSeriesChartAxisSettingsPanelComponent implements OnInit {
@Input()
aliasController: IAliasController;
@Input()
dataKeyCallbacks: DataKeysCallbacks;
@Input()
datasource: Datasource;
@Input() @Input()
axisType: 'xAxis' | 'yAxis' = 'xAxis'; axisType: 'xAxis' | 'yAxis' = 'xAxis';

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

@ -157,24 +157,38 @@
type="number" min="1" placeholder="{{ 'widgets.chart.chart-axis.scale-auto' | translate }}"> type="number" min="1" placeholder="{{ 'widgets.chart.chart-axis.scale-auto' | translate }}">
</mat-form-field> </mat-form-field>
</div> </div>
<div *ngIf="axisType === 'yAxis'" class="tb-form-panel stroked">
<div class="flex flex-col gap-2">
<div translate>widgets.chart.chart-axis.scale-limits</div>
<div class="tb-form-table">
<div class="tb-form-table-header">
<div class="tb-form-table-header-cell" [class]="alwaysExpanded ? 'basis-10' : 'basis-1/5'" translate>widgets.chart.chart-axis.limit</div>
<div class="tb-form-table-header-cell flex-grow flex-shrink" [class]="alwaysExpanded ? 'basis-1/2' : 'basis-40%'" translate>widgets.chart.chart-axis.source</div>
<div class="tb-form-table-header-cell flex-grow flex-shrink" [class]="alwaysExpanded ? 'basis-1/2' : 'basis-40%'" translate>widgets.chart.chart-axis.key-value</div>
</div>
<div class="tb-form-table-body">
<tb-axis-scale-row
[isPanelView]="alwaysExpanded"
[datasource]="datasource"
[callbacks]="dataKeyCallbacks"
[aliasController]="aliasController"
formControlName="min"
labelKey="widgets.chart.chart-axis.scale-min">
</tb-axis-scale-row>
<mat-divider class="mt-2 gt-sm:hidden"></mat-divider>
<tb-axis-scale-row
[isPanelView]="alwaysExpanded"
[datasource]="datasource"
[callbacks]="dataKeyCallbacks"
[aliasController]="aliasController"
formControlName="max"
labelKey="widgets.chart.chart-axis.scale-max">
</tb-axis-scale-row>
</div>
</div>
</div>
</div>
</ng-template> </ng-template>
</mat-expansion-panel> </mat-expansion-panel>
<div *ngIf="axisType === 'yAxis'" class="tb-form-panel no-border no-padding-top">
<div class="tb-form-row space-between column-xs">
<div translate>widgets.chart.chart-axis.scale</div>
<div class="flex flex-row items-center justify-start gap-2">
<div class="tb-small-label" translate>widgets.chart.chart-axis.scale-min</div>
<mat-form-field appearance="outline" class="number" subscriptSizing="dynamic">
<input matInput formControlName="min"
type="number" placeholder="{{ 'widgets.chart.chart-axis.scale-auto' | translate }}">
</mat-form-field>
<div class="tb-small-label" translate>widgets.chart.chart-axis.scale-max</div>
<mat-form-field appearance="outline" class="number" subscriptSizing="dynamic">
<input matInput formControlName="max"
type="number" placeholder="{{ 'widgets.chart.chart-axis.scale-auto' | translate }}">
</mat-form-field>
</div>
</div>
</div>
</div> </div>
</ng-container> </ng-container>

27
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 { Component, DestroyRef, forwardRef, Input, OnInit } from '@angular/core';
import { import {
ControlValueAccessor, ControlValueAccessor,
NG_VALIDATORS,
NG_VALUE_ACCESSOR, NG_VALUE_ACCESSOR,
UntypedFormBuilder, UntypedFormBuilder,
UntypedFormGroup, UntypedFormGroup,
ValidationErrors, Validator,
Validators Validators
} from '@angular/forms'; } from '@angular/forms';
import { import {
@ -32,6 +34,9 @@ import { merge } from 'rxjs';
import { coerceBoolean } from '@shared/decorators/coercion'; import { coerceBoolean } from '@shared/decorators/coercion';
import { WidgetService } from '@core/http/widget.service'; import { WidgetService } from '@core/http/widget.service';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; 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({ @Component({
selector: 'tb-time-series-chart-axis-settings', selector: 'tb-time-series-chart-axis-settings',
@ -42,10 +47,15 @@ import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
provide: NG_VALUE_ACCESSOR, provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => TimeSeriesChartAxisSettingsComponent), useExisting: forwardRef(() => TimeSeriesChartAxisSettingsComponent),
multi: true multi: true
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => TimeSeriesChartAxisSettingsComponent),
multi: true
} }
] ]
}) })
export class TimeSeriesChartAxisSettingsComponent implements OnInit, ControlValueAccessor { export class TimeSeriesChartAxisSettingsComponent implements OnInit, ControlValueAccessor, Validator {
@Input() @Input()
@coerceBoolean() @coerceBoolean()
@ -61,6 +71,15 @@ export class TimeSeriesChartAxisSettingsComponent implements OnInit, ControlValu
defaultXAxisTicksFormat = defaultXAxisTicksFormat; defaultXAxisTicksFormat = defaultXAxisTicksFormat;
@Input()
aliasController: IAliasController;
@Input()
dataKeyCallbacks: DataKeysCallbacks;
@Input()
datasource: Datasource;
@Input() @Input()
disabled: boolean; disabled: boolean;
@ -147,6 +166,12 @@ export class TimeSeriesChartAxisSettingsComponent implements OnInit, ControlValu
registerOnTouched(_fn: any): void { registerOnTouched(_fn: any): void {
} }
validate(): ValidationErrors | null {
return this.axisSettingsFormGroup.valid ? null : {
axisSettings: false
};
}
setDisabledState(isDisabled: boolean): void { setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled; this.disabled = isDisabled;
if (isDisabled) { if (isDisabled) {

117
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 { import {
defaultTimeSeriesChartYAxisSettings, defaultTimeSeriesChartYAxisSettings,
getNextTimeSeriesYAxisId, getNextTimeSeriesYAxisId,
TimeSeriesChartYAxes, TimeSeriesChartYAxisId, TimeSeriesChartYAxes,
TimeSeriesChartYAxisId,
TimeSeriesChartYAxisSettings, TimeSeriesChartYAxisSettings,
timeSeriesChartYAxisValid, timeSeriesChartYAxisValid,
timeSeriesChartYAxisValidator timeSeriesChartYAxisValidator
@ -47,6 +48,9 @@ import { mergeDeep } from '@core/utils';
import { CdkDragDrop } from '@angular/cdk/drag-drop'; import { CdkDragDrop } from '@angular/cdk/drag-drop';
import { coerceBoolean } from '@shared/decorators/coercion'; import { coerceBoolean } from '@shared/decorators/coercion';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; 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({ @Component({
selector: 'tb-time-series-chart-y-axes-panel', 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 { export class TimeSeriesChartYAxesPanelComponent implements ControlValueAccessor, OnInit, Validator {
@Input()
aliasController: IAliasController;
@Input()
dataKeyCallbacks: DataKeysCallbacks;
@Input()
datasource: Datasource;
@Input() @Input()
disabled: boolean; disabled: boolean;
@ -113,6 +126,7 @@ export class TimeSeriesChartYAxesPanelComponent implements ControlValueAccessor,
for (const axis of axes) { for (const axis of axes) {
yAxes[axis.id] = axis; yAxes[axis.id] = axis;
} }
this.updateLatestDataKeys(Object.values(yAxes));
this.propagateChange(yAxes); this.propagateChange(yAxes);
} }
); );
@ -135,7 +149,7 @@ export class TimeSeriesChartYAxesPanelComponent implements ControlValueAccessor,
} }
writeValue(value: TimeSeriesChartYAxes | undefined): void { writeValue(value: TimeSeriesChartYAxes | undefined): void {
const yAxes: TimeSeriesChartYAxes = value || {}; const yAxes: TimeSeriesChartYAxes = this.checkLatestDataKeys(value || {});
if (!yAxes.default) { if (!yAxes.default) {
yAxes.default = mergeDeep({} as TimeSeriesChartYAxisSettings, defaultTimeSeriesChartYAxisSettings, yAxes.default = mergeDeep({} as TimeSeriesChartYAxisSettings, defaultTimeSeriesChartYAxisSettings,
{id: 'default', order: 0} as TimeSeriesChartYAxisSettings); {id: 'default', order: 0} as TimeSeriesChartYAxisSettings);
@ -182,6 +196,8 @@ export class TimeSeriesChartYAxesPanelComponent implements ControlValueAccessor,
const axes: TimeSeriesChartYAxisSettings[] = this.yAxesFormGroup.get('axes').value; const axes: TimeSeriesChartYAxisSettings[] = this.yAxesFormGroup.get('axes').value;
axis.id = getNextTimeSeriesYAxisId(axes); axis.id = getNextTimeSeriesYAxisId(axes);
axis.order = axes.length; 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 axesArray = this.yAxesFormGroup.get('axes') as UntypedFormArray;
const axisControl = this.fb.control(axis, [timeSeriesChartYAxisValidator]); const axisControl = this.fb.control(axis, [timeSeriesChartYAxisValidator]);
axesArray.push(axisControl); axesArray.push(axisControl);
@ -194,4 +210,101 @@ export class TimeSeriesChartYAxesPanelComponent implements ControlValueAccessor,
}); });
return this.fb.array(axesControls); 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;
}
} }

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

@ -30,14 +30,14 @@
</mat-option> </mat-option>
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
<div class="tb-min-field"> <div class="tb-min-field" formGroupName="min">
<mat-form-field appearance="outline" class="tb-inline-field number" subscriptSizing="dynamic"> <mat-form-field appearance="outline" class="tb-inline-field number" subscriptSizing="dynamic">
<input matInput formControlName="min" type="number" placeholder="{{ 'widgets.chart.chart-axis.scale-auto' | translate }}"> <input matInput formControlName="value" type="number" placeholder="{{ 'widgets.chart.chart-axis.scale-auto' | translate }}">
</mat-form-field> </mat-form-field>
</div> </div>
<div class="tb-max-field"> <div class="tb-max-field" formGroupName="max">
<mat-form-field appearance="outline" class="tb-inline-field number" subscriptSizing="dynamic"> <mat-form-field appearance="outline" class="tb-inline-field number" subscriptSizing="dynamic">
<input matInput formControlName="max" type="number" placeholder="{{ 'widgets.chart.chart-axis.scale-auto' | translate }}"> <input matInput formControlName="value" type="number" placeholder="{{ 'widgets.chart.chart-axis.scale-auto' | translate }}">
</mat-form-field> </mat-form-field>
</div> </div>
<div class="tb-units-field"> <div class="tb-units-field">

88
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 { deepClone } from '@core/utils';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; 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({ @Component({
selector: 'tb-time-series-chart-y-axis-row', selector: 'tb-time-series-chart-y-axis-row',
@ -85,6 +87,7 @@ export class TimeSeriesChartYAxisRowComponent implements ControlValueAccessor, O
constructor(private fb: UntypedFormBuilder, constructor(private fb: UntypedFormBuilder,
private translate: TranslateService, private translate: TranslateService,
private popoverService: TbPopoverService, private popoverService: TbPopoverService,
private timeSeriesChartYAxesPanel: TimeSeriesChartYAxesPanelComponent,
private renderer: Renderer2, private renderer: Renderer2,
private viewContainerRef: ViewContainerRef, private viewContainerRef: ViewContainerRef,
private cd: ChangeDetectorRef, private cd: ChangeDetectorRef,
@ -97,8 +100,8 @@ export class TimeSeriesChartYAxisRowComponent implements ControlValueAccessor, O
position: [null, []], position: [null, []],
units: [null, []], units: [null, []],
decimals: [null, []], decimals: [null, []],
min: [null, []], min: this.createLimitFormGroup(),
max: [null, []], max: this.createLimitFormGroup(),
show: [null, []] show: [null, []]
}); });
this.axisFormGroup.valueChanges.pipe( this.axisFormGroup.valueChanges.pipe(
@ -132,17 +135,19 @@ export class TimeSeriesChartYAxisRowComponent implements ControlValueAccessor, O
writeValue(value: TimeSeriesChartYAxisSettings): void { writeValue(value: TimeSeriesChartYAxisSettings): void {
this.modelValue = value; this.modelValue = value;
this.axisFormGroup.patchValue( const min = this.normalizeLimit(value.min);
{ const max = this.normalizeLimit(value.max);
label: value.label,
position: value.position, this.axisFormGroup.patchValue({
units: value.units, label: value.label,
decimals: value.decimals, position: value.position,
min: value.min, units: value.units,
max: value.max, decimals: value.decimals,
show: value.show, min,
}, {emitEvent: false} max,
); show: value.show,
}, { emitEvent: false });
this.updateValidators(); this.updateValidators();
this.cd.markForCheck(); this.cd.markForCheck();
} }
@ -165,7 +170,10 @@ export class TimeSeriesChartYAxisRowComponent implements ControlValueAccessor, O
axisType: 'yAxis', axisType: 'yAxis',
panelTitle: this.translate.instant('widgets.time-series-chart.axis.y-axis-settings'), panelTitle: this.translate.instant('widgets.time-series-chart.axis.y-axis-settings'),
axisSettings: deepClone(this.modelValue), axisSettings: deepClone(this.modelValue),
advanced: this.advanced advanced: this.advanced,
aliasController: this.timeSeriesChartYAxesPanel.aliasController,
dataKeyCallbacks: this.timeSeriesChartYAxesPanel.dataKeyCallbacks,
datasource: this.timeSeriesChartYAxesPanel.datasource
}, },
isModal: true 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() { private updateValidators() {
const show: boolean = this.axisFormGroup.get('show').value; const show: boolean = this.axisFormGroup.get('show').value;
if (show) { if (show) {
@ -203,10 +215,21 @@ export class TimeSeriesChartYAxisRowComponent implements ControlValueAccessor, O
this.axisFormGroup.get('units').disable({emitEvent: false}); this.axisFormGroup.get('units').disable({emitEvent: false});
this.axisFormGroup.get('decimals').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() { private updateModel() {
const value = this.axisFormGroup.value; const value = this.axisFormGroup.getRawValue();
this.modelValue.label = value.label; this.modelValue.label = value.label;
this.modelValue.position = value.position; this.modelValue.position = value.position;
this.modelValue.units = value.units; this.modelValue.units = value.units;
@ -216,4 +239,39 @@ export class TimeSeriesChartYAxisRowComponent implements ControlValueAccessor, O
this.modelValue.show = value.show; this.modelValue.show = value.show;
this.propagateChange(this.modelValue); 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,
};
}
} }

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

@ -267,6 +267,7 @@ import {
import { import {
ShapeFillStripeSettingsPanelComponent ShapeFillStripeSettingsPanelComponent
} from '@home/components/widget/lib/settings/common/map/shape-fill-stripe-settings-panel.component'; } from '@home/components/widget/lib/settings/common/map/shape-fill-stripe-settings-panel.component';
import { AxisScaleRowComponent } from './axis-scale-row.component';
@NgModule({ @NgModule({
declarations: [ declarations: [
@ -372,7 +373,8 @@ import {
DataKeysComponent, DataKeysComponent,
DataKeyConfigDialogComponent, DataKeyConfigDialogComponent,
DataKeyConfigComponent, DataKeyConfigComponent,
WidgetSettingsComponent WidgetSettingsComponent,
AxisScaleRowComponent
], ],
imports: [ imports: [
CommonModule, CommonModule,
@ -453,7 +455,8 @@ import {
DataKeysComponent, DataKeysComponent,
DataKeyConfigDialogComponent, DataKeyConfigDialogComponent,
DataKeyConfigComponent, DataKeyConfigComponent,
WidgetSettingsComponent WidgetSettingsComponent,
AxisScaleRowComponent
], ],
providers: [ providers: [
ColorSettingsComponentService, ColorSettingsComponentService,

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

@ -8050,6 +8050,14 @@
"update-animation-delay": "Update animation delay" "update-animation-delay": "Update animation delay"
}, },
"chart-axis": { "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": "Scale",
"scale-min": "min", "scale-min": "min",
"scale-max": "max", "scale-max": "max",

3
ui-ngx/tailwind.config.js

@ -84,7 +84,8 @@ module.exports = {
'25': '6.25rem', '25': '6.25rem',
'37.5': '9.375rem', '37.5': '9.375rem',
'62.5': '15.625rem', '62.5': '15.625rem',
'72.5': '18.125rem' '72.5': '18.125rem',
'40%': '40%'
}, },
flex: { flex: {
full: '1 1 100%' full: '1 1 100%'

Loading…
Cancel
Save