From d3628bb9d89bea4b2c2f82d2170ef80ef9627f8a Mon Sep 17 00:00:00 2001 From: Maksym Tsymbarov Date: Sun, 15 Feb 2026 01:07:57 +0200 Subject: [PATCH 1/5] Fixed Range and Bar chart limits setup --- .../widget_types/bar_chart_with_labels.json | 2 +- .../json/system/widget_types/range_chart.json | 2 +- ...hart-with-labels-basic-config.component.ts | 12 +- .../range-chart-basic-config.component.ts | 7 +- .../bar-chart-with-labels-widget.component.ts | 6 + .../lib/chart/range-chart-widget.component.ts | 6 + .../lib/chart/time-series-chart.models.ts | 100 ++++++++++++++++ .../widget/lib/chart/time-series-chart.ts | 23 +--- ...t-with-labels-widget-settings.component.ts | 6 + .../range-chart-widget-settings.component.ts | 7 +- .../common/axis-scale-row.component.ts | 37 +++--- ...me-series-chart-axis-settings.component.ts | 6 +- ...ime-series-chart-y-axes-panel.component.ts | 108 +----------------- .../time-series-chart-y-axis-row.component.ts | 29 +---- 14 files changed, 176 insertions(+), 175 deletions(-) diff --git a/application/src/main/data/json/system/widget_types/bar_chart_with_labels.json b/application/src/main/data/json/system/widget_types/bar_chart_with_labels.json index 7e0bfe1e66..f23747fb52 100644 --- a/application/src/main/data/json/system/widget_types/bar_chart_with_labels.json +++ b/application/src/main/data/json/system/widget_types/bar_chart_with_labels.json @@ -11,7 +11,7 @@ "resources": [], "templateHtml": "\n", "templateCss": ".legend {\n font-size: 13px;\n line-height: 10px;\n}\n\n.legend table { \n border-spacing: 0px;\n border-collapse: separate;\n}\n\n.mouse-events .flot-overlay {\n cursor: crosshair; \n}\n\n", - "controllerScript": "self.onInit = function() {\n self.ctx.$scope.barChartWidget.onInit();\n};\n\nself.onDataUpdated = function() {\n self.ctx.$scope.barChartWidget.onDataUpdated();\n}\n\nself.typeParameters = function() {\n return {\n previewWidth: '80%',\n embedTitlePanel: true,\n embedActionsPanel: true,\n supportsUnitConversion: true,\n hasAdditionalLatestDataKeys: false,\n defaultDataKeysFunction: function() {\n return [{ name: 'humidity', label: 'Humidity', type: 'timeseries' }];\n }\n };\n}\n", + "controllerScript": "self.onInit = function() {\n self.ctx.$scope.barChartWidget.onInit();\n};\n\nself.onDataUpdated = function() {\n self.ctx.$scope.barChartWidget.onDataUpdated();\n}\n\nself.onLatestDataUpdated = function() {\n self.ctx.$scope.barChartWidget.onLatestDataUpdated();\n}\n\nself.typeParameters = function() {\n return {\n previewWidth: '80%',\n embedTitlePanel: true,\n embedActionsPanel: true,\n supportsUnitConversion: true,\n hasAdditionalLatestDataKeys: false,\n defaultDataKeysFunction: function() {\n return [{ name: 'humidity', label: 'Humidity', type: 'timeseries' }];\n }\n };\n}\n", "settingsForm": [], "dataKeySettingsForm": [], "latestDataKeySettingsForm": [], diff --git a/application/src/main/data/json/system/widget_types/range_chart.json b/application/src/main/data/json/system/widget_types/range_chart.json index 5d8293debe..1e033ecb2b 100644 --- a/application/src/main/data/json/system/widget_types/range_chart.json +++ b/application/src/main/data/json/system/widget_types/range_chart.json @@ -11,7 +11,7 @@ "resources": [], "templateHtml": "\n", "templateCss": ".legend {\n font-size: 13px;\n line-height: 10px;\n}\n\n.legend table { \n border-spacing: 0px;\n border-collapse: separate;\n}\n\n.mouse-events .flot-overlay {\n cursor: crosshair; \n}\n\n", - "controllerScript": "self.onInit = function() {\n self.ctx.$scope.rangeChartWidget.onInit();\n};\n\nself.onDataUpdated = function() {\n self.ctx.$scope.rangeChartWidget.onDataUpdated();\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true,\n previewWidth: '80%',\n embedTitlePanel: true,\n embedActionsPanel: true,\n supportsUnitConversion: true,\n hasAdditionalLatestDataKeys: false,\n defaultDataKeysFunction: function() {\n return [{ name: 'temperature', label: 'Temperature', type: 'timeseries' }];\n }\n };\n}\n", + "controllerScript": "self.onInit = function() {\n self.ctx.$scope.rangeChartWidget.onInit();\n};\n\nself.onDataUpdated = function() {\n self.ctx.$scope.rangeChartWidget.onDataUpdated();\n}\n\nself.onLatestDataUpdated = function() {\n self.ctx.$scope.rangeChartWidget.onLatestDataUpdated();\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true,\n previewWidth: '80%',\n embedTitlePanel: true,\n embedActionsPanel: true,\n supportsUnitConversion: true,\n hasAdditionalLatestDataKeys: false,\n defaultDataKeysFunction: function() {\n return [{ name: 'temperature', label: 'Temperature', type: 'timeseries' }];\n }\n };\n}\n", "settingsForm": [], "dataKeySettingsForm": [], "latestDataKeySettingsForm": [], diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/bar-chart-with-labels-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/bar-chart-with-labels-basic-config.component.ts index 1436275c7d..0d6311d0b7 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/bar-chart-with-labels-basic-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/bar-chart-with-labels-basic-config.component.ts @@ -45,7 +45,10 @@ import { barChartWithLabelsDefaultSettings, BarChartWithLabelsWidgetSettings } from '@home/components/widget/lib/chart/bar-chart-with-labels-widget.models'; -import { TimeSeriesChartType } from '@home/components/widget/lib/chart/time-series-chart.models'; +import { + TimeSeriesChartType, + updateLatestDataKeys +} from '@home/components/widget/lib/chart/time-series-chart.models'; import { getSourceTbUnitSymbol } from '@shared/models/unit.models'; @Component({ @@ -75,7 +78,7 @@ export class BarChartWithLabelsBasicConfigComponent extends BasicWidgetConfigCom tooltipDatePreviewFn = this._tooltipDatePreviewFn.bind(this); predefinedValues = widgetTitleAutocompleteValues; - + constructor(protected store: Store, protected widgetConfigComponent: WidgetConfigComponent, private $injector: Injector, @@ -166,6 +169,11 @@ export class BarChartWithLabelsBasicConfigComponent extends BasicWidgetConfigCom }); } + protected onConfigChanged(widgetConfig: WidgetConfigComponentData) { + updateLatestDataKeys([widgetConfig.config.settings.yAxis], this.datasource, this.callbacks); + super.onConfigChanged(widgetConfig); + } + protected prepareOutputConfig(config: any): WidgetConfigComponentData { setTimewindowConfig(this.widgetConfig.config, config.timewindowConfig); this.widgetConfig.config.datasources = config.datasources; diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/range-chart-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/range-chart-basic-config.component.ts index d93a77e879..8b3e2c54ce 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/range-chart-basic-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/range-chart-basic-config.component.ts @@ -47,7 +47,7 @@ import { } from '@home/components/widget/lib/chart/range-chart-widget.models'; import { lineSeriesStepTypes, - lineSeriesStepTypeTranslations + lineSeriesStepTypeTranslations, updateLatestDataKeys } from '@home/components/widget/lib/chart/time-series-chart.models'; import { chartLabelPositions, @@ -288,6 +288,11 @@ export class RangeChartBasicConfigComponent extends BasicWidgetConfigComponent { return this.widgetConfig; } + protected onConfigChanged(widgetConfig: WidgetConfigComponentData) { + updateLatestDataKeys([widgetConfig.config.settings.yAxis], this.datasource, this.callbacks); + super.onConfigChanged(widgetConfig); + } + protected validatorTriggers(): string[] { return ['showTitle', 'showIcon', 'showRangeThresholds', 'fillArea', 'showLine', 'step', 'showPointLabel', 'enablePointLabelBackground', 'showLegend', 'showTooltip', 'tooltipShowDate']; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/bar-chart-with-labels-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/bar-chart-with-labels-widget.component.ts index f9bd213fdb..71fc4bdcfd 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/bar-chart-with-labels-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/bar-chart-with-labels-widget.component.ts @@ -133,6 +133,12 @@ export class BarChartWithLabelsWidgetComponent implements OnInit, OnDestroy, Aft } } + public onLatestDataUpdated() { + if (this.timeSeriesChart) { + this.timeSeriesChart.latestUpdated(); + } + } + public onLegendKeyEnter(key: DataKey) { this.timeSeriesChart.keyEnter(key); } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.component.ts index 27fe86a393..7feea30ed4 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.component.ts @@ -161,6 +161,12 @@ export class RangeChartWidgetComponent implements OnInit, OnDestroy, AfterViewIn } } + public onLatestDataUpdated() { + if (this.timeSeriesChart) { + this.timeSeriesChart.latestUpdated(); + } + } + public toggleRangeItem(item: RangeItem) { item.enabled = !item.enabled; this.timeSeriesChart.toggleVisualMapRange(item.index); 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 4816852768..e815eefe11 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 @@ -99,6 +99,8 @@ import { TimeSeriesChartTooltipWidgetSettings } from '@home/components/widget/lib/chart/time-series-chart-tooltip.models'; import { TbUnit, TbUnitConverter } from '@shared/models/unit.models'; +import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; +import { DataKeysCallbacks } from '@home/components/widget/lib/settings/common/key/data-keys.component.models'; type TimeSeriesChartDataEntry = [number, any, number, number]; @@ -1495,3 +1497,101 @@ const createSeriesLabelOption = (item: TimeSeriesChartDataItem, show: boolean, } return labelOption; }; + +export const checkLatestDataKeys = (yAxes: TimeSeriesChartYAxes, datasource: Datasource): TimeSeriesChartYAxes => { + const latestKeys = datasource?.latestDataKeys || []; + const result: TimeSeriesChartYAxes = {}; + + for (const [id, axis] of Object.entries(yAxes)) { + axis.min = normalizeAxisLimit(axis.min); + axis.max = normalizeAxisLimit(axis.max); + const minCfg = axis.min; + const maxCfg = axis.max; + + const minValid = !!minCfg && ( + minCfg.type !== ValueSourceType.latestKey || + latestKeys.some(k => isYAxisKey(k, minCfg)) + ); + + const maxValid = !!maxCfg && ( + maxCfg.type !== ValueSourceType.latestKey || + latestKeys.some(k => isYAxisKey(k, maxCfg)) + ); + + if (minValid && maxValid) { + result[id] = axis; + } + } + + return result; +} + +export const updateLatestDataKeys = (yAxes: TimeSeriesChartYAxisSettings[], datasource: Datasource, dataKeyCallbacks: DataKeysCallbacks)=> { + if (datasource) { + let latestKeys = datasource.latestDataKeys; + if (!latestKeys) { + latestKeys = []; + datasource.latestDataKeys = latestKeys; + } + const existingYAxisKeys = latestKeys.filter(k => k.settings?.__yAxisMinKey === true || k.settings?.__yAxisMaxKey === true); + const foundYAxisKeys: DataKey[] = []; + + for(const yAxis of yAxes) { + const min = yAxis.min as ValueSourceConfig; + const max = yAxis.max as ValueSourceConfig; + if (min && min.type === ValueSourceType.latestKey) { + const found = existingYAxisKeys.find(k => isYAxisKey(k, min)); + if (!found) { + const newKey = dataKeyCallbacks.generateDataKey(min.latestKey, min.latestKeyType, + null, true, null); + newKey.settings.__yAxisMinKey = true; + latestKeys.push(newKey); + } else if (foundYAxisKeys.indexOf(found) === -1) { + foundYAxisKeys.push(found); + } + } + if (max && max.type === ValueSourceType.latestKey) { + const found = existingYAxisKeys.find(k => isYAxisKey(k, max)); + if (!found) { + const newKey = dataKeyCallbacks.generateDataKey(max.latestKey, max.latestKeyType, + null, true, null); + newKey.settings.__yAxisMaxKey = true; + latestKeys.push(newKey); + } else if (foundYAxisKeys.indexOf(found) === -1) { + foundYAxisKeys.push(found); + } + } + } + const toRemove = existingYAxisKeys.filter(k => foundYAxisKeys.indexOf(k) === -1); + for (const key of toRemove) { + const index = latestKeys.indexOf(key); + if (index > -1) { + latestKeys.splice(index, 1); + } + } + } +} + +export const isYAxisKey = (d: DataKey, limit: ValueSourceConfig): boolean => { + return (d.type === DataKeyType.function && d.label === limit.latestKey) || + (d.type !== DataKeyType.function && d.name === limit.latestKey && + d.type === limit.latestKeyType); +} + +export const normalizeAxisLimit = (limit: string | number | ValueSourceConfig): ValueSourceConfig => { + if (!limit) { + return { + type: ValueSourceType.constant, + value: null, + entityAlias: null + }; + } else if (typeof limit === 'number' || typeof limit === 'string') { + return { + type: ValueSourceType.constant, + value: Number(limit), + entityAlias: null + }; + } + return limit; +} + diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.ts index 21f5f2d68c..d1928d75f0 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 @@ -23,7 +23,7 @@ import { createTimeSeriesYAxis, defaultTimeSeriesChartYAxisSettings, generateChartData, - LineSeriesStepType, + LineSeriesStepType, normalizeAxisLimit, parseThresholdData, TimeSeriesChartAxis, TimeSeriesChartDataItem, @@ -581,8 +581,8 @@ export class TbTimeSeriesChart { yAxisSettingsList.sort((a1, a2) => a1.order - a2.order); const axisLimitDatasources: Datasource[] = []; for (const yAxisSettings of yAxisSettingsList) { - yAxisSettings.min = this.normalizeAxisLimit(yAxisSettings.min); - yAxisSettings.max = this.normalizeAxisLimit(yAxisSettings.max); + yAxisSettings.min = normalizeAxisLimit(yAxisSettings.min); + yAxisSettings.max = normalizeAxisLimit(yAxisSettings.max); const axisSettings = mergeDeep({} as TimeSeriesChartYAxisSettings, defaultTimeSeriesChartYAxisSettings, yAxisSettings); const units = isNotEmptyTbUnits(axisSettings.units) ? axisSettings.units : this.ctx.units; @@ -1080,21 +1080,4 @@ export class TbTimeSeriesChart { this.timeSeriesChart.setOption(this.timeSeriesChartOptions); } } - - private normalizeAxisLimit(limit: string | number | ValueSourceConfig): string | number | ValueSourceConfig { - if (!limit) { - return { - type: ValueSourceType.constant, - value: null, - entityAlias: null - }; - } else if (typeof limit === 'number' || typeof limit === 'string') { - return { - type: ValueSourceType.constant, - value: Number(limit), - entityAlias: null - }; - } - return limit; - } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/bar-chart-with-labels-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/bar-chart-with-labels-widget-settings.component.ts index cc0c5c03cd..10325d3ecc 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/bar-chart-with-labels-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/bar-chart-with-labels-widget-settings.component.ts @@ -31,6 +31,7 @@ import { barChartWithLabelsDefaultSettings } from '@home/components/widget/lib/chart/bar-chart-with-labels-widget.models'; import { getSourceTbUnitSymbol } from '@shared/models/unit.models'; +import { updateLatestDataKeys } from '@home/components/widget/lib/chart/time-series-chart.models'; @Component({ selector: 'tb-bar-chart-with-labels-widget-settings', @@ -122,6 +123,11 @@ export class BarChartWithLabelsWidgetSettingsComponent extends WidgetSettingsCom }); } + protected onSettingsChanged(updated: WidgetSettings) { + updateLatestDataKeys([updated.yAxis], this.datasource, this.dataKeyCallbacks); + super.onSettingsChanged(updated); + } + protected validatorTriggers(): string[] { return ['showBarLabel', 'showBarValue', 'showBarBorder', 'showLegend', 'showTooltip', 'tooltipShowDate']; } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/range-chart-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/range-chart-widget-settings.component.ts index 5b1b20c2fd..a1edde95d9 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/range-chart-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/range-chart-widget-settings.component.ts @@ -30,7 +30,7 @@ import { rangeChartDefaultSettings } from '@home/components/widget/lib/chart/ran import { DateFormatProcessor, DateFormatSettings } from '@shared/models/widget-settings.models'; import { lineSeriesStepTypes, - lineSeriesStepTypeTranslations + lineSeriesStepTypeTranslations, updateLatestDataKeys } from '@home/components/widget/lib/chart/time-series-chart.models'; import { chartLabelPositions, @@ -268,6 +268,11 @@ export class RangeChartWidgetSettingsComponent extends WidgetSettingsComponent { } } + protected onSettingsChanged(updated: WidgetSettings) { + updateLatestDataKeys([updated.yAxis], this.datasource, this.dataKeyCallbacks); + super.onSettingsChanged(updated); + } + private _pointLabelPreviewFn(): string { const units = getSourceTbUnitSymbol(this.widgetConfig.config.units); const decimals: number = this.widgetConfig.config.decimals; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/axis-scale-row.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/axis-scale-row.component.ts index 88b01c19c5..9fe6657bef 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/axis-scale-row.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/axis-scale-row.component.ts @@ -96,7 +96,7 @@ export class AxisScaleRowComponent implements ControlValueAccessor, OnInit, Vali this.limitForm = this.fb.group({ type: [ValueSourceType.constant], value: [null], - entityAlias: [null] + entityAlias: [null, [Validators.required]] }); this.latestKeyFormControl = this.fb.control(null, [Validators.required]); this.entityKeyFormControl = this.fb.control(null, [Validators.required]); @@ -168,23 +168,24 @@ export class AxisScaleRowComponent implements ControlValueAccessor, OnInit, Vali } private updateValidators() { - const axisTypeControl = this.limitForm.get('type'); - if (axisTypeControl && this.entityKeyFormControl && this.latestKeyFormControl) { - const type = axisTypeControl.value; - if (type === ValueSourceType.latestKey) { - this.latestKeyFormControl.setValidators([Validators.required]); - this.entityKeyFormControl.clearValidators(); - } else if (type === ValueSourceType.entity) { - this.latestKeyFormControl.clearValidators(); - this.limitForm.get('entityAlias').setValidators([Validators.required]); - this.entityKeyFormControl.setValidators([Validators.required]); - } else { - this.latestKeyFormControl.clearValidators(); - this.entityKeyFormControl.clearValidators(); - } - this.latestKeyFormControl.updateValueAndValidity({ emitEvent: false }); - this.entityKeyFormControl.updateValueAndValidity({ emitEvent: false }); - } + const type = this.limitForm.get('type')?.value; + const entityAliasCtr = this.limitForm.get('entityAlias'); + + const isLatestKey = type === ValueSourceType.latestKey; + const isEntity = type === ValueSourceType.entity; + + isLatestKey ? this.latestKeyFormControl.enable({ emitEvent: false }) + : this.latestKeyFormControl.disable({ emitEvent: false }); + + isEntity ? this.entityKeyFormControl.enable({ emitEvent: false }) + : this.entityKeyFormControl.disable({ emitEvent: false }); + + isEntity ? entityAliasCtr.enable({ emitEvent: false }) + : entityAliasCtr.disable({ emitEvent: false }); + + this.latestKeyFormControl.updateValueAndValidity({ emitEvent: false }); + this.entityKeyFormControl.updateValueAndValidity({ emitEvent: false }); + entityAliasCtr.updateValueAndValidity({ emitEvent: false }); } private updateModel() { 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 f31775f22f..9cbd41d0cd 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 @@ -25,7 +25,7 @@ import { Validators } from '@angular/forms'; import { - AxisPosition, defaultXAxisTicksFormat, + AxisPosition, defaultXAxisTicksFormat, normalizeAxisLimit, timeSeriesAxisPositionTranslations, TimeSeriesChartAxisSettings, TimeSeriesChartXAxisSettings, TimeSeriesChartYAxisSettings @@ -137,8 +137,8 @@ export class TimeSeriesChartAxisSettingsComponent implements OnInit, ControlValu this.axisSettingsFormGroup.addControl('ticksGenerator', this.fb.control(null, [])); this.axisSettingsFormGroup.addControl('interval', this.fb.control(null, [Validators.min(0)])); this.axisSettingsFormGroup.addControl('splitNumber', this.fb.control(null, [Validators.min(1)])); - this.axisSettingsFormGroup.addControl('min', this.fb.control(null, [])); - this.axisSettingsFormGroup.addControl('max', this.fb.control(null, [])); + this.axisSettingsFormGroup.addControl('min', this.fb.control(normalizeAxisLimit(null), [])); + this.axisSettingsFormGroup.addControl('max', this.fb.control(normalizeAxisLimit(null), [])); } else if (this.axisType === 'xAxis') { this.axisSettingsFormGroup.addControl('ticksFormat', this.fb.control(null, [])); } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axes-panel.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axes-panel.component.ts index 1d4b93c712..042a3f1a14 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axes-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axes-panel.component.ts @@ -36,13 +36,15 @@ import { Validator } from '@angular/forms'; import { + checkLatestDataKeys, defaultTimeSeriesChartYAxisSettings, getNextTimeSeriesYAxisId, TimeSeriesChartYAxes, TimeSeriesChartYAxisId, TimeSeriesChartYAxisSettings, timeSeriesChartYAxisValid, - timeSeriesChartYAxisValidator + timeSeriesChartYAxisValidator, + updateLatestDataKeys } from '@home/components/widget/lib/chart/time-series-chart.models'; import { mergeDeep } from '@core/utils'; import { CdkDragDrop } from '@angular/cdk/drag-drop'; @@ -50,7 +52,7 @@ import { coerceBoolean } from '@shared/decorators/coercion'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { IAliasController } from '@app/core/public-api'; import { DataKeysCallbacks } from '@home/components/widget/lib/settings/common/key/data-keys.component.models'; -import { DataKey, DataKeyType, Datasource, ValueSourceConfig, ValueSourceType } from '@app/shared/public-api'; +import { Datasource } from '@app/shared/public-api'; @Component({ selector: 'tb-time-series-chart-y-axes-panel', @@ -126,7 +128,7 @@ export class TimeSeriesChartYAxesPanelComponent implements ControlValueAccessor, for (const axis of axes) { yAxes[axis.id] = axis; } - this.updateLatestDataKeys(Object.values(yAxes)); + updateLatestDataKeys(Object.values(yAxes), this.datasource, this.dataKeyCallbacks); this.propagateChange(yAxes); } ); @@ -149,7 +151,7 @@ export class TimeSeriesChartYAxesPanelComponent implements ControlValueAccessor, } writeValue(value: TimeSeriesChartYAxes | undefined): void { - const yAxes: TimeSeriesChartYAxes = this.checkLatestDataKeys(value || {}); + const yAxes: TimeSeriesChartYAxes = checkLatestDataKeys(value || {}, this.datasource); if (!yAxes.default) { yAxes.default = mergeDeep({} as TimeSeriesChartYAxisSettings, defaultTimeSeriesChartYAxisSettings, {id: 'default', order: 0} as TimeSeriesChartYAxisSettings); @@ -196,8 +198,6 @@ export class TimeSeriesChartYAxesPanelComponent implements ControlValueAccessor, const axes: TimeSeriesChartYAxisSettings[] = this.yAxesFormGroup.get('axes').value; axis.id = getNextTimeSeriesYAxisId(axes); axis.order = axes.length; - axis.min = this.normalizeAxisLimit(axis.min); - axis.max = this.normalizeAxisLimit(axis.max); const axesArray = this.yAxesFormGroup.get('axes') as UntypedFormArray; const axisControl = this.fb.control(axis, [timeSeriesChartYAxisValidator]); axesArray.push(axisControl); @@ -211,100 +211,4 @@ export class TimeSeriesChartYAxesPanelComponent implements ControlValueAccessor, return this.fb.array(axesControls); } - private checkLatestDataKeys(yAxes: TimeSeriesChartYAxes): TimeSeriesChartYAxes { - const latestKeys = this.datasource?.latestDataKeys || []; - const result: TimeSeriesChartYAxes = {}; - - for (const [id, axis] of Object.entries(yAxes)) { - axis.min = this.normalizeAxisLimit(axis.min); - axis.max = this.normalizeAxisLimit(axis.max); - const minCfg = axis.min; - const maxCfg = axis.max; - - const minValid = !!minCfg && ( - minCfg.type !== ValueSourceType.latestKey || - latestKeys.some(k => this.isYAxisKey(k, minCfg)) - ); - - const maxValid = !!maxCfg && ( - maxCfg.type !== ValueSourceType.latestKey || - latestKeys.some(k => this.isYAxisKey(k, maxCfg)) - ); - - if (minValid && maxValid) { - result[id] = axis; - } - } - - return result; - } - - private updateLatestDataKeys(yAxes: TimeSeriesChartYAxisSettings[]) { - if (this.datasource) { - let latestKeys = this.datasource.latestDataKeys; - if (!latestKeys) { - latestKeys = []; - this.datasource.latestDataKeys = latestKeys; - } - const existingYAxisKeys = latestKeys.filter(k => k.settings?.__yAxisMinKey === true || k.settings?.__yAxisMaxKey === true); - const foundYAxisKeys: DataKey[] = []; - - for(const yAxis of yAxes) { - const min = yAxis.min as ValueSourceConfig; - const max = yAxis.max as ValueSourceConfig; - if (min.type === ValueSourceType.latestKey) { - const found = existingYAxisKeys.find(k => this.isYAxisKey(k, min)); - if (!found) { - const newKey = this.dataKeyCallbacks.generateDataKey(min.latestKey, min.latestKeyType, - null, true, null); - newKey.settings.__yAxisMinKey = true; - latestKeys.push(newKey); - } else if (foundYAxisKeys.indexOf(found) === -1) { - foundYAxisKeys.push(found); - } - } - if (max.type === ValueSourceType.latestKey) { - const found = existingYAxisKeys.find(k => this.isYAxisKey(k, max)); - if (!found) { - const newKey = this.dataKeyCallbacks.generateDataKey(max.latestKey, max.latestKeyType, - null, true, null); - newKey.settings.__yAxisMaxKey = true; - latestKeys.push(newKey); - } else if (foundYAxisKeys.indexOf(found) === -1) { - foundYAxisKeys.push(found); - } - } - } - const toRemove = existingYAxisKeys.filter(k => foundYAxisKeys.indexOf(k) === -1); - for (const key of toRemove) { - const index = latestKeys.indexOf(key); - if (index > -1) { - latestKeys.splice(index, 1); - } - } - } - } - - private isYAxisKey(d: DataKey, limit: ValueSourceConfig): boolean { - return (d.type === DataKeyType.function && d.label === limit.latestKey) || - (d.type !== DataKeyType.function && d.name === limit.latestKey && - d.type === limit.latestKeyType); - } - - private normalizeAxisLimit(limit: string | number | ValueSourceConfig): ValueSourceConfig { - if (!limit) { - return { - type: ValueSourceType.constant, - value: null, - entityAlias: null - }; - } else if (typeof limit === 'number' || typeof limit === 'string') { - return { - type: ValueSourceType.constant, - value: Number(limit), - entityAlias: null - }; - } - return limit; - } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-row.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-row.component.ts index 54d54d1a98..cbf9855a26 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-row.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-row.component.ts @@ -29,7 +29,7 @@ import { } from '@angular/core'; import { ControlValueAccessor, NG_VALUE_ACCESSOR, UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; import { - AxisPosition, + AxisPosition, normalizeAxisLimit, timeSeriesAxisPositionTranslations, TimeSeriesChartYAxisSettings } from '@home/components/widget/lib/chart/time-series-chart.models'; @@ -135,8 +135,8 @@ export class TimeSeriesChartYAxisRowComponent implements ControlValueAccessor, O writeValue(value: TimeSeriesChartYAxisSettings): void { this.modelValue = value; - const min = this.normalizeLimit(value.min); - const max = this.normalizeLimit(value.max); + const min = normalizeAxisLimit(value.min); + const max = normalizeAxisLimit(value.max); this.axisFormGroup.patchValue({ label: value.label, @@ -251,27 +251,4 @@ export class TimeSeriesChartYAxisRowComponent implements ControlValueAccessor, O entityKeyType: [null, []] }); } - - private normalizeLimit(limit: any) { - const base = { - type: ValueSourceType.constant, - value: null, - latestKey: null, - latestKeyType: null, - entityAlias: null, - entityKey: null, - entityKeyType: null - }; - - if (limit == null) return base; - - if (typeof limit === 'number' || typeof limit === 'string') { - return { ...base, type: ValueSourceType.constant, value: Number(limit) }; - } - - return { - ...base, - ...limit, - }; - } } From 2a13590f7e3817bc949ff732206bd8cf4e1517a3 Mon Sep 17 00:00:00 2001 From: Maksym Tsymbarov Date: Thu, 12 Feb 2026 18:03:16 +0200 Subject: [PATCH 2/5] Moved from library RGBA and HSLA inputs for color picker --- .../@iplab+ngx-color-picker+18.0.1.patch | 26 +++++++ .../color-picker/color-input.base.scss | 28 ++++++++ .../color-picker-panel.component.scss | 2 +- .../color-picker/color-picker.component.html | 6 +- .../color-picker/color-picker.component.scss | 4 +- .../color-picker/hex-input.component.scss | 6 +- .../color-picker/hsla-input.component.html | 54 ++++++++++++++ .../color-picker/hsla-input.component.ts | 71 +++++++++++++++++++ .../color-picker/input-change.directive.ts | 45 ++++++++++++ .../color-picker/rgba-input.component.html | 52 ++++++++++++++ .../color-picker/rgba-input.component.ts | 70 ++++++++++++++++++ ui-ngx/src/app/shared/shared.module.ts | 8 ++- 12 files changed, 361 insertions(+), 11 deletions(-) create mode 100644 ui-ngx/patches/@iplab+ngx-color-picker+18.0.1.patch create mode 100644 ui-ngx/src/app/shared/components/color-picker/color-input.base.scss create mode 100644 ui-ngx/src/app/shared/components/color-picker/hsla-input.component.html create mode 100644 ui-ngx/src/app/shared/components/color-picker/hsla-input.component.ts create mode 100644 ui-ngx/src/app/shared/components/color-picker/input-change.directive.ts create mode 100644 ui-ngx/src/app/shared/components/color-picker/rgba-input.component.html create mode 100644 ui-ngx/src/app/shared/components/color-picker/rgba-input.component.ts diff --git a/ui-ngx/patches/@iplab+ngx-color-picker+18.0.1.patch b/ui-ngx/patches/@iplab+ngx-color-picker+18.0.1.patch new file mode 100644 index 0000000000..64fe082fb8 --- /dev/null +++ b/ui-ngx/patches/@iplab+ngx-color-picker+18.0.1.patch @@ -0,0 +1,26 @@ +diff --git a/node_modules/@iplab/ngx-color-picker/esm2022/lib/helpers/color.class.mjs b/node_modules/@iplab/ngx-color-picker/esm2022/lib/helpers/color.class.mjs +index 6c7f8b1..f390b3b 100644 +--- a/node_modules/@iplab/ngx-color-picker/esm2022/lib/helpers/color.class.mjs ++++ b/node_modules/@iplab/ngx-color-picker/esm2022/lib/helpers/color.class.mjs +@@ -173,7 +173,7 @@ export class Color { + const s = (color.saturation / 100) * (l <= 1 ? l : 2 - l); + const value = (l + s) / 2; + const saturation = (2 * s) / (l + s) || 0; +- return new Hsva(hue, saturation, value, color.alpha); ++ return new Hsva(hue, saturation * 100, value * 100, color.alpha); + } + rgbaToHsva(color) { + const red = color.red / 255; +diff --git a/node_modules/@iplab/ngx-color-picker/fesm2022/iplab-ngx-color-picker.mjs b/node_modules/@iplab/ngx-color-picker/fesm2022/iplab-ngx-color-picker.mjs +index a3b270c..9a9b592 100644 +--- a/node_modules/@iplab/ngx-color-picker/fesm2022/iplab-ngx-color-picker.mjs ++++ b/node_modules/@iplab/ngx-color-picker/fesm2022/iplab-ngx-color-picker.mjs +@@ -505,7 +505,7 @@ class Color { + const s = (color.saturation / 100) * (l <= 1 ? l : 2 - l); + const value = (l + s) / 2; + const saturation = (2 * s) / (l + s) || 0; +- return new Hsva(hue, saturation, value, color.alpha); ++ return new Hsva(hue, saturation * 100, value * 100, color.alpha); + } + rgbaToHsva(color) { + const red = color.red / 255; diff --git a/ui-ngx/src/app/shared/components/color-picker/color-input.base.scss b/ui-ngx/src/app/shared/components/color-picker/color-input.base.scss new file mode 100644 index 0000000000..bae2e433aa --- /dev/null +++ b/ui-ngx/src/app/shared/components/color-picker/color-input.base.scss @@ -0,0 +1,28 @@ +/** + * Copyright © 2016-2026 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. + */ +:host { + .color-input-container { + display: flex; + gap: 4px; + align-items: center; + } + .color-input { + max-width: 72px; + margin-bottom: 4px; + } +} + + diff --git a/ui-ngx/src/app/shared/components/color-picker/color-picker-panel.component.scss b/ui-ngx/src/app/shared/components/color-picker/color-picker-panel.component.scss index f9db566be8..5e867f9e61 100644 --- a/ui-ngx/src/app/shared/components/color-picker/color-picker-panel.component.scss +++ b/ui-ngx/src/app/shared/components/color-picker/color-picker-panel.component.scss @@ -16,7 +16,7 @@ @import "../scss/constants"; .tb-color-picker-panel { - width: 342px; + width: 370px; display: flex; flex-direction: column; max-height: calc(100vh - 24px); diff --git a/ui-ngx/src/app/shared/components/color-picker/color-picker.component.html b/ui-ngx/src/app/shared/components/color-picker/color-picker.component.html index 676dafbfef..fcb1cb067d 100644 --- a/ui-ngx/src/app/shared/components/color-picker/color-picker.component.html +++ b/ui-ngx/src/app/shared/components/color-picker/color-picker.component.html @@ -37,10 +37,8 @@ HSLA
- - + +
diff --git a/ui-ngx/src/app/shared/components/color-picker/color-picker.component.scss b/ui-ngx/src/app/shared/components/color-picker/color-picker.component.scss index ed161a5b9f..799bcdf00c 100644 --- a/ui-ngx/src/app/shared/components/color-picker/color-picker.component.scss +++ b/ui-ngx/src/app/shared/components/color-picker/color-picker.component.scss @@ -83,7 +83,7 @@ height: 56px; display: flex; align-items: center; - gap: 20px; + gap: 8px; .presentation-select { font-size: 14px; @@ -104,7 +104,7 @@ display: flex; flex-direction: row; flex-wrap: wrap; - justify-content: space-between; + justify-content: center; gap: 8px; @media #{$mat-xs} { flex-direction: column; diff --git a/ui-ngx/src/app/shared/components/color-picker/hex-input.component.scss b/ui-ngx/src/app/shared/components/color-picker/hex-input.component.scss index 2f7a404c0d..73a6aff9f6 100644 --- a/ui-ngx/src/app/shared/components/color-picker/hex-input.component.scss +++ b/ui-ngx/src/app/shared/components/color-picker/hex-input.component.scss @@ -19,11 +19,11 @@ gap: 8px; } .hex-input { - max-width: 190px; + max-width: 220px; } .alpha-input { - min-width: 60px; - max-width: 60px; + min-width: 72px; + max-width: 72px; } ::ng-deep { diff --git a/ui-ngx/src/app/shared/components/color-picker/hsla-input.component.html b/ui-ngx/src/app/shared/components/color-picker/hsla-input.component.html new file mode 100644 index 0000000000..cf8e25eb51 --- /dev/null +++ b/ui-ngx/src/app/shared/components/color-picker/hsla-input.component.html @@ -0,0 +1,54 @@ + +
+
+ + + + @if (labelVisible) { + H + } +
+
+ + + {{suffixValue}} + + @if (labelVisible) { + S + } +
+
+ + + {{suffixValue}} + + @if (labelVisible) { + L + } +
+
+ + + {{suffixValue}} + + @if (labelVisible) { + A + } +
+
diff --git a/ui-ngx/src/app/shared/components/color-picker/hsla-input.component.ts b/ui-ngx/src/app/shared/components/color-picker/hsla-input.component.ts new file mode 100644 index 0000000000..7965c48f28 --- /dev/null +++ b/ui-ngx/src/app/shared/components/color-picker/hsla-input.component.ts @@ -0,0 +1,71 @@ +/// +/// Copyright © 2016-2026 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, Output } from '@angular/core'; +import { Color } from '@iplab/ngx-color-picker'; +import { coerceBoolean } from '@shared/decorators/coercion'; + +type Channel = 'H' | 'S' | 'L'; + +@Component({ + selector: 'tb-hsla-input', + templateUrl: './hsla-input.component.html', + styleUrl: './color-input.base.scss' +}) +export class HslaInputComponent { + + @Input() + public color: Color; + + @Output() + public colorChange = new EventEmitter(false); + + @Input() + @coerceBoolean() + public labelVisible = false; + + @Input() + public suffixValue = '%'; + + public get value() { + return this.color.getHsla(); + } + + public get alphaValue(): number { + return this.color ? Math.round(this.color.getHsla().getAlpha() * 100) : 0; + } + + public onAlphaInputChange(inputValue: number): void { + if (!this.color) return; + const hsla = this.color.getHsla(); + const alpha = +inputValue / 100; + if (hsla.alpha !== alpha) { + const newColor = new Color().setHsla(hsla.getHue(), hsla.getSaturation(), hsla.getLightness(), alpha); + this.colorChange.emit(newColor); + } + } + + public onInputChange(newValue: number, channel: Channel): void { + if (!this.color) return; + const hsla = this.value; + const hue = channel === 'H' ? +newValue : hsla.getHue(); + const saturation = channel === 'S' ? +newValue : hsla.getSaturation(); + const lightness = channel === 'L' ? +newValue : hsla.getLightness(); + if (hue === hsla.getHue() && saturation === hsla.getSaturation() && lightness === hsla.getLightness()) return; + const newColor = new Color().setHsla(hue, saturation, lightness, hsla.getAlpha()); + this.colorChange.emit(newColor); + } +} diff --git a/ui-ngx/src/app/shared/components/color-picker/input-change.directive.ts b/ui-ngx/src/app/shared/components/color-picker/input-change.directive.ts new file mode 100644 index 0000000000..0c493a2118 --- /dev/null +++ b/ui-ngx/src/app/shared/components/color-picker/input-change.directive.ts @@ -0,0 +1,45 @@ +/// +/// Copyright © 2016-2026 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 { Directive, EventEmitter, HostBinding, HostListener, Input, numberAttribute, Output } from '@angular/core'; + +@Directive({ + selector: '[inputChange]' +}) +export class InputChangeDirective { + + @Input({transform: numberAttribute}) + @HostBinding('attr.min') + min = 0; + + @Input({transform: numberAttribute}) + @HostBinding('attr.max') + max = 255; + + @Output() + public inputChange = new EventEmitter(); + + @HostListener('input', ['$event']) + public inputChanges(event: any): void { + const element = event.target as HTMLInputElement || event.srcElement as HTMLInputElement; + const value = element.value; + + const numeric = parseFloat(value); + if (!isNaN(numeric) && numeric >= this.min && numeric <= this.max) { + this.inputChange.emit(numeric); + } + } +} diff --git a/ui-ngx/src/app/shared/components/color-picker/rgba-input.component.html b/ui-ngx/src/app/shared/components/color-picker/rgba-input.component.html new file mode 100644 index 0000000000..3cb856f147 --- /dev/null +++ b/ui-ngx/src/app/shared/components/color-picker/rgba-input.component.html @@ -0,0 +1,52 @@ + +
+
+ + + + @if (labelVisible) { + R + } +
+
+ + + + @if (labelVisible) { + G + } +
+
+ + + + @if (labelVisible) { + B + } +
+
+ + + {{suffixValue}} + + @if (labelVisible) { + A + } +
+
diff --git a/ui-ngx/src/app/shared/components/color-picker/rgba-input.component.ts b/ui-ngx/src/app/shared/components/color-picker/rgba-input.component.ts new file mode 100644 index 0000000000..1cf9ea5768 --- /dev/null +++ b/ui-ngx/src/app/shared/components/color-picker/rgba-input.component.ts @@ -0,0 +1,70 @@ +/// +/// Copyright © 2016-2026 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, Output } from '@angular/core'; +import { Color } from '@iplab/ngx-color-picker'; +import { coerceBoolean } from '@shared/decorators/coercion'; + +type Channel = 'R' | 'G' | 'B' | 'A'; + +@Component({ + selector: 'tb-rgba-input', + templateUrl: './rgba-input.component.html', + styleUrl: './color-input.base.scss' +}) +export class RgbaInputComponent { + + @Input() + public color: Color; + + @Output() + public colorChange = new EventEmitter(false); + + @Input() + @coerceBoolean() + public labelVisible = false; + + @Input() + public suffixValue = '%'; + + public get value() { + return this.color.getRgba(); + } + + public get alphaValue(): string { + return this.color ? Math.round(this.color.getRgba().getAlpha() * 100).toString() : ''; + } + + public onAlphaInputChange(inputValue: number): void { + if (!this.color) return; + const color = this.color.getRgba(); + const alpha = +inputValue / 100; + if (color.getAlpha() !== alpha) { + const newColor = new Color().setRgba(color.getRed(), color.getGreen(), color.getBlue(), alpha).toRgbaString(); + this.colorChange.emit(new Color(newColor)); + } + } + + onInputChange(newValue: number, channel: Channel) { + if (!this.color) return; + const rgba = this.value; + const red = channel === 'R' ? newValue : rgba.getRed(); + const green = channel === 'G' ? newValue : rgba.getGreen(); + const blue = channel === 'B' ? newValue : rgba.getBlue(); + if (red === rgba.getRed() && green === rgba.getGreen() && blue === rgba.getBlue()) return; + this.colorChange.emit(new Color().setRgba(red, green, blue, rgba.alpha)); + } +} diff --git a/ui-ngx/src/app/shared/shared.module.ts b/ui-ngx/src/app/shared/shared.module.ts index dfe86539bc..cefc1c8824 100644 --- a/ui-ngx/src/app/shared/shared.module.ts +++ b/ui-ngx/src/app/shared/shared.module.ts @@ -239,6 +239,9 @@ import { DateExpirationPipe } from '@shared/pipe/date-expiration.pipe'; import { EntityLimitExceededDialogComponent } from '@shared/components/dialog/entity-limit-exceeded-dialog.component'; import { DynamicMatDialogModule } from '@shared/components/dialog/dynamic/dynamic-dialog.module'; import { MAT_BUTTON_TOGGLE_DEFAULT_OPTIONS } from '@angular/material/button-toggle'; +import { RgbaInputComponent } from '@shared/components/color-picker/rgba-input.component'; +import { HslaInputComponent } from '@shared/components/color-picker/hsla-input.component'; +import { InputChangeDirective } from '@shared/components/color-picker/input-change.directive'; export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) { return markedOptionsService; @@ -466,7 +469,10 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) MqttVersionSelectComponent, PasswordRequirementsTooltipComponent, TimeUnitInputComponent, - StringPatternAutocompleteComponent + StringPatternAutocompleteComponent, + RgbaInputComponent, + HslaInputComponent, + InputChangeDirective ], imports: [ CommonModule, From 6488153b4fdfb6b57475ee0ac2dcab6c7b07e1a4 Mon Sep 17 00:00:00 2001 From: Maksym Tsymbarov Date: Thu, 12 Feb 2026 18:19:17 +0200 Subject: [PATCH 3/5] Updated patch --- .../@iplab+ngx-color-picker+20.0.0.patch | 51 ++++--------------- 1 file changed, 9 insertions(+), 42 deletions(-) diff --git a/ui-ngx/patches/@iplab+ngx-color-picker+20.0.0.patch b/ui-ngx/patches/@iplab+ngx-color-picker+20.0.0.patch index 211a3c8b0b..b0087a67e9 100644 --- a/ui-ngx/patches/@iplab+ngx-color-picker+20.0.0.patch +++ b/ui-ngx/patches/@iplab+ngx-color-picker+20.0.0.patch @@ -1,46 +1,13 @@ diff --git a/node_modules/@iplab/ngx-color-picker/fesm2022/iplab-ngx-color-picker.mjs b/node_modules/@iplab/ngx-color-picker/fesm2022/iplab-ngx-color-picker.mjs -index a372799..a3d709a 100644 +index a372799..f64a6f8 100644 --- a/node_modules/@iplab/ngx-color-picker/fesm2022/iplab-ngx-color-picker.mjs +++ b/node_modules/@iplab/ngx-color-picker/fesm2022/iplab-ngx-color-picker.mjs -@@ -1129,11 +1129,11 @@ class RgbaComponent { - this.color.set(newColor); +@@ -516,7 +516,7 @@ class Color { + const s = (color.saturation / 100) * (l <= 1 ? l : 2 - l); + const value = (l + s) / 2; + const saturation = (2 * s) / (l + s) || 0; +- return new Hsva(hue, saturation, value, color.alpha); ++ return new Hsva(hue, saturation * 100, value * 100, color.alpha); } - static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.4", ngImport: i0, type: RgbaComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); } -- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.0.4", type: RgbaComponent, isStandalone: true, selector: "rgba-input-component", inputs: { color: { classPropertyName: "color", publicName: "color", isSignal: true, isRequired: true, transformFunction: null }, labelVisible: { classPropertyName: "labelVisible", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, isAlphaVisible: { classPropertyName: "isAlphaVisible", publicName: "alpha", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { color: "colorChange" }, ngImport: i0, template: "
\r\n \r\n @if (labelVisible()) {\r\n R\r\n }\r\n
\r\n
\r\n \r\n @if (labelVisible()) {\r\n G\r\n }\r\n
\r\n
\r\n \r\n @if (labelVisible()) {\r\n B\r\n }\r\n
\r\n@if (isAlphaVisible()) {\r\n
\r\n \r\n @if (labelVisible()) {\r\n A\r\n }\r\n
\r\n}", styles: [":host,:host ::ng-deep *{padding:0;margin:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}\n", ":host{display:table;width:100%;text-align:center;color:#b4b4b4;font-size:11px}.column{display:table-cell;padding:0 2px}input{width:100%;border:1px solid rgb(218,218,218);color:#272727;text-align:center;font-size:12px;-webkit-appearance:none;border-radius:0;margin:0 0 6px;height:26px;outline:none}\n", ""], dependencies: [{ kind: "directive", type: ColorPickerInputDirective, selector: "[inputChange]", inputs: ["min", "max"], outputs: ["inputChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); } -+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.0.4", type: RgbaComponent, isStandalone: true, selector: "rgba-input-component", inputs: { color: { classPropertyName: "color", publicName: "color", isSignal: true, isRequired: true, transformFunction: null }, labelVisible: { classPropertyName: "labelVisible", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, isAlphaVisible: { classPropertyName: "isAlphaVisible", publicName: "alpha", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { color: "colorChange" }, ngImport: i0, template: "
\r\n \r\n @if (labelVisible()) {\r\n R\r\n }\r\n
\r\n
\r\n \r\n @if (labelVisible()) {\r\n G\r\n }\r\n
\r\n
\r\n \r\n @if (labelVisible()) {\r\n B\r\n }\r\n
\r\n@if (isAlphaVisible()) {\r\n
\r\n \r\n @if (labelVisible()) {\r\n A\r\n }\r\n
\r\n}", styles: [":host,:host ::ng-deep *{padding:0;margin:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}\n", ":host{display:table;width:100%;text-align:center;color:#b4b4b4;font-size:11px}.column{display:table-cell;padding:0 2px}input{width:100%;border:1px solid rgb(218,218,218);color:#272727;text-align:center;font-size:12px;-webkit-appearance:none;border-radius:0;margin:0 0 6px;height:26px;outline:none}\n", ""], dependencies: [{ kind: "directive", type: ColorPickerInputDirective, selector: "[inputChange]", inputs: ["min", "max"], outputs: ["inputChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); } - } - i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.4", ngImport: i0, type: RgbaComponent, decorators: [{ - type: Component, -- args: [{ selector: `rgba-input-component`, changeDetection: ChangeDetectionStrategy.OnPush, imports: [ColorPickerInputDirective], template: "
\r\n \r\n @if (labelVisible()) {\r\n R\r\n }\r\n
\r\n
\r\n \r\n @if (labelVisible()) {\r\n G\r\n }\r\n
\r\n
\r\n \r\n @if (labelVisible()) {\r\n B\r\n }\r\n
\r\n@if (isAlphaVisible()) {\r\n
\r\n \r\n @if (labelVisible()) {\r\n A\r\n }\r\n
\r\n}", styles: [":host,:host ::ng-deep *{padding:0;margin:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}\n", ":host{display:table;width:100%;text-align:center;color:#b4b4b4;font-size:11px}.column{display:table-cell;padding:0 2px}input{width:100%;border:1px solid rgb(218,218,218);color:#272727;text-align:center;font-size:12px;-webkit-appearance:none;border-radius:0;margin:0 0 6px;height:26px;outline:none}\n"] }] -+ args: [{ selector: `rgba-input-component`, changeDetection: ChangeDetectionStrategy.OnPush, imports: [ColorPickerInputDirective], template: "
\r\n \r\n @if (labelVisible()) {\r\n R\r\n }\r\n
\r\n
\r\n \r\n @if (labelVisible()) {\r\n G\r\n }\r\n
\r\n
\r\n \r\n @if (labelVisible()) {\r\n B\r\n }\r\n
\r\n@if (isAlphaVisible()) {\r\n
\r\n \r\n @if (labelVisible()) {\r\n A\r\n }\r\n
\r\n}", styles: [":host,:host ::ng-deep *{padding:0;margin:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}\n", ":host{display:table;width:100%;text-align:center;color:#b4b4b4;font-size:11px}.column{display:table-cell;padding:0 2px}input{width:100%;border:1px solid rgb(218,218,218);color:#272727;text-align:center;font-size:12px;-webkit-appearance:none;border-radius:0;margin:0 0 6px;height:26px;outline:none}\n"] }] - }] }); - - class HslaComponent { -@@ -1155,11 +1155,11 @@ class HslaComponent { - this.color.set(newColor); - } - static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.4", ngImport: i0, type: HslaComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); } -- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.0.4", type: HslaComponent, isStandalone: true, selector: "hsla-input-component", inputs: { color: { classPropertyName: "color", publicName: "color", isSignal: true, isRequired: true, transformFunction: null }, labelVisible: { classPropertyName: "labelVisible", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, isAlphaVisible: { classPropertyName: "isAlphaVisible", publicName: "alpha", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { color: "colorChange" }, ngImport: i0, template: "
\r\n \r\n @if (labelVisible()) {\r\n H\r\n }\r\n
\r\n
\r\n \r\n @if (labelVisible()) {\r\n S\r\n }\r\n
\r\n
\r\n \r\n @if (labelVisible()) {\r\n L\r\n }\r\n
\r\n@if (isAlphaVisible()) {\r\n
\r\n \r\n @if (labelVisible()) {\r\n A\r\n }\r\n
\r\n}", styles: [":host,:host ::ng-deep *{padding:0;margin:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}\n", ":host{display:table;width:100%;text-align:center;color:#b4b4b4;font-size:11px}.column{display:table-cell;padding:0 2px}input{width:100%;border:1px solid rgb(218,218,218);color:#272727;text-align:center;font-size:12px;-webkit-appearance:none;border-radius:0;margin:0 0 6px;height:26px;outline:none}\n", ""], dependencies: [{ kind: "directive", type: ColorPickerInputDirective, selector: "[inputChange]", inputs: ["min", "max"], outputs: ["inputChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); } -+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.0.4", type: HslaComponent, isStandalone: true, selector: "hsla-input-component", inputs: { color: { classPropertyName: "color", publicName: "color", isSignal: true, isRequired: true, transformFunction: null }, labelVisible: { classPropertyName: "labelVisible", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, isAlphaVisible: { classPropertyName: "isAlphaVisible", publicName: "alpha", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { color: "colorChange" }, ngImport: i0, template: "
\r\n \r\n @if (labelVisible()) {\r\n H\r\n }\r\n
\r\n
\r\n \r\n @if (labelVisible()) {\r\n S\r\n }\r\n
\r\n
\r\n \r\n @if (labelVisible()) {\r\n L\r\n }\r\n
\r\n@if (isAlphaVisible()) {\r\n
\r\n \r\n @if (labelVisible()) {\r\n A\r\n }\r\n
\r\n}", styles: [":host,:host ::ng-deep *{padding:0;margin:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}\n", ":host{display:table;width:100%;text-align:center;color:#b4b4b4;font-size:11px}.column{display:table-cell;padding:0 2px}input{width:100%;border:1px solid rgb(218,218,218);color:#272727;text-align:center;font-size:12px;-webkit-appearance:none;border-radius:0;margin:0 0 6px;height:26px;outline:none}\n", ""], dependencies: [{ kind: "directive", type: ColorPickerInputDirective, selector: "[inputChange]", inputs: ["min", "max"], outputs: ["inputChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); } - } - i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.4", ngImport: i0, type: HslaComponent, decorators: [{ - type: Component, -- args: [{ selector: `hsla-input-component`, changeDetection: ChangeDetectionStrategy.OnPush, imports: [ColorPickerInputDirective], template: "
\r\n \r\n @if (labelVisible()) {\r\n H\r\n }\r\n
\r\n
\r\n \r\n @if (labelVisible()) {\r\n S\r\n }\r\n
\r\n
\r\n \r\n @if (labelVisible()) {\r\n L\r\n }\r\n
\r\n@if (isAlphaVisible()) {\r\n
\r\n \r\n @if (labelVisible()) {\r\n A\r\n }\r\n
\r\n}", styles: [":host,:host ::ng-deep *{padding:0;margin:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}\n", ":host{display:table;width:100%;text-align:center;color:#b4b4b4;font-size:11px}.column{display:table-cell;padding:0 2px}input{width:100%;border:1px solid rgb(218,218,218);color:#272727;text-align:center;font-size:12px;-webkit-appearance:none;border-radius:0;margin:0 0 6px;height:26px;outline:none}\n"] }] -+ args: [{ selector: `hsla-input-component`, changeDetection: ChangeDetectionStrategy.OnPush, imports: [ColorPickerInputDirective], template: "
\r\n \r\n @if (labelVisible()) {\r\n H\r\n }\r\n
\r\n
\r\n \r\n @if (labelVisible()) {\r\n S\r\n }\r\n
\r\n
\r\n \r\n @if (labelVisible()) {\r\n L\r\n }\r\n
\r\n@if (isAlphaVisible()) {\r\n
\r\n \r\n @if (labelVisible()) {\r\n A\r\n }\r\n
\r\n}", styles: [":host,:host ::ng-deep *{padding:0;margin:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}\n", ":host{display:table;width:100%;text-align:center;color:#b4b4b4;font-size:11px}.column{display:table-cell;padding:0 2px}input{width:100%;border:1px solid rgb(218,218,218);color:#272727;text-align:center;font-size:12px;-webkit-appearance:none;border-radius:0;margin:0 0 6px;height:26px;outline:none}\n"] }] - }] }); - - class HexComponent { -@@ -1190,11 +1190,11 @@ class HexComponent { - } - } - static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.4", ngImport: i0, type: HexComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); } -- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.0.4", type: HexComponent, isStandalone: true, selector: "hex-input-component", inputs: { color: { classPropertyName: "color", publicName: "color", isSignal: true, isRequired: true, transformFunction: null }, labelVisible: { classPropertyName: "labelVisible", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, prefixValue: { classPropertyName: "prefixValue", publicName: "prefix", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { color: "colorChange" }, ngImport: i0, template: "
\r\n \r\n @if (labelVisible()) {\r\n HEX\r\n }\r\n
", styles: [":host,:host ::ng-deep *{padding:0;margin:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}\n", ":host{display:table;width:100%;text-align:center;color:#b4b4b4;font-size:11px}.column{display:table-cell;padding:0 2px}input{width:100%;border:1px solid rgb(218,218,218);color:#272727;text-align:center;font-size:12px;-webkit-appearance:none;border-radius:0;margin:0 0 6px;height:26px;outline:none}\n", ""], changeDetection: i0.ChangeDetectionStrategy.OnPush }); } -+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.0.4", type: HexComponent, isStandalone: true, selector: "hex-input-component", inputs: { color: { classPropertyName: "color", publicName: "color", isSignal: true, isRequired: true, transformFunction: null }, labelVisible: { classPropertyName: "labelVisible", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, prefixValue: { classPropertyName: "prefixValue", publicName: "prefix", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { color: "colorChange" }, ngImport: i0, template: "
\r\n \r\n @if (labelVisible()) {\r\n HEX\r\n }\r\n
", styles: [":host,:host ::ng-deep *{padding:0;margin:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}\n", ":host{display:table;width:100%;text-align:center;color:#b4b4b4;font-size:11px}.column{display:table-cell;padding:0 2px}input{width:100%;border:1px solid rgb(218,218,218);color:#272727;text-align:center;font-size:12px;-webkit-appearance:none;border-radius:0;margin:0 0 6px;height:26px;outline:none}\n", ""], changeDetection: i0.ChangeDetectionStrategy.OnPush }); } - } - i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.4", ngImport: i0, type: HexComponent, decorators: [{ - type: Component, -- args: [{ selector: `hex-input-component`, changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, template: "
\r\n \r\n @if (labelVisible()) {\r\n HEX\r\n }\r\n
", styles: [":host,:host ::ng-deep *{padding:0;margin:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}\n", ":host{display:table;width:100%;text-align:center;color:#b4b4b4;font-size:11px}.column{display:table-cell;padding:0 2px}input{width:100%;border:1px solid rgb(218,218,218);color:#272727;text-align:center;font-size:12px;-webkit-appearance:none;border-radius:0;margin:0 0 6px;height:26px;outline:none}\n"] }] -+ args: [{ selector: `hex-input-component`, changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, template: "
\r\n \r\n @if (labelVisible()) {\r\n HEX\r\n }\r\n
", styles: [":host,:host ::ng-deep *{padding:0;margin:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}\n", ":host{display:table;width:100%;text-align:center;color:#b4b4b4;font-size:11px}.column{display:table-cell;padding:0 2px}input{width:100%;border:1px solid rgb(218,218,218);color:#272727;text-align:center;font-size:12px;-webkit-appearance:none;border-radius:0;margin:0 0 6px;height:26px;outline:none}\n"] }] - }] }); - - const OpacityAnimation = trigger('opacityAnimation', [ + rgbaToHsva(color) { + const red = color.red / 255; From 77bad150b12065a2abd97d0dcab4824ee73452fa Mon Sep 17 00:00:00 2001 From: Maksym Tsymbarov Date: Thu, 12 Feb 2026 18:42:50 +0200 Subject: [PATCH 4/5] Angular V20 updated --- .../shared/components/color-picker/hsla-input.component.ts | 3 ++- .../shared/components/color-picker/input-change.directive.ts | 3 ++- .../shared/components/color-picker/rgba-input.component.ts | 5 +++-- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/ui-ngx/src/app/shared/components/color-picker/hsla-input.component.ts b/ui-ngx/src/app/shared/components/color-picker/hsla-input.component.ts index 7965c48f28..40ec056972 100644 --- a/ui-ngx/src/app/shared/components/color-picker/hsla-input.component.ts +++ b/ui-ngx/src/app/shared/components/color-picker/hsla-input.component.ts @@ -23,7 +23,8 @@ type Channel = 'H' | 'S' | 'L'; @Component({ selector: 'tb-hsla-input', templateUrl: './hsla-input.component.html', - styleUrl: './color-input.base.scss' + styleUrl: './color-input.base.scss', + standalone: false }) export class HslaInputComponent { diff --git a/ui-ngx/src/app/shared/components/color-picker/input-change.directive.ts b/ui-ngx/src/app/shared/components/color-picker/input-change.directive.ts index 0c493a2118..55604df28e 100644 --- a/ui-ngx/src/app/shared/components/color-picker/input-change.directive.ts +++ b/ui-ngx/src/app/shared/components/color-picker/input-change.directive.ts @@ -17,7 +17,8 @@ import { Directive, EventEmitter, HostBinding, HostListener, Input, numberAttribute, Output } from '@angular/core'; @Directive({ - selector: '[inputChange]' + selector: '[inputChange]', + standalone: false }) export class InputChangeDirective { diff --git a/ui-ngx/src/app/shared/components/color-picker/rgba-input.component.ts b/ui-ngx/src/app/shared/components/color-picker/rgba-input.component.ts index 1cf9ea5768..04f2dbc44b 100644 --- a/ui-ngx/src/app/shared/components/color-picker/rgba-input.component.ts +++ b/ui-ngx/src/app/shared/components/color-picker/rgba-input.component.ts @@ -18,12 +18,13 @@ import { Component, EventEmitter, Input, Output } from '@angular/core'; import { Color } from '@iplab/ngx-color-picker'; import { coerceBoolean } from '@shared/decorators/coercion'; -type Channel = 'R' | 'G' | 'B' | 'A'; +type Channel = 'R' | 'G' | 'B'; @Component({ selector: 'tb-rgba-input', templateUrl: './rgba-input.component.html', - styleUrl: './color-input.base.scss' + styleUrl: './color-input.base.scss', + standalone: false }) export class RgbaInputComponent { From 1a7cae27e0618461f94e1e768296d7496c9d47d1 Mon Sep 17 00:00:00 2001 From: Maksym Tsymbarov Date: Tue, 17 Feb 2026 12:48:22 +0200 Subject: [PATCH 5/5] Fix merge errors --- .../@iplab+ngx-color-picker+18.0.1.patch | 26 ------------------- 1 file changed, 26 deletions(-) delete mode 100644 ui-ngx/patches/@iplab+ngx-color-picker+18.0.1.patch diff --git a/ui-ngx/patches/@iplab+ngx-color-picker+18.0.1.patch b/ui-ngx/patches/@iplab+ngx-color-picker+18.0.1.patch deleted file mode 100644 index 64fe082fb8..0000000000 --- a/ui-ngx/patches/@iplab+ngx-color-picker+18.0.1.patch +++ /dev/null @@ -1,26 +0,0 @@ -diff --git a/node_modules/@iplab/ngx-color-picker/esm2022/lib/helpers/color.class.mjs b/node_modules/@iplab/ngx-color-picker/esm2022/lib/helpers/color.class.mjs -index 6c7f8b1..f390b3b 100644 ---- a/node_modules/@iplab/ngx-color-picker/esm2022/lib/helpers/color.class.mjs -+++ b/node_modules/@iplab/ngx-color-picker/esm2022/lib/helpers/color.class.mjs -@@ -173,7 +173,7 @@ export class Color { - const s = (color.saturation / 100) * (l <= 1 ? l : 2 - l); - const value = (l + s) / 2; - const saturation = (2 * s) / (l + s) || 0; -- return new Hsva(hue, saturation, value, color.alpha); -+ return new Hsva(hue, saturation * 100, value * 100, color.alpha); - } - rgbaToHsva(color) { - const red = color.red / 255; -diff --git a/node_modules/@iplab/ngx-color-picker/fesm2022/iplab-ngx-color-picker.mjs b/node_modules/@iplab/ngx-color-picker/fesm2022/iplab-ngx-color-picker.mjs -index a3b270c..9a9b592 100644 ---- a/node_modules/@iplab/ngx-color-picker/fesm2022/iplab-ngx-color-picker.mjs -+++ b/node_modules/@iplab/ngx-color-picker/fesm2022/iplab-ngx-color-picker.mjs -@@ -505,7 +505,7 @@ class Color { - const s = (color.saturation / 100) * (l <= 1 ? l : 2 - l); - const value = (l + s) / 2; - const saturation = (2 * s) / (l + s) || 0; -- return new Hsva(hue, saturation, value, color.alpha); -+ return new Hsva(hue, saturation * 100, value * 100, color.alpha); - } - rgbaToHsva(color) { - const red = color.red / 255;