Browse Source

Merge remote-tracking branch 'upstream/master' into fix/apiUsageDashboard

pull/10326/head
Vladyslav_Prykhodko 3 years ago
parent
commit
00d5b37c3c
  1. 25
      dao/src/main/java/org/thingsboard/server/dao/attributes/CachedAttributesService.java
  2. 26
      dao/src/main/java/org/thingsboard/server/dao/service/Validator.java
  3. 43
      dao/src/main/java/org/thingsboard/server/dao/sqlts/SqlTimeseriesLatestDao.java
  4. 30
      dao/src/main/java/org/thingsboard/server/dao/timeseries/BaseTimeseriesService.java
  5. 4
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbAbstractGetAttributesNode.java
  6. 18
      ui-ngx/src/app/modules/home/components/widget/config/basic/chart/time-series-chart-basic-config.component.html
  7. 43
      ui-ngx/src/app/modules/home/components/widget/config/basic/chart/time-series-chart-basic-config.component.ts
  8. 39
      ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-key-row.component.html
  9. 17
      ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-key-row.component.scss
  10. 46
      ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-key-row.component.ts
  11. 5
      ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-keys-panel.component.html
  12. 15
      ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-keys-panel.component.scss
  13. 8
      ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-keys-panel.component.ts
  14. 30
      ui-ngx/src/app/modules/home/components/widget/lib/cards/aggregated-value-card-widget.component.ts
  15. 11
      ui-ngx/src/app/modules/home/components/widget/lib/cards/value-chart-card-widget.component.ts
  16. 216
      ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.models.ts
  17. 102
      ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.ts
  18. 12
      ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-key-settings.component.html
  19. 12
      ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-key-settings.component.ts
  20. 18
      ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-widget-settings.component.html
  21. 39
      ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-widget-settings.component.ts
  22. 89
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-animation-settings.component.html
  23. 124
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-animation-settings.component.ts
  24. 18
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-axis-settings.component.html
  25. 18
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-axis-settings.component.ts
  26. 28
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-threshold-row.component.html
  27. 14
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-threshold-row.component.scss
  28. 32
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-threshold-row.component.ts
  29. 10
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-threshold-settings-panel.component.html
  30. 6
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-threshold-settings-panel.component.ts
  31. 2
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-thresholds-panel.component.html
  32. 11
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-thresholds-panel.component.scss
  33. 5
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-thresholds-panel.component.ts
  34. 63
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axes-panel.component.html
  35. 87
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axes-panel.component.scss
  36. 180
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axes-panel.component.ts
  37. 72
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-row.component.html
  38. 95
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-row.component.scss
  39. 201
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-row.component.ts
  40. 43
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-settings-panel.component.html
  41. 49
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-settings-panel.component.scss
  42. 66
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-settings-panel.component.ts
  43. 20
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget-settings-common.module.ts
  44. 19
      ui-ngx/src/app/shared/models/common.ts
  45. 20
      ui-ngx/src/assets/locale/locale.constant-en_US.json

25
dao/src/main/java/org/thingsboard/server/dao/attributes/CachedAttributesService.java

@ -113,15 +113,15 @@ public class CachedAttributesService implements AttributesService {
validate(entityId, scope); validate(entityId, scope);
Validator.validateString(attributeKey, "Incorrect attribute key " + attributeKey); Validator.validateString(attributeKey, "Incorrect attribute key " + attributeKey);
AttributeCacheKey attributeCacheKey = new AttributeCacheKey(scope, entityId, attributeKey); return cacheExecutor.submit(() -> {
TbCacheValueWrapper<AttributeKvEntry> cachedAttributeValue = cache.get(attributeCacheKey); AttributeCacheKey attributeCacheKey = new AttributeCacheKey(scope, entityId, attributeKey);
if (cachedAttributeValue != null) { TbCacheValueWrapper<AttributeKvEntry> cachedAttributeValue = cache.get(attributeCacheKey);
hitCounter.increment(); if (cachedAttributeValue != null) {
AttributeKvEntry cachedAttributeKvEntry = cachedAttributeValue.get(); hitCounter.increment();
return Futures.immediateFuture(Optional.ofNullable(cachedAttributeKvEntry)); AttributeKvEntry cachedAttributeKvEntry = cachedAttributeValue.get();
} else { return Optional.ofNullable(cachedAttributeKvEntry);
missCounter.increment(); } else {
return cacheExecutor.submit(() -> { missCounter.increment();
var cacheTransaction = cache.newTransactionForKey(attributeCacheKey); var cacheTransaction = cache.newTransactionForKey(attributeCacheKey);
try { try {
Optional<AttributeKvEntry> result = attributesDao.find(tenantId, entityId, scope, attributeKey); Optional<AttributeKvEntry> 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); log.debug("Could not find attribute from cache: [{}] [{}] [{}]", entityId, scope, attributeKey, e);
throw e; throw e;
} }
}); }
} });
} }
@Override @Override
@ -207,7 +207,8 @@ public class CachedAttributesService implements AttributesService {
@Override @Override
public ListenableFuture<List<AttributeKvEntry>> findAll(TenantId tenantId, EntityId entityId, String scope) { public ListenableFuture<List<AttributeKvEntry>> findAll(TenantId tenantId, EntityId entityId, String scope) {
validate(entityId, 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 @Override

26
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.List;
import java.util.UUID; import java.util.UUID;
import java.util.function.Function;
import java.util.regex.Pattern; import java.util.regex.Pattern;
public class Validator { public class Validator {
@ -46,6 +47,19 @@ public class Validator {
} }
} }
/**
* This method validate <code>EntityId</code> entity id. If entity id is invalid than throw
* <code>IncorrectParameterException</code> exception
*
* @param entityId the entityId
* @param errorMessageFunction the error message for exception that apply entityId
*/
public static void validateEntityId(EntityId entityId, Function<EntityId, String> errorMessageFunction) {
if (entityId == null || entityId.getId() == null) {
throw new IncorrectParameterException(errorMessageFunction.apply(entityId));
}
}
/** /**
* This method validate <code>String</code> string. If string is invalid than throw * This method validate <code>String</code> string. If string is invalid than throw
* <code>IncorrectParameterException</code> exception * <code>IncorrectParameterException</code> exception
@ -59,6 +73,18 @@ public class Validator {
} }
} }
/*
* This method validate <code>String</code> string. If string is invalid than throw
* <code>IncorrectParameterException</code> exception
*
* @param val the value
* @param errorMessageFunction the error message function that apply value
*/
public static void validateString(String val, Function<String, String> errorMessageFunction) {
if (val == null || val.isEmpty()) {
throw new IncorrectParameterException(errorMessageFunction.apply(val));
}
}
/** /**
* This method validate <code>long</code> value. If value isn't positive than throw * This method validate <code>long</code> value. If value isn't positive than throw

43
dao/src/main/java/org/thingsboard/server/dao/sqlts/SqlTimeseriesLatestDao.java

@ -151,12 +151,12 @@ public class SqlTimeseriesLatestDao extends BaseAbstractSqlTimeseriesDao impleme
@Override @Override
public ListenableFuture<Optional<TsKvEntry>> findLatestOpt(TenantId tenantId, EntityId entityId, String key) { public ListenableFuture<Optional<TsKvEntry>> findLatestOpt(TenantId tenantId, EntityId entityId, String key) {
return Futures.immediateFuture(Optional.ofNullable(doFindLatest(entityId, key))); return service.submit(() -> Optional.ofNullable(doFindLatest(entityId, key)));
} }
@Override @Override
public ListenableFuture<TsKvEntry> findLatest(TenantId tenantId, EntityId entityId, String key) { public ListenableFuture<TsKvEntry> findLatest(TenantId tenantId, EntityId entityId, String key) {
return Futures.immediateFuture(getLatestTsKvEntry(entityId, key)); return service.submit(() -> getLatestTsKvEntry(entityId, key));
} }
@Override @Override
@ -221,36 +221,29 @@ public class SqlTimeseriesLatestDao extends BaseAbstractSqlTimeseriesDao impleme
} }
protected ListenableFuture<TsKvLatestRemovingResult> getRemoveLatestFuture(TenantId tenantId, EntityId entityId, DeleteTsKvQuery query) { protected ListenableFuture<TsKvLatestRemovingResult> getRemoveLatestFuture(TenantId tenantId, EntityId entityId, DeleteTsKvQuery query) {
TsKvEntry latest = doFindLatest(entityId, query.getKey()); ListenableFuture<TsKvEntry> latestFuture = service.submit(() -> doFindLatest(entityId, query.getKey()));
return Futures.transformAsync(latestFuture, latest -> {
if (latest == null) { if (latest == null) {
return Futures.immediateFuture(new TsKvLatestRemovingResult(query.getKey(), false)); return Futures.immediateFuture(new TsKvLatestRemovingResult(query.getKey(), false));
} }
boolean isRemoved = false;
long ts = latest.getTs(); long ts = latest.getTs();
ListenableFuture<Boolean> removedLatestFuture; if (ts >= query.getStartTs() && ts < query.getEndTs()) {
if (ts >= query.getStartTs() && ts < query.getEndTs()) { TsKvLatestEntity latestEntity = new TsKvLatestEntity();
TsKvLatestEntity latestEntity = new TsKvLatestEntity(); latestEntity.setEntityId(entityId.getId());
latestEntity.setEntityId(entityId.getId()); latestEntity.setKey(getOrSaveKeyId(query.getKey()));
latestEntity.setKey(getOrSaveKeyId(query.getKey()));
removedLatestFuture = service.submit(() -> {
tsKvLatestRepository.delete(latestEntity); tsKvLatestRepository.delete(latestEntity);
return true; isRemoved = true;
}); if (query.getRewriteLatestIfDeleted()) {
} else { return getNewLatestEntryFuture(tenantId, entityId, query);
removedLatestFuture = Futures.immediateFuture(false); }
}
return Futures.transformAsync(removedLatestFuture, isRemoved -> {
if (isRemoved && query.getRewriteLatestIfDeleted()) {
return getNewLatestEntryFuture(tenantId, entityId, query);
} }
return Futures.immediateFuture(new TsKvLatestRemovingResult(query.getKey(), isRemoved)); return Futures.immediateFuture(new TsKvLatestRemovingResult(query.getKey(), isRemoved));
}, MoreExecutors.directExecutor()); }, MoreExecutors.directExecutor());
} }
protected ListenableFuture<List<TsKvEntry>> getFindAllLatestFuture(EntityId entityId) { protected ListenableFuture<List<TsKvEntry>> getFindAllLatestFuture(EntityId entityId) {
return Futures.immediateFuture( return service.submit(() ->
DaoUtil.convertDataList(Lists.newArrayList( DaoUtil.convertDataList(Lists.newArrayList(
searchTsKvLatestRepository.findAllByEntityId(entityId.getId())))); searchTsKvLatestRepository.findAllByEntityId(entityId.getId()))));
} }

30
dao/src/main/java/org/thingsboard/server/dao/timeseries/BaseTimeseriesService.java

@ -16,7 +16,6 @@
package org.thingsboard.server.dao.timeseries; package org.thingsboard.server.dao.timeseries;
import com.google.common.base.Function; 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.Futures;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors; import com.google.common.util.concurrent.MoreExecutors;
@ -43,6 +42,7 @@ import org.thingsboard.server.dao.entityview.EntityViewService;
import org.thingsboard.server.dao.exception.IncorrectParameterException; import org.thingsboard.server.dao.exception.IncorrectParameterException;
import org.thingsboard.server.dao.service.Validator; import org.thingsboard.server.dao.service.Validator;
import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
@ -126,18 +126,22 @@ public class BaseTimeseriesService implements TimeseriesService {
@Override @Override
public ListenableFuture<List<TsKvEntry>> findLatest(TenantId tenantId, EntityId entityId, Collection<String> keys) { public ListenableFuture<List<TsKvEntry>> findLatest(TenantId tenantId, EntityId entityId, Collection<String> keys) {
validate(entityId); validate(entityId);
List<ListenableFuture<TsKvEntry>> futures = Lists.newArrayListWithExpectedSize(keys.size()); List<ListenableFuture<TsKvEntry>> futures = new ArrayList<>(keys.size());
keys.forEach(key -> Validator.validateString(key, "Incorrect key " + key)); keys.forEach(key -> Validator.validateString(key, k -> "Incorrect key " + k));
keys.forEach(key -> futures.add(timeseriesLatestDao.findLatest(tenantId, entityId, key))); for (String key : keys) {
futures.add(timeseriesLatestDao.findLatest(tenantId, entityId, key));
}
return Futures.allAsList(futures); return Futures.allAsList(futures);
} }
@Override @Override
public List<TsKvEntry> findLatestSync(TenantId tenantId, EntityId entityId, Collection<String> keys) { public List<TsKvEntry> findLatestSync(TenantId tenantId, EntityId entityId, Collection<String> keys) {
validate(entityId); validate(entityId);
List<TsKvEntry> latestEntries = Lists.newArrayListWithExpectedSize(keys.size()); List<TsKvEntry> latestEntries = new ArrayList<>(keys.size());
keys.forEach(key -> Validator.validateString(key, "Incorrect key " + key)); keys.forEach(key -> Validator.validateString(key, k -> "Incorrect key " + k));
keys.forEach(key -> latestEntries.add(timeseriesLatestDao.findLatestSync(tenantId, entityId, key))); for (String key : keys) {
latestEntries.add(timeseriesLatestDao.findLatestSync(tenantId, entityId, key));
}
return latestEntries; return latestEntries;
} }
@ -165,7 +169,7 @@ public class BaseTimeseriesService implements TimeseriesService {
@Override @Override
public ListenableFuture<Integer> save(TenantId tenantId, EntityId entityId, TsKvEntry tsKvEntry) { public ListenableFuture<Integer> save(TenantId tenantId, EntityId entityId, TsKvEntry tsKvEntry) {
validate(entityId); validate(entityId);
List<ListenableFuture<Integer>> futures = Lists.newArrayListWithExpectedSize(INSERTS_PER_ENTRY); List<ListenableFuture<Integer>> futures = new ArrayList<>(INSERTS_PER_ENTRY);
saveAndRegisterFutures(tenantId, futures, entityId, tsKvEntry, 0L); saveAndRegisterFutures(tenantId, futures, entityId, tsKvEntry, 0L);
return Futures.transform(Futures.allAsList(futures), SUM_ALL_INTEGERS, MoreExecutors.directExecutor()); return Futures.transform(Futures.allAsList(futures), SUM_ALL_INTEGERS, MoreExecutors.directExecutor());
} }
@ -182,7 +186,7 @@ public class BaseTimeseriesService implements TimeseriesService {
private ListenableFuture<Integer> doSave(TenantId tenantId, EntityId entityId, List<TsKvEntry> tsKvEntries, long ttl, boolean saveLatest) { private ListenableFuture<Integer> doSave(TenantId tenantId, EntityId entityId, List<TsKvEntry> tsKvEntries, long ttl, boolean saveLatest) {
int inserts = saveLatest ? INSERTS_PER_ENTRY : INSERTS_PER_ENTRY_WITHOUT_LATEST; int inserts = saveLatest ? INSERTS_PER_ENTRY : INSERTS_PER_ENTRY_WITHOUT_LATEST;
List<ListenableFuture<Integer>> futures = Lists.newArrayListWithExpectedSize(tsKvEntries.size() * inserts); List<ListenableFuture<Integer>> futures = new ArrayList<>(tsKvEntries.size() * inserts);
for (TsKvEntry tsKvEntry : tsKvEntries) { for (TsKvEntry tsKvEntry : tsKvEntries) {
if (saveLatest) { if (saveLatest) {
saveAndRegisterFutures(tenantId, futures, entityId, tsKvEntry, ttl); saveAndRegisterFutures(tenantId, futures, entityId, tsKvEntry, ttl);
@ -195,7 +199,7 @@ public class BaseTimeseriesService implements TimeseriesService {
@Override @Override
public ListenableFuture<List<Void>> saveLatest(TenantId tenantId, EntityId entityId, List<TsKvEntry> tsKvEntries) { public ListenableFuture<List<Void>> saveLatest(TenantId tenantId, EntityId entityId, List<TsKvEntry> tsKvEntries) {
List<ListenableFuture<Void>> futures = Lists.newArrayListWithExpectedSize(tsKvEntries.size()); List<ListenableFuture<Void>> futures = new ArrayList<>(tsKvEntries.size());
for (TsKvEntry tsKvEntry : tsKvEntries) { for (TsKvEntry tsKvEntry : tsKvEntries) {
futures.add(timeseriesLatestDao.saveLatest(tenantId, entityId, tsKvEntry)); futures.add(timeseriesLatestDao.saveLatest(tenantId, entityId, tsKvEntry));
} }
@ -242,7 +246,7 @@ public class BaseTimeseriesService implements TimeseriesService {
public ListenableFuture<List<TsKvLatestRemovingResult>> remove(TenantId tenantId, EntityId entityId, List<DeleteTsKvQuery> deleteTsKvQueries) { public ListenableFuture<List<TsKvLatestRemovingResult>> remove(TenantId tenantId, EntityId entityId, List<DeleteTsKvQuery> deleteTsKvQueries) {
validate(entityId); validate(entityId);
deleteTsKvQueries.forEach(BaseTimeseriesService::validate); deleteTsKvQueries.forEach(BaseTimeseriesService::validate);
List<ListenableFuture<TsKvLatestRemovingResult>> futures = Lists.newArrayListWithExpectedSize(deleteTsKvQueries.size() * DELETES_PER_ENTRY); List<ListenableFuture<TsKvLatestRemovingResult>> futures = new ArrayList<>(deleteTsKvQueries.size() * DELETES_PER_ENTRY);
for (DeleteTsKvQuery tsKvQuery : deleteTsKvQueries) { for (DeleteTsKvQuery tsKvQuery : deleteTsKvQueries) {
deleteAndRegisterFutures(tenantId, futures, entityId, tsKvQuery); deleteAndRegisterFutures(tenantId, futures, entityId, tsKvQuery);
} }
@ -252,7 +256,7 @@ public class BaseTimeseriesService implements TimeseriesService {
@Override @Override
public ListenableFuture<List<TsKvLatestRemovingResult>> removeLatest(TenantId tenantId, EntityId entityId, Collection<String> keys) { public ListenableFuture<List<TsKvLatestRemovingResult>> removeLatest(TenantId tenantId, EntityId entityId, Collection<String> keys) {
validate(entityId); validate(entityId);
List<ListenableFuture<TsKvLatestRemovingResult>> futures = Lists.newArrayListWithExpectedSize(keys.size()); List<ListenableFuture<TsKvLatestRemovingResult>> futures = new ArrayList<>(keys.size());
for (String key : keys) { for (String key : keys) {
DeleteTsKvQuery query = new BaseDeleteTsKvQuery(key, 0, System.currentTimeMillis(), false); DeleteTsKvQuery query = new BaseDeleteTsKvQuery(key, 0, System.currentTimeMillis(), false);
futures.add(timeseriesLatestDao.removeLatest(tenantId, entityId, query)); futures.add(timeseriesLatestDao.removeLatest(tenantId, entityId, query));
@ -281,7 +285,7 @@ public class BaseTimeseriesService implements TimeseriesService {
} }
private static void validate(EntityId entityId) { private static void validate(EntityId entityId) {
Validator.validateEntityId(entityId, "Incorrect entityId " + entityId); Validator.validateEntityId(entityId, id -> "Incorrect entityId " + id);
} }
private void validate(ReadTsKvQuery query) { private void validate(ReadTsKvQuery query) {

4
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbAbstractGetAttributesNode.java

@ -99,10 +99,10 @@ public abstract class TbAbstractGetAttributesNode<C extends TbGetAttributesNodeC
private void safePutAttributes(TbContext ctx, TbMsg msg, ObjectNode msgDataNode, T entityId) { private void safePutAttributes(TbContext ctx, TbMsg msg, ObjectNode msgDataNode, T entityId) {
Set<TbPair<String, List<String>>> failuresPairSet = ConcurrentHashMap.newKeySet(); Set<TbPair<String, List<String>>> failuresPairSet = ConcurrentHashMap.newKeySet();
var getKvEntryPairFutures = Futures.allAsList( 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, CLIENT_SCOPE, TbNodeUtils.processPatterns(config.getClientAttributeNames(), msg), failuresPairSet),
getAttrAsync(ctx, entityId, SHARED_SCOPE, TbNodeUtils.processPatterns(config.getSharedAttributeNames(), 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 -> { withCallback(getKvEntryPairFutures, futuresList -> {
var msgMetaData = msg.getMetaData().copy(); var msgMetaData = msg.getMetaData().copy();

18
ui-ngx/src/app/modules/home/components/widget/config/basic/chart/time-series-chart-basic-config.component.html

@ -25,6 +25,10 @@
forceSingleDatasource forceSingleDatasource
formControlName="datasources"> formControlName="datasources">
</tb-datasources> </tb-datasources>
<tb-time-series-chart-y-axes-panel
formControlName="yAxes"
(axisRemoved)="yAxisRemoved($event)">
</tb-time-series-chart-y-axes-panel>
<tb-data-keys-panel <tb-data-keys-panel
panelTitle="{{ 'widgets.chart.series' | translate }}" panelTitle="{{ 'widgets.chart.series' | translate }}"
addKeyTitle="{{ 'widgets.chart.add-series' | translate }}" addKeyTitle="{{ 'widgets.chart.add-series' | translate }}"
@ -32,6 +36,8 @@
removeKeyTitle="{{ 'widgets.chart.remove-series' | translate }}" removeKeyTitle="{{ 'widgets.chart.remove-series' | translate }}"
noKeysText="{{ 'widgets.chart.no-series' | translate }}" noKeysText="{{ 'widgets.chart.no-series' | translate }}"
requiredKeysText="{{ 'widgets.chart.no-series-error' | translate }}" requiredKeysText="{{ 'widgets.chart.no-series-error' | translate }}"
timeSeriesChart
[yAxisIds]="yAxisIds"
[showTimeSeriesType]="chartType === TimeSeriesChartType.default" [showTimeSeriesType]="chartType === TimeSeriesChartType.default"
hideSourceSelection hideSourceSelection
[datasourceType]="datasource?.type" [datasourceType]="datasource?.type"
@ -44,7 +50,8 @@
[aliasController]="aliasController" [aliasController]="aliasController"
[dataKeyCallbacks]="callbacks" [dataKeyCallbacks]="callbacks"
[datasource]="datasource" [datasource]="datasource"
[widgetConfig]="widgetConfig?.config"> [widgetConfig]="widgetConfig?.config"
[yAxisIds]="yAxisIds">
</tb-time-series-chart-thresholds-panel> </tb-time-series-chart-thresholds-panel>
<div class="tb-form-panel"> <div class="tb-form-panel">
<div class="tb-form-panel-title" translate>widget-config.appearance</div> <div class="tb-form-panel-title" translate>widget-config.appearance</div>
@ -104,11 +111,7 @@
</div> </div>
</div> </div>
<div class="tb-form-panel"> <div class="tb-form-panel">
<div class="tb-form-panel-title" translate>widgets.time-series-chart.axis.axes</div> <div class="tb-form-panel-title" translate>widgets.time-series-chart.axis.x-axis</div>
<tb-time-series-chart-axis-settings
formControlName="yAxis"
axisType="yAxis">
</tb-time-series-chart-axis-settings>
<tb-time-series-chart-axis-settings <tb-time-series-chart-axis-settings
formControlName="xAxis" formControlName="xAxis"
axisType="xAxis"> axisType="xAxis">
@ -220,6 +223,9 @@
</ng-template> </ng-template>
</mat-expansion-panel> </mat-expansion-panel>
</div> </div>
<tb-time-series-chart-animation-settings
formControlName="animation">
</tb-time-series-chart-animation-settings>
<div class="tb-form-panel"> <div class="tb-form-panel">
<div class="tb-form-panel-title" translate>widget-config.card-appearance</div> <div class="tb-form-panel-title" translate>widget-config.card-appearance</div>
<div class="tb-form-row space-between"> <div class="tb-form-row space-between">

43
ui-ngx/src/app/modules/home/components/widget/config/basic/chart/time-series-chart-basic-config.component.ts

@ -45,7 +45,11 @@ import {
TimeSeriesChartWidgetSettings TimeSeriesChartWidgetSettings
} from '@home/components/widget/lib/chart/time-series-chart-widget.models'; } from '@home/components/widget/lib/chart/time-series-chart-widget.models';
import { EChartsTooltipTrigger } from '@home/components/widget/lib/chart/echarts-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({ @Component({
selector: 'tb-time-series-chart-basic-config', 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; TimeSeriesChartType = TimeSeriesChartType;
EChartsTooltipTrigger = EChartsTooltipTrigger; EChartsTooltipTrigger = EChartsTooltipTrigger;
@ -86,6 +95,15 @@ export class TimeSeriesChartBasicConfigComponent extends BasicWidgetConfigCompon
super(store, widgetConfigComponent); 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 { protected configForm(): UntypedFormGroup {
return this.timeSeriesChartWidgetConfigForm; return this.timeSeriesChartWidgetConfigForm;
} }
@ -109,6 +127,8 @@ export class TimeSeriesChartBasicConfigComponent extends BasicWidgetConfigCompon
this.timeSeriesChartWidgetConfigForm = this.fb.group({ this.timeSeriesChartWidgetConfigForm = this.fb.group({
timewindowConfig: [getTimewindowConfig(configData.config), []], timewindowConfig: [getTimewindowConfig(configData.config), []],
datasources: [configData.config.datasources, []], datasources: [configData.config.datasources, []],
yAxes: [settings.yAxes, []],
series: [this.getSeries(configData.config.datasources), []], series: [this.getSeries(configData.config.datasources), []],
thresholds: [settings.thresholds, []], thresholds: [settings.thresholds, []],
@ -126,7 +146,6 @@ export class TimeSeriesChartBasicConfigComponent extends BasicWidgetConfigCompon
dataZoom: [settings.dataZoom, []], dataZoom: [settings.dataZoom, []],
stack: [settings.stack, []], stack: [settings.stack, []],
yAxis: [settings.yAxis, []],
xAxis: [settings.xAxis, []], xAxis: [settings.xAxis, []],
noAggregationBarWidthSettings: [settings.noAggregationBarWidthSettings, []], noAggregationBarWidthSettings: [settings.noAggregationBarWidthSettings, []],
@ -149,6 +168,8 @@ export class TimeSeriesChartBasicConfigComponent extends BasicWidgetConfigCompon
tooltipBackgroundColor: [settings.tooltipBackgroundColor, []], tooltipBackgroundColor: [settings.tooltipBackgroundColor, []],
tooltipBackgroundBlur: [settings.tooltipBackgroundBlur, []], tooltipBackgroundBlur: [settings.tooltipBackgroundBlur, []],
animation: [settings.animation, []],
background: [settings.background, []], background: [settings.background, []],
cardButtons: [this.getCardButtons(configData.config), []], cardButtons: [this.getCardButtons(configData.config), []],
@ -180,7 +201,7 @@ export class TimeSeriesChartBasicConfigComponent extends BasicWidgetConfigCompon
this.widgetConfig.config.settings.dataZoom = config.dataZoom; this.widgetConfig.config.settings.dataZoom = config.dataZoom;
this.widgetConfig.config.settings.stack = config.stack; 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.xAxis = config.xAxis;
this.widgetConfig.config.settings.noAggregationBarWidthSettings = config.noAggregationBarWidthSettings; this.widgetConfig.config.settings.noAggregationBarWidthSettings = config.noAggregationBarWidthSettings;
@ -202,6 +223,8 @@ export class TimeSeriesChartBasicConfigComponent extends BasicWidgetConfigCompon
this.widgetConfig.config.settings.tooltipBackgroundColor = config.tooltipBackgroundColor; this.widgetConfig.config.settings.tooltipBackgroundColor = config.tooltipBackgroundColor;
this.widgetConfig.config.settings.tooltipBackgroundBlur = config.tooltipBackgroundBlur; this.widgetConfig.config.settings.tooltipBackgroundBlur = config.tooltipBackgroundBlur;
this.widgetConfig.config.settings.animation = config.animation;
this.widgetConfig.config.settings.background = config.background; this.widgetConfig.config.settings.background = config.background;
this.setCardButtons(config.cardButtons, this.widgetConfig.config); this.setCardButtons(config.cardButtons, this.widgetConfig.config);
@ -303,6 +326,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[] { private getCardButtons(config: WidgetConfig): string[] {
const buttons: string[] = []; const buttons: string[] = [];
if (isUndefined(config.enableFullscreen) || config.enableFullscreen) { if (isUndefined(config.enableFullscreen) || config.enableFullscreen) {

39
ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-key-row.component.html

@ -38,6 +38,30 @@
<mat-form-field *ngIf="!hideDataKeyLabel" class="tb-inline-field tb-label-field" appearance="outline" subscriptSizing="dynamic"> <mat-form-field *ngIf="!hideDataKeyLabel" class="tb-inline-field tb-label-field" appearance="outline" subscriptSizing="dynamic">
<input matInput formControlName="label" placeholder="{{ 'widget-config.set' | translate }}"> <input matInput formControlName="label" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field> </mat-form-field>
<div *ngIf="timeSeriesChart && showTimeSeriesType" class="tb-time-series-type-field">
<mat-form-field *ngIf="displayUnitsOrDigits" class="tb-inline-field fixed-height" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="timeSeriesType" panelClass="tb-time-series-type-field">
<mat-select-trigger>
<tb-icon *ngIf="keyRowFormGroup.get('timeSeriesType').value">
{{ timeSeriesChartSeriesTypeIcons.get(keyRowFormGroup.get('timeSeriesType').value) }}
</tb-icon>
</mat-select-trigger>
<mat-option *ngFor="let type of timeSeriesChartSeriesTypes" [value]="type" class="flex">
<tb-icon>{{ timeSeriesChartSeriesTypeIcons.get(type) }}</tb-icon>
<span>{{ timeSeriesChartSeriesTypeTranslations.get(type) | translate }}</span>
</mat-option>
</mat-select>
</mat-form-field>
</div>
<div *ngIf="timeSeriesChart" class="tb-y-axis-field">
<mat-form-field *ngIf="displayUnitsOrDigits" class="tb-inline-field fixed-height" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="yAxis">
<mat-option *ngFor="let yAxis of yAxisIds" [value]="yAxis">
{{ yAxis }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
<div *ngIf="!hideDataKeyColor" class="tb-color-field"> <div *ngIf="!hideDataKeyColor" class="tb-color-field">
<tb-color-input asBoxInput <tb-color-input asBoxInput
formControlName="color"> formControlName="color">
@ -54,21 +78,6 @@
<div *ngIf="singleRow" matSuffix fxHide.lt-md translate>widget-config.decimals-suffix</div> <div *ngIf="singleRow" matSuffix fxHide.lt-md translate>widget-config.decimals-suffix</div>
</mat-form-field> </mat-form-field>
</div> </div>
<div *ngIf="showTimeSeriesType" class="tb-time-series-type-field">
<mat-form-field *ngIf="displayUnitsOrDigits" class="tb-inline-field fixed-height" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="timeSeriesType" panelClass="tb-time-series-type-field">
<mat-select-trigger>
<tb-icon *ngIf="keyRowFormGroup.get('timeSeriesType').value">
{{ timeSeriesChartSeriesTypeIcons.get(keyRowFormGroup.get('timeSeriesType').value) }}
</tb-icon>
</mat-select-trigger>
<mat-option *ngFor="let type of timeSeriesChartSeriesTypes" [value]="type" class="flex">
<tb-icon>{{ timeSeriesChartSeriesTypeIcons.get(type) }}</tb-icon>
<span>{{ timeSeriesChartSeriesTypeTranslations.get(type) | translate }}</span>
</mat-option>
</mat-select>
</mat-form-field>
</div>
<div *ngIf="!singleRow" class="tb-form-table-row-cell-buttons"> <div *ngIf="!singleRow" class="tb-form-table-row-cell-buttons">
<div fxHide.lt-lg class="tb-settings-button"> <div fxHide.lt-lg class="tb-settings-button">
<button *ngIf="modelValue" <button *ngIf="modelValue"

17
ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-key-row.component.scss

@ -57,6 +57,14 @@
min-width: 60px; min-width: 60px;
} }
.tb-y-axis-field {
width: 100px;
min-width: 100px;
.mat-mdc-form-field {
width: 100%;
}
}
.tb-time-series-type-field { .tb-time-series-type-field {
width: 60px; width: 60px;
min-width: 60px; min-width: 60px;
@ -72,7 +80,16 @@
} }
} }
.tb-y-axis-field {
@media #{$mat-md} {
display: none;
}
}
.tb-time-series-type-field { .tb-time-series-type-field {
@media #{$mat-xs} {
display: none;
}
@media #{$mat-md} { @media #{$mat-md} {
display: none; display: none;
} }

46
ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-key-row.component.ts

@ -19,9 +19,9 @@ import {
Component, Component,
EventEmitter, EventEmitter,
forwardRef, forwardRef,
Input, Input, OnChanges,
OnInit, OnInit,
Output, Output, SimpleChanges,
ViewEncapsulation ViewEncapsulation
} from '@angular/core'; } from '@angular/core';
import { import {
@ -60,7 +60,7 @@ import {
TimeSeriesChartSeriesType, TimeSeriesChartSeriesType,
timeSeriesChartSeriesTypeIcons, timeSeriesChartSeriesTypeIcons,
timeSeriesChartSeriesTypes, timeSeriesChartSeriesTypes,
timeSeriesChartSeriesTypeTranslations timeSeriesChartSeriesTypeTranslations, TimeSeriesChartYAxisId
} from '@home/components/widget/lib/chart/time-series-chart.models'; } from '@home/components/widget/lib/chart/time-series-chart.models';
export const dataKeyValid = (key: DataKey): boolean => !!key && !!key.type && !!key.name; export const dataKeyValid = (key: DataKey): boolean => !!key && !!key.type && !!key.name;
@ -88,7 +88,7 @@ export const dataKeyRowValidator = (control: AbstractControl): ValidationErrors
], ],
encapsulation: ViewEncapsulation.None encapsulation: ViewEncapsulation.None
}) })
export class DataKeyRowComponent implements ControlValueAccessor, OnInit { export class DataKeyRowComponent implements ControlValueAccessor, OnInit, OnChanges {
timeSeriesChartSeriesTypes = timeSeriesChartSeriesTypes; timeSeriesChartSeriesTypes = timeSeriesChartSeriesTypes;
timeSeriesChartSeriesTypeTranslations = timeSeriesChartSeriesTypeTranslations; timeSeriesChartSeriesTypeTranslations = timeSeriesChartSeriesTypeTranslations;
@ -138,10 +138,17 @@ export class DataKeyRowComponent implements ControlValueAccessor, OnInit {
@coerceBoolean() @coerceBoolean()
hideDecimals = false; hideDecimals = false;
@Input()
@coerceBoolean()
timeSeriesChart = false;
@Input() @Input()
@coerceBoolean() @coerceBoolean()
showTimeSeriesType = false; showTimeSeriesType = false;
@Input()
yAxisIds: TimeSeriesChartYAxisId[];
@Input() @Input()
@coerceBoolean() @coerceBoolean()
singleRow = true; singleRow = true;
@ -237,7 +244,8 @@ export class DataKeyRowComponent implements ControlValueAccessor, OnInit {
if (this.hasAdditionalLatestDataKeys) { if (this.hasAdditionalLatestDataKeys) {
this.keyRowFormGroup.addControl('latest', this.fb.control(false)); this.keyRowFormGroup.addControl('latest', this.fb.control(false));
} }
if (this.showTimeSeriesType) { if (this.timeSeriesChart) {
this.keyRowFormGroup.addControl('yAxis', this.fb.control(null));
this.keyRowFormGroup.addControl('timeSeriesType', this.fb.control(null)); this.keyRowFormGroup.addControl('timeSeriesType', this.fb.control(null));
} }
merge(this.keyFormControl.valueChanges, this.keyRowFormGroup.valueChanges).subscribe( merge(this.keyFormControl.valueChanges, this.keyRowFormGroup.valueChanges).subscribe(
@ -245,6 +253,20 @@ export class DataKeyRowComponent implements ControlValueAccessor, OnInit {
); );
} }
ngOnChanges(changes: SimpleChanges): void {
for (const propName of Object.keys(changes)) {
const change = changes[propName];
if (!change.firstChange && change.currentValue !== change.previousValue) {
if (this.timeSeriesChart && ['yAxisIds'].includes(propName)) {
if (this.modelValue?.settings?.yAxisId &&
!this.yAxisIds.includes(this.modelValue.settings.yAxisId)) {
this.keyRowFormGroup.patchValue({yAxis: 'default'}, {emitEvent: true});
}
}
}
}
}
registerOnChange(fn: any): void { registerOnChange(fn: any): void {
this.propagateChange = fn; this.propagateChange = fn;
} }
@ -278,10 +300,12 @@ export class DataKeyRowComponent implements ControlValueAccessor, OnInit {
latest: (value as any)?.latest latest: (value as any)?.latest
}, {emitEvent: false}); }, {emitEvent: false});
} }
if (this.showTimeSeriesType) { if (this.timeSeriesChart) {
const settings = value?.settings as TimeSeriesChartKeySettings; const settings = value?.settings as TimeSeriesChartKeySettings;
const yAxis = settings?.yAxisId || 'default';
const timeSeriesType = settings?.type || TimeSeriesChartSeriesType.line; const timeSeriesType = settings?.type || TimeSeriesChartSeriesType.line;
this.keyRowFormGroup.patchValue({ this.keyRowFormGroup.patchValue({
yAxis,
timeSeriesType timeSeriesType
}, {emitEvent: false}); }, {emitEvent: false});
} }
@ -319,7 +343,8 @@ export class DataKeyRowComponent implements ControlValueAccessor, OnInit {
this.keyRowFormGroup.get('color').patchValue(this.modelValue.color, {emitEvent: false}); this.keyRowFormGroup.get('color').patchValue(this.modelValue.color, {emitEvent: false});
this.keyRowFormGroup.get('units').patchValue(this.modelValue.units, {emitEvent: false}); this.keyRowFormGroup.get('units').patchValue(this.modelValue.units, {emitEvent: false});
this.keyRowFormGroup.get('decimals').patchValue(this.modelValue.decimals, {emitEvent: false}); this.keyRowFormGroup.get('decimals').patchValue(this.modelValue.decimals, {emitEvent: false});
if (this.showTimeSeriesType) { if (this.timeSeriesChart) {
this.keyRowFormGroup.get('yAxis').patchValue(this.modelValue.settings?.yAxisId, {emitEvent: false});
this.keyRowFormGroup.get('timeSeriesType').patchValue(this.modelValue.settings?.type, {emitEvent: false}); this.keyRowFormGroup.get('timeSeriesType').patchValue(this.modelValue.settings?.type, {emitEvent: false});
} }
this.keyFormControl.patchValue(deepClone(this.modelValue), {emitEvent: false}); this.keyFormControl.patchValue(deepClone(this.modelValue), {emitEvent: false});
@ -335,7 +360,8 @@ export class DataKeyRowComponent implements ControlValueAccessor, OnInit {
if (!this.keyRowFormGroup.get('label').value) { if (!this.keyRowFormGroup.get('label').value) {
this.keyRowFormGroup.get('label').patchValue(key.label, {emitEvent: false}); this.keyRowFormGroup.get('label').patchValue(key.label, {emitEvent: false});
} }
if (this.showTimeSeriesType) { if (this.timeSeriesChart) {
this.keyRowFormGroup.get('yAxis').patchValue(key.settings?.yAxisId, {emitEvent: false});
this.keyRowFormGroup.get('timeSeriesType').patchValue(key.settings?.type, {emitEvent: false}); this.keyRowFormGroup.get('timeSeriesType').patchValue(key.settings?.type, {emitEvent: false});
} }
return key; return key;
@ -346,8 +372,10 @@ export class DataKeyRowComponent implements ControlValueAccessor, OnInit {
if (this.modelValue !== null) { if (this.modelValue !== null) {
const value: DataKey = this.keyRowFormGroup.value; const value: DataKey = this.keyRowFormGroup.value;
this.modelValue = {...this.modelValue, ...value}; this.modelValue = {...this.modelValue, ...value};
if (this.showTimeSeriesType) { if (this.timeSeriesChart) {
this.modelValue.settings.yAxisId = (this.modelValue as any).yAxis;
this.modelValue.settings.type = (this.modelValue as any).timeSeriesType; this.modelValue.settings.type = (this.modelValue as any).timeSeriesType;
delete (this.modelValue as any).yAxis;
delete (this.modelValue as any).timeSeriesType; delete (this.modelValue as any).timeSeriesType;
} }
} }

5
ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-keys-panel.component.html

@ -22,10 +22,11 @@
<div *ngIf="hasAdditionalLatestDataKeys" class="tb-form-table-header-cell tb-source-header" translate>datakey.source</div> <div *ngIf="hasAdditionalLatestDataKeys" class="tb-form-table-header-cell tb-source-header" translate>datakey.source</div>
<div class="tb-form-table-header-cell tb-key-header" translate>datakey.key</div> <div class="tb-form-table-header-cell tb-key-header" translate>datakey.key</div>
<div class="tb-form-table-header-cell tb-label-header" translate>datakey.label</div> <div class="tb-form-table-header-cell tb-label-header" translate>datakey.label</div>
<div *ngIf="timeSeriesChart && showTimeSeriesType" class="tb-form-table-header-cell tb-time-series-type-header" translate>widgets.time-series-chart.series.type</div>
<div *ngIf="timeSeriesChart" class="tb-form-table-header-cell tb-y-axis-header" translate>widgets.time-series-chart.axis.y-axis</div>
<div *ngIf="!hideDataKeyColor" class="tb-form-table-header-cell tb-color-header" translate>datakey.color</div> <div *ngIf="!hideDataKeyColor" class="tb-form-table-header-cell tb-color-header" translate>datakey.color</div>
<div *ngIf="!hideUnits" class="tb-form-table-header-cell tb-units-header" translate>widget-config.units-short</div> <div *ngIf="!hideUnits" class="tb-form-table-header-cell tb-units-header" translate>widget-config.units-short</div>
<div *ngIf="!hideDecimals" class="tb-form-table-header-cell tb-decimals-header" translate>widget-config.decimals-short</div> <div *ngIf="!hideDecimals" class="tb-form-table-header-cell tb-decimals-header" translate>widget-config.decimals-short</div>
<div *ngIf="showTimeSeriesType" class="tb-form-table-header-cell tb-time-series-type-header" translate>widgets.time-series-chart.series.type</div>
<div class="tb-form-table-header-cell tb-actions-header"></div> <div class="tb-form-table-header-cell tb-actions-header"></div>
</div> </div>
<div *ngIf="keysFormArray().controls.length; else noKeys" class="tb-form-table-body tb-drop-list" <div *ngIf="keysFormArray().controls.length; else noKeys" class="tb-form-table-body tb-drop-list"
@ -44,7 +45,9 @@
[hasAdditionalLatestDataKeys]="hasAdditionalLatestDataKeys" [hasAdditionalLatestDataKeys]="hasAdditionalLatestDataKeys"
[hideDataKeyColor]="hideDataKeyColor" [hideDataKeyColor]="hideDataKeyColor"
[hideDecimals]="hideDecimals" [hideDecimals]="hideDecimals"
[timeSeriesChart]="timeSeriesChart"
[showTimeSeriesType]="showTimeSeriesType" [showTimeSeriesType]="showTimeSeriesType"
[yAxisIds]="yAxisIds"
[hideUnits]="hideUnits" [hideUnits]="hideUnits"
[hideDataKeyDecimals]="hideDataKeyDecimals" [hideDataKeyDecimals]="hideDataKeyDecimals"
[hideDataKeyUnits]="hideDataKeyUnits" [hideDataKeyUnits]="hideDataKeyUnits"

15
ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-keys-panel.component.scss

@ -50,6 +50,11 @@
min-width: 60px; min-width: 60px;
} }
&.tb-y-axis-header {
width: 100px;
min-width: 100px;
}
&.tb-time-series-type-header { &.tb-time-series-type-header {
width: 60px; width: 60px;
min-width: 60px; min-width: 60px;
@ -70,7 +75,17 @@
display: block; display: block;
} }
} }
&.tb-y-axis-header {
@media #{$mat-md} {
display: none;
}
}
&.tb-time-series-type-header { &.tb-time-series-type-header {
@media #{$mat-xs} {
display: none;
}
@media #{$mat-md} { @media #{$mat-md} {
display: none; display: none;
} }

8
ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-keys-panel.component.ts

@ -44,6 +44,7 @@ import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
import { UtilsService } from '@core/services/utils.service'; import { UtilsService } from '@core/services/utils.service';
import { DataKeysCallbacks, DataKeySettingsFunction } from '@home/components/widget/config/data-keys.component.models'; import { DataKeysCallbacks, DataKeySettingsFunction } from '@home/components/widget/config/data-keys.component.models';
import { coerceBoolean } from '@shared/decorators/coercion'; import { coerceBoolean } from '@shared/decorators/coercion';
import { TimeSeriesChartYAxisId } from '@home/components/widget/lib/chart/time-series-chart.models';
@Component({ @Component({
selector: 'tb-data-keys-panel', selector: 'tb-data-keys-panel',
@ -119,10 +120,17 @@ export class DataKeysPanelComponent implements ControlValueAccessor, OnInit, OnC
@coerceBoolean() @coerceBoolean()
hideSourceSelection = false; hideSourceSelection = false;
@Input()
@coerceBoolean()
timeSeriesChart = false;
@Input() @Input()
@coerceBoolean() @coerceBoolean()
showTimeSeriesType = false; showTimeSeriesType = false;
@Input()
yAxisIds: TimeSeriesChartYAxisId[];
dataKeyType: DataKeyType; dataKeyType: DataKeyType;
keysListFormGroup: UntypedFormGroup; keysListFormGroup: UntypedFormGroup;

30
ui-ngx/src/app/modules/home/components/widget/lib/cards/aggregated-value-card-widget.component.ts

@ -54,10 +54,12 @@ import { ImagePipe } from '@shared/pipe/image.pipe';
import { DomSanitizer } from '@angular/platform-browser'; import { DomSanitizer } from '@angular/platform-browser';
import { TbTimeSeriesChart } from '@home/components/widget/lib/chart/time-series-chart'; import { TbTimeSeriesChart } from '@home/components/widget/lib/chart/time-series-chart';
import { import {
TimeSeriesChartAxisSettings,
TimeSeriesChartKeySettings, TimeSeriesChartKeySettings,
TimeSeriesChartSeriesType, TimeSeriesChartSeriesType,
TimeSeriesChartSettings TimeSeriesChartSettings, TimeSeriesChartYAxisSettings
} from '@home/components/widget/lib/chart/time-series-chart.models'; } from '@home/components/widget/lib/chart/time-series-chart.models';
import { DeepPartial } from '@shared/models/common';
const valuesLayoutHeight = 66; const valuesLayoutHeight = 66;
const valuesLayoutVerticalPadding = 16; const valuesLayoutVerticalPadding = 16;
@ -172,25 +174,27 @@ export class AggregatedValueCardWidgetComponent implements OnInit, AfterViewInit
ngAfterViewInit(): void { ngAfterViewInit(): void {
if (this.showChart && this.ctx.datasources?.length) { if (this.showChart && this.ctx.datasources?.length) {
const settings: TimeSeriesChartSettings = { const settings: DeepPartial<TimeSeriesChartSettings> = {
dataZoom: false, dataZoom: false,
xAxis: { xAxis: {
show: false show: false
}, },
yAxis: { yAxes: {
show: true, default: {
showLine: false, show: true,
showTicks: false, showLine: false,
showTickLabels: false, showTicks: false,
showSplitLines: true, showTickLabels: false,
min: 'dataMin', showSplitLines: true,
max: 'dataMax', min: 'dataMin',
intervalCalculator: max: 'dataMax',
'var scale = axis.scale; return !scale.isBlank() ? ((scale.getExtent()[1] - scale.getExtent()[0]) / 2) : undefined;' intervalCalculator:
'var scale = axis.scale; return !scale.isBlank() ? ((scale.getExtent()[1] - scale.getExtent()[0]) / 2) : undefined;'
}
}, },
tooltipDateInterval: false, tooltipDateInterval: false,
tooltipDateFormat: simpleDateFormat('dd MMM yyyy HH:mm:ss') tooltipDateFormat: simpleDateFormat('dd MMM yyyy HH:mm:ss')
} as TimeSeriesChartSettings; };
this.lineChart = new TbTimeSeriesChart(this.ctx, settings, this.chartElement.nativeElement, this.renderer, true); this.lineChart = new TbTimeSeriesChart(this.ctx, settings, this.chartElement.nativeElement, this.renderer, true);

11
ui-ngx/src/app/modules/home/components/widget/lib/cards/value-chart-card-widget.component.ts

@ -57,6 +57,7 @@ import {
TimeSeriesChartSeriesType, TimeSeriesChartSeriesType,
TimeSeriesChartSettings TimeSeriesChartSettings
} from '@home/components/widget/lib/chart/time-series-chart.models'; } from '@home/components/widget/lib/chart/time-series-chart.models';
import { DeepPartial } from '@shared/models/common';
const layoutHeight = 56; const layoutHeight = 56;
const valueRelativeWidth = 0.35; const valueRelativeWidth = 0.35;
@ -162,17 +163,19 @@ export class ValueChartCardWidgetComponent implements OnInit, AfterViewInit, OnD
} }
public ngAfterViewInit() { public ngAfterViewInit() {
const settings: TimeSeriesChartSettings = { const settings: DeepPartial<TimeSeriesChartSettings> = {
dataZoom: false, dataZoom: false,
xAxis: { xAxis: {
show: false show: false
}, },
yAxis: { yAxes: {
show: false default: {
show: false,
}
}, },
tooltipDateInterval: false, tooltipDateInterval: false,
tooltipDateFormat: simpleDateFormat('dd MMM yyyy HH:mm:ss') tooltipDateFormat: simpleDateFormat('dd MMM yyyy HH:mm:ss')
} as TimeSeriesChartSettings; };
this.lineChart = new TbTimeSeriesChart(this.ctx, settings, this.chartElement.nativeElement, this.renderer, false); this.lineChart = new TbTimeSeriesChart(this.ctx, settings, this.chartElement.nativeElement, this.renderer, false);

216
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 { ComponentStyle, Font, simpleDateFormat, textStyle } from '@shared/models/widget-settings.models';
import { XAXisOption, YAXisOption } from 'echarts/types/dist/shared'; import { XAXisOption, YAXisOption } from 'echarts/types/dist/shared';
import { CustomSeriesOption, LineSeriesOption } from 'echarts/charts'; 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 { LinearGradientObject } from 'zrender/lib/graphic/LinearGradient';
import tinycolor from 'tinycolor2'; import tinycolor from 'tinycolor2';
import Axis2D from 'echarts/types/src/coord/cartesian/Axis2D'; import Axis2D from 'echarts/types/src/coord/cartesian/Axis2D';
@ -289,13 +297,82 @@ export interface TimeSeriesChartAxisSettings {
splitLinesColor: string; splitLinesColor: string;
} }
export type TimeSeriesChartYAxisId = 'default' | string;
export interface TimeSeriesChartYAxisSettings extends TimeSeriesChartAxisSettings { export interface TimeSeriesChartYAxisSettings extends TimeSeriesChartAxisSettings {
id?: TimeSeriesChartYAxisId;
order?: number;
units?: string;
decimals?: number;
min?: number | string; min?: number | string;
max?: number | string; max?: number | string;
intervalCalculator?: string; intervalCalculator?: string;
ticksFormatter?: 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 { export interface TimeSeriesChartThreshold {
type: TimeSeriesChartThresholdType; type: TimeSeriesChartThresholdType;
value?: number; value?: number;
@ -304,6 +381,7 @@ export interface TimeSeriesChartThreshold {
entityAlias?: string; entityAlias?: string;
entityKey?: string; entityKey?: string;
entityKeyType?: DataKeyType.attribute | DataKeyType.timeseries; entityKeyType?: DataKeyType.attribute | DataKeyType.timeseries;
yAxisId: TimeSeriesChartYAxisId;
units?: string; units?: string;
decimals?: number; decimals?: number;
lineColor: string; lineColor: string;
@ -320,7 +398,7 @@ export interface TimeSeriesChartThreshold {
} }
export const timeSeriesChartThresholdValid = (threshold: TimeSeriesChartThreshold): boolean => { export const timeSeriesChartThresholdValid = (threshold: TimeSeriesChartThreshold): boolean => {
if (!threshold.type) { if (!threshold.type || !threshold.yAxisId) {
return false; return false;
} }
switch (threshold.type) { switch (threshold.type) {
@ -355,7 +433,8 @@ export const timeSeriesChartThresholdValidator = (control: AbstractControl): Val
export const timeSeriesChartThresholdDefaultSettings: TimeSeriesChartThreshold = { export const timeSeriesChartThresholdDefaultSettings: TimeSeriesChartThreshold = {
type: TimeSeriesChartThresholdType.constant, type: TimeSeriesChartThresholdType.constant,
units: '', yAxisId: 'default',
units: null,
decimals: 0, decimals: 0,
lineColor: timeSeriesChartColorScheme['threshold.line'].light, lineColor: timeSeriesChartColorScheme['threshold.line'].light,
lineType: TimeSeriesChartLineType.solid, lineType: TimeSeriesChartLineType.solid,
@ -404,13 +483,61 @@ export interface TimeSeriesChartNoAggregationBarWidthSettings {
barWidth?: TimeSeriesChartBarWidth; 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 { export interface TimeSeriesChartSettings extends EChartsTooltipWidgetSettings {
thresholds: TimeSeriesChartThreshold[]; thresholds: TimeSeriesChartThreshold[];
darkMode: boolean; darkMode: boolean;
dataZoom: boolean; dataZoom: boolean;
stack: boolean; stack: boolean;
yAxis: TimeSeriesChartYAxisSettings; yAxes: TimeSeriesChartYAxes;
xAxis: TimeSeriesChartAxisSettings; xAxis: TimeSeriesChartAxisSettings;
animation: TimeSeriesChartAnimationSettings;
noAggregationBarWidthSettings: TimeSeriesChartNoAggregationBarWidthSettings; noAggregationBarWidthSettings: TimeSeriesChartNoAggregationBarWidthSettings;
} }
@ -419,36 +546,10 @@ export const timeSeriesChartDefaultSettings: TimeSeriesChartSettings = {
darkMode: false, darkMode: false,
dataZoom: true, dataZoom: true,
stack: false, stack: false,
yAxis: { yAxes: {
show: true, default: mergeDeep({} as TimeSeriesChartYAxisSettings,
label: '', defaultTimeSeriesChartYAxisSettings,
labelFont: { { id: 'default', order: 0 } as TimeSeriesChartYAxisSettings)
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
}, },
xAxis: { xAxis: {
show: true, show: true,
@ -480,6 +581,16 @@ export const timeSeriesChartDefaultSettings: TimeSeriesChartSettings = {
showSplitLines: true, showSplitLines: true,
splitLinesColor: timeSeriesChartColorScheme['axis.splitLine'].light 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: { noAggregationBarWidthSettings: {
strategy: TimeSeriesChartNoAggregationBarWidthStrategy.group, strategy: TimeSeriesChartNoAggregationBarWidthStrategy.group,
groupWidth: { groupWidth: {
@ -558,6 +669,7 @@ export interface BarSeriesSettings {
} }
export interface TimeSeriesChartKeySettings { export interface TimeSeriesChartKeySettings {
yAxisId: TimeSeriesChartYAxisId;
showInLegend: boolean; showInLegend: boolean;
dataHiddenByDefault: boolean; dataHiddenByDefault: boolean;
type: TimeSeriesChartSeriesType; type: TimeSeriesChartSeriesType;
@ -566,6 +678,7 @@ export interface TimeSeriesChartKeySettings {
} }
export const timeSeriesChartKeyDefaultSettings: TimeSeriesChartKeySettings = { export const timeSeriesChartKeyDefaultSettings: TimeSeriesChartKeySettings = {
yAxisId: 'default',
showInLegend: true, showInLegend: true,
dataHiddenByDefault: false, dataHiddenByDefault: false,
type: TimeSeriesChartSeriesType.line, type: TimeSeriesChartSeriesType.line,
@ -626,6 +739,7 @@ export const timeSeriesChartKeyDefaultSettings: TimeSeriesChartKeySettings = {
}; };
export interface TimeSeriesChartDataItem extends EChartsSeriesItem { export interface TimeSeriesChartDataItem extends EChartsSeriesItem {
yAxisId: TimeSeriesChartYAxisId;
yAxisIndex: number; yAxisIndex: number;
option?: LineSeriesOption | CustomSeriesOption; option?: LineSeriesOption | CustomSeriesOption;
barRenderContext?: BarRenderContext; barRenderContext?: BarRenderContext;
@ -635,6 +749,7 @@ type TimeSeriesChartThresholdValue = number | string | (number | string)[];
export interface TimeSeriesChartThresholdItem { export interface TimeSeriesChartThresholdItem {
id: string; id: string;
yAxisId: TimeSeriesChartYAxisId;
yAxisIndex: number; yAxisIndex: number;
latestDataKey?: DataKey; latestDataKey?: DataKey;
units?: string; units?: string;
@ -646,14 +761,16 @@ export interface TimeSeriesChartThresholdItem {
export interface TimeSeriesChartYAxis { export interface TimeSeriesChartYAxis {
id: string; id: string;
units: string; decimals: number;
settings: TimeSeriesChartYAxisSettings;
option: YAXisOption & ValueAxisBaseOption; option: YAXisOption & ValueAxisBaseOption;
intervalCalculator?: (axis: Axis2D) => number; intervalCalculator?: (axis: Axis2D) => number;
ticksFormatter?: (value: any) => string; ticksFormatter?: (value: any) => string;
} }
export const createTimeSeriesYAxis = (axisId: string, units: string, export const createTimeSeriesYAxis = (units: string,
decimals: number, settings: TimeSeriesChartYAxisSettings, decimals: number,
settings: TimeSeriesChartYAxisSettings,
darkMode: boolean): TimeSeriesChartYAxis => { darkMode: boolean): TimeSeriesChartYAxis => {
const yAxisTickLabelStyle = createChartTextStyle(settings.tickLabelFont, const yAxisTickLabelStyle = createChartTextStyle(settings.tickLabelFont,
settings.tickLabelColor, darkMode, 'axis.tickLabel'); settings.tickLabelColor, darkMode, 'axis.tickLabel');
@ -664,13 +781,14 @@ export const createTimeSeriesYAxis = (axisId: string, units: string,
ticksFormatter = parseFunction(settings.ticksFormatter, ['value']); ticksFormatter = parseFunction(settings.ticksFormatter, ['value']);
} }
const yAxis: TimeSeriesChartYAxis = { const yAxis: TimeSeriesChartYAxis = {
id: axisId, id: settings.id,
units, decimals,
settings,
option: { option: {
show: settings.show, show: settings.show,
type: 'value', type: 'value',
position: settings.position, position: settings.position,
id: axisId, id: settings.id,
offset: 0, offset: 0,
alignTicks: true, alignTicks: true,
scale: true, scale: true,
@ -843,13 +961,11 @@ const generateChartThresholds = (thresholdItems: TimeSeriesChartThresholdItem[],
id: item.id, id: item.id,
dataGroupId: item.id, dataGroupId: item.id,
yAxisIndex: item.yAxisIndex, yAxisIndex: item.yAxisIndex,
animation: true,
data: [], data: [],
tooltip: { tooltip: {
show: false show: false
}, },
markLine: { markLine: {
animation: true,
lineStyle: { lineStyle: {
width: item.settings.lineWidth, width: item.settings.lineWidth,
color: prepareChartThemeColor(item.settings.lineColor, darkMode, 'threshold.line'), color: prepareChartThemeColor(item.settings.lineColor, darkMode, 'threshold.line'),
@ -956,16 +1072,19 @@ const generateChartSeries = (dataItems: TimeSeriesChartDataItem[],
}; };
export const updateDarkMode = (options: EChartsOption, settings: TimeSeriesChartSettings, export const updateDarkMode = (options: EChartsOption, settings: TimeSeriesChartSettings,
yAxisList: TimeSeriesChartYAxis[],
dataItems: TimeSeriesChartDataItem[], thresholdDataItems: TimeSeriesChartThresholdItem[], dataItems: TimeSeriesChartDataItem[], thresholdDataItems: TimeSeriesChartThresholdItem[],
darkMode: boolean): EChartsOption => { darkMode: boolean): EChartsOption => {
options.darkMode = darkMode; options.darkMode = darkMode;
if (Array.isArray(options.yAxis)) { if (Array.isArray(options.yAxis)) {
for (const yAxis of options.yAxis) { for (let i = 0; i < options.yAxis.length; i++) {
yAxis.nameTextStyle.color = prepareChartThemeColor(settings.yAxis.labelColor, darkMode, 'axis.label'); const yAxis = options.yAxis[i];
yAxis.axisLabel.color = prepareChartThemeColor(settings.yAxis.tickLabelColor, darkMode, 'axis.tickLabel'); const yAxisSettings = yAxisList[i].settings;
yAxis.axisLine.lineStyle.color = prepareChartThemeColor(settings.yAxis.lineColor, darkMode, 'axis.line'); yAxis.nameTextStyle.color = prepareChartThemeColor(yAxisSettings.labelColor, darkMode, 'axis.label');
yAxis.axisTick.lineStyle.color = prepareChartThemeColor(settings.yAxis.ticksColor, darkMode, 'axis.ticks'); yAxis.axisLabel.color = prepareChartThemeColor(yAxisSettings.tickLabelColor, darkMode, 'axis.tickLabel');
yAxis.splitLine.lineStyle.color = prepareChartThemeColor(settings.yAxis.splitLinesColor, darkMode, 'axis.splitLine'); 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)) { if (Array.isArray(options.xAxis)) {
@ -1032,7 +1151,6 @@ const createTimeSeriesChartSeries = (item: TimeSeriesChartDataItem,
emphasis: { emphasis: {
focus: 'series' focus: 'series'
}, },
animation: true,
dimensions: [ dimensions: [
{name: 'intervalStart', type: 'number'}, {name: 'intervalStart', type: 'number'},
{name: 'intervalEnd', type: 'number'} {name: 'intervalEnd', type: 'number'}

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

@ -19,7 +19,7 @@ import {
AxisPosition, AxisPosition,
calculateThresholdsOffset, calculateThresholdsOffset,
createTimeSeriesXAxisOption, createTimeSeriesXAxisOption,
createTimeSeriesYAxis, createTimeSeriesYAxis, defaultTimeSeriesChartYAxisSettings,
generateChartData, generateChartData,
parseThresholdData, parseThresholdData,
SeriesLabelPosition, SeriesLabelPosition,
@ -29,11 +29,11 @@ import {
TimeSeriesChartKeySettings, TimeSeriesChartKeySettings,
TimeSeriesChartSeriesType, TimeSeriesChartSeriesType,
TimeSeriesChartSettings, TimeSeriesChartSettings,
TimeSeriesChartShape, TimeSeriesChartShape, TimeSeriesChartThreshold, timeSeriesChartThresholdDefaultSettings,
TimeSeriesChartThresholdItem, TimeSeriesChartThresholdItem,
TimeSeriesChartThresholdType, TimeSeriesChartThresholdType,
TimeSeriesChartType, TimeSeriesChartType,
TimeSeriesChartYAxis, TimeSeriesChartYAxis, TimeSeriesChartYAxisId, TimeSeriesChartYAxisSettings,
updateDarkMode updateDarkMode
} from '@home/components/widget/lib/chart/time-series-chart.models'; } from '@home/components/widget/lib/chart/time-series-chart.models';
import { ResizeObserver } from '@juggle/resize-observer'; 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 { DataKeyType } from '@shared/models/telemetry/telemetry.models';
import { WidgetSubscriptionOptions } from '@core/api/widget-api.models'; import { WidgetSubscriptionOptions } from '@core/api/widget-api.models';
import { DataKeySettingsFunction } from '@home/components/widget/config/data-keys.component.models'; import { DataKeySettingsFunction } from '@home/components/widget/config/data-keys.component.models';
import { DeepPartial } from '@shared/models/common';
export class TbTimeSeriesChart { export class TbTimeSeriesChart {
@ -94,9 +95,11 @@ export class TbTimeSeriesChart {
private readonly shapeResize$: ResizeObserver; private readonly shapeResize$: ResizeObserver;
private readonly settings: TimeSeriesChartSettings;
private yAxisList: TimeSeriesChartYAxis[] = [];
private dataItems: TimeSeriesChartDataItem[] = []; private dataItems: TimeSeriesChartDataItem[] = [];
private thresholdItems: TimeSeriesChartThresholdItem[] = []; private thresholdItems: TimeSeriesChartThresholdItem[] = [];
private yAxisList: TimeSeriesChartYAxis[] = [];
private timeSeriesChart: ECharts; private timeSeriesChart: ECharts;
private timeSeriesChartOptions: EChartsOption; private timeSeriesChartOptions: EChartsOption;
@ -120,13 +123,16 @@ export class TbTimeSeriesChart {
yMax$ = this.yMaxSubject.asObservable(); yMax$ = this.yMaxSubject.asObservable();
constructor(private ctx: WidgetContext, constructor(private ctx: WidgetContext,
private readonly settings: TimeSeriesChartSettings, private readonly inputSettings: DeepPartial<TimeSeriesChartSettings>,
private chartElement: HTMLElement, private chartElement: HTMLElement,
private renderer: Renderer2, private renderer: Renderer2,
private autoResize = true) { 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.darkMode = this.settings.darkMode;
this.setupYAxes();
this.setupData(); this.setupData();
this.setupThresholds(); this.setupThresholds();
if (this.settings.showTooltip && this.settings.tooltipShowDate) { if (this.settings.showTooltip && this.settings.tooltipShowDate) {
@ -258,7 +264,8 @@ export class TbTimeSeriesChart {
if (this.darkMode !== darkMode) { if (this.darkMode !== darkMode) {
this.darkMode = darkMode; this.darkMode = darkMode;
if (this.timeSeriesChart) { 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.thresholdItems, darkMode);
this.timeSeriesChart.setOption(this.timeSeriesChartOptions); 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 units = dataKey.units && dataKey.units.length ? dataKey.units : this.ctx.units;
const decimals = isDefinedAndNotNull(dataKey.decimals) ? dataKey.decimals : const decimals = isDefinedAndNotNull(dataKey.decimals) ? dataKey.decimals :
(isDefinedAndNotNull(this.ctx.decimals) ? this.ctx.decimals : 2); (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({ this.dataItems.push({
id: this.nextComponentId(), id: this.nextComponentId(),
units, units,
decimals, decimals,
yAxisIndex: this.getYAxis(units, decimals), yAxisId,
yAxisIndex: this.getYAxisIndex(yAxisId),
dataKey, dataKey,
data: namedData, data: namedData,
enabled: !keySettings.dataHiddenByDefault enabled: !keySettings.dataHiddenByDefault
@ -303,7 +315,9 @@ export class TbTimeSeriesChart {
private setupThresholds(): void { private setupThresholds(): void {
const thresholdDatasources: Datasource[] = []; const thresholdDatasources: Datasource[] = [];
for (const threshold of this.settings.thresholds) { for (const thresholdSettings of this.settings.thresholds) {
const threshold = mergeDeep<TimeSeriesChartThreshold>({} as TimeSeriesChartThreshold,
timeSeriesChartThresholdDefaultSettings, thresholdSettings);
let latestDataKey: DataKey = null; let latestDataKey: DataKey = null;
let entityDataKey: DataKey = null; let entityDataKey: DataKey = null;
let value = null; let value = null;
@ -351,11 +365,16 @@ export class TbTimeSeriesChart {
const units = threshold.units && threshold.units.length ? threshold.units : this.ctx.units; const units = threshold.units && threshold.units.length ? threshold.units : this.ctx.units;
const decimals = isDefinedAndNotNull(threshold.decimals) ? threshold.decimals : const decimals = isDefinedAndNotNull(threshold.decimals) ? threshold.decimals :
(isDefinedAndNotNull(this.ctx.decimals) ? this.ctx.decimals : 2); (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 = { const thresholdItem: TimeSeriesChartThresholdItem = {
id: this.nextComponentId(), id: this.nextComponentId(),
units, units,
decimals, decimals,
yAxisIndex: this.getYAxis(units, decimals), yAxisId,
yAxisIndex: this.getYAxisIndex(yAxisId),
value, value,
latestDataKey, latestDataKey,
settings: threshold settings: threshold
@ -368,17 +387,32 @@ export class TbTimeSeriesChart {
this.subscribeForEntityThresholds(thresholdDatasources); 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<TimeSeriesChartYAxisSettings>({} 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 { private nextComponentId(): string {
return (this.componentIndexCounter++) + ''; return (this.componentIndexCounter++) + '';
} }
private getYAxis(units: string, decimals: number): number { private getYAxisIndex(id: TimeSeriesChartYAxisId): number {
let yAxisIndex = this.yAxisList.findIndex(axis => axis.units === units); let yAxisIndex = this.yAxisList.findIndex(axis => axis.id === id);
if (yAxisIndex === -1) { if (yAxisIndex === -1) {
const yAxisId = this.yAxisList.length + ''; yAxisIndex = this.yAxisList.findIndex(axis => axis.id === 'default');
const yAxis = createTimeSeriesYAxis(yAxisId, units, decimals, this.settings.yAxis, this.darkMode); if (yAxisIndex === -1 && this.yAxisList.length) {
this.yAxisList.push(yAxis); yAxisIndex = 0;
yAxisIndex = this.yAxisList.length - 1; }
} }
return yAxisIndex; return yAxisIndex;
} }
@ -460,7 +494,15 @@ export class TbTimeSeriesChart {
realtime: true, realtime: true,
bottom: 10 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; this.timeSeriesChartOptions.xAxis[0].tbTimeWindow = this.ctx.defaultSubscription.timeWindow;
@ -588,7 +630,7 @@ export class TbTimeSeriesChart {
result.offset += 5; result.offset += 5;
} }
width = newWidth; width = newWidth;
const showLine = !!width && this.settings.yAxis.showLine; const showLine = !!width && yAxis.settings.showLine;
if (yAxis.option.axisLine.show !== showLine) { if (yAxis.option.axisLine.show !== showLine) {
yAxis.option.axisLine.show = showLine; yAxis.option.axisLine.show = showLine;
result.changed = true; result.changed = true;
@ -597,7 +639,7 @@ export class TbTimeSeriesChart {
yAxis.option.offset = result.offset; yAxis.option.offset = result.offset;
result.changed = true; result.changed = true;
} }
if (this.settings.yAxis.label) { if (yAxis.settings.label) {
if (!width) { if (!width) {
if (yAxis.option.name) { if (yAxis.option.name) {
yAxis.option.name = null; yAxis.option.name = null;
@ -605,7 +647,7 @@ export class TbTimeSeriesChart {
} }
} else { } else {
if (!yAxis.option.name) { if (!yAxis.option.name) {
yAxis.option.name = this.settings.yAxis.label; yAxis.option.name = yAxis.settings.label;
result.changed = true; result.changed = true;
} }
const nameGap = width; const nameGap = width;
@ -613,7 +655,7 @@ export class TbTimeSeriesChart {
yAxis.option.nameGap = nameGap; yAxis.option.nameGap = nameGap;
result.changed = true; 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; result.offset += nameWidth;
} }
} }
@ -623,15 +665,25 @@ export class TbTimeSeriesChart {
} }
private scaleYAxis(yAxis: TimeSeriesChartYAxis): boolean { private scaleYAxis(yAxis: TimeSeriesChartYAxis): boolean {
const yAxisIndex = this.yAxisList.indexOf(yAxis); const axisBarDataItems = this.dataItems.filter(d => d.yAxisId === yAxis.id && d.enabled &&
const axisBarDataItems = this.dataItems.filter(d => d.yAxisIndex === yAxisIndex && d.enabled &&
d.data.length && d.dataKey.settings.type === TimeSeriesChartSeriesType.bar); d.data.length && d.dataKey.settings.type === TimeSeriesChartSeriesType.bar);
return !axisBarDataItems.length; 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 { private calculateYAxisInterval(axisList: TimeSeriesChartYAxis[]): boolean {
let changed = false; let changed = false;
for (const yAxis of axisList) { 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) { if (yAxis.intervalCalculator) {
const axis = getYAxis(this.timeSeriesChart, yAxis.id); const axis = getYAxis(this.timeSeriesChart, yAxis.id);
if (axis) { if (axis) {
@ -649,8 +701,10 @@ export class TbTimeSeriesChart {
} }
private minTopOffset(): number { private minTopOffset(): number {
const showTickLabels =
!!this.yAxisList.find(yAxis => yAxis.settings.show && yAxis.settings.showTickLabels);
return (this.topPointLabels) ? 20 : return (this.topPointLabels) ? 20 :
((this.settings.yAxis.show && this.settings.yAxis.showTickLabels) ? 10 : 5); (showTickLabels ? 10 : 5);
} }
private minBottomOffset(): number { private minBottomOffset(): number {

12
ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-key-settings.component.html

@ -29,6 +29,18 @@
</div> </div>
</mat-slide-toggle> </mat-slide-toggle>
</div> </div>
<div class="tb-form-panel">
<div class="tb-form-row space-between column-xs">
<div translate>widgets.time-series-chart.axis.y-axis</div>
<mat-form-field class="medium-width" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="yAxisId">
<mat-option *ngFor="let yAxis of yAxisIds" [value]="yAxis">
{{ yAxis }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
</div>
<div class="tb-form-panel"> <div class="tb-form-panel">
<div *ngIf="chartType === TimeSeriesChartType.default; else chartTypeTitle" fxLayout="row" fxLayoutAlign="space-between center"> <div *ngIf="chartType === TimeSeriesChartType.default; else chartTypeTitle" fxLayout="row" fxLayoutAlign="space-between center">
<div class="tb-form-panel-title" translate>widgets.time-series-chart.series.series-type</div> <div class="tb-form-panel-title" translate>widgets.time-series-chart.series.series-type</div>

12
ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-key-settings.component.ts

@ -25,9 +25,10 @@ import {
TimeSeriesChartKeySettings, TimeSeriesChartKeySettings,
TimeSeriesChartSeriesType, TimeSeriesChartSeriesType,
timeSeriesChartSeriesTypes, timeSeriesChartSeriesTypes,
timeSeriesChartSeriesTypeTranslations, TimeSeriesChartType, timeSeriesChartTypeTranslations timeSeriesChartSeriesTypeTranslations, TimeSeriesChartType, timeSeriesChartTypeTranslations, TimeSeriesChartYAxisId
} from '@home/components/widget/lib/chart/time-series-chart.models'; } from '@home/components/widget/lib/chart/time-series-chart.models';
import { WidgetConfigComponentData } from '@home/models/widget-component.models'; import { WidgetConfigComponentData } from '@home/models/widget-component.models';
import { TimeSeriesChartWidgetSettings } from '@home/components/widget/lib/chart/time-series-chart-widget.models';
@Component({ @Component({
selector: 'tb-time-series-chart-key-settings', selector: 'tb-time-series-chart-key-settings',
@ -50,6 +51,8 @@ export class TimeSeriesChartKeySettingsComponent extends WidgetSettingsComponent
chartType = TimeSeriesChartType.default; chartType = TimeSeriesChartType.default;
yAxisIds: TimeSeriesChartYAxisId[];
constructor(protected store: Store<AppState>, constructor(protected store: Store<AppState>,
private fb: UntypedFormBuilder) { private fb: UntypedFormBuilder) {
super(store); super(store);
@ -64,6 +67,8 @@ export class TimeSeriesChartKeySettingsComponent extends WidgetSettingsComponent
if (isDefinedAndNotNull(params.chartType)) { if (isDefinedAndNotNull(params.chartType)) {
this.chartType = 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 { protected defaultSettings(): WidgetSettings {
@ -73,7 +78,12 @@ export class TimeSeriesChartKeySettingsComponent extends WidgetSettingsComponent
protected onSettingsSet(settings: WidgetSettings) { protected onSettingsSet(settings: WidgetSettings) {
const seriesSettings = settings as TimeSeriesChartKeySettings; const seriesSettings = settings as TimeSeriesChartKeySettings;
let yAxisId = seriesSettings.yAxisId;
if (!this.yAxisIds.includes(yAxisId)) {
yAxisId = 'default';
}
this.timeSeriesChartKeySettingsForm = this.fb.group({ this.timeSeriesChartKeySettingsForm = this.fb.group({
yAxisId: [yAxisId, []],
showInLegend: [seriesSettings.showInLegend, []], showInLegend: [seriesSettings.showInLegend, []],
dataHiddenByDefault: [seriesSettings.dataHiddenByDefault, []], dataHiddenByDefault: [seriesSettings.dataHiddenByDefault, []],
type: [seriesSettings.type, []], type: [seriesSettings.type, []],

18
ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-widget-settings.component.html

@ -16,12 +16,18 @@
--> -->
<ng-container [formGroup]="timeSeriesChartWidgetSettingsForm"> <ng-container [formGroup]="timeSeriesChartWidgetSettingsForm">
<tb-time-series-chart-y-axes-panel
formControlName="yAxes"
(axisRemoved)="yAxisRemoved($event)"
advanced>
</tb-time-series-chart-y-axes-panel>
<tb-time-series-chart-thresholds-panel <tb-time-series-chart-thresholds-panel
formControlName="thresholds" formControlName="thresholds"
[aliasController]="aliasController" [aliasController]="aliasController"
[dataKeyCallbacks]="dataKeyCallbacks" [dataKeyCallbacks]="dataKeyCallbacks"
[datasource]="datasource" [datasource]="datasource"
[widgetConfig]="widgetConfig?.config"> [widgetConfig]="widgetConfig?.config"
[yAxisIds]="yAxisIds">
</tb-time-series-chart-thresholds-panel> </tb-time-series-chart-thresholds-panel>
<div class="tb-form-panel"> <div class="tb-form-panel">
<div class="tb-form-panel-title" translate>widgets.time-series-chart.chart-style</div> <div class="tb-form-panel-title" translate>widgets.time-series-chart.chart-style</div>
@ -38,12 +44,7 @@
</mat-slide-toggle> </mat-slide-toggle>
</div> </div>
<div class="tb-form-panel"> <div class="tb-form-panel">
<div class="tb-form-panel-title" translate>widgets.time-series-chart.axis.axes</div> <div class="tb-form-panel-title" translate>widgets.time-series-chart.axis.x-axis</div>
<tb-time-series-chart-axis-settings
formControlName="yAxis"
advanced
axisType="yAxis">
</tb-time-series-chart-axis-settings>
<tb-time-series-chart-axis-settings <tb-time-series-chart-axis-settings
formControlName="xAxis" formControlName="xAxis"
advanced advanced
@ -156,6 +157,9 @@
</ng-template> </ng-template>
</mat-expansion-panel> </mat-expansion-panel>
</div> </div>
<tb-time-series-chart-animation-settings
formControlName="animation">
</tb-time-series-chart-animation-settings>
<div class="tb-form-row space-between"> <div class="tb-form-row space-between">
<div>{{ 'widgets.background.background' | translate }}</div> <div>{{ 'widgets.background.background' | translate }}</div>
<tb-background-settings formControlName="background"> <tb-background-settings formControlName="background">

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

@ -16,6 +16,7 @@
import { Component, Injector } from '@angular/core'; import { Component, Injector } from '@angular/core';
import { import {
DataKey,
Datasource, Datasource,
legendPositions, legendPositions,
legendPositionTranslationMap, legendPositionTranslationMap,
@ -32,7 +33,11 @@ import {
timeSeriesChartWidgetDefaultSettings, timeSeriesChartWidgetDefaultSettings,
TimeSeriesChartWidgetSettings TimeSeriesChartWidgetSettings
} from '@home/components/widget/lib/chart/time-series-chart-widget.models'; } from '@home/components/widget/lib/chart/time-series-chart-widget.models';
import { TimeSeriesChartType } from '@home/components/widget/lib/chart/time-series-chart.models'; import {
TimeSeriesChartKeySettings,
TimeSeriesChartType, TimeSeriesChartYAxes,
TimeSeriesChartYAxisId
} from '@home/components/widget/lib/chart/time-series-chart.models';
import { WidgetConfigComponentData } from '@home/models/widget-component.models'; import { WidgetConfigComponentData } from '@home/models/widget-component.models';
@Component({ @Component({
@ -51,6 +56,11 @@ export class TimeSeriesChartWidgetSettingsComponent extends WidgetSettingsCompon
} }
} }
public get yAxisIds(): TimeSeriesChartYAxisId[] {
const yAxes: TimeSeriesChartYAxes = this.timeSeriesChartWidgetSettingsForm.get('yAxes').value;
return Object.keys(yAxes);
}
TimeSeriesChartType = TimeSeriesChartType; TimeSeriesChartType = TimeSeriesChartType;
EChartsTooltipTrigger = EChartsTooltipTrigger; EChartsTooltipTrigger = EChartsTooltipTrigger;
@ -73,6 +83,15 @@ export class TimeSeriesChartWidgetSettingsComponent extends WidgetSettingsCompon
super(store); super(store);
} }
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 settingsForm(): UntypedFormGroup { protected settingsForm(): UntypedFormGroup {
return this.timeSeriesChartWidgetSettingsForm; return this.timeSeriesChartWidgetSettingsForm;
} }
@ -91,12 +110,12 @@ export class TimeSeriesChartWidgetSettingsComponent extends WidgetSettingsCompon
protected onSettingsSet(settings: WidgetSettings) { protected onSettingsSet(settings: WidgetSettings) {
this.timeSeriesChartWidgetSettingsForm = this.fb.group({ this.timeSeriesChartWidgetSettingsForm = this.fb.group({
yAxes: [settings.yAxes, []],
thresholds: [settings.thresholds, []], thresholds: [settings.thresholds, []],
dataZoom: [settings.dataZoom, []], dataZoom: [settings.dataZoom, []],
stack: [settings.stack, []], stack: [settings.stack, []],
yAxis: [settings.yAxis, []],
xAxis: [settings.xAxis, []], xAxis: [settings.xAxis, []],
noAggregationBarWidthSettings: [settings.noAggregationBarWidthSettings, []], noAggregationBarWidthSettings: [settings.noAggregationBarWidthSettings, []],
@ -119,6 +138,8 @@ export class TimeSeriesChartWidgetSettingsComponent extends WidgetSettingsCompon
tooltipBackgroundColor: [settings.tooltipBackgroundColor, []], tooltipBackgroundColor: [settings.tooltipBackgroundColor, []],
tooltipBackgroundBlur: [settings.tooltipBackgroundBlur, []], tooltipBackgroundBlur: [settings.tooltipBackgroundBlur, []],
animation: [settings.animation, []],
background: [settings.background, []] background: [settings.background, []]
}); });
} }
@ -173,6 +194,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 { private _tooltipValuePreviewFn(): string {
return formatValue(22, 0, '°C', false); return formatValue(22, 0, '°C', false);
} }

89
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-animation-settings.component.html

@ -0,0 +1,89 @@
<!--
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.
-->
<ng-container [formGroup]="animationSettingsFormGroup">
<div class="tb-form-panel tb-slide-toggle">
<mat-expansion-panel class="tb-settings" [(expanded)]="settingsExpanded"
[disabled]="!animationSettingsFormGroup.get('animation').value">
<mat-expansion-panel-header fxLayout="row wrap">
<mat-panel-title>
<mat-slide-toggle class="mat-slide" formControlName="animation" (click)="$event.stopPropagation()"
fxLayoutAlign="center">
{{ 'widgets.time-series-chart.animation.animation' | translate }}
</mat-slide-toggle>
</mat-panel-title>
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
<div class="tb-form-row space-between column-xs">
<div>{{ 'widgets.time-series-chart.animation.animation-threshold' | translate }}</div>
<mat-form-field appearance="outline" class="medium-width number" subscriptSizing="dynamic">
<input matInput formControlName="animationThreshold" type="number" min="0" step="1" placeholder="{{ 'widget-config.set' | translate }}">
<div matSuffix>ms</div>
</mat-form-field>
</div>
<div class="tb-form-row space-between column-xs">
<div>{{ 'widgets.time-series-chart.animation.animation-duration' | translate }}</div>
<mat-form-field appearance="outline" class="medium-width number" subscriptSizing="dynamic">
<input matInput formControlName="animationDuration" type="number" min="0" step="1" placeholder="{{ 'widget-config.set' | translate }}">
<div matSuffix>ms</div>
</mat-form-field>
</div>
<div class="tb-form-row space-between column-xs">
<div translate>widgets.time-series-chart.animation.animation-easing</div>
<mat-form-field class="medium-width" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="animationEasing">
<mat-option *ngFor="let easing of timeSeriesChartAnimationEasings" [value]="easing">
{{ easing }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
<div class="tb-form-row space-between column-xs">
<div>{{ 'widgets.time-series-chart.animation.animation-delay' | translate }}</div>
<mat-form-field appearance="outline" class="medium-width number" subscriptSizing="dynamic">
<input matInput formControlName="animationDelay" type="number" min="0" step="1" placeholder="{{ 'widget-config.set' | translate }}">
<div matSuffix>ms</div>
</mat-form-field>
</div>
<div class="tb-form-row space-between column-xs">
<div>{{ 'widgets.time-series-chart.animation.update-animation-duration' | translate }}</div>
<mat-form-field appearance="outline" class="medium-width number" subscriptSizing="dynamic">
<input matInput formControlName="animationDurationUpdate" type="number" min="0" step="1" placeholder="{{ 'widget-config.set' | translate }}">
<div matSuffix>ms</div>
</mat-form-field>
</div>
<div class="tb-form-row space-between column-xs">
<div translate>widgets.time-series-chart.animation.update-animation-easing</div>
<mat-form-field class="medium-width" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="animationEasingUpdate">
<mat-option *ngFor="let easing of timeSeriesChartAnimationEasings" [value]="easing">
{{ easing }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
<div class="tb-form-row space-between column-xs">
<div>{{ 'widgets.time-series-chart.animation.update-animation-delay' | translate }}</div>
<mat-form-field appearance="outline" class="medium-width number" subscriptSizing="dynamic">
<input matInput formControlName="animationDelayUpdate" type="number" min="0" step="1" placeholder="{{ 'widget-config.set' | translate }}">
<div matSuffix>ms</div>
</mat-form-field>
</div>
</ng-template>
</mat-expansion-panel>
</div>
</ng-container>

124
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);
}
}

18
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-axis-settings.component.html

@ -17,13 +17,13 @@
--> -->
<ng-container [formGroup]="axisSettingsFormGroup"> <ng-container [formGroup]="axisSettingsFormGroup">
<div class="tb-form-panel stroked tb-slide-toggle"> <div class="tb-form-panel stroked tb-slide-toggle">
<mat-expansion-panel class="tb-settings" [(expanded)]="settingsExpanded" <mat-expansion-panel class="tb-settings" [(expanded)]="alwaysExpanded || settingsExpanded"
[disabled]="!axisSettingsFormGroup.get('show').value"> [disabled]="alwaysExpanded || !axisSettingsFormGroup.get('show').value">
<mat-expansion-panel-header fxLayout="row wrap"> <mat-expansion-panel-header fxLayout="row wrap">
<mat-panel-title> <mat-panel-title>
<mat-slide-toggle class="mat-slide" formControlName="show" (click)="$event.stopPropagation()" <mat-slide-toggle class="mat-slide" formControlName="show" (click)="$event.stopPropagation()"
fxLayoutAlign="center"> fxLayoutAlign="center">
{{ axisTitle | translate }} {{ 'widgets.time-series-chart.axis.show' | translate }}
</mat-slide-toggle> </mat-slide-toggle>
</mat-panel-title> </mat-panel-title>
</mat-expansion-panel-header> </mat-expansion-panel-header>
@ -56,6 +56,18 @@
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
</div> </div>
<div *ngIf="axisType === 'yAxis'" class="tb-form-row space-between">
<div translate>widget-config.units-short</div>
<tb-unit-input
formControlName="units">
</tb-unit-input>
</div>
<div *ngIf="axisType === 'yAxis'" class="tb-form-row space-between">
<div translate>widget-config.decimals-short</div>
<mat-form-field appearance="outline" class="number" subscriptSizing="dynamic">
<input matInput formControlName="decimals" type="number" min="0" max="15" step="1" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
</div>
<div class="tb-form-panel no-padding stroked"> <div class="tb-form-panel no-padding stroked">
<div class="tb-form-row no-border space-between column-xs"> <div class="tb-form-row no-border space-between column-xs">
<mat-slide-toggle class="mat-slide" formControlName="showTickLabels"> <mat-slide-toggle class="mat-slide" formControlName="showTickLabels">

18
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 { 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 { import {
AxisPosition, AxisPosition,
timeSeriesAxisPositionTranslations, timeSeriesAxisPositionTranslations,
@ -40,9 +46,11 @@ import { WidgetService } from '@core/http/widget.service';
}) })
export class TimeSeriesChartAxisSettingsComponent implements OnInit, ControlValueAccessor { export class TimeSeriesChartAxisSettingsComponent implements OnInit, ControlValueAccessor {
settingsExpanded = false; @Input()
@coerceBoolean()
alwaysExpanded = false;
axisTitle: string; settingsExpanded = false;
axisPositions: AxisPosition[]; axisPositions: AxisPosition[];
@ -72,8 +80,6 @@ export class TimeSeriesChartAxisSettingsComponent implements OnInit, ControlValu
ngOnInit(): void { 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] : this.axisPositions = this.axisType === 'xAxis' ? [AxisPosition.top, AxisPosition.bottom] :
[AxisPosition.left, AxisPosition.right]; [AxisPosition.left, AxisPosition.right];
@ -94,6 +100,8 @@ export class TimeSeriesChartAxisSettingsComponent implements OnInit, ControlValu
splitLinesColor: [null, []] splitLinesColor: [null, []]
}); });
if (this.axisType === 'yAxis') { 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('ticksFormatter', this.fb.control(null, []));
this.axisSettingsFormGroup.addControl('min', this.fb.control(null, [])); this.axisSettingsFormGroup.addControl('min', this.fb.control(null, []));
this.axisSettingsFormGroup.addControl('max', this.fb.control(null, [])); this.axisSettingsFormGroup.addControl('max', this.fb.control(null, []));

28
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"> [formControl]="entityKeyFormControl">
</tb-data-key-input> </tb-data-key-input>
</div> </div>
<div class="tb-y-axis-field">
<mat-form-field class="tb-inline-field fixed-height" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="yAxisId">
<mat-option *ngFor="let yAxis of yAxisIds" [value]="yAxis">
{{ yAxis }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
<div class="tb-color-field"> <div class="tb-color-field">
<tb-color-input asBoxInput <tb-color-input asBoxInput
colorClearButton colorClearButton
@ -89,17 +98,14 @@
</mat-form-field> </mat-form-field>
</div> </div>
<div class="tb-form-table-row-cell-buttons"> <div class="tb-form-table-row-cell-buttons">
<div class="tb-settings-button"> <button type="button"
<button *ngIf="modelValue" mat-icon-button
type="button" #matButton
mat-icon-button (click)="editThreshold($event, matButton)"
#matButton matTooltip="{{ 'widgets.time-series-chart.threshold.threshold-settings' | translate }}"
(click)="editThreshold($event, matButton)" matTooltipPosition="above">
matTooltip="{{ 'widgets.time-series-chart.threshold.threshold-settings' | translate }}" <mat-icon>settings</mat-icon>
matTooltipPosition="above"> </button>
<mat-icon>settings</mat-icon>
</button>
</div>
<button type="button" <button type="button"
mat-icon-button mat-icon-button
(click)="thresholdRemoved.emit()" (click)="thresholdRemoved.emit()"

14
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-threshold-row.component.scss

@ -55,6 +55,14 @@
} }
} }
.tb-y-axis-field {
width: 100px;
min-width: 100px;
.mat-mdc-form-field {
width: 100%;
}
}
.tb-color-field, .tb-units-field, .tb-decimals-field { .tb-color-field, .tb-units-field, .tb-decimals-field {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
@ -77,6 +85,12 @@
min-width: 60px; min-width: 60px;
} }
.tb-y-axis-field {
@media #{$mat-xs} {
display: none;
}
}
.tb-units-field, .tb-color-field, .tb-decimals-field { .tb-units-field, .tb-color-field, .tb-decimals-field {
display: none; display: none;
@media #{$mat-gt-md} { @media #{$mat-gt-md} {

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

@ -19,10 +19,10 @@ import {
Component, Component,
EventEmitter, EventEmitter,
forwardRef, forwardRef,
Input, Input, OnChanges,
OnInit, OnInit,
Output, Output,
Renderer2, Renderer2, SimpleChanges,
ViewContainerRef, ViewContainerRef,
ViewEncapsulation ViewEncapsulation
} from '@angular/core'; } from '@angular/core';
@ -36,7 +36,7 @@ import {
} from '@angular/forms'; } from '@angular/forms';
import { import {
TimeSeriesChartThreshold, TimeSeriesChartThreshold,
TimeSeriesChartThresholdType, TimeSeriesChartThresholdType, TimeSeriesChartYAxisId,
timeSeriesThresholdTypes, timeSeriesThresholdTypes,
timeSeriesThresholdTypeTranslations timeSeriesThresholdTypeTranslations
} from '@home/components/widget/lib/chart/time-series-chart.models'; } from '@home/components/widget/lib/chart/time-series-chart.models';
@ -67,7 +67,7 @@ import {
], ],
encapsulation: ViewEncapsulation.None encapsulation: ViewEncapsulation.None
}) })
export class TimeSeriesChartThresholdRowComponent implements ControlValueAccessor, OnInit { export class TimeSeriesChartThresholdRowComponent implements ControlValueAccessor, OnInit, OnChanges {
DataKeyType = DataKeyType; DataKeyType = DataKeyType;
@ -98,6 +98,9 @@ export class TimeSeriesChartThresholdRowComponent implements ControlValueAccesso
@Input() @Input()
disabled: boolean; disabled: boolean;
@Input()
yAxisIds: TimeSeriesChartYAxisId[];
@Output() @Output()
thresholdRemoved = new EventEmitter(); thresholdRemoved = new EventEmitter();
@ -124,6 +127,7 @@ export class TimeSeriesChartThresholdRowComponent implements ControlValueAccesso
type: [null, []], type: [null, []],
value: [null, [Validators.required]], value: [null, [Validators.required]],
entityAlias: [null, [Validators.required]], entityAlias: [null, [Validators.required]],
yAxisId: [null, [Validators.required]],
lineColor: [null, []], lineColor: [null, []],
units: [null, []], units: [null, []],
decimals: [null, []] decimals: [null, []]
@ -144,6 +148,20 @@ export class TimeSeriesChartThresholdRowComponent implements ControlValueAccesso
}); });
} }
ngOnChanges(changes: SimpleChanges): void {
for (const propName of Object.keys(changes)) {
const change = changes[propName];
if (!change.firstChange && change.currentValue !== change.previousValue) {
if (['yAxisIds'].includes(propName)) {
if (this.modelValue?.yAxisId &&
!this.yAxisIds.includes(this.modelValue.yAxisId)) {
this.thresholdFormGroup.patchValue({yAxisId: 'default'}, {emitEvent: true});
}
}
}
}
}
registerOnChange(fn: any): void { registerOnChange(fn: any): void {
this.propagateChange = fn; this.propagateChange = fn;
} }
@ -170,6 +188,7 @@ export class TimeSeriesChartThresholdRowComponent implements ControlValueAccesso
type: value.type, type: value.type,
value: value.value, value: value.value,
entityAlias: value.entityAlias, entityAlias: value.entityAlias,
yAxisId: value.yAxisId,
lineColor: value.lineColor, lineColor: value.lineColor,
units: value.units, units: value.units,
decimals: value.decimals, decimals: value.decimals,
@ -200,7 +219,8 @@ export class TimeSeriesChartThresholdRowComponent implements ControlValueAccesso
} else { } else {
const ctx: any = { const ctx: any = {
thresholdSettings: deepClone(this.modelValue), thresholdSettings: deepClone(this.modelValue),
widgetConfig: this.widgetConfig widgetConfig: this.widgetConfig,
yAxisIds: this.yAxisIds
}; };
const thresholdSettingsPanelPopover = this.popoverService.displayPopover(trigger, this.renderer, const thresholdSettingsPanelPopover = this.popoverService.displayPopover(trigger, this.renderer,
this.viewContainerRef, TimeSeriesChartThresholdSettingsPanelComponent, ['leftOnly', 'leftTopOnly', 'leftBottomOnly'], true, null, this.viewContainerRef, TimeSeriesChartThresholdSettingsPanelComponent, ['leftOnly', 'leftTopOnly', 'leftBottomOnly'], true, null,
@ -213,6 +233,7 @@ export class TimeSeriesChartThresholdRowComponent implements ControlValueAccesso
this.modelValue = {...this.modelValue, ...thresholdSettings}; this.modelValue = {...this.modelValue, ...thresholdSettings};
this.thresholdFormGroup.patchValue( this.thresholdFormGroup.patchValue(
{ {
yAxisId: this.modelValue.yAxisId,
units: this.modelValue.units, units: this.modelValue.units,
decimals: this.modelValue.decimals, decimals: this.modelValue.decimals,
lineColor: this.modelValue.lineColor lineColor: this.modelValue.lineColor
@ -248,6 +269,7 @@ export class TimeSeriesChartThresholdRowComponent implements ControlValueAccesso
this.modelValue.type = value.type; this.modelValue.type = value.type;
this.modelValue.value = value.value; this.modelValue.value = value.value;
this.modelValue.entityAlias = value.entityAlias; this.modelValue.entityAlias = value.entityAlias;
this.modelValue.yAxisId = value.yAxisId;
this.modelValue.lineColor = value.lineColor; this.modelValue.lineColor = value.lineColor;
this.modelValue.units = value.units; this.modelValue.units = value.units;
this.modelValue.decimals = value.decimals; this.modelValue.decimals = value.decimals;

10
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-threshold-settings-panel.component.html

@ -18,6 +18,16 @@
<div class="tb-threshold-settings-panel" [formGroup]="thresholdSettingsFormGroup"> <div class="tb-threshold-settings-panel" [formGroup]="thresholdSettingsFormGroup">
<div class="tb-threshold-settings-title">{{ 'widgets.time-series-chart.threshold.threshold-settings' | translate }}</div> <div class="tb-threshold-settings-title">{{ 'widgets.time-series-chart.threshold.threshold-settings' | translate }}</div>
<div class="tb-threshold-settings-panel-content"> <div class="tb-threshold-settings-panel-content">
<div class="tb-form-row space-between column-xs">
<div translate>widgets.time-series-chart.axis.y-axis</div>
<mat-form-field class="medium-width" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="yAxisId">
<mat-option *ngFor="let yAxis of yAxisIds" [value]="yAxis">
{{ yAxis }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
<div class="tb-form-row space-between"> <div class="tb-form-row space-between">
<div translate>widget-config.units-short</div> <div translate>widget-config.units-short</div>
<tb-unit-input <tb-unit-input

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

@ -21,7 +21,7 @@ import {
TimeSeriesChartShape, TimeSeriesChartShape,
timeSeriesChartShapes, timeSeriesChartShapes,
timeSeriesChartShapeTranslations, timeSeriesChartShapeTranslations,
TimeSeriesChartThreshold, TimeSeriesChartThreshold, TimeSeriesChartYAxisId,
timeSeriesLineTypes, timeSeriesLineTypes,
timeSeriesLineTypeTranslations, timeSeriesLineTypeTranslations,
timeSeriesThresholdLabelPositions, timeSeriesThresholdLabelPositions,
@ -60,6 +60,9 @@ export class TimeSeriesChartThresholdSettingsPanelComponent implements OnInit {
@Input() @Input()
widgetConfig: WidgetConfig; widgetConfig: WidgetConfig;
@Input()
yAxisIds: TimeSeriesChartYAxisId[];
@Input() @Input()
popover: TbPopoverComponent<TimeSeriesChartThresholdSettingsPanelComponent>; popover: TbPopoverComponent<TimeSeriesChartThresholdSettingsPanelComponent>;
@ -74,6 +77,7 @@ export class TimeSeriesChartThresholdSettingsPanelComponent implements OnInit {
ngOnInit(): void { ngOnInit(): void {
this.thresholdSettingsFormGroup = this.fb.group( this.thresholdSettingsFormGroup = this.fb.group(
{ {
yAxisId: [this.thresholdSettings.yAxisId, [Validators.required]],
units: [this.thresholdSettings.units, []], units: [this.thresholdSettings.units, []],
decimals: [this.thresholdSettings.decimals, [Validators.min(0)]], decimals: [this.thresholdSettings.decimals, [Validators.min(0)]],
lineColor: [this.thresholdSettings.lineColor, []], lineColor: [this.thresholdSettings.lineColor, []],

2
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-thresholds-panel.component.html

@ -21,6 +21,7 @@
<div class="tb-form-table-header"> <div class="tb-form-table-header">
<div class="tb-form-table-header-cell tb-threshold-source-header" translate>widgets.time-series-chart.threshold.source</div> <div class="tb-form-table-header-cell tb-threshold-source-header" translate>widgets.time-series-chart.threshold.source</div>
<div class="tb-form-table-header-cell tb-threshold-key-value-header" translate>widgets.time-series-chart.threshold.key-value</div> <div class="tb-form-table-header-cell tb-threshold-key-value-header" translate>widgets.time-series-chart.threshold.key-value</div>
<div class="tb-form-table-header-cell tb-y-axis-header" translate>widgets.time-series-chart.axis.y-axis</div>
<div class="tb-form-table-header-cell tb-color-header" translate>widgets.color.color</div> <div class="tb-form-table-header-cell tb-color-header" translate>widgets.color.color</div>
<div class="tb-form-table-header-cell tb-units-header" translate>widget-config.units-short</div> <div class="tb-form-table-header-cell tb-units-header" translate>widget-config.units-short</div>
<div class="tb-form-table-header-cell tb-decimals-header" translate>widget-config.decimals-short</div> <div class="tb-form-table-header-cell tb-decimals-header" translate>widget-config.decimals-short</div>
@ -30,6 +31,7 @@
<div *ngFor="let thresholdControl of thresholdsFormArray().controls; trackBy: trackByThreshold; let $index = index; let $last = last"> <div *ngFor="let thresholdControl of thresholdsFormArray().controls; trackBy: trackByThreshold; let $index = index; let $last = last">
<tb-time-series-chart-threshold-row fxFlex <tb-time-series-chart-threshold-row fxFlex
[formControl]="thresholdControl" [formControl]="thresholdControl"
[yAxisIds]="yAxisIds"
(thresholdRemoved)="removeThreshold($index)"> (thresholdRemoved)="removeThreshold($index)">
</tb-time-series-chart-threshold-row> </tb-time-series-chart-threshold-row>
<mat-divider *ngIf="!$last"></mat-divider> <mat-divider *ngIf="!$last"></mat-divider>

11
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-thresholds-panel.component.scss

@ -33,6 +33,11 @@
} }
} }
&.tb-y-axis-header {
width: 100px;
min-width: 100px;
}
&.tb-units-header { &.tb-units-header {
width: 80px; width: 80px;
min-width: 80px; min-width: 80px;
@ -53,6 +58,12 @@
min-width: 80px; min-width: 80px;
} }
&.tb-y-axis-header {
@media #{$mat-xs} {
display: none;
}
}
&.tb-units-header, &.tb-color-header, &.tb-decimals-header { &.tb-units-header, &.tb-color-header, &.tb-decimals-header {
display: none; display: none;
@media #{$mat-gt-md} { @media #{$mat-gt-md} {

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

@ -31,7 +31,7 @@ import {
timeSeriesChartThresholdDefaultSettings, timeSeriesChartThresholdDefaultSettings,
TimeSeriesChartThresholdType, TimeSeriesChartThresholdType,
timeSeriesChartThresholdValid, timeSeriesChartThresholdValid,
timeSeriesChartThresholdValidator timeSeriesChartThresholdValidator, TimeSeriesChartYAxisId
} from '@home/components/widget/lib/chart/time-series-chart.models'; } from '@home/components/widget/lib/chart/time-series-chart.models';
import { mergeDeep } from '@core/utils'; import { mergeDeep } from '@core/utils';
import { IAliasController } from '@core/api/widget-api.models'; import { IAliasController } from '@core/api/widget-api.models';
@ -74,6 +74,9 @@ export class TimeSeriesChartThresholdsPanelComponent implements ControlValueAcce
@Input() @Input()
widgetConfig: WidgetConfig; widgetConfig: WidgetConfig;
@Input()
yAxisIds: TimeSeriesChartYAxisId[];
thresholdsFormGroup: UntypedFormGroup; thresholdsFormGroup: UntypedFormGroup;
private propagateChange = (_val: any) => {}; private propagateChange = (_val: any) => {};

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

@ -0,0 +1,63 @@
<!--
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.
-->
<div class="tb-form-panel tb-y-axes-panel">
<div class="tb-form-panel-title">{{ 'widgets.time-series-chart.y-axes' | translate }}</div>
<div class="tb-form-table">
<div class="tb-form-table-header no-padding-right">
<div class="tb-form-table-header-cell tb-axis-id-header" translate>widgets.time-series-chart.axis.id</div>
<div class="tb-form-table-header-cell tb-show-header" translate>widgets.time-series-chart.axis.show</div>
<div class="tb-form-table-header-cell tb-label-header" translate>widgets.time-series-chart.axis.label</div>
<div class="tb-form-table-header-cell tb-position-header" translate>widgets.time-series-chart.axis.position</div>
<div class="tb-form-table-header-cell tb-min-header" translate>widgets.time-series-chart.axis.min</div>
<div class="tb-form-table-header-cell tb-max-header" translate>widgets.time-series-chart.axis.max</div>
<div class="tb-form-table-header-cell tb-units-header" translate>widget-config.units-short</div>
<div class="tb-form-table-header-cell tb-decimals-header" translate>widget-config.decimals-short</div>
<div class="tb-form-table-header-cell tb-actions-header"></div>
</div>
<div class="tb-form-table-body tb-drop-list"
cdkDropList cdkDropListOrientation="vertical"
[cdkDropListDisabled]="!dragEnabled"
(cdkDropListDropped)="axisDrop($event)">
<div cdkDrag [cdkDragDisabled]="!dragEnabled"
class="tb-draggable-form-table-row"
*ngFor="let axisControl of axesFormArray().controls; trackBy: trackByAxis; let $index = index;">
<tb-time-series-chart-y-axis-row fxFlex
[formControl]="axisControl"
[advanced]="advanced"
(axisRemoved)="removeAxis($index)">
</tb-time-series-chart-y-axis-row>
<div class="tb-form-table-row-cell-buttons">
<button fxHide.lt-lg
mat-icon-button
type="button"
cdkDragHandle
[ngClass]="{'tb-hidden': !dragEnabled}"
matTooltip="{{ 'action.drag' | translate }}"
matTooltipPosition="above">
<mat-icon>drag_indicator</mat-icon>
</button>
</div>
</div>
</div>
</div>
<div>
<button type="button" mat-stroked-button color="primary" (click)="addAxis()">
{{ 'widgets.time-series-chart.axis.add-y-axis' | translate }}
</button>
</div>
</div>

87
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;
}
}
}

180
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<TimeSeriesChartYAxisId>();
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<string[]>) {
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<TimeSeriesChartYAxisSettings>({} 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<AbstractControl> = [];
axes.forEach((axis) => {
axesControls.push(this.fb.control(axis, [timeSeriesChartYAxisValidator]));
});
return this.fb.array(axesControls);
}
}

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

@ -0,0 +1,72 @@
<!--
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.
-->
<div [formGroup]="axisFormGroup" class="tb-form-table-row tb-axis-row">
<div class="tb-axis-id-field">
{{ modelValue.id }}
</div>
<mat-checkbox class="tb-show-field" formControlName="show"></mat-checkbox>
<mat-form-field class="tb-inline-field tb-label-field" appearance="outline" subscriptSizing="dynamic">
<input matInput formControlName="label" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
<mat-form-field class="tb-inline-field tb-position-field" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="position">
<mat-option *ngFor="let position of axisPositions" [value]="position">
{{ timeSeriesAxisPositionTranslations.get(position) | translate }}
</mat-option>
</mat-select>
</mat-form-field>
<div class="tb-min-field">
<mat-form-field appearance="outline" class="tb-inline-field number" subscriptSizing="dynamic">
<input matInput formControlName="min" type="number" placeholder="{{ 'widgets.time-series-chart.axis.scale-auto' | translate }}">
</mat-form-field>
</div>
<div class="tb-max-field">
<mat-form-field appearance="outline" class="tb-inline-field number" subscriptSizing="dynamic">
<input matInput formControlName="max" type="number" placeholder="{{ 'widgets.time-series-chart.axis.scale-auto' | translate }}">
</mat-form-field>
</div>
<div class="tb-units-field">
<tb-unit-input formControlName="units">
</tb-unit-input>
</div>
<div class="tb-decimals-field">
<mat-form-field appearance="outline" class="tb-inline-field number" subscriptSizing="dynamic">
<input matInput formControlName="decimals" type="number" min="0" max="15" step="1" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
</div>
<div class="tb-form-table-row-cell-buttons">
<button type="button"
mat-icon-button
#matButton
(click)="editAxis($event, matButton)"
matTooltip="{{ 'widgets.time-series-chart.axis.y-axis-settings' | translate }}"
matTooltipPosition="above">
<mat-icon>settings</mat-icon>
</button>
<div class="tb-remove-button">
<button *ngIf="modelValue.id !== 'default'"
type="button"
mat-icon-button
(click)="axisRemoved.emit()"
matTooltip="{{ 'widgets.time-series-chart.axis.remove-y-axis' | translate }}"
matTooltipPosition="above">
<mat-icon>delete</mat-icon>
</button>
</div>
</div>
</div>

95
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;
}
}

201
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);
}
}

43
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 @@
<!--
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.
-->
<div class="tb-y-axis-settings-panel" [formGroup]="yAxisSettingsFormGroup">
<div class="tb-y-axis-settings-title">{{ 'widgets.time-series-chart.axis.y-axis-settings' | translate }}</div>
<div class="tb-y-axis-settings-panel-content">
<tb-time-series-chart-axis-settings
formControlName="yAxis"
alwaysExpanded
[advanced]="advanced"
axisType="yAxis">
</tb-time-series-chart-axis-settings>
</div>
<div class="tb-y-axis-settings-panel-buttons">
<button mat-button
color="primary"
type="button"
(click)="cancel()">
{{ 'action.cancel' | translate }}
</button>
<button mat-raised-button
color="primary"
type="button"
(click)="applyYAxisSettings()"
[disabled]="yAxisSettingsFormGroup.invalid || !yAxisSettingsFormGroup.dirty">
{{ 'action.apply' | translate }}
</button>
</div>
</div>

49
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;
}
}

66
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<TimeSeriesChartYAxisSettingsPanelComponent>;
@Output()
yAxisSettingsApplied = new EventEmitter<TimeSeriesChartYAxisSettings>();
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);
}
}

20
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget-settings-common.module.ts

@ -109,6 +109,18 @@ import {
import { import {
TimeSeriesNoAggregationBarWidthSettingsComponent TimeSeriesNoAggregationBarWidthSettingsComponent
} from '@home/components/widget/lib/settings/common/chart/time-series-no-aggregation-bar-width-settings.component'; } 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';
import {
TimeSeriesChartAnimationSettingsComponent
} from '@home/components/widget/lib/settings/common/chart/time-series-chart-animation-settings.component';
@NgModule({ @NgModule({
declarations: [ declarations: [
@ -150,6 +162,10 @@ import {
TimeSeriesChartThresholdRowComponent, TimeSeriesChartThresholdRowComponent,
TimeSeriesChartThresholdSettingsPanelComponent, TimeSeriesChartThresholdSettingsPanelComponent,
TimeSeriesNoAggregationBarWidthSettingsComponent, TimeSeriesNoAggregationBarWidthSettingsComponent,
TimeSeriesChartYAxesPanelComponent,
TimeSeriesChartYAxisRowComponent,
TimeSeriesChartYAxisSettingsPanelComponent,
TimeSeriesChartAnimationSettingsComponent,
DataKeyInputComponent, DataKeyInputComponent,
EntityAliasInputComponent EntityAliasInputComponent
], ],
@ -197,6 +213,10 @@ import {
TimeSeriesChartThresholdRowComponent, TimeSeriesChartThresholdRowComponent,
TimeSeriesChartThresholdSettingsPanelComponent, TimeSeriesChartThresholdSettingsPanelComponent,
TimeSeriesNoAggregationBarWidthSettingsComponent, TimeSeriesNoAggregationBarWidthSettingsComponent,
TimeSeriesChartYAxesPanelComponent,
TimeSeriesChartYAxisRowComponent,
TimeSeriesChartYAxisSettingsPanelComponent,
TimeSeriesChartAnimationSettingsComponent,
DataKeyInputComponent, DataKeyInputComponent,
EntityAliasInputComponent EntityAliasInputComponent
], ],

19
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> = T extends object ? {
[P in keyof T]?: DeepPartial<T[P]>;
} : T;

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

@ -6612,6 +6612,7 @@
"stack-mode": "Stack mode", "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.", "stack-mode-hint": "Stacks series on the chart. The series with the same unit would be put on top of each other.",
"axes": "Axes", "axes": "Axes",
"y-axes": "Y axes",
"line-type": "Line type", "line-type": "Line type",
"line-type-solid": "Solid", "line-type-solid": "Solid",
"line-type-dashed": "Dashed", "line-type-dashed": "Dashed",
@ -6673,6 +6674,9 @@
"axes": "Axes", "axes": "Axes",
"x-axis": "X axis", "x-axis": "X axis",
"y-axis": "Y axis", "y-axis": "Y axis",
"y-axis-settings": "Y axis settings",
"remove-y-axis": "Remove Y axis",
"id": "Id",
"label": "Label", "label": "Label",
"position": "Position", "position": "Position",
"position-left": "Left", "position-left": "Left",
@ -6689,7 +6693,21 @@
"scale": "Scale", "scale": "Scale",
"scale-min": "min", "scale-min": "min",
"scale-max": "max", "scale-max": "max",
"scale-auto": "Auto" "scale-auto": "Auto",
"min": "Min",
"max": "Max",
"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": { "series": {
"legend-settings": "Legend settings", "legend-settings": "Legend settings",

Loading…
Cancel
Save