Browse Source

Added support for dynamic y axis limits determination

pull/14488/head
Maksym Tsymbarov 8 months ago
parent
commit
63cc15091c
  1. 30
      ui-ngx/src/app/modules/home/components/widget/config/basic/chart/bar-chart-basic-config.component.ts
  2. 3
      ui-ngx/src/app/modules/home/components/widget/config/basic/chart/bar-chart-with-labels-basic-config.component.html
  3. 78
      ui-ngx/src/app/modules/home/components/widget/config/basic/chart/latest-chart-basic-config.component.html
  4. 30
      ui-ngx/src/app/modules/home/components/widget/config/basic/chart/polar-area-chart-basic-config.component.ts
  5. 3
      ui-ngx/src/app/modules/home/components/widget/config/basic/chart/range-chart-basic-config.component.html
  6. 3
      ui-ngx/src/app/modules/home/components/widget/config/basic/chart/time-series-chart-basic-config.component.html
  7. 5
      ui-ngx/src/app/modules/home/components/widget/lib/chart/bar-chart-widget.models.ts
  8. 5
      ui-ngx/src/app/modules/home/components/widget/lib/chart/bars-chart.models.ts
  9. 225
      ui-ngx/src/app/modules/home/components/widget/lib/chart/bars-chart.ts
  10. 42
      ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.models.ts
  11. 201
      ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.ts
  12. 30
      ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/bar-chart-widget-settings.component.ts
  13. 3
      ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/bar-chart-with-labels-widget-settings.component.html
  14. 80
      ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/latest-chart-widget-settings.component.html
  15. 10
      ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/latest-chart-widget-settings.component.ts
  16. 33
      ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/polar-area-chart-widget-settings.component.ts
  17. 3
      ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/range-chart-widget-settings.component.html
  18. 71
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/axis-scale-row.component.html
  19. 15
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/axis-scale-row.component.scss
  20. 204
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/axis-scale-row.component.ts
  21. 3
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-axis-settings-panel.component.html
  22. 12
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-axis-settings-panel.component.ts
  23. 48
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-axis-settings.component.html
  24. 41
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-axis-settings.component.ts
  25. 12
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axes-panel.component.ts
  26. 7
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-row.component.ts
  27. 7
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget-settings-common.module.ts
  28. 8
      ui-ngx/src/assets/locale/locale.constant-en_US.json
  29. 3
      ui-ngx/tailwind.config.js

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

@ -29,6 +29,8 @@ import {
import {
LatestChartBasicConfigComponent
} from '@home/components/widget/config/basic/chart/latest-chart-basic-config.component';
import { AxisLimitConfig } from '@home/components/widget/lib/chart/time-series-chart.models';
import { ValueSourceType } from '@shared/models/widget-settings.models';
@Component({
selector: 'tb-bar-chart-basic-config',
@ -77,4 +79,32 @@ export class BarChartBasicConfigComponent extends LatestChartBasicConfigComponen
this.widgetConfig.config.settings.axisTickLabelFont = config.axisTickLabelFont;
this.widgetConfig.config.settings.axisTickLabelColor = config.axisTickLabelColor;
}
protected onConfigSet(configData: WidgetConfigComponentData) {
configData.config.settings.axisMin = this.normalizeAxisLimit(configData.config.settings.axisMin);
configData.config.settings.axisMax = this.normalizeAxisLimit(configData.config.settings.axisMax);
super.onConfigSet(configData);
}
private normalizeAxisLimit(limit: any): AxisLimitConfig {
if (limit && typeof limit === 'object' && 'type' in limit) {
return {
type: limit.type || ValueSourceType.constant,
value: limit.value ?? null,
entityAlias: limit.entityAlias ?? null
};
}
if (typeof limit === 'number') {
return {
type: ValueSourceType.constant,
value: limit,
entityAlias: null
};
}
return {
type: ValueSourceType.constant,
value: null,
entityAlias: null
};
}
}

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

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

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

@ -346,18 +346,8 @@
<div class="tb-form-panel">
<div class="tb-form-panel-title" translate>widgets.bar-chart.bar-axis</div>
<div class="tb-form-row space-between column-xs">
<div translate>widgets.chart.chart-axis.scale</div>
<div translate>widgets.chart.chart-axis.scale-appearance</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="axisMin"
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="axisMax"
type="number" placeholder="{{ 'widgets.chart.chart-axis.scale-auto' | translate }}">
</mat-form-field>
<tb-font-settings formControlName="axisTickLabelFont"
clearButton
disabledLineHeight
@ -370,6 +360,33 @@
</tb-color-input>
</div>
</div>
<div class="tb-form-row column align-start">
<div translate>widgets.chart.chart-axis.scale-limits</div>
<div class="tb-form-table w-full">
<div class="tb-form-table-header w-full">
<div class="tb-form-table-header-cell flex-grow flex-shrink basis-1/5" translate>widgets.chart.chart-axis.limit</div>
<div class="tb-form-table-header-cell flex-grow flex-shrink basis-40%" translate>widgets.chart.chart-axis.source</div>
<div class="tb-form-table-header-cell flex-grow flex-shrink basis-40%" translate>widgets.chart.chart-axis.key-value</div>
</div>
<div class="tb-form-table-body">
<tb-axis-scale-row
[datasource]="datasource"
[callbacks]="callbacks"
[aliasController]="aliasController"
formControlName="axisMin"
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
[datasource]="datasource"
[callbacks]="callbacks"
[aliasController]="aliasController"
formControlName="axisMax"
labelKey="widgets.chart.chart-axis.scale-max">
</tb-axis-scale-row>
</div>
</div>
</div>
</div>
</ng-container>
</ng-template>
@ -386,18 +403,8 @@
<div class="tb-form-panel">
<div class="tb-form-panel-title" translate>widgets.polar-area-chart.polar-axis</div>
<div class="tb-form-row space-between column-xs">
<div translate>widgets.chart.chart-axis.scale</div>
<div translate>widgets.chart.chart-axis.scale-appearance</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="axisMin"
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="axisMax"
type="number" placeholder="{{ 'widgets.chart.chart-axis.scale-auto' | translate }}">
</mat-form-field>
<tb-font-settings formControlName="axisTickLabelFont"
clearButton
disabledLineHeight
@ -416,6 +423,33 @@
<input matInput formControlName="angleAxisStartAngle" type="number" min="0" max="360" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
</div>
<div class="tb-form-row column align-start">
<div translate>widgets.chart.chart-axis.scale-limits</div>
<div class="tb-form-table w-full">
<div class="tb-form-table-header w-full">
<div class="tb-form-table-header-cell flex-grow flex-shrink basis-1/5" translate>widgets.chart.chart-axis.limit</div>
<div class="tb-form-table-header-cell flex-grow flex-shrink basis-40%" translate>widgets.chart.chart-axis.source</div>
<div class="tb-form-table-header-cell flex-grow flex-shrink basis-40%" translate>widgets.chart.chart-axis.key-value</div>
</div>
<div class="tb-form-table-body">
<tb-axis-scale-row
[datasource]="datasource"
[callbacks]="callbacks"
[aliasController]="aliasController"
formControlName="axisMin"
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
[datasource]="datasource"
[callbacks]="callbacks"
[aliasController]="aliasController"
formControlName="axisMax"
labelKey="widgets.chart.chart-axis.scale-max">
</tb-axis-scale-row>
</div>
</div>
</div>
</div>
</ng-container>
</ng-template>

30
ui-ngx/src/app/modules/home/components/widget/config/basic/chart/polar-area-chart-basic-config.component.ts

@ -29,6 +29,8 @@ import {
import {
LatestChartBasicConfigComponent
} from '@home/components/widget/config/basic/chart/latest-chart-basic-config.component';
import { AxisLimitConfig } from '@home/components/widget/lib/chart/time-series-chart.models';
import { ValueSourceType } from '@shared/models/widget-settings.models';
@Component({
selector: 'tb-polar-area-chart-basic-config',
@ -79,4 +81,32 @@ export class PolarAreaChartBasicConfigComponent extends LatestChartBasicConfigCo
this.widgetConfig.config.settings.axisTickLabelColor = config.axisTickLabelColor;
this.widgetConfig.config.settings.angleAxisStartAngle = config.angleAxisStartAngle;
}
protected onConfigSet(configData: WidgetConfigComponentData) {
configData.config.settings.axisMin = this.normalizeAxisLimit(configData.config.settings.axisMin);
configData.config.settings.axisMax = this.normalizeAxisLimit(configData.config.settings.axisMax);
super.onConfigSet(configData);
}
private normalizeAxisLimit(limit: any): AxisLimitConfig {
if (limit && typeof limit === 'object' && 'type' in limit) {
return {
type: limit.type || ValueSourceType.constant,
value: limit.value ?? null,
entityAlias: limit.entityAlias ?? null
};
}
if (typeof limit === 'number') {
return {
type: ValueSourceType.constant,
value: limit,
entityAlias: null
};
}
return {
type: ValueSourceType.constant,
value: null,
entityAlias: null
};
}
}

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

@ -235,6 +235,9 @@
<div class="tb-form-panel-title" translate>widgets.time-series-chart.axis.y-axis</div>
<tb-time-series-chart-axis-settings
formControlName="yAxis"
[aliasController]="aliasController"
[dataKeyCallbacks]="callbacks"
[datasource]="datasource"
axisType="yAxis"
hideUnits
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">
</tb-time-series-chart-states-panel>
<tb-time-series-chart-y-axes-panel
[aliasController]="aliasController"
[dataKeyCallbacks]="callbacks"
[datasource]="datasource"
formControlName="yAxes"
[supportsUnitConversion]="widgetConfig.typeParameters.supportsUnitConversion"
(axisRemoved)="yAxisRemoved($event)">

5
ui-ngx/src/app/modules/home/components/widget/lib/chart/bar-chart-widget.models.ts

@ -31,10 +31,11 @@ import {
ChartBarSettings,
chartColorScheme
} from '@home/components/widget/lib/chart/chart.models';
import { AxisLimitConfig } from '@home/components/widget/lib/chart/time-series-chart.models';
export interface BarChartWidgetSettings extends LatestChartWidgetSettings {
axisMin?: number;
axisMax?: number;
axisMin?: number | string | AxisLimitConfig;
axisMax?: number | string | AxisLimitConfig;
axisTickLabelFont: Font;
axisTickLabelColor: string;
barSettings: ChartBarSettings;

5
ui-ngx/src/app/modules/home/components/widget/lib/chart/bars-chart.models.ts

@ -24,11 +24,12 @@ import {
chartColorScheme
} from '@home/components/widget/lib/chart/chart.models';
import { Font } from '@shared/models/widget-settings.models';
import { AxisLimitConfig } from '@home/components/widget/lib/chart/time-series-chart.models';
export interface BarsChartSettings extends LatestChartSettings {
polar: boolean;
axisMin?: number | string;
axisMax?: number | string;
axisMin?: number | string | AxisLimitConfig;
axisMax?: number | string | AxisLimitConfig;
axisTickLabelFont: Font;
axisTickLabelColor: string;
angleAxisStartAngle?: number;

225
ui-ngx/src/app/modules/home/components/widget/lib/chart/bars-chart.ts

@ -25,7 +25,7 @@ import { ComponentStyle } from '@shared/models/widget-settings.models';
import { LinearGradientObject } from 'zrender/lib/graphic/LinearGradient';
import tinycolor from 'tinycolor2';
import { BarDataItemOption, BarSeriesLabelOption } from 'echarts/types/src/chart/bar/BarSeries';
import { isDefinedAndNotNull } from '@core/utils';
import { isDefinedAndNotNull, isNumber, isString } from '@core/utils';
import {
ChartFillType,
ChartLabelPosition,
@ -35,9 +35,19 @@ import {
} from '@home/components/widget/lib/chart/chart.models';
import { ValueAxisBaseOption } from 'echarts/types/src/coord/axisCommonTypes';
import { RadiusAxisOption, YAXisOption } from 'echarts/types/dist/shared';
import { DataKey, Datasource, DatasourceType, widgetType } from '@shared/models/widget.models';
import { WidgetSubscriptionOptions } from '@core/api/widget-api.models';
import { ValueSourceType } from '@shared/models/widget-settings.models';
import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
import { AxisLimitConfig } from '@home/components/widget/lib/chart/time-series-chart.models';
export class TbBarsChart extends TbLatestChart<BarsChartSettings> {
private dynamicAxisMin: number | string = null;
private dynamicAxisMax: number | string = null;
private minLatestDataKey: DataKey = null;
private maxLatestDataKey: DataKey = null;
constructor(ctx: WidgetContext,
inputSettings: DeepPartial<PieChartSettings>,
chartElement: HTMLElement,
@ -52,6 +62,212 @@ export class TbBarsChart extends TbLatestChart<BarsChartSettings> {
return barsChartDefaultSettings;
}
protected initSettings() {
super.initSettings();
this.setupAxisLimits();
}
private setupAxisLimits(): void {
const axisLimitDatasources: Datasource[] = [];
if (isDefinedAndNotNull(this.settings.axisMin)) {
this.processAxisLimit(this.settings.axisMin, 'min', axisLimitDatasources);
}
if (isDefinedAndNotNull(this.settings.axisMax)) {
this.processAxisLimit(this.settings.axisMax, 'max', axisLimitDatasources);
}
this.subscribeForAxisLimits(axisLimitDatasources);
}
private processAxisLimit(
limit: any,
limitType: 'min' | 'max',
axisLimitDatasources: Datasource[]
): void {
if (limit && typeof limit === 'object' && 'type' in limit) {
const axisLimit = limit as AxisLimitConfig;
if (axisLimit.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 === (axisLimit.value as DataKey).label) ||
(d.type !== DataKeyType.function && d.name === (axisLimit.value as DataKey).name &&
d.type === (axisLimit.value as DataKey).type));
if (latestDataKey) {
break;
}
}
}
if (latestDataKey) {
if (limitType === 'min') {
this.minLatestDataKey = latestDataKey;
} else {
this.maxLatestDataKey = latestDataKey;
}
}
} else if (axisLimit.type === ValueSourceType.entity) {
const entityAliasId = this.ctx.aliasController.getEntityAliasId(axisLimit.entityAlias);
if (entityAliasId) {
let datasource = axisLimitDatasources.find(d => d.entityAliasId === entityAliasId);
const entityDataKey: DataKey = {
type: (axisLimit.value as DataKey).type,
name: (axisLimit.value as DataKey).name,
label: (axisLimit.value as DataKey).name,
settings: {
axisLimit: limitType
}
};
if (datasource) {
datasource.dataKeys.push(entityDataKey);
} else {
datasource = {
type: DatasourceType.entity,
name: axisLimit.entityAlias,
aliasName: axisLimit.entityAlias,
entityAliasId,
dataKeys: [entityDataKey]
};
axisLimitDatasources.push(datasource);
}
}
} else if (axisLimit.type === ValueSourceType.constant) {
const value = axisLimit.value as number;
if (limitType === 'min') {
this.dynamicAxisMin = value;
} else {
this.dynamicAxisMax = value;
}
}
} else if (typeof limit === 'number' || typeof limit === 'string') {
if (limitType === 'min') {
this.dynamicAxisMin = limit;
} else {
this.dynamicAxisMax = limit;
}
}
}
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 data of subscription.data) {
const limitType = data.dataKey.settings.axisLimit as ('min' | 'max');
if (data.data[0]) {
const value = this.parseAxisLimitData(data.data[0][1]);
if (limitType === 'min') {
if (this.dynamicAxisMin !== value) {
this.dynamicAxisMin = value;
update = true;
}
} else {
if (this.dynamicAxisMax !== value) {
this.dynamicAxisMax = value;
update = true;
}
}
}
}
}
if (this.latestChart && update) {
this.updateAxisLimits();
}
}
}
};
this.ctx.subscriptionApi.createSubscription(axisLimitsSubscriptionOptions, true).subscribe();
}
}
private parseAxisLimitData(data: any): number | null {
let value: number;
if (isDefinedAndNotNull(data)) {
if (isNumber(data)) {
value = data;
} else if (isString(data)) {
value = Number(data);
}
}
if (isDefinedAndNotNull(value) && !isNaN(value)) {
return value;
}
return null;
}
private updateAxisLimitsFromLatest(): boolean {
let update = false;
if (this.ctx.latestData) {
if (this.minLatestDataKey) {
const data = this.ctx.latestData.find(d => d.dataKey === this.minLatestDataKey);
if (data?.data[0]) {
const value = this.parseAxisLimitData(data.data[0][1]);
if (this.dynamicAxisMin !== value) {
this.dynamicAxisMin = value;
update = true;
}
}
}
if (this.maxLatestDataKey) {
const data = this.ctx.latestData.find(d => d.dataKey === this.maxLatestDataKey);
if (data?.data[0]) {
const value = this.parseAxisLimitData(data.data[0][1]);
if (this.dynamicAxisMax !== value) {
this.dynamicAxisMax = value;
update = true;
}
}
}
}
return update;
}
private updateAxisLimits(): void {
if (this.latestChart && !this.latestChart.isDisposed()) {
const axisTickLabelStyle = createChartTextStyle(this.settings.axisTickLabelFont,
this.settings.axisTickLabelColor, false, 'axis.tickLabel');
const valueAxis: ValueAxisBaseOption = {
type: 'value',
min: this.dynamicAxisMin,
max: this.dynamicAxisMax,
axisLabel: {
color: axisTickLabelStyle.color,
fontStyle: axisTickLabelStyle.fontStyle,
fontWeight: axisTickLabelStyle.fontWeight,
fontFamily: axisTickLabelStyle.fontFamily,
fontSize: axisTickLabelStyle.fontSize,
formatter: (value: any) => this.valueFormatter.format(value)
}
};
if (this.settings.polar) {
this.latestChartOption.radiusAxis = valueAxis as RadiusAxisOption;
} else {
this.latestChartOption.yAxis = valueAxis as YAXisOption;
}
this.latestChart.setOption(this.latestChartOption);
}
}
public latestUpdated() {
if (this.updateAxisLimitsFromLatest()) {
this.updateAxisLimits();
}
}
protected prepareLatestChartOption() {
let labelStyle: ComponentStyle = {};
if (this.settings.barSettings.showLabel) {
@ -89,10 +305,12 @@ export class TbBarsChart extends TbLatestChart<BarsChartSettings> {
const axisTickLabelStyle = createChartTextStyle(this.settings.axisTickLabelFont,
this.settings.axisTickLabelColor, false, 'axis.tickLabel');
const minValue = isDefinedAndNotNull(this.dynamicAxisMin) ? this.dynamicAxisMin : undefined;
const maxValue = isDefinedAndNotNull(this.dynamicAxisMax) ? this.dynamicAxisMax : undefined;
const valueAxis: ValueAxisBaseOption = {
type: 'value',
min: this.settings.axisMin,
max: this.settings.axisMax,
min: minValue,
max: maxValue,
axisLabel: {
color: axisTickLabelStyle.color,
fontStyle: axisTickLabelStyle.fontStyle,
@ -102,6 +320,7 @@ export class TbBarsChart extends TbLatestChart<BarsChartSettings> {
formatter: (value: any) => this.valueFormatter.format(value)
}
};
if (this.settings.polar) {
this.latestChartOption.polar = {
radius: '100%'

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

@ -381,12 +381,18 @@ export interface TimeSeriesChartYAxisSettings extends TimeSeriesChartAxisSetting
decimals?: number;
interval?: number;
splitNumber?: number;
min?: number | string;
max?: number | string;
min?: number | string | AxisLimitConfig;
max?: number | string | AxisLimitConfig;
ticksGenerator?: TimeSeriesChartTicksGenerator | string;
ticksFormatter?: TimeSeriesChartTicksFormatter | string;
}
export interface AxisLimitConfig {
entityAlias: string;
type: ValueSourceType;
value: number | DataKey;
}
export const timeSeriesChartYAxisValid = (axis: TimeSeriesChartYAxisSettings): boolean =>
!(!axis.id || isUndefinedOrNull(axis.order));
@ -867,6 +873,11 @@ export interface TimeSeriesChartAxis {
id: string;
settings: TimeSeriesChartAxisSettings;
option: CartesianAxisOption;
dynamicMin?: string| number;
dynamicMax?: string| number;
minLatestDataKey?: DataKey;
maxLatestDataKey?: DataKey;
unitConvertor?: (value: number) => number;
}
export interface TimeSeriesChartYAxis extends TimeSeriesChartAxis {
@ -926,6 +937,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 +973,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,

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

@ -17,6 +17,7 @@
import { WidgetContext } from '@home/models/widget-component.models';
import {
adjustTimeAxisExtentToData,
AxisLimitConfig,
calculateThresholdsOffset,
createTimeSeriesVisualMapOption,
createTimeSeriesXAxis,
@ -61,6 +62,8 @@ import {
isDefined,
isDefinedAndNotNull,
isEqual,
isNumber,
isString,
mergeDeep
} from '@core/utils';
import { DataKey, Datasource, DatasourceType, FormattedData, widgetType } from '@shared/models/widget.models';
@ -287,8 +290,14 @@ export class TbTimeSeriesChart {
}
}
}
if (this.updateAxisLimitsFromLatest()) {
update = true;
}
if (this.timeSeriesChart && update) {
this.updateSeriesData();
if (this.updateAxisLimitsFromLatest()) {
this.updateAxisLimits();
}
}
}
@ -550,6 +559,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<TimeSeriesChartYAxisSettings>({} as TimeSeriesChartYAxisSettings,
defaultTimeSeriesChartYAxisSettings, yAxisSettings);
@ -563,8 +573,98 @@ 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.processAxisLimit(axisSettings.min, 'min', yAxis, axisLimitDatasources, unitConvertor);
}
if (isDefinedAndNotNull(axisSettings.max)) {
this.processAxisLimit(axisSettings.max, 'max', yAxis, axisLimitDatasources, unitConvertor);
}
if (yAxis.minLatestDataKey || yAxis.maxLatestDataKey) {
yAxis.unitConvertor = unitConvertor;
}
this.yAxisList.push(yAxis);
}
this.subscribeForAxisLimits(axisLimitDatasources);
}
private processAxisLimit(
limit: any,
limitType: 'min' | 'max',
yAxis: TimeSeriesChartYAxis,
axisLimitDatasources: Datasource[],
unitConvertor?: (value: number) => number
): void {
if (limit && typeof limit === 'object' && 'type' in limit) {
const axisLimit = limit as AxisLimitConfig;
if (axisLimit.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 === (axisLimit.value as DataKey).label) ||
(d.type !== DataKeyType.function && d.name === (axisLimit.value as DataKey).name &&
d.type === (axisLimit.value as DataKey).type));
if (latestDataKey) {
break;
}
}
}
if (latestDataKey) {
if (limitType === 'min') {
yAxis.minLatestDataKey = latestDataKey;
} else {
yAxis.maxLatestDataKey = latestDataKey;
}
}
} else if (axisLimit.type === ValueSourceType.entity) {
const entityAliasId = this.ctx.aliasController.getEntityAliasId(axisLimit.entityAlias);
if (entityAliasId) {
let datasource = axisLimitDatasources.find(d => d.entityAliasId === entityAliasId);
const entityDataKey: DataKey = {
type: (axisLimit.value as DataKey).type,
name: (axisLimit.value as DataKey).name,
label: (axisLimit.value as DataKey).name,
settings: {
yAxisId: yAxis.id,
axisLimit: limitType
}
};
if (datasource) {
datasource.dataKeys.push(entityDataKey);
} else {
datasource = {
type: DatasourceType.entity,
name: axisLimit.entityAlias,
aliasName: axisLimit.entityAlias,
entityAliasId,
dataKeys: [entityDataKey]
};
axisLimitDatasources.push(datasource);
}
}
} else if (axisLimit.type === ValueSourceType.constant) {
const value = unitConvertor ? unitConvertor(axisLimit.value as number) : axisLimit.value as number;
if (limitType === 'min') {
yAxis.dynamicMin = value;
yAxis.option.min = value;
} else {
yAxis.dynamicMax = value;
yAxis.option.max = value;
}
}
} else if (typeof limit === 'number' || typeof limit === 'string') {
let value = limit;
if (typeof value === 'number' && unitConvertor) {
value = unitConvertor(value);
}
if (limitType === 'min') {
yAxis.dynamicMin = value;
yAxis.option.min = value;
} else {
yAxis.dynamicMax = value;
yAxis.option.max = value;
}
}
}
private setupVisualMap(): void {
@ -622,6 +722,45 @@ 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 (limitType === 'min') {
yAxis.dynamicMin = value;
yAxis.option.min = value;
} else {
yAxis.dynamicMax = 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 +853,58 @@ export class TbTimeSeriesChart {
}
}
private updateAxisLimitsFromLatest(): boolean {
let update = false;
if (this.ctx.latestData) {
for (const yAxis of this.yAxisList) {
if (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.dynamicMin !== value) {
yAxis.dynamicMin = value;
yAxis.option.min = value;
update = true;
}
}
}
if (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.dynamicMax !== value) {
yAxis.dynamicMax = value;
yAxis.option.max = value;
update = true;
}
}
}
}
}
return update;
}
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 +1027,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 &&

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

@ -26,6 +26,8 @@ import {
import {
LatestChartWidgetSettingsComponent
} from '@home/components/widget/lib/settings/chart/latest-chart-widget-settings.component';
import { AxisLimitConfig } from '@home/components/widget/lib/chart/time-series-chart.models';
import { ValueSourceType } from '@shared/models/widget-settings.models';
@Component({
selector: 'tb-bar-chart-widget-settings',
@ -57,4 +59,32 @@ export class BarChartWidgetSettingsComponent extends LatestChartWidgetSettingsCo
latestChartWidgetSettingsForm.addControl('axisTickLabelFont', this.fb.control(settings.axisTickLabelFont, []));
latestChartWidgetSettingsForm.addControl('axisTickLabelColor', this.fb.control(settings.axisTickLabelColor, []));
}
protected prepareInputSettings(settings: WidgetSettings): WidgetSettings {
settings.axisMin = this.normalizeAxisLimit(settings.axisMin);
settings.axisMax = this.normalizeAxisLimit(settings.axisMax);
return super.prepareInputSettings(settings);
}
private normalizeAxisLimit(limit: any): AxisLimitConfig {
if (limit && typeof limit === 'object' && 'type' in limit) {
return {
type: limit.type || ValueSourceType.constant,
value: limit.value ?? null,
entityAlias: limit.entityAlias ?? null
};
}
if (typeof limit === 'number') {
return {
type: ValueSourceType.constant,
value: limit,
entityAlias: null
};
}
return {
type: ValueSourceType.constant,
value: null,
entityAlias: null
};
}
}

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>
<tb-time-series-chart-axis-settings
formControlName="yAxis"
[aliasController]="aliasController"
[dataKeyCallbacks]="callbacks"
[datasource]="datasource"
axisType="yAxis"
advanced
hideUnits

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

@ -263,18 +263,8 @@
<div class="tb-form-panel">
<div class="tb-form-panel-title" translate>widgets.bar-chart.bar-axis</div>
<div class="tb-form-row space-between column-xs">
<div translate>widgets.chart.chart-axis.scale</div>
<div translate>widgets.chart.chart-axis.scale-appearance</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="axisMin"
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="axisMax"
type="number" placeholder="{{ 'widgets.chart.chart-axis.scale-auto' | translate }}">
</mat-form-field>
<tb-font-settings formControlName="axisTickLabelFont"
clearButton
disabledLineHeight
@ -287,6 +277,35 @@
</tb-color-input>
</div>
</div>
<div class="tb-form-row column align-start">
<div translate>widgets.chart.chart-axis.scale-limits</div>
<div class="tb-form-table w-full">
<div class="tb-form-table-header w-full">
<div class="tb-form-table-header-cell flex-grow flex-shrink basis-1/5" translate>widgets.chart.chart-axis.limit</div>
<div class="tb-form-table-header-cell flex-grow flex-shrink basis-40%" translate>widgets.chart.chart-axis.source</div>
<div class="tb-form-table-header-cell flex-grow flex-shrink basis-40%" translate>widgets.chart.chart-axis.key-value</div>
</div>
<div class="tb-form-table-body">
<div class="tb-form-table-body">
<tb-axis-scale-row
[datasource]="datasource"
[callbacks]="callbacks"
[aliasController]="aliasController"
formControlName="axisMin"
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
[datasource]="datasource"
[callbacks]="callbacks"
[aliasController]="aliasController"
formControlName="axisMax"
labelKey="widgets.chart.chart-axis.scale-max">
</tb-axis-scale-row>
</div>
</div>
</div>
</div>
</div>
</div>
</ng-container>
@ -311,18 +330,8 @@
<div class="tb-form-panel">
<div class="tb-form-panel-title" translate>widgets.polar-area-chart.polar-axis</div>
<div class="tb-form-row space-between column-xs">
<div translate>widgets.chart.chart-axis.scale</div>
<div translate>widgets.chart.chart-axis.scale-appearance</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="axisMin"
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="axisMax"
type="number" placeholder="{{ 'widgets.chart.chart-axis.scale-auto' | translate }}">
</mat-form-field>
<tb-font-settings formControlName="axisTickLabelFont"
clearButton
disabledLineHeight
@ -341,6 +350,33 @@
<input matInput formControlName="angleAxisStartAngle" type="number" min="0" max="360" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
</div>
<div class="tb-form-row column align-start">
<div translate>widgets.chart.chart-axis.scale-limits</div>
<div class="tb-form-table w-full">
<div class="tb-form-table-header w-full">
<div class="tb-form-table-header-cell flex-grow flex-shrink basis-1/5" translate>widgets.chart.chart-axis.limit</div>
<div class="tb-form-table-header-cell flex-grow flex-shrink basis-40%" translate>widgets.chart.chart-axis.source</div>
<div class="tb-form-table-header-cell flex-grow flex-shrink basis-40%" translate>widgets.chart.chart-axis.key-value</div>
</div>
<div class="tb-form-table-body">
<tb-axis-scale-row
[datasource]="datasource"
[callbacks]="callbacks"
[aliasController]="aliasController"
formControlName="axisMin"
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
[datasource]="datasource"
[callbacks]="callbacks"
[aliasController]="aliasController"
formControlName="axisMax"
labelKey="widgets.chart.chart-axis.scale-max">
</tb-axis-scale-row>
</div>
</div>
</div>
</div>
</div>
</ng-container>

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

@ -22,6 +22,7 @@ import {
LatestChartWidgetSettings
} from '@home/components/widget/lib/chart/latest-chart.models';
import {
Datasource,
legendPositions,
legendPositionTranslationMap,
WidgetSettings,
@ -103,6 +104,15 @@ export abstract class LatestChartWidgetSettingsComponent<S extends LatestChartWi
return layout === DoughnutLayout.with_total;
}
public get datasource(): Datasource {
const datasources: Datasource[] = this.widgetConfig.config.datasources;
if (datasources && datasources.length) {
return datasources[0];
} else {
return null;
}
}
constructor(protected store: Store<AppState>,
protected fb: UntypedFormBuilder) {
super(store);

33
ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/polar-area-chart-widget-settings.component.ts

@ -26,6 +26,8 @@ import {
import {
LatestChartWidgetSettingsComponent
} from '@home/components/widget/lib/settings/chart/latest-chart-widget-settings.component';
import { AxisLimitConfig } from '@home/components/widget/lib/chart/time-series-chart.models';
import { ValueSourceType } from '@shared/models/widget-settings.models';
@Component({
selector: 'tb-polar-area-chart-widget-settings',
@ -52,10 +54,37 @@ export class PolarAreaChartWidgetSettingsComponent extends LatestChartWidgetSett
protected setupLatestChartControls(latestChartWidgetSettingsForm: UntypedFormGroup, settings: WidgetSettings) {
latestChartWidgetSettingsForm.addControl('barSettings', this.fb.control(settings.barSettings, []));
latestChartWidgetSettingsForm.addControl('axisMin', this.fb.control(settings.axisMin, []));
latestChartWidgetSettingsForm.addControl('axisMax', this.fb.control(settings.axisMax, []));
latestChartWidgetSettingsForm.addControl('axisMin', this.fb.control(settings.axisMin));
latestChartWidgetSettingsForm.addControl('axisMax', this.fb.control(settings.axisMax));
latestChartWidgetSettingsForm.addControl('axisTickLabelFont', this.fb.control(settings.axisTickLabelFont, []));
latestChartWidgetSettingsForm.addControl('axisTickLabelColor', this.fb.control(settings.axisTickLabelColor, [Validators.min(0)]));
latestChartWidgetSettingsForm.addControl('angleAxisStartAngle', this.fb.control(settings.angleAxisStartAngle, []));
}
protected prepareInputSettings(settings: WidgetSettings): WidgetSettings {
settings.axisMin = this.normalizeAxisLimit(settings.axisMin);
settings.axisMax = this.normalizeAxisLimit(settings.axisMax);
return super.prepareInputSettings(settings);
}
private normalizeAxisLimit(limit: any): AxisLimitConfig {
if (limit && typeof limit === 'object' && 'type' in limit) {
return {
type: limit.type || ValueSourceType.constant,
value: limit.value ?? null,
entityAlias: limit.entityAlias ?? null
};
}
if (typeof limit === 'number') {
return {
type: ValueSourceType.constant,
value: limit,
entityAlias: null
};
}
return {
type: ValueSourceType.constant,
value: null,
entityAlias: null
};
}
}

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>
<tb-time-series-chart-axis-settings
formControlName="yAxis"
[aliasController]="aliasController"
[dataKeyCallbacks]="callbacks"
[datasource]="datasource"
axisType="yAxis"
hideUnits
hideDecimals>

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" [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"
formControlName="value">
</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"
formControlName="value">
</tb-data-key-input>
</div>
</div>

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

@ -0,0 +1,15 @@
/**
* 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.
*/

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

@ -0,0 +1,204 @@
///
/// 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,
UntypedFormGroup, ValidationErrors, Validator,
Validators,
} from '@angular/forms';
import { ValueSourceType, ValueSourceTypes, ValueSourceTypeTranslation } from '@shared/models/widget-settings.models';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { AxisLimitConfig } from '@home/components/widget/lib/chart/time-series-chart.models';
import { 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 { isEqual } from '@core/utils';
@Component({
selector: 'tb-axis-scale-row',
templateUrl: './axis-scale-row.component.html',
styleUrl: './axis-scale-row.component.scss',
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;
private propagateChanges: (value: any) => void = () => {};
private modelValue: AxisLimitConfig | null = null;
constructor(private fb: UntypedFormBuilder,
private destroyRef: DestroyRef) {
}
ngOnInit() {
this.limitForm = this.fb.group({
type: [ValueSourceType.constant],
value: [null],
entityAlias: [null]
});
this.subscribeToTypeChanges();
this.limitForm.valueChanges
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(value => {
this.updateValidators();
this.updateView(value);
});
}
writeValue(value: AxisLimitConfig) {
this.modelValue = value;
if (!this.limitForm) {
return;
}
if (value) {
this.limitForm.patchValue({
type: value.type ?? ValueSourceType.constant,
value: value.value ?? null,
entityAlias: value.entityAlias ?? null
},
{ emitEvent: false }
);
} else {
this.limitForm.patchValue({
type: ValueSourceType.constant,
value: null,
entityAlias: null
},
{ emitEvent: false }
);
}
this.updateValidators();
}
registerOnChange(fn: any) {
this.propagateChanges = fn;
}
registerOnTouched(fn: any) {
}
validate(): ValidationErrors | null {
return this.limitForm.valid ? null : {
axisLimitForm: {
valid: false,
errors: this.getFormErrors()
}
};
}
private getFormErrors(): any {
const errors: any = {};
Object.keys(this.limitForm.controls).forEach(key => {
const control = this.limitForm.get(key);
if (control && control.errors) {
errors[key] = control.errors;
}
});
return errors;
}
private updateValidators() {
const axisTypeControl = this.limitForm.get('type');
const axisValueControl = this.limitForm.get('value');
const axisEntityAliasControl = this.limitForm.get('entityAlias');
if (axisTypeControl && axisValueControl && axisEntityAliasControl) {
const type = axisTypeControl.value;
if (type === ValueSourceType.latestKey || type === ValueSourceType.entity) {
axisValueControl.setValidators([Validators.required]);
} else {
axisValueControl.clearValidators();
}
if (type === ValueSourceType.entity) {
axisEntityAliasControl.setValidators([Validators.required]);
} else {
axisEntityAliasControl.clearValidators();
}
axisValueControl.updateValueAndValidity({ emitEvent: false });
axisEntityAliasControl.updateValueAndValidity({ emitEvent: false });
}
}
private subscribeToTypeChanges() {
this.limitForm.controls.type.valueChanges
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(() => {
const axisValueControl = this.limitForm.get('value');
const axisEntityAliasControl = this.limitForm.get('entityAlias');
if (axisValueControl) {
axisValueControl.setValue(null, { emitEvent: true });
}
if (axisEntityAliasControl) {
axisEntityAliasControl.setValue(null, { emitEvent: true });
}
});
}
private updateView(value: AxisLimitConfig) {
if (!isEqual(this.modelValue, value)) {
this.modelValue = value;
this.propagateChanges(value);
}
}
}

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-panel-content">
<tb-time-series-chart-axis-settings
[aliasController]="aliasController"
[dataKeyCallbacks]="dataKeyCallbacks"
[datasource]="datasource"
formControlName="axis"
alwaysExpanded
[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
} from '@home/components/widget/lib/chart/time-series-chart.models';
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({
selector: 'tb-time-series-chart-axis-settings-panel',
@ -32,6 +35,15 @@ import { coerceBoolean } from '@shared/decorators/coercion';
})
export class TimeSeriesChartAxisSettingsPanelComponent implements OnInit {
@Input()
aliasController: IAliasController;
@Input()
dataKeyCallbacks: DataKeysCallbacks;
@Input()
datasource: Datasource;
@Input()
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 }}">
</mat-form-field>
</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>
</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>
</ng-container>

41
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,15 @@ export class TimeSeriesChartAxisSettingsComponent implements OnInit, ControlValu
registerOnTouched(_fn: any): void {
}
validate(): ValidationErrors | null {
return this.axisSettingsFormGroup.valid ? null : {
axisSettings: {
valid: false,
errors: this.getFormErrors()
}
};
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
if (isDisabled) {
@ -222,6 +250,17 @@ export class TimeSeriesChartAxisSettingsComponent implements OnInit, ControlValu
}
}
private getFormErrors(): any {
const errors: any = {};
Object.keys(this.axisSettingsFormGroup.controls).forEach(key => {
const control = this.axisSettingsFormGroup.get(key);
if (control && control.errors) {
errors[key] = control.errors;
}
});
return errors;
}
private updateModel() {
this.modelValue = this.axisSettingsFormGroup.getRawValue();
this.propagateChange(this.modelValue);

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

@ -47,6 +47,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 { Datasource } from '@app/shared/public-api';
@Component({
selector: 'tb-time-series-chart-y-axes-panel',
@ -68,6 +71,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;

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

@ -42,6 +42,7 @@ 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';
@Component({
selector: 'tb-time-series-chart-y-axis-row',
@ -85,6 +86,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,
@ -165,7 +167,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
});

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

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

@ -7638,6 +7638,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",

3
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%'

Loading…
Cancel
Save