diff --git a/application/src/main/data/json/system/widget_types/time_series_chart.json b/application/src/main/data/json/system/widget_types/time_series_chart.json index b8cd24a151..7d2fff170f 100644 --- a/application/src/main/data/json/system/widget_types/time_series_chart.json +++ b/application/src/main/data/json/system/widget_types/time_series_chart.json @@ -11,16 +11,16 @@ "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.timeSeriesChartWidget.onInit();\n};\n\nself.onDataUpdated = function() {\n self.ctx.$scope.timeSeriesChartWidget.onDataUpdated();\n}\n\nself.onLatestDataUpdated = function() {\n self.ctx.$scope.timeSeriesChartWidget.onLatestDataUpdated();\n}\n\nself.typeParameters = function() {\n return {\n previewWidth: '80%',\n embedTitlePanel: true,\n hasAdditionalLatestDataKeys: true,\n defaultDataKeysFunction: function() {\n return [{ name: 'temperature', label: 'Temperature', type: 'timeseries', units: '°C', decimals: 0 }];\n }\n };\n}\n", + "controllerScript": "self.onInit = function() {\n self.ctx.$scope.timeSeriesChartWidget.onInit();\n};\n\nself.onDataUpdated = function() {\n self.ctx.$scope.timeSeriesChartWidget.onDataUpdated();\n}\n\nself.onLatestDataUpdated = function() {\n self.ctx.$scope.timeSeriesChartWidget.onLatestDataUpdated();\n}\n\nself.typeParameters = function() {\n return {\n previewWidth: '80%',\n embedTitlePanel: true,\n hasAdditionalLatestDataKeys: true,\n dataKeySettingsFunction: TbTimeSeriesChart.dataKeySettings(),\n defaultDataKeysFunction: function() {\n return [{ name: 'temperature', label: 'Temperature', type: 'timeseries', units: '°C', decimals: 0 }];\n }\n };\n}\n", "settingsSchema": "{}", "dataKeySettingsSchema": "{}", "latestDataKeySettingsSchema": "{}", "settingsDirective": "", - "dataKeySettingsDirective": "", + "dataKeySettingsDirective": "tb-time-series-chart-key-settings", "latestDataKeySettingsDirective": "", - "hasBasicMode": false, - "basicModeDirective": "", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"First\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Second\",\"color\":\"#FFC107\",\"settings\":{},\"_hash\":0.5534217244004682,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]},\"latestDataKeys\":null}],\"timewindow\":{\"hideInterval\":false,\"hideLastInterval\":false,\"hideQuickInterval\":false,\"hideAggregation\":false,\"hideAggInterval\":false,\"hideTimezone\":false,\"selectedTab\":0,\"realtime\":{\"realtimeType\":0,\"timewindowMs\":60000,\"quickInterval\":\"CURRENT_DAY\",\"interval\":1000},\"aggregation\":{\"type\":\"AVG\",\"limit\":25000},\"timezone\":null},\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"showLegend\":true,\"legendConfig\":{\"position\":\"top\",\"sortDataKeys\":false,\"showMin\":false,\"showMax\":false,\"showAvg\":true,\"showTotal\":false,\"showLatest\":false}},\"title\":\"Time series chart\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":false,\"titleIcon\":\"thermostat\",\"iconColor\":\"#1F6BDD\",\"useDashboardTimewindow\":false,\"displayTimewindow\":true,\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"units\":\"\",\"decimals\":null,\"noDataDisplayMessage\":\"\",\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":null,\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true},\"margin\":\"0px\",\"borderRadius\":\"0px\",\"iconSize\":\"0px\"}" + "hasBasicMode": true, + "basicModeDirective": "tb-time-series-chart-basic-config", + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"First\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Second\",\"color\":\"#FFC107\",\"settings\":{\"type\":\"bar\"},\"_hash\":0.5534217244004682,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]},\"latestDataKeys\":null}],\"timewindow\":{\"hideInterval\":false,\"hideLastInterval\":false,\"hideQuickInterval\":false,\"hideAggregation\":false,\"hideAggInterval\":false,\"hideTimezone\":false,\"selectedTab\":0,\"realtime\":{\"realtimeType\":0,\"timewindowMs\":60000,\"quickInterval\":\"CURRENT_DAY\",\"interval\":1000},\"aggregation\":{\"type\":\"AVG\",\"limit\":25000},\"timezone\":null},\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"showLegend\":true,\"legendConfig\":{\"direction\":\"column\",\"position\":\"top\",\"sortDataKeys\":false,\"showMin\":false,\"showMax\":false,\"showAvg\":true,\"showTotal\":false,\"showLatest\":false},\"thresholds\":[],\"dataZoom\":true,\"stack\":false,\"yAxis\":{\"show\":true,\"label\":\"\",\"labelFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"600\",\"lineHeight\":\"1\"},\"labelColor\":\"rgba(0, 0, 0, 0.54)\",\"position\":\"left\",\"showTickLabels\":true,\"tickLabelFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"1\"},\"tickLabelColor\":\"rgba(0, 0, 0, 0.54)\",\"showTicks\":true,\"ticksColor\":\"rgba(0, 0, 0, 0.54)\",\"showLine\":true,\"lineColor\":\"rgba(0, 0, 0, 0.54)\",\"showSplitLines\":true,\"splitLinesColor\":\"rgba(0, 0, 0, 0.12)\"},\"xAxis\":{\"show\":true,\"label\":\"\",\"labelFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"600\",\"lineHeight\":\"1\"},\"labelColor\":\"rgba(0, 0, 0, 0.54)\",\"position\":\"bottom\",\"showTickLabels\":true,\"tickLabelFont\":{\"family\":\"Roboto\",\"size\":10,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"1\"},\"tickLabelColor\":\"rgba(0, 0, 0, 0.54)\",\"showTicks\":true,\"ticksColor\":\"rgba(0, 0, 0, 0.54)\",\"showLine\":true,\"lineColor\":\"rgba(0, 0, 0, 0.54)\",\"showSplitLines\":true,\"splitLinesColor\":\"rgba(0, 0, 0, 0.12)\"},\"legendLabelFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"16px\"},\"legendLabelColor\":\"rgba(0, 0, 0, 0.76)\",\"showTooltip\":true,\"tooltipTrigger\":\"axis\",\"tooltipValueFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"16px\"},\"tooltipValueColor\":\"rgba(0, 0, 0, 0.76)\",\"tooltipShowDate\":true,\"tooltipDateFormat\":{\"format\":\"dd MMM yyyy HH:mm:ss\",\"lastUpdateAgo\":false,\"custom\":false},\"tooltipDateFont\":{\"family\":\"Roboto\",\"size\":11,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"16px\"},\"tooltipDateColor\":\"rgba(0, 0, 0, 0.76)\",\"tooltipDateInterval\":true,\"tooltipBackgroundColor\":\"rgba(255, 255, 255, 0.76)\",\"tooltipBackgroundBlur\":4,\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Time series chart\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":false,\"titleIcon\":\"thermostat\",\"iconColor\":\"#1F6BDD\",\"useDashboardTimewindow\":false,\"displayTimewindow\":true,\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"units\":\"\",\"decimals\":null,\"noDataDisplayMessage\":\"\",\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":null,\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true},\"margin\":\"0px\",\"borderRadius\":\"0px\",\"iconSize\":\"0px\"}" }, "tags": [ "chart", diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/add-widget-dialog.component.ts b/ui-ngx/src/app/modules/home/components/dashboard-page/add-widget-dialog.component.ts index 0d02fc956f..2089c03d5d 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/add-widget-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/add-widget-dialog.component.ts @@ -29,6 +29,7 @@ import { WidgetConfigComponentData, WidgetInfo } from '@home/models/widget-compo import { isDefined, isDefinedAndNotNull, isString } from '@core/utils'; import { TranslateService } from '@ngx-translate/core'; import { WidgetConfigComponent } from '@home/components/widget/widget-config.component'; +import { DataKeySettingsFunction } from '@home/components/widget/config/data-keys.component.models'; export interface AddWidgetDialogData { dashboard: Dashboard; @@ -97,6 +98,7 @@ export class AddWidgetDialogComponent extends DialogComponent + + + + + + + +
+
TODO: Thresholds
+
+
+
widget-config.appearance
+
+ + {{ 'widget-config.title' | translate }} + +
+ + + + + + + +
+
+
+ + {{ 'widget-config.card-icon' | translate }} + +
+ + + + + + + + +
+
+
+
+
widgets.time-series-chart.chart
+
+ + {{ 'widgets.time-series-chart.data-zoom' | translate }} + +
+
+ +
+ {{ 'widgets.time-series-chart.stack-mode' | translate }} +
+
+
+
+
+
widgets.time-series-chart.axes
+
+ TODO: Y Axis +
+
+ TODO: X Axis +
+
+
+ + + + + {{ 'widget-config.legend' | translate }} + + + + +
+
{{ 'legend.label' | translate }}
+
+ + + + +
+
+ + +
+
+
+
+ + + + + {{ 'widget-config.tooltip' | translate }} + + + + +
+
{{ 'tooltip.trigger' | translate }}
+ + {{ 'tooltip.trigger-point' | translate }} + {{ 'tooltip.trigger-axis' | translate }} + +
+
+
{{ 'tooltip.value' | translate }}
+
+ + + + +
+
+
+ + {{ 'tooltip.date' | translate }} + +
+ + + + + +
+
+
+ +
+ {{ 'tooltip.show-date-time-interval' | translate }} +
+
+
+
+
{{ 'tooltip.background-color' | translate }}
+ + +
+
+
{{ 'tooltip.background-blur' | translate }}
+ + +
px
+
+
+
+
+
+
+
widget-config.card-appearance
+
+
{{ 'widgets.background.background' | translate }}
+ + +
+
+
widget-config.show-card-buttons
+ + {{ 'fullscreen.fullscreen' | translate }} + +
+
+
{{ 'widget-config.card-border-radius' | translate }}
+ + + +
+
+ + +
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 new file mode 100644 index 0000000000..4387e5d52b --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/time-series-chart-basic-config.component.ts @@ -0,0 +1,314 @@ +/// +/// 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, Injector } from '@angular/core'; +import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { BasicWidgetConfigComponent } from '@home/components/widget/config/widget-config.component.models'; +import { WidgetConfigComponentData } from '@home/models/widget-component.models'; +import { + DataKey, + Datasource, + legendPositions, + legendPositionTranslationMap, + WidgetConfig, +} from '@shared/models/widget.models'; +import { WidgetConfigComponent } from '@home/components/widget/widget-config.component'; +import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; +import { + getTimewindowConfig, + setTimewindowConfig +} from '@home/components/widget/config/timewindow-config-panel.component'; +import { formatValue, isUndefined, mergeDeep } from '@core/utils'; +import { + cssSizeToStrSize, + DateFormatProcessor, + DateFormatSettings, + resolveCssSize +} from '@shared/models/widget-settings.models'; +import { + timeSeriesChartWidgetDefaultSettings, + TimeSeriesChartWidgetSettings +} from '@home/components/widget/lib/chart/time-series-chart-widget.models'; +import { ValueType } from '@shared/models/constants'; +import { EChartsTooltipTrigger } from '@home/components/widget/lib/chart/echarts-widget.models'; + +@Component({ + selector: 'tb-time-series-chart-basic-config', + templateUrl: './time-series-chart-basic-config.component.html', + styleUrls: ['../basic-config.scss'] +}) +export class TimeSeriesChartBasicConfigComponent extends BasicWidgetConfigComponent { + + public get datasource(): Datasource { + const datasources: Datasource[] = this.timeSeriesChartWidgetConfigForm.get('datasources').value; + if (datasources && datasources.length) { + return datasources[0]; + } else { + return null; + } + } + + EChartsTooltipTrigger = EChartsTooltipTrigger; + + legendPositions = legendPositions; + + legendPositionTranslationMap = legendPositionTranslationMap; + + timeSeriesChartWidgetConfigForm: UntypedFormGroup; + + tooltipValuePreviewFn = this._tooltipValuePreviewFn.bind(this); + + tooltipDatePreviewFn = this._tooltipDatePreviewFn.bind(this); + + constructor(protected store: Store, + protected widgetConfigComponent: WidgetConfigComponent, + private $injector: Injector, + private fb: UntypedFormBuilder) { + super(store, widgetConfigComponent); + } + + protected configForm(): UntypedFormGroup { + return this.timeSeriesChartWidgetConfigForm; + } + + protected defaultDataKeys(configData: WidgetConfigComponentData): DataKey[] { + return [{ name: 'temperature', label: 'Temperature', type: DataKeyType.timeseries, units: '°C', decimals: 0 }]; + } + + protected onConfigSet(configData: WidgetConfigComponentData) { + const settings: TimeSeriesChartWidgetSettings = mergeDeep({} as TimeSeriesChartWidgetSettings, + timeSeriesChartWidgetDefaultSettings, configData.config.settings as TimeSeriesChartWidgetSettings); + const iconSize = resolveCssSize(configData.config.iconSize); + this.timeSeriesChartWidgetConfigForm = this.fb.group({ + timewindowConfig: [getTimewindowConfig(configData.config), []], + datasources: [configData.config.datasources, []], + series: [this.getSeries(configData.config.datasources), []], + thresholds: [settings.thresholds, []], + + showTitle: [configData.config.showTitle, []], + title: [configData.config.title, []], + titleFont: [configData.config.titleFont, []], + titleColor: [configData.config.titleColor, []], + + showIcon: [configData.config.showTitleIcon, []], + iconSize: [iconSize[0], [Validators.min(0)]], + iconSizeUnit: [iconSize[1], []], + icon: [configData.config.titleIcon, []], + iconColor: [configData.config.iconColor, []], + + dataZoom: [settings.dataZoom, []], + stack: [settings.stack, []], + + yAxis: [settings.yAxis, []], + xAxis: [settings.xAxis, []], + + showLegend: [settings.showLegend, []], + legendLabelFont: [settings.legendLabelFont, []], + legendLabelColor: [settings.legendLabelColor, []], + legendConfig: [settings.legendConfig, []], + + showTooltip: [settings.showTooltip, []], + tooltipTrigger: [settings.tooltipTrigger, []], + tooltipValueFont: [settings.tooltipValueFont, []], + tooltipValueColor: [settings.tooltipValueColor, []], + tooltipShowDate: [settings.tooltipShowDate, []], + tooltipDateFormat: [settings.tooltipDateFormat, []], + tooltipDateFont: [settings.tooltipDateFont, []], + tooltipDateColor: [settings.tooltipDateColor, []], + tooltipDateInterval: [settings.tooltipDateInterval, []], + + tooltipBackgroundColor: [settings.tooltipBackgroundColor, []], + tooltipBackgroundBlur: [settings.tooltipBackgroundBlur, []], + + background: [settings.background, []], + + cardButtons: [this.getCardButtons(configData.config), []], + borderRadius: [configData.config.borderRadius, []], + + actions: [configData.config.actions || {}, []] + }); + } + + protected prepareOutputConfig(config: any): WidgetConfigComponentData { + setTimewindowConfig(this.widgetConfig.config, config.timewindowConfig); + this.widgetConfig.config.datasources = config.datasources; + this.setSeries(config.series, this.widgetConfig.config.datasources); + + this.widgetConfig.config.showTitle = config.showTitle; + this.widgetConfig.config.title = config.title; + this.widgetConfig.config.titleFont = config.titleFont; + this.widgetConfig.config.titleColor = config.titleColor; + + this.widgetConfig.config.showTitleIcon = config.showIcon; + this.widgetConfig.config.iconSize = cssSizeToStrSize(config.iconSize, config.iconSizeUnit); + this.widgetConfig.config.titleIcon = config.icon; + this.widgetConfig.config.iconColor = config.iconColor; + + this.widgetConfig.config.settings = this.widgetConfig.config.settings || {}; + + this.widgetConfig.config.settings.thresholds = config.thresholds; + + 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.xAxis = config.xAxis; + + this.widgetConfig.config.settings.showLegend = config.showLegend; + this.widgetConfig.config.settings.legendLabelFont = config.legendLabelFont; + this.widgetConfig.config.settings.legendLabelColor = config.legendLabelColor; + this.widgetConfig.config.settings.legendConfig = config.legendConfig; + + this.widgetConfig.config.settings.showTooltip = config.showTooltip; + this.widgetConfig.config.settings.tooltipTrigger = config.tooltipTrigger; + this.widgetConfig.config.settings.tooltipValueFont = config.tooltipValueFont; + this.widgetConfig.config.settings.tooltipValueColor = config.tooltipValueColor; + this.widgetConfig.config.settings.tooltipShowDate = config.tooltipShowDate; + this.widgetConfig.config.settings.tooltipDateFormat = config.tooltipDateFormat; + this.widgetConfig.config.settings.tooltipDateFont = config.tooltipDateFont; + this.widgetConfig.config.settings.tooltipDateColor = config.tooltipDateColor; + this.widgetConfig.config.settings.tooltipDateInterval = config.tooltipDateInterval; + this.widgetConfig.config.settings.tooltipBackgroundColor = config.tooltipBackgroundColor; + this.widgetConfig.config.settings.tooltipBackgroundBlur = config.tooltipBackgroundBlur; + + this.widgetConfig.config.settings.background = config.background; + + this.setCardButtons(config.cardButtons, this.widgetConfig.config); + this.widgetConfig.config.borderRadius = config.borderRadius; + + this.widgetConfig.config.actions = config.actions; + return this.widgetConfig; + } + + protected validatorTriggers(): string[] { + return ['showTitle', 'showIcon', 'showLegend', 'showTooltip', 'tooltipShowDate']; + } + + protected updateValidators(emitEvent: boolean, trigger?: string) { + const showTitle: boolean = this.timeSeriesChartWidgetConfigForm.get('showTitle').value; + const showIcon: boolean = this.timeSeriesChartWidgetConfigForm.get('showIcon').value; + const showLegend: boolean = this.timeSeriesChartWidgetConfigForm.get('showLegend').value; + const showTooltip: boolean = this.timeSeriesChartWidgetConfigForm.get('showTooltip').value; + const tooltipShowDate: boolean = this.timeSeriesChartWidgetConfigForm.get('tooltipShowDate').value; + + if (showTitle) { + this.timeSeriesChartWidgetConfigForm.get('title').enable(); + this.timeSeriesChartWidgetConfigForm.get('titleFont').enable(); + this.timeSeriesChartWidgetConfigForm.get('titleColor').enable(); + this.timeSeriesChartWidgetConfigForm.get('showIcon').enable({emitEvent: false}); + if (showIcon) { + this.timeSeriesChartWidgetConfigForm.get('iconSize').enable(); + this.timeSeriesChartWidgetConfigForm.get('iconSizeUnit').enable(); + this.timeSeriesChartWidgetConfigForm.get('icon').enable(); + this.timeSeriesChartWidgetConfigForm.get('iconColor').enable(); + } else { + this.timeSeriesChartWidgetConfigForm.get('iconSize').disable(); + this.timeSeriesChartWidgetConfigForm.get('iconSizeUnit').disable(); + this.timeSeriesChartWidgetConfigForm.get('icon').disable(); + this.timeSeriesChartWidgetConfigForm.get('iconColor').disable(); + } + } else { + this.timeSeriesChartWidgetConfigForm.get('title').disable(); + this.timeSeriesChartWidgetConfigForm.get('titleFont').disable(); + this.timeSeriesChartWidgetConfigForm.get('titleColor').disable(); + this.timeSeriesChartWidgetConfigForm.get('showIcon').disable({emitEvent: false}); + this.timeSeriesChartWidgetConfigForm.get('iconSize').disable(); + this.timeSeriesChartWidgetConfigForm.get('iconSizeUnit').disable(); + this.timeSeriesChartWidgetConfigForm.get('icon').disable(); + this.timeSeriesChartWidgetConfigForm.get('iconColor').disable(); + } + + if (showLegend) { + this.timeSeriesChartWidgetConfigForm.get('legendLabelFont').enable(); + this.timeSeriesChartWidgetConfigForm.get('legendLabelColor').enable(); + this.timeSeriesChartWidgetConfigForm.get('legendConfig').enable(); + } else { + this.timeSeriesChartWidgetConfigForm.get('legendLabelFont').disable(); + this.timeSeriesChartWidgetConfigForm.get('legendLabelColor').disable(); + this.timeSeriesChartWidgetConfigForm.get('legendConfig').disable(); + } + + if (showTooltip) { + this.timeSeriesChartWidgetConfigForm.get('tooltipTrigger').enable(); + this.timeSeriesChartWidgetConfigForm.get('tooltipValueFont').enable(); + this.timeSeriesChartWidgetConfigForm.get('tooltipValueColor').enable(); + this.timeSeriesChartWidgetConfigForm.get('tooltipShowDate').enable({emitEvent: false}); + this.timeSeriesChartWidgetConfigForm.get('tooltipBackgroundColor').enable(); + this.timeSeriesChartWidgetConfigForm.get('tooltipBackgroundBlur').enable(); + if (tooltipShowDate) { + this.timeSeriesChartWidgetConfigForm.get('tooltipDateFormat').enable(); + this.timeSeriesChartWidgetConfigForm.get('tooltipDateFont').enable(); + this.timeSeriesChartWidgetConfigForm.get('tooltipDateColor').enable(); + this.timeSeriesChartWidgetConfigForm.get('tooltipDateInterval').enable(); + } else { + this.timeSeriesChartWidgetConfigForm.get('tooltipDateFormat').disable(); + this.timeSeriesChartWidgetConfigForm.get('tooltipDateFont').disable(); + this.timeSeriesChartWidgetConfigForm.get('tooltipDateColor').disable(); + this.timeSeriesChartWidgetConfigForm.get('tooltipDateInterval').disable(); + } + } else { + this.timeSeriesChartWidgetConfigForm.get('tooltipValueFont').disable(); + this.timeSeriesChartWidgetConfigForm.get('tooltipValueColor').disable(); + this.timeSeriesChartWidgetConfigForm.get('tooltipShowDate').disable({emitEvent: false}); + this.timeSeriesChartWidgetConfigForm.get('tooltipDateFormat').disable(); + this.timeSeriesChartWidgetConfigForm.get('tooltipDateFont').disable(); + this.timeSeriesChartWidgetConfigForm.get('tooltipDateColor').disable(); + this.timeSeriesChartWidgetConfigForm.get('tooltipDateInterval').disable(); + this.timeSeriesChartWidgetConfigForm.get('tooltipBackgroundColor').disable(); + this.timeSeriesChartWidgetConfigForm.get('tooltipBackgroundBlur').disable(); + } + } + + private getSeries(datasources?: Datasource[]): DataKey[] { + if (datasources && datasources.length) { + return datasources[0].dataKeys || []; + } + return []; + } + + private setSeries(series: DataKey[], datasources?: Datasource[]) { + if (datasources && datasources.length) { + datasources[0].dataKeys = series; + } + } + + private getCardButtons(config: WidgetConfig): string[] { + const buttons: string[] = []; + if (isUndefined(config.enableFullscreen) || config.enableFullscreen) { + buttons.push('fullscreen'); + } + return buttons; + } + + private setCardButtons(buttons: string[], config: WidgetConfig) { + config.enableFullscreen = buttons.includes('fullscreen'); + } + + private _tooltipValuePreviewFn(): string { + return formatValue(22, 0, '°C', false); + } + + private _tooltipDatePreviewFn(): string { + const dateFormat: DateFormatSettings = this.timeSeriesChartWidgetConfigForm.get('tooltipDateFormat').value; + const processor = DateFormatProcessor.fromSettings(this.$injector, dateFormat); + processor.update(Date.now()); + return processor.formatted; + } + + protected readonly ValueType = ValueType; +} 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 8b233f0b09..1c4767b963 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 @@ -158,6 +158,21 @@
widget-config.decimals-suffix
+
+ + + + + {{ timeSeriesChartSeriesTypeIcons.get(keyRowFormGroup.get('timeSeriesType').value) }} + + + + {{ timeSeriesChartSeriesTypeIcons.get(type) }} + {{ timeSeriesChartSeriesTypeTranslations.get(type) | translate }} + + + +
1; } @@ -256,7 +264,8 @@ export class DataKeysPanelComponent implements ControlValueAccessor, OnInit, OnC } addKey() { - const dataKey = this.callbacks.generateDataKey('', null, this.datakeySettingsSchema); + const dataKey = this.callbacks.generateDataKey('', null, this.datakeySettingsSchema, + false, this.dataKeySettingsFunction); dataKey.label = ''; dataKey.decimals = 0; if (this.hasAdditionalLatestDataKeys) { diff --git a/ui-ngx/src/app/modules/home/components/widget/config/data-key-config.component.html b/ui-ngx/src/app/modules/home/components/widget/config/data-key-config.component.html index fe6c347649..37c85e173d 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/data-key-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/data-key-config.component.html @@ -197,6 +197,7 @@ [dashboard]="dashboard" [aliasController]="aliasController" [widget]="widget" + [widgetConfig]="widgetConfig" formControlName="settings">
diff --git a/ui-ngx/src/app/modules/home/components/widget/config/data-key-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/data-key-config.component.ts index b120a60809..9ebe8821cf 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/data-key-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/data-key-config.component.ts @@ -40,7 +40,7 @@ import { UtilsService } from '@core/services/utils.service'; import { TranslateService } from '@ngx-translate/core'; import { MatDialog } from '@angular/material/dialog'; import { EntityService } from '@core/http/entity.service'; -import { DataKeysCallbacks } from '@home/components/widget/config/data-keys.component.models'; +import { DataKeysCallbacks, DataKeySettingsFunction } from '@home/components/widget/config/data-keys.component.models'; import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; import { Observable, of } from 'rxjs'; import { map, mergeMap, publishReplay, refCount, tap } from 'rxjs/operators'; @@ -51,8 +51,10 @@ import { WidgetService } from '@core/http/widget.service'; import { Dashboard } from '@shared/models/dashboard.models'; import { IAliasController } from '@core/api/widget-api.models'; import { aggregationTranslations, AggregationType, ComparisonDuration } from '@shared/models/time/time.models'; -import { genNextLabel } from '@core/utils'; +import { genNextLabel, isDefinedAndNotNull } from '@core/utils'; import { coerceBoolean } from '@shared/decorators/coercion'; +import { WidgetConfigComponentData } from '@home/models/widget-component.models'; +import { WidgetComponentService } from '@home/components/widget/widget-component.service'; @Component({ selector: 'tb-data-key-config', @@ -155,6 +157,8 @@ export class DataKeyConfigComponent extends PageComponent implements OnInit, Con modelValue: DataKey; + widgetConfig: WidgetConfigComponentData; + private propagateChange = null; public dataKeyFormGroup: UntypedFormGroup; @@ -180,12 +184,31 @@ export class DataKeyConfigComponent extends PageComponent implements OnInit, Con private dialog: MatDialog, private translate: TranslateService, private widgetService: WidgetService, + private widgetComponentService: WidgetComponentService, private fb: UntypedFormBuilder) { super(store); this.functionScopeVariables = this.widgetService.getWidgetScopeVariables(); } ngOnInit(): void { + + const widgetInfo = this.widgetComponentService.getInstantWidgetInfo(this.widget); + const typeParameters = widgetInfo.typeParameters; + const dataKeySettingsFunction: DataKeySettingsFunction = typeParameters?.dataKeySettingsFunction; + + this.widgetConfig = { + widgetName: widgetInfo.widgetName, + config: this.widget.config, + widgetType: this.widget.type, + typeParameters, + dataKeySettingsFunction, + settingsDirective: widgetInfo.settingsDirective, + dataKeySettingsDirective: widgetInfo.dataKeySettingsDirective, + latestDataKeySettingsDirective: widgetInfo.latestDataKeySettingsDirective, + hasBasicMode: isDefinedAndNotNull(widgetInfo.hasBasicMode) ? widgetInfo.hasBasicMode : false, + basicModeDirective: widgetInfo.basicModeDirective + } as WidgetConfigComponentData; + this.alarmKeys = []; for (const name of Object.keys(alarmFields)) { this.alarmKeys.push({ diff --git a/ui-ngx/src/app/modules/home/components/widget/config/data-keys.component.models.ts b/ui-ngx/src/app/modules/home/components/widget/config/data-keys.component.models.ts index 893631f57b..b0e80973f0 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/data-keys.component.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/data-keys.component.models.ts @@ -18,8 +18,11 @@ import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; import { DataKey, JsonSettingsSchema } from '@shared/models/widget.models'; import { Observable } from 'rxjs'; +export type DataKeySettingsFunction = (key: DataKey, isLatestDataKey: boolean) => any; + export interface DataKeysCallbacks { - generateDataKey: (chip: any, type: DataKeyType, datakeySettingsSchema: JsonSettingsSchema) => DataKey; + generateDataKey: (chip: any, type: DataKeyType, datakeySettingsSchema: JsonSettingsSchema, + isLatestDataKey: boolean, dataKeySettingsFunction: DataKeySettingsFunction) => DataKey; fetchEntityKeys: (entityAliasId: string, types: Array) => Observable>; fetchEntityKeysForDevice: (deviceId: string, types: Array) => Observable>; } diff --git a/ui-ngx/src/app/modules/home/components/widget/config/data-keys.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/data-keys.component.ts index 800c831b67..8c2df629de 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/data-keys.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/data-keys.component.ts @@ -52,7 +52,7 @@ import { coerceBooleanProperty } from '@angular/cdk/coercion'; import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; import { DataKey, DatasourceType, JsonSettingsSchema, Widget, widgetType } from '@shared/models/widget.models'; import { IAliasController } from '@core/api/widget-api.models'; -import { DataKeysCallbacks } from './data-keys.component.models'; +import { DataKeysCallbacks, DataKeySettingsFunction } from './data-keys.component.models'; import { alarmFields } from '@shared/models/alarm.models'; import { UtilsService } from '@core/services/utils.service'; import { ErrorStateMatcher } from '@angular/material/core'; @@ -139,6 +139,10 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, OnChange @Input() optDataKeys: boolean; + @Input() + @coerceBoolean() + latestDataKeys = false; + @Input() @coerceBoolean() simpleDataKeysLabel = false; @@ -149,6 +153,9 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, OnChange @Input() datakeySettingsSchema: JsonSettingsSchema; + @Input() + datakeySettingsFunction: DataKeySettingsFunction; + @Input() dataKeySettingsDirective: string; @@ -361,7 +368,8 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, OnChange if (this.widgetType === widgetType.alarm) { this.keys = this.utils.getDefaultAlarmDataKeys(); } else if (this.isCountDatasource) { - this.keys = [this.callbacks.generateDataKey('count', DataKeyType.count, this.datakeySettingsSchema)]; + this.keys = [this.callbacks.generateDataKey('count', DataKeyType.count, this.datakeySettingsSchema, + this.latestDataKeys, this.datakeySettingsFunction)]; } else { this.keys = []; } @@ -447,7 +455,8 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, OnChange } private addFromChipValue(chip: DataKey) { - const key = this.callbacks.generateDataKey(chip.name, chip.type, this.datakeySettingsSchema); + const key = this.callbacks.generateDataKey(chip.name, chip.type, this.datakeySettingsSchema, this.latestDataKeys, + this.datakeySettingsFunction); this.addKey(key); } diff --git a/ui-ngx/src/app/modules/home/components/widget/config/datasource.component.html b/ui-ngx/src/app/modules/home/components/widget/config/datasource.component.html index b72f9f0e5d..13a52a295b 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/datasource.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/datasource.component.html @@ -72,6 +72,7 @@ [aliasController]="aliasController" [datakeySettingsSchema]="dataKeySettingsSchema" [dataKeySettingsDirective]="dataKeySettingsDirective" + [datakeySettingsFunction]="dataKeySettingsFunction" [dashboard]="dashboard" [widget]="widget" [callbacks]="dataKeysCallbacks" @@ -82,10 +83,12 @@ { - const dataKey = this.constructDataKey(configData, key); + const dataKey = this.constructDataKey(configData, key, false); dataKeys.push(dataKey); }); } if (latestKeys && latestKeys.length) { latestDataKeys.length = 0; latestKeys.forEach(key => { - const dataKey = this.constructDataKey(configData, key); + const dataKey = this.constructDataKey(configData, key, true); latestDataKeys.push(dataKey); }); } } - protected constructDataKey(configData: WidgetConfigComponentData, key: DataKey): DataKey { + protected constructDataKey(configData: WidgetConfigComponentData, key: DataKey, isLatestKey: boolean): DataKey { const dataKey = - this.widgetConfigComponent.widgetConfigCallbacks.generateDataKey(key.name, key.type, configData.dataKeySettingsSchema); + this.widgetConfigComponent.widgetConfigCallbacks.generateDataKey(key.name, key.type, + configData.dataKeySettingsSchema, isLatestKey, configData.dataKeySettingsFunction); if (key.label) { dataKey.label = key.label; } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/cards/aggregated-value-card-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/cards/aggregated-value-card-widget.component.ts index ae7c3c5eee..3edce958f0 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/cards/aggregated-value-card-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/cards/aggregated-value-card-widget.component.ts @@ -148,13 +148,14 @@ export class AggregatedValueCardWidgetComponent implements OnInit, AfterViewInit if (this.ctx.defaultSubscription.firstDatasource?.dataKeys?.length) { this.lineChartDataKey = this.ctx.defaultSubscription.firstDatasource?.dataKeys[0]; this.lineChartDataKey.settings = { - showPointLabel: false, type: TimeSeriesChartSeriesType.line, lineSettings: { - smooth: false, showLine: true, + step: false, + smooth: false, lineWidth: 2, - showPoints: false + showPoints: false, + showPointLabel: false } } as TimeSeriesChartKeySettings; } 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 b59098de17..f5a305a247 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 @@ -148,13 +148,14 @@ export class ValueChartCardWidgetComponent implements OnInit, AfterViewInit, OnD if (this.ctx.defaultSubscription.firstDatasource?.dataKeys?.length) { this.lineChartDataKey = this.ctx.defaultSubscription.firstDatasource?.dataKeys[0]; this.lineChartDataKey.settings = { - showPointLabel: false, type: TimeSeriesChartSeriesType.line, lineSettings: { - smooth: true, showLine: true, + step: false, + smooth: true, lineWidth: 2, - showPoints: false + showPoints: false, + showPointLabel: false } } as TimeSeriesChartKeySettings; } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/echarts-widget.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/echarts-widget.models.ts index 7564bd9cab..06e8e98fd0 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/echarts-widget.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/echarts-widget.models.ts @@ -44,7 +44,7 @@ import { } from 'echarts/charts'; import { LabelLayout } from 'echarts/features'; import { CanvasRenderer, SVGRenderer } from 'echarts/renderers'; -import { DataEntry, DataKey, DataSet } from '@shared/models/widget.models'; +import { DataEntry, DataKey, DataSet, LegendDirection } from '@shared/models/widget.models'; import { calculateAggIntervalWithWidgetTimeWindow, IntervalMath, @@ -304,6 +304,13 @@ export enum EChartsTooltipTrigger { axis = 'axis' } +export const tooltipTriggerTranslationMap = new Map( + [ + [ EChartsTooltipTrigger.point, 'tooltip.trigger-point' ], + [ EChartsTooltipTrigger.axis, 'tooltip.trigger-axis' ] + ] +); + export interface EChartsTooltipWidgetSettings { showTooltip: boolean; tooltipTrigger?: EChartsTooltipTrigger; 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 ef8b432e46..3965f69000 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 @@ -40,11 +40,42 @@ import { import { DataKey } from '@shared/models/widget.models'; import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; import { TbColorScheme } from '@shared/models/color.models'; +import { DoughnutLayout } from '@home/components/widget/lib/chart/doughnut-widget.models'; -export enum PointLabelPosition { - top = 'top', - bottom = 'bottom' -} +const timeSeriesChartColorScheme: TbColorScheme = { + 'threshold.line': { + light: 'rgba(0, 0, 0, 0.76)', + dark: '#eee' + }, + 'threshold.label': { + light: 'rgba(0, 0, 0, 0.76)', + dark: '#eee' + }, + 'axis.line': { + light: 'rgba(0, 0, 0, 0.54)', + dark: '#B9B8CE' + }, + 'axis.label': { + light: 'rgba(0, 0, 0, 0.54)', + dark: '#B9B8CE' + }, + 'axis.ticks': { + light: 'rgba(0, 0, 0, 0.54)', + dark: '#B9B8CE' + }, + 'axis.tickLabel': { + light: 'rgba(0, 0, 0, 0.54)', + dark: '#B9B8CE' + }, + 'axis.splitLine': { + light: 'rgba(0, 0, 0, 0.12)', + dark: '#484753' + }, + 'series.label': { + light: 'rgba(0, 0, 0, 0.76)', + dark: '#eee' + } +}; export enum AxisPosition { left = 'left', @@ -86,19 +117,63 @@ export enum ThresholdLabelPosition { insideEndBottom = 'insideEndBottom' } +export enum TimeSeriesChartThresholdType { + constant = 'constant', + latestKey = 'latestKey', + entity = 'entity' +} + +export enum SeriesFillType { + none = 'none', + opacity = 'opacity', + gradient = 'gradient' +} + +export enum SeriesLabelPosition { + top = 'top', + bottom = 'bottom' +} + +export enum LineSeriesStepType { + start = 'start', + middle = 'middle', + end = 'end' +} + +export enum TimeSeriesChartSeriesType { + line = 'line', + bar = 'bar' +} + +export const timeSeriesChartSeriesTypes = Object.keys(TimeSeriesChartSeriesType) as TimeSeriesChartSeriesType[]; + +export const timeSeriesChartSeriesTypeTranslations = new Map( + [ + [TimeSeriesChartSeriesType.line, 'widgets.time-series-chart.series.type-line'], + [TimeSeriesChartSeriesType.bar, 'widgets.time-series-chart.series.type-bar'] + ] +); + +export const timeSeriesChartSeriesTypeIcons = new Map( + [ + [TimeSeriesChartSeriesType.line, 'mdi:chart-line'], + [TimeSeriesChartSeriesType.bar, 'mdi:chart-bar'] + ] +); + export interface TimeSeriesChartAxisSettings { show: boolean; - position: AxisPosition; label?: string; labelFont?: Font; labelColor?: string; - showLine: boolean; - lineColor: string; - showTicks: boolean; - ticksColor: string; + position: AxisPosition; showTickLabels: boolean; tickLabelFont: Font; tickLabelColor: string; + showTicks: boolean; + ticksColor: string; + showLine: boolean; + lineColor: string; showSplitLines: boolean; splitLinesColor: string; } @@ -109,24 +184,19 @@ export interface TimeSeriesChartYAxisSettings extends TimeSeriesChartAxisSetting intervalCalculator?: string; } -export enum TimeSeriesChartThresholdType { - constant = 'constant', - latestKey = 'latestKey', - entity = 'entity' -} - export interface TimeSeriesChartThreshold { type: TimeSeriesChartThresholdType; value?: number; - latestKeyName?: string; + latestKey?: string; + latestKeyType?: DataKeyType.attribute | DataKeyType.timeseries; entityAlias?: string; - entityKeyType?: DataKeyType.attribute | DataKeyType.timeseries; entityKey?: string; + entityKeyType?: DataKeyType.attribute | DataKeyType.timeseries; units?: string; decimals?: number; - lineWidth: number; - lineType: TimeSeriesChartLineType; lineColor: string; + lineType: TimeSeriesChartLineType; + lineWidth: number; startSymbol: TimeSeriesChartShape; startSymbolSize: number; endSymbol: TimeSeriesChartShape; @@ -137,48 +207,13 @@ export interface TimeSeriesChartThreshold { labelColor: string; } -const timeSeriesChartColorScheme: TbColorScheme = { - 'threshold.line': { - light: 'rgba(0, 0, 0, 0.76)', - dark: '#eee' - }, - 'threshold.label': { - light: 'rgba(0, 0, 0, 0.76)', - dark: '#eee' - }, - 'axis.line': { - light: 'rgba(0, 0, 0, 0.54)', - dark: '#B9B8CE' - }, - 'axis.label': { - light: 'rgba(0, 0, 0, 0.54)', - dark: '#B9B8CE' - }, - 'axis.ticks': { - light: 'rgba(0, 0, 0, 0.54)', - dark: '#B9B8CE' - }, - 'axis.tickLabel': { - light: 'rgba(0, 0, 0, 0.54)', - dark: '#B9B8CE' - }, - 'axis.splitLine': { - light: 'rgba(0, 0, 0, 0.12)', - dark: '#484753' - }, - 'series.pointLabel': { - light: 'rgba(0, 0, 0, 0.76)', - dark: '#eee' - } -}; - export const timeSeriesChartThresholdDefaultSettings: TimeSeriesChartThreshold = { type: TimeSeriesChartThresholdType.constant, units: '', decimals: 0, - lineWidth: 1, - lineType: TimeSeriesChartLineType.solid, lineColor: timeSeriesChartColorScheme['threshold.line'].light, + lineType: TimeSeriesChartLineType.solid, + lineWidth: 1, startSymbol: TimeSeriesChartShape.none, startSymbolSize: 5, endSymbol: TimeSeriesChartShape.arrow, @@ -197,22 +232,21 @@ export const timeSeriesChartThresholdDefaultSettings: TimeSeriesChartThreshold = }; export interface TimeSeriesChartSettings extends EChartsTooltipWidgetSettings { + thresholds: TimeSeriesChartThreshold[]; darkMode: boolean; dataZoom: boolean; stack: boolean; - thresholds: TimeSeriesChartThreshold[]; - xAxis: TimeSeriesChartAxisSettings; yAxis: TimeSeriesChartYAxisSettings; + xAxis: TimeSeriesChartAxisSettings; } export const timeSeriesChartDefaultSettings: TimeSeriesChartSettings = { + thresholds: [], darkMode: false, dataZoom: true, stack: false, - thresholds: [], - xAxis: { + yAxis: { show: true, - position: AxisPosition.bottom, label: '', labelFont: { family: 'Roboto', @@ -223,26 +257,26 @@ export const timeSeriesChartDefaultSettings: TimeSeriesChartSettings = { lineHeight: '1' }, labelColor: timeSeriesChartColorScheme['axis.label'].light, - showLine: true, - lineColor: timeSeriesChartColorScheme['axis.line'].light, - showTicks: true, - ticksColor: timeSeriesChartColorScheme['axis.ticks'].light, + position: AxisPosition.left, showTickLabels: true, tickLabelFont: { family: 'Roboto', - size: 10, + size: 12, sizeUnit: 'px', style: 'normal', weight: '400', lineHeight: '1' }, tickLabelColor: timeSeriesChartColorScheme['axis.tickLabel'].light, + showTicks: true, + ticksColor: timeSeriesChartColorScheme['axis.ticks'].light, + showLine: true, + lineColor: timeSeriesChartColorScheme['axis.line'].light, showSplitLines: true, splitLinesColor: timeSeriesChartColorScheme['axis.splitLine'].light }, - yAxis: { + xAxis: { show: true, - position: AxisPosition.left, label: '', labelFont: { family: 'Roboto', @@ -253,20 +287,21 @@ export const timeSeriesChartDefaultSettings: TimeSeriesChartSettings = { lineHeight: '1' }, labelColor: timeSeriesChartColorScheme['axis.label'].light, - showLine: true, - lineColor: timeSeriesChartColorScheme['axis.line'].light, - showTicks: true, - ticksColor: timeSeriesChartColorScheme['axis.ticks'].light, + position: AxisPosition.bottom, showTickLabels: true, tickLabelFont: { family: 'Roboto', - size: 12, + size: 10, sizeUnit: 'px', style: 'normal', weight: '400', lineHeight: '1' }, tickLabelColor: timeSeriesChartColorScheme['axis.tickLabel'].light, + showTicks: true, + ticksColor: timeSeriesChartColorScheme['axis.ticks'].light, + showLine: true, + lineColor: timeSeriesChartColorScheme['axis.line'].light, showSplitLines: true, splitLinesColor: timeSeriesChartColorScheme['axis.splitLine'].light }, @@ -282,7 +317,6 @@ export const timeSeriesChartDefaultSettings: TimeSeriesChartSettings = { }, tooltipValueColor: 'rgba(0, 0, 0, 0.76)', tooltipShowDate: true, - tooltipDateInterval: true, tooltipDateFormat: simpleDateFormat('dd MMM yyyy HH:mm:ss'), tooltipDateFont: { family: 'Roboto', @@ -293,16 +327,11 @@ export const timeSeriesChartDefaultSettings: TimeSeriesChartSettings = { lineHeight: '16px' }, tooltipDateColor: 'rgba(0, 0, 0, 0.76)', + tooltipDateInterval: true, tooltipBackgroundColor: 'rgba(255, 255, 255, 0.76)', tooltipBackgroundBlur: 4 }; -export enum SeriesFillType { - none = 'none', - opacity = 'opacity', - gradient = 'gradient' -} - export interface SeriesFillSettings { type: SeriesFillType; opacity: number; @@ -313,36 +342,36 @@ export interface SeriesFillSettings { } export interface LineSeriesSettings { - step: false | 'start' | 'end' | 'middle'; - smooth: boolean; showLine: boolean; - lineWidth: number; + step: boolean; + stepType: LineSeriesStepType; + smooth: boolean; lineType: TimeSeriesChartLineType; - fillAreaSettings: SeriesFillSettings; + lineWidth: number; showPoints: boolean; + showPointLabel: boolean; + pointLabelPosition: SeriesLabelPosition; + pointLabelFont: Font; + pointLabelColor: string; pointShape: TimeSeriesChartShape; pointSize: number; + fillAreaSettings: SeriesFillSettings; } export interface BarSeriesSettings { showBorder: boolean; borderWidth: number; borderRadius: number; + showLabel: boolean; + labelPosition: SeriesLabelPosition; + labelFont: Font; + labelColor: string; backgroundSettings: SeriesFillSettings; } -export enum TimeSeriesChartSeriesType { - line = 'line', - bar = 'bar' -} - export interface TimeSeriesChartKeySettings { - dataHiddenByDefault: boolean; showInLegend: boolean; - showPointLabel: boolean; - pointLabelPosition: PointLabelPosition; - pointLabelFont: Font; - pointLabelColor: string; + dataHiddenByDefault: boolean; type: TimeSeriesChartSeriesType; lineSettings: LineSeriesSettings; barSettings: BarSeriesSettings; @@ -351,24 +380,28 @@ export interface TimeSeriesChartKeySettings { export const timeSeriesChartKeyDefaultSettings: TimeSeriesChartKeySettings = { showInLegend: true, dataHiddenByDefault: false, - showPointLabel: false, - pointLabelPosition: PointLabelPosition.top, - pointLabelFont: { - family: 'Roboto', - size: 11, - sizeUnit: 'px', - style: 'normal', - weight: '400', - lineHeight: '1' - }, - pointLabelColor: timeSeriesChartColorScheme['series.pointLabel'].light, type: TimeSeriesChartSeriesType.line, lineSettings: { + showLine: true, step: false, + stepType: LineSeriesStepType.start, smooth: false, - showLine: true, - lineWidth: 2, lineType: TimeSeriesChartLineType.solid, + lineWidth: 2, + showPoints: false, + showPointLabel: false, + pointLabelPosition: SeriesLabelPosition.top, + pointLabelFont: { + family: 'Roboto', + size: 11, + sizeUnit: 'px', + style: 'normal', + weight: '400', + lineHeight: '1' + }, + pointLabelColor: timeSeriesChartColorScheme['series.label'].light, + pointShape: TimeSeriesChartShape.emptyCircle, + pointSize: 4, fillAreaSettings: { type: SeriesFillType.none, opacity: 0.4, @@ -376,15 +409,23 @@ export const timeSeriesChartKeyDefaultSettings: TimeSeriesChartKeySettings = { start: 100, end: 0 } - }, - showPoints: false, - pointShape: TimeSeriesChartShape.emptyCircle, - pointSize: 4 + } }, barSettings: { showBorder: false, borderWidth: 2, borderRadius: 0, + showLabel: false, + labelPosition: SeriesLabelPosition.top, + labelFont: { + family: 'Roboto', + size: 11, + sizeUnit: 'px', + style: 'normal', + weight: '400', + lineHeight: '1' + }, + labelColor: timeSeriesChartColorScheme['series.label'].light, backgroundSettings: { type: SeriesFillType.none, opacity: 0.4, @@ -707,21 +748,23 @@ export const updateDarkMode = (options: EChartsOption, settings: TimeSeriesChart } for (const item of dataItems) { if (item.dataKey.settings.type === TimeSeriesChartSeriesType.line) { + const lineSettings = item.dataKey.settings as LineSeriesSettings; if (item.option.label?.show) { - item.option.label.rich.value.color = prepareChartThemeColor(item.dataKey.settings.pointLabelColor, darkMode, 'series.pointLabel'); + item.option.label.rich.value.color = prepareChartThemeColor(lineSettings.pointLabelColor, darkMode, 'series.label'); } if (Array.isArray(options.series)) { const series = options.series.find(s => s.id === item.id); if (series) { if (series.label?.show) { - series.label.rich.value.color = prepareChartThemeColor(item.dataKey.settings.pointLabelColor, darkMode, 'series.pointLabel'); + series.label.rich.value.color = prepareChartThemeColor(lineSettings.pointLabelColor, darkMode, 'series.label'); } } } } else { if (item.barRenderContext?.labelOption?.show) { - item.barRenderContext.labelOption.rich.value.color = prepareChartThemeColor(item.dataKey.settings.pointLabelColor, - darkMode, 'series.pointLabel'); + const barSettings = item.dataKey.settings as BarSeriesSettings; + item.barRenderContext.labelOption.rich.value.color = prepareChartThemeColor(barSettings.labelColor, + darkMode, 'series.label'); } } } @@ -747,21 +790,6 @@ const createTimeSeriesChartSeries = (item: TimeSeriesChartDataItem, const dataKey = item.dataKey; const settings: TimeSeriesChartKeySettings = dataKey.settings; const seriesColor = item.dataKey.color; - let pointLabelStyle: ComponentStyle = {}; - if (settings.showPointLabel) { - pointLabelStyle = createChartTextStyle(settings.pointLabelFont, settings.pointLabelColor, darkMode, 'series.pointLabel'); - } - const label: SeriesLabelOption = { - show: settings.showPointLabel, - position: settings.pointLabelPosition, - formatter: (params): string => { - const value = formatValue(params.value[1], item.decimals, item.units, false); - return `{value|${value}}`; - }, - rich: { - value: pointLabelStyle - } - }; seriesOption = { id: item.id, dataGroupId: item.id, @@ -788,8 +816,9 @@ const createTimeSeriesChartSeries = (item: TimeSeriesChartDataItem, const lineSettings = settings.lineSettings; const lineSeriesOption = seriesOption as LineSeriesOption; lineSeriesOption.type = 'line'; - lineSeriesOption.label = label; - lineSeriesOption.step = lineSettings.step; + lineSeriesOption.label = createSeriesLabelOption(item, lineSettings.showPointLabel, + lineSettings.pointLabelFont, lineSettings.pointLabelColor, lineSettings.pointLabelPosition, darkMode); + lineSeriesOption.step = lineSettings.step ? lineSettings.stepType : false; lineSeriesOption.smooth = lineSettings.smooth; lineSeriesOption.lineStyle = { width: lineSettings.showLine ? lineSettings.lineWidth : 0, @@ -825,7 +854,8 @@ const createTimeSeriesChartSeries = (item: TimeSeriesChartDataItem, barVisualSettings.color = createLinearOpacityGradient(seriesColor, barSettings.backgroundSettings.gradient); } item.barRenderContext.visualSettings = barVisualSettings; - item.barRenderContext.labelOption = label; + item.barRenderContext.labelOption = createSeriesLabelOption(item, barSettings.showLabel, + barSettings.labelFont, barSettings.labelColor, barSettings.labelPosition, darkMode); barSeriesOption.renderItem = (params, api) => renderTimeSeriesBar(params, api, item.barRenderContext); } @@ -834,6 +864,26 @@ const createTimeSeriesChartSeries = (item: TimeSeriesChartDataItem, return seriesOption; }; +const createSeriesLabelOption = (item: TimeSeriesChartDataItem, show: boolean, + labelFont: Font, labelColor: string, position: SeriesLabelPosition, + darkMode: boolean): SeriesLabelOption => { + let labelStyle: ComponentStyle = {}; + if (show) { + labelStyle = createChartTextStyle(labelFont, labelColor, darkMode, 'series.label'); + } + return { + show, + position, + formatter: (params): string => { + const value = formatValue(params.value[1], item.decimals, item.units, false); + return `{value|${value}}`; + }, + rich: { + value: labelStyle + } + }; +}; + const createChartTextStyle = (font: Font, color: string, darkMode: boolean, colorKey?: string): ComponentStyle => { const style = textStyle(font); delete style.lineHeight; 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 48c2dfaf57..c36327fec6 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 @@ -21,6 +21,7 @@ import { createTimeSeriesXAxisOption, createTimeSeriesYAxis, generateChartData, + parseThresholdData, SeriesLabelPosition, TimeSeriesChartDataItem, timeSeriesChartDefaultSettings, timeSeriesChartKeyDefaultSettings, @@ -30,8 +31,6 @@ import { TimeSeriesChartThresholdItem, TimeSeriesChartThresholdType, TimeSeriesChartYAxis, - parseThresholdData, - PointLabelPosition, updateDarkMode } from '@home/components/widget/lib/chart/time-series-chart.models'; import { ResizeObserver } from '@juggle/resize-observer'; @@ -60,9 +59,20 @@ import { BehaviorSubject } from 'rxjs'; 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'; export class TbTimeSeriesChart { + public static dataKeySettings(): DataKeySettingsFunction { + return (key, isLatestDataKey) => { + if (!isLatestDataKey) { + return mergeDeep({} as TimeSeriesChartKeySettings, + timeSeriesChartKeyDefaultSettings); + } + return null; + }; + } + private readonly shapeResize$: ResizeObserver; private dataItems: TimeSeriesChartDataItem[] = []; @@ -247,7 +257,10 @@ export class TbTimeSeriesChart { for (const dataKey of dataKeys) { const keySettings = mergeDeep({} as TimeSeriesChartKeySettings, timeSeriesChartKeyDefaultSettings, dataKey.settings); - if (keySettings.showPointLabel && keySettings.pointLabelPosition === PointLabelPosition.top) { + if ((keySettings.type === TimeSeriesChartSeriesType.line && keySettings.lineSettings.showPointLabel && + keySettings.lineSettings.pointLabelPosition === SeriesLabelPosition.top) || + (keySettings.type === TimeSeriesChartSeriesType.bar && keySettings.barSettings.showLabel && + keySettings.barSettings.labelPosition === SeriesLabelPosition.top)) { this.topPointLabels = true; } dataKey.settings = keySettings; @@ -280,8 +293,9 @@ export class TbTimeSeriesChart { if (this.ctx.datasources.length) { for (const datasource of this.ctx.datasources) { latestDataKey = datasource.latestDataKeys?.find(d => - (d.type === DataKeyType.function && d.label === threshold.latestKeyName) || - (d.type !== DataKeyType.function && d.name === threshold.latestKeyName)); + (d.type === DataKeyType.function && d.label === threshold.latestKey) || + (d.type !== DataKeyType.function && d.name === threshold.latestKey && + d.type === threshold.latestKeyType)); if (latestDataKey) { break; } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget2.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget2.ts index a760d1ece1..a46e6a8427 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget2.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget2.ts @@ -298,6 +298,6 @@ export class MapWidgetController implements MapWidgetInterface { } } -export let TbMapWidgetV2: MapWidgetStaticInterface = MapWidgetController; +export const TbMapWidgetV2: MapWidgetStaticInterface = MapWidgetController; 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 new file mode 100644 index 0000000000..b1d5d3d302 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-key-settings.component.html @@ -0,0 +1,36 @@ + + +
+
widgets.time-series-chart.series.legend-settings
+ +
+ {{ 'widgets.time-series-chart.series.show-in-legend' | translate }} +
+
+ +
+ {{ 'widgets.time-series-chart.series.hidden-by-default' | translate }} +
+
+
+
+
widgets.time-series-chart.series.series-type
+ TODO: +
+
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 new file mode 100644 index 0000000000..8f266fa9b6 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-key-settings.component.ts @@ -0,0 +1,82 @@ +/// +/// 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 } from '@angular/core'; +import { WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models'; +import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { mergeDeep } from '@core/utils'; +import { + timeSeriesChartKeyDefaultSettings, + TimeSeriesChartKeySettings +} from '@home/components/widget/lib/chart/time-series-chart.models'; +import { WidgetConfigComponentData } from '@home/models/widget-component.models'; + +@Component({ + selector: 'tb-time-series-chart-key-settings', + templateUrl: './time-series-chart-key-settings.component.html', + styleUrls: ['./../widget-settings.scss'] +}) +export class TimeSeriesChartKeySettingsComponent extends WidgetSettingsComponent { + + timeSeriesChartKeySettingsForm: UntypedFormGroup; + + constructor(protected store: Store, + private fb: UntypedFormBuilder) { + super(store); + } + + protected settingsForm(): UntypedFormGroup { + return this.timeSeriesChartKeySettingsForm; + } + + protected onWidgetConfigSet(widgetConfig: WidgetConfigComponentData) { + const params = widgetConfig.typeParameters as any; + // const timeSeriesChartType = params.timeSeriesChartType; + } + + protected defaultSettings(): WidgetSettings { + return mergeDeep({} as TimeSeriesChartKeySettings, + timeSeriesChartKeyDefaultSettings); + } + + protected onSettingsSet(settings: WidgetSettings) { + const seriesSettings = settings as TimeSeriesChartKeySettings; + this.timeSeriesChartKeySettingsForm = this.fb.group({ + showInLegend: [seriesSettings.showInLegend, []], + dataHiddenByDefault: [seriesSettings.dataHiddenByDefault, []], + type: [seriesSettings.type, []], + lineSettings: [settings.lineSettings, []], + barSettings: [settings.barSettings, []] + }); + } + + protected validatorTriggers(): string[] { + return ['showInLegend']; + } + + protected updateValidators(_emitEvent: boolean) { + const showInLegend: boolean = this.timeSeriesChartKeySettingsForm.get('showInLegend').value; + if (showInLegend) { + this.timeSeriesChartKeySettingsForm.get('dataHiddenByDefault').enable(); + } else { + this.timeSeriesChartKeySettingsForm.get('dataHiddenByDefault').patchValue(false, {emitEvent: false}); + this.timeSeriesChartKeySettingsForm.get('dataHiddenByDefault').disable(); + } + } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/legend-config.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/legend-config.component.html index 44ab1d0bad..3dd5e08860 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/legend-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/legend-config.component.html @@ -16,7 +16,7 @@ --> -
+
{{ 'legend.direction' | translate }}
@@ -31,8 +31,9 @@ + [disabled]="!hideDirection && + legendConfigForm.get('direction').value === legendDirection.row && + (pos === legendPosition.left || pos === legendPosition.right)"> {{ legendPositionTranslations.get(legendPosition[pos]) | translate }} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/legend-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/legend-config.component.ts index cf7b3d4e29..8b065247f3 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/legend-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/legend-config.component.ts @@ -25,6 +25,7 @@ import { legendPositionTranslationMap } from '@shared/models/widget.models'; import { Subscription } from 'rxjs'; +import { coerceBoolean } from '@shared/decorators/coercion'; // @dynamic @Component({ @@ -43,6 +44,10 @@ export class LegendConfigComponent implements OnInit, OnDestroy, ControlValueAcc @Input() disabled: boolean; + @Input() + @coerceBoolean() + hideDirection = false; + legendConfigForm: UntypedFormGroup; legendDirection = LegendDirection; legendDirections = Object.keys(LegendDirection); @@ -60,15 +65,17 @@ export class LegendConfigComponent implements OnInit, OnDestroy, ControlValueAcc ngOnInit(): void { this.legendConfigForm = this.fb.group({ - direction: [null, []], position: [null, []], showValues: [[], []], sortDataKeys: [null, []] }); - this.legendSettingsFormDirectionChanges$ = this.legendConfigForm.get('direction').valueChanges + if (!this.hideDirection) { + this.legendConfigForm.addControl('direction', this.fb.control([null, []])); + this.legendSettingsFormDirectionChanges$ = this.legendConfigForm.get('direction').valueChanges .subscribe((direction: LegendDirection) => { this.onDirectionChanged(direction); }); + } this.legendSettingsFormChanges$ = this.legendConfigForm.valueChanges.subscribe( () => this.legendConfigUpdated() ); @@ -114,23 +121,30 @@ export class LegendConfigComponent implements OnInit, OnDestroy, ControlValueAcc writeValue(legendConfig: LegendConfig): void { if (legendConfig) { - this.legendConfigForm.patchValue({ - direction: legendConfig.direction, + const value: any = { position: legendConfig.position, showValues: this.getShowValues(legendConfig), sortDataKeys: isDefined(legendConfig.sortDataKeys) ? legendConfig.sortDataKeys : false - }, {emitEvent: false}); + }; + if (!this.hideDirection) { + value.direction = legendConfig.direction; + } + this.legendConfigForm.patchValue(value, {emitEvent: false}); + } + if (!this.hideDirection) { + this.onDirectionChanged(legendConfig?.direction); } - this.onDirectionChanged(legendConfig.direction); } private legendConfigUpdated() { const configValue = this.legendConfigForm.value; const legendConfig: Partial = { - direction: configValue.direction, position: configValue.position, sortDataKeys: configValue.sortDataKeys }; + if (!this.hideDirection) { + legendConfig.direction = configValue.direction; + } this.setShowValues(configValue.showValues, legendConfig); this.propagateChange(legendConfig); } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts index 2943b4cc09..4183a06db2 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts @@ -330,6 +330,9 @@ import { import { ToggleButtonWidgetSettingsComponent } from '@home/components/widget/lib/settings/button/toggle-button-widget-settings.component'; +import { + TimeSeriesChartKeySettingsComponent +} from '@home/components/widget/lib/settings/chart/time-series-chart-key-settings.component'; @NgModule({ declarations: [ @@ -448,7 +451,8 @@ import { CommandButtonWidgetSettingsComponent, PowerButtonWidgetSettingsComponent, SliderWidgetSettingsComponent, - ToggleButtonWidgetSettingsComponent + ToggleButtonWidgetSettingsComponent, + TimeSeriesChartKeySettingsComponent ], imports: [ CommonModule, @@ -572,7 +576,8 @@ import { CommandButtonWidgetSettingsComponent, PowerButtonWidgetSettingsComponent, SliderWidgetSettingsComponent, - ToggleButtonWidgetSettingsComponent + ToggleButtonWidgetSettingsComponent, + TimeSeriesChartKeySettingsComponent ] }) export class WidgetSettingsModule { @@ -663,5 +668,6 @@ export const widgetSettingsComponentsMap: {[key: string]: Type { + (window as any).TbTimeSeriesChart = mod.TbTimeSeriesChart; + })) + ); widgetModulesTasks.push(from(import('@home/components/widget/lib/analogue-compass')).pipe( tap((mod) => { (window as any).TbAnalogueCompass = mod.TbAnalogueCompass; @@ -577,6 +583,9 @@ export class WidgetComponentService { if (!isFunction(result.typeParameters.defaultLatestDataKeysFunction)) { result.typeParameters.defaultLatestDataKeysFunction = null; } + if (!isFunction(result.typeParameters.dataKeySettingsFunction)) { + result.typeParameters.dataKeySettingsFunction = null; + } if (isUndefined(result.typeParameters.displayRpcMessageToast)) { result.typeParameters.displayRpcMessageToast = true; } diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts index 49e4e2ea59..5ace8ac616 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts @@ -84,6 +84,7 @@ import { coerceBoolean } from '@shared/decorators/coercion'; import { basicWidgetConfigComponentsMap } from '@home/components/widget/config/basic/basic-widget-config.module'; import { TimewindowConfigData } from '@home/components/widget/config/timewindow-config-panel.component'; import Timeout = NodeJS.Timeout; +import { DataKeySettingsFunction } from '@home/components/widget/config/data-keys.component.models'; const emptySettingsSchema: JsonSchema = { type: 'object', @@ -734,7 +735,8 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, OnDe } } - public generateDataKey(chip: any, type: DataKeyType, datakeySettingsSchema: JsonSettingsSchema): DataKey { + public generateDataKey(chip: any, type: DataKeyType, datakeySettingsSchema: JsonSettingsSchema, + isLatestDataKey: boolean, dataKeySettingsFunction: DataKeySettingsFunction): DataKey { if (isObject(chip)) { (chip as DataKey)._hash = Math.random(); return chip; @@ -767,6 +769,11 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, OnDe } if (datakeySettingsSchema && isDefined(datakeySettingsSchema.schema)) { result.settings = this.utils.generateObjectFromJsonSchema(datakeySettingsSchema.schema); + } else if (dataKeySettingsFunction) { + const settings = dataKeySettingsFunction(result, isLatestDataKey); + if (settings) { + result.settings = settings; + } } return result; } diff --git a/ui-ngx/src/app/modules/home/models/widget-component.models.ts b/ui-ngx/src/app/modules/home/models/widget-component.models.ts index 2ea34377d6..26e5fd4057 100644 --- a/ui-ngx/src/app/modules/home/models/widget-component.models.ts +++ b/ui-ngx/src/app/modules/home/models/widget-component.models.ts @@ -102,6 +102,7 @@ import { ImagePipe, MillisecondsToTimeStringPipe, TelemetrySubscriber } from '@a import { UserId } from '@shared/models/id/user-id'; import { UserSettingsService } from '@core/http/user-settings.service'; import { DynamicComponentModule } from '@core/services/dynamic-component-factory.service'; +import { DataKeySettingsFunction } from '@home/components/widget/config/data-keys.component.models'; export interface IWidgetAction { name: string; @@ -549,6 +550,7 @@ export interface WidgetConfigComponentData { settingsSchema: JsonSettingsSchema; dataKeySettingsSchema: JsonSettingsSchema; latestDataKeySettingsSchema: JsonSettingsSchema; + dataKeySettingsFunction: DataKeySettingsFunction; settingsDirective: string; dataKeySettingsDirective: string; latestDataKeySettingsDirective: string; diff --git a/ui-ngx/src/app/shared/models/widget-settings.models.ts b/ui-ngx/src/app/shared/models/widget-settings.models.ts index 8e60c0d9cb..1e2e0d2415 100644 --- a/ui-ngx/src/app/shared/models/widget-settings.models.ts +++ b/ui-ngx/src/app/shared/models/widget-settings.models.ts @@ -348,7 +348,7 @@ export const customDateFormat = (format: string): DateFormatSettings => ({ custom: true }); -export const dateFormats = ['MMM dd yyyy HH:mm', 'dd MMM yyyy HH:mm', 'yyyy MMM dd HH:mm', +export const dateFormats = ['MMM dd yyyy HH:mm', 'dd MMM yyyy HH:mm', 'dd MMM yyyy HH:mm:ss', 'yyyy MMM dd HH:mm', 'MM/dd/yyyy HH:mm', 'dd/MM/yyyy HH:mm', 'yyyy/MM/dd HH:mm:ss', 'yyyy-MM-dd HH:mm:ss', 'yyyy-MM-dd HH:mm:ss.SSS'] .map(f => simpleDateFormat(f)).concat([lastUpdateAgoDateFormat(), customDateFormat('EEE, MMMM dd, yyyy')]); diff --git a/ui-ngx/src/app/shared/models/widget.models.ts b/ui-ngx/src/app/shared/models/widget.models.ts index 5f4ca8f7be..a164d8d3ec 100644 --- a/ui-ngx/src/app/shared/models/widget.models.ts +++ b/ui-ngx/src/app/shared/models/widget.models.ts @@ -43,6 +43,7 @@ import { WidgetConfigComponentData } from '@home/models/widget-component.models' import { ComponentStyle, Font, TimewindowStyle } from '@shared/models/widget-settings.models'; import { NULL_UUID } from '@shared/models/id/has-uuid'; import { HasTenantId } from '@shared/models/entity.models'; +import { DataKeySettingsFunction } from '@home/components/widget/config/data-keys.component.models'; export enum widgetType { timeseries = 'timeseries', @@ -184,6 +185,7 @@ export interface WidgetTypeParameters { hideDataSettings?: boolean; defaultDataKeysFunction?: (configComponent: any, configData: any) => DataKey[]; defaultLatestDataKeysFunction?: (configComponent: any, configData: any) => DataKey[]; + dataKeySettingsFunction?: DataKeySettingsFunction; displayRpcMessageToast?: boolean; } 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 eacf783c53..a36847b334 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -4334,8 +4334,13 @@ "preview": "Preview" }, "tooltip": { + "trigger": "Trigger", + "trigger-point": "Point", + "trigger-axis": "Axis", "value": "Value", "date": "Date", + "show-date-time-interval": "Show date time interval", + "show-date-time-interval-hint": "Show date time interval according to the data aggregation.", "background-color": "Background color", "background-blur": "Background blur" }, @@ -6590,6 +6595,24 @@ "table-tabs": "Table tabs", "show-cell-actions-menu-mobile": "Show cell actions dropdown menu in mobile mode" }, + "time-series-chart": { + "chart": "Chart", + "data-zoom": "Data zoom", + "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", + "series": { + "legend-settings": "Legend settings", + "show-in-legend": "Show in legend", + "show-in-legend-hint": "Show series name and data in legend.", + "hidden-by-default": "Hidden by default", + "hidden-by-default-hint": "Make series hidden in legend by default.", + "series-type": "Series type", + "type": "Type", + "type-line": "Line", + "type-bar": "Bar" + } + }, "wind-speed-direction": { "layout": "Layout", "layout-default": "Default", diff --git a/ui-ngx/src/form.scss b/ui-ngx/src/form.scss index 524a4e86ac..c3e2599bd9 100644 --- a/ui-ngx/src/form.scss +++ b/ui-ngx/src/form.scss @@ -467,6 +467,17 @@ } } } + &.fixed-height { + .mat-mdc-form-field-infix { + max-height: 40px; + .mat-mdc-select { + max-height: 24px; + .mat-mdc-select-trigger { + max-height: 24px; + } + } + } + } } .tb-form-table { @@ -670,4 +681,14 @@ } } } + + .mat-mdc-option { + &.flex { + .mdc-list-item__primary-text { + display: flex; + align-items: center; + justify-content: flex-start; + } + } + } }