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);
Validator.validateString(attributeKey, "Incorrect attribute key " + attributeKey);
AttributeCacheKey attributeCacheKey = new AttributeCacheKey(scope, entityId, attributeKey);
TbCacheValueWrapper<AttributeKvEntry> cachedAttributeValue = cache.get(attributeCacheKey);
if (cachedAttributeValue != null) {
hitCounter.increment();
AttributeKvEntry cachedAttributeKvEntry = cachedAttributeValue.get();
return Futures.immediateFuture(Optional.ofNullable(cachedAttributeKvEntry));
} else {
missCounter.increment();
return cacheExecutor.submit(() -> {
return cacheExecutor.submit(() -> {
AttributeCacheKey attributeCacheKey = new AttributeCacheKey(scope, entityId, attributeKey);
TbCacheValueWrapper<AttributeKvEntry> cachedAttributeValue = cache.get(attributeCacheKey);
if (cachedAttributeValue != null) {
hitCounter.increment();
AttributeKvEntry cachedAttributeKvEntry = cachedAttributeValue.get();
return Optional.ofNullable(cachedAttributeKvEntry);
} else {
missCounter.increment();
var cacheTransaction = cache.newTransactionForKey(attributeCacheKey);
try {
Optional<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);
throw e;
}
});
}
}
});
}
@Override
@ -207,7 +207,8 @@ public class CachedAttributesService implements AttributesService {
@Override
public ListenableFuture<List<AttributeKvEntry>> findAll(TenantId tenantId, EntityId entityId, String scope) {
validate(entityId, scope);
return Futures.immediateFuture(attributesDao.findAll(tenantId, entityId, scope));
// We can`t watch on cache because the keys are unknown.
return jpaExecutorService.submit(() -> attributesDao.findAll(tenantId, entityId, scope));
}
@Override

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.UUID;
import java.util.function.Function;
import java.util.regex.Pattern;
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
* <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

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

@ -151,12 +151,12 @@ public class SqlTimeseriesLatestDao extends BaseAbstractSqlTimeseriesDao impleme
@Override
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
public ListenableFuture<TsKvEntry> findLatest(TenantId tenantId, EntityId entityId, String key) {
return Futures.immediateFuture(getLatestTsKvEntry(entityId, key));
return service.submit(() -> getLatestTsKvEntry(entityId, key));
}
@Override
@ -221,36 +221,29 @@ public class SqlTimeseriesLatestDao extends BaseAbstractSqlTimeseriesDao impleme
}
protected ListenableFuture<TsKvLatestRemovingResult> getRemoveLatestFuture(TenantId tenantId, EntityId entityId, DeleteTsKvQuery query) {
TsKvEntry latest = doFindLatest(entityId, query.getKey());
if (latest == null) {
return Futures.immediateFuture(new TsKvLatestRemovingResult(query.getKey(), false));
}
long ts = latest.getTs();
ListenableFuture<Boolean> removedLatestFuture;
if (ts >= query.getStartTs() && ts < query.getEndTs()) {
TsKvLatestEntity latestEntity = new TsKvLatestEntity();
latestEntity.setEntityId(entityId.getId());
latestEntity.setKey(getOrSaveKeyId(query.getKey()));
removedLatestFuture = service.submit(() -> {
ListenableFuture<TsKvEntry> latestFuture = service.submit(() -> doFindLatest(entityId, query.getKey()));
return Futures.transformAsync(latestFuture, latest -> {
if (latest == null) {
return Futures.immediateFuture(new TsKvLatestRemovingResult(query.getKey(), false));
}
boolean isRemoved = false;
long ts = latest.getTs();
if (ts >= query.getStartTs() && ts < query.getEndTs()) {
TsKvLatestEntity latestEntity = new TsKvLatestEntity();
latestEntity.setEntityId(entityId.getId());
latestEntity.setKey(getOrSaveKeyId(query.getKey()));
tsKvLatestRepository.delete(latestEntity);
return true;
});
} else {
removedLatestFuture = Futures.immediateFuture(false);
}
return Futures.transformAsync(removedLatestFuture, isRemoved -> {
if (isRemoved && query.getRewriteLatestIfDeleted()) {
return getNewLatestEntryFuture(tenantId, entityId, query);
isRemoved = true;
if (query.getRewriteLatestIfDeleted()) {
return getNewLatestEntryFuture(tenantId, entityId, query);
}
}
return Futures.immediateFuture(new TsKvLatestRemovingResult(query.getKey(), isRemoved));
}, MoreExecutors.directExecutor());
}
protected ListenableFuture<List<TsKvEntry>> getFindAllLatestFuture(EntityId entityId) {
return Futures.immediateFuture(
return service.submit(() ->
DaoUtil.convertDataList(Lists.newArrayList(
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;
import com.google.common.base.Function;
import com.google.common.collect.Lists;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
@ -43,6 +42,7 @@ import org.thingsboard.server.dao.entityview.EntityViewService;
import org.thingsboard.server.dao.exception.IncorrectParameterException;
import org.thingsboard.server.dao.service.Validator;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
@ -126,18 +126,22 @@ public class BaseTimeseriesService implements TimeseriesService {
@Override
public ListenableFuture<List<TsKvEntry>> findLatest(TenantId tenantId, EntityId entityId, Collection<String> keys) {
validate(entityId);
List<ListenableFuture<TsKvEntry>> futures = Lists.newArrayListWithExpectedSize(keys.size());
keys.forEach(key -> Validator.validateString(key, "Incorrect key " + key));
keys.forEach(key -> futures.add(timeseriesLatestDao.findLatest(tenantId, entityId, key)));
List<ListenableFuture<TsKvEntry>> futures = new ArrayList<>(keys.size());
keys.forEach(key -> Validator.validateString(key, k -> "Incorrect key " + k));
for (String key : keys) {
futures.add(timeseriesLatestDao.findLatest(tenantId, entityId, key));
}
return Futures.allAsList(futures);
}
@Override
public List<TsKvEntry> findLatestSync(TenantId tenantId, EntityId entityId, Collection<String> keys) {
validate(entityId);
List<TsKvEntry> latestEntries = Lists.newArrayListWithExpectedSize(keys.size());
keys.forEach(key -> Validator.validateString(key, "Incorrect key " + key));
keys.forEach(key -> latestEntries.add(timeseriesLatestDao.findLatestSync(tenantId, entityId, key)));
List<TsKvEntry> latestEntries = new ArrayList<>(keys.size());
keys.forEach(key -> Validator.validateString(key, k -> "Incorrect key " + k));
for (String key : keys) {
latestEntries.add(timeseriesLatestDao.findLatestSync(tenantId, entityId, key));
}
return latestEntries;
}
@ -165,7 +169,7 @@ public class BaseTimeseriesService implements TimeseriesService {
@Override
public ListenableFuture<Integer> save(TenantId tenantId, EntityId entityId, TsKvEntry tsKvEntry) {
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);
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) {
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) {
if (saveLatest) {
saveAndRegisterFutures(tenantId, futures, entityId, tsKvEntry, ttl);
@ -195,7 +199,7 @@ public class BaseTimeseriesService implements TimeseriesService {
@Override
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) {
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) {
validate(entityId);
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) {
deleteAndRegisterFutures(tenantId, futures, entityId, tsKvQuery);
}
@ -252,7 +256,7 @@ public class BaseTimeseriesService implements TimeseriesService {
@Override
public ListenableFuture<List<TsKvLatestRemovingResult>> removeLatest(TenantId tenantId, EntityId entityId, Collection<String> keys) {
validate(entityId);
List<ListenableFuture<TsKvLatestRemovingResult>> futures = Lists.newArrayListWithExpectedSize(keys.size());
List<ListenableFuture<TsKvLatestRemovingResult>> futures = new ArrayList<>(keys.size());
for (String key : keys) {
DeleteTsKvQuery query = new BaseDeleteTsKvQuery(key, 0, System.currentTimeMillis(), false);
futures.add(timeseriesLatestDao.removeLatest(tenantId, entityId, query));
@ -281,7 +285,7 @@ public class BaseTimeseriesService implements TimeseriesService {
}
private static void validate(EntityId entityId) {
Validator.validateEntityId(entityId, "Incorrect entityId " + entityId);
Validator.validateEntityId(entityId, id -> "Incorrect entityId " + id);
}
private void validate(ReadTsKvQuery query) {

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) {
Set<TbPair<String, List<String>>> failuresPairSet = ConcurrentHashMap.newKeySet();
var getKvEntryPairFutures = Futures.allAsList(
getLatestTelemetry(ctx, entityId, TbNodeUtils.processPatterns(config.getLatestTsKeyNames(), msg), failuresPairSet),
getAttrAsync(ctx, entityId, CLIENT_SCOPE, TbNodeUtils.processPatterns(config.getClientAttributeNames(), msg), failuresPairSet),
getAttrAsync(ctx, entityId, SHARED_SCOPE, TbNodeUtils.processPatterns(config.getSharedAttributeNames(), msg), failuresPairSet),
getAttrAsync(ctx, entityId, SERVER_SCOPE, TbNodeUtils.processPatterns(config.getServerAttributeNames(), msg), failuresPairSet)
getAttrAsync(ctx, entityId, SERVER_SCOPE, TbNodeUtils.processPatterns(config.getServerAttributeNames(), msg), failuresPairSet),
getLatestTelemetry(ctx, entityId, TbNodeUtils.processPatterns(config.getLatestTsKeyNames(), msg), failuresPairSet)
);
withCallback(getKvEntryPairFutures, futuresList -> {
var msgMetaData = msg.getMetaData().copy();

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

@ -25,6 +25,10 @@
forceSingleDatasource
formControlName="datasources">
</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
panelTitle="{{ 'widgets.chart.series' | translate }}"
addKeyTitle="{{ 'widgets.chart.add-series' | translate }}"
@ -32,6 +36,8 @@
removeKeyTitle="{{ 'widgets.chart.remove-series' | translate }}"
noKeysText="{{ 'widgets.chart.no-series' | translate }}"
requiredKeysText="{{ 'widgets.chart.no-series-error' | translate }}"
timeSeriesChart
[yAxisIds]="yAxisIds"
[showTimeSeriesType]="chartType === TimeSeriesChartType.default"
hideSourceSelection
[datasourceType]="datasource?.type"
@ -44,7 +50,8 @@
[aliasController]="aliasController"
[dataKeyCallbacks]="callbacks"
[datasource]="datasource"
[widgetConfig]="widgetConfig?.config">
[widgetConfig]="widgetConfig?.config"
[yAxisIds]="yAxisIds">
</tb-time-series-chart-thresholds-panel>
<div class="tb-form-panel">
<div class="tb-form-panel-title" translate>widget-config.appearance</div>
@ -104,11 +111,7 @@
</div>
</div>
<div class="tb-form-panel">
<div class="tb-form-panel-title" translate>widgets.time-series-chart.axis.axes</div>
<tb-time-series-chart-axis-settings
formControlName="yAxis"
axisType="yAxis">
</tb-time-series-chart-axis-settings>
<div class="tb-form-panel-title" translate>widgets.time-series-chart.axis.x-axis</div>
<tb-time-series-chart-axis-settings
formControlName="xAxis"
axisType="xAxis">
@ -220,6 +223,9 @@
</ng-template>
</mat-expansion-panel>
</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-title" translate>widget-config.card-appearance</div>
<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
} from '@home/components/widget/lib/chart/time-series-chart-widget.models';
import { EChartsTooltipTrigger } from '@home/components/widget/lib/chart/echarts-widget.models';
import { TimeSeriesChartType } from '@home/components/widget/lib/chart/time-series-chart.models';
import {
TimeSeriesChartKeySettings, TimeSeriesChartThreshold,
TimeSeriesChartType, TimeSeriesChartYAxes,
TimeSeriesChartYAxisId
} from '@home/components/widget/lib/chart/time-series-chart.models';
@Component({
selector: 'tb-time-series-chart-basic-config',
@ -63,6 +67,11 @@ export class TimeSeriesChartBasicConfigComponent extends BasicWidgetConfigCompon
}
}
public get yAxisIds(): TimeSeriesChartYAxisId[] {
const yAxes: TimeSeriesChartYAxes = this.timeSeriesChartWidgetConfigForm.get('yAxes').value;
return Object.keys(yAxes);
}
TimeSeriesChartType = TimeSeriesChartType;
EChartsTooltipTrigger = EChartsTooltipTrigger;
@ -86,6 +95,15 @@ export class TimeSeriesChartBasicConfigComponent extends BasicWidgetConfigCompon
super(store, widgetConfigComponent);
}
public yAxisRemoved(yAxisId: TimeSeriesChartYAxisId): void {
if (this.widgetConfig.config.datasources && this.widgetConfig.config.datasources.length > 1) {
for (let i = 1; i < this.widgetConfig.config.datasources.length; i++) {
const datasource = this.widgetConfig.config.datasources[i];
this.removeYaxisId(datasource.dataKeys, yAxisId);
}
}
}
protected configForm(): UntypedFormGroup {
return this.timeSeriesChartWidgetConfigForm;
}
@ -109,6 +127,8 @@ export class TimeSeriesChartBasicConfigComponent extends BasicWidgetConfigCompon
this.timeSeriesChartWidgetConfigForm = this.fb.group({
timewindowConfig: [getTimewindowConfig(configData.config), []],
datasources: [configData.config.datasources, []],
yAxes: [settings.yAxes, []],
series: [this.getSeries(configData.config.datasources), []],
thresholds: [settings.thresholds, []],
@ -126,7 +146,6 @@ export class TimeSeriesChartBasicConfigComponent extends BasicWidgetConfigCompon
dataZoom: [settings.dataZoom, []],
stack: [settings.stack, []],
yAxis: [settings.yAxis, []],
xAxis: [settings.xAxis, []],
noAggregationBarWidthSettings: [settings.noAggregationBarWidthSettings, []],
@ -149,6 +168,8 @@ export class TimeSeriesChartBasicConfigComponent extends BasicWidgetConfigCompon
tooltipBackgroundColor: [settings.tooltipBackgroundColor, []],
tooltipBackgroundBlur: [settings.tooltipBackgroundBlur, []],
animation: [settings.animation, []],
background: [settings.background, []],
cardButtons: [this.getCardButtons(configData.config), []],
@ -180,7 +201,7 @@ export class TimeSeriesChartBasicConfigComponent extends BasicWidgetConfigCompon
this.widgetConfig.config.settings.dataZoom = config.dataZoom;
this.widgetConfig.config.settings.stack = config.stack;
this.widgetConfig.config.settings.yAxis = config.yAxis;
this.widgetConfig.config.settings.yAxes = config.yAxes;
this.widgetConfig.config.settings.xAxis = config.xAxis;
this.widgetConfig.config.settings.noAggregationBarWidthSettings = config.noAggregationBarWidthSettings;
@ -202,6 +223,8 @@ export class TimeSeriesChartBasicConfigComponent extends BasicWidgetConfigCompon
this.widgetConfig.config.settings.tooltipBackgroundColor = config.tooltipBackgroundColor;
this.widgetConfig.config.settings.tooltipBackgroundBlur = config.tooltipBackgroundBlur;
this.widgetConfig.config.settings.animation = config.animation;
this.widgetConfig.config.settings.background = config.background;
this.setCardButtons(config.cardButtons, this.widgetConfig.config);
@ -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[] {
const buttons: string[] = [];
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">
<input matInput formControlName="label" placeholder="{{ 'widget-config.set' | translate }}">
</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">
<tb-color-input asBoxInput
formControlName="color">
@ -54,21 +78,6 @@
<div *ngIf="singleRow" matSuffix fxHide.lt-md translate>widget-config.decimals-suffix</div>
</mat-form-field>
</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 fxHide.lt-lg class="tb-settings-button">
<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;
}
.tb-y-axis-field {
width: 100px;
min-width: 100px;
.mat-mdc-form-field {
width: 100%;
}
}
.tb-time-series-type-field {
width: 60px;
min-width: 60px;
@ -72,7 +80,16 @@
}
}
.tb-y-axis-field {
@media #{$mat-md} {
display: none;
}
}
.tb-time-series-type-field {
@media #{$mat-xs} {
display: none;
}
@media #{$mat-md} {
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,
EventEmitter,
forwardRef,
Input,
Input, OnChanges,
OnInit,
Output,
Output, SimpleChanges,
ViewEncapsulation
} from '@angular/core';
import {
@ -60,7 +60,7 @@ import {
TimeSeriesChartSeriesType,
timeSeriesChartSeriesTypeIcons,
timeSeriesChartSeriesTypes,
timeSeriesChartSeriesTypeTranslations
timeSeriesChartSeriesTypeTranslations, TimeSeriesChartYAxisId
} from '@home/components/widget/lib/chart/time-series-chart.models';
export const dataKeyValid = (key: DataKey): boolean => !!key && !!key.type && !!key.name;
@ -88,7 +88,7 @@ export const dataKeyRowValidator = (control: AbstractControl): ValidationErrors
],
encapsulation: ViewEncapsulation.None
})
export class DataKeyRowComponent implements ControlValueAccessor, OnInit {
export class DataKeyRowComponent implements ControlValueAccessor, OnInit, OnChanges {
timeSeriesChartSeriesTypes = timeSeriesChartSeriesTypes;
timeSeriesChartSeriesTypeTranslations = timeSeriesChartSeriesTypeTranslations;
@ -138,10 +138,17 @@ export class DataKeyRowComponent implements ControlValueAccessor, OnInit {
@coerceBoolean()
hideDecimals = false;
@Input()
@coerceBoolean()
timeSeriesChart = false;
@Input()
@coerceBoolean()
showTimeSeriesType = false;
@Input()
yAxisIds: TimeSeriesChartYAxisId[];
@Input()
@coerceBoolean()
singleRow = true;
@ -237,7 +244,8 @@ export class DataKeyRowComponent implements ControlValueAccessor, OnInit {
if (this.hasAdditionalLatestDataKeys) {
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));
}
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 {
this.propagateChange = fn;
}
@ -278,10 +300,12 @@ export class DataKeyRowComponent implements ControlValueAccessor, OnInit {
latest: (value as any)?.latest
}, {emitEvent: false});
}
if (this.showTimeSeriesType) {
if (this.timeSeriesChart) {
const settings = value?.settings as TimeSeriesChartKeySettings;
const yAxis = settings?.yAxisId || 'default';
const timeSeriesType = settings?.type || TimeSeriesChartSeriesType.line;
this.keyRowFormGroup.patchValue({
yAxis,
timeSeriesType
}, {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('units').patchValue(this.modelValue.units, {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.keyFormControl.patchValue(deepClone(this.modelValue), {emitEvent: false});
@ -335,7 +360,8 @@ export class DataKeyRowComponent implements ControlValueAccessor, OnInit {
if (!this.keyRowFormGroup.get('label').value) {
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});
}
return key;
@ -346,8 +372,10 @@ export class DataKeyRowComponent implements ControlValueAccessor, OnInit {
if (this.modelValue !== null) {
const value: DataKey = this.keyRowFormGroup.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;
delete (this.modelValue as any).yAxis;
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 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 *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="!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="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>
<div *ngIf="keysFormArray().controls.length; else noKeys" class="tb-form-table-body tb-drop-list"
@ -44,7 +45,9 @@
[hasAdditionalLatestDataKeys]="hasAdditionalLatestDataKeys"
[hideDataKeyColor]="hideDataKeyColor"
[hideDecimals]="hideDecimals"
[timeSeriesChart]="timeSeriesChart"
[showTimeSeriesType]="showTimeSeriesType"
[yAxisIds]="yAxisIds"
[hideUnits]="hideUnits"
[hideDataKeyDecimals]="hideDataKeyDecimals"
[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;
}
&.tb-y-axis-header {
width: 100px;
min-width: 100px;
}
&.tb-time-series-type-header {
width: 60px;
min-width: 60px;
@ -70,7 +75,17 @@
display: block;
}
}
&.tb-y-axis-header {
@media #{$mat-md} {
display: none;
}
}
&.tb-time-series-type-header {
@media #{$mat-xs} {
display: none;
}
@media #{$mat-md} {
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 { DataKeysCallbacks, DataKeySettingsFunction } from '@home/components/widget/config/data-keys.component.models';
import { coerceBoolean } from '@shared/decorators/coercion';
import { TimeSeriesChartYAxisId } from '@home/components/widget/lib/chart/time-series-chart.models';
@Component({
selector: 'tb-data-keys-panel',
@ -119,10 +120,17 @@ export class DataKeysPanelComponent implements ControlValueAccessor, OnInit, OnC
@coerceBoolean()
hideSourceSelection = false;
@Input()
@coerceBoolean()
timeSeriesChart = false;
@Input()
@coerceBoolean()
showTimeSeriesType = false;
@Input()
yAxisIds: TimeSeriesChartYAxisId[];
dataKeyType: DataKeyType;
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 { TbTimeSeriesChart } from '@home/components/widget/lib/chart/time-series-chart';
import {
TimeSeriesChartAxisSettings,
TimeSeriesChartKeySettings,
TimeSeriesChartSeriesType,
TimeSeriesChartSettings
TimeSeriesChartSettings, TimeSeriesChartYAxisSettings
} from '@home/components/widget/lib/chart/time-series-chart.models';
import { DeepPartial } from '@shared/models/common';
const valuesLayoutHeight = 66;
const valuesLayoutVerticalPadding = 16;
@ -172,25 +174,27 @@ export class AggregatedValueCardWidgetComponent implements OnInit, AfterViewInit
ngAfterViewInit(): void {
if (this.showChart && this.ctx.datasources?.length) {
const settings: TimeSeriesChartSettings = {
const settings: DeepPartial<TimeSeriesChartSettings> = {
dataZoom: false,
xAxis: {
show: false
},
yAxis: {
show: true,
showLine: false,
showTicks: false,
showTickLabels: false,
showSplitLines: true,
min: 'dataMin',
max: 'dataMax',
intervalCalculator:
'var scale = axis.scale; return !scale.isBlank() ? ((scale.getExtent()[1] - scale.getExtent()[0]) / 2) : undefined;'
yAxes: {
default: {
show: true,
showLine: false,
showTicks: false,
showTickLabels: false,
showSplitLines: true,
min: 'dataMin',
max: 'dataMax',
intervalCalculator:
'var scale = axis.scale; return !scale.isBlank() ? ((scale.getExtent()[1] - scale.getExtent()[0]) / 2) : undefined;'
}
},
tooltipDateInterval: false,
tooltipDateFormat: simpleDateFormat('dd MMM yyyy HH:mm:ss')
} as TimeSeriesChartSettings;
};
this.lineChart = new TbTimeSeriesChart(this.ctx, settings, this.chartElement.nativeElement, this.renderer, true);

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

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

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 { XAXisOption, YAXisOption } from 'echarts/types/dist/shared';
import { CustomSeriesOption, LineSeriesOption } from 'echarts/charts';
import { formatValue, isDefinedAndNotNull, isUndefined, isUndefinedOrNull, parseFunction } from '@core/utils';
import {
formatValue,
isDefinedAndNotNull,
isNumeric,
isUndefined,
isUndefinedOrNull,
mergeDeep,
parseFunction
} from '@core/utils';
import { LinearGradientObject } from 'zrender/lib/graphic/LinearGradient';
import tinycolor from 'tinycolor2';
import Axis2D from 'echarts/types/src/coord/cartesian/Axis2D';
@ -289,13 +297,82 @@ export interface TimeSeriesChartAxisSettings {
splitLinesColor: string;
}
export type TimeSeriesChartYAxisId = 'default' | string;
export interface TimeSeriesChartYAxisSettings extends TimeSeriesChartAxisSettings {
id?: TimeSeriesChartYAxisId;
order?: number;
units?: string;
decimals?: number;
min?: number | string;
max?: number | string;
intervalCalculator?: string;
ticksFormatter?: string;
}
export const timeSeriesChartYAxisValid = (axis: TimeSeriesChartYAxisSettings): boolean =>
!(!axis.id || isUndefinedOrNull(axis.order));
export const timeSeriesChartYAxisValidator = (control: AbstractControl): ValidationErrors | null => {
const axis: TimeSeriesChartYAxisSettings = control.value;
if (!timeSeriesChartYAxisValid(axis)) {
return {
axis: true
};
}
return null;
};
export const getNextTimeSeriesYAxisId = (axes: TimeSeriesChartYAxisSettings[]): TimeSeriesChartYAxisId => {
let id = 0;
for (const axis of axes) {
const existingId = axis.id;
if (existingId.startsWith('axis')) {
const idSuffix = existingId.substring('axis'.length);
if (isNumeric(idSuffix)) {
id = Math.max(id, Number(idSuffix));
}
}
}
return 'axis' + (id+1);
};
export const defaultTimeSeriesChartYAxisSettings: TimeSeriesChartYAxisSettings = {
units: null,
decimals: 0,
show: true,
label: '',
labelFont: {
family: 'Roboto',
size: 12,
sizeUnit: 'px',
style: 'normal',
weight: '600',
lineHeight: '1'
},
labelColor: timeSeriesChartColorScheme['axis.label'].light,
position: AxisPosition.left,
showTickLabels: true,
tickLabelFont: {
family: 'Roboto',
size: 12,
sizeUnit: 'px',
style: 'normal',
weight: '400',
lineHeight: '1'
},
tickLabelColor: timeSeriesChartColorScheme['axis.tickLabel'].light,
ticksFormatter: null,
showTicks: true,
ticksColor: timeSeriesChartColorScheme['axis.ticks'].light,
showLine: true,
lineColor: timeSeriesChartColorScheme['axis.line'].light,
showSplitLines: true,
splitLinesColor: timeSeriesChartColorScheme['axis.splitLine'].light
};
export type TimeSeriesChartYAxes = {[id: TimeSeriesChartYAxisId]: TimeSeriesChartYAxisSettings};
export interface TimeSeriesChartThreshold {
type: TimeSeriesChartThresholdType;
value?: number;
@ -304,6 +381,7 @@ export interface TimeSeriesChartThreshold {
entityAlias?: string;
entityKey?: string;
entityKeyType?: DataKeyType.attribute | DataKeyType.timeseries;
yAxisId: TimeSeriesChartYAxisId;
units?: string;
decimals?: number;
lineColor: string;
@ -320,7 +398,7 @@ export interface TimeSeriesChartThreshold {
}
export const timeSeriesChartThresholdValid = (threshold: TimeSeriesChartThreshold): boolean => {
if (!threshold.type) {
if (!threshold.type || !threshold.yAxisId) {
return false;
}
switch (threshold.type) {
@ -355,7 +433,8 @@ export const timeSeriesChartThresholdValidator = (control: AbstractControl): Val
export const timeSeriesChartThresholdDefaultSettings: TimeSeriesChartThreshold = {
type: TimeSeriesChartThresholdType.constant,
units: '',
yAxisId: 'default',
units: null,
decimals: 0,
lineColor: timeSeriesChartColorScheme['threshold.line'].light,
lineType: TimeSeriesChartLineType.solid,
@ -404,13 +483,61 @@ export interface TimeSeriesChartNoAggregationBarWidthSettings {
barWidth?: TimeSeriesChartBarWidth;
}
export enum TimeSeriesChartAnimationEasing {
linear = 'linear',
quadraticIn = 'quadraticIn',
quadraticOut = 'quadraticOut',
quadraticInOut = 'quadraticInOut',
cubicIn = 'cubicIn',
cubicOut = 'cubicOut',
cubicInOut = 'cubicInOut',
quarticIn = 'quarticIn',
quarticOut = 'quarticOut',
quarticInOut = 'quarticInOut',
quinticIn = 'quinticIn',
quinticOut = 'quinticOut',
quinticInOut = 'quinticInOut',
sinusoidalIn = 'sinusoidalIn',
sinusoidalOut = 'sinusoidalOut',
sinusoidalInOut = 'sinusoidalInOut',
exponentialIn = 'exponentialIn',
exponentialOut = 'exponentialOut',
exponentialInOut = 'exponentialInOut',
circularIn = 'circularIn',
circularOut = 'circularOut',
circularInOut = 'circularInOut',
elasticIn = 'elasticIn',
elasticOut = 'elasticOut',
elasticInOut = 'elasticInOut',
backIn = 'backIn',
backOut = 'backOut',
backInOut = 'backInOut',
bounceIn = 'bounceIn',
bounceOut = 'bounceOut',
bounceInOut = 'bounceInOut'
}
export const timeSeriesChartAnimationEasings = Object.keys(TimeSeriesChartAnimationEasing) as TimeSeriesChartAnimationEasing[];
export interface TimeSeriesChartAnimationSettings {
animation: boolean;
animationThreshold: number;
animationDuration: number;
animationEasing: TimeSeriesChartAnimationEasing;
animationDelay: number;
animationDurationUpdate: number;
animationEasingUpdate: TimeSeriesChartAnimationEasing;
animationDelayUpdate: number;
}
export interface TimeSeriesChartSettings extends EChartsTooltipWidgetSettings {
thresholds: TimeSeriesChartThreshold[];
darkMode: boolean;
dataZoom: boolean;
stack: boolean;
yAxis: TimeSeriesChartYAxisSettings;
yAxes: TimeSeriesChartYAxes;
xAxis: TimeSeriesChartAxisSettings;
animation: TimeSeriesChartAnimationSettings;
noAggregationBarWidthSettings: TimeSeriesChartNoAggregationBarWidthSettings;
}
@ -419,36 +546,10 @@ export const timeSeriesChartDefaultSettings: TimeSeriesChartSettings = {
darkMode: false,
dataZoom: true,
stack: false,
yAxis: {
show: true,
label: '',
labelFont: {
family: 'Roboto',
size: 12,
sizeUnit: 'px',
style: 'normal',
weight: '600',
lineHeight: '1'
},
labelColor: timeSeriesChartColorScheme['axis.label'].light,
position: AxisPosition.left,
showTickLabels: true,
tickLabelFont: {
family: 'Roboto',
size: 12,
sizeUnit: 'px',
style: 'normal',
weight: '400',
lineHeight: '1'
},
tickLabelColor: timeSeriesChartColorScheme['axis.tickLabel'].light,
ticksFormatter: null,
showTicks: true,
ticksColor: timeSeriesChartColorScheme['axis.ticks'].light,
showLine: true,
lineColor: timeSeriesChartColorScheme['axis.line'].light,
showSplitLines: true,
splitLinesColor: timeSeriesChartColorScheme['axis.splitLine'].light
yAxes: {
default: mergeDeep({} as TimeSeriesChartYAxisSettings,
defaultTimeSeriesChartYAxisSettings,
{ id: 'default', order: 0 } as TimeSeriesChartYAxisSettings)
},
xAxis: {
show: true,
@ -480,6 +581,16 @@ export const timeSeriesChartDefaultSettings: TimeSeriesChartSettings = {
showSplitLines: true,
splitLinesColor: timeSeriesChartColorScheme['axis.splitLine'].light
},
animation: {
animation: true,
animationThreshold: 2000,
animationDuration: 1000,
animationEasing: TimeSeriesChartAnimationEasing.cubicOut,
animationDelay: 0,
animationDurationUpdate: 300,
animationEasingUpdate: TimeSeriesChartAnimationEasing.cubicOut,
animationDelayUpdate: 0
},
noAggregationBarWidthSettings: {
strategy: TimeSeriesChartNoAggregationBarWidthStrategy.group,
groupWidth: {
@ -558,6 +669,7 @@ export interface BarSeriesSettings {
}
export interface TimeSeriesChartKeySettings {
yAxisId: TimeSeriesChartYAxisId;
showInLegend: boolean;
dataHiddenByDefault: boolean;
type: TimeSeriesChartSeriesType;
@ -566,6 +678,7 @@ export interface TimeSeriesChartKeySettings {
}
export const timeSeriesChartKeyDefaultSettings: TimeSeriesChartKeySettings = {
yAxisId: 'default',
showInLegend: true,
dataHiddenByDefault: false,
type: TimeSeriesChartSeriesType.line,
@ -626,6 +739,7 @@ export const timeSeriesChartKeyDefaultSettings: TimeSeriesChartKeySettings = {
};
export interface TimeSeriesChartDataItem extends EChartsSeriesItem {
yAxisId: TimeSeriesChartYAxisId;
yAxisIndex: number;
option?: LineSeriesOption | CustomSeriesOption;
barRenderContext?: BarRenderContext;
@ -635,6 +749,7 @@ type TimeSeriesChartThresholdValue = number | string | (number | string)[];
export interface TimeSeriesChartThresholdItem {
id: string;
yAxisId: TimeSeriesChartYAxisId;
yAxisIndex: number;
latestDataKey?: DataKey;
units?: string;
@ -646,14 +761,16 @@ export interface TimeSeriesChartThresholdItem {
export interface TimeSeriesChartYAxis {
id: string;
units: string;
decimals: number;
settings: TimeSeriesChartYAxisSettings;
option: YAXisOption & ValueAxisBaseOption;
intervalCalculator?: (axis: Axis2D) => number;
ticksFormatter?: (value: any) => string;
}
export const createTimeSeriesYAxis = (axisId: string, units: string,
decimals: number, settings: TimeSeriesChartYAxisSettings,
export const createTimeSeriesYAxis = (units: string,
decimals: number,
settings: TimeSeriesChartYAxisSettings,
darkMode: boolean): TimeSeriesChartYAxis => {
const yAxisTickLabelStyle = createChartTextStyle(settings.tickLabelFont,
settings.tickLabelColor, darkMode, 'axis.tickLabel');
@ -664,13 +781,14 @@ export const createTimeSeriesYAxis = (axisId: string, units: string,
ticksFormatter = parseFunction(settings.ticksFormatter, ['value']);
}
const yAxis: TimeSeriesChartYAxis = {
id: axisId,
units,
id: settings.id,
decimals,
settings,
option: {
show: settings.show,
type: 'value',
position: settings.position,
id: axisId,
id: settings.id,
offset: 0,
alignTicks: true,
scale: true,
@ -843,13 +961,11 @@ const generateChartThresholds = (thresholdItems: TimeSeriesChartThresholdItem[],
id: item.id,
dataGroupId: item.id,
yAxisIndex: item.yAxisIndex,
animation: true,
data: [],
tooltip: {
show: false
},
markLine: {
animation: true,
lineStyle: {
width: item.settings.lineWidth,
color: prepareChartThemeColor(item.settings.lineColor, darkMode, 'threshold.line'),
@ -956,16 +1072,19 @@ const generateChartSeries = (dataItems: TimeSeriesChartDataItem[],
};
export const updateDarkMode = (options: EChartsOption, settings: TimeSeriesChartSettings,
yAxisList: TimeSeriesChartYAxis[],
dataItems: TimeSeriesChartDataItem[], thresholdDataItems: TimeSeriesChartThresholdItem[],
darkMode: boolean): EChartsOption => {
options.darkMode = darkMode;
if (Array.isArray(options.yAxis)) {
for (const yAxis of options.yAxis) {
yAxis.nameTextStyle.color = prepareChartThemeColor(settings.yAxis.labelColor, darkMode, 'axis.label');
yAxis.axisLabel.color = prepareChartThemeColor(settings.yAxis.tickLabelColor, darkMode, 'axis.tickLabel');
yAxis.axisLine.lineStyle.color = prepareChartThemeColor(settings.yAxis.lineColor, darkMode, 'axis.line');
yAxis.axisTick.lineStyle.color = prepareChartThemeColor(settings.yAxis.ticksColor, darkMode, 'axis.ticks');
yAxis.splitLine.lineStyle.color = prepareChartThemeColor(settings.yAxis.splitLinesColor, darkMode, 'axis.splitLine');
for (let i = 0; i < options.yAxis.length; i++) {
const yAxis = options.yAxis[i];
const yAxisSettings = yAxisList[i].settings;
yAxis.nameTextStyle.color = prepareChartThemeColor(yAxisSettings.labelColor, darkMode, 'axis.label');
yAxis.axisLabel.color = prepareChartThemeColor(yAxisSettings.tickLabelColor, darkMode, 'axis.tickLabel');
yAxis.axisLine.lineStyle.color = prepareChartThemeColor(yAxisSettings.lineColor, darkMode, 'axis.line');
yAxis.axisTick.lineStyle.color = prepareChartThemeColor(yAxisSettings.ticksColor, darkMode, 'axis.ticks');
yAxis.splitLine.lineStyle.color = prepareChartThemeColor(yAxisSettings.splitLinesColor, darkMode, 'axis.splitLine');
}
}
if (Array.isArray(options.xAxis)) {
@ -1032,7 +1151,6 @@ const createTimeSeriesChartSeries = (item: TimeSeriesChartDataItem,
emphasis: {
focus: 'series'
},
animation: true,
dimensions: [
{name: 'intervalStart', type: 'number'},
{name: 'intervalEnd', type: 'number'}

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

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

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

@ -29,6 +29,18 @@
</div>
</mat-slide-toggle>
</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 *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>

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

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">
<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
formControlName="thresholds"
[aliasController]="aliasController"
[dataKeyCallbacks]="dataKeyCallbacks"
[datasource]="datasource"
[widgetConfig]="widgetConfig?.config">
[widgetConfig]="widgetConfig?.config"
[yAxisIds]="yAxisIds">
</tb-time-series-chart-thresholds-panel>
<div class="tb-form-panel">
<div class="tb-form-panel-title" translate>widgets.time-series-chart.chart-style</div>
@ -38,12 +44,7 @@
</mat-slide-toggle>
</div>
<div class="tb-form-panel">
<div class="tb-form-panel-title" translate>widgets.time-series-chart.axis.axes</div>
<tb-time-series-chart-axis-settings
formControlName="yAxis"
advanced
axisType="yAxis">
</tb-time-series-chart-axis-settings>
<div class="tb-form-panel-title" translate>widgets.time-series-chart.axis.x-axis</div>
<tb-time-series-chart-axis-settings
formControlName="xAxis"
advanced
@ -156,6 +157,9 @@
</ng-template>
</mat-expansion-panel>
</div>
<tb-time-series-chart-animation-settings
formControlName="animation">
</tb-time-series-chart-animation-settings>
<div class="tb-form-row space-between">
<div>{{ 'widgets.background.background' | translate }}</div>
<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 {
DataKey,
Datasource,
legendPositions,
legendPositionTranslationMap,
@ -32,7 +33,11 @@ import {
timeSeriesChartWidgetDefaultSettings,
TimeSeriesChartWidgetSettings
} 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';
@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;
EChartsTooltipTrigger = EChartsTooltipTrigger;
@ -73,6 +83,15 @@ export class TimeSeriesChartWidgetSettingsComponent extends WidgetSettingsCompon
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 {
return this.timeSeriesChartWidgetSettingsForm;
}
@ -91,12 +110,12 @@ export class TimeSeriesChartWidgetSettingsComponent extends WidgetSettingsCompon
protected onSettingsSet(settings: WidgetSettings) {
this.timeSeriesChartWidgetSettingsForm = this.fb.group({
yAxes: [settings.yAxes, []],
thresholds: [settings.thresholds, []],
dataZoom: [settings.dataZoom, []],
stack: [settings.stack, []],
yAxis: [settings.yAxis, []],
xAxis: [settings.xAxis, []],
noAggregationBarWidthSettings: [settings.noAggregationBarWidthSettings, []],
@ -119,6 +138,8 @@ export class TimeSeriesChartWidgetSettingsComponent extends WidgetSettingsCompon
tooltipBackgroundColor: [settings.tooltipBackgroundColor, []],
tooltipBackgroundBlur: [settings.tooltipBackgroundBlur, []],
animation: [settings.animation, []],
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 {
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">
<div class="tb-form-panel stroked tb-slide-toggle">
<mat-expansion-panel class="tb-settings" [(expanded)]="settingsExpanded"
[disabled]="!axisSettingsFormGroup.get('show').value">
<mat-expansion-panel class="tb-settings" [(expanded)]="alwaysExpanded || settingsExpanded"
[disabled]="alwaysExpanded || !axisSettingsFormGroup.get('show').value">
<mat-expansion-panel-header fxLayout="row wrap">
<mat-panel-title>
<mat-slide-toggle class="mat-slide" formControlName="show" (click)="$event.stopPropagation()"
fxLayoutAlign="center">
{{ axisTitle | translate }}
{{ 'widgets.time-series-chart.axis.show' | translate }}
</mat-slide-toggle>
</mat-panel-title>
</mat-expansion-panel-header>
@ -56,6 +56,18 @@
</mat-select>
</mat-form-field>
</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-row no-border space-between column-xs">
<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 { ControlValueAccessor, NG_VALUE_ACCESSOR, UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
import {
ControlValueAccessor,
NG_VALUE_ACCESSOR,
UntypedFormBuilder,
UntypedFormGroup,
Validators
} from '@angular/forms';
import {
AxisPosition,
timeSeriesAxisPositionTranslations,
@ -40,9 +46,11 @@ import { WidgetService } from '@core/http/widget.service';
})
export class TimeSeriesChartAxisSettingsComponent implements OnInit, ControlValueAccessor {
settingsExpanded = false;
@Input()
@coerceBoolean()
alwaysExpanded = false;
axisTitle: string;
settingsExpanded = false;
axisPositions: AxisPosition[];
@ -72,8 +80,6 @@ export class TimeSeriesChartAxisSettingsComponent implements OnInit, ControlValu
ngOnInit(): void {
this.axisTitle = this.axisType === 'xAxis' ? 'widgets.time-series-chart.axis.x-axis' : 'widgets.time-series-chart.axis.y-axis';
this.axisPositions = this.axisType === 'xAxis' ? [AxisPosition.top, AxisPosition.bottom] :
[AxisPosition.left, AxisPosition.right];
@ -94,6 +100,8 @@ export class TimeSeriesChartAxisSettingsComponent implements OnInit, ControlValu
splitLinesColor: [null, []]
});
if (this.axisType === 'yAxis') {
this.axisSettingsFormGroup.addControl('units', this.fb.control(null, []));
this.axisSettingsFormGroup.addControl('decimals', this.fb.control(null, [Validators.min(0)]));
this.axisSettingsFormGroup.addControl('ticksFormatter', this.fb.control(null, []));
this.axisSettingsFormGroup.addControl('min', this.fb.control(null, []));
this.axisSettingsFormGroup.addControl('max', this.fb.control(null, []));

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">
</tb-data-key-input>
</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">
<tb-color-input asBoxInput
colorClearButton
@ -89,17 +98,14 @@
</mat-form-field>
</div>
<div class="tb-form-table-row-cell-buttons">
<div class="tb-settings-button">
<button *ngIf="modelValue"
type="button"
mat-icon-button
#matButton
(click)="editThreshold($event, matButton)"
matTooltip="{{ 'widgets.time-series-chart.threshold.threshold-settings' | translate }}"
matTooltipPosition="above">
<mat-icon>settings</mat-icon>
</button>
</div>
<button type="button"
mat-icon-button
#matButton
(click)="editThreshold($event, matButton)"
matTooltip="{{ 'widgets.time-series-chart.threshold.threshold-settings' | translate }}"
matTooltipPosition="above">
<mat-icon>settings</mat-icon>
</button>
<button type="button"
mat-icon-button
(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 {
display: flex;
flex-direction: row;
@ -77,6 +85,12 @@
min-width: 60px;
}
.tb-y-axis-field {
@media #{$mat-xs} {
display: none;
}
}
.tb-units-field, .tb-color-field, .tb-decimals-field {
display: none;
@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,
EventEmitter,
forwardRef,
Input,
Input, OnChanges,
OnInit,
Output,
Renderer2,
Renderer2, SimpleChanges,
ViewContainerRef,
ViewEncapsulation
} from '@angular/core';
@ -36,7 +36,7 @@ import {
} from '@angular/forms';
import {
TimeSeriesChartThreshold,
TimeSeriesChartThresholdType,
TimeSeriesChartThresholdType, TimeSeriesChartYAxisId,
timeSeriesThresholdTypes,
timeSeriesThresholdTypeTranslations
} from '@home/components/widget/lib/chart/time-series-chart.models';
@ -67,7 +67,7 @@ import {
],
encapsulation: ViewEncapsulation.None
})
export class TimeSeriesChartThresholdRowComponent implements ControlValueAccessor, OnInit {
export class TimeSeriesChartThresholdRowComponent implements ControlValueAccessor, OnInit, OnChanges {
DataKeyType = DataKeyType;
@ -98,6 +98,9 @@ export class TimeSeriesChartThresholdRowComponent implements ControlValueAccesso
@Input()
disabled: boolean;
@Input()
yAxisIds: TimeSeriesChartYAxisId[];
@Output()
thresholdRemoved = new EventEmitter();
@ -124,6 +127,7 @@ export class TimeSeriesChartThresholdRowComponent implements ControlValueAccesso
type: [null, []],
value: [null, [Validators.required]],
entityAlias: [null, [Validators.required]],
yAxisId: [null, [Validators.required]],
lineColor: [null, []],
units: [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 {
this.propagateChange = fn;
}
@ -170,6 +188,7 @@ export class TimeSeriesChartThresholdRowComponent implements ControlValueAccesso
type: value.type,
value: value.value,
entityAlias: value.entityAlias,
yAxisId: value.yAxisId,
lineColor: value.lineColor,
units: value.units,
decimals: value.decimals,
@ -200,7 +219,8 @@ export class TimeSeriesChartThresholdRowComponent implements ControlValueAccesso
} else {
const ctx: any = {
thresholdSettings: deepClone(this.modelValue),
widgetConfig: this.widgetConfig
widgetConfig: this.widgetConfig,
yAxisIds: this.yAxisIds
};
const thresholdSettingsPanelPopover = this.popoverService.displayPopover(trigger, this.renderer,
this.viewContainerRef, TimeSeriesChartThresholdSettingsPanelComponent, ['leftOnly', 'leftTopOnly', 'leftBottomOnly'], true, null,
@ -213,6 +233,7 @@ export class TimeSeriesChartThresholdRowComponent implements ControlValueAccesso
this.modelValue = {...this.modelValue, ...thresholdSettings};
this.thresholdFormGroup.patchValue(
{
yAxisId: this.modelValue.yAxisId,
units: this.modelValue.units,
decimals: this.modelValue.decimals,
lineColor: this.modelValue.lineColor
@ -248,6 +269,7 @@ export class TimeSeriesChartThresholdRowComponent implements ControlValueAccesso
this.modelValue.type = value.type;
this.modelValue.value = value.value;
this.modelValue.entityAlias = value.entityAlias;
this.modelValue.yAxisId = value.yAxisId;
this.modelValue.lineColor = value.lineColor;
this.modelValue.units = value.units;
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-title">{{ 'widgets.time-series-chart.threshold.threshold-settings' | translate }}</div>
<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 translate>widget-config.units-short</div>
<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,
timeSeriesChartShapes,
timeSeriesChartShapeTranslations,
TimeSeriesChartThreshold,
TimeSeriesChartThreshold, TimeSeriesChartYAxisId,
timeSeriesLineTypes,
timeSeriesLineTypeTranslations,
timeSeriesThresholdLabelPositions,
@ -60,6 +60,9 @@ export class TimeSeriesChartThresholdSettingsPanelComponent implements OnInit {
@Input()
widgetConfig: WidgetConfig;
@Input()
yAxisIds: TimeSeriesChartYAxisId[];
@Input()
popover: TbPopoverComponent<TimeSeriesChartThresholdSettingsPanelComponent>;
@ -74,6 +77,7 @@ export class TimeSeriesChartThresholdSettingsPanelComponent implements OnInit {
ngOnInit(): void {
this.thresholdSettingsFormGroup = this.fb.group(
{
yAxisId: [this.thresholdSettings.yAxisId, [Validators.required]],
units: [this.thresholdSettings.units, []],
decimals: [this.thresholdSettings.decimals, [Validators.min(0)]],
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-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-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-units-header" translate>widget-config.units-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">
<tb-time-series-chart-threshold-row fxFlex
[formControl]="thresholdControl"
[yAxisIds]="yAxisIds"
(thresholdRemoved)="removeThreshold($index)">
</tb-time-series-chart-threshold-row>
<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 {
width: 80px;
min-width: 80px;
@ -53,6 +58,12 @@
min-width: 80px;
}
&.tb-y-axis-header {
@media #{$mat-xs} {
display: none;
}
}
&.tb-units-header, &.tb-color-header, &.tb-decimals-header {
display: none;
@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,
TimeSeriesChartThresholdType,
timeSeriesChartThresholdValid,
timeSeriesChartThresholdValidator
timeSeriesChartThresholdValidator, TimeSeriesChartYAxisId
} from '@home/components/widget/lib/chart/time-series-chart.models';
import { mergeDeep } from '@core/utils';
import { IAliasController } from '@core/api/widget-api.models';
@ -74,6 +74,9 @@ export class TimeSeriesChartThresholdsPanelComponent implements ControlValueAcce
@Input()
widgetConfig: WidgetConfig;
@Input()
yAxisIds: TimeSeriesChartYAxisId[];
thresholdsFormGroup: UntypedFormGroup;
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 {
TimeSeriesNoAggregationBarWidthSettingsComponent
} from '@home/components/widget/lib/settings/common/chart/time-series-no-aggregation-bar-width-settings.component';
import {
TimeSeriesChartYAxesPanelComponent
} from '@home/components/widget/lib/settings/common/chart/time-series-chart-y-axes-panel.component';
import {
TimeSeriesChartYAxisRowComponent
} from '@home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-row.component';
import {
TimeSeriesChartYAxisSettingsPanelComponent
} from '@home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-settings-panel.component';
import {
TimeSeriesChartAnimationSettingsComponent
} from '@home/components/widget/lib/settings/common/chart/time-series-chart-animation-settings.component';
@NgModule({
declarations: [
@ -150,6 +162,10 @@ import {
TimeSeriesChartThresholdRowComponent,
TimeSeriesChartThresholdSettingsPanelComponent,
TimeSeriesNoAggregationBarWidthSettingsComponent,
TimeSeriesChartYAxesPanelComponent,
TimeSeriesChartYAxisRowComponent,
TimeSeriesChartYAxisSettingsPanelComponent,
TimeSeriesChartAnimationSettingsComponent,
DataKeyInputComponent,
EntityAliasInputComponent
],
@ -197,6 +213,10 @@ import {
TimeSeriesChartThresholdRowComponent,
TimeSeriesChartThresholdSettingsPanelComponent,
TimeSeriesNoAggregationBarWidthSettingsComponent,
TimeSeriesChartYAxesPanelComponent,
TimeSeriesChartYAxisRowComponent,
TimeSeriesChartYAxisSettingsPanelComponent,
TimeSeriesChartAnimationSettingsComponent,
DataKeyInputComponent,
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-hint": "Stacks series on the chart. The series with the same unit would be put on top of each other.",
"axes": "Axes",
"y-axes": "Y axes",
"line-type": "Line type",
"line-type-solid": "Solid",
"line-type-dashed": "Dashed",
@ -6673,6 +6674,9 @@
"axes": "Axes",
"x-axis": "X axis",
"y-axis": "Y axis",
"y-axis-settings": "Y axis settings",
"remove-y-axis": "Remove Y axis",
"id": "Id",
"label": "Label",
"position": "Position",
"position-left": "Left",
@ -6689,7 +6693,21 @@
"scale": "Scale",
"scale-min": "min",
"scale-max": "max",
"scale-auto": "Auto"
"scale-auto": "Auto",
"min": "Min",
"max": "Max",
"show": "Show",
"add-y-axis": "Add Y axis"
},
"animation": {
"animation": "Animation",
"animation-threshold": "Animation threshold",
"animation-duration": "Animation duration",
"animation-easing": "Animation easing",
"animation-delay": "Animation delay",
"update-animation-duration": "Update animation duration",
"update-animation-easing": "Update animation easing",
"update-animation-delay": "Update animation delay"
},
"series": {
"legend-settings": "Legend settings",

Loading…
Cancel
Save