From 71583c5a292b33ef73700718d1386f7cf2df7bb3 Mon Sep 17 00:00:00 2001 From: Sergey Matvienko Date: Thu, 7 Mar 2024 17:41:51 +0100 Subject: [PATCH 01/10] TbAbstractGetAttributesNode: get latest moved to the last step because SqlTimeseriesLatestDao.findLatest() effectively do blocking DB call, so getting attr async will do the job without awaiting the latest response. --- .../rule/engine/metadata/TbAbstractGetAttributesNode.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbAbstractGetAttributesNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbAbstractGetAttributesNode.java index fab1893702..7cf144ec66 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbAbstractGetAttributesNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbAbstractGetAttributesNode.java @@ -99,10 +99,10 @@ public abstract class TbAbstractGetAttributesNode>> failuresPairSet = ConcurrentHashMap.newKeySet(); var getKvEntryPairFutures = Futures.allAsList( - getLatestTelemetry(ctx, entityId, TbNodeUtils.processPatterns(config.getLatestTsKeyNames(), msg), failuresPairSet), getAttrAsync(ctx, entityId, CLIENT_SCOPE, TbNodeUtils.processPatterns(config.getClientAttributeNames(), msg), failuresPairSet), getAttrAsync(ctx, entityId, SHARED_SCOPE, TbNodeUtils.processPatterns(config.getSharedAttributeNames(), msg), failuresPairSet), - getAttrAsync(ctx, entityId, SERVER_SCOPE, TbNodeUtils.processPatterns(config.getServerAttributeNames(), msg), failuresPairSet) + getAttrAsync(ctx, entityId, SERVER_SCOPE, TbNodeUtils.processPatterns(config.getServerAttributeNames(), msg), failuresPairSet), + getLatestTelemetry(ctx, entityId, TbNodeUtils.processPatterns(config.getLatestTsKeyNames(), msg), failuresPairSet) ); withCallback(getKvEntryPairFutures, futuresList -> { var msgMetaData = msg.getMetaData().copy(); From c8abb9ed8eefa3b2bd9df16db1f754d014d2d0f7 Mon Sep 17 00:00:00 2001 From: Sergey Matvienko Date: Thu, 7 Mar 2024 19:01:10 +0100 Subject: [PATCH 02/10] Validator.validateString implementation with function --- .../thingsboard/server/dao/service/Validator.java | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/Validator.java b/dao/src/main/java/org/thingsboard/server/dao/service/Validator.java index 77f81d639f..16454e5914 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/Validator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/Validator.java @@ -27,6 +27,7 @@ import org.thingsboard.server.dao.exception.IncorrectParameterException; import java.util.List; import java.util.UUID; +import java.util.function.Function; import java.util.regex.Pattern; public class Validator { @@ -59,6 +60,18 @@ public class Validator { } } + /* + * This method validate String string. If string is invalid than throw + * IncorrectParameterException exception + * + * @param val the value + * @param errorMessageFunction the error message function that apply value + */ + public static void validateString(String val, Function errorMessageFunction) { + if (val == null || val.isEmpty()) { + throw new IncorrectParameterException(errorMessageFunction.apply(val)); + } + } /** * This method validate long value. If value isn't positive than throw From 8c513bbf82b0c023acfa2789ee4fb012fe4dcc33 Mon Sep 17 00:00:00 2001 From: Sergey Matvienko Date: Thu, 7 Mar 2024 19:03:14 +0100 Subject: [PATCH 03/10] BaseTimeseriesService reduce memory footprint at findLatest --- .../dao/timeseries/BaseTimeseriesService.java | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/dao/src/main/java/org/thingsboard/server/dao/timeseries/BaseTimeseriesService.java b/dao/src/main/java/org/thingsboard/server/dao/timeseries/BaseTimeseriesService.java index 1f77fa3c1d..8d04d1f570 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/timeseries/BaseTimeseriesService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/timeseries/BaseTimeseriesService.java @@ -43,6 +43,7 @@ import org.thingsboard.server.dao.entityview.EntityViewService; import org.thingsboard.server.dao.exception.IncorrectParameterException; import org.thingsboard.server.dao.service.Validator; +import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; @@ -126,18 +127,22 @@ public class BaseTimeseriesService implements TimeseriesService { @Override public ListenableFuture> findLatest(TenantId tenantId, EntityId entityId, Collection keys) { validate(entityId); - List> futures = Lists.newArrayListWithExpectedSize(keys.size()); - keys.forEach(key -> Validator.validateString(key, "Incorrect key " + key)); - keys.forEach(key -> futures.add(timeseriesLatestDao.findLatest(tenantId, entityId, key))); + List> futures = new ArrayList<>(keys.size()); + keys.forEach(key -> Validator.validateString(key, k -> "Incorrect key " + k)); + for (String key : keys) { + futures.add(timeseriesLatestDao.findLatest(tenantId, entityId, key)); + } return Futures.allAsList(futures); } @Override public List findLatestSync(TenantId tenantId, EntityId entityId, Collection keys) { validate(entityId); - List latestEntries = Lists.newArrayListWithExpectedSize(keys.size()); - keys.forEach(key -> Validator.validateString(key, "Incorrect key " + key)); - keys.forEach(key -> latestEntries.add(timeseriesLatestDao.findLatestSync(tenantId, entityId, key))); + List latestEntries = new ArrayList(keys.size()); + keys.forEach(key -> Validator.validateString(key, k -> "Incorrect key " + k)); + for (String key : keys) { + latestEntries.add(timeseriesLatestDao.findLatestSync(tenantId, entityId, key)); + } return latestEntries; } From 45e9a9f642b9c5167738ba73437fbab533e49fb1 Mon Sep 17 00:00:00 2001 From: Sergey Matvienko Date: Thu, 7 Mar 2024 20:26:59 +0100 Subject: [PATCH 04/10] Validator.validateEntityId with function added to reduce an overhead --- .../thingsboard/server/dao/service/Validator.java | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/Validator.java b/dao/src/main/java/org/thingsboard/server/dao/service/Validator.java index 16454e5914..b3a121baf6 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/Validator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/Validator.java @@ -47,6 +47,19 @@ public class Validator { } } + /** + * This method validate EntityId entity id. If entity id is invalid than throw + * IncorrectParameterException exception + * + * @param entityId the entityId + * @param errorMessageFunction the error message for exception that apply entityId + */ + public static void validateEntityId(EntityId entityId, Function errorMessageFunction) { + if (entityId == null || entityId.getId() == null) { + throw new IncorrectParameterException(errorMessageFunction.apply(entityId)); + } + } + /** * This method validate String string. If string is invalid than throw * IncorrectParameterException exception From c97cbbefa3199c4401d191e2803310a6efb59468 Mon Sep 17 00:00:00 2001 From: Sergey Matvienko Date: Thu, 7 Mar 2024 20:27:29 +0100 Subject: [PATCH 05/10] BaseTimeseriesService reduce memory footprint at validateEntityId --- .../server/dao/timeseries/BaseTimeseriesService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dao/src/main/java/org/thingsboard/server/dao/timeseries/BaseTimeseriesService.java b/dao/src/main/java/org/thingsboard/server/dao/timeseries/BaseTimeseriesService.java index 8d04d1f570..50c884f86e 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/timeseries/BaseTimeseriesService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/timeseries/BaseTimeseriesService.java @@ -286,7 +286,7 @@ public class BaseTimeseriesService implements TimeseriesService { } private static void validate(EntityId entityId) { - Validator.validateEntityId(entityId, "Incorrect entityId " + entityId); + Validator.validateEntityId(entityId, id -> "Incorrect entityId " + id); } private void validate(ReadTsKvQuery query) { From c2183be6f551021a24aab2e4742261e3b8161504 Mon Sep 17 00:00:00 2001 From: Sergey Matvienko Date: Fri, 8 Mar 2024 10:39:56 +0100 Subject: [PATCH 06/10] BaseTimeseriesService Memory footprint reduced. replaced Lists.newArrayListWithExpectedSize with new ArrayList(size) as we are always use fixed size array and never exceed the initial size. The newArrayListWithExpectedSize implementation adds some additional space to grow beyond initial size. --- .../dao/timeseries/BaseTimeseriesService.java | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/dao/src/main/java/org/thingsboard/server/dao/timeseries/BaseTimeseriesService.java b/dao/src/main/java/org/thingsboard/server/dao/timeseries/BaseTimeseriesService.java index 50c884f86e..cfba6786f3 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/timeseries/BaseTimeseriesService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/timeseries/BaseTimeseriesService.java @@ -16,7 +16,6 @@ package org.thingsboard.server.dao.timeseries; import com.google.common.base.Function; -import com.google.common.collect.Lists; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.MoreExecutors; @@ -138,7 +137,7 @@ public class BaseTimeseriesService implements TimeseriesService { @Override public List findLatestSync(TenantId tenantId, EntityId entityId, Collection keys) { validate(entityId); - List latestEntries = new ArrayList(keys.size()); + List latestEntries = new ArrayList<>(keys.size()); keys.forEach(key -> Validator.validateString(key, k -> "Incorrect key " + k)); for (String key : keys) { latestEntries.add(timeseriesLatestDao.findLatestSync(tenantId, entityId, key)); @@ -170,7 +169,7 @@ public class BaseTimeseriesService implements TimeseriesService { @Override public ListenableFuture save(TenantId tenantId, EntityId entityId, TsKvEntry tsKvEntry) { validate(entityId); - List> futures = Lists.newArrayListWithExpectedSize(INSERTS_PER_ENTRY); + List> futures = new ArrayList<>(INSERTS_PER_ENTRY); saveAndRegisterFutures(tenantId, futures, entityId, tsKvEntry, 0L); return Futures.transform(Futures.allAsList(futures), SUM_ALL_INTEGERS, MoreExecutors.directExecutor()); } @@ -187,7 +186,7 @@ public class BaseTimeseriesService implements TimeseriesService { private ListenableFuture doSave(TenantId tenantId, EntityId entityId, List tsKvEntries, long ttl, boolean saveLatest) { int inserts = saveLatest ? INSERTS_PER_ENTRY : INSERTS_PER_ENTRY_WITHOUT_LATEST; - List> futures = Lists.newArrayListWithExpectedSize(tsKvEntries.size() * inserts); + List> futures = new ArrayList<>(tsKvEntries.size() * inserts); for (TsKvEntry tsKvEntry : tsKvEntries) { if (saveLatest) { saveAndRegisterFutures(tenantId, futures, entityId, tsKvEntry, ttl); @@ -200,7 +199,7 @@ public class BaseTimeseriesService implements TimeseriesService { @Override public ListenableFuture> saveLatest(TenantId tenantId, EntityId entityId, List tsKvEntries) { - List> futures = Lists.newArrayListWithExpectedSize(tsKvEntries.size()); + List> futures = new ArrayList<>(tsKvEntries.size()); for (TsKvEntry tsKvEntry : tsKvEntries) { futures.add(timeseriesLatestDao.saveLatest(tenantId, entityId, tsKvEntry)); } @@ -247,7 +246,7 @@ public class BaseTimeseriesService implements TimeseriesService { public ListenableFuture> remove(TenantId tenantId, EntityId entityId, List deleteTsKvQueries) { validate(entityId); deleteTsKvQueries.forEach(BaseTimeseriesService::validate); - List> futures = Lists.newArrayListWithExpectedSize(deleteTsKvQueries.size() * DELETES_PER_ENTRY); + List> futures = new ArrayList<>(deleteTsKvQueries.size() * DELETES_PER_ENTRY); for (DeleteTsKvQuery tsKvQuery : deleteTsKvQueries) { deleteAndRegisterFutures(tenantId, futures, entityId, tsKvQuery); } @@ -257,7 +256,7 @@ public class BaseTimeseriesService implements TimeseriesService { @Override public ListenableFuture> removeLatest(TenantId tenantId, EntityId entityId, Collection keys) { validate(entityId); - List> futures = Lists.newArrayListWithExpectedSize(keys.size()); + List> futures = new ArrayList<>(keys.size()); for (String key : keys) { DeleteTsKvQuery query = new BaseDeleteTsKvQuery(key, 0, System.currentTimeMillis(), false); futures.add(timeseriesLatestDao.removeLatest(tenantId, entityId, query)); From 59d0f36697c0e7bad424670b4b8c97a7ce3f3652 Mon Sep 17 00:00:00 2001 From: Sergey Matvienko Date: Fri, 8 Mar 2024 15:22:16 +0100 Subject: [PATCH 07/10] CachedAttributesService: replaced blocking calls and immediate futures with the true async --- .../attributes/CachedAttributesService.java | 25 ++++++++++--------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/dao/src/main/java/org/thingsboard/server/dao/attributes/CachedAttributesService.java b/dao/src/main/java/org/thingsboard/server/dao/attributes/CachedAttributesService.java index 1666015a48..8058b0cee5 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/attributes/CachedAttributesService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/attributes/CachedAttributesService.java @@ -113,15 +113,15 @@ public class CachedAttributesService implements AttributesService { validate(entityId, scope); Validator.validateString(attributeKey, "Incorrect attribute key " + attributeKey); - AttributeCacheKey attributeCacheKey = new AttributeCacheKey(scope, entityId, attributeKey); - TbCacheValueWrapper cachedAttributeValue = cache.get(attributeCacheKey); - if (cachedAttributeValue != null) { - hitCounter.increment(); - AttributeKvEntry cachedAttributeKvEntry = cachedAttributeValue.get(); - return Futures.immediateFuture(Optional.ofNullable(cachedAttributeKvEntry)); - } else { - missCounter.increment(); - return cacheExecutor.submit(() -> { + return cacheExecutor.submit(() -> { + AttributeCacheKey attributeCacheKey = new AttributeCacheKey(scope, entityId, attributeKey); + TbCacheValueWrapper cachedAttributeValue = cache.get(attributeCacheKey); + if (cachedAttributeValue != null) { + hitCounter.increment(); + AttributeKvEntry cachedAttributeKvEntry = cachedAttributeValue.get(); + return Optional.ofNullable(cachedAttributeKvEntry); + } else { + missCounter.increment(); var cacheTransaction = cache.newTransactionForKey(attributeCacheKey); try { Optional result = attributesDao.find(tenantId, entityId, scope, attributeKey); @@ -133,8 +133,8 @@ public class CachedAttributesService implements AttributesService { log.debug("Could not find attribute from cache: [{}] [{}] [{}]", entityId, scope, attributeKey, e); throw e; } - }); - } + } + }); } @Override @@ -207,7 +207,8 @@ public class CachedAttributesService implements AttributesService { @Override public ListenableFuture> findAll(TenantId tenantId, EntityId entityId, String scope) { validate(entityId, scope); - return Futures.immediateFuture(attributesDao.findAll(tenantId, entityId, scope)); + // We can`t watch on cache because the keys are unknown. + return jpaExecutorService.submit(() -> attributesDao.findAll(tenantId, entityId, scope)); } @Override From c4b243ef10f7e5b3c6ea534f864c8f13e92e4221 Mon Sep 17 00:00:00 2001 From: Sergey Matvienko Date: Fri, 8 Mar 2024 18:20:13 +0100 Subject: [PATCH 08/10] SqlTimeseriesLatestDao async refactoring: replaced Futures.immediateFuture() blocking call with true async using service.submit() and Futures.transformAsync() --- .../dao/sqlts/SqlTimeseriesLatestDao.java | 43 ++++++++----------- 1 file changed, 18 insertions(+), 25 deletions(-) diff --git a/dao/src/main/java/org/thingsboard/server/dao/sqlts/SqlTimeseriesLatestDao.java b/dao/src/main/java/org/thingsboard/server/dao/sqlts/SqlTimeseriesLatestDao.java index 112daf0637..f1a58aa417 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sqlts/SqlTimeseriesLatestDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sqlts/SqlTimeseriesLatestDao.java @@ -151,12 +151,12 @@ public class SqlTimeseriesLatestDao extends BaseAbstractSqlTimeseriesDao impleme @Override public ListenableFuture> findLatestOpt(TenantId tenantId, EntityId entityId, String key) { - return Futures.immediateFuture(Optional.ofNullable(doFindLatest(entityId, key))); + return service.submit(() -> Optional.ofNullable(doFindLatest(entityId, key))); } @Override public ListenableFuture findLatest(TenantId tenantId, EntityId entityId, String key) { - return Futures.immediateFuture(getLatestTsKvEntry(entityId, key)); + return service.submit(() -> getLatestTsKvEntry(entityId, key)); } @Override @@ -221,36 +221,29 @@ public class SqlTimeseriesLatestDao extends BaseAbstractSqlTimeseriesDao impleme } protected ListenableFuture getRemoveLatestFuture(TenantId tenantId, EntityId entityId, DeleteTsKvQuery query) { - TsKvEntry latest = doFindLatest(entityId, query.getKey()); - - if (latest == null) { - return Futures.immediateFuture(new TsKvLatestRemovingResult(query.getKey(), false)); - } - - long ts = latest.getTs(); - ListenableFuture removedLatestFuture; - if (ts >= query.getStartTs() && ts < query.getEndTs()) { - TsKvLatestEntity latestEntity = new TsKvLatestEntity(); - latestEntity.setEntityId(entityId.getId()); - latestEntity.setKey(getOrSaveKeyId(query.getKey())); - removedLatestFuture = service.submit(() -> { + ListenableFuture latestFuture = service.submit(() -> doFindLatest(entityId, query.getKey())); + return Futures.transformAsync(latestFuture, latest -> { + if (latest == null) { + return Futures.immediateFuture(new TsKvLatestRemovingResult(query.getKey(), false)); + } + boolean isRemoved = false; + long ts = latest.getTs(); + if (ts >= query.getStartTs() && ts < query.getEndTs()) { + TsKvLatestEntity latestEntity = new TsKvLatestEntity(); + latestEntity.setEntityId(entityId.getId()); + latestEntity.setKey(getOrSaveKeyId(query.getKey())); tsKvLatestRepository.delete(latestEntity); - return true; - }); - } else { - removedLatestFuture = Futures.immediateFuture(false); - } - - return Futures.transformAsync(removedLatestFuture, isRemoved -> { - if (isRemoved && query.getRewriteLatestIfDeleted()) { - return getNewLatestEntryFuture(tenantId, entityId, query); + isRemoved = true; + if (query.getRewriteLatestIfDeleted()) { + return getNewLatestEntryFuture(tenantId, entityId, query); + } } return Futures.immediateFuture(new TsKvLatestRemovingResult(query.getKey(), isRemoved)); }, MoreExecutors.directExecutor()); } protected ListenableFuture> getFindAllLatestFuture(EntityId entityId) { - return Futures.immediateFuture( + return service.submit(() -> DaoUtil.convertDataList(Lists.newArrayList( searchTsKvLatestRepository.findAllByEntityId(entityId.getId())))); } From c688f516c7d7e73869211673b018f1defa4d0765 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Mon, 11 Mar 2024 15:44:56 +0200 Subject: [PATCH 09/10] UI: Multiple Y axes configuration support for time series chart. --- ...e-series-chart-basic-config.component.html | 15 +- ...ime-series-chart-basic-config.component.ts | 39 +++- .../basic/common/data-key-row.component.html | 39 ++-- .../basic/common/data-key-row.component.scss | 17 ++ .../basic/common/data-key-row.component.ts | 46 +++- .../common/data-keys-panel.component.html | 5 +- .../common/data-keys-panel.component.scss | 15 ++ .../basic/common/data-keys-panel.component.ts | 8 + .../aggregated-value-card-widget.component.ts | 30 +-- .../value-chart-card-widget.component.ts | 11 +- .../lib/chart/time-series-chart.models.ts | 216 ++++++++++++++---- .../widget/lib/chart/time-series-chart.ts | 102 +++++++-- ...e-series-chart-key-settings.component.html | 12 + ...ime-series-chart-key-settings.component.ts | 12 +- ...eries-chart-widget-settings.component.html | 15 +- ...-series-chart-widget-settings.component.ts | 37 ++- ...-series-chart-axis-settings.component.html | 18 +- ...me-series-chart-axis-settings.component.ts | 18 +- ...-series-chart-threshold-row.component.html | 28 ++- ...-series-chart-threshold-row.component.scss | 14 ++ ...me-series-chart-threshold-row.component.ts | 32 ++- ...rt-threshold-settings-panel.component.html | 10 + ...hart-threshold-settings-panel.component.ts | 6 +- ...ries-chart-thresholds-panel.component.html | 2 + ...ries-chart-thresholds-panel.component.scss | 11 + ...series-chart-thresholds-panel.component.ts | 5 +- ...e-series-chart-y-axes-panel.component.html | 63 +++++ ...e-series-chart-y-axes-panel.component.scss | 87 +++++++ ...ime-series-chart-y-axes-panel.component.ts | 180 +++++++++++++++ ...ime-series-chart-y-axis-row.component.html | 72 ++++++ ...ime-series-chart-y-axis-row.component.scss | 95 ++++++++ .../time-series-chart-y-axis-row.component.ts | 201 ++++++++++++++++ ...chart-y-axis-settings-panel.component.html | 43 ++++ ...chart-y-axis-settings-panel.component.scss | 49 ++++ ...s-chart-y-axis-settings-panel.component.ts | 66 ++++++ .../common/widget-settings-common.module.ts | 15 ++ ui-ngx/src/app/shared/models/common.ts | 19 ++ .../assets/locale/locale.constant-en_US.json | 10 +- 38 files changed, 1502 insertions(+), 161 deletions(-) create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axes-panel.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axes-panel.component.scss create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axes-panel.component.ts create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-row.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-row.component.scss create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-row.component.ts create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-settings-panel.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-settings-panel.component.scss create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-settings-panel.component.ts create mode 100644 ui-ngx/src/app/shared/models/common.ts diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/time-series-chart-basic-config.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/time-series-chart-basic-config.component.html index 3d099c2645..1f62a13c4e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/time-series-chart-basic-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/time-series-chart-basic-config.component.html @@ -25,6 +25,10 @@ forceSingleDatasource formControlName="datasources"> + + + [widgetConfig]="widgetConfig?.config" + [yAxisIds]="yAxisIds">
widget-config.appearance
@@ -104,11 +111,7 @@
-
widgets.time-series-chart.axis.axes
- - +
widgets.time-series-chart.axis.x-axis
diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/time-series-chart-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/time-series-chart-basic-config.component.ts index 5307147b0e..f9ee35da23 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/time-series-chart-basic-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/time-series-chart-basic-config.component.ts @@ -45,7 +45,11 @@ import { TimeSeriesChartWidgetSettings } from '@home/components/widget/lib/chart/time-series-chart-widget.models'; import { EChartsTooltipTrigger } from '@home/components/widget/lib/chart/echarts-widget.models'; -import { TimeSeriesChartType } from '@home/components/widget/lib/chart/time-series-chart.models'; +import { + TimeSeriesChartKeySettings, TimeSeriesChartThreshold, + TimeSeriesChartType, TimeSeriesChartYAxes, + TimeSeriesChartYAxisId +} from '@home/components/widget/lib/chart/time-series-chart.models'; @Component({ selector: 'tb-time-series-chart-basic-config', @@ -63,6 +67,11 @@ export class TimeSeriesChartBasicConfigComponent extends BasicWidgetConfigCompon } } + public get yAxisIds(): TimeSeriesChartYAxisId[] { + const yAxes: TimeSeriesChartYAxes = this.timeSeriesChartWidgetConfigForm.get('yAxes').value; + return Object.keys(yAxes); + } + TimeSeriesChartType = TimeSeriesChartType; EChartsTooltipTrigger = EChartsTooltipTrigger; @@ -86,6 +95,15 @@ export class TimeSeriesChartBasicConfigComponent extends BasicWidgetConfigCompon super(store, widgetConfigComponent); } + public yAxisRemoved(yAxisId: TimeSeriesChartYAxisId): void { + if (this.widgetConfig.config.datasources && this.widgetConfig.config.datasources.length > 1) { + for (let i = 1; i < this.widgetConfig.config.datasources.length; i++) { + const datasource = this.widgetConfig.config.datasources[i]; + this.removeYaxisId(datasource.dataKeys, yAxisId); + } + } + } + protected configForm(): UntypedFormGroup { return this.timeSeriesChartWidgetConfigForm; } @@ -109,6 +127,8 @@ export class TimeSeriesChartBasicConfigComponent extends BasicWidgetConfigCompon this.timeSeriesChartWidgetConfigForm = this.fb.group({ timewindowConfig: [getTimewindowConfig(configData.config), []], datasources: [configData.config.datasources, []], + + yAxes: [settings.yAxes, []], series: [this.getSeries(configData.config.datasources), []], thresholds: [settings.thresholds, []], @@ -126,7 +146,6 @@ export class TimeSeriesChartBasicConfigComponent extends BasicWidgetConfigCompon dataZoom: [settings.dataZoom, []], stack: [settings.stack, []], - yAxis: [settings.yAxis, []], xAxis: [settings.xAxis, []], noAggregationBarWidthSettings: [settings.noAggregationBarWidthSettings, []], @@ -180,7 +199,7 @@ export class TimeSeriesChartBasicConfigComponent extends BasicWidgetConfigCompon this.widgetConfig.config.settings.dataZoom = config.dataZoom; this.widgetConfig.config.settings.stack = config.stack; - this.widgetConfig.config.settings.yAxis = config.yAxis; + this.widgetConfig.config.settings.yAxes = config.yAxes; this.widgetConfig.config.settings.xAxis = config.xAxis; this.widgetConfig.config.settings.noAggregationBarWidthSettings = config.noAggregationBarWidthSettings; @@ -303,6 +322,20 @@ export class TimeSeriesChartBasicConfigComponent extends BasicWidgetConfigCompon } } + private removeYaxisId(series: DataKey[], yAxisId: TimeSeriesChartYAxisId): boolean { + let changed = false; + if (series) { + series.forEach(key => { + const keySettings = ((key.settings || {}) as TimeSeriesChartKeySettings); + if (keySettings.yAxisId === yAxisId) { + keySettings.yAxisId = 'default'; + changed = true; + } + }); + } + return changed; + } + private getCardButtons(config: WidgetConfig): string[] { const buttons: string[] = []; if (isUndefined(config.enableFullscreen) || config.enableFullscreen) { diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-key-row.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-key-row.component.html index 3ef6891808..0c8be80bf6 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-key-row.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-key-row.component.html @@ -38,6 +38,30 @@ +
+ + + + + {{ timeSeriesChartSeriesTypeIcons.get(keyRowFormGroup.get('timeSeriesType').value) }} + + + + {{ timeSeriesChartSeriesTypeIcons.get(type) }} + {{ timeSeriesChartSeriesTypeTranslations.get(type) | translate }} + + + +
+
+ + + + {{ yAxis }} + + + +
@@ -54,21 +78,6 @@
widget-config.decimals-suffix
-
- - - - - {{ timeSeriesChartSeriesTypeIcons.get(keyRowFormGroup.get('timeSeriesType').value) }} - - - - {{ timeSeriesChartSeriesTypeIcons.get(type) }} - {{ timeSeriesChartSeriesTypeTranslations.get(type) | translate }} - - - -
= { dataZoom: false, xAxis: { show: false }, - yAxis: { - show: true, - showLine: false, - showTicks: false, - showTickLabels: false, - showSplitLines: true, - min: 'dataMin', - max: 'dataMax', - intervalCalculator: - 'var scale = axis.scale; return !scale.isBlank() ? ((scale.getExtent()[1] - scale.getExtent()[0]) / 2) : undefined;' + yAxes: { + default: { + show: true, + showLine: false, + showTicks: false, + showTickLabels: false, + showSplitLines: true, + min: 'dataMin', + max: 'dataMax', + intervalCalculator: + 'var scale = axis.scale; return !scale.isBlank() ? ((scale.getExtent()[1] - scale.getExtent()[0]) / 2) : undefined;' + } }, tooltipDateInterval: false, tooltipDateFormat: simpleDateFormat('dd MMM yyyy HH:mm:ss') - } as TimeSeriesChartSettings; + }; this.lineChart = new TbTimeSeriesChart(this.ctx, settings, this.chartElement.nativeElement, this.renderer, true); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/cards/value-chart-card-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/cards/value-chart-card-widget.component.ts index f5a305a247..a7981b3acf 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/cards/value-chart-card-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/cards/value-chart-card-widget.component.ts @@ -57,6 +57,7 @@ import { TimeSeriesChartSeriesType, TimeSeriesChartSettings } from '@home/components/widget/lib/chart/time-series-chart.models'; +import { DeepPartial } from '@shared/models/common'; const layoutHeight = 56; const valueRelativeWidth = 0.35; @@ -162,17 +163,19 @@ export class ValueChartCardWidgetComponent implements OnInit, AfterViewInit, OnD } public ngAfterViewInit() { - const settings: TimeSeriesChartSettings = { + const settings: DeepPartial = { dataZoom: false, xAxis: { show: false }, - yAxis: { - show: false + yAxes: { + default: { + show: false, + } }, tooltipDateInterval: false, tooltipDateFormat: simpleDateFormat('dd MMM yyyy HH:mm:ss') - } as TimeSeriesChartSettings; + }; this.lineChart = new TbTimeSeriesChart(this.ctx, settings, this.chartElement.nativeElement, this.renderer, false); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.models.ts index edfaa3692d..9af41fa378 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.models.ts @@ -25,7 +25,15 @@ import { import { ComponentStyle, Font, simpleDateFormat, textStyle } from '@shared/models/widget-settings.models'; import { XAXisOption, YAXisOption } from 'echarts/types/dist/shared'; import { CustomSeriesOption, LineSeriesOption } from 'echarts/charts'; -import { formatValue, isDefinedAndNotNull, isUndefined, isUndefinedOrNull, parseFunction } from '@core/utils'; +import { + formatValue, + isDefinedAndNotNull, + isNumeric, + isUndefined, + isUndefinedOrNull, + mergeDeep, + parseFunction +} from '@core/utils'; import { LinearGradientObject } from 'zrender/lib/graphic/LinearGradient'; import tinycolor from 'tinycolor2'; import Axis2D from 'echarts/types/src/coord/cartesian/Axis2D'; @@ -289,13 +297,82 @@ export interface TimeSeriesChartAxisSettings { splitLinesColor: string; } +export type TimeSeriesChartYAxisId = 'default' | string; + export interface TimeSeriesChartYAxisSettings extends TimeSeriesChartAxisSettings { + id?: TimeSeriesChartYAxisId; + order?: number; + units?: string; + decimals?: number; min?: number | string; max?: number | string; intervalCalculator?: string; ticksFormatter?: string; } +export const timeSeriesChartYAxisValid = (axis: TimeSeriesChartYAxisSettings): boolean => + !(!axis.id || isUndefinedOrNull(axis.order)); + +export const timeSeriesChartYAxisValidator = (control: AbstractControl): ValidationErrors | null => { + const axis: TimeSeriesChartYAxisSettings = control.value; + if (!timeSeriesChartYAxisValid(axis)) { + return { + axis: true + }; + } + return null; +}; + +export const getNextTimeSeriesYAxisId = (axes: TimeSeriesChartYAxisSettings[]): TimeSeriesChartYAxisId => { + let id = 0; + for (const axis of axes) { + const existingId = axis.id; + if (existingId.startsWith('axis')) { + const idSuffix = existingId.substring('axis'.length); + if (isNumeric(idSuffix)) { + id = Math.max(id, Number(idSuffix)); + } + } + } + return 'axis' + (id+1); +}; + +export const defaultTimeSeriesChartYAxisSettings: TimeSeriesChartYAxisSettings = { + units: null, + decimals: 0, + show: true, + label: '', + labelFont: { + family: 'Roboto', + size: 12, + sizeUnit: 'px', + style: 'normal', + weight: '600', + lineHeight: '1' + }, + labelColor: timeSeriesChartColorScheme['axis.label'].light, + position: AxisPosition.left, + showTickLabels: true, + tickLabelFont: { + family: 'Roboto', + size: 12, + sizeUnit: 'px', + style: 'normal', + weight: '400', + lineHeight: '1' + }, + tickLabelColor: timeSeriesChartColorScheme['axis.tickLabel'].light, + ticksFormatter: null, + showTicks: true, + ticksColor: timeSeriesChartColorScheme['axis.ticks'].light, + showLine: true, + lineColor: timeSeriesChartColorScheme['axis.line'].light, + showSplitLines: true, + splitLinesColor: timeSeriesChartColorScheme['axis.splitLine'].light +}; + +export type TimeSeriesChartYAxes = {[id: TimeSeriesChartYAxisId]: TimeSeriesChartYAxisSettings}; + export interface TimeSeriesChartThreshold { type: TimeSeriesChartThresholdType; value?: number; @@ -304,6 +381,7 @@ export interface TimeSeriesChartThreshold { entityAlias?: string; entityKey?: string; entityKeyType?: DataKeyType.attribute | DataKeyType.timeseries; + yAxisId: TimeSeriesChartYAxisId; units?: string; decimals?: number; lineColor: string; @@ -320,7 +398,7 @@ export interface TimeSeriesChartThreshold { } export const timeSeriesChartThresholdValid = (threshold: TimeSeriesChartThreshold): boolean => { - if (!threshold.type) { + if (!threshold.type || !threshold.yAxisId) { return false; } switch (threshold.type) { @@ -355,7 +433,8 @@ export const timeSeriesChartThresholdValidator = (control: AbstractControl): Val export const timeSeriesChartThresholdDefaultSettings: TimeSeriesChartThreshold = { type: TimeSeriesChartThresholdType.constant, - units: '', + yAxisId: 'default', + units: null, decimals: 0, lineColor: timeSeriesChartColorScheme['threshold.line'].light, lineType: TimeSeriesChartLineType.solid, @@ -404,13 +483,61 @@ export interface TimeSeriesChartNoAggregationBarWidthSettings { barWidth?: TimeSeriesChartBarWidth; } +export enum TimeSeriesChartAnimationEasing { + linear = 'linear', + quadraticIn = 'quadraticIn', + quadraticOut = 'quadraticOut', + quadraticInOut = 'quadraticInOut', + cubicIn = 'cubicIn', + cubicOut = 'cubicOut', + cubicInOut = 'cubicInOut', + quarticIn = 'quarticIn', + quarticOut = 'quarticOut', + quarticInOut = 'quarticInOut', + quinticIn = 'quinticIn', + quinticOut = 'quinticOut', + quinticInOut = 'quinticInOut', + sinusoidalIn = 'sinusoidalIn', + sinusoidalOut = 'sinusoidalOut', + sinusoidalInOut = 'sinusoidalInOut', + exponentialIn = 'exponentialIn', + exponentialOut = 'exponentialOut', + exponentialInOut = 'exponentialInOut', + circularIn = 'circularIn', + circularOut = 'circularOut', + circularInOut = 'circularInOut', + elasticIn = 'elasticIn', + elasticOut = 'elasticOut', + elasticInOut = 'elasticInOut', + backIn = 'backIn', + backOut = 'backOut', + backInOut = 'backInOut', + bounceIn = 'bounceIn', + bounceOut = 'bounceOut', + bounceInOut = 'bounceInOut' +} + +export const timeSeriesChartAnimationEasings = Object.keys(TimeSeriesChartAnimationEasing) as TimeSeriesChartAnimationEasing[]; + +export interface TimeSeriesChartAnimationSettings { + animation: boolean; + animationThreshold: number; + animationDuration: number; + animationEasing: TimeSeriesChartAnimationEasing; + animationDelay: number; + animationDurationUpdate: number; + animationEasingUpdate: TimeSeriesChartAnimationEasing; + animationDelayUpdate: number; +} + export interface TimeSeriesChartSettings extends EChartsTooltipWidgetSettings { thresholds: TimeSeriesChartThreshold[]; darkMode: boolean; dataZoom: boolean; stack: boolean; - yAxis: TimeSeriesChartYAxisSettings; + yAxes: TimeSeriesChartYAxes; xAxis: TimeSeriesChartAxisSettings; + animation: TimeSeriesChartAnimationSettings; noAggregationBarWidthSettings: TimeSeriesChartNoAggregationBarWidthSettings; } @@ -419,36 +546,10 @@ export const timeSeriesChartDefaultSettings: TimeSeriesChartSettings = { darkMode: false, dataZoom: true, stack: false, - yAxis: { - show: true, - label: '', - labelFont: { - family: 'Roboto', - size: 12, - sizeUnit: 'px', - style: 'normal', - weight: '600', - lineHeight: '1' - }, - labelColor: timeSeriesChartColorScheme['axis.label'].light, - position: AxisPosition.left, - showTickLabels: true, - tickLabelFont: { - family: 'Roboto', - size: 12, - sizeUnit: 'px', - style: 'normal', - weight: '400', - lineHeight: '1' - }, - tickLabelColor: timeSeriesChartColorScheme['axis.tickLabel'].light, - ticksFormatter: null, - showTicks: true, - ticksColor: timeSeriesChartColorScheme['axis.ticks'].light, - showLine: true, - lineColor: timeSeriesChartColorScheme['axis.line'].light, - showSplitLines: true, - splitLinesColor: timeSeriesChartColorScheme['axis.splitLine'].light + yAxes: { + default: mergeDeep({} as TimeSeriesChartYAxisSettings, + defaultTimeSeriesChartYAxisSettings, + { id: 'default', order: 0 } as TimeSeriesChartYAxisSettings) }, xAxis: { show: true, @@ -480,6 +581,16 @@ export const timeSeriesChartDefaultSettings: TimeSeriesChartSettings = { showSplitLines: true, splitLinesColor: timeSeriesChartColorScheme['axis.splitLine'].light }, + animation: { + animation: true, + animationThreshold: 2000, + animationDuration: 1000, + animationEasing: TimeSeriesChartAnimationEasing.cubicOut, + animationDelay: 0, + animationDurationUpdate: 300, + animationEasingUpdate: TimeSeriesChartAnimationEasing.cubicOut, + animationDelayUpdate: 0 + }, noAggregationBarWidthSettings: { strategy: TimeSeriesChartNoAggregationBarWidthStrategy.group, groupWidth: { @@ -558,6 +669,7 @@ export interface BarSeriesSettings { } export interface TimeSeriesChartKeySettings { + yAxisId: TimeSeriesChartYAxisId; showInLegend: boolean; dataHiddenByDefault: boolean; type: TimeSeriesChartSeriesType; @@ -566,6 +678,7 @@ export interface TimeSeriesChartKeySettings { } export const timeSeriesChartKeyDefaultSettings: TimeSeriesChartKeySettings = { + yAxisId: 'default', showInLegend: true, dataHiddenByDefault: false, type: TimeSeriesChartSeriesType.line, @@ -626,6 +739,7 @@ export const timeSeriesChartKeyDefaultSettings: TimeSeriesChartKeySettings = { }; export interface TimeSeriesChartDataItem extends EChartsSeriesItem { + yAxisId: TimeSeriesChartYAxisId; yAxisIndex: number; option?: LineSeriesOption | CustomSeriesOption; barRenderContext?: BarRenderContext; @@ -635,6 +749,7 @@ type TimeSeriesChartThresholdValue = number | string | (number | string)[]; export interface TimeSeriesChartThresholdItem { id: string; + yAxisId: TimeSeriesChartYAxisId; yAxisIndex: number; latestDataKey?: DataKey; units?: string; @@ -646,14 +761,16 @@ export interface TimeSeriesChartThresholdItem { export interface TimeSeriesChartYAxis { id: string; - units: string; + decimals: number; + settings: TimeSeriesChartYAxisSettings; option: YAXisOption & ValueAxisBaseOption; intervalCalculator?: (axis: Axis2D) => number; ticksFormatter?: (value: any) => string; } -export const createTimeSeriesYAxis = (axisId: string, units: string, - decimals: number, settings: TimeSeriesChartYAxisSettings, +export const createTimeSeriesYAxis = (units: string, + decimals: number, + settings: TimeSeriesChartYAxisSettings, darkMode: boolean): TimeSeriesChartYAxis => { const yAxisTickLabelStyle = createChartTextStyle(settings.tickLabelFont, settings.tickLabelColor, darkMode, 'axis.tickLabel'); @@ -664,13 +781,14 @@ export const createTimeSeriesYAxis = (axisId: string, units: string, ticksFormatter = parseFunction(settings.ticksFormatter, ['value']); } const yAxis: TimeSeriesChartYAxis = { - id: axisId, - units, + id: settings.id, + decimals, + settings, option: { show: settings.show, type: 'value', position: settings.position, - id: axisId, + id: settings.id, offset: 0, alignTicks: true, scale: true, @@ -843,13 +961,11 @@ const generateChartThresholds = (thresholdItems: TimeSeriesChartThresholdItem[], id: item.id, dataGroupId: item.id, yAxisIndex: item.yAxisIndex, - animation: true, data: [], tooltip: { show: false }, markLine: { - animation: true, lineStyle: { width: item.settings.lineWidth, color: prepareChartThemeColor(item.settings.lineColor, darkMode, 'threshold.line'), @@ -956,16 +1072,19 @@ const generateChartSeries = (dataItems: TimeSeriesChartDataItem[], }; export const updateDarkMode = (options: EChartsOption, settings: TimeSeriesChartSettings, + yAxisList: TimeSeriesChartYAxis[], dataItems: TimeSeriesChartDataItem[], thresholdDataItems: TimeSeriesChartThresholdItem[], darkMode: boolean): EChartsOption => { options.darkMode = darkMode; if (Array.isArray(options.yAxis)) { - for (const yAxis of options.yAxis) { - yAxis.nameTextStyle.color = prepareChartThemeColor(settings.yAxis.labelColor, darkMode, 'axis.label'); - yAxis.axisLabel.color = prepareChartThemeColor(settings.yAxis.tickLabelColor, darkMode, 'axis.tickLabel'); - yAxis.axisLine.lineStyle.color = prepareChartThemeColor(settings.yAxis.lineColor, darkMode, 'axis.line'); - yAxis.axisTick.lineStyle.color = prepareChartThemeColor(settings.yAxis.ticksColor, darkMode, 'axis.ticks'); - yAxis.splitLine.lineStyle.color = prepareChartThemeColor(settings.yAxis.splitLinesColor, darkMode, 'axis.splitLine'); + for (let i = 0; i < options.yAxis.length; i++) { + const yAxis = options.yAxis[i]; + const yAxisSettings = yAxisList[i].settings; + yAxis.nameTextStyle.color = prepareChartThemeColor(yAxisSettings.labelColor, darkMode, 'axis.label'); + yAxis.axisLabel.color = prepareChartThemeColor(yAxisSettings.tickLabelColor, darkMode, 'axis.tickLabel'); + yAxis.axisLine.lineStyle.color = prepareChartThemeColor(yAxisSettings.lineColor, darkMode, 'axis.line'); + yAxis.axisTick.lineStyle.color = prepareChartThemeColor(yAxisSettings.ticksColor, darkMode, 'axis.ticks'); + yAxis.splitLine.lineStyle.color = prepareChartThemeColor(yAxisSettings.splitLinesColor, darkMode, 'axis.splitLine'); } } if (Array.isArray(options.xAxis)) { @@ -1032,7 +1151,6 @@ const createTimeSeriesChartSeries = (item: TimeSeriesChartDataItem, emphasis: { focus: 'series' }, - animation: true, dimensions: [ {name: 'intervalStart', type: 'number'}, {name: 'intervalEnd', type: 'number'} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.ts index 6d790d5439..abad3f79e8 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.ts @@ -19,7 +19,7 @@ import { AxisPosition, calculateThresholdsOffset, createTimeSeriesXAxisOption, - createTimeSeriesYAxis, + createTimeSeriesYAxis, defaultTimeSeriesChartYAxisSettings, generateChartData, parseThresholdData, SeriesLabelPosition, @@ -29,11 +29,11 @@ import { TimeSeriesChartKeySettings, TimeSeriesChartSeriesType, TimeSeriesChartSettings, - TimeSeriesChartShape, + TimeSeriesChartShape, TimeSeriesChartThreshold, timeSeriesChartThresholdDefaultSettings, TimeSeriesChartThresholdItem, TimeSeriesChartThresholdType, TimeSeriesChartType, - TimeSeriesChartYAxis, + TimeSeriesChartYAxis, TimeSeriesChartYAxisId, TimeSeriesChartYAxisSettings, updateDarkMode } from '@home/components/widget/lib/chart/time-series-chart.models'; import { ResizeObserver } from '@juggle/resize-observer'; @@ -63,6 +63,7 @@ import { AggregationType } from '@shared/models/time/time.models'; import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; import { WidgetSubscriptionOptions } from '@core/api/widget-api.models'; import { DataKeySettingsFunction } from '@home/components/widget/config/data-keys.component.models'; +import { DeepPartial } from '@shared/models/common'; export class TbTimeSeriesChart { @@ -94,9 +95,11 @@ export class TbTimeSeriesChart { private readonly shapeResize$: ResizeObserver; + private readonly settings: TimeSeriesChartSettings; + + private yAxisList: TimeSeriesChartYAxis[] = []; private dataItems: TimeSeriesChartDataItem[] = []; private thresholdItems: TimeSeriesChartThresholdItem[] = []; - private yAxisList: TimeSeriesChartYAxis[] = []; private timeSeriesChart: ECharts; private timeSeriesChartOptions: EChartsOption; @@ -120,13 +123,16 @@ export class TbTimeSeriesChart { yMax$ = this.yMaxSubject.asObservable(); constructor(private ctx: WidgetContext, - private readonly settings: TimeSeriesChartSettings, + private readonly inputSettings: DeepPartial, private chartElement: HTMLElement, private renderer: Renderer2, private autoResize = true) { - this.settings = mergeDeep({} as TimeSeriesChartSettings, timeSeriesChartDefaultSettings, this.settings); + this.settings = mergeDeep({} as TimeSeriesChartSettings, + timeSeriesChartDefaultSettings, + this.inputSettings as TimeSeriesChartSettings); this.darkMode = this.settings.darkMode; + this.setupYAxes(); this.setupData(); this.setupThresholds(); if (this.settings.showTooltip && this.settings.tooltipShowDate) { @@ -258,7 +264,8 @@ export class TbTimeSeriesChart { if (this.darkMode !== darkMode) { this.darkMode = darkMode; if (this.timeSeriesChart) { - this.timeSeriesChartOptions = updateDarkMode(this.timeSeriesChartOptions, this.settings, this.dataItems, + this.timeSeriesChartOptions = updateDarkMode(this.timeSeriesChartOptions, + this.settings, this.yAxisList, this.dataItems, this.thresholdItems, darkMode); this.timeSeriesChart.setOption(this.timeSeriesChartOptions); } @@ -287,11 +294,16 @@ export class TbTimeSeriesChart { const units = dataKey.units && dataKey.units.length ? dataKey.units : this.ctx.units; const decimals = isDefinedAndNotNull(dataKey.decimals) ? dataKey.decimals : (isDefinedAndNotNull(this.ctx.decimals) ? this.ctx.decimals : 2); + let yAxisId = keySettings.yAxisId; + if (!Object.keys(this.settings.yAxes).includes(yAxisId)) { + yAxisId = 'default'; + } this.dataItems.push({ id: this.nextComponentId(), units, decimals, - yAxisIndex: this.getYAxis(units, decimals), + yAxisId, + yAxisIndex: this.getYAxisIndex(yAxisId), dataKey, data: namedData, enabled: !keySettings.dataHiddenByDefault @@ -303,7 +315,9 @@ export class TbTimeSeriesChart { private setupThresholds(): void { const thresholdDatasources: Datasource[] = []; - for (const threshold of this.settings.thresholds) { + for (const thresholdSettings of this.settings.thresholds) { + const threshold = mergeDeep({} as TimeSeriesChartThreshold, + timeSeriesChartThresholdDefaultSettings, thresholdSettings); let latestDataKey: DataKey = null; let entityDataKey: DataKey = null; let value = null; @@ -351,11 +365,16 @@ export class TbTimeSeriesChart { const units = threshold.units && threshold.units.length ? threshold.units : this.ctx.units; const decimals = isDefinedAndNotNull(threshold.decimals) ? threshold.decimals : (isDefinedAndNotNull(this.ctx.decimals) ? this.ctx.decimals : 2); + let yAxisId = threshold.yAxisId; + if (!Object.keys(this.settings.yAxes).includes(yAxisId)) { + yAxisId = 'default'; + } const thresholdItem: TimeSeriesChartThresholdItem = { id: this.nextComponentId(), units, decimals, - yAxisIndex: this.getYAxis(units, decimals), + yAxisId, + yAxisIndex: this.getYAxisIndex(yAxisId), value, latestDataKey, settings: threshold @@ -368,17 +387,32 @@ export class TbTimeSeriesChart { this.subscribeForEntityThresholds(thresholdDatasources); } + private setupYAxes(): void { + const yAxisSettingsList = Object.values(this.settings.yAxes); + yAxisSettingsList.sort((a1, a2) => a1.order - a2.order); + for (const yAxisSettings of yAxisSettingsList) { + const axisSettings = mergeDeep({} as TimeSeriesChartYAxisSettings, + defaultTimeSeriesChartYAxisSettings, yAxisSettings); + const units = axisSettings.units && axisSettings.units.length ? axisSettings.units : this.ctx.units; + const decimals = isDefinedAndNotNull(axisSettings.decimals) ? axisSettings.decimals : + (isDefinedAndNotNull(this.ctx.decimals) ? this.ctx.decimals : 2); + const yAxis = createTimeSeriesYAxis(units, decimals, axisSettings, this.darkMode); + this.yAxisList.push(yAxis); + } + } + + private nextComponentId(): string { return (this.componentIndexCounter++) + ''; } - private getYAxis(units: string, decimals: number): number { - let yAxisIndex = this.yAxisList.findIndex(axis => axis.units === units); + private getYAxisIndex(id: TimeSeriesChartYAxisId): number { + let yAxisIndex = this.yAxisList.findIndex(axis => axis.id === id); if (yAxisIndex === -1) { - const yAxisId = this.yAxisList.length + ''; - const yAxis = createTimeSeriesYAxis(yAxisId, units, decimals, this.settings.yAxis, this.darkMode); - this.yAxisList.push(yAxis); - yAxisIndex = this.yAxisList.length - 1; + yAxisIndex = this.yAxisList.findIndex(axis => axis.id === 'default'); + if (yAxisIndex === -1 && this.yAxisList.length) { + yAxisIndex = 0; + } } return yAxisIndex; } @@ -460,7 +494,15 @@ export class TbTimeSeriesChart { realtime: true, bottom: 10 } - ] + ], + animation: this.settings.animation.animation, + animationThreshold: this.settings.animation.animationThreshold, + animationDuration: this.settings.animation.animationDuration, + animationEasing: this.settings.animation.animationEasing, + animationDelay: this.settings.animation.animationDelay, + animationDurationUpdate: this.settings.animation.animationDurationUpdate, + animationEasingUpdate: this.settings.animation.animationEasingUpdate, + animationDelayUpdate: this.settings.animation.animationDelayUpdate }; this.timeSeriesChartOptions.xAxis[0].tbTimeWindow = this.ctx.defaultSubscription.timeWindow; @@ -588,7 +630,7 @@ export class TbTimeSeriesChart { result.offset += 5; } width = newWidth; - const showLine = !!width && this.settings.yAxis.showLine; + const showLine = !!width && yAxis.settings.showLine; if (yAxis.option.axisLine.show !== showLine) { yAxis.option.axisLine.show = showLine; result.changed = true; @@ -597,7 +639,7 @@ export class TbTimeSeriesChart { yAxis.option.offset = result.offset; result.changed = true; } - if (this.settings.yAxis.label) { + if (yAxis.settings.label) { if (!width) { if (yAxis.option.name) { yAxis.option.name = null; @@ -605,7 +647,7 @@ export class TbTimeSeriesChart { } } else { if (!yAxis.option.name) { - yAxis.option.name = this.settings.yAxis.label; + yAxis.option.name = yAxis.settings.label; result.changed = true; } const nameGap = width; @@ -613,7 +655,7 @@ export class TbTimeSeriesChart { yAxis.option.nameGap = nameGap; result.changed = true; } - const nameWidth = measureYAxisNameWidth(this.timeSeriesChart, yAxis.id, this.settings.yAxis.label); + const nameWidth = measureYAxisNameWidth(this.timeSeriesChart, yAxis.id, yAxis.settings.label); result.offset += nameWidth; } } @@ -623,15 +665,25 @@ export class TbTimeSeriesChart { } private scaleYAxis(yAxis: TimeSeriesChartYAxis): boolean { - const yAxisIndex = this.yAxisList.indexOf(yAxis); - const axisBarDataItems = this.dataItems.filter(d => d.yAxisIndex === yAxisIndex && d.enabled && + const axisBarDataItems = this.dataItems.filter(d => d.yAxisId === yAxis.id && d.enabled && d.data.length && d.dataKey.settings.type === TimeSeriesChartSeriesType.bar); return !axisBarDataItems.length; } + private yAxisEmpty(yAxis: TimeSeriesChartYAxis): boolean { + const axisDataItems = this.dataItems.filter(d => d.yAxisId === yAxis.id && d.enabled && + d.data.length); + return !axisDataItems.length; + } + private calculateYAxisInterval(axisList: TimeSeriesChartYAxis[]): boolean { let changed = false; for (const yAxis of axisList) { + const minInterval = this.yAxisEmpty(yAxis) ? undefined : (1 / Math.pow(10, yAxis.decimals)); + if (yAxis.option.minInterval !== minInterval) { + yAxis.option.minInterval = minInterval; + changed = true; + } if (yAxis.intervalCalculator) { const axis = getYAxis(this.timeSeriesChart, yAxis.id); if (axis) { @@ -649,8 +701,10 @@ export class TbTimeSeriesChart { } private minTopOffset(): number { + const showTickLabels = + !!this.yAxisList.find(yAxis => yAxis.settings.show && yAxis.settings.showTickLabels); return (this.topPointLabels) ? 20 : - ((this.settings.yAxis.show && this.settings.yAxis.showTickLabels) ? 10 : 5); + (showTickLabels ? 10 : 5); } private minBottomOffset(): number { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-key-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-key-settings.component.html index 0d470703b6..5f0fbb9e37 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-key-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-key-settings.component.html @@ -29,6 +29,18 @@
+
+
+
widgets.time-series-chart.axis.y-axis
+ + + + {{ yAxis }} + + + +
+
widgets.time-series-chart.series.series-type
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-key-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-key-settings.component.ts index 4da23f32e0..d1ac428396 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-key-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-key-settings.component.ts @@ -25,9 +25,10 @@ import { TimeSeriesChartKeySettings, TimeSeriesChartSeriesType, timeSeriesChartSeriesTypes, - timeSeriesChartSeriesTypeTranslations, TimeSeriesChartType, timeSeriesChartTypeTranslations + timeSeriesChartSeriesTypeTranslations, TimeSeriesChartType, timeSeriesChartTypeTranslations, TimeSeriesChartYAxisId } from '@home/components/widget/lib/chart/time-series-chart.models'; import { WidgetConfigComponentData } from '@home/models/widget-component.models'; +import { TimeSeriesChartWidgetSettings } from '@home/components/widget/lib/chart/time-series-chart-widget.models'; @Component({ selector: 'tb-time-series-chart-key-settings', @@ -50,6 +51,8 @@ export class TimeSeriesChartKeySettingsComponent extends WidgetSettingsComponent chartType = TimeSeriesChartType.default; + yAxisIds: TimeSeriesChartYAxisId[]; + constructor(protected store: Store, private fb: UntypedFormBuilder) { super(store); @@ -64,6 +67,8 @@ export class TimeSeriesChartKeySettingsComponent extends WidgetSettingsComponent if (isDefinedAndNotNull(params.chartType)) { this.chartType = params.chartType; } + const widgetSettings = (widgetConfig.config?.settings || {}) as TimeSeriesChartWidgetSettings; + this.yAxisIds = widgetSettings.yAxes ? Object.keys(widgetSettings.yAxes) : ['default']; } protected defaultSettings(): WidgetSettings { @@ -73,7 +78,12 @@ export class TimeSeriesChartKeySettingsComponent extends WidgetSettingsComponent protected onSettingsSet(settings: WidgetSettings) { const seriesSettings = settings as TimeSeriesChartKeySettings; + let yAxisId = seriesSettings.yAxisId; + if (!this.yAxisIds.includes(yAxisId)) { + yAxisId = 'default'; + } this.timeSeriesChartKeySettingsForm = this.fb.group({ + yAxisId: [yAxisId, []], showInLegend: [seriesSettings.showInLegend, []], dataHiddenByDefault: [seriesSettings.dataHiddenByDefault, []], type: [seriesSettings.type, []], diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-widget-settings.component.html index 35f0d147ac..933961a7ae 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-widget-settings.component.html @@ -16,12 +16,18 @@ --> + + + [widgetConfig]="widgetConfig?.config" + [yAxisIds]="yAxisIds">
widgets.time-series-chart.chart-style
@@ -38,12 +44,7 @@
-
widgets.time-series-chart.axis.axes
- - +
widgets.time-series-chart.axis.x-axis
1) { + for (let i = 1; i < this.widgetConfig.config.datasources.length; i++) { + const datasource = this.widgetConfig.config.datasources[i]; + this.removeYaxisId(datasource.dataKeys, yAxisId); + } + } + } + protected settingsForm(): UntypedFormGroup { return this.timeSeriesChartWidgetSettingsForm; } @@ -91,12 +110,12 @@ export class TimeSeriesChartWidgetSettingsComponent extends WidgetSettingsCompon protected onSettingsSet(settings: WidgetSettings) { this.timeSeriesChartWidgetSettingsForm = this.fb.group({ + yAxes: [settings.yAxes, []], thresholds: [settings.thresholds, []], dataZoom: [settings.dataZoom, []], stack: [settings.stack, []], - yAxis: [settings.yAxis, []], xAxis: [settings.xAxis, []], noAggregationBarWidthSettings: [settings.noAggregationBarWidthSettings, []], @@ -173,6 +192,20 @@ export class TimeSeriesChartWidgetSettingsComponent extends WidgetSettingsCompon } } + private removeYaxisId(series: DataKey[], yAxisId: TimeSeriesChartYAxisId): boolean { + let changed = false; + if (series) { + series.forEach(key => { + const keySettings = ((key.settings || {}) as TimeSeriesChartKeySettings); + if (keySettings.yAxisId === yAxisId) { + keySettings.yAxisId = 'default'; + changed = true; + } + }); + } + return changed; + } + private _tooltipValuePreviewFn(): string { return formatValue(22, 0, '°C', false); } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-axis-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-axis-settings.component.html index 24f3dc0883..9511baf121 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-axis-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-axis-settings.component.html @@ -17,13 +17,13 @@ -->
- + - {{ axisTitle | translate }} + {{ 'widgets.time-series-chart.axis.show' | translate }} @@ -56,6 +56,18 @@
+
+
widget-config.units-short
+ + +
+
+
widget-config.decimals-short
+ + + +
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-axis-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-axis-settings.component.ts index 328574d648..a88fc2e123 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-axis-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-axis-settings.component.ts @@ -15,7 +15,13 @@ /// import { Component, forwardRef, Input, OnInit } from '@angular/core'; -import { ControlValueAccessor, NG_VALUE_ACCESSOR, UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; +import { + ControlValueAccessor, + NG_VALUE_ACCESSOR, + UntypedFormBuilder, + UntypedFormGroup, + Validators +} from '@angular/forms'; import { AxisPosition, timeSeriesAxisPositionTranslations, @@ -40,9 +46,11 @@ import { WidgetService } from '@core/http/widget.service'; }) export class TimeSeriesChartAxisSettingsComponent implements OnInit, ControlValueAccessor { - settingsExpanded = false; + @Input() + @coerceBoolean() + alwaysExpanded = false; - axisTitle: string; + settingsExpanded = false; axisPositions: AxisPosition[]; @@ -72,8 +80,6 @@ export class TimeSeriesChartAxisSettingsComponent implements OnInit, ControlValu ngOnInit(): void { - this.axisTitle = this.axisType === 'xAxis' ? 'widgets.time-series-chart.axis.x-axis' : 'widgets.time-series-chart.axis.y-axis'; - this.axisPositions = this.axisType === 'xAxis' ? [AxisPosition.top, AxisPosition.bottom] : [AxisPosition.left, AxisPosition.right]; @@ -94,6 +100,8 @@ export class TimeSeriesChartAxisSettingsComponent implements OnInit, ControlValu splitLinesColor: [null, []] }); if (this.axisType === 'yAxis') { + this.axisSettingsFormGroup.addControl('units', this.fb.control(null, [])); + this.axisSettingsFormGroup.addControl('decimals', this.fb.control(null, [Validators.min(0)])); this.axisSettingsFormGroup.addControl('ticksFormatter', this.fb.control(null, [])); this.axisSettingsFormGroup.addControl('min', this.fb.control(null, [])); this.axisSettingsFormGroup.addControl('max', this.fb.control(null, [])); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-threshold-row.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-threshold-row.component.html index f82a717d78..7e2c9f74c2 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-threshold-row.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-threshold-row.component.html @@ -73,6 +73,15 @@ [formControl]="entityKeyFormControl">
+
+ + + + {{ yAxis }} + + + +
-
- -
+ +
+
+
+
+
+ +
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axes-panel.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axes-panel.component.scss new file mode 100644 index 0000000000..52aa69f59d --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axes-panel.component.scss @@ -0,0 +1,87 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@import '../../../../../../../../../scss/constants'; + +.tb-y-axes-panel { + .tb-form-table-header-cell { + &.tb-axis-id-header { + width: 60px; + min-width: 60px; + } + + &.tb-label-header { + flex: 1 1 60%; + min-width: 100px; + } + + &.tb-position-header { + flex: 1; + min-width: 60px; + @media #{$mat-gt-md} { + flex: 1 1 40%; + } + } + + &.tb-units-header { + width: 80px; + min-width: 80px; + } + + &.tb-decimals-header { + width: 60px; + min-width: 60px; + } + + &.tb-min-header, &.tb-max-header { + width: 80px; + min-width: 80px; + } + + &.tb-show-header { + width: 40px; + min-width: 40px; + } + + &.tb-actions-header { + width: 80px; + min-width: 80px; + @media #{$mat-gt-md} { + width: 120px; + min-width: 120px; + } + } + + &.tb-label-header, &.tb-units-header, &.tb-decimals-header { + display: none; + @media #{$mat-gt-md} { + display: block; + } + } + + &.tb-min-header, &.tb-max-header { + display: none; + @media #{$mat-gt-xs} { + display: block; + } + } + } + + .tb-form-table-body { + tb-time-series-chart-y-axis-row { + overflow: hidden; + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axes-panel.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axes-panel.component.ts new file mode 100644 index 0000000000..9879ffe877 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axes-panel.component.ts @@ -0,0 +1,180 @@ +/// +/// Copyright © 2016-2024 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, EventEmitter, forwardRef, Input, OnInit, Output, ViewEncapsulation } from '@angular/core'; +import { + AbstractControl, + ControlValueAccessor, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + UntypedFormArray, + UntypedFormBuilder, + UntypedFormControl, + UntypedFormGroup, + Validator +} from '@angular/forms'; +import { + defaultTimeSeriesChartYAxisSettings, + getNextTimeSeriesYAxisId, + TimeSeriesChartYAxes, TimeSeriesChartYAxisId, + TimeSeriesChartYAxisSettings, + timeSeriesChartYAxisValid, + timeSeriesChartYAxisValidator +} from '@home/components/widget/lib/chart/time-series-chart.models'; +import { mergeDeep } from '@core/utils'; +import { CdkDragDrop } from '@angular/cdk/drag-drop'; +import { coerceBoolean } from '@shared/decorators/coercion'; + +@Component({ + selector: 'tb-time-series-chart-y-axes-panel', + templateUrl: './time-series-chart-y-axes-panel.component.html', + styleUrls: ['./time-series-chart-y-axes-panel.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => TimeSeriesChartYAxesPanelComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => TimeSeriesChartYAxesPanelComponent), + multi: true + } + ], + encapsulation: ViewEncapsulation.None +}) +export class TimeSeriesChartYAxesPanelComponent implements ControlValueAccessor, OnInit, Validator { + + @Input() + disabled: boolean; + + @Input() + @coerceBoolean() + advanced = false; + + @Output() + axisRemoved = new EventEmitter(); + + yAxesFormGroup: UntypedFormGroup; + + get dragEnabled(): boolean { + return this.axesFormArray().controls.length > 1; + } + + private propagateChange = (_val: any) => {}; + + constructor(private fb: UntypedFormBuilder) { + } + + ngOnInit() { + this.yAxesFormGroup = this.fb.group({ + axes: [this.fb.array([]), []] + }); + this.yAxesFormGroup.valueChanges.subscribe( + () => { + let axes: TimeSeriesChartYAxisSettings[] = this.yAxesFormGroup.get('axes').value; + for (let i = 0; i < axes.length; i++) { + axes[i].order = i; + } + if (axes) { + axes = axes.filter(axis => timeSeriesChartYAxisValid(axis)); + } + const yAxes: TimeSeriesChartYAxes = {}; + for (const axis of axes) { + yAxes[axis.id] = axis; + } + this.propagateChange(yAxes); + } + ); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (isDisabled) { + this.yAxesFormGroup.disable({emitEvent: false}); + } else { + this.yAxesFormGroup.enable({emitEvent: false}); + } + } + + writeValue(value: TimeSeriesChartYAxes | undefined): void { + const yAxes: TimeSeriesChartYAxes = value || {}; + if (!yAxes.default) { + yAxes.default = mergeDeep({} as TimeSeriesChartYAxisSettings, defaultTimeSeriesChartYAxisSettings, + {id: 'default', order: 0} as TimeSeriesChartYAxisSettings); + } + const yAxisSettingsList = Object.values(yAxes); + yAxisSettingsList.sort((a1, a2) => a1.order - a2.order); + this.yAxesFormGroup.setControl('axes', this.prepareAxesFormArray(yAxisSettingsList), {emitEvent: false}); + } + + public validate(c: UntypedFormControl) { + const valid = this.yAxesFormGroup.valid; + return valid ? null : { + yAxes: { + valid: false, + }, + }; + } + + axisDrop(event: CdkDragDrop) { + const axesArray = this.yAxesFormGroup.get('axes') as UntypedFormArray; + const axis = axesArray.at(event.previousIndex); + axesArray.removeAt(event.previousIndex); + axesArray.insert(event.currentIndex, axis); + } + + axesFormArray(): UntypedFormArray { + return this.yAxesFormGroup.get('axes') as UntypedFormArray; + } + + trackByAxis(index: number, axisControl: AbstractControl): any { + return axisControl; + } + + removeAxis(index: number) { + const axis = + (this.yAxesFormGroup.get('axes') as UntypedFormArray).at(index).value as TimeSeriesChartYAxisSettings; + (this.yAxesFormGroup.get('axes') as UntypedFormArray).removeAt(index); + this.axisRemoved.emit(axis.id); + } + + addAxis() { + const axis = mergeDeep({} as TimeSeriesChartYAxisSettings, + defaultTimeSeriesChartYAxisSettings); + const axes: TimeSeriesChartYAxisSettings[] = this.yAxesFormGroup.get('axes').value; + axis.id = getNextTimeSeriesYAxisId(axes); + axis.order = axes.length; + const axesArray = this.yAxesFormGroup.get('axes') as UntypedFormArray; + const axisControl = this.fb.control(axis, [timeSeriesChartYAxisValidator]); + axesArray.push(axisControl); + } + + private prepareAxesFormArray(axes: TimeSeriesChartYAxisSettings[]): UntypedFormArray { + const axesControls: Array = []; + axes.forEach((axis) => { + axesControls.push(this.fb.control(axis, [timeSeriesChartYAxisValidator])); + }); + return this.fb.array(axesControls); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-row.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-row.component.html new file mode 100644 index 0000000000..0b1907033f --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-row.component.html @@ -0,0 +1,72 @@ + +
+
+ {{ modelValue.id }} +
+ + + + + + + + {{ timeSeriesAxisPositionTranslations.get(position) | translate }} + + + +
+ + + +
+
+ + + +
+
+ + +
+
+ + + +
+
+ +
+ +
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-row.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-row.component.scss new file mode 100644 index 0000000000..12db0d9228 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-row.component.scss @@ -0,0 +1,95 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@import '../../../../../../../../../scss/constants'; + +.tb-form-table-row.tb-axis-row { + + .tb-axis-id-field { + color: rgba(0, 0, 0, 0.76); + font-weight: 500; + font-size: 14px; + line-height: 20px; + letter-spacing: 0.2px; + width: 60px; + min-width: 60px; + } + + .tb-label-field { + flex: 1 1 60%; + min-width: 100px; + } + + .tb-position-field { + flex: 1; + min-width: 60px; + @media #{$mat-gt-md} { + flex: 1 1 40%; + } + } + + .tb-units-field, .tb-decimals-field, .tb-min-field, .tb-max-field { + display: flex; + flex-direction: row; + place-content: center; + align-items: center; + } + + .tb-units-field { + width: 80px; + min-width: 80px; + } + + .tb-decimals-field { + width: 60px; + min-width: 60px; + } + + .tb-min-field, .tb-max-field { + width: 80px; + min-width: 80px; + } + + .tb-show-field { + width: 40px; + min-width: 40px; + } + + .tb-label-field { + display: none; + @media #{$mat-gt-md} { + display: block; + } + } + + .tb-units-field, .tb-decimals-field { + display: none; + @media #{$mat-gt-md} { + display: flex; + } + } + + .tb-min-field, .tb-max-field { + display: none; + @media #{$mat-gt-xs} { + display: flex; + } + } + + .tb-remove-button { + width: 40px; + min-width: 40px; + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-row.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-row.component.ts new file mode 100644 index 0000000000..a4824fd51e --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-row.component.ts @@ -0,0 +1,201 @@ +/// +/// Copyright © 2016-2024 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { + ChangeDetectorRef, + Component, + EventEmitter, + forwardRef, + Input, + OnInit, + Output, + Renderer2, + ViewContainerRef, + ViewEncapsulation +} from '@angular/core'; +import { ControlValueAccessor, NG_VALUE_ACCESSOR, UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; +import { + AxisPosition, + timeSeriesAxisPositionTranslations, + TimeSeriesChartYAxisSettings +} from '@home/components/widget/lib/chart/time-series-chart.models'; +import { MatButton } from '@angular/material/button'; +import { TbPopoverService } from '@shared/components/popover.service'; +import { coerceBoolean } from '@shared/decorators/coercion'; +import { + TimeSeriesChartYAxisSettingsPanelComponent +} from '@home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-settings-panel.component'; +import { deepClone } from '@core/utils'; + +@Component({ + selector: 'tb-time-series-chart-y-axis-row', + templateUrl: './time-series-chart-y-axis-row.component.html', + styleUrls: ['./time-series-chart-y-axis-row.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => TimeSeriesChartYAxisRowComponent), + multi: true + } + ], + encapsulation: ViewEncapsulation.None +}) +export class TimeSeriesChartYAxisRowComponent implements ControlValueAccessor, OnInit { + + axisPositions = [AxisPosition.left, AxisPosition.right]; + + timeSeriesAxisPositionTranslations = timeSeriesAxisPositionTranslations; + + @Input() + disabled: boolean; + + @Input() + @coerceBoolean() + advanced = false; + + @Output() + axisRemoved = new EventEmitter(); + + axisFormGroup: UntypedFormGroup; + + modelValue: TimeSeriesChartYAxisSettings; + + private propagateChange = (_val: any) => {}; + + constructor(private fb: UntypedFormBuilder, + private popoverService: TbPopoverService, + private renderer: Renderer2, + private viewContainerRef: ViewContainerRef, + private cd: ChangeDetectorRef) { + } + + ngOnInit() { + this.axisFormGroup = this.fb.group({ + label: [null, []], + position: [null, []], + units: [null, []], + decimals: [null, []], + min: [null, []], + max: [null, []], + show: [null, []] + }); + this.axisFormGroup.valueChanges.subscribe( + () => this.updateModel() + ); + this.axisFormGroup.get('show').valueChanges.subscribe(() => { + this.updateValidators(); + }); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(_fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (isDisabled) { + this.axisFormGroup.disable({emitEvent: false}); + } else { + this.axisFormGroup.enable({emitEvent: false}); + this.updateValidators(); + } + } + + writeValue(value: TimeSeriesChartYAxisSettings): void { + this.modelValue = value; + this.axisFormGroup.patchValue( + { + label: value.label, + position: value.position, + units: value.units, + decimals: value.decimals, + min: value.min, + max: value.max, + show: value.show, + }, {emitEvent: false} + ); + this.updateValidators(); + this.cd.markForCheck(); + } + + editAxis($event: Event, matButton: MatButton) { + if ($event) { + $event.stopPropagation(); + } + const trigger = matButton._elementRef.nativeElement; + if (this.popoverService.hasPopover(trigger)) { + this.popoverService.hidePopover(trigger); + } else { + const ctx: any = { + yAxisSettings: deepClone(this.modelValue), + advanced: this.advanced + }; + const yAxisSettingsPanelPopover = this.popoverService.displayPopover(trigger, this.renderer, + this.viewContainerRef, TimeSeriesChartYAxisSettingsPanelComponent, ['leftOnly', 'leftTopOnly', 'leftBottomOnly'], true, null, + ctx, + {}, + {}, {}, true); + yAxisSettingsPanelPopover.tbComponentRef.instance.popover = yAxisSettingsPanelPopover; + yAxisSettingsPanelPopover.tbComponentRef.instance.yAxisSettingsApplied.subscribe((yAxisSettings) => { + yAxisSettingsPanelPopover.hide(); + this.modelValue = {...this.modelValue, ...yAxisSettings}; + this.axisFormGroup.patchValue( + { + label: this.modelValue.label, + position: this.modelValue.position, + units: this.modelValue.units, + decimals: this.modelValue.decimals, + min: this.modelValue.min, + max: this.modelValue.max, + show: this.modelValue.show + }, + {emitEvent: false}); + this.updateValidators(); + this.propagateChange(this.modelValue); + }); + } + } + + private updateValidators() { + const show: boolean = this.axisFormGroup.get('show').value; + if (show) { + this.axisFormGroup.get('label').enable({emitEvent: false}); + this.axisFormGroup.get('position').enable({emitEvent: false}); + this.axisFormGroup.get('units').enable({emitEvent: false}); + this.axisFormGroup.get('decimals').enable({emitEvent: false}); + } else { + this.axisFormGroup.get('label').disable({emitEvent: false}); + this.axisFormGroup.get('position').disable({emitEvent: false}); + this.axisFormGroup.get('units').disable({emitEvent: false}); + this.axisFormGroup.get('decimals').disable({emitEvent: false}); + } + } + + private updateModel() { + const value = this.axisFormGroup.value; + this.modelValue.label = value.label; + this.modelValue.position = value.position; + this.modelValue.units = value.units; + this.modelValue.decimals = value.decimals; + this.modelValue.min = value.min; + this.modelValue.max = value.max; + this.modelValue.show = value.show; + this.propagateChange(this.modelValue); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-settings-panel.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-settings-panel.component.html new file mode 100644 index 0000000000..759074b4be --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-settings-panel.component.html @@ -0,0 +1,43 @@ + +
+
{{ 'widgets.time-series-chart.axis.y-axis-settings' | translate }}
+
+ + +
+
+ + +
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-settings-panel.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-settings-panel.component.scss new file mode 100644 index 0000000000..095c38b7f7 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-settings-panel.component.scss @@ -0,0 +1,49 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@import '../../../../../../../../../scss/constants'; + +.tb-y-axis-settings-panel { + width: 530px; + display: flex; + flex-direction: column; + gap: 16px; + @media #{$mat-lt-md} { + width: 90vw; + } + .tb-y-axis-settings-panel-content { + display: flex; + flex-direction: column; + gap: 16px; + overflow: auto; + margin: -10px; + padding: 10px; + } + .tb-y-axis-settings-title { + font-size: 16px; + font-weight: 500; + line-height: 24px; + letter-spacing: 0.25px; + color: rgba(0, 0, 0, 0.87); + } + .tb-y-axis-settings-panel-buttons { + height: 40px; + display: flex; + flex-direction: row; + gap: 16px; + justify-content: flex-end; + align-items: flex-end; + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-settings-panel.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-settings-panel.component.ts new file mode 100644 index 0000000000..8046aafd29 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-settings-panel.component.ts @@ -0,0 +1,66 @@ +/// +/// Copyright © 2016-2024 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, EventEmitter, Input, OnInit, Output, ViewEncapsulation } from '@angular/core'; +import { TbPopoverComponent } from '@shared/components/popover.component'; +import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; +import { TimeSeriesChartYAxisSettings } from '@home/components/widget/lib/chart/time-series-chart.models'; +import { coerceBoolean } from '@shared/decorators/coercion'; + +@Component({ + selector: 'tb-time-series-chart-y-axis-settings-panel', + templateUrl: './time-series-chart-y-axis-settings-panel.component.html', + providers: [], + styleUrls: ['./time-series-chart-y-axis-settings-panel.component.scss'], + encapsulation: ViewEncapsulation.None +}) +export class TimeSeriesChartYAxisSettingsPanelComponent implements OnInit { + + @Input() + yAxisSettings: TimeSeriesChartYAxisSettings; + + @Input() + @coerceBoolean() + advanced = false; + + @Input() + popover: TbPopoverComponent; + + @Output() + yAxisSettingsApplied = new EventEmitter(); + + yAxisSettingsFormGroup: UntypedFormGroup; + + constructor(private fb: UntypedFormBuilder) { + } + + ngOnInit(): void { + this.yAxisSettingsFormGroup = this.fb.group( + { + yAxis: [this.yAxisSettings, []] + } + ); + } + + cancel() { + this.popover?.hide(); + } + + applyYAxisSettings() { + const yAxisSettings = this.yAxisSettingsFormGroup.get('yAxis').getRawValue(); + this.yAxisSettingsApplied.emit(yAxisSettings); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget-settings-common.module.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget-settings-common.module.ts index b741a8e358..714da2a0c7 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget-settings-common.module.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget-settings-common.module.ts @@ -109,6 +109,15 @@ import { import { TimeSeriesNoAggregationBarWidthSettingsComponent } from '@home/components/widget/lib/settings/common/chart/time-series-no-aggregation-bar-width-settings.component'; +import { + TimeSeriesChartYAxesPanelComponent +} from '@home/components/widget/lib/settings/common/chart/time-series-chart-y-axes-panel.component'; +import { + TimeSeriesChartYAxisRowComponent +} from '@home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-row.component'; +import { + TimeSeriesChartYAxisSettingsPanelComponent +} from '@home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-settings-panel.component'; @NgModule({ declarations: [ @@ -150,6 +159,9 @@ import { TimeSeriesChartThresholdRowComponent, TimeSeriesChartThresholdSettingsPanelComponent, TimeSeriesNoAggregationBarWidthSettingsComponent, + TimeSeriesChartYAxesPanelComponent, + TimeSeriesChartYAxisRowComponent, + TimeSeriesChartYAxisSettingsPanelComponent, DataKeyInputComponent, EntityAliasInputComponent ], @@ -197,6 +209,9 @@ import { TimeSeriesChartThresholdRowComponent, TimeSeriesChartThresholdSettingsPanelComponent, TimeSeriesNoAggregationBarWidthSettingsComponent, + TimeSeriesChartYAxesPanelComponent, + TimeSeriesChartYAxisRowComponent, + TimeSeriesChartYAxisSettingsPanelComponent, DataKeyInputComponent, EntityAliasInputComponent ], diff --git a/ui-ngx/src/app/shared/models/common.ts b/ui-ngx/src/app/shared/models/common.ts new file mode 100644 index 0000000000..c331cc5127 --- /dev/null +++ b/ui-ngx/src/app/shared/models/common.ts @@ -0,0 +1,19 @@ +/// +/// Copyright © 2016-2024 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +export type DeepPartial = T extends object ? { + [P in keyof T]?: DeepPartial; +} : T; diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index 216ed878b2..830a54f88e 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -6609,6 +6609,7 @@ "stack-mode": "Stack mode", "stack-mode-hint": "Stacks series on the chart. The series with the same unit would be put on top of each other.", "axes": "Axes", + "y-axes": "Y axes", "line-type": "Line type", "line-type-solid": "Solid", "line-type-dashed": "Dashed", @@ -6670,6 +6671,9 @@ "axes": "Axes", "x-axis": "X axis", "y-axis": "Y axis", + "y-axis-settings": "Y axis settings", + "remove-y-axis": "Remove Y axis", + "id": "Id", "label": "Label", "position": "Position", "position-left": "Left", @@ -6686,7 +6690,11 @@ "scale": "Scale", "scale-min": "min", "scale-max": "max", - "scale-auto": "Auto" + "scale-auto": "Auto", + "min": "Min", + "max": "Max", + "show": "Show", + "add-y-axis": "Add Y axis" }, "series": { "legend-settings": "Legend settings", From 7823f1533c92eba581cb0fb0484b0a0e280bac4b Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Mon, 11 Mar 2024 16:12:44 +0200 Subject: [PATCH 10/10] UI: Time series chart animation settings. --- ...e-series-chart-basic-config.component.html | 3 + ...ime-series-chart-basic-config.component.ts | 4 + ...eries-chart-widget-settings.component.html | 3 + ...-series-chart-widget-settings.component.ts | 2 + ...es-chart-animation-settings.component.html | 89 +++++++++++++ ...ries-chart-animation-settings.component.ts | 124 ++++++++++++++++++ .../common/widget-settings-common.module.ts | 5 + .../assets/locale/locale.constant-en_US.json | 10 ++ 8 files changed, 240 insertions(+) create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-animation-settings.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-animation-settings.component.ts diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/time-series-chart-basic-config.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/time-series-chart-basic-config.component.html index 1f62a13c4e..adaeb78acb 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/time-series-chart-basic-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/time-series-chart-basic-config.component.html @@ -223,6 +223,9 @@
+ +
widget-config.card-appearance
diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/time-series-chart-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/time-series-chart-basic-config.component.ts index f9ee35da23..ba4b4c6371 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/time-series-chart-basic-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/time-series-chart-basic-config.component.ts @@ -168,6 +168,8 @@ export class TimeSeriesChartBasicConfigComponent extends BasicWidgetConfigCompon tooltipBackgroundColor: [settings.tooltipBackgroundColor, []], tooltipBackgroundBlur: [settings.tooltipBackgroundBlur, []], + animation: [settings.animation, []], + background: [settings.background, []], cardButtons: [this.getCardButtons(configData.config), []], @@ -221,6 +223,8 @@ export class TimeSeriesChartBasicConfigComponent extends BasicWidgetConfigCompon this.widgetConfig.config.settings.tooltipBackgroundColor = config.tooltipBackgroundColor; this.widgetConfig.config.settings.tooltipBackgroundBlur = config.tooltipBackgroundBlur; + this.widgetConfig.config.settings.animation = config.animation; + this.widgetConfig.config.settings.background = config.background; this.setCardButtons(config.cardButtons, this.widgetConfig.config); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-widget-settings.component.html index 933961a7ae..42249a67cd 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-widget-settings.component.html @@ -157,6 +157,9 @@
+ +
{{ 'widgets.background.background' | translate }}
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-widget-settings.component.ts index 003b5005aa..e270b97137 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-widget-settings.component.ts @@ -138,6 +138,8 @@ export class TimeSeriesChartWidgetSettingsComponent extends WidgetSettingsCompon tooltipBackgroundColor: [settings.tooltipBackgroundColor, []], tooltipBackgroundBlur: [settings.tooltipBackgroundBlur, []], + animation: [settings.animation, []], + background: [settings.background, []] }); } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-animation-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-animation-settings.component.html new file mode 100644 index 0000000000..5e0ad8e2ec --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-animation-settings.component.html @@ -0,0 +1,89 @@ + + +
+ + + + + {{ 'widgets.time-series-chart.animation.animation' | translate }} + + + + +
+
{{ 'widgets.time-series-chart.animation.animation-threshold' | translate }}
+ + +
ms
+
+
+
+
{{ 'widgets.time-series-chart.animation.animation-duration' | translate }}
+ + +
ms
+
+
+
+
widgets.time-series-chart.animation.animation-easing
+ + + + {{ easing }} + + + +
+
+
{{ 'widgets.time-series-chart.animation.animation-delay' | translate }}
+ + +
ms
+
+
+
+
{{ 'widgets.time-series-chart.animation.update-animation-duration' | translate }}
+ + +
ms
+
+
+
+
widgets.time-series-chart.animation.update-animation-easing
+ + + + {{ easing }} + + + +
+
+
{{ 'widgets.time-series-chart.animation.update-animation-delay' | translate }}
+ + +
ms
+
+
+
+
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-animation-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-animation-settings.component.ts new file mode 100644 index 0000000000..5da5aa2fe9 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-animation-settings.component.ts @@ -0,0 +1,124 @@ +/// +/// 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, forwardRef, Input, OnInit } from '@angular/core'; +import { + ControlValueAccessor, + NG_VALUE_ACCESSOR, + UntypedFormBuilder, + UntypedFormGroup, + Validators +} from '@angular/forms'; +import { + timeSeriesChartAnimationEasings, + TimeSeriesChartAnimationSettings +} from '@home/components/widget/lib/chart/time-series-chart.models'; +import { WidgetService } from '@core/http/widget.service'; + +@Component({ + selector: 'tb-time-series-chart-animation-settings', + templateUrl: './time-series-chart-animation-settings.component.html', + styleUrls: ['./../../widget-settings.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => TimeSeriesChartAnimationSettingsComponent), + multi: true + } + ] +}) +export class TimeSeriesChartAnimationSettingsComponent implements OnInit, ControlValueAccessor { + + settingsExpanded = false; + + timeSeriesChartAnimationEasings = timeSeriesChartAnimationEasings; + + @Input() + disabled: boolean; + + private modelValue: TimeSeriesChartAnimationSettings; + + private propagateChange = null; + + public animationSettingsFormGroup: UntypedFormGroup; + + constructor(private fb: UntypedFormBuilder, + private widgetService: WidgetService,) { + } + + ngOnInit(): void { + this.animationSettingsFormGroup = this.fb.group({ + animation: [null, []], + animationThreshold: [null, [Validators.min(0)]], + animationDuration: [null, [Validators.min(0)]], + animationEasing: [null, []], + animationDelay: [null, [Validators.min(0)]], + animationDurationUpdate: [null, [Validators.min(0)]], + animationEasingUpdate: [null, []], + animationDelayUpdate: [null, [Validators.min(0)]] + }); + this.animationSettingsFormGroup.valueChanges.subscribe(() => { + this.updateModel(); + }); + this.animationSettingsFormGroup.get('animation').valueChanges + .subscribe(() => { + this.updateValidators(); + }); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(_fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (isDisabled) { + this.animationSettingsFormGroup.disable({emitEvent: false}); + } else { + this.animationSettingsFormGroup.enable({emitEvent: false}); + this.updateValidators(); + } + } + + writeValue(value: TimeSeriesChartAnimationSettings): void { + this.modelValue = value; + this.animationSettingsFormGroup.patchValue( + value, {emitEvent: false} + ); + this.updateValidators(); + this.animationSettingsFormGroup.get('animation').valueChanges.subscribe((animation) => { + this.settingsExpanded = animation; + }); + } + + private updateValidators() { + const animation: boolean = this.animationSettingsFormGroup.get('animation').value; + if (animation) { + this.animationSettingsFormGroup.enable({emitEvent: false}); + } else { + this.animationSettingsFormGroup.disable({emitEvent: false}); + this.animationSettingsFormGroup.get('animation').enable({emitEvent: false}); + } + } + + private updateModel() { + this.modelValue = this.animationSettingsFormGroup.getRawValue(); + this.propagateChange(this.modelValue); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget-settings-common.module.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget-settings-common.module.ts index 714da2a0c7..b80b20f402 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget-settings-common.module.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget-settings-common.module.ts @@ -118,6 +118,9 @@ import { import { TimeSeriesChartYAxisSettingsPanelComponent } from '@home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-settings-panel.component'; +import { + TimeSeriesChartAnimationSettingsComponent +} from '@home/components/widget/lib/settings/common/chart/time-series-chart-animation-settings.component'; @NgModule({ declarations: [ @@ -162,6 +165,7 @@ import { TimeSeriesChartYAxesPanelComponent, TimeSeriesChartYAxisRowComponent, TimeSeriesChartYAxisSettingsPanelComponent, + TimeSeriesChartAnimationSettingsComponent, DataKeyInputComponent, EntityAliasInputComponent ], @@ -212,6 +216,7 @@ import { TimeSeriesChartYAxesPanelComponent, TimeSeriesChartYAxisRowComponent, TimeSeriesChartYAxisSettingsPanelComponent, + TimeSeriesChartAnimationSettingsComponent, DataKeyInputComponent, EntityAliasInputComponent ], 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 830a54f88e..decb97384e 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -6696,6 +6696,16 @@ "show": "Show", "add-y-axis": "Add Y axis" }, + "animation": { + "animation": "Animation", + "animation-threshold": "Animation threshold", + "animation-duration": "Animation duration", + "animation-easing": "Animation easing", + "animation-delay": "Animation delay", + "update-animation-duration": "Update animation duration", + "update-animation-easing": "Update animation easing", + "update-animation-delay": "Update animation delay" + }, "series": { "legend-settings": "Legend settings", "show-in-legend": "Show in legend",