From c688f516c7d7e73869211673b018f1defa4d0765 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Mon, 11 Mar 2024 15:44:56 +0200 Subject: [PATCH] UI: Multiple Y axes configuration support for time series chart. --- ...e-series-chart-basic-config.component.html | 15 +- ...ime-series-chart-basic-config.component.ts | 39 +++- .../basic/common/data-key-row.component.html | 39 ++-- .../basic/common/data-key-row.component.scss | 17 ++ .../basic/common/data-key-row.component.ts | 46 +++- .../common/data-keys-panel.component.html | 5 +- .../common/data-keys-panel.component.scss | 15 ++ .../basic/common/data-keys-panel.component.ts | 8 + .../aggregated-value-card-widget.component.ts | 30 +-- .../value-chart-card-widget.component.ts | 11 +- .../lib/chart/time-series-chart.models.ts | 216 ++++++++++++++---- .../widget/lib/chart/time-series-chart.ts | 102 +++++++-- ...e-series-chart-key-settings.component.html | 12 + ...ime-series-chart-key-settings.component.ts | 12 +- ...eries-chart-widget-settings.component.html | 15 +- ...-series-chart-widget-settings.component.ts | 37 ++- ...-series-chart-axis-settings.component.html | 18 +- ...me-series-chart-axis-settings.component.ts | 18 +- ...-series-chart-threshold-row.component.html | 28 ++- ...-series-chart-threshold-row.component.scss | 14 ++ ...me-series-chart-threshold-row.component.ts | 32 ++- ...rt-threshold-settings-panel.component.html | 10 + ...hart-threshold-settings-panel.component.ts | 6 +- ...ries-chart-thresholds-panel.component.html | 2 + ...ries-chart-thresholds-panel.component.scss | 11 + ...series-chart-thresholds-panel.component.ts | 5 +- ...e-series-chart-y-axes-panel.component.html | 63 +++++ ...e-series-chart-y-axes-panel.component.scss | 87 +++++++ ...ime-series-chart-y-axes-panel.component.ts | 180 +++++++++++++++ ...ime-series-chart-y-axis-row.component.html | 72 ++++++ ...ime-series-chart-y-axis-row.component.scss | 95 ++++++++ .../time-series-chart-y-axis-row.component.ts | 201 ++++++++++++++++ ...chart-y-axis-settings-panel.component.html | 43 ++++ ...chart-y-axis-settings-panel.component.scss | 49 ++++ ...s-chart-y-axis-settings-panel.component.ts | 66 ++++++ .../common/widget-settings-common.module.ts | 15 ++ ui-ngx/src/app/shared/models/common.ts | 19 ++ .../assets/locale/locale.constant-en_US.json | 10 +- 38 files changed, 1502 insertions(+), 161 deletions(-) create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axes-panel.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axes-panel.component.scss create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axes-panel.component.ts create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-row.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-row.component.scss create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-row.component.ts create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-settings-panel.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-settings-panel.component.scss create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-settings-panel.component.ts create mode 100644 ui-ngx/src/app/shared/models/common.ts diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/time-series-chart-basic-config.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/time-series-chart-basic-config.component.html index 3d099c2645..1f62a13c4e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/time-series-chart-basic-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/time-series-chart-basic-config.component.html @@ -25,6 +25,10 @@ forceSingleDatasource formControlName="datasources"> + + + [widgetConfig]="widgetConfig?.config" + [yAxisIds]="yAxisIds">
widget-config.appearance
@@ -104,11 +111,7 @@
-
widgets.time-series-chart.axis.axes
- - +
widgets.time-series-chart.axis.x-axis
diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/time-series-chart-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/time-series-chart-basic-config.component.ts index 5307147b0e..f9ee35da23 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/time-series-chart-basic-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/time-series-chart-basic-config.component.ts @@ -45,7 +45,11 @@ import { TimeSeriesChartWidgetSettings } from '@home/components/widget/lib/chart/time-series-chart-widget.models'; import { EChartsTooltipTrigger } from '@home/components/widget/lib/chart/echarts-widget.models'; -import { TimeSeriesChartType } from '@home/components/widget/lib/chart/time-series-chart.models'; +import { + TimeSeriesChartKeySettings, TimeSeriesChartThreshold, + TimeSeriesChartType, TimeSeriesChartYAxes, + TimeSeriesChartYAxisId +} from '@home/components/widget/lib/chart/time-series-chart.models'; @Component({ selector: 'tb-time-series-chart-basic-config', @@ -63,6 +67,11 @@ export class TimeSeriesChartBasicConfigComponent extends BasicWidgetConfigCompon } } + public get yAxisIds(): TimeSeriesChartYAxisId[] { + const yAxes: TimeSeriesChartYAxes = this.timeSeriesChartWidgetConfigForm.get('yAxes').value; + return Object.keys(yAxes); + } + TimeSeriesChartType = TimeSeriesChartType; EChartsTooltipTrigger = EChartsTooltipTrigger; @@ -86,6 +95,15 @@ export class TimeSeriesChartBasicConfigComponent extends BasicWidgetConfigCompon super(store, widgetConfigComponent); } + public yAxisRemoved(yAxisId: TimeSeriesChartYAxisId): void { + if (this.widgetConfig.config.datasources && this.widgetConfig.config.datasources.length > 1) { + for (let i = 1; i < this.widgetConfig.config.datasources.length; i++) { + const datasource = this.widgetConfig.config.datasources[i]; + this.removeYaxisId(datasource.dataKeys, yAxisId); + } + } + } + protected configForm(): UntypedFormGroup { return this.timeSeriesChartWidgetConfigForm; } @@ -109,6 +127,8 @@ export class TimeSeriesChartBasicConfigComponent extends BasicWidgetConfigCompon this.timeSeriesChartWidgetConfigForm = this.fb.group({ timewindowConfig: [getTimewindowConfig(configData.config), []], datasources: [configData.config.datasources, []], + + yAxes: [settings.yAxes, []], series: [this.getSeries(configData.config.datasources), []], thresholds: [settings.thresholds, []], @@ -126,7 +146,6 @@ export class TimeSeriesChartBasicConfigComponent extends BasicWidgetConfigCompon dataZoom: [settings.dataZoom, []], stack: [settings.stack, []], - yAxis: [settings.yAxis, []], xAxis: [settings.xAxis, []], noAggregationBarWidthSettings: [settings.noAggregationBarWidthSettings, []], @@ -180,7 +199,7 @@ export class TimeSeriesChartBasicConfigComponent extends BasicWidgetConfigCompon this.widgetConfig.config.settings.dataZoom = config.dataZoom; this.widgetConfig.config.settings.stack = config.stack; - this.widgetConfig.config.settings.yAxis = config.yAxis; + this.widgetConfig.config.settings.yAxes = config.yAxes; this.widgetConfig.config.settings.xAxis = config.xAxis; this.widgetConfig.config.settings.noAggregationBarWidthSettings = config.noAggregationBarWidthSettings; @@ -303,6 +322,20 @@ export class TimeSeriesChartBasicConfigComponent extends BasicWidgetConfigCompon } } + private removeYaxisId(series: DataKey[], yAxisId: TimeSeriesChartYAxisId): boolean { + let changed = false; + if (series) { + series.forEach(key => { + const keySettings = ((key.settings || {}) as TimeSeriesChartKeySettings); + if (keySettings.yAxisId === yAxisId) { + keySettings.yAxisId = 'default'; + changed = true; + } + }); + } + return changed; + } + private getCardButtons(config: WidgetConfig): string[] { const buttons: string[] = []; if (isUndefined(config.enableFullscreen) || config.enableFullscreen) { diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-key-row.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-key-row.component.html index 3ef6891808..0c8be80bf6 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-key-row.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-key-row.component.html @@ -38,6 +38,30 @@ +
+ + + + + {{ timeSeriesChartSeriesTypeIcons.get(keyRowFormGroup.get('timeSeriesType').value) }} + + + + {{ timeSeriesChartSeriesTypeIcons.get(type) }} + {{ timeSeriesChartSeriesTypeTranslations.get(type) | translate }} + + + +
+
+ + + + {{ yAxis }} + + + +
@@ -54,21 +78,6 @@
widget-config.decimals-suffix
-
- - - - - {{ timeSeriesChartSeriesTypeIcons.get(keyRowFormGroup.get('timeSeriesType').value) }} - - - - {{ timeSeriesChartSeriesTypeIcons.get(type) }} - {{ timeSeriesChartSeriesTypeTranslations.get(type) | translate }} - - - -
= { dataZoom: false, xAxis: { show: false }, - yAxis: { - show: true, - showLine: false, - showTicks: false, - showTickLabels: false, - showSplitLines: true, - min: 'dataMin', - max: 'dataMax', - intervalCalculator: - 'var scale = axis.scale; return !scale.isBlank() ? ((scale.getExtent()[1] - scale.getExtent()[0]) / 2) : undefined;' + yAxes: { + default: { + show: true, + showLine: false, + showTicks: false, + showTickLabels: false, + showSplitLines: true, + min: 'dataMin', + max: 'dataMax', + intervalCalculator: + 'var scale = axis.scale; return !scale.isBlank() ? ((scale.getExtent()[1] - scale.getExtent()[0]) / 2) : undefined;' + } }, tooltipDateInterval: false, tooltipDateFormat: simpleDateFormat('dd MMM yyyy HH:mm:ss') - } as TimeSeriesChartSettings; + }; this.lineChart = new TbTimeSeriesChart(this.ctx, settings, this.chartElement.nativeElement, this.renderer, true); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/cards/value-chart-card-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/cards/value-chart-card-widget.component.ts index f5a305a247..a7981b3acf 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/cards/value-chart-card-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/cards/value-chart-card-widget.component.ts @@ -57,6 +57,7 @@ import { TimeSeriesChartSeriesType, TimeSeriesChartSettings } from '@home/components/widget/lib/chart/time-series-chart.models'; +import { DeepPartial } from '@shared/models/common'; const layoutHeight = 56; const valueRelativeWidth = 0.35; @@ -162,17 +163,19 @@ export class ValueChartCardWidgetComponent implements OnInit, AfterViewInit, OnD } public ngAfterViewInit() { - const settings: TimeSeriesChartSettings = { + const settings: DeepPartial = { dataZoom: false, xAxis: { show: false }, - yAxis: { - show: false + yAxes: { + default: { + show: false, + } }, tooltipDateInterval: false, tooltipDateFormat: simpleDateFormat('dd MMM yyyy HH:mm:ss') - } as TimeSeriesChartSettings; + }; this.lineChart = new TbTimeSeriesChart(this.ctx, settings, this.chartElement.nativeElement, this.renderer, false); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.models.ts index edfaa3692d..9af41fa378 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.models.ts @@ -25,7 +25,15 @@ import { import { ComponentStyle, Font, simpleDateFormat, textStyle } from '@shared/models/widget-settings.models'; import { XAXisOption, YAXisOption } from 'echarts/types/dist/shared'; import { CustomSeriesOption, LineSeriesOption } from 'echarts/charts'; -import { formatValue, isDefinedAndNotNull, isUndefined, isUndefinedOrNull, parseFunction } from '@core/utils'; +import { + formatValue, + isDefinedAndNotNull, + isNumeric, + isUndefined, + isUndefinedOrNull, + mergeDeep, + parseFunction +} from '@core/utils'; import { LinearGradientObject } from 'zrender/lib/graphic/LinearGradient'; import tinycolor from 'tinycolor2'; import Axis2D from 'echarts/types/src/coord/cartesian/Axis2D'; @@ -289,13 +297,82 @@ export interface TimeSeriesChartAxisSettings { splitLinesColor: string; } +export type TimeSeriesChartYAxisId = 'default' | string; + export interface TimeSeriesChartYAxisSettings extends TimeSeriesChartAxisSettings { + id?: TimeSeriesChartYAxisId; + order?: number; + units?: string; + decimals?: number; min?: number | string; max?: number | string; intervalCalculator?: string; ticksFormatter?: string; } +export const timeSeriesChartYAxisValid = (axis: TimeSeriesChartYAxisSettings): boolean => + !(!axis.id || isUndefinedOrNull(axis.order)); + +export const timeSeriesChartYAxisValidator = (control: AbstractControl): ValidationErrors | null => { + const axis: TimeSeriesChartYAxisSettings = control.value; + if (!timeSeriesChartYAxisValid(axis)) { + return { + axis: true + }; + } + return null; +}; + +export const getNextTimeSeriesYAxisId = (axes: TimeSeriesChartYAxisSettings[]): TimeSeriesChartYAxisId => { + let id = 0; + for (const axis of axes) { + const existingId = axis.id; + if (existingId.startsWith('axis')) { + const idSuffix = existingId.substring('axis'.length); + if (isNumeric(idSuffix)) { + id = Math.max(id, Number(idSuffix)); + } + } + } + return 'axis' + (id+1); +}; + +export const defaultTimeSeriesChartYAxisSettings: TimeSeriesChartYAxisSettings = { + units: null, + decimals: 0, + show: true, + label: '', + labelFont: { + family: 'Roboto', + size: 12, + sizeUnit: 'px', + style: 'normal', + weight: '600', + lineHeight: '1' + }, + labelColor: timeSeriesChartColorScheme['axis.label'].light, + position: AxisPosition.left, + showTickLabels: true, + tickLabelFont: { + family: 'Roboto', + size: 12, + sizeUnit: 'px', + style: 'normal', + weight: '400', + lineHeight: '1' + }, + tickLabelColor: timeSeriesChartColorScheme['axis.tickLabel'].light, + ticksFormatter: null, + showTicks: true, + ticksColor: timeSeriesChartColorScheme['axis.ticks'].light, + showLine: true, + lineColor: timeSeriesChartColorScheme['axis.line'].light, + showSplitLines: true, + splitLinesColor: timeSeriesChartColorScheme['axis.splitLine'].light +}; + +export type TimeSeriesChartYAxes = {[id: TimeSeriesChartYAxisId]: TimeSeriesChartYAxisSettings}; + export interface TimeSeriesChartThreshold { type: TimeSeriesChartThresholdType; value?: number; @@ -304,6 +381,7 @@ export interface TimeSeriesChartThreshold { entityAlias?: string; entityKey?: string; entityKeyType?: DataKeyType.attribute | DataKeyType.timeseries; + yAxisId: TimeSeriesChartYAxisId; units?: string; decimals?: number; lineColor: string; @@ -320,7 +398,7 @@ export interface TimeSeriesChartThreshold { } export const timeSeriesChartThresholdValid = (threshold: TimeSeriesChartThreshold): boolean => { - if (!threshold.type) { + if (!threshold.type || !threshold.yAxisId) { return false; } switch (threshold.type) { @@ -355,7 +433,8 @@ export const timeSeriesChartThresholdValidator = (control: AbstractControl): Val export const timeSeriesChartThresholdDefaultSettings: TimeSeriesChartThreshold = { type: TimeSeriesChartThresholdType.constant, - units: '', + yAxisId: 'default', + units: null, decimals: 0, lineColor: timeSeriesChartColorScheme['threshold.line'].light, lineType: TimeSeriesChartLineType.solid, @@ -404,13 +483,61 @@ export interface TimeSeriesChartNoAggregationBarWidthSettings { barWidth?: TimeSeriesChartBarWidth; } +export enum TimeSeriesChartAnimationEasing { + linear = 'linear', + quadraticIn = 'quadraticIn', + quadraticOut = 'quadraticOut', + quadraticInOut = 'quadraticInOut', + cubicIn = 'cubicIn', + cubicOut = 'cubicOut', + cubicInOut = 'cubicInOut', + quarticIn = 'quarticIn', + quarticOut = 'quarticOut', + quarticInOut = 'quarticInOut', + quinticIn = 'quinticIn', + quinticOut = 'quinticOut', + quinticInOut = 'quinticInOut', + sinusoidalIn = 'sinusoidalIn', + sinusoidalOut = 'sinusoidalOut', + sinusoidalInOut = 'sinusoidalInOut', + exponentialIn = 'exponentialIn', + exponentialOut = 'exponentialOut', + exponentialInOut = 'exponentialInOut', + circularIn = 'circularIn', + circularOut = 'circularOut', + circularInOut = 'circularInOut', + elasticIn = 'elasticIn', + elasticOut = 'elasticOut', + elasticInOut = 'elasticInOut', + backIn = 'backIn', + backOut = 'backOut', + backInOut = 'backInOut', + bounceIn = 'bounceIn', + bounceOut = 'bounceOut', + bounceInOut = 'bounceInOut' +} + +export const timeSeriesChartAnimationEasings = Object.keys(TimeSeriesChartAnimationEasing) as TimeSeriesChartAnimationEasing[]; + +export interface TimeSeriesChartAnimationSettings { + animation: boolean; + animationThreshold: number; + animationDuration: number; + animationEasing: TimeSeriesChartAnimationEasing; + animationDelay: number; + animationDurationUpdate: number; + animationEasingUpdate: TimeSeriesChartAnimationEasing; + animationDelayUpdate: number; +} + export interface TimeSeriesChartSettings extends EChartsTooltipWidgetSettings { thresholds: TimeSeriesChartThreshold[]; darkMode: boolean; dataZoom: boolean; stack: boolean; - yAxis: TimeSeriesChartYAxisSettings; + yAxes: TimeSeriesChartYAxes; xAxis: TimeSeriesChartAxisSettings; + animation: TimeSeriesChartAnimationSettings; noAggregationBarWidthSettings: TimeSeriesChartNoAggregationBarWidthSettings; } @@ -419,36 +546,10 @@ export const timeSeriesChartDefaultSettings: TimeSeriesChartSettings = { darkMode: false, dataZoom: true, stack: false, - yAxis: { - show: true, - label: '', - labelFont: { - family: 'Roboto', - size: 12, - sizeUnit: 'px', - style: 'normal', - weight: '600', - lineHeight: '1' - }, - labelColor: timeSeriesChartColorScheme['axis.label'].light, - position: AxisPosition.left, - showTickLabels: true, - tickLabelFont: { - family: 'Roboto', - size: 12, - sizeUnit: 'px', - style: 'normal', - weight: '400', - lineHeight: '1' - }, - tickLabelColor: timeSeriesChartColorScheme['axis.tickLabel'].light, - ticksFormatter: null, - showTicks: true, - ticksColor: timeSeriesChartColorScheme['axis.ticks'].light, - showLine: true, - lineColor: timeSeriesChartColorScheme['axis.line'].light, - showSplitLines: true, - splitLinesColor: timeSeriesChartColorScheme['axis.splitLine'].light + yAxes: { + default: mergeDeep({} as TimeSeriesChartYAxisSettings, + defaultTimeSeriesChartYAxisSettings, + { id: 'default', order: 0 } as TimeSeriesChartYAxisSettings) }, xAxis: { show: true, @@ -480,6 +581,16 @@ export const timeSeriesChartDefaultSettings: TimeSeriesChartSettings = { showSplitLines: true, splitLinesColor: timeSeriesChartColorScheme['axis.splitLine'].light }, + animation: { + animation: true, + animationThreshold: 2000, + animationDuration: 1000, + animationEasing: TimeSeriesChartAnimationEasing.cubicOut, + animationDelay: 0, + animationDurationUpdate: 300, + animationEasingUpdate: TimeSeriesChartAnimationEasing.cubicOut, + animationDelayUpdate: 0 + }, noAggregationBarWidthSettings: { strategy: TimeSeriesChartNoAggregationBarWidthStrategy.group, groupWidth: { @@ -558,6 +669,7 @@ export interface BarSeriesSettings { } export interface TimeSeriesChartKeySettings { + yAxisId: TimeSeriesChartYAxisId; showInLegend: boolean; dataHiddenByDefault: boolean; type: TimeSeriesChartSeriesType; @@ -566,6 +678,7 @@ export interface TimeSeriesChartKeySettings { } export const timeSeriesChartKeyDefaultSettings: TimeSeriesChartKeySettings = { + yAxisId: 'default', showInLegend: true, dataHiddenByDefault: false, type: TimeSeriesChartSeriesType.line, @@ -626,6 +739,7 @@ export const timeSeriesChartKeyDefaultSettings: TimeSeriesChartKeySettings = { }; export interface TimeSeriesChartDataItem extends EChartsSeriesItem { + yAxisId: TimeSeriesChartYAxisId; yAxisIndex: number; option?: LineSeriesOption | CustomSeriesOption; barRenderContext?: BarRenderContext; @@ -635,6 +749,7 @@ type TimeSeriesChartThresholdValue = number | string | (number | string)[]; export interface TimeSeriesChartThresholdItem { id: string; + yAxisId: TimeSeriesChartYAxisId; yAxisIndex: number; latestDataKey?: DataKey; units?: string; @@ -646,14 +761,16 @@ export interface TimeSeriesChartThresholdItem { export interface TimeSeriesChartYAxis { id: string; - units: string; + decimals: number; + settings: TimeSeriesChartYAxisSettings; option: YAXisOption & ValueAxisBaseOption; intervalCalculator?: (axis: Axis2D) => number; ticksFormatter?: (value: any) => string; } -export const createTimeSeriesYAxis = (axisId: string, units: string, - decimals: number, settings: TimeSeriesChartYAxisSettings, +export const createTimeSeriesYAxis = (units: string, + decimals: number, + settings: TimeSeriesChartYAxisSettings, darkMode: boolean): TimeSeriesChartYAxis => { const yAxisTickLabelStyle = createChartTextStyle(settings.tickLabelFont, settings.tickLabelColor, darkMode, 'axis.tickLabel'); @@ -664,13 +781,14 @@ export const createTimeSeriesYAxis = (axisId: string, units: string, ticksFormatter = parseFunction(settings.ticksFormatter, ['value']); } const yAxis: TimeSeriesChartYAxis = { - id: axisId, - units, + id: settings.id, + decimals, + settings, option: { show: settings.show, type: 'value', position: settings.position, - id: axisId, + id: settings.id, offset: 0, alignTicks: true, scale: true, @@ -843,13 +961,11 @@ const generateChartThresholds = (thresholdItems: TimeSeriesChartThresholdItem[], id: item.id, dataGroupId: item.id, yAxisIndex: item.yAxisIndex, - animation: true, data: [], tooltip: { show: false }, markLine: { - animation: true, lineStyle: { width: item.settings.lineWidth, color: prepareChartThemeColor(item.settings.lineColor, darkMode, 'threshold.line'), @@ -956,16 +1072,19 @@ const generateChartSeries = (dataItems: TimeSeriesChartDataItem[], }; export const updateDarkMode = (options: EChartsOption, settings: TimeSeriesChartSettings, + yAxisList: TimeSeriesChartYAxis[], dataItems: TimeSeriesChartDataItem[], thresholdDataItems: TimeSeriesChartThresholdItem[], darkMode: boolean): EChartsOption => { options.darkMode = darkMode; if (Array.isArray(options.yAxis)) { - for (const yAxis of options.yAxis) { - yAxis.nameTextStyle.color = prepareChartThemeColor(settings.yAxis.labelColor, darkMode, 'axis.label'); - yAxis.axisLabel.color = prepareChartThemeColor(settings.yAxis.tickLabelColor, darkMode, 'axis.tickLabel'); - yAxis.axisLine.lineStyle.color = prepareChartThemeColor(settings.yAxis.lineColor, darkMode, 'axis.line'); - yAxis.axisTick.lineStyle.color = prepareChartThemeColor(settings.yAxis.ticksColor, darkMode, 'axis.ticks'); - yAxis.splitLine.lineStyle.color = prepareChartThemeColor(settings.yAxis.splitLinesColor, darkMode, 'axis.splitLine'); + for (let i = 0; i < options.yAxis.length; i++) { + const yAxis = options.yAxis[i]; + const yAxisSettings = yAxisList[i].settings; + yAxis.nameTextStyle.color = prepareChartThemeColor(yAxisSettings.labelColor, darkMode, 'axis.label'); + yAxis.axisLabel.color = prepareChartThemeColor(yAxisSettings.tickLabelColor, darkMode, 'axis.tickLabel'); + yAxis.axisLine.lineStyle.color = prepareChartThemeColor(yAxisSettings.lineColor, darkMode, 'axis.line'); + yAxis.axisTick.lineStyle.color = prepareChartThemeColor(yAxisSettings.ticksColor, darkMode, 'axis.ticks'); + yAxis.splitLine.lineStyle.color = prepareChartThemeColor(yAxisSettings.splitLinesColor, darkMode, 'axis.splitLine'); } } if (Array.isArray(options.xAxis)) { @@ -1032,7 +1151,6 @@ const createTimeSeriesChartSeries = (item: TimeSeriesChartDataItem, emphasis: { focus: 'series' }, - animation: true, dimensions: [ {name: 'intervalStart', type: 'number'}, {name: 'intervalEnd', type: 'number'} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.ts index 6d790d5439..abad3f79e8 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.ts @@ -19,7 +19,7 @@ import { AxisPosition, calculateThresholdsOffset, createTimeSeriesXAxisOption, - createTimeSeriesYAxis, + createTimeSeriesYAxis, defaultTimeSeriesChartYAxisSettings, generateChartData, parseThresholdData, SeriesLabelPosition, @@ -29,11 +29,11 @@ import { TimeSeriesChartKeySettings, TimeSeriesChartSeriesType, TimeSeriesChartSettings, - TimeSeriesChartShape, + TimeSeriesChartShape, TimeSeriesChartThreshold, timeSeriesChartThresholdDefaultSettings, TimeSeriesChartThresholdItem, TimeSeriesChartThresholdType, TimeSeriesChartType, - TimeSeriesChartYAxis, + TimeSeriesChartYAxis, TimeSeriesChartYAxisId, TimeSeriesChartYAxisSettings, updateDarkMode } from '@home/components/widget/lib/chart/time-series-chart.models'; import { ResizeObserver } from '@juggle/resize-observer'; @@ -63,6 +63,7 @@ import { AggregationType } from '@shared/models/time/time.models'; import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; import { WidgetSubscriptionOptions } from '@core/api/widget-api.models'; import { DataKeySettingsFunction } from '@home/components/widget/config/data-keys.component.models'; +import { DeepPartial } from '@shared/models/common'; export class TbTimeSeriesChart { @@ -94,9 +95,11 @@ export class TbTimeSeriesChart { private readonly shapeResize$: ResizeObserver; + private readonly settings: TimeSeriesChartSettings; + + private yAxisList: TimeSeriesChartYAxis[] = []; private dataItems: TimeSeriesChartDataItem[] = []; private thresholdItems: TimeSeriesChartThresholdItem[] = []; - private yAxisList: TimeSeriesChartYAxis[] = []; private timeSeriesChart: ECharts; private timeSeriesChartOptions: EChartsOption; @@ -120,13 +123,16 @@ export class TbTimeSeriesChart { yMax$ = this.yMaxSubject.asObservable(); constructor(private ctx: WidgetContext, - private readonly settings: TimeSeriesChartSettings, + private readonly inputSettings: DeepPartial, private chartElement: HTMLElement, private renderer: Renderer2, private autoResize = true) { - this.settings = mergeDeep({} as TimeSeriesChartSettings, timeSeriesChartDefaultSettings, this.settings); + this.settings = mergeDeep({} as TimeSeriesChartSettings, + timeSeriesChartDefaultSettings, + this.inputSettings as TimeSeriesChartSettings); this.darkMode = this.settings.darkMode; + this.setupYAxes(); this.setupData(); this.setupThresholds(); if (this.settings.showTooltip && this.settings.tooltipShowDate) { @@ -258,7 +264,8 @@ export class TbTimeSeriesChart { if (this.darkMode !== darkMode) { this.darkMode = darkMode; if (this.timeSeriesChart) { - this.timeSeriesChartOptions = updateDarkMode(this.timeSeriesChartOptions, this.settings, this.dataItems, + this.timeSeriesChartOptions = updateDarkMode(this.timeSeriesChartOptions, + this.settings, this.yAxisList, this.dataItems, this.thresholdItems, darkMode); this.timeSeriesChart.setOption(this.timeSeriesChartOptions); } @@ -287,11 +294,16 @@ export class TbTimeSeriesChart { const units = dataKey.units && dataKey.units.length ? dataKey.units : this.ctx.units; const decimals = isDefinedAndNotNull(dataKey.decimals) ? dataKey.decimals : (isDefinedAndNotNull(this.ctx.decimals) ? this.ctx.decimals : 2); + let yAxisId = keySettings.yAxisId; + if (!Object.keys(this.settings.yAxes).includes(yAxisId)) { + yAxisId = 'default'; + } this.dataItems.push({ id: this.nextComponentId(), units, decimals, - yAxisIndex: this.getYAxis(units, decimals), + yAxisId, + yAxisIndex: this.getYAxisIndex(yAxisId), dataKey, data: namedData, enabled: !keySettings.dataHiddenByDefault @@ -303,7 +315,9 @@ export class TbTimeSeriesChart { private setupThresholds(): void { const thresholdDatasources: Datasource[] = []; - for (const threshold of this.settings.thresholds) { + for (const thresholdSettings of this.settings.thresholds) { + const threshold = mergeDeep({} as TimeSeriesChartThreshold, + timeSeriesChartThresholdDefaultSettings, thresholdSettings); let latestDataKey: DataKey = null; let entityDataKey: DataKey = null; let value = null; @@ -351,11 +365,16 @@ export class TbTimeSeriesChart { const units = threshold.units && threshold.units.length ? threshold.units : this.ctx.units; const decimals = isDefinedAndNotNull(threshold.decimals) ? threshold.decimals : (isDefinedAndNotNull(this.ctx.decimals) ? this.ctx.decimals : 2); + let yAxisId = threshold.yAxisId; + if (!Object.keys(this.settings.yAxes).includes(yAxisId)) { + yAxisId = 'default'; + } const thresholdItem: TimeSeriesChartThresholdItem = { id: this.nextComponentId(), units, decimals, - yAxisIndex: this.getYAxis(units, decimals), + yAxisId, + yAxisIndex: this.getYAxisIndex(yAxisId), value, latestDataKey, settings: threshold @@ -368,17 +387,32 @@ export class TbTimeSeriesChart { this.subscribeForEntityThresholds(thresholdDatasources); } + private setupYAxes(): void { + const yAxisSettingsList = Object.values(this.settings.yAxes); + yAxisSettingsList.sort((a1, a2) => a1.order - a2.order); + for (const yAxisSettings of yAxisSettingsList) { + const axisSettings = mergeDeep({} as TimeSeriesChartYAxisSettings, + defaultTimeSeriesChartYAxisSettings, yAxisSettings); + const units = axisSettings.units && axisSettings.units.length ? axisSettings.units : this.ctx.units; + const decimals = isDefinedAndNotNull(axisSettings.decimals) ? axisSettings.decimals : + (isDefinedAndNotNull(this.ctx.decimals) ? this.ctx.decimals : 2); + const yAxis = createTimeSeriesYAxis(units, decimals, axisSettings, this.darkMode); + this.yAxisList.push(yAxis); + } + } + + private nextComponentId(): string { return (this.componentIndexCounter++) + ''; } - private getYAxis(units: string, decimals: number): number { - let yAxisIndex = this.yAxisList.findIndex(axis => axis.units === units); + private getYAxisIndex(id: TimeSeriesChartYAxisId): number { + let yAxisIndex = this.yAxisList.findIndex(axis => axis.id === id); if (yAxisIndex === -1) { - const yAxisId = this.yAxisList.length + ''; - const yAxis = createTimeSeriesYAxis(yAxisId, units, decimals, this.settings.yAxis, this.darkMode); - this.yAxisList.push(yAxis); - yAxisIndex = this.yAxisList.length - 1; + yAxisIndex = this.yAxisList.findIndex(axis => axis.id === 'default'); + if (yAxisIndex === -1 && this.yAxisList.length) { + yAxisIndex = 0; + } } return yAxisIndex; } @@ -460,7 +494,15 @@ export class TbTimeSeriesChart { realtime: true, bottom: 10 } - ] + ], + animation: this.settings.animation.animation, + animationThreshold: this.settings.animation.animationThreshold, + animationDuration: this.settings.animation.animationDuration, + animationEasing: this.settings.animation.animationEasing, + animationDelay: this.settings.animation.animationDelay, + animationDurationUpdate: this.settings.animation.animationDurationUpdate, + animationEasingUpdate: this.settings.animation.animationEasingUpdate, + animationDelayUpdate: this.settings.animation.animationDelayUpdate }; this.timeSeriesChartOptions.xAxis[0].tbTimeWindow = this.ctx.defaultSubscription.timeWindow; @@ -588,7 +630,7 @@ export class TbTimeSeriesChart { result.offset += 5; } width = newWidth; - const showLine = !!width && this.settings.yAxis.showLine; + const showLine = !!width && yAxis.settings.showLine; if (yAxis.option.axisLine.show !== showLine) { yAxis.option.axisLine.show = showLine; result.changed = true; @@ -597,7 +639,7 @@ export class TbTimeSeriesChart { yAxis.option.offset = result.offset; result.changed = true; } - if (this.settings.yAxis.label) { + if (yAxis.settings.label) { if (!width) { if (yAxis.option.name) { yAxis.option.name = null; @@ -605,7 +647,7 @@ export class TbTimeSeriesChart { } } else { if (!yAxis.option.name) { - yAxis.option.name = this.settings.yAxis.label; + yAxis.option.name = yAxis.settings.label; result.changed = true; } const nameGap = width; @@ -613,7 +655,7 @@ export class TbTimeSeriesChart { yAxis.option.nameGap = nameGap; result.changed = true; } - const nameWidth = measureYAxisNameWidth(this.timeSeriesChart, yAxis.id, this.settings.yAxis.label); + const nameWidth = measureYAxisNameWidth(this.timeSeriesChart, yAxis.id, yAxis.settings.label); result.offset += nameWidth; } } @@ -623,15 +665,25 @@ export class TbTimeSeriesChart { } private scaleYAxis(yAxis: TimeSeriesChartYAxis): boolean { - const yAxisIndex = this.yAxisList.indexOf(yAxis); - const axisBarDataItems = this.dataItems.filter(d => d.yAxisIndex === yAxisIndex && d.enabled && + const axisBarDataItems = this.dataItems.filter(d => d.yAxisId === yAxis.id && d.enabled && d.data.length && d.dataKey.settings.type === TimeSeriesChartSeriesType.bar); return !axisBarDataItems.length; } + private yAxisEmpty(yAxis: TimeSeriesChartYAxis): boolean { + const axisDataItems = this.dataItems.filter(d => d.yAxisId === yAxis.id && d.enabled && + d.data.length); + return !axisDataItems.length; + } + private calculateYAxisInterval(axisList: TimeSeriesChartYAxis[]): boolean { let changed = false; for (const yAxis of axisList) { + const minInterval = this.yAxisEmpty(yAxis) ? undefined : (1 / Math.pow(10, yAxis.decimals)); + if (yAxis.option.minInterval !== minInterval) { + yAxis.option.minInterval = minInterval; + changed = true; + } if (yAxis.intervalCalculator) { const axis = getYAxis(this.timeSeriesChart, yAxis.id); if (axis) { @@ -649,8 +701,10 @@ export class TbTimeSeriesChart { } private minTopOffset(): number { + const showTickLabels = + !!this.yAxisList.find(yAxis => yAxis.settings.show && yAxis.settings.showTickLabels); return (this.topPointLabels) ? 20 : - ((this.settings.yAxis.show && this.settings.yAxis.showTickLabels) ? 10 : 5); + (showTickLabels ? 10 : 5); } private minBottomOffset(): number { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-key-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-key-settings.component.html index 0d470703b6..5f0fbb9e37 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-key-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-key-settings.component.html @@ -29,6 +29,18 @@
+
+
+
widgets.time-series-chart.axis.y-axis
+ + + + {{ yAxis }} + + + +
+
widgets.time-series-chart.series.series-type
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-key-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-key-settings.component.ts index 4da23f32e0..d1ac428396 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-key-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-key-settings.component.ts @@ -25,9 +25,10 @@ import { TimeSeriesChartKeySettings, TimeSeriesChartSeriesType, timeSeriesChartSeriesTypes, - timeSeriesChartSeriesTypeTranslations, TimeSeriesChartType, timeSeriesChartTypeTranslations + timeSeriesChartSeriesTypeTranslations, TimeSeriesChartType, timeSeriesChartTypeTranslations, TimeSeriesChartYAxisId } from '@home/components/widget/lib/chart/time-series-chart.models'; import { WidgetConfigComponentData } from '@home/models/widget-component.models'; +import { TimeSeriesChartWidgetSettings } from '@home/components/widget/lib/chart/time-series-chart-widget.models'; @Component({ selector: 'tb-time-series-chart-key-settings', @@ -50,6 +51,8 @@ export class TimeSeriesChartKeySettingsComponent extends WidgetSettingsComponent chartType = TimeSeriesChartType.default; + yAxisIds: TimeSeriesChartYAxisId[]; + constructor(protected store: Store, private fb: UntypedFormBuilder) { super(store); @@ -64,6 +67,8 @@ export class TimeSeriesChartKeySettingsComponent extends WidgetSettingsComponent if (isDefinedAndNotNull(params.chartType)) { this.chartType = params.chartType; } + const widgetSettings = (widgetConfig.config?.settings || {}) as TimeSeriesChartWidgetSettings; + this.yAxisIds = widgetSettings.yAxes ? Object.keys(widgetSettings.yAxes) : ['default']; } protected defaultSettings(): WidgetSettings { @@ -73,7 +78,12 @@ export class TimeSeriesChartKeySettingsComponent extends WidgetSettingsComponent protected onSettingsSet(settings: WidgetSettings) { const seriesSettings = settings as TimeSeriesChartKeySettings; + let yAxisId = seriesSettings.yAxisId; + if (!this.yAxisIds.includes(yAxisId)) { + yAxisId = 'default'; + } this.timeSeriesChartKeySettingsForm = this.fb.group({ + yAxisId: [yAxisId, []], showInLegend: [seriesSettings.showInLegend, []], dataHiddenByDefault: [seriesSettings.dataHiddenByDefault, []], type: [seriesSettings.type, []], diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-widget-settings.component.html index 35f0d147ac..933961a7ae 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-widget-settings.component.html @@ -16,12 +16,18 @@ --> + + + [widgetConfig]="widgetConfig?.config" + [yAxisIds]="yAxisIds">
widgets.time-series-chart.chart-style
@@ -38,12 +44,7 @@
-
widgets.time-series-chart.axis.axes
- - +
widgets.time-series-chart.axis.x-axis
1) { + for (let i = 1; i < this.widgetConfig.config.datasources.length; i++) { + const datasource = this.widgetConfig.config.datasources[i]; + this.removeYaxisId(datasource.dataKeys, yAxisId); + } + } + } + protected settingsForm(): UntypedFormGroup { return this.timeSeriesChartWidgetSettingsForm; } @@ -91,12 +110,12 @@ export class TimeSeriesChartWidgetSettingsComponent extends WidgetSettingsCompon protected onSettingsSet(settings: WidgetSettings) { this.timeSeriesChartWidgetSettingsForm = this.fb.group({ + yAxes: [settings.yAxes, []], thresholds: [settings.thresholds, []], dataZoom: [settings.dataZoom, []], stack: [settings.stack, []], - yAxis: [settings.yAxis, []], xAxis: [settings.xAxis, []], noAggregationBarWidthSettings: [settings.noAggregationBarWidthSettings, []], @@ -173,6 +192,20 @@ export class TimeSeriesChartWidgetSettingsComponent extends WidgetSettingsCompon } } + private removeYaxisId(series: DataKey[], yAxisId: TimeSeriesChartYAxisId): boolean { + let changed = false; + if (series) { + series.forEach(key => { + const keySettings = ((key.settings || {}) as TimeSeriesChartKeySettings); + if (keySettings.yAxisId === yAxisId) { + keySettings.yAxisId = 'default'; + changed = true; + } + }); + } + return changed; + } + private _tooltipValuePreviewFn(): string { return formatValue(22, 0, '°C', false); } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-axis-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-axis-settings.component.html index 24f3dc0883..9511baf121 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-axis-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-axis-settings.component.html @@ -17,13 +17,13 @@ -->
- + - {{ axisTitle | translate }} + {{ 'widgets.time-series-chart.axis.show' | translate }} @@ -56,6 +56,18 @@
+
+
widget-config.units-short
+ + +
+
+
widget-config.decimals-short
+ + + +
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-axis-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-axis-settings.component.ts index 328574d648..a88fc2e123 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-axis-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-axis-settings.component.ts @@ -15,7 +15,13 @@ /// import { Component, forwardRef, Input, OnInit } from '@angular/core'; -import { ControlValueAccessor, NG_VALUE_ACCESSOR, UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; +import { + ControlValueAccessor, + NG_VALUE_ACCESSOR, + UntypedFormBuilder, + UntypedFormGroup, + Validators +} from '@angular/forms'; import { AxisPosition, timeSeriesAxisPositionTranslations, @@ -40,9 +46,11 @@ import { WidgetService } from '@core/http/widget.service'; }) export class TimeSeriesChartAxisSettingsComponent implements OnInit, ControlValueAccessor { - settingsExpanded = false; + @Input() + @coerceBoolean() + alwaysExpanded = false; - axisTitle: string; + settingsExpanded = false; axisPositions: AxisPosition[]; @@ -72,8 +80,6 @@ export class TimeSeriesChartAxisSettingsComponent implements OnInit, ControlValu ngOnInit(): void { - this.axisTitle = this.axisType === 'xAxis' ? 'widgets.time-series-chart.axis.x-axis' : 'widgets.time-series-chart.axis.y-axis'; - this.axisPositions = this.axisType === 'xAxis' ? [AxisPosition.top, AxisPosition.bottom] : [AxisPosition.left, AxisPosition.right]; @@ -94,6 +100,8 @@ export class TimeSeriesChartAxisSettingsComponent implements OnInit, ControlValu splitLinesColor: [null, []] }); if (this.axisType === 'yAxis') { + this.axisSettingsFormGroup.addControl('units', this.fb.control(null, [])); + this.axisSettingsFormGroup.addControl('decimals', this.fb.control(null, [Validators.min(0)])); this.axisSettingsFormGroup.addControl('ticksFormatter', this.fb.control(null, [])); this.axisSettingsFormGroup.addControl('min', this.fb.control(null, [])); this.axisSettingsFormGroup.addControl('max', this.fb.control(null, [])); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-threshold-row.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-threshold-row.component.html index f82a717d78..7e2c9f74c2 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-threshold-row.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-threshold-row.component.html @@ -73,6 +73,15 @@ [formControl]="entityKeyFormControl">
+
+ + + + {{ yAxis }} + + + +
-
- -
+ +
+
+
+
+
+ +
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axes-panel.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axes-panel.component.scss new file mode 100644 index 0000000000..52aa69f59d --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axes-panel.component.scss @@ -0,0 +1,87 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@import '../../../../../../../../../scss/constants'; + +.tb-y-axes-panel { + .tb-form-table-header-cell { + &.tb-axis-id-header { + width: 60px; + min-width: 60px; + } + + &.tb-label-header { + flex: 1 1 60%; + min-width: 100px; + } + + &.tb-position-header { + flex: 1; + min-width: 60px; + @media #{$mat-gt-md} { + flex: 1 1 40%; + } + } + + &.tb-units-header { + width: 80px; + min-width: 80px; + } + + &.tb-decimals-header { + width: 60px; + min-width: 60px; + } + + &.tb-min-header, &.tb-max-header { + width: 80px; + min-width: 80px; + } + + &.tb-show-header { + width: 40px; + min-width: 40px; + } + + &.tb-actions-header { + width: 80px; + min-width: 80px; + @media #{$mat-gt-md} { + width: 120px; + min-width: 120px; + } + } + + &.tb-label-header, &.tb-units-header, &.tb-decimals-header { + display: none; + @media #{$mat-gt-md} { + display: block; + } + } + + &.tb-min-header, &.tb-max-header { + display: none; + @media #{$mat-gt-xs} { + display: block; + } + } + } + + .tb-form-table-body { + tb-time-series-chart-y-axis-row { + overflow: hidden; + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axes-panel.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axes-panel.component.ts new file mode 100644 index 0000000000..9879ffe877 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axes-panel.component.ts @@ -0,0 +1,180 @@ +/// +/// Copyright © 2016-2024 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, EventEmitter, forwardRef, Input, OnInit, Output, ViewEncapsulation } from '@angular/core'; +import { + AbstractControl, + ControlValueAccessor, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + UntypedFormArray, + UntypedFormBuilder, + UntypedFormControl, + UntypedFormGroup, + Validator +} from '@angular/forms'; +import { + defaultTimeSeriesChartYAxisSettings, + getNextTimeSeriesYAxisId, + TimeSeriesChartYAxes, TimeSeriesChartYAxisId, + TimeSeriesChartYAxisSettings, + timeSeriesChartYAxisValid, + timeSeriesChartYAxisValidator +} from '@home/components/widget/lib/chart/time-series-chart.models'; +import { mergeDeep } from '@core/utils'; +import { CdkDragDrop } from '@angular/cdk/drag-drop'; +import { coerceBoolean } from '@shared/decorators/coercion'; + +@Component({ + selector: 'tb-time-series-chart-y-axes-panel', + templateUrl: './time-series-chart-y-axes-panel.component.html', + styleUrls: ['./time-series-chart-y-axes-panel.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => TimeSeriesChartYAxesPanelComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => TimeSeriesChartYAxesPanelComponent), + multi: true + } + ], + encapsulation: ViewEncapsulation.None +}) +export class TimeSeriesChartYAxesPanelComponent implements ControlValueAccessor, OnInit, Validator { + + @Input() + disabled: boolean; + + @Input() + @coerceBoolean() + advanced = false; + + @Output() + axisRemoved = new EventEmitter(); + + yAxesFormGroup: UntypedFormGroup; + + get dragEnabled(): boolean { + return this.axesFormArray().controls.length > 1; + } + + private propagateChange = (_val: any) => {}; + + constructor(private fb: UntypedFormBuilder) { + } + + ngOnInit() { + this.yAxesFormGroup = this.fb.group({ + axes: [this.fb.array([]), []] + }); + this.yAxesFormGroup.valueChanges.subscribe( + () => { + let axes: TimeSeriesChartYAxisSettings[] = this.yAxesFormGroup.get('axes').value; + for (let i = 0; i < axes.length; i++) { + axes[i].order = i; + } + if (axes) { + axes = axes.filter(axis => timeSeriesChartYAxisValid(axis)); + } + const yAxes: TimeSeriesChartYAxes = {}; + for (const axis of axes) { + yAxes[axis.id] = axis; + } + this.propagateChange(yAxes); + } + ); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (isDisabled) { + this.yAxesFormGroup.disable({emitEvent: false}); + } else { + this.yAxesFormGroup.enable({emitEvent: false}); + } + } + + writeValue(value: TimeSeriesChartYAxes | undefined): void { + const yAxes: TimeSeriesChartYAxes = value || {}; + if (!yAxes.default) { + yAxes.default = mergeDeep({} as TimeSeriesChartYAxisSettings, defaultTimeSeriesChartYAxisSettings, + {id: 'default', order: 0} as TimeSeriesChartYAxisSettings); + } + const yAxisSettingsList = Object.values(yAxes); + yAxisSettingsList.sort((a1, a2) => a1.order - a2.order); + this.yAxesFormGroup.setControl('axes', this.prepareAxesFormArray(yAxisSettingsList), {emitEvent: false}); + } + + public validate(c: UntypedFormControl) { + const valid = this.yAxesFormGroup.valid; + return valid ? null : { + yAxes: { + valid: false, + }, + }; + } + + axisDrop(event: CdkDragDrop) { + const axesArray = this.yAxesFormGroup.get('axes') as UntypedFormArray; + const axis = axesArray.at(event.previousIndex); + axesArray.removeAt(event.previousIndex); + axesArray.insert(event.currentIndex, axis); + } + + axesFormArray(): UntypedFormArray { + return this.yAxesFormGroup.get('axes') as UntypedFormArray; + } + + trackByAxis(index: number, axisControl: AbstractControl): any { + return axisControl; + } + + removeAxis(index: number) { + const axis = + (this.yAxesFormGroup.get('axes') as UntypedFormArray).at(index).value as TimeSeriesChartYAxisSettings; + (this.yAxesFormGroup.get('axes') as UntypedFormArray).removeAt(index); + this.axisRemoved.emit(axis.id); + } + + addAxis() { + const axis = mergeDeep({} as TimeSeriesChartYAxisSettings, + defaultTimeSeriesChartYAxisSettings); + const axes: TimeSeriesChartYAxisSettings[] = this.yAxesFormGroup.get('axes').value; + axis.id = getNextTimeSeriesYAxisId(axes); + axis.order = axes.length; + const axesArray = this.yAxesFormGroup.get('axes') as UntypedFormArray; + const axisControl = this.fb.control(axis, [timeSeriesChartYAxisValidator]); + axesArray.push(axisControl); + } + + private prepareAxesFormArray(axes: TimeSeriesChartYAxisSettings[]): UntypedFormArray { + const axesControls: Array = []; + axes.forEach((axis) => { + axesControls.push(this.fb.control(axis, [timeSeriesChartYAxisValidator])); + }); + return this.fb.array(axesControls); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-row.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-row.component.html new file mode 100644 index 0000000000..0b1907033f --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-row.component.html @@ -0,0 +1,72 @@ + +
+
+ {{ modelValue.id }} +
+ + + + + + + + {{ timeSeriesAxisPositionTranslations.get(position) | translate }} + + + +
+ + + +
+
+ + + +
+
+ + +
+
+ + + +
+
+ +
+ +
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-row.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-row.component.scss new file mode 100644 index 0000000000..12db0d9228 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-row.component.scss @@ -0,0 +1,95 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@import '../../../../../../../../../scss/constants'; + +.tb-form-table-row.tb-axis-row { + + .tb-axis-id-field { + color: rgba(0, 0, 0, 0.76); + font-weight: 500; + font-size: 14px; + line-height: 20px; + letter-spacing: 0.2px; + width: 60px; + min-width: 60px; + } + + .tb-label-field { + flex: 1 1 60%; + min-width: 100px; + } + + .tb-position-field { + flex: 1; + min-width: 60px; + @media #{$mat-gt-md} { + flex: 1 1 40%; + } + } + + .tb-units-field, .tb-decimals-field, .tb-min-field, .tb-max-field { + display: flex; + flex-direction: row; + place-content: center; + align-items: center; + } + + .tb-units-field { + width: 80px; + min-width: 80px; + } + + .tb-decimals-field { + width: 60px; + min-width: 60px; + } + + .tb-min-field, .tb-max-field { + width: 80px; + min-width: 80px; + } + + .tb-show-field { + width: 40px; + min-width: 40px; + } + + .tb-label-field { + display: none; + @media #{$mat-gt-md} { + display: block; + } + } + + .tb-units-field, .tb-decimals-field { + display: none; + @media #{$mat-gt-md} { + display: flex; + } + } + + .tb-min-field, .tb-max-field { + display: none; + @media #{$mat-gt-xs} { + display: flex; + } + } + + .tb-remove-button { + width: 40px; + min-width: 40px; + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-row.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-row.component.ts new file mode 100644 index 0000000000..a4824fd51e --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-row.component.ts @@ -0,0 +1,201 @@ +/// +/// Copyright © 2016-2024 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { + ChangeDetectorRef, + Component, + EventEmitter, + forwardRef, + Input, + OnInit, + Output, + Renderer2, + ViewContainerRef, + ViewEncapsulation +} from '@angular/core'; +import { ControlValueAccessor, NG_VALUE_ACCESSOR, UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; +import { + AxisPosition, + timeSeriesAxisPositionTranslations, + TimeSeriesChartYAxisSettings +} from '@home/components/widget/lib/chart/time-series-chart.models'; +import { MatButton } from '@angular/material/button'; +import { TbPopoverService } from '@shared/components/popover.service'; +import { coerceBoolean } from '@shared/decorators/coercion'; +import { + TimeSeriesChartYAxisSettingsPanelComponent +} from '@home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-settings-panel.component'; +import { deepClone } from '@core/utils'; + +@Component({ + selector: 'tb-time-series-chart-y-axis-row', + templateUrl: './time-series-chart-y-axis-row.component.html', + styleUrls: ['./time-series-chart-y-axis-row.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => TimeSeriesChartYAxisRowComponent), + multi: true + } + ], + encapsulation: ViewEncapsulation.None +}) +export class TimeSeriesChartYAxisRowComponent implements ControlValueAccessor, OnInit { + + axisPositions = [AxisPosition.left, AxisPosition.right]; + + timeSeriesAxisPositionTranslations = timeSeriesAxisPositionTranslations; + + @Input() + disabled: boolean; + + @Input() + @coerceBoolean() + advanced = false; + + @Output() + axisRemoved = new EventEmitter(); + + axisFormGroup: UntypedFormGroup; + + modelValue: TimeSeriesChartYAxisSettings; + + private propagateChange = (_val: any) => {}; + + constructor(private fb: UntypedFormBuilder, + private popoverService: TbPopoverService, + private renderer: Renderer2, + private viewContainerRef: ViewContainerRef, + private cd: ChangeDetectorRef) { + } + + ngOnInit() { + this.axisFormGroup = this.fb.group({ + label: [null, []], + position: [null, []], + units: [null, []], + decimals: [null, []], + min: [null, []], + max: [null, []], + show: [null, []] + }); + this.axisFormGroup.valueChanges.subscribe( + () => this.updateModel() + ); + this.axisFormGroup.get('show').valueChanges.subscribe(() => { + this.updateValidators(); + }); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(_fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (isDisabled) { + this.axisFormGroup.disable({emitEvent: false}); + } else { + this.axisFormGroup.enable({emitEvent: false}); + this.updateValidators(); + } + } + + writeValue(value: TimeSeriesChartYAxisSettings): void { + this.modelValue = value; + this.axisFormGroup.patchValue( + { + label: value.label, + position: value.position, + units: value.units, + decimals: value.decimals, + min: value.min, + max: value.max, + show: value.show, + }, {emitEvent: false} + ); + this.updateValidators(); + this.cd.markForCheck(); + } + + editAxis($event: Event, matButton: MatButton) { + if ($event) { + $event.stopPropagation(); + } + const trigger = matButton._elementRef.nativeElement; + if (this.popoverService.hasPopover(trigger)) { + this.popoverService.hidePopover(trigger); + } else { + const ctx: any = { + yAxisSettings: deepClone(this.modelValue), + advanced: this.advanced + }; + const yAxisSettingsPanelPopover = this.popoverService.displayPopover(trigger, this.renderer, + this.viewContainerRef, TimeSeriesChartYAxisSettingsPanelComponent, ['leftOnly', 'leftTopOnly', 'leftBottomOnly'], true, null, + ctx, + {}, + {}, {}, true); + yAxisSettingsPanelPopover.tbComponentRef.instance.popover = yAxisSettingsPanelPopover; + yAxisSettingsPanelPopover.tbComponentRef.instance.yAxisSettingsApplied.subscribe((yAxisSettings) => { + yAxisSettingsPanelPopover.hide(); + this.modelValue = {...this.modelValue, ...yAxisSettings}; + this.axisFormGroup.patchValue( + { + label: this.modelValue.label, + position: this.modelValue.position, + units: this.modelValue.units, + decimals: this.modelValue.decimals, + min: this.modelValue.min, + max: this.modelValue.max, + show: this.modelValue.show + }, + {emitEvent: false}); + this.updateValidators(); + this.propagateChange(this.modelValue); + }); + } + } + + private updateValidators() { + const show: boolean = this.axisFormGroup.get('show').value; + if (show) { + this.axisFormGroup.get('label').enable({emitEvent: false}); + this.axisFormGroup.get('position').enable({emitEvent: false}); + this.axisFormGroup.get('units').enable({emitEvent: false}); + this.axisFormGroup.get('decimals').enable({emitEvent: false}); + } else { + this.axisFormGroup.get('label').disable({emitEvent: false}); + this.axisFormGroup.get('position').disable({emitEvent: false}); + this.axisFormGroup.get('units').disable({emitEvent: false}); + this.axisFormGroup.get('decimals').disable({emitEvent: false}); + } + } + + private updateModel() { + const value = this.axisFormGroup.value; + this.modelValue.label = value.label; + this.modelValue.position = value.position; + this.modelValue.units = value.units; + this.modelValue.decimals = value.decimals; + this.modelValue.min = value.min; + this.modelValue.max = value.max; + this.modelValue.show = value.show; + this.propagateChange(this.modelValue); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-settings-panel.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-settings-panel.component.html new file mode 100644 index 0000000000..759074b4be --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-settings-panel.component.html @@ -0,0 +1,43 @@ + +
+
{{ 'widgets.time-series-chart.axis.y-axis-settings' | translate }}
+
+ + +
+
+ + +
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-settings-panel.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-settings-panel.component.scss new file mode 100644 index 0000000000..095c38b7f7 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-settings-panel.component.scss @@ -0,0 +1,49 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@import '../../../../../../../../../scss/constants'; + +.tb-y-axis-settings-panel { + width: 530px; + display: flex; + flex-direction: column; + gap: 16px; + @media #{$mat-lt-md} { + width: 90vw; + } + .tb-y-axis-settings-panel-content { + display: flex; + flex-direction: column; + gap: 16px; + overflow: auto; + margin: -10px; + padding: 10px; + } + .tb-y-axis-settings-title { + font-size: 16px; + font-weight: 500; + line-height: 24px; + letter-spacing: 0.25px; + color: rgba(0, 0, 0, 0.87); + } + .tb-y-axis-settings-panel-buttons { + height: 40px; + display: flex; + flex-direction: row; + gap: 16px; + justify-content: flex-end; + align-items: flex-end; + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-settings-panel.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-settings-panel.component.ts new file mode 100644 index 0000000000..8046aafd29 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-settings-panel.component.ts @@ -0,0 +1,66 @@ +/// +/// Copyright © 2016-2024 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, EventEmitter, Input, OnInit, Output, ViewEncapsulation } from '@angular/core'; +import { TbPopoverComponent } from '@shared/components/popover.component'; +import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; +import { TimeSeriesChartYAxisSettings } from '@home/components/widget/lib/chart/time-series-chart.models'; +import { coerceBoolean } from '@shared/decorators/coercion'; + +@Component({ + selector: 'tb-time-series-chart-y-axis-settings-panel', + templateUrl: './time-series-chart-y-axis-settings-panel.component.html', + providers: [], + styleUrls: ['./time-series-chart-y-axis-settings-panel.component.scss'], + encapsulation: ViewEncapsulation.None +}) +export class TimeSeriesChartYAxisSettingsPanelComponent implements OnInit { + + @Input() + yAxisSettings: TimeSeriesChartYAxisSettings; + + @Input() + @coerceBoolean() + advanced = false; + + @Input() + popover: TbPopoverComponent; + + @Output() + yAxisSettingsApplied = new EventEmitter(); + + yAxisSettingsFormGroup: UntypedFormGroup; + + constructor(private fb: UntypedFormBuilder) { + } + + ngOnInit(): void { + this.yAxisSettingsFormGroup = this.fb.group( + { + yAxis: [this.yAxisSettings, []] + } + ); + } + + cancel() { + this.popover?.hide(); + } + + applyYAxisSettings() { + const yAxisSettings = this.yAxisSettingsFormGroup.get('yAxis').getRawValue(); + this.yAxisSettingsApplied.emit(yAxisSettings); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget-settings-common.module.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget-settings-common.module.ts index b741a8e358..714da2a0c7 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget-settings-common.module.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget-settings-common.module.ts @@ -109,6 +109,15 @@ import { import { TimeSeriesNoAggregationBarWidthSettingsComponent } from '@home/components/widget/lib/settings/common/chart/time-series-no-aggregation-bar-width-settings.component'; +import { + TimeSeriesChartYAxesPanelComponent +} from '@home/components/widget/lib/settings/common/chart/time-series-chart-y-axes-panel.component'; +import { + TimeSeriesChartYAxisRowComponent +} from '@home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-row.component'; +import { + TimeSeriesChartYAxisSettingsPanelComponent +} from '@home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-settings-panel.component'; @NgModule({ declarations: [ @@ -150,6 +159,9 @@ import { TimeSeriesChartThresholdRowComponent, TimeSeriesChartThresholdSettingsPanelComponent, TimeSeriesNoAggregationBarWidthSettingsComponent, + TimeSeriesChartYAxesPanelComponent, + TimeSeriesChartYAxisRowComponent, + TimeSeriesChartYAxisSettingsPanelComponent, DataKeyInputComponent, EntityAliasInputComponent ], @@ -197,6 +209,9 @@ import { TimeSeriesChartThresholdRowComponent, TimeSeriesChartThresholdSettingsPanelComponent, TimeSeriesNoAggregationBarWidthSettingsComponent, + TimeSeriesChartYAxesPanelComponent, + TimeSeriesChartYAxisRowComponent, + TimeSeriesChartYAxisSettingsPanelComponent, DataKeyInputComponent, EntityAliasInputComponent ], diff --git a/ui-ngx/src/app/shared/models/common.ts b/ui-ngx/src/app/shared/models/common.ts new file mode 100644 index 0000000000..c331cc5127 --- /dev/null +++ b/ui-ngx/src/app/shared/models/common.ts @@ -0,0 +1,19 @@ +/// +/// Copyright © 2016-2024 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +export type DeepPartial = T extends object ? { + [P in keyof T]?: DeepPartial; +} : T; diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index 216ed878b2..830a54f88e 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -6609,6 +6609,7 @@ "stack-mode": "Stack mode", "stack-mode-hint": "Stacks series on the chart. The series with the same unit would be put on top of each other.", "axes": "Axes", + "y-axes": "Y axes", "line-type": "Line type", "line-type-solid": "Solid", "line-type-dashed": "Dashed", @@ -6670,6 +6671,9 @@ "axes": "Axes", "x-axis": "X axis", "y-axis": "Y axis", + "y-axis-settings": "Y axis settings", + "remove-y-axis": "Remove Y axis", + "id": "Id", "label": "Label", "position": "Position", "position-left": "Left", @@ -6686,7 +6690,11 @@ "scale": "Scale", "scale-min": "min", "scale-max": "max", - "scale-auto": "Auto" + "scale-auto": "Auto", + "min": "Min", + "max": "Max", + "show": "Show", + "add-y-axis": "Add Y axis" }, "series": { "legend-settings": "Legend settings",