164 changed files with 25449 additions and 8799 deletions
File diff suppressed because it is too large
File diff suppressed because it is too large
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -0,0 +1,151 @@ |
|||
/** |
|||
* 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. |
|||
*/ |
|||
package org.thingsboard.server.transport.lwm2m.server.store.util; |
|||
|
|||
import org.eclipse.leshan.core.link.Link; |
|||
import org.eclipse.leshan.core.node.LwM2mMultipleResource; |
|||
import org.eclipse.leshan.core.node.LwM2mResource; |
|||
import org.eclipse.leshan.core.node.LwM2mSingleResource; |
|||
import org.eclipse.leshan.core.request.Identity; |
|||
import org.eclipse.leshan.core.request.WriteRequest; |
|||
import org.eclipse.leshan.server.registration.Registration; |
|||
import org.junit.Assert; |
|||
import org.junit.Test; |
|||
import org.mockito.Mockito; |
|||
import org.thingsboard.server.common.data.TbResource; |
|||
import org.thingsboard.server.common.data.device.data.PowerMode; |
|||
import org.thingsboard.server.common.data.id.CustomerId; |
|||
import org.thingsboard.server.common.data.id.DeviceId; |
|||
import org.thingsboard.server.common.data.id.DeviceProfileId; |
|||
import org.thingsboard.server.common.data.id.TenantId; |
|||
import org.thingsboard.server.common.transport.TransportResourceCache; |
|||
import org.thingsboard.server.common.transport.auth.TransportDeviceInfo; |
|||
import org.thingsboard.server.common.transport.auth.ValidateDeviceCredentialsResponse; |
|||
import org.thingsboard.server.gen.transport.TransportProtos; |
|||
import org.thingsboard.server.transport.lwm2m.server.LwM2mTransportContext; |
|||
import org.thingsboard.server.transport.lwm2m.server.LwM2mTransportServerHelper; |
|||
import org.thingsboard.server.transport.lwm2m.server.LwM2mVersionedModelProvider; |
|||
import org.thingsboard.server.transport.lwm2m.server.client.LwM2MClientState; |
|||
import org.thingsboard.server.transport.lwm2m.server.client.LwM2mClient; |
|||
import org.thingsboard.server.transport.lwm2m.server.client.LwM2mClientContext; |
|||
import org.thingsboard.server.transport.lwm2m.server.client.ResourceValue; |
|||
|
|||
import java.net.InetSocketAddress; |
|||
import java.nio.file.Files; |
|||
import java.nio.file.Path; |
|||
import java.util.Map; |
|||
import java.util.Optional; |
|||
import java.util.UUID; |
|||
|
|||
import static org.junit.Assert.assertEquals; |
|||
import static org.junit.Assert.assertNotNull; |
|||
import static org.mockito.ArgumentMatchers.any; |
|||
import static org.mockito.ArgumentMatchers.eq; |
|||
import static org.mockito.Mockito.mock; |
|||
import static org.mockito.Mockito.when; |
|||
|
|||
public class LwM2MClientSerDesTest { |
|||
|
|||
@Test |
|||
public void serializeDeserialize() throws Exception { |
|||
LwM2mClient client = new LwM2mClient("nodeId", "testEndpoint"); |
|||
|
|||
TransportDeviceInfo tdi = new TransportDeviceInfo(); |
|||
tdi.setPowerMode(PowerMode.PSM); |
|||
tdi.setPsmActivityTimer(10000L); |
|||
tdi.setPagingTransmissionWindow(2000L); |
|||
tdi.setEdrxCycle(3000L); |
|||
tdi.setTenantId(TenantId.fromUUID(UUID.randomUUID())); |
|||
tdi.setCustomerId(new CustomerId(UUID.randomUUID())); |
|||
tdi.setDeviceId(new DeviceId(UUID.randomUUID())); |
|||
tdi.setDeviceProfileId(new DeviceProfileId(UUID.randomUUID())); |
|||
tdi.setDeviceName("testDevice"); |
|||
tdi.setDeviceType("testType"); |
|||
ValidateDeviceCredentialsResponse credentialsResponse = ValidateDeviceCredentialsResponse.builder() |
|||
.deviceInfo(tdi) |
|||
.build(); |
|||
|
|||
client.init(credentialsResponse, UUID.randomUUID()); |
|||
|
|||
Registration registration = |
|||
new Registration.Builder("test", "testEndpoint", Identity |
|||
.unsecure(new InetSocketAddress(1000))) |
|||
.supportedContentFormats() |
|||
.supportedObjects(Map.of(15, "1.0", 17, "1.0")) |
|||
.objectLinks(new Link[]{new Link("/")}) |
|||
.build(); |
|||
|
|||
client.setRegistration(registration); |
|||
client.setState(LwM2MClientState.REGISTERED); |
|||
client.getSharedAttributes().put("key1", TransportProtos.TsKvProto.newBuilder().setTs(0).setKv(TransportProtos.KeyValueProto.newBuilder().setStringV("test").build()).build()); |
|||
client.getSharedAttributes().put("key2", TransportProtos.TsKvProto.newBuilder().setTs(1).setKv(TransportProtos.KeyValueProto.newBuilder().setDoubleV(1.02).build()).build()); |
|||
|
|||
TransportResourceCache resourceCache = mock(TransportResourceCache.class); |
|||
LwM2mTransportContext context = mock(LwM2mTransportContext.class); |
|||
LwM2mClientContext clientContext = mock(LwM2mClientContext.class); |
|||
|
|||
var provider = new LwM2mVersionedModelProvider(clientContext, new LwM2mTransportServerHelper(context), context); |
|||
|
|||
TbResource resource15 = new TbResource(); |
|||
resource15.setData(Files.readAllBytes(Path.of(this.getClass().getClassLoader().getResource("15.xml").toURI()))); |
|||
TbResource resource17 = new TbResource(); |
|||
resource17.setData(Files.readAllBytes(Path.of(this.getClass().getClassLoader().getResource("17.xml").toURI()))); |
|||
|
|||
when(resourceCache.get(any(), any(), eq("15_1.0"))).thenReturn(Optional.of(resource15)); |
|||
when(resourceCache.get(any(), any(), eq("17_1.0"))).thenReturn(Optional.of(resource17)); |
|||
when(context.getTransportResourceCache()).thenReturn(resourceCache); |
|||
when(clientContext.getClientByEndpoint(any())).thenReturn(client); |
|||
|
|||
LwM2mResource singleResource = LwM2mSingleResource.newStringResource(15, "testValue"); |
|||
LwM2mResource multipleResource = LwM2mMultipleResource.newStringResource(17, Map.of(0, "testValue", 1, "testValue")); |
|||
client.saveResourceValue("/15_1.0/0/0", singleResource, provider, WriteRequest.Mode.UPDATE); |
|||
client.saveResourceValue("/17_1.0/0/0", multipleResource, provider, WriteRequest.Mode.UPDATE); |
|||
|
|||
byte[] bytes = LwM2MClientSerDes.serialize(client); |
|||
Assert.assertNotNull(bytes); |
|||
|
|||
LwM2mClient desClient = LwM2MClientSerDes.deserialize(bytes); |
|||
|
|||
assertEquals(client.getNodeId(), desClient.getNodeId()); |
|||
assertEquals(client.getEndpoint(), desClient.getEndpoint()); |
|||
assertEquals(client.getSharedAttributes(), desClient.getSharedAttributes()); |
|||
assertEquals(client.getKeyTsLatestMap(), desClient.getKeyTsLatestMap()); |
|||
assertEquals(client.getTenantId(), desClient.getTenantId()); |
|||
assertEquals(client.getProfileId(), desClient.getProfileId()); |
|||
assertEquals(client.getDeviceId(), desClient.getDeviceId()); |
|||
assertEquals(client.getState(), desClient.getState()); |
|||
assertEquals(client.getSession(), desClient.getSession()); |
|||
assertEquals(client.getPowerMode(), desClient.getPowerMode()); |
|||
assertEquals(client.getPsmActivityTimer(), desClient.getPsmActivityTimer()); |
|||
assertEquals(client.getPagingTransmissionWindow(), desClient.getPagingTransmissionWindow()); |
|||
assertEquals(client.getEdrxCycle(), desClient.getEdrxCycle()); |
|||
assertEquals(client.getRegistration(), desClient.getRegistration()); |
|||
assertEquals(client.isAsleep(), desClient.isAsleep()); |
|||
assertEquals(client.getLastUplinkTime(), desClient.getLastUplinkTime()); |
|||
assertEquals(client.getSleepTask(), desClient.getSleepTask()); |
|||
assertEquals(client.getClientSupportContentFormats(), desClient.getClientSupportContentFormats()); |
|||
assertEquals(client.getDefaultContentFormat(), desClient.getDefaultContentFormat()); |
|||
assertEquals(client.getRetryAttempts().get(), desClient.getRetryAttempts().get()); |
|||
assertEquals(client.getLastSentRpcId(), desClient.getLastSentRpcId()); |
|||
|
|||
Map<String, ResourceValue> expectedResources = client.getResources(); |
|||
Map<String, ResourceValue> actualResources = desClient.getResources(); |
|||
assertNotNull(actualResources); |
|||
assertEquals(expectedResources.size(), actualResources.size()); |
|||
expectedResources.forEach((key, value) -> assertEquals(value.toString(), actualResources.get(key).toString())); |
|||
} |
|||
|
|||
} |
|||
@ -1,101 +0,0 @@ |
|||
/** |
|||
* 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. |
|||
*/ |
|||
package org.thingsboard.server.transport.lwm2m.server.store.util; |
|||
|
|||
import org.eclipse.leshan.core.link.Link; |
|||
import org.eclipse.leshan.core.request.Identity; |
|||
import org.eclipse.leshan.server.registration.Registration; |
|||
import org.junit.Assert; |
|||
import org.junit.Test; |
|||
import org.thingsboard.server.common.data.device.data.PowerMode; |
|||
import org.thingsboard.server.common.data.id.CustomerId; |
|||
import org.thingsboard.server.common.data.id.DeviceId; |
|||
import org.thingsboard.server.common.data.id.DeviceProfileId; |
|||
import org.thingsboard.server.common.data.id.TenantId; |
|||
import org.thingsboard.server.common.transport.auth.TransportDeviceInfo; |
|||
import org.thingsboard.server.common.transport.auth.ValidateDeviceCredentialsResponse; |
|||
import org.thingsboard.server.gen.transport.TransportProtos; |
|||
import org.thingsboard.server.transport.lwm2m.server.client.LwM2MClientState; |
|||
import org.thingsboard.server.transport.lwm2m.server.client.LwM2mClient; |
|||
|
|||
import java.net.InetSocketAddress; |
|||
import java.util.UUID; |
|||
|
|||
public class LwM2MClientSerDesTest { |
|||
|
|||
@Test |
|||
public void serializeDeserialize() { |
|||
LwM2mClient client = new LwM2mClient("nodeId", "testEndpoint"); |
|||
|
|||
TransportDeviceInfo tdi = new TransportDeviceInfo(); |
|||
tdi.setPowerMode(PowerMode.PSM); |
|||
tdi.setPsmActivityTimer(10000L); |
|||
tdi.setPagingTransmissionWindow(2000L); |
|||
tdi.setEdrxCycle(3000L); |
|||
tdi.setTenantId(TenantId.fromUUID(UUID.randomUUID())); |
|||
tdi.setCustomerId(new CustomerId(UUID.randomUUID())); |
|||
tdi.setDeviceId(new DeviceId(UUID.randomUUID())); |
|||
tdi.setDeviceProfileId(new DeviceProfileId(UUID.randomUUID())); |
|||
tdi.setDeviceName("testDevice"); |
|||
tdi.setDeviceType("testType"); |
|||
ValidateDeviceCredentialsResponse credentialsResponse = ValidateDeviceCredentialsResponse.builder() |
|||
.deviceInfo(tdi) |
|||
.build(); |
|||
|
|||
client.init(credentialsResponse, UUID.randomUUID()); |
|||
|
|||
Registration registration = |
|||
new Registration.Builder("test", "testEndpoint", Identity |
|||
.unsecure(new InetSocketAddress(1000))) |
|||
.supportedContentFormats() |
|||
.objectLinks(new Link[]{new Link("/")}) |
|||
.build(); |
|||
|
|||
client.setRegistration(registration); |
|||
client.setState(LwM2MClientState.REGISTERED); |
|||
|
|||
client.getSharedAttributes().put("key1", TransportProtos.TsKvProto.newBuilder().setTs(0).setKv(TransportProtos.KeyValueProto.newBuilder().setStringV("test").build()).build()); |
|||
client.getSharedAttributes().put("key2", TransportProtos.TsKvProto.newBuilder().setTs(1).setKv(TransportProtos.KeyValueProto.newBuilder().setDoubleV(1.02).build()).build()); |
|||
|
|||
byte[] bytes = LwM2MClientSerDes.serialize(client); |
|||
Assert.assertNotNull(bytes); |
|||
|
|||
LwM2mClient desClient = LwM2MClientSerDes.deserialize(bytes); |
|||
|
|||
Assert.assertEquals(client.getNodeId(), desClient.getNodeId()); |
|||
Assert.assertEquals(client.getEndpoint(), desClient.getEndpoint()); |
|||
Assert.assertEquals(client.getResources(), desClient.getResources()); |
|||
Assert.assertEquals(client.getSharedAttributes(), desClient.getSharedAttributes()); |
|||
Assert.assertEquals(client.getKeyTsLatestMap(), desClient.getKeyTsLatestMap()); |
|||
Assert.assertEquals(client.getTenantId(), desClient.getTenantId()); |
|||
Assert.assertEquals(client.getProfileId(), desClient.getProfileId()); |
|||
Assert.assertEquals(client.getDeviceId(), desClient.getDeviceId()); |
|||
Assert.assertEquals(client.getState(), desClient.getState()); |
|||
Assert.assertEquals(client.getSession(), desClient.getSession()); |
|||
Assert.assertEquals(client.getPowerMode(), desClient.getPowerMode()); |
|||
Assert.assertEquals(client.getPsmActivityTimer(), desClient.getPsmActivityTimer()); |
|||
Assert.assertEquals(client.getPagingTransmissionWindow(), desClient.getPagingTransmissionWindow()); |
|||
Assert.assertEquals(client.getEdrxCycle(), desClient.getEdrxCycle()); |
|||
Assert.assertEquals(client.getRegistration(), desClient.getRegistration()); |
|||
Assert.assertEquals(client.isAsleep(), desClient.isAsleep()); |
|||
Assert.assertEquals(client.getLastUplinkTime(), desClient.getLastUplinkTime()); |
|||
Assert.assertEquals(client.getSleepTask(), desClient.getSleepTask()); |
|||
Assert.assertEquals(client.getClientSupportContentFormats(), desClient.getClientSupportContentFormats()); |
|||
Assert.assertEquals(client.getDefaultContentFormat(), desClient.getDefaultContentFormat()); |
|||
Assert.assertEquals(client.getRetryAttempts().get(), desClient.getRetryAttempts().get()); |
|||
Assert.assertEquals(client.getLastSentRpcId(), desClient.getLastSentRpcId()); |
|||
} |
|||
} |
|||
@ -0,0 +1,26 @@ |
|||
<?xml version="1.0" encoding="UTF-8"?> |
|||
<LWM2M> |
|||
<Object ObjectType="MODefinition"> |
|||
<Name>Test 15</Name> |
|||
<Description1></Description1> |
|||
<ObjectID>15</ObjectID> |
|||
<ObjectURN>urn:oma:lwm2m:oma:15:1.0</ObjectURN> |
|||
<LWM2MVersion>1.0</LWM2MVersion> |
|||
<ObjectVersion>1.0</ObjectVersion> |
|||
<MultipleInstances>Multiple</MultipleInstances> |
|||
<Mandatory>Optional</Mandatory> |
|||
<Resources> |
|||
<Item ID="0"> |
|||
<Name>Test</Name> |
|||
<Operations>R</Operations> |
|||
<MultipleInstances>Single</MultipleInstances> |
|||
<Mandatory>Mandatory</Mandatory> |
|||
<Type>String</Type> |
|||
<RangeEnumeration></RangeEnumeration> |
|||
<Units></Units> |
|||
<Description></Description> |
|||
</Item> |
|||
</Resources> |
|||
<Description2></Description2> |
|||
</Object> |
|||
</LWM2M> |
|||
@ -0,0 +1,26 @@ |
|||
<?xml version="1.0" encoding="UTF-8"?> |
|||
<LWM2M> |
|||
<Object ObjectType="MODefinition"> |
|||
<Name>Test 17</Name> |
|||
<Description1></Description1> |
|||
<ObjectID>17</ObjectID> |
|||
<ObjectURN>urn:oma:lwm2m:oma:17:1.0</ObjectURN> |
|||
<LWM2MVersion>1.0</LWM2MVersion> |
|||
<ObjectVersion>1.0</ObjectVersion> |
|||
<MultipleInstances>Multiple</MultipleInstances> |
|||
<Mandatory>Mandatory</Mandatory> |
|||
<Resources> |
|||
<Item ID="0"> |
|||
<Name>Test</Name> |
|||
<Operations>R</Operations> |
|||
<MultipleInstances>Multiple</MultipleInstances> |
|||
<Mandatory>Optional</Mandatory> |
|||
<Type>String</Type> |
|||
<RangeEnumeration></RangeEnumeration> |
|||
<Units></Units> |
|||
<Description></Description> |
|||
</Item> |
|||
</Resources> |
|||
<Description2></Description2> |
|||
</Object> |
|||
</LWM2M> |
|||
File diff suppressed because one or more lines are too long
@ -1,5 +1,5 @@ |
|||
diff --git a/node_modules/@angular/core/fesm2020/core.mjs b/node_modules/@angular/core/fesm2020/core.mjs
|
|||
index 3e93015..9efcb96 100755
|
|||
index e9a9b75..17044d9 100755
|
|||
--- a/node_modules/@angular/core/fesm2020/core.mjs
|
|||
+++ b/node_modules/@angular/core/fesm2020/core.mjs
|
|||
@@ -11053,13 +11053,13 @@ function findDirectiveDefMatches(tView, tNode) {
|
|||
@ -0,0 +1,426 @@ |
|||
diff --git a/node_modules/echarts/lib/component/dataZoom/DataZoomModel.js b/node_modules/echarts/lib/component/dataZoom/DataZoomModel.js
|
|||
index d6c05f3..aafb0b8 100644
|
|||
--- a/node_modules/echarts/lib/component/dataZoom/DataZoomModel.js
|
|||
+++ b/node_modules/echarts/lib/component/dataZoom/DataZoomModel.js
|
|||
@@ -362,7 +362,10 @@ var DataZoomModel = /** @class */function (_super) {
|
|||
return axisProxy.getDataValueWindow(); |
|||
} |
|||
} else { |
|||
- return this.getAxisProxy(axisDim, axisIndex).getDataValueWindow();
|
|||
+ var axisProxy = this.getAxisProxy(axisDim, axisIndex);
|
|||
+ if (axisProxy) {
|
|||
+ return axisProxy.getDataValueWindow();
|
|||
+ }
|
|||
} |
|||
}; |
|||
/** |
|||
@@ -381,10 +384,10 @@ var DataZoomModel = /** @class */function (_super) {
|
|||
var axisInfo = this._targetAxisInfoMap.get(axisDim); |
|||
for (var j = 0; j < axisInfo.indexList.length; j++) { |
|||
var proxy = this.getAxisProxy(axisDim, axisInfo.indexList[j]); |
|||
- if (proxy.hostedBy(this)) {
|
|||
+ if (proxy && proxy.hostedBy(this)) {
|
|||
return proxy; |
|||
} |
|||
- if (!firstProxy) {
|
|||
+ if (proxy && !firstProxy) {
|
|||
firstProxy = proxy; |
|||
} |
|||
} |
|||
diff --git a/node_modules/echarts/lib/component/dataZoom/InsideZoomView.js b/node_modules/echarts/lib/component/dataZoom/InsideZoomView.js
|
|||
index 06469b2..cf0b2ea 100644
|
|||
--- a/node_modules/echarts/lib/component/dataZoom/InsideZoomView.js
|
|||
+++ b/node_modules/echarts/lib/component/dataZoom/InsideZoomView.js
|
|||
@@ -96,11 +96,14 @@ var getRangeHandlers = {
|
|||
range[0] = (range[0] - percentPoint) * scale + percentPoint; |
|||
range[1] = (range[1] - percentPoint) * scale + percentPoint; |
|||
// Restrict range. |
|||
- var minMaxSpan = this.dataZoomModel.findRepresentativeAxisProxy().getMinMaxSpan();
|
|||
- sliderMove(0, range, [0, 100], 0, minMaxSpan.minSpan, minMaxSpan.maxSpan);
|
|||
- this.range = range;
|
|||
- if (lastRange[0] !== range[0] || lastRange[1] !== range[1]) {
|
|||
- return range;
|
|||
+ var proxy = this.dataZoomModel.findRepresentativeAxisProxy();
|
|||
+ if (proxy) {
|
|||
+ var minMaxSpan = proxy.getMinMaxSpan();
|
|||
+ sliderMove(0, range, [0, 100], 0, minMaxSpan.minSpan, minMaxSpan.maxSpan);
|
|||
+ this.range = range;
|
|||
+ if (lastRange[0] !== range[0] || lastRange[1] !== range[1]) {
|
|||
+ return range;
|
|||
+ }
|
|||
} |
|||
}, |
|||
pan: makeMover(function (range, axisModel, coordSysInfo, coordSysMainType, controller, e) { |
|||
diff --git a/node_modules/echarts/lib/component/dataZoom/SliderZoomView.js b/node_modules/echarts/lib/component/dataZoom/SliderZoomView.js
|
|||
index 98912e0..0cda6be 100644
|
|||
--- a/node_modules/echarts/lib/component/dataZoom/SliderZoomView.js
|
|||
+++ b/node_modules/echarts/lib/component/dataZoom/SliderZoomView.js
|
|||
@@ -64,7 +64,7 @@ var DEFAULT_MOVE_HANDLE_SIZE = 7;
|
|||
var HORIZONTAL = 'horizontal'; |
|||
var VERTICAL = 'vertical'; |
|||
var LABEL_GAP = 5; |
|||
-var SHOW_DATA_SHADOW_SERIES_TYPE = ['line', 'bar', 'candlestick', 'scatter'];
|
|||
+var SHOW_DATA_SHADOW_SERIES_TYPE = ['line', 'bar', 'candlestick', 'scatter', 'custom'];
|
|||
var REALTIME_ANIMATION_CONFIG = { |
|||
easing: 'cubicOut', |
|||
duration: 100, |
|||
@@ -359,30 +359,33 @@ var SliderZoomView = /** @class */function (_super) {
|
|||
var result; |
|||
var ecModel = this.ecModel; |
|||
dataZoomModel.eachTargetAxis(function (axisDim, axisIndex) { |
|||
- var seriesModels = dataZoomModel.getAxisProxy(axisDim, axisIndex).getTargetSeriesModels();
|
|||
- each(seriesModels, function (seriesModel) {
|
|||
- if (result) {
|
|||
- return;
|
|||
- }
|
|||
- if (showDataShadow !== true && indexOf(SHOW_DATA_SHADOW_SERIES_TYPE, seriesModel.get('type')) < 0) {
|
|||
- return;
|
|||
- }
|
|||
- var thisAxis = ecModel.getComponent(getAxisMainType(axisDim), axisIndex).axis;
|
|||
- var otherDim = getOtherDim(axisDim);
|
|||
- var otherAxisInverse;
|
|||
- var coordSys = seriesModel.coordinateSystem;
|
|||
- if (otherDim != null && coordSys.getOtherAxis) {
|
|||
- otherAxisInverse = coordSys.getOtherAxis(thisAxis).inverse;
|
|||
- }
|
|||
- otherDim = seriesModel.getData().mapDimension(otherDim);
|
|||
- result = {
|
|||
- thisAxis: thisAxis,
|
|||
- series: seriesModel,
|
|||
- thisDim: axisDim,
|
|||
- otherDim: otherDim,
|
|||
- otherAxisInverse: otherAxisInverse
|
|||
- };
|
|||
- }, this);
|
|||
+ var axisProxy = dataZoomModel.getAxisProxy(axisDim, axisIndex);
|
|||
+ if (axisProxy) {
|
|||
+ var seriesModels = axisProxy.getTargetSeriesModels();
|
|||
+ each(seriesModels, function (seriesModel) {
|
|||
+ if (result) {
|
|||
+ return;
|
|||
+ }
|
|||
+ if (showDataShadow !== true && indexOf(SHOW_DATA_SHADOW_SERIES_TYPE, seriesModel.get('type')) < 0) {
|
|||
+ return;
|
|||
+ }
|
|||
+ var thisAxis = ecModel.getComponent(getAxisMainType(axisDim), axisIndex).axis;
|
|||
+ var otherDim = getOtherDim(axisDim);
|
|||
+ var otherAxisInverse;
|
|||
+ var coordSys = seriesModel.coordinateSystem;
|
|||
+ if (otherDim != null && coordSys.getOtherAxis) {
|
|||
+ otherAxisInverse = coordSys.getOtherAxis(thisAxis).inverse;
|
|||
+ }
|
|||
+ otherDim = seriesModel.getData().mapDimension(otherDim);
|
|||
+ result = {
|
|||
+ thisAxis: thisAxis,
|
|||
+ series: seriesModel,
|
|||
+ thisDim: axisDim,
|
|||
+ otherDim: otherDim,
|
|||
+ otherAxisInverse: otherAxisInverse
|
|||
+ };
|
|||
+ }, this);
|
|||
+ }
|
|||
}, this); |
|||
return result; |
|||
}; |
|||
@@ -530,12 +533,17 @@ var SliderZoomView = /** @class */function (_super) {
|
|||
var dataZoomModel = this.dataZoomModel; |
|||
var handleEnds = this._handleEnds; |
|||
var viewExtend = this._getViewExtent(); |
|||
- var minMaxSpan = dataZoomModel.findRepresentativeAxisProxy().getMinMaxSpan();
|
|||
- var percentExtent = [0, 100];
|
|||
- sliderMove(delta, handleEnds, viewExtend, dataZoomModel.get('zoomLock') ? 'all' : handleIndex, minMaxSpan.minSpan != null ? linearMap(minMaxSpan.minSpan, percentExtent, viewExtend, true) : null, minMaxSpan.maxSpan != null ? linearMap(minMaxSpan.maxSpan, percentExtent, viewExtend, true) : null);
|
|||
- var lastRange = this._range;
|
|||
- var range = this._range = asc([linearMap(handleEnds[0], viewExtend, percentExtent, true), linearMap(handleEnds[1], viewExtend, percentExtent, true)]);
|
|||
- return !lastRange || lastRange[0] !== range[0] || lastRange[1] !== range[1];
|
|||
+ var proxy = dataZoomModel.findRepresentativeAxisProxy();
|
|||
+ if (proxy) {
|
|||
+ var minMaxSpan = proxy.getMinMaxSpan();
|
|||
+ var percentExtent = [0, 100];
|
|||
+ sliderMove(delta, handleEnds, viewExtend, dataZoomModel.get('zoomLock') ? 'all' : handleIndex, minMaxSpan.minSpan != null ? linearMap(minMaxSpan.minSpan, percentExtent, viewExtend, true) : null, minMaxSpan.maxSpan != null ? linearMap(minMaxSpan.maxSpan, percentExtent, viewExtend, true) : null);
|
|||
+ var lastRange = this._range;
|
|||
+ var range = this._range = asc([linearMap(handleEnds[0], viewExtend, percentExtent, true), linearMap(handleEnds[1], viewExtend, percentExtent, true)]);
|
|||
+ return !lastRange || lastRange[0] !== range[0] || lastRange[1] !== range[1];
|
|||
+ } else {
|
|||
+ return false;
|
|||
+ }
|
|||
}; |
|||
SliderZoomView.prototype._updateView = function (nonRealtime) { |
|||
var displaybles = this._displayables; |
|||
diff --git a/node_modules/echarts/lib/component/dataZoom/dataZoomProcessor.js b/node_modules/echarts/lib/component/dataZoom/dataZoomProcessor.js
|
|||
index ce98fed..e154118 100644
|
|||
--- a/node_modules/echarts/lib/component/dataZoom/dataZoomProcessor.js
|
|||
+++ b/node_modules/echarts/lib/component/dataZoom/dataZoomProcessor.js
|
|||
@@ -90,7 +90,10 @@ var dataZoomProcessor = {
|
|||
// init stage and not after action dispatch handler, because |
|||
// reset should be called after seriesData.restoreData. |
|||
dataZoomModel.eachTargetAxis(function (axisDim, axisIndex) { |
|||
- dataZoomModel.getAxisProxy(axisDim, axisIndex).reset(dataZoomModel);
|
|||
+ var axisProxy = dataZoomModel.getAxisProxy(axisDim, axisIndex);
|
|||
+ if (axisProxy) {
|
|||
+ axisProxy.reset(dataZoomModel);
|
|||
+ }
|
|||
}); |
|||
// Caution: data zoom filtering is order sensitive when using |
|||
// percent range and no min/max/scale set on axis. |
|||
@@ -107,7 +110,10 @@ var dataZoomProcessor = {
|
|||
// So we should filter x-axis after reset x-axis immediately, |
|||
// and then reset y-axis and filter y-axis. |
|||
dataZoomModel.eachTargetAxis(function (axisDim, axisIndex) { |
|||
- dataZoomModel.getAxisProxy(axisDim, axisIndex).filterData(dataZoomModel, api);
|
|||
+ var axisProxy = dataZoomModel.getAxisProxy(axisDim, axisIndex);
|
|||
+ if (axisProxy) {
|
|||
+ axisProxy.filterData(dataZoomModel, api);
|
|||
+ }
|
|||
}); |
|||
}); |
|||
ecModel.eachComponent('dataZoom', function (dataZoomModel) { |
|||
diff --git a/node_modules/echarts/lib/component/toolbox/feature/DataZoom.js b/node_modules/echarts/lib/component/toolbox/feature/DataZoom.js
|
|||
index cf8d6bc..f9b9f90 100644
|
|||
--- a/node_modules/echarts/lib/component/toolbox/feature/DataZoom.js
|
|||
+++ b/node_modules/echarts/lib/component/toolbox/feature/DataZoom.js
|
|||
@@ -109,9 +109,12 @@ var DataZoomFeature = /** @class */function (_super) {
|
|||
var axisModel = axis.model; |
|||
var dataZoomModel = findDataZoom(dimName, axisModel, ecModel); |
|||
// Restrict range. |
|||
- var minMaxSpan = dataZoomModel.findRepresentativeAxisProxy(axisModel).getMinMaxSpan();
|
|||
- if (minMaxSpan.minValueSpan != null || minMaxSpan.maxValueSpan != null) {
|
|||
- minMax = sliderMove(0, minMax.slice(), axis.scale.getExtent(), 0, minMaxSpan.minValueSpan, minMaxSpan.maxValueSpan);
|
|||
+ var proxy = dataZoomModel.findRepresentativeAxisProxy(axisModel);
|
|||
+ if (proxy) {
|
|||
+ var minMaxSpan = proxy.getMinMaxSpan();
|
|||
+ if (minMaxSpan.minValueSpan != null || minMaxSpan.maxValueSpan != null) {
|
|||
+ minMax = sliderMove(0, minMax.slice(), axis.scale.getExtent(), 0, minMaxSpan.minValueSpan, minMaxSpan.maxValueSpan);
|
|||
+ }
|
|||
} |
|||
dataZoomModel && (snapshot[dataZoomModel.id] = { |
|||
dataZoomId: dataZoomModel.id, |
|||
diff --git a/node_modules/echarts/lib/component/tooltip/TooltipView.js b/node_modules/echarts/lib/component/tooltip/TooltipView.js
|
|||
index b8a9b95..11e49c0 100644
|
|||
--- a/node_modules/echarts/lib/component/tooltip/TooltipView.js
|
|||
+++ b/node_modules/echarts/lib/component/tooltip/TooltipView.js
|
|||
@@ -360,7 +360,7 @@ var TooltipView = /** @class */function (_super) {
|
|||
each(itemCoordSys.dataByAxis, function (axisItem) { |
|||
var axisModel = ecModel.getComponent(axisItem.axisDim + 'Axis', axisItem.axisIndex); |
|||
var axisValue = axisItem.value; |
|||
- if (!axisModel || axisValue == null) {
|
|||
+ if (!axisModel || !axisModel.axis || axisValue == null) {
|
|||
return; |
|||
} |
|||
var axisValueLabel = axisPointerViewHelper.getValueLabel(axisValue, axisModel.axis, ecModel, axisItem.seriesDataIndices, axisItem.valueLabelOpt); |
|||
diff --git a/node_modules/echarts/lib/coord/axisHelper.js b/node_modules/echarts/lib/coord/axisHelper.js
|
|||
index a76c66b..e5b7ee5 100644
|
|||
--- a/node_modules/echarts/lib/coord/axisHelper.js
|
|||
+++ b/node_modules/echarts/lib/coord/axisHelper.js
|
|||
@@ -187,7 +187,9 @@ export function createScaleByModel(model, axisType) {
|
|||
}); |
|||
default: |
|||
// case 'value'/'interval', 'log', or others. |
|||
- return new (Scale.getClass(axisType) || IntervalScale)();
|
|||
+ return new (Scale.getClass(axisType) || IntervalScale)({
|
|||
+ ticksGenerator: model.get('ticksGenerator')
|
|||
+ });
|
|||
} |
|||
} |
|||
} |
|||
diff --git a/node_modules/echarts/lib/coord/cartesian/Grid.js b/node_modules/echarts/lib/coord/cartesian/Grid.js
|
|||
index 5b18f02..39a57f8 100644
|
|||
--- a/node_modules/echarts/lib/coord/cartesian/Grid.js
|
|||
+++ b/node_modules/echarts/lib/coord/cartesian/Grid.js
|
|||
@@ -91,11 +91,11 @@ var Grid = /** @class */function () {
|
|||
var scale = axis.scale; |
|||
if ( |
|||
// Only value and log axis without interval support alignTicks. |
|||
- isIntervalOrLogScale(scale) && model.get('alignTicks') && model.get('interval') == null) {
|
|||
+ isIntervalOrLogScale(scale) && model.get('alignTicks') && model.get('interval') == null && model.get('ticksGenerator') == null) {
|
|||
axisNeedsAlign.push(axis); |
|||
} else { |
|||
niceScaleExtent(scale, model); |
|||
- if (isIntervalOrLogScale(scale)) {
|
|||
+ if (isIntervalOrLogScale(scale) && !scale.isBlank()) {
|
|||
// Can only align to interval or log axis. |
|||
alignTo = axis; |
|||
} |
|||
@@ -105,10 +105,15 @@ var Grid = /** @class */function () {
|
|||
// All axes has set alignTicks. Pick the first one. |
|||
// PENDING. Should we find the axis that both set interval, min, max and align to this one? |
|||
if (axisNeedsAlign.length) { |
|||
- if (!alignTo) {
|
|||
- alignTo = axisNeedsAlign.pop();
|
|||
- niceScaleExtent(alignTo.scale, alignTo.model);
|
|||
+ while (!alignTo && axisNeedsAlign.length) {
|
|||
+ var axis = axisNeedsAlign.pop();
|
|||
+ niceScaleExtent(axis.scale, axis.model);
|
|||
+ if (!axis.scale.isBlank()) {
|
|||
+ alignTo = axis;
|
|||
+ }
|
|||
} |
|||
+ }
|
|||
+ if (axisNeedsAlign.length && alignTo) {
|
|||
each(axisNeedsAlign, function (axis) { |
|||
alignScaleTicks(axis.scale, axis.model, alignTo.scale); |
|||
}); |
|||
diff --git a/node_modules/echarts/lib/data/SeriesData.js b/node_modules/echarts/lib/data/SeriesData.js
|
|||
index 98d5ce8..1c293a6 100644
|
|||
--- a/node_modules/echarts/lib/data/SeriesData.js
|
|||
+++ b/node_modules/echarts/lib/data/SeriesData.js
|
|||
@@ -900,13 +900,16 @@ var SeriesData = /** @class */function () {
|
|||
var dimInfo = data._dimInfos[dim]; |
|||
// Currently, only dimensions that has ordinalMeta can create inverted indices. |
|||
var ordinalMeta = dimInfo.ordinalMeta; |
|||
+ var stack = dimInfo.stack;
|
|||
var store = data._store; |
|||
- if (ordinalMeta) {
|
|||
- invertedIndices = invertedIndicesMap[dim] = new CtorInt32Array(ordinalMeta.categories.length);
|
|||
- // The default value of TypedArray is 0. To avoid miss
|
|||
- // mapping to 0, we should set it as INDEX_NOT_FOUND.
|
|||
- for (var i = 0; i < invertedIndices.length; i++) {
|
|||
- invertedIndices[i] = INDEX_NOT_FOUND;
|
|||
+ if (ordinalMeta || stack) {
|
|||
+ invertedIndices = invertedIndicesMap[dim] = stack ? new Array(store.count()) : new CtorInt32Array(ordinalMeta.categories.length);
|
|||
+ if (ordinalMeta) {
|
|||
+ // The default value of TypedArray is 0. To avoid miss
|
|||
+ // mapping to 0, we should set it as INDEX_NOT_FOUND.
|
|||
+ for (var i = 0; i < invertedIndices.length; i++) {
|
|||
+ invertedIndices[i] = INDEX_NOT_FOUND;
|
|||
+ }
|
|||
} |
|||
for (var i = 0; i < store.count(); i++) { |
|||
// Only support the case that all values are distinct. |
|||
diff --git a/node_modules/echarts/lib/data/Source.js b/node_modules/echarts/lib/data/Source.js
|
|||
index 7dda49b..2dd2b98 100644
|
|||
--- a/node_modules/echarts/lib/data/Source.js
|
|||
+++ b/node_modules/echarts/lib/data/Source.js
|
|||
@@ -252,7 +252,8 @@ function normalizeDimensionsOption(dimensionsDefine) {
|
|||
var item = { |
|||
name: rawItem.name, |
|||
displayName: rawItem.displayName, |
|||
- type: rawItem.type
|
|||
+ type: rawItem.type,
|
|||
+ stack: rawItem.stack
|
|||
}; |
|||
// User can set null in dimensions. |
|||
// We don't auto specify name, otherwise a given name may |
|||
diff --git a/node_modules/echarts/lib/data/helper/createDimensions.js b/node_modules/echarts/lib/data/helper/createDimensions.js
|
|||
index 00d7eb7..dd514b1 100644
|
|||
--- a/node_modules/echarts/lib/data/helper/createDimensions.js
|
|||
+++ b/node_modules/echarts/lib/data/helper/createDimensions.js
|
|||
@@ -110,6 +110,9 @@ source, opt) {
|
|||
} |
|||
dimDefItem.type != null && (resultItem.type = dimDefItem.type); |
|||
dimDefItem.displayName != null && (resultItem.displayName = dimDefItem.displayName); |
|||
+ if (dimDefItem.stack) {
|
|||
+ resultItem.stack = true;
|
|||
+ }
|
|||
var newIdx = resultList.length; |
|||
indicesMap[dimIdx] = newIdx; |
|||
resultItem.storeDimIndex = dimIdx; |
|||
diff --git a/node_modules/echarts/lib/data/helper/dataStackHelper.js b/node_modules/echarts/lib/data/helper/dataStackHelper.js
|
|||
index c25de1b..ea8300d 100644
|
|||
--- a/node_modules/echarts/lib/data/helper/dataStackHelper.js
|
|||
+++ b/node_modules/echarts/lib/data/helper/dataStackHelper.js
|
|||
@@ -91,7 +91,7 @@ export function enableDataStack(seriesModel, dimensionsInput, opt) {
|
|||
} |
|||
if (mayStack && !dimensionInfo.isExtraCoord) { |
|||
// Find the first ordinal dimension as the stackedByDimInfo. |
|||
- if (!byIndex && !stackedByDimInfo && dimensionInfo.ordinalMeta) {
|
|||
+ if (!byIndex && !stackedByDimInfo && (dimensionInfo.ordinalMeta || dimensionInfo.stack)) {
|
|||
stackedByDimInfo = dimensionInfo; |
|||
} |
|||
// Find the first stackable dimension as the stackedDimInfo. |
|||
diff --git a/node_modules/echarts/lib/scale/Interval.js b/node_modules/echarts/lib/scale/Interval.js
|
|||
index 1094662..363c0a5 100644
|
|||
--- a/node_modules/echarts/lib/scale/Interval.js
|
|||
+++ b/node_modules/echarts/lib/scale/Interval.js
|
|||
@@ -46,12 +46,17 @@ import * as numberUtil from '../util/number.js';
|
|||
import * as formatUtil from '../util/format.js'; |
|||
import Scale from './Scale.js'; |
|||
import * as helper from './helper.js'; |
|||
+import { isFunction } from 'zrender/lib/core/util.js';
|
|||
var roundNumber = numberUtil.round; |
|||
var IntervalScale = /** @class */function (_super) { |
|||
__extends(IntervalScale, _super); |
|||
- function IntervalScale() {
|
|||
- var _this = _super !== null && _super.apply(this, arguments) || this;
|
|||
+ function IntervalScale(setting) {
|
|||
+ var _this = _super.call(this, setting) || this;
|
|||
_this.type = 'interval'; |
|||
+ var ticksGenerator = _this.getSetting('ticksGenerator');
|
|||
+ if (isFunction(ticksGenerator)) {
|
|||
+ _this._ticksGenerator = ticksGenerator;
|
|||
+ }
|
|||
// Step is calculated in adjustExtent. |
|||
_this._interval = 0; |
|||
_this._intervalPrecision = 2; |
|||
@@ -104,7 +109,17 @@ var IntervalScale = /** @class */function (_super) {
|
|||
var extent = this._extent; |
|||
var niceTickExtent = this._niceExtent; |
|||
var intervalPrecision = this._intervalPrecision; |
|||
- var ticks = [];
|
|||
+ var ticksGenerator = this._ticksGenerator;
|
|||
+ var ticks;
|
|||
+ if (ticksGenerator) {
|
|||
+ try {
|
|||
+ ticks = ticksGenerator(extent, interval, niceTickExtent, intervalPrecision);
|
|||
+ if (ticks) {
|
|||
+ return ticks;
|
|||
+ }
|
|||
+ } catch (_e) {}
|
|||
+ }
|
|||
+ ticks = [];
|
|||
// If interval is 0, return []; |
|||
if (!interval) { |
|||
return ticks; |
|||
diff --git a/node_modules/echarts/types/dist/shared.d.ts b/node_modules/echarts/types/dist/shared.d.ts
|
|||
index ca74097..ef41ce2 100644
|
|||
--- a/node_modules/echarts/types/dist/shared.d.ts
|
|||
+++ b/node_modules/echarts/types/dist/shared.d.ts
|
|||
@@ -2422,6 +2422,9 @@ interface AxisBaseOptionCommon extends ComponentOption, AnimationOptionMixin {
|
|||
max: number; |
|||
}) => ScaleDataValue); |
|||
} |
|||
+
|
|||
+declare type NumericAxisTicksGenerator = (extent?: number[], interval?: number, niceTickExtent?: number[], intervalPrecision?: number) => {value: number}[];
|
|||
+
|
|||
interface NumericAxisBaseOptionCommon extends AxisBaseOptionCommon { |
|||
boundaryGap?: [number | string, number | string]; |
|||
/** |
|||
@@ -2447,6 +2450,8 @@ interface NumericAxisBaseOptionCommon extends AxisBaseOptionCommon {
|
|||
* Will be ignored if interval is set. |
|||
*/ |
|||
alignTicks?: boolean; |
|||
+
|
|||
+ ticksGenerator?: NumericAxisTicksGenerator;
|
|||
} |
|||
interface CategoryAxisBaseOption extends AxisBaseOptionCommon { |
|||
type?: 'category'; |
|||
@@ -6412,6 +6417,7 @@ declare type DimensionDefinition = {
|
|||
type?: DataStoreDimensionType; |
|||
name?: DimensionName; |
|||
displayName?: string; |
|||
+ stack?: boolean;
|
|||
}; |
|||
declare type DimensionDefinitionLoose = DimensionDefinition['name'] | DimensionDefinition; |
|||
declare const SOURCE_FORMAT_ORIGINAL: "original"; |
|||
diff --git a/node_modules/echarts/types/src/coord/axisCommonTypes.d.ts b/node_modules/echarts/types/src/coord/axisCommonTypes.d.ts
|
|||
index c5c2792..d524b70 100644
|
|||
--- a/node_modules/echarts/types/src/coord/axisCommonTypes.d.ts
|
|||
+++ b/node_modules/echarts/types/src/coord/axisCommonTypes.d.ts
|
|||
@@ -56,6 +56,9 @@ export interface AxisBaseOptionCommon extends ComponentOption, AnimationOptionMi
|
|||
max: number; |
|||
}) => ScaleDataValue); |
|||
} |
|||
+
|
|||
+export declare type NumericAxisTicksGenerator = (extent?: number[], interval?: number, niceTickExtent?: number[], intervalPrecision?: number) => {value: number}[];
|
|||
+
|
|||
export interface NumericAxisBaseOptionCommon extends AxisBaseOptionCommon { |
|||
boundaryGap?: [number | string, number | string]; |
|||
/** |
|||
@@ -81,6 +84,8 @@ export interface NumericAxisBaseOptionCommon extends AxisBaseOptionCommon {
|
|||
* Will be ignored if interval is set. |
|||
*/ |
|||
alignTicks?: boolean; |
|||
+
|
|||
+ ticksGenerator?: NumericAxisTicksGenerator;
|
|||
} |
|||
export interface CategoryAxisBaseOption extends AxisBaseOptionCommon { |
|||
type?: 'category'; |
|||
@ -0,0 +1,259 @@ |
|||
<!-- |
|||
|
|||
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]="timeSeriesChartWidgetConfigForm"> |
|||
<tb-timewindow-config-panel formControlName="timewindowConfig"> |
|||
</tb-timewindow-config-panel> |
|||
<tb-datasources |
|||
[configMode]="basicMode" |
|||
hideDatasourceLabel |
|||
hideDataKeys |
|||
forceSingleDatasource |
|||
formControlName="datasources"> |
|||
</tb-datasources> |
|||
<tb-data-keys-panel |
|||
panelTitle="{{ 'widgets.chart.series' | translate }}" |
|||
addKeyTitle="{{ 'widgets.chart.add-series' | translate }}" |
|||
keySettingsTitle="{{ 'widgets.chart.series-settings' | translate }}" |
|||
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" |
|||
[deviceId]="datasource?.deviceId" |
|||
[entityAliasId]="datasource?.entityAliasId" |
|||
formControlName="series"> |
|||
</tb-data-keys-panel> |
|||
<tb-time-series-chart-y-axes-panel |
|||
formControlName="yAxes" |
|||
(axisRemoved)="yAxisRemoved($event)"> |
|||
</tb-time-series-chart-y-axes-panel> |
|||
<tb-time-series-chart-thresholds-panel |
|||
formControlName="thresholds" |
|||
[aliasController]="aliasController" |
|||
[dataKeyCallbacks]="callbacks" |
|||
[datasource]="datasource" |
|||
[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> |
|||
<div class="tb-form-row column-xs"> |
|||
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="showTitle"> |
|||
{{ 'widget-config.title' | translate }} |
|||
</mat-slide-toggle> |
|||
<div fxFlex fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px"> |
|||
<mat-form-field class="flex" appearance="outline" subscriptSizing="dynamic"> |
|||
<input matInput formControlName="title" placeholder="{{ 'widget-config.set' | translate }}"> |
|||
</mat-form-field> |
|||
<tb-font-settings formControlName="titleFont" |
|||
clearButton |
|||
[previewText]="timeSeriesChartWidgetConfigForm.get('title').value" |
|||
[initialPreviewStyle]="widgetConfig.config.titleStyle"> |
|||
</tb-font-settings> |
|||
<tb-color-input asBoxInput |
|||
colorClearButton |
|||
formControlName="titleColor"> |
|||
</tb-color-input> |
|||
</div> |
|||
</div> |
|||
<div class="tb-form-row column-xs"> |
|||
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="showIcon"> |
|||
{{ 'widget-config.card-icon' | translate }} |
|||
</mat-slide-toggle> |
|||
<div fxFlex fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px"> |
|||
<mat-form-field appearance="outline" class="flex number" subscriptSizing="dynamic"> |
|||
<input matInput type="number" min="0" formControlName="iconSize" placeholder="{{ 'widget-config.set' | translate }}"> |
|||
</mat-form-field> |
|||
<tb-css-unit-select fxFlex formControlName="iconSizeUnit"></tb-css-unit-select> |
|||
<tb-material-icon-select asBoxInput |
|||
iconClearButton |
|||
[color]="timeSeriesChartWidgetConfigForm.get('iconColor').value" |
|||
formControlName="icon"> |
|||
</tb-material-icon-select> |
|||
<tb-color-input asBoxInput |
|||
colorClearButton |
|||
formControlName="iconColor"> |
|||
</tb-color-input> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
<div class="tb-form-panel"> |
|||
<div class="tb-form-panel-title" translate>widgets.time-series-chart.chart</div> |
|||
<div class="tb-form-row"> |
|||
<mat-slide-toggle class="mat-slide" formControlName="dataZoom"> |
|||
{{ 'widgets.time-series-chart.data-zoom' | translate }} |
|||
</mat-slide-toggle> |
|||
</div> |
|||
<div class="tb-form-row"> |
|||
<mat-slide-toggle class="mat-slide" formControlName="stack"> |
|||
<div tb-hint-tooltip-icon="{{'widgets.time-series-chart.stack-mode-hint' | translate}}"> |
|||
{{ 'widgets.time-series-chart.stack-mode' | translate }} |
|||
</div> |
|||
</mat-slide-toggle> |
|||
</div> |
|||
</div> |
|||
<div class="tb-form-panel"> |
|||
<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"> |
|||
</tb-time-series-chart-axis-settings> |
|||
</div> |
|||
<tb-time-series-no-aggregation-bar-width-settings |
|||
*ngIf="chartType === TimeSeriesChartType.default || chartType === TimeSeriesChartType.bar" |
|||
formControlName="noAggregationBarWidthSettings"> |
|||
</tb-time-series-no-aggregation-bar-width-settings> |
|||
<div class="tb-form-panel tb-slide-toggle"> |
|||
<mat-expansion-panel class="tb-settings" [expanded]="timeSeriesChartWidgetConfigForm.get('showLegend').value" |
|||
[disabled]="!timeSeriesChartWidgetConfigForm.get('showLegend').value"> |
|||
<mat-expansion-panel-header fxLayout="row wrap"> |
|||
<mat-panel-title> |
|||
<mat-slide-toggle class="mat-slide" formControlName="showLegend" (click)="$event.stopPropagation()" |
|||
fxLayoutAlign="center"> |
|||
{{ 'widget-config.legend' | translate }} |
|||
</mat-slide-toggle> |
|||
</mat-panel-title> |
|||
</mat-expansion-panel-header> |
|||
<ng-template matExpansionPanelContent> |
|||
<div class="tb-form-row space-between"> |
|||
<div>{{ 'legend.label' | translate }}</div> |
|||
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px"> |
|||
<tb-font-settings formControlName="legendLabelFont" |
|||
previewText="Temperature"> |
|||
</tb-font-settings> |
|||
<tb-color-input asBoxInput |
|||
colorClearButton |
|||
formControlName="legendLabelColor"> |
|||
</tb-color-input> |
|||
</div> |
|||
</div> |
|||
<tb-legend-config hideDirection |
|||
formControlName="legendConfig"> |
|||
</tb-legend-config> |
|||
</ng-template> |
|||
</mat-expansion-panel> |
|||
</div> |
|||
<div class="tb-form-panel tb-slide-toggle"> |
|||
<mat-expansion-panel class="tb-settings" [expanded]="timeSeriesChartWidgetConfigForm.get('showTooltip').value" |
|||
[disabled]="!timeSeriesChartWidgetConfigForm.get('showTooltip').value"> |
|||
<mat-expansion-panel-header fxLayout="row wrap"> |
|||
<mat-panel-title> |
|||
<mat-slide-toggle class="mat-slide" formControlName="showTooltip" (click)="$event.stopPropagation()" |
|||
fxLayoutAlign="center"> |
|||
{{ 'widget-config.tooltip' | translate }} |
|||
</mat-slide-toggle> |
|||
</mat-panel-title> |
|||
</mat-expansion-panel-header> |
|||
<ng-template matExpansionPanelContent> |
|||
<div class="tb-form-row space-between"> |
|||
<div>{{ 'tooltip.trigger' | translate }}</div> |
|||
<mat-chip-listbox class="center-stretch" formControlName="tooltipTrigger"> |
|||
<mat-chip-option [selectable]="timeSeriesChartWidgetConfigForm.get('tooltipTrigger').value !== EChartsTooltipTrigger.point" |
|||
[value]="EChartsTooltipTrigger.point">{{ 'tooltip.trigger-point' | translate }}</mat-chip-option> |
|||
<mat-chip-option [selectable]="timeSeriesChartWidgetConfigForm.get('tooltipTrigger').value !== EChartsTooltipTrigger.axis" |
|||
[value]="EChartsTooltipTrigger.axis">{{ 'tooltip.trigger-axis' | translate }}</mat-chip-option> |
|||
</mat-chip-listbox> |
|||
</div> |
|||
<div class="tb-form-row space-between"> |
|||
<div>{{ 'tooltip.value' | translate }}</div> |
|||
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px"> |
|||
<tb-font-settings formControlName="tooltipValueFont" |
|||
[previewText]="tooltipValuePreviewFn"> |
|||
</tb-font-settings> |
|||
<tb-color-input asBoxInput |
|||
colorClearButton |
|||
formControlName="tooltipValueColor"> |
|||
</tb-color-input> |
|||
</div> |
|||
</div> |
|||
<div class="tb-form-row column-xs"> |
|||
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="tooltipShowDate"> |
|||
{{ 'tooltip.date' | translate }} |
|||
</mat-slide-toggle> |
|||
<div fxFlex.gt-xs fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px"> |
|||
<tb-date-format-select fxFlex excludeLastUpdateAgo includeAuto |
|||
formControlName="tooltipDateFormat"></tb-date-format-select> |
|||
<tb-font-settings formControlName="tooltipDateFont" |
|||
[previewText]="tooltipDatePreviewFn"> |
|||
</tb-font-settings> |
|||
<tb-color-input asBoxInput |
|||
colorClearButton |
|||
formControlName="tooltipDateColor"> |
|||
</tb-color-input> |
|||
</div> |
|||
</div> |
|||
<div class="tb-form-row"> |
|||
<mat-slide-toggle class="mat-slide" formControlName="tooltipDateInterval"> |
|||
<div tb-hint-tooltip-icon="{{'tooltip.show-date-time-interval-hint' | translate}}"> |
|||
{{ 'tooltip.show-date-time-interval' | translate }} |
|||
</div> |
|||
</mat-slide-toggle> |
|||
</div> |
|||
<div class="tb-form-row space-between"> |
|||
<div>{{ 'tooltip.background-color' | translate }}</div> |
|||
<tb-color-input asBoxInput |
|||
colorClearButton |
|||
formControlName="tooltipBackgroundColor"> |
|||
</tb-color-input> |
|||
</div> |
|||
<div class="tb-form-row space-between"> |
|||
<div>{{ 'tooltip.background-blur' | translate }}</div> |
|||
<mat-form-field appearance="outline" class="number" subscriptSizing="dynamic"> |
|||
<input matInput formControlName="tooltipBackgroundBlur" type="number" min="0" step="1" placeholder="{{ 'widget-config.set' | translate }}"> |
|||
<div matSuffix>px</div> |
|||
</mat-form-field> |
|||
</div> |
|||
</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"> |
|||
<div>{{ 'widgets.background.background' | translate }}</div> |
|||
<tb-background-settings formControlName="background"> |
|||
</tb-background-settings> |
|||
</div> |
|||
<div class="tb-form-row space-between column-lt-md"> |
|||
<div translate>widget-config.show-card-buttons</div> |
|||
<mat-chip-listbox multiple formControlName="cardButtons"> |
|||
<mat-chip-option value="fullscreen">{{ 'fullscreen.fullscreen' | translate }}</mat-chip-option> |
|||
</mat-chip-listbox> |
|||
</div> |
|||
<div class="tb-form-row space-between"> |
|||
<div>{{ 'widget-config.card-border-radius' | translate }}</div> |
|||
<mat-form-field appearance="outline" subscriptSizing="dynamic"> |
|||
<input matInput formControlName="borderRadius" placeholder="{{ 'widget-config.set' | translate }}"> |
|||
</mat-form-field> |
|||
</div> |
|||
<div class="tb-form-row space-between"> |
|||
<div>{{ 'widget-config.card-padding' | translate }}</div> |
|||
<mat-form-field appearance="outline" subscriptSizing="dynamic"> |
|||
<input matInput formControlName="padding" placeholder="{{ 'widget-config.set' | translate }}"> |
|||
</mat-form-field> |
|||
</div> |
|||
</div> |
|||
<tb-widget-actions-panel |
|||
formControlName="actions"> |
|||
</tb-widget-actions-panel> |
|||
</ng-container> |
|||
@ -0,0 +1,367 @@ |
|||
///
|
|||
/// Copyright © 2016-2024 The Thingsboard Authors
|
|||
///
|
|||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
|||
/// you may not use this file except in compliance with the License.
|
|||
/// You may obtain a copy of the License at
|
|||
///
|
|||
/// http://www.apache.org/licenses/LICENSE-2.0
|
|||
///
|
|||
/// Unless required by applicable law or agreed to in writing, software
|
|||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
|||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|||
/// See the License for the specific language governing permissions and
|
|||
/// limitations under the License.
|
|||
///
|
|||
|
|||
import { Component, Injector } from '@angular/core'; |
|||
import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; |
|||
import { Store } from '@ngrx/store'; |
|||
import { AppState } from '@core/core.state'; |
|||
import { BasicWidgetConfigComponent } from '@home/components/widget/config/widget-config.component.models'; |
|||
import { WidgetConfigComponentData } from '@home/models/widget-component.models'; |
|||
import { |
|||
DataKey, |
|||
Datasource, |
|||
legendPositions, |
|||
legendPositionTranslationMap, |
|||
WidgetConfig, |
|||
} from '@shared/models/widget.models'; |
|||
import { WidgetConfigComponent } from '@home/components/widget/widget-config.component'; |
|||
import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; |
|||
import { |
|||
getTimewindowConfig, |
|||
setTimewindowConfig |
|||
} from '@home/components/widget/config/timewindow-config-panel.component'; |
|||
import { formatValue, isDefinedAndNotNull, isUndefined, mergeDeep } from '@core/utils'; |
|||
import { |
|||
cssSizeToStrSize, |
|||
DateFormatProcessor, |
|||
DateFormatSettings, |
|||
resolveCssSize |
|||
} from '@shared/models/widget-settings.models'; |
|||
import { |
|||
timeSeriesChartWidgetDefaultSettings, |
|||
TimeSeriesChartWidgetSettings |
|||
} from '@home/components/widget/lib/chart/time-series-chart-widget.models'; |
|||
import { EChartsTooltipTrigger } from '@home/components/widget/lib/chart/echarts-widget.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', |
|||
templateUrl: './time-series-chart-basic-config.component.html', |
|||
styleUrls: ['../basic-config.scss'] |
|||
}) |
|||
export class TimeSeriesChartBasicConfigComponent extends BasicWidgetConfigComponent { |
|||
|
|||
public get datasource(): Datasource { |
|||
const datasources: Datasource[] = this.timeSeriesChartWidgetConfigForm.get('datasources').value; |
|||
if (datasources && datasources.length) { |
|||
return datasources[0]; |
|||
} else { |
|||
return null; |
|||
} |
|||
} |
|||
|
|||
public get yAxisIds(): TimeSeriesChartYAxisId[] { |
|||
const yAxes: TimeSeriesChartYAxes = this.timeSeriesChartWidgetConfigForm.get('yAxes').value; |
|||
return Object.keys(yAxes); |
|||
} |
|||
|
|||
TimeSeriesChartType = TimeSeriesChartType; |
|||
|
|||
EChartsTooltipTrigger = EChartsTooltipTrigger; |
|||
|
|||
legendPositions = legendPositions; |
|||
|
|||
legendPositionTranslationMap = legendPositionTranslationMap; |
|||
|
|||
timeSeriesChartWidgetConfigForm: UntypedFormGroup; |
|||
|
|||
tooltipValuePreviewFn = this._tooltipValuePreviewFn.bind(this); |
|||
|
|||
tooltipDatePreviewFn = this._tooltipDatePreviewFn.bind(this); |
|||
|
|||
chartType: TimeSeriesChartType = TimeSeriesChartType.default; |
|||
|
|||
constructor(protected store: Store<AppState>, |
|||
protected widgetConfigComponent: WidgetConfigComponent, |
|||
private $injector: Injector, |
|||
private fb: UntypedFormBuilder) { |
|||
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; |
|||
} |
|||
|
|||
protected setupConfig(widgetConfig: WidgetConfigComponentData) { |
|||
const params = widgetConfig.typeParameters as any; |
|||
if (isDefinedAndNotNull(params.chartType)) { |
|||
this.chartType = params.chartType; |
|||
} |
|||
super.setupConfig(widgetConfig); |
|||
} |
|||
|
|||
protected defaultDataKeys(configData: WidgetConfigComponentData): DataKey[] { |
|||
return [{ name: 'temperature', label: 'Temperature', type: DataKeyType.timeseries, units: '°C', decimals: 0 }]; |
|||
} |
|||
|
|||
protected onConfigSet(configData: WidgetConfigComponentData) { |
|||
const settings: TimeSeriesChartWidgetSettings = mergeDeep<TimeSeriesChartWidgetSettings>({} as TimeSeriesChartWidgetSettings, |
|||
timeSeriesChartWidgetDefaultSettings, configData.config.settings as TimeSeriesChartWidgetSettings); |
|||
const iconSize = resolveCssSize(configData.config.iconSize); |
|||
this.timeSeriesChartWidgetConfigForm = this.fb.group({ |
|||
timewindowConfig: [getTimewindowConfig(configData.config), []], |
|||
datasources: [configData.config.datasources, []], |
|||
|
|||
yAxes: [settings.yAxes, []], |
|||
series: [this.getSeries(configData.config.datasources), []], |
|||
thresholds: [settings.thresholds, []], |
|||
|
|||
showTitle: [configData.config.showTitle, []], |
|||
title: [configData.config.title, []], |
|||
titleFont: [configData.config.titleFont, []], |
|||
titleColor: [configData.config.titleColor, []], |
|||
|
|||
showIcon: [configData.config.showTitleIcon, []], |
|||
iconSize: [iconSize[0], [Validators.min(0)]], |
|||
iconSizeUnit: [iconSize[1], []], |
|||
icon: [configData.config.titleIcon, []], |
|||
iconColor: [configData.config.iconColor, []], |
|||
|
|||
dataZoom: [settings.dataZoom, []], |
|||
stack: [settings.stack, []], |
|||
|
|||
xAxis: [settings.xAxis, []], |
|||
|
|||
noAggregationBarWidthSettings: [settings.noAggregationBarWidthSettings, []], |
|||
|
|||
showLegend: [settings.showLegend, []], |
|||
legendLabelFont: [settings.legendLabelFont, []], |
|||
legendLabelColor: [settings.legendLabelColor, []], |
|||
legendConfig: [settings.legendConfig, []], |
|||
|
|||
showTooltip: [settings.showTooltip, []], |
|||
tooltipTrigger: [settings.tooltipTrigger, []], |
|||
tooltipValueFont: [settings.tooltipValueFont, []], |
|||
tooltipValueColor: [settings.tooltipValueColor, []], |
|||
tooltipShowDate: [settings.tooltipShowDate, []], |
|||
tooltipDateFormat: [settings.tooltipDateFormat, []], |
|||
tooltipDateFont: [settings.tooltipDateFont, []], |
|||
tooltipDateColor: [settings.tooltipDateColor, []], |
|||
tooltipDateInterval: [settings.tooltipDateInterval, []], |
|||
|
|||
tooltipBackgroundColor: [settings.tooltipBackgroundColor, []], |
|||
tooltipBackgroundBlur: [settings.tooltipBackgroundBlur, []], |
|||
|
|||
animation: [settings.animation, []], |
|||
|
|||
background: [settings.background, []], |
|||
|
|||
cardButtons: [this.getCardButtons(configData.config), []], |
|||
borderRadius: [configData.config.borderRadius, []], |
|||
padding: [settings.padding, []], |
|||
|
|||
actions: [configData.config.actions || {}, []] |
|||
}); |
|||
} |
|||
|
|||
protected prepareOutputConfig(config: any): WidgetConfigComponentData { |
|||
setTimewindowConfig(this.widgetConfig.config, config.timewindowConfig); |
|||
this.widgetConfig.config.datasources = config.datasources; |
|||
this.setSeries(config.series, this.widgetConfig.config.datasources); |
|||
|
|||
this.widgetConfig.config.showTitle = config.showTitle; |
|||
this.widgetConfig.config.title = config.title; |
|||
this.widgetConfig.config.titleFont = config.titleFont; |
|||
this.widgetConfig.config.titleColor = config.titleColor; |
|||
|
|||
this.widgetConfig.config.showTitleIcon = config.showIcon; |
|||
this.widgetConfig.config.iconSize = cssSizeToStrSize(config.iconSize, config.iconSizeUnit); |
|||
this.widgetConfig.config.titleIcon = config.icon; |
|||
this.widgetConfig.config.iconColor = config.iconColor; |
|||
|
|||
this.widgetConfig.config.settings = this.widgetConfig.config.settings || {}; |
|||
|
|||
this.widgetConfig.config.settings.thresholds = config.thresholds; |
|||
|
|||
this.widgetConfig.config.settings.dataZoom = config.dataZoom; |
|||
this.widgetConfig.config.settings.stack = config.stack; |
|||
|
|||
this.widgetConfig.config.settings.yAxes = config.yAxes; |
|||
this.widgetConfig.config.settings.xAxis = config.xAxis; |
|||
|
|||
this.widgetConfig.config.settings.noAggregationBarWidthSettings = config.noAggregationBarWidthSettings; |
|||
|
|||
this.widgetConfig.config.settings.showLegend = config.showLegend; |
|||
this.widgetConfig.config.settings.legendLabelFont = config.legendLabelFont; |
|||
this.widgetConfig.config.settings.legendLabelColor = config.legendLabelColor; |
|||
this.widgetConfig.config.settings.legendConfig = config.legendConfig; |
|||
|
|||
this.widgetConfig.config.settings.showTooltip = config.showTooltip; |
|||
this.widgetConfig.config.settings.tooltipTrigger = config.tooltipTrigger; |
|||
this.widgetConfig.config.settings.tooltipValueFont = config.tooltipValueFont; |
|||
this.widgetConfig.config.settings.tooltipValueColor = config.tooltipValueColor; |
|||
this.widgetConfig.config.settings.tooltipShowDate = config.tooltipShowDate; |
|||
this.widgetConfig.config.settings.tooltipDateFormat = config.tooltipDateFormat; |
|||
this.widgetConfig.config.settings.tooltipDateFont = config.tooltipDateFont; |
|||
this.widgetConfig.config.settings.tooltipDateColor = config.tooltipDateColor; |
|||
this.widgetConfig.config.settings.tooltipDateInterval = config.tooltipDateInterval; |
|||
this.widgetConfig.config.settings.tooltipBackgroundColor = config.tooltipBackgroundColor; |
|||
this.widgetConfig.config.settings.tooltipBackgroundBlur = config.tooltipBackgroundBlur; |
|||
|
|||
this.widgetConfig.config.settings.animation = config.animation; |
|||
|
|||
this.widgetConfig.config.settings.background = config.background; |
|||
|
|||
this.setCardButtons(config.cardButtons, this.widgetConfig.config); |
|||
this.widgetConfig.config.borderRadius = config.borderRadius; |
|||
this.widgetConfig.config.settings.padding = config.padding; |
|||
|
|||
this.widgetConfig.config.actions = config.actions; |
|||
return this.widgetConfig; |
|||
} |
|||
|
|||
protected validatorTriggers(): string[] { |
|||
return ['showTitle', 'showIcon', 'showLegend', 'showTooltip', 'tooltipShowDate']; |
|||
} |
|||
|
|||
protected updateValidators(emitEvent: boolean, trigger?: string) { |
|||
const showTitle: boolean = this.timeSeriesChartWidgetConfigForm.get('showTitle').value; |
|||
const showIcon: boolean = this.timeSeriesChartWidgetConfigForm.get('showIcon').value; |
|||
const showLegend: boolean = this.timeSeriesChartWidgetConfigForm.get('showLegend').value; |
|||
const showTooltip: boolean = this.timeSeriesChartWidgetConfigForm.get('showTooltip').value; |
|||
const tooltipShowDate: boolean = this.timeSeriesChartWidgetConfigForm.get('tooltipShowDate').value; |
|||
|
|||
if (showTitle) { |
|||
this.timeSeriesChartWidgetConfigForm.get('title').enable(); |
|||
this.timeSeriesChartWidgetConfigForm.get('titleFont').enable(); |
|||
this.timeSeriesChartWidgetConfigForm.get('titleColor').enable(); |
|||
this.timeSeriesChartWidgetConfigForm.get('showIcon').enable({emitEvent: false}); |
|||
if (showIcon) { |
|||
this.timeSeriesChartWidgetConfigForm.get('iconSize').enable(); |
|||
this.timeSeriesChartWidgetConfigForm.get('iconSizeUnit').enable(); |
|||
this.timeSeriesChartWidgetConfigForm.get('icon').enable(); |
|||
this.timeSeriesChartWidgetConfigForm.get('iconColor').enable(); |
|||
} else { |
|||
this.timeSeriesChartWidgetConfigForm.get('iconSize').disable(); |
|||
this.timeSeriesChartWidgetConfigForm.get('iconSizeUnit').disable(); |
|||
this.timeSeriesChartWidgetConfigForm.get('icon').disable(); |
|||
this.timeSeriesChartWidgetConfigForm.get('iconColor').disable(); |
|||
} |
|||
} else { |
|||
this.timeSeriesChartWidgetConfigForm.get('title').disable(); |
|||
this.timeSeriesChartWidgetConfigForm.get('titleFont').disable(); |
|||
this.timeSeriesChartWidgetConfigForm.get('titleColor').disable(); |
|||
this.timeSeriesChartWidgetConfigForm.get('showIcon').disable({emitEvent: false}); |
|||
this.timeSeriesChartWidgetConfigForm.get('iconSize').disable(); |
|||
this.timeSeriesChartWidgetConfigForm.get('iconSizeUnit').disable(); |
|||
this.timeSeriesChartWidgetConfigForm.get('icon').disable(); |
|||
this.timeSeriesChartWidgetConfigForm.get('iconColor').disable(); |
|||
} |
|||
|
|||
if (showLegend) { |
|||
this.timeSeriesChartWidgetConfigForm.get('legendLabelFont').enable(); |
|||
this.timeSeriesChartWidgetConfigForm.get('legendLabelColor').enable(); |
|||
this.timeSeriesChartWidgetConfigForm.get('legendConfig').enable(); |
|||
} else { |
|||
this.timeSeriesChartWidgetConfigForm.get('legendLabelFont').disable(); |
|||
this.timeSeriesChartWidgetConfigForm.get('legendLabelColor').disable(); |
|||
this.timeSeriesChartWidgetConfigForm.get('legendConfig').disable(); |
|||
} |
|||
|
|||
if (showTooltip) { |
|||
this.timeSeriesChartWidgetConfigForm.get('tooltipTrigger').enable(); |
|||
this.timeSeriesChartWidgetConfigForm.get('tooltipValueFont').enable(); |
|||
this.timeSeriesChartWidgetConfigForm.get('tooltipValueColor').enable(); |
|||
this.timeSeriesChartWidgetConfigForm.get('tooltipShowDate').enable({emitEvent: false}); |
|||
this.timeSeriesChartWidgetConfigForm.get('tooltipBackgroundColor').enable(); |
|||
this.timeSeriesChartWidgetConfigForm.get('tooltipBackgroundBlur').enable(); |
|||
if (tooltipShowDate) { |
|||
this.timeSeriesChartWidgetConfigForm.get('tooltipDateFormat').enable(); |
|||
this.timeSeriesChartWidgetConfigForm.get('tooltipDateFont').enable(); |
|||
this.timeSeriesChartWidgetConfigForm.get('tooltipDateColor').enable(); |
|||
this.timeSeriesChartWidgetConfigForm.get('tooltipDateInterval').enable(); |
|||
} else { |
|||
this.timeSeriesChartWidgetConfigForm.get('tooltipDateFormat').disable(); |
|||
this.timeSeriesChartWidgetConfigForm.get('tooltipDateFont').disable(); |
|||
this.timeSeriesChartWidgetConfigForm.get('tooltipDateColor').disable(); |
|||
this.timeSeriesChartWidgetConfigForm.get('tooltipDateInterval').disable(); |
|||
} |
|||
} else { |
|||
this.timeSeriesChartWidgetConfigForm.get('tooltipValueFont').disable(); |
|||
this.timeSeriesChartWidgetConfigForm.get('tooltipValueColor').disable(); |
|||
this.timeSeriesChartWidgetConfigForm.get('tooltipShowDate').disable({emitEvent: false}); |
|||
this.timeSeriesChartWidgetConfigForm.get('tooltipDateFormat').disable(); |
|||
this.timeSeriesChartWidgetConfigForm.get('tooltipDateFont').disable(); |
|||
this.timeSeriesChartWidgetConfigForm.get('tooltipDateColor').disable(); |
|||
this.timeSeriesChartWidgetConfigForm.get('tooltipDateInterval').disable(); |
|||
this.timeSeriesChartWidgetConfigForm.get('tooltipBackgroundColor').disable(); |
|||
this.timeSeriesChartWidgetConfigForm.get('tooltipBackgroundBlur').disable(); |
|||
} |
|||
} |
|||
|
|||
private getSeries(datasources?: Datasource[]): DataKey[] { |
|||
if (datasources && datasources.length) { |
|||
return datasources[0].dataKeys || []; |
|||
} |
|||
return []; |
|||
} |
|||
|
|||
private setSeries(series: DataKey[], datasources?: Datasource[]) { |
|||
if (datasources && datasources.length) { |
|||
datasources[0].dataKeys = series; |
|||
} |
|||
} |
|||
|
|||
private 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) { |
|||
buttons.push('fullscreen'); |
|||
} |
|||
return buttons; |
|||
} |
|||
|
|||
private setCardButtons(buttons: string[], config: WidgetConfig) { |
|||
config.enableFullscreen = buttons.includes('fullscreen'); |
|||
} |
|||
|
|||
private _tooltipValuePreviewFn(): string { |
|||
return formatValue(22, 0, '°C', false); |
|||
} |
|||
|
|||
private _tooltipDatePreviewFn(): string { |
|||
const dateFormat: DateFormatSettings = this.timeSeriesChartWidgetConfigForm.get('tooltipDateFormat').value; |
|||
const processor = DateFormatProcessor.fromSettings(this.$injector, dateFormat); |
|||
processor.update(Date.now()); |
|||
return processor.formatted; |
|||
} |
|||
} |
|||
@ -0,0 +1,166 @@ |
|||
///
|
|||
/// 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 { LinearGradientObject } from 'zrender/lib/graphic/LinearGradient'; |
|||
import { Interval, IntervalMath } from '@shared/models/time/time.models'; |
|||
import { LabelFormatterCallback, SeriesLabelOption } from 'echarts/types/src/util/types'; |
|||
import { |
|||
TimeSeriesChartDataItem, |
|||
TimeSeriesChartNoAggregationBarWidthStrategy |
|||
} from '@home/components/widget/lib/chart/time-series-chart.models'; |
|||
import { CustomSeriesRenderItemParams } from 'echarts'; |
|||
import { CustomSeriesRenderItemAPI, CustomSeriesRenderItemReturn } from 'echarts/types/dist/shared'; |
|||
import { isNumeric } from '@core/utils'; |
|||
import * as echarts from 'echarts/core'; |
|||
|
|||
export interface BarVisualSettings { |
|||
color: string | LinearGradientObject; |
|||
borderColor: string; |
|||
borderWidth: number; |
|||
borderRadius: number; |
|||
} |
|||
|
|||
export interface BarRenderSharedContext { |
|||
timeInterval: Interval; |
|||
noAggregationBarWidthStrategy: TimeSeriesChartNoAggregationBarWidthStrategy; |
|||
noAggregationWidthRelative: boolean; |
|||
noAggregationWidth: number; |
|||
} |
|||
|
|||
export interface BarRenderContext { |
|||
shared: BarRenderSharedContext; |
|||
barsCount?: number; |
|||
barIndex?: number; |
|||
noAggregation?: boolean; |
|||
visualSettings?: BarVisualSettings; |
|||
labelOption?: SeriesLabelOption; |
|||
barStackIndex?: number; |
|||
currentStackItems?: TimeSeriesChartDataItem[]; |
|||
} |
|||
|
|||
export const renderTimeSeriesBar = (params: CustomSeriesRenderItemParams, api: CustomSeriesRenderItemAPI, |
|||
renderCtx: BarRenderContext): CustomSeriesRenderItemReturn => { |
|||
const time = api.value(0) as number; |
|||
let start = api.value(2) as number; |
|||
let end = api.value(3) as number; |
|||
let interval = end - start; |
|||
const ts = start ? start : time; |
|||
|
|||
const separateBar = renderCtx.noAggregation && |
|||
renderCtx.shared.noAggregationBarWidthStrategy === TimeSeriesChartNoAggregationBarWidthStrategy.separate; |
|||
|
|||
if (renderCtx.noAggregation) { |
|||
if (renderCtx.shared.noAggregationWidthRelative) { |
|||
const scaleWidth = api.getWidth() / api.size([1,0])[0]; |
|||
interval = scaleWidth * (renderCtx.shared.noAggregationWidth / 100); |
|||
} else { |
|||
interval = renderCtx.shared.noAggregationWidth; |
|||
} |
|||
start = time - interval / 2; |
|||
end = time + interval / 2; |
|||
} |
|||
if (!start || !end || !interval) { |
|||
interval = IntervalMath.numberValue(renderCtx.shared.timeInterval); |
|||
start = time - interval / 2; |
|||
} |
|||
|
|||
const gap = 0.3; |
|||
const barInterval = separateBar ? interval : interval / (renderCtx.barsCount + gap * (renderCtx.barsCount + 3)); |
|||
const intervalGap = barInterval * gap * 2; |
|||
const barGap = barInterval * gap; |
|||
const value = api.value(1); |
|||
const startTime = separateBar ? start : start + intervalGap + (barInterval + barGap) * renderCtx.barIndex; |
|||
const delta = barInterval; |
|||
let offset = 0; |
|||
if (renderCtx.currentStackItems?.length) { |
|||
for (let i = 0; i < renderCtx.barStackIndex; i++) { |
|||
const stackItem = renderCtx.currentStackItems[i]; |
|||
const dataName = ts + ''; |
|||
const data = stackItem.data.find(d => d.name === dataName); |
|||
if (data) { |
|||
const val = data.value[1]; |
|||
if (isNumeric(val)) { |
|||
offset += Number(val); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
let lowerLeft: number[]; |
|||
if (offset !== 0 && isNumeric(value)) { |
|||
lowerLeft = api.coord([startTime, value >= 0 ? Number(value) + offset : offset]); |
|||
} else { |
|||
lowerLeft = api.coord([startTime, value >= 0 ? value : 0]); |
|||
} |
|||
const size = api.size([delta, value]); |
|||
const width = size[0]; |
|||
const height = size[1]; |
|||
|
|||
const coordSys: {x: number; y: number; width: number; height: number} = params.coordSys as any; |
|||
|
|||
const rectShape = echarts.graphic.clipRectByRect({ |
|||
x: lowerLeft[0], |
|||
y: lowerLeft[1], |
|||
width, |
|||
height |
|||
}, { |
|||
x: coordSys.x, |
|||
y: coordSys.y, |
|||
width: coordSys.width, |
|||
height: coordSys.height |
|||
}); |
|||
|
|||
const zeroPos = api.coord([0, offset]); |
|||
|
|||
const style: any = { |
|||
fill: renderCtx.visualSettings.color, |
|||
stroke: renderCtx.visualSettings.borderColor, |
|||
lineWidth: renderCtx.visualSettings.borderWidth |
|||
}; |
|||
|
|||
if (renderCtx.labelOption.show) { |
|||
let position = renderCtx.labelOption.position; |
|||
if (value < 0) { |
|||
if (position === 'top') { |
|||
position = 'bottom'; |
|||
} else if (position === 'bottom') { |
|||
position = 'top'; |
|||
} |
|||
} |
|||
style.text = (renderCtx.labelOption.formatter as LabelFormatterCallback)({value: [null, value]} as any); |
|||
style.textDistance = 5; |
|||
style.textPosition = position; |
|||
style.rich = renderCtx.labelOption.rich; |
|||
} |
|||
|
|||
let borderRadius: number[]; |
|||
if (value < 0) { |
|||
borderRadius = [0, 0, renderCtx.visualSettings.borderRadius, renderCtx.visualSettings.borderRadius]; |
|||
} else { |
|||
borderRadius = [renderCtx.visualSettings.borderRadius, renderCtx.visualSettings.borderRadius, 0, 0]; |
|||
} |
|||
return rectShape && { |
|||
type: 'rect', |
|||
id: time + '', |
|||
shape: {...rectShape, r: borderRadius}, |
|||
style, |
|||
focus: 'series', |
|||
transition: 'all', |
|||
enterFrom: { |
|||
style: { opacity: 0 }, |
|||
shape: { height: 0, y: zeroPos[1] } |
|||
} |
|||
}; |
|||
}; |
|||
@ -0,0 +1,123 @@ |
|||
<!-- |
|||
|
|||
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-time-series-chart-panel" [style.padding]="padding" |
|||
[class.overlay]="overlayEnabled" [style]="backgroundStyle$ | async"> |
|||
<div class="tb-time-series-chart-overlay" [style]="overlayStyle"></div> |
|||
<ng-container *ngTemplateOutlet="widgetTitlePanel"></ng-container> |
|||
<div class="tb-time-series-chart-content" [class]="legendClass"> |
|||
<div #chartShape class="tb-time-series-chart-shape"> |
|||
</div> |
|||
<div *ngIf="showLegend" class="tb-time-series-chart-legend" [class.tb-simple-legend]="!displayLegendValues"> |
|||
<ng-container *ngIf="displayLegendValues; else simpleLegend"> |
|||
<ng-container *ngIf="horizontalLegendPosition; else verticalLegend"> |
|||
<ng-container *ngTemplateOutlet="horizontalLegend"></ng-container> |
|||
</ng-container> |
|||
</ng-container> |
|||
</div> |
|||
<ng-template #horizontalLegend> |
|||
<table class="tb-time-series-chart-legend-table"> |
|||
<thead> |
|||
<tr> |
|||
<th></th> |
|||
<th *ngFor="let legendKey of legendKeys"> |
|||
<ng-container *ngTemplateOutlet="legendItem; context:{legendKey: legendKey}"></ng-container> |
|||
</th> |
|||
</tr> |
|||
</thead> |
|||
<tbody> |
|||
<ng-container *ngIf="legendConfig.showMin === true"> |
|||
<ng-container *ngTemplateOutlet="legendDataRow; context:{label: 'legend.Min', type: 'min'}"></ng-container> |
|||
</ng-container> |
|||
<ng-container *ngIf="legendConfig.showMax === true"> |
|||
<ng-container *ngTemplateOutlet="legendDataRow; context:{label: 'legend.Max', type: 'max'}"></ng-container> |
|||
</ng-container> |
|||
<ng-container *ngIf="legendConfig.showAvg === true"> |
|||
<ng-container *ngTemplateOutlet="legendDataRow; context:{label: 'legend.Avg', type: 'avg'}"></ng-container> |
|||
</ng-container> |
|||
<ng-container *ngIf="legendConfig.showTotal === true"> |
|||
<ng-container *ngTemplateOutlet="legendDataRow; context:{label: 'legend.Total', type: 'total'}"></ng-container> |
|||
</ng-container> |
|||
<ng-container *ngIf="legendConfig.showLatest === true"> |
|||
<ng-container *ngTemplateOutlet="legendDataRow; context:{label: 'legend.Latest', type: 'latest'}"></ng-container> |
|||
</ng-container> |
|||
</tbody> |
|||
</table> |
|||
</ng-template> |
|||
<ng-template #verticalLegend> |
|||
<table class="tb-time-series-chart-legend-table vertical"> |
|||
<thead> |
|||
<tr> |
|||
<th></th> |
|||
<th class="tb-time-series-chart-legend-type-label right" *ngIf="legendConfig.showMin === true">{{ 'legend.Min' | translate }}</th> |
|||
<th class="tb-time-series-chart-legend-type-label right" *ngIf="legendConfig.showMax === true">{{ 'legend.Max' | translate }}</th> |
|||
<th class="tb-time-series-chart-legend-type-label right" *ngIf="legendConfig.showAvg === true">{{ 'legend.Avg' | translate }}</th> |
|||
<th class="tb-time-series-chart-legend-type-label right" *ngIf="legendConfig.showTotal === true">{{ 'legend.Total' | translate }}</th> |
|||
<th class="tb-time-series-chart-legend-type-label right" *ngIf="legendConfig.showLatest === true">{{ 'legend.Latest' | translate }}</th> |
|||
</tr> |
|||
</thead> |
|||
<tbody> |
|||
<tr *ngFor="let legendKey of legendKeys"> |
|||
<th> |
|||
<ng-container *ngTemplateOutlet="legendItem; context:{legendKey: legendKey, left: true}"></ng-container> |
|||
</th> |
|||
<td *ngIf="legendConfig.showMin === true" class="tb-time-series-chart-legend-value"> |
|||
{{ legendData.data[legendKey.dataIndex].min }} |
|||
</td> |
|||
<td *ngIf="legendConfig.showMax === true" class="tb-time-series-chart-legend-value"> |
|||
{{ legendData.data[legendKey.dataIndex].max }} |
|||
</td> |
|||
<td *ngIf="legendConfig.showAvg === true" class="tb-time-series-chart-legend-value"> |
|||
{{ legendData.data[legendKey.dataIndex].avg }} |
|||
</td> |
|||
<td *ngIf="legendConfig.showTotal === true" class="tb-time-series-chart-legend-value"> |
|||
{{ legendData.data[legendKey.dataIndex].total }} |
|||
</td> |
|||
<td *ngIf="legendConfig.showLatest === true" class="tb-time-series-chart-legend-value"> |
|||
{{ legendData.data[legendKey.dataIndex].latest }} |
|||
</td> |
|||
</tr> |
|||
</tbody> |
|||
</table> |
|||
</ng-template> |
|||
<ng-template #simpleLegend> |
|||
<ng-container *ngFor="let legendKey of legendKeys"> |
|||
<ng-container *ngTemplateOutlet="legendItem; context:{legendKey: legendKey}"></ng-container> |
|||
</ng-container> |
|||
</ng-template> |
|||
<ng-template #legendItem let-legendKey="legendKey" let-left="left"> |
|||
<div class="tb-time-series-chart-legend-item" |
|||
[class.left]="left"> |
|||
<div class="tb-time-series-chart-legend-item-label" |
|||
(mouseenter)="onLegendKeyEnter(legendKey)" |
|||
(mouseleave)="onLegendKeyLeave(legendKey)" |
|||
(click)="toggleLegendKey(legendKey)"> |
|||
<div class="tb-time-series-chart-legend-item-label-circle" [style]="{background: !legendKey.dataKey.hidden ? legendKey.dataKey.color : null}"></div> |
|||
<div [style]="!legendKey.dataKey.hidden ? legendLabelStyle : disabledLegendLabelStyle">{{ legendKey.dataKey.label }}</div> |
|||
</div> |
|||
</div> |
|||
</ng-template> |
|||
<ng-template #legendDataRow let-label="label" let-type="type"> |
|||
<tr> |
|||
<th class="tb-time-series-chart-legend-type-label">{{ label | translate }}</th> |
|||
<td *ngFor="let legendKey of legendKeys" class="tb-time-series-chart-legend-value"> |
|||
{{ legendData.data[legendKey.dataIndex][type] }} |
|||
</td> |
|||
</tr> |
|||
</ng-template> |
|||
</div> |
|||
</div> |
|||
@ -0,0 +1,172 @@ |
|||
/** |
|||
* 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. |
|||
*/ |
|||
|
|||
$maxLegendWidth: 25%; |
|||
$maxLegendHeight: 35%; |
|||
|
|||
.tb-time-series-chart-panel { |
|||
width: 100%; |
|||
height: 100%; |
|||
position: relative; |
|||
display: flex; |
|||
flex-direction: column; |
|||
gap: 8px; |
|||
&.overlay { |
|||
padding: 20px 24px 24px 24px; |
|||
} |
|||
> div:not(.tb-time-series-chart-overlay) { |
|||
z-index: 1; |
|||
} |
|||
.tb-time-series-chart-overlay { |
|||
position: absolute; |
|||
top: 12px; |
|||
left: 12px; |
|||
bottom: 12px; |
|||
right: 12px; |
|||
} |
|||
div.tb-widget-title { |
|||
padding: 0; |
|||
} |
|||
.tb-time-series-chart-content { |
|||
flex: 1; |
|||
min-width: 0; |
|||
min-height: 0; |
|||
display: flex; |
|||
flex-direction: column; |
|||
gap: 8px; |
|||
&.legend-top { |
|||
flex-direction: column-reverse; |
|||
} |
|||
&.legend-right { |
|||
flex-direction: row; |
|||
} |
|||
&.legend-left { |
|||
flex-direction: row-reverse; |
|||
} |
|||
.tb-time-series-chart-shape { |
|||
flex: 1; |
|||
min-width: 0; |
|||
min-height: 0; |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
} |
|||
&.legend-right, &.legend-left { |
|||
.tb-time-series-chart-legend { |
|||
display: inline-grid; |
|||
grid-auto-flow: column; |
|||
grid-template-rows: repeat(auto-fit, minmax(16px, min-content)); |
|||
max-width: calc($maxLegendWidth - 8px); |
|||
height: fit-content; |
|||
max-height: 100%; |
|||
} |
|||
} |
|||
&.legend-top, &.legend-bottom { |
|||
.tb-time-series-chart-legend { |
|||
align-self: center; |
|||
&.tb-simple-legend { |
|||
justify-content: center; |
|||
} |
|||
&:not(.tb-simple-legend) { |
|||
width: 100%; |
|||
} |
|||
} |
|||
} |
|||
.tb-time-series-chart-legend { |
|||
display: flex; |
|||
align-items: flex-start; |
|||
align-self: stretch; |
|||
column-gap: 16px; |
|||
row-gap: 8px; |
|||
flex-wrap: wrap; |
|||
overflow: auto; |
|||
width: fit-content; |
|||
max-width: 100%; |
|||
max-height: calc($maxLegendHeight - 8px); |
|||
.tb-time-series-chart-legend-table { |
|||
border-spacing: 0; |
|||
table-layout: fixed; |
|||
&.vertical { |
|||
width: 100%; |
|||
table-layout: auto; |
|||
tbody { |
|||
th { |
|||
width: 95%; |
|||
} |
|||
} |
|||
} |
|||
th, td { |
|||
&:not(:last-child) { |
|||
padding-right: 16px; |
|||
} |
|||
} |
|||
thead tr, tbody tr:not(:last-child) { |
|||
th, td { |
|||
padding-bottom: 8px; |
|||
} |
|||
} |
|||
.tb-time-series-chart-legend-item { |
|||
align-items: flex-end; |
|||
&.left { |
|||
align-items: flex-start; |
|||
} |
|||
} |
|||
} |
|||
.tb-time-series-chart-legend-item { |
|||
display: flex; |
|||
flex-direction: column; |
|||
justify-content: center; |
|||
align-items: flex-start; |
|||
user-select: none; |
|||
.tb-time-series-chart-legend-item-label { |
|||
display: flex; |
|||
align-items: center; |
|||
gap: 4px; |
|||
color: #ccc; |
|||
white-space: nowrap; |
|||
cursor: pointer; |
|||
.tb-time-series-chart-legend-item-label-circle { |
|||
width: 8px; |
|||
height: 8px; |
|||
border-radius: 50%; |
|||
background-color: #ccc; |
|||
} |
|||
} |
|||
} |
|||
.tb-time-series-chart-legend-type-label { |
|||
font-size: 12px; |
|||
font-style: normal; |
|||
font-weight: 400; |
|||
line-height: 16px; |
|||
color: rgba(0, 0, 0, 0.38); |
|||
white-space: nowrap; |
|||
text-align: left; |
|||
&.right { |
|||
text-align: right; |
|||
} |
|||
} |
|||
.tb-time-series-chart-legend-value { |
|||
font-size: 12px; |
|||
font-style: normal; |
|||
font-weight: 500; |
|||
line-height: 16px; |
|||
color: rgba(0, 0, 0, 0.87); |
|||
white-space: nowrap; |
|||
text-align: right; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,167 @@ |
|||
///
|
|||
/// 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 { |
|||
AfterViewInit, |
|||
ChangeDetectorRef, |
|||
Component, |
|||
ElementRef, |
|||
Input, |
|||
OnDestroy, |
|||
OnInit, |
|||
Renderer2, |
|||
TemplateRef, |
|||
ViewChild, |
|||
ViewEncapsulation |
|||
} from '@angular/core'; |
|||
import { timeSeriesChartKeyDefaultSettings, TimeSeriesChartKeySettings } from '@home/components/widget/lib/chart/time-series-chart.models'; |
|||
import { WidgetContext } from '@home/models/widget-component.models'; |
|||
import { Observable } from 'rxjs'; |
|||
import { backgroundStyle, ComponentStyle, overlayStyle, textStyle } from '@shared/models/widget-settings.models'; |
|||
import { ImagePipe } from '@shared/pipe/image.pipe'; |
|||
import { DomSanitizer } from '@angular/platform-browser'; |
|||
import { LegendConfig, LegendData, LegendKey, LegendPosition } from '@shared/models/widget.models'; |
|||
import { TbTimeSeriesChart } from '@home/components/widget/lib/chart/time-series-chart'; |
|||
import { |
|||
timeSeriesChartWidgetDefaultSettings, |
|||
TimeSeriesChartWidgetSettings |
|||
} from '@home/components/widget/lib/chart/time-series-chart-widget.models'; |
|||
import { mergeDeep } from '@core/utils'; |
|||
|
|||
@Component({ |
|||
selector: 'tb-time-series-chart-widget', |
|||
templateUrl: './time-series-chart-widget.component.html', |
|||
styleUrls: ['./time-series-chart-widget.component.scss'], |
|||
encapsulation: ViewEncapsulation.None |
|||
}) |
|||
export class TimeSeriesChartWidgetComponent implements OnInit, OnDestroy, AfterViewInit { |
|||
|
|||
@ViewChild('chartShape', {static: false}) |
|||
chartShape: ElementRef<HTMLElement>; |
|||
|
|||
settings: TimeSeriesChartWidgetSettings; |
|||
|
|||
@Input() |
|||
ctx: WidgetContext; |
|||
|
|||
@Input() |
|||
widgetTitlePanel: TemplateRef<any>; |
|||
|
|||
horizontalLegendPosition = false; |
|||
|
|||
showLegend: boolean; |
|||
legendClass: string; |
|||
legendConfig: LegendConfig; |
|||
legendData: LegendData; |
|||
legendKeys: LegendKey[]; |
|||
|
|||
backgroundStyle$: Observable<ComponentStyle>; |
|||
overlayStyle: ComponentStyle = {}; |
|||
overlayEnabled: boolean; |
|||
padding: string; |
|||
|
|||
legendLabelStyle: ComponentStyle; |
|||
disabledLegendLabelStyle: ComponentStyle; |
|||
|
|||
displayLegendValues = false; |
|||
|
|||
private timeSeriesChart: TbTimeSeriesChart; |
|||
|
|||
constructor(private imagePipe: ImagePipe, |
|||
private sanitizer: DomSanitizer, |
|||
private renderer: Renderer2, |
|||
private cd: ChangeDetectorRef) { |
|||
} |
|||
|
|||
ngOnInit(): void { |
|||
this.ctx.$scope.timeSeriesChartWidget = this; |
|||
this.settings = {...timeSeriesChartWidgetDefaultSettings, ...this.ctx.settings}; |
|||
|
|||
this.backgroundStyle$ = backgroundStyle(this.settings.background, this.imagePipe, this.sanitizer); |
|||
this.overlayStyle = overlayStyle(this.settings.background.overlay); |
|||
this.overlayEnabled = this.settings.background.overlay.enabled; |
|||
this.padding = this.overlayEnabled ? undefined : this.settings.padding; |
|||
|
|||
this.showLegend = this.settings.showLegend; |
|||
if (this.showLegend) { |
|||
this.legendData = this.ctx.defaultSubscription.legendData; |
|||
this.legendConfig = this.settings.legendConfig; |
|||
this.legendKeys = this.legendData.keys; |
|||
if (this.legendConfig.sortDataKeys) { |
|||
this.legendKeys = this.legendData.keys.sort((key1, key2) => key1.dataKey.label.localeCompare(key2.dataKey.label)); |
|||
} |
|||
this.legendKeys.forEach(legendKey => { |
|||
legendKey.dataKey.settings = mergeDeep<TimeSeriesChartKeySettings>({} as TimeSeriesChartKeySettings, |
|||
timeSeriesChartKeyDefaultSettings, legendKey.dataKey.settings); |
|||
legendKey.dataKey.hidden = legendKey.dataKey.settings.dataHiddenByDefault; |
|||
}); |
|||
this.legendKeys = this.legendKeys.filter(legendKey => legendKey.dataKey.settings.showInLegend); |
|||
if (!this.legendKeys.length) { |
|||
this.showLegend = false; |
|||
} |
|||
} |
|||
|
|||
if (this.showLegend) { |
|||
this.horizontalLegendPosition = [LegendPosition.left, LegendPosition.right].includes(this.legendConfig.position); |
|||
this.legendClass = `legend-${this.legendConfig.position}`; |
|||
this.legendLabelStyle = textStyle(this.settings.legendLabelFont); |
|||
this.disabledLegendLabelStyle = textStyle(this.settings.legendLabelFont); |
|||
this.legendLabelStyle.color = this.settings.legendLabelColor; |
|||
this.displayLegendValues = this.legendConfig.showMin || this.legendConfig.showMax || |
|||
this.legendConfig.showAvg || this.legendConfig.showTotal || this.legendConfig.showLatest; |
|||
} |
|||
} |
|||
|
|||
ngAfterViewInit() { |
|||
this.timeSeriesChart = new TbTimeSeriesChart(this.ctx, this.settings, this.chartShape.nativeElement, this.renderer); |
|||
} |
|||
|
|||
ngOnDestroy() { |
|||
if (this.timeSeriesChart) { |
|||
this.timeSeriesChart.destroy(); |
|||
} |
|||
} |
|||
|
|||
public onInit() { |
|||
const borderRadius = this.ctx.$widgetElement.css('borderRadius'); |
|||
this.overlayStyle = {...this.overlayStyle, ...{borderRadius}}; |
|||
this.cd.detectChanges(); |
|||
} |
|||
|
|||
public onDataUpdated() { |
|||
if (this.timeSeriesChart) { |
|||
this.timeSeriesChart.update(); |
|||
} |
|||
} |
|||
|
|||
public onLatestDataUpdated() { |
|||
if (this.timeSeriesChart) { |
|||
this.timeSeriesChart.latestUpdated(); |
|||
} |
|||
} |
|||
|
|||
public onLegendKeyEnter(legendKey: LegendKey) { |
|||
this.timeSeriesChart.keyEnter(legendKey.dataKey); |
|||
} |
|||
|
|||
public onLegendKeyLeave(legendKey: LegendKey) { |
|||
this.timeSeriesChart.keyLeave(legendKey.dataKey); |
|||
} |
|||
|
|||
public toggleLegendKey(legendKey: LegendKey) { |
|||
this.timeSeriesChart.toggleKey(legendKey.dataKey); |
|||
} |
|||
} |
|||
@ -0,0 +1,54 @@ |
|||
///
|
|||
/// 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 { timeSeriesChartDefaultSettings, TimeSeriesChartSettings } from '@home/components/widget/lib/chart/time-series-chart.models'; |
|||
import { BackgroundSettings, BackgroundType, Font } from '@shared/models/widget-settings.models'; |
|||
import { defaultLegendConfig, LegendConfig, LegendPosition, widgetType } from '@shared/models/widget.models'; |
|||
import { mergeDeep } from '@core/utils'; |
|||
|
|||
export interface TimeSeriesChartWidgetSettings extends TimeSeriesChartSettings { |
|||
showLegend: boolean; |
|||
legendLabelFont: Font; |
|||
legendLabelColor: string; |
|||
legendConfig: LegendConfig; |
|||
background: BackgroundSettings; |
|||
padding: string; |
|||
} |
|||
|
|||
export const timeSeriesChartWidgetDefaultSettings: TimeSeriesChartWidgetSettings = |
|||
mergeDeep({} as TimeSeriesChartWidgetSettings, timeSeriesChartDefaultSettings as TimeSeriesChartWidgetSettings, { |
|||
showLegend: true, |
|||
legendLabelFont: { |
|||
family: 'Roboto', |
|||
size: 12, |
|||
sizeUnit: 'px', |
|||
style: 'normal', |
|||
weight: '400', |
|||
lineHeight: '16px' |
|||
}, |
|||
legendLabelColor: 'rgba(0, 0, 0, 0.76)', |
|||
legendConfig: {...defaultLegendConfig(widgetType.timeseries), position: LegendPosition.top}, |
|||
background: { |
|||
type: BackgroundType.color, |
|||
color: '#fff', |
|||
overlay: { |
|||
enabled: false, |
|||
color: 'rgba(255,255,255,0.72)', |
|||
blur: 3 |
|||
} |
|||
}, |
|||
padding: '12px' |
|||
} as TimeSeriesChartWidgetSettings); |
|||
File diff suppressed because it is too large
@ -0,0 +1,742 @@ |
|||
///
|
|||
/// 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 { WidgetContext } from '@home/models/widget-component.models'; |
|||
import { |
|||
AxisPosition, |
|||
calculateThresholdsOffset, |
|||
createTimeSeriesXAxisOption, |
|||
createTimeSeriesYAxis, |
|||
defaultTimeSeriesChartYAxisSettings, |
|||
generateChartData, |
|||
parseThresholdData, |
|||
SeriesLabelPosition, |
|||
TimeSeriesChartDataItem, |
|||
timeSeriesChartDefaultSettings, |
|||
timeSeriesChartKeyDefaultSettings, |
|||
TimeSeriesChartKeySettings, |
|||
TimeSeriesChartNoAggregationBarWidthStrategy, |
|||
TimeSeriesChartSeriesType, |
|||
TimeSeriesChartSettings, |
|||
TimeSeriesChartShape, |
|||
TimeSeriesChartThreshold, |
|||
timeSeriesChartThresholdDefaultSettings, |
|||
TimeSeriesChartThresholdItem, |
|||
TimeSeriesChartThresholdType, |
|||
TimeSeriesChartType, |
|||
TimeSeriesChartYAxis, |
|||
TimeSeriesChartYAxisId, |
|||
TimeSeriesChartYAxisSettings, |
|||
updateDarkMode |
|||
} from '@home/components/widget/lib/chart/time-series-chart.models'; |
|||
import { ResizeObserver } from '@juggle/resize-observer'; |
|||
import { |
|||
calculateXAxisHeight, |
|||
calculateYAxisWidth, |
|||
ECharts, |
|||
echartsModule, |
|||
EChartsOption, |
|||
echartsTooltipFormatter, |
|||
EChartsTooltipTrigger, |
|||
getAxisExtent, |
|||
measureXAxisNameHeight, |
|||
measureYAxisNameWidth, |
|||
toNamedData |
|||
} from '@home/components/widget/lib/chart/echarts-widget.models'; |
|||
import { DateFormatProcessor } from '@shared/models/widget-settings.models'; |
|||
import { isDefinedAndNotNull, isEqual, mergeDeep } from '@core/utils'; |
|||
import { DataKey, Datasource, DatasourceType, widgetType } from '@shared/models/widget.models'; |
|||
import * as echarts from 'echarts/core'; |
|||
import { CallbackDataParams } from 'echarts/types/dist/shared'; |
|||
import { Renderer2 } from '@angular/core'; |
|||
import { CustomSeriesOption, LineSeriesOption } from 'echarts/charts'; |
|||
import { BehaviorSubject } from 'rxjs'; |
|||
import { AggregationType } from '@shared/models/time/time.models'; |
|||
import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; |
|||
import { WidgetSubscriptionOptions } from '@core/api/widget-api.models'; |
|||
import { DataKeySettingsFunction } from '@home/components/widget/config/data-keys.component.models'; |
|||
import { DeepPartial } from '@shared/models/common'; |
|||
import { BarRenderSharedContext } from '@home/components/widget/lib/chart/time-series-chart-bar.models'; |
|||
|
|||
export class TbTimeSeriesChart { |
|||
|
|||
public static dataKeySettings(type = TimeSeriesChartType.default): DataKeySettingsFunction { |
|||
return (_key, isLatestDataKey) => { |
|||
if (!isLatestDataKey) { |
|||
const settings = mergeDeep<TimeSeriesChartKeySettings>({} as TimeSeriesChartKeySettings, |
|||
timeSeriesChartKeyDefaultSettings); |
|||
if (type === TimeSeriesChartType.line) { |
|||
settings.type = TimeSeriesChartSeriesType.line; |
|||
} else if (type === TimeSeriesChartType.bar) { |
|||
settings.type = TimeSeriesChartSeriesType.bar; |
|||
} else if (type === TimeSeriesChartType.point) { |
|||
settings.type = TimeSeriesChartSeriesType.line; |
|||
settings.lineSettings.showLine = false; |
|||
settings.lineSettings.showPoints = true; |
|||
settings.lineSettings.pointShape = TimeSeriesChartShape.circle; |
|||
settings.lineSettings.pointSize = 8; |
|||
} |
|||
return settings; |
|||
} |
|||
return null; |
|||
}; |
|||
} |
|||
|
|||
private get noAggregation(): boolean { |
|||
return this.ctx.defaultSubscription.timeWindowConfig?.aggregation?.type === AggregationType.NONE; |
|||
} |
|||
|
|||
private readonly shapeResize$: ResizeObserver; |
|||
|
|||
private readonly settings: TimeSeriesChartSettings; |
|||
|
|||
private yAxisList: TimeSeriesChartYAxis[] = []; |
|||
private dataItems: TimeSeriesChartDataItem[] = []; |
|||
private thresholdItems: TimeSeriesChartThresholdItem[] = []; |
|||
|
|||
private timeSeriesChart: ECharts; |
|||
private timeSeriesChartOptions: EChartsOption; |
|||
|
|||
private readonly tooltipDateFormat: DateFormatProcessor; |
|||
|
|||
private yMinSubject = new BehaviorSubject(-1); |
|||
private yMaxSubject = new BehaviorSubject(1); |
|||
|
|||
private darkMode = false; |
|||
|
|||
private darkModeObserver: MutationObserver; |
|||
|
|||
private topPointLabels = false; |
|||
|
|||
private componentIndexCounter = 0; |
|||
|
|||
private highlightedDataKey: DataKey; |
|||
|
|||
private barRenderSharedContext: BarRenderSharedContext; |
|||
|
|||
yMin$ = this.yMinSubject.asObservable(); |
|||
yMax$ = this.yMaxSubject.asObservable(); |
|||
|
|||
constructor(private ctx: WidgetContext, |
|||
private readonly inputSettings: DeepPartial<TimeSeriesChartSettings>, |
|||
private chartElement: HTMLElement, |
|||
private renderer: Renderer2, |
|||
private autoResize = true) { |
|||
|
|||
this.settings = mergeDeep({} as TimeSeriesChartSettings, |
|||
timeSeriesChartDefaultSettings, |
|||
this.inputSettings as TimeSeriesChartSettings); |
|||
const dashboardPageElement = this.ctx.$containerParent.parents('.tb-dashboard-page'); |
|||
this.darkMode = this.settings.darkMode || dashboardPageElement.hasClass('dark'); |
|||
this.setupYAxes(); |
|||
this.setupData(); |
|||
this.setupThresholds(); |
|||
if (this.settings.showTooltip && this.settings.tooltipShowDate) { |
|||
this.tooltipDateFormat = DateFormatProcessor.fromSettings(this.ctx.$injector, this.settings.tooltipDateFormat); |
|||
} |
|||
this.onResize(); |
|||
if (this.autoResize) { |
|||
this.shapeResize$ = new ResizeObserver(() => { |
|||
this.onResize(); |
|||
}); |
|||
this.shapeResize$.observe(this.chartElement); |
|||
} |
|||
this.darkModeObserver = new MutationObserver(mutations => { |
|||
for(let mutation of mutations) { |
|||
if (mutation.type === 'attributes' && mutation.attributeName === 'class') { |
|||
const darkMode = dashboardPageElement.hasClass('dark'); |
|||
this.setDarkMode(darkMode); |
|||
} |
|||
} |
|||
}); |
|||
this.darkModeObserver.observe(dashboardPageElement[0], { attributes: true }); |
|||
} |
|||
|
|||
public update(): void { |
|||
for (const item of this.dataItems) { |
|||
const datasourceData = this.ctx.data ? this.ctx.data.find(d => d.dataKey === item.dataKey) : null; |
|||
if (!isEqual(item.dataSet, datasourceData?.data)) { |
|||
item.dataSet = datasourceData?.data; |
|||
item.data = datasourceData?.data ? toNamedData(datasourceData.data) : []; |
|||
} |
|||
} |
|||
this.onResize(); |
|||
if (this.timeSeriesChart) { |
|||
this.timeSeriesChartOptions.xAxis[0].min = this.ctx.defaultSubscription.timeWindow.minTime; |
|||
this.timeSeriesChartOptions.xAxis[0].max = this.ctx.defaultSubscription.timeWindow.maxTime; |
|||
this.timeSeriesChartOptions.xAxis[0].tbTimeWindow = this.ctx.defaultSubscription.timeWindow; |
|||
if (this.noAggregation) { |
|||
this.timeSeriesChartOptions.tooltip[0].axisPointer.type = 'line'; |
|||
} else { |
|||
this.timeSeriesChartOptions.tooltip[0].axisPointer.type = 'shadow'; |
|||
} |
|||
this.barRenderSharedContext.timeInterval = this.ctx.timeWindow.interval; |
|||
this.updateSeriesData(true); |
|||
if (this.highlightedDataKey) { |
|||
this.keyEnter(this.highlightedDataKey); |
|||
} |
|||
} |
|||
} |
|||
|
|||
public latestUpdated() { |
|||
let update = false; |
|||
if (this.ctx.latestData) { |
|||
for (const item of this.thresholdItems) { |
|||
if (item.settings.type === TimeSeriesChartThresholdType.latestKey && item.latestDataKey) { |
|||
const data = this.ctx.latestData.find(d => d.dataKey === item.latestDataKey); |
|||
if (data.data[0]) { |
|||
item.value = parseThresholdData(data.data[0][1]); |
|||
update = true; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
if (this.timeSeriesChart && update) { |
|||
this.updateSeriesData(); |
|||
} |
|||
} |
|||
|
|||
public keyEnter(dataKey: DataKey): void { |
|||
this.highlightedDataKey = dataKey; |
|||
const item = this.dataItems.find(d => d.dataKey === dataKey); |
|||
if (item) { |
|||
this.timeSeriesChart.dispatchAction({ |
|||
type: 'highlight', |
|||
seriesId: item.id |
|||
}); |
|||
} |
|||
} |
|||
|
|||
public keyLeave(dataKey: DataKey): void { |
|||
this.highlightedDataKey = null; |
|||
const item = this.dataItems.find(d => d.dataKey === dataKey); |
|||
if (item) { |
|||
this.timeSeriesChart.dispatchAction({ |
|||
type: 'downplay', |
|||
seriesId: item.id |
|||
}); |
|||
} |
|||
} |
|||
|
|||
public toggleKey(dataKey: DataKey): void { |
|||
const enable = dataKey.hidden; |
|||
const dataItem = this.dataItems.find(d => d.dataKey === dataKey); |
|||
if (dataItem) { |
|||
dataItem.enabled = enable; |
|||
if (!enable) { |
|||
this.timeSeriesChart.dispatchAction({ |
|||
type: 'downplay', |
|||
seriesId: dataItem.id |
|||
}); |
|||
} |
|||
this.timeSeriesChartOptions.series = this.updateSeries(); |
|||
const mergeList = ['series']; |
|||
if (this.updateYAxisScale(this.yAxisList)) { |
|||
this.timeSeriesChartOptions.yAxis = this.yAxisList.map(axis => axis.option); |
|||
mergeList.push('yAxis'); |
|||
} |
|||
this.timeSeriesChart.setOption(this.timeSeriesChartOptions, this.settings.stack ? {notMerge: true} : {replaceMerge: mergeList}); |
|||
this.updateAxes(); |
|||
dataKey.hidden = !enable; |
|||
if (enable) { |
|||
this.timeSeriesChart.dispatchAction({ |
|||
type: 'highlight', |
|||
seriesId: dataItem.id |
|||
}); |
|||
} |
|||
} |
|||
} |
|||
|
|||
public destroy(): void { |
|||
if (this.shapeResize$) { |
|||
this.shapeResize$.disconnect(); |
|||
} |
|||
if (this.timeSeriesChart) { |
|||
this.timeSeriesChart.dispose(); |
|||
} |
|||
this.yMinSubject.complete(); |
|||
this.yMaxSubject.complete(); |
|||
this.darkModeObserver.disconnect(); |
|||
} |
|||
|
|||
public resize(): void { |
|||
this.onResize(); |
|||
} |
|||
|
|||
public setDarkMode(darkMode: boolean): void { |
|||
if (this.darkMode !== darkMode) { |
|||
this.darkMode = darkMode; |
|||
if (this.timeSeriesChart) { |
|||
this.timeSeriesChartOptions = updateDarkMode(this.timeSeriesChartOptions, |
|||
this.settings, this.yAxisList, this.dataItems, |
|||
this.thresholdItems, darkMode); |
|||
this.timeSeriesChart.setOption(this.timeSeriesChartOptions); |
|||
} |
|||
} |
|||
} |
|||
|
|||
public isDarkMode(): boolean { |
|||
return this.darkMode; |
|||
} |
|||
|
|||
private setupData(): void { |
|||
const noAggregationBarWidthSettings = this.settings.noAggregationBarWidthSettings; |
|||
const targetBarWidth = noAggregationBarWidthSettings.strategy === TimeSeriesChartNoAggregationBarWidthStrategy.group ? |
|||
noAggregationBarWidthSettings.groupWidth : noAggregationBarWidthSettings.barWidth; |
|||
this.barRenderSharedContext = { |
|||
timeInterval: this.ctx.timeWindow?.interval, |
|||
noAggregationBarWidthStrategy: noAggregationBarWidthSettings.strategy, |
|||
noAggregationWidthRelative: targetBarWidth.relative, |
|||
noAggregationWidth: targetBarWidth.relative ? targetBarWidth.relativeWidth : targetBarWidth.absoluteWidth |
|||
}; |
|||
if (this.ctx.datasources.length) { |
|||
for (const datasource of this.ctx.datasources) { |
|||
const dataKeys = datasource.dataKeys; |
|||
for (const dataKey of dataKeys) { |
|||
const keySettings = mergeDeep<TimeSeriesChartKeySettings>({} as TimeSeriesChartKeySettings, |
|||
timeSeriesChartKeyDefaultSettings, dataKey.settings); |
|||
if ((keySettings.type === TimeSeriesChartSeriesType.line && keySettings.lineSettings.showPointLabel && |
|||
keySettings.lineSettings.pointLabelPosition === SeriesLabelPosition.top) || |
|||
(keySettings.type === TimeSeriesChartSeriesType.bar && keySettings.barSettings.showLabel)) { |
|||
this.topPointLabels = true; |
|||
} |
|||
dataKey.settings = keySettings; |
|||
const datasourceData = this.ctx.data ? this.ctx.data.find(d => d.dataKey === dataKey) : null; |
|||
const namedData = datasourceData?.data ? toNamedData(datasourceData.data) : []; |
|||
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, |
|||
yAxisId, |
|||
yAxisIndex: this.getYAxisIndex(yAxisId), |
|||
dataKey, |
|||
data: namedData, |
|||
enabled: !keySettings.dataHiddenByDefault |
|||
}); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
private setupThresholds(): void { |
|||
const thresholdDatasources: Datasource[] = []; |
|||
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; |
|||
if (threshold.type === TimeSeriesChartThresholdType.latestKey) { |
|||
if (this.ctx.datasources.length) { |
|||
for (const datasource of this.ctx.datasources) { |
|||
latestDataKey = datasource.latestDataKeys?.find(d => |
|||
(d.type === DataKeyType.function && d.label === threshold.latestKey) || |
|||
(d.type !== DataKeyType.function && d.name === threshold.latestKey && |
|||
d.type === threshold.latestKeyType)); |
|||
if (latestDataKey) { |
|||
break; |
|||
} |
|||
} |
|||
} |
|||
if (!latestDataKey) { |
|||
continue; |
|||
} |
|||
} else if (threshold.type === TimeSeriesChartThresholdType.entity) { |
|||
const entityAliasId = this.ctx.aliasController.getEntityAliasId(threshold.entityAlias); |
|||
if (!entityAliasId) { |
|||
continue; |
|||
} |
|||
let datasource = thresholdDatasources.find(d => d.entityAliasId === entityAliasId); |
|||
entityDataKey = { |
|||
type: threshold.entityKeyType, |
|||
name: threshold.entityKey, |
|||
label: threshold.entityKey, |
|||
settings: {} |
|||
}; |
|||
if (datasource) { |
|||
datasource.dataKeys.push(entityDataKey); |
|||
} |
|||
datasource = { |
|||
type: DatasourceType.entity, |
|||
name: threshold.entityAlias, |
|||
aliasName: threshold.entityAlias, |
|||
entityAliasId, |
|||
dataKeys: [ entityDataKey ] |
|||
}; |
|||
thresholdDatasources.push(datasource); |
|||
} else { // constant
|
|||
value = threshold.value; |
|||
} |
|||
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, |
|||
yAxisId, |
|||
yAxisIndex: this.getYAxisIndex(yAxisId), |
|||
value, |
|||
latestDataKey, |
|||
settings: threshold |
|||
}; |
|||
if (entityDataKey) { |
|||
entityDataKey.settings.thresholdItemId = thresholdItem.id; |
|||
} |
|||
this.thresholdItems.push(thresholdItem); |
|||
} |
|||
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 getYAxisIndex(id: TimeSeriesChartYAxisId): number { |
|||
let yAxisIndex = this.yAxisList.findIndex(axis => axis.id === id); |
|||
if (yAxisIndex === -1) { |
|||
yAxisIndex = this.yAxisList.findIndex(axis => axis.id === 'default'); |
|||
if (yAxisIndex === -1 && this.yAxisList.length) { |
|||
yAxisIndex = 0; |
|||
} |
|||
} |
|||
return yAxisIndex; |
|||
} |
|||
|
|||
private subscribeForEntityThresholds(datasources: Datasource[]) { |
|||
if (datasources.length) { |
|||
const thresholdsSourcesSubscriptionOptions: WidgetSubscriptionOptions = { |
|||
datasources, |
|||
useDashboardTimewindow: false, |
|||
type: widgetType.latest, |
|||
callbacks: { |
|||
onDataUpdated: (subscription) => { |
|||
let update = false; |
|||
if (subscription.data) { |
|||
for (const item of this.thresholdItems) { |
|||
if (item.settings.type === TimeSeriesChartThresholdType.entity) { |
|||
const data = subscription.data.find(d => d.dataKey.settings?.thresholdItemId === item.id); |
|||
if (data.data[0]) { |
|||
item.value = parseThresholdData(data.data[0][1]); |
|||
update = true; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
if (this.timeSeriesChart && update) { |
|||
this.updateSeriesData(); |
|||
} |
|||
} |
|||
} |
|||
}; |
|||
this.ctx.subscriptionApi.createSubscription(thresholdsSourcesSubscriptionOptions, true).subscribe(); |
|||
} |
|||
} |
|||
|
|||
private drawChart() { |
|||
echartsModule.init(); |
|||
this.timeSeriesChart = echarts.init(this.chartElement, null, { |
|||
renderer: 'canvas' |
|||
}); |
|||
this.timeSeriesChartOptions = { |
|||
darkMode: this.darkMode, |
|||
backgroundColor: 'transparent', |
|||
tooltip: [{ |
|||
trigger: this.settings.tooltipTrigger === EChartsTooltipTrigger.axis ? 'axis' : 'item', |
|||
confine: true, |
|||
appendTo: 'body', |
|||
axisPointer: { |
|||
type: this.noAggregation ? 'line' : 'shadow' |
|||
}, |
|||
formatter: (params: CallbackDataParams[]) => |
|||
this.settings.showTooltip ? echartsTooltipFormatter(this.renderer, this.tooltipDateFormat, |
|||
this.settings, params, 0, '', -1, this.dataItems, |
|||
this.noAggregation ? null : this.ctx.timeWindow.interval) : undefined, |
|||
padding: [8, 12], |
|||
backgroundColor: this.settings.tooltipBackgroundColor, |
|||
borderWidth: 0, |
|||
extraCssText: `line-height: 1; backdrop-filter: blur(${this.settings.tooltipBackgroundBlur}px);` |
|||
}], |
|||
grid: [{ |
|||
top: this.minTopOffset(), |
|||
left: this.settings.dataZoom ? 5 : 0, |
|||
right: this.settings.dataZoom ? 5 : 0, |
|||
bottom: this.minBottomOffset() |
|||
}], |
|||
xAxis: [ |
|||
createTimeSeriesXAxisOption(this.settings.xAxis, this.ctx.defaultSubscription.timeWindow.minTime, |
|||
this.ctx.defaultSubscription.timeWindow.maxTime, this.ctx.date, this.darkMode) |
|||
], |
|||
yAxis: this.yAxisList.map(axis => axis.option), |
|||
dataZoom: [ |
|||
{ |
|||
type: 'inside', |
|||
disabled: !this.settings.dataZoom, |
|||
realtime: true |
|||
}, |
|||
{ |
|||
type: 'slider', |
|||
show: this.settings.dataZoom, |
|||
showDetail: false, |
|||
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; |
|||
|
|||
this.timeSeriesChartOptions.series = this.updateSeries(); |
|||
if (this.updateYAxisScale(this.yAxisList)) { |
|||
this.timeSeriesChartOptions.yAxis = this.yAxisList.map(axis => axis.option); |
|||
} |
|||
|
|||
this.timeSeriesChart.setOption(this.timeSeriesChartOptions); |
|||
this.updateAxes(); |
|||
|
|||
if (this.settings.dataZoom) { |
|||
this.timeSeriesChart.on('datazoom', () => { |
|||
this.updateAxes(); |
|||
}); |
|||
} |
|||
} |
|||
|
|||
private updateSeriesData(updateScale = false): void { |
|||
this.timeSeriesChartOptions.series = this.updateSeries(); |
|||
if (updateScale && this.updateYAxisScale(this.yAxisList)) { |
|||
this.timeSeriesChartOptions.yAxis = this.yAxisList.map(axis => axis.option); |
|||
} |
|||
this.timeSeriesChart.setOption(this.timeSeriesChartOptions); |
|||
this.updateAxes(); |
|||
} |
|||
|
|||
private updateSeries(): Array<LineSeriesOption | CustomSeriesOption> { |
|||
return generateChartData(this.dataItems, this.thresholdItems, |
|||
this.settings.stack, |
|||
this.noAggregation, |
|||
this.barRenderSharedContext, this.darkMode); |
|||
} |
|||
|
|||
private updateAxes() { |
|||
const leftAxisList = this.yAxisList.filter(axis => axis.option.position === 'left'); |
|||
let res = this.updateYAxisOffset(leftAxisList); |
|||
let leftOffset = res.offset + (!res.offset && this.settings.dataZoom ? 5 : 0); |
|||
let changed = res.changed; |
|||
const rightAxisList = this.yAxisList.filter(axis => axis.option.position === 'right'); |
|||
res = this.updateYAxisOffset(rightAxisList); |
|||
let rightOffset = res.offset + (!res.offset && this.settings.dataZoom ? 5 : 0); |
|||
changed = changed || res.changed; |
|||
let bottomOffset = this.minBottomOffset(); |
|||
const minTopOffset = this.minTopOffset(); |
|||
let topOffset = minTopOffset; |
|||
if (this.timeSeriesChartOptions.xAxis[0].show) { |
|||
const xAxisHeight = calculateXAxisHeight(this.timeSeriesChart); |
|||
if (this.timeSeriesChartOptions.xAxis[0].position === AxisPosition.bottom) { |
|||
bottomOffset += xAxisHeight; |
|||
} else { |
|||
topOffset = Math.max(minTopOffset, xAxisHeight); |
|||
} |
|||
if (this.settings.xAxis.label) { |
|||
const nameHeight = measureXAxisNameHeight(this.timeSeriesChart, this.timeSeriesChartOptions.xAxis[0].name); |
|||
if (this.timeSeriesChartOptions.xAxis[0].position === AxisPosition.bottom) { |
|||
bottomOffset += nameHeight; |
|||
} else { |
|||
topOffset = Math.max(minTopOffset, xAxisHeight + nameHeight); |
|||
} |
|||
const nameGap = xAxisHeight; |
|||
if (this.timeSeriesChartOptions.xAxis[0].nameGap !== nameGap) { |
|||
this.timeSeriesChartOptions.xAxis[0].nameGap = nameGap; |
|||
changed = true; |
|||
} |
|||
} |
|||
} |
|||
|
|||
const thresholdsOffset = calculateThresholdsOffset(this.timeSeriesChart, this.thresholdItems, this.yAxisList); |
|||
leftOffset = Math.max(leftOffset, thresholdsOffset[0]); |
|||
rightOffset = Math.max(rightOffset, thresholdsOffset[1]); |
|||
|
|||
if (this.timeSeriesChartOptions.grid[0].left !== leftOffset || |
|||
this.timeSeriesChartOptions.grid[0].right !== rightOffset || |
|||
this.timeSeriesChartOptions.grid[0].bottom !== bottomOffset || |
|||
this.timeSeriesChartOptions.grid[0].top !== topOffset) { |
|||
this.timeSeriesChartOptions.grid[0].left = leftOffset; |
|||
this.timeSeriesChartOptions.grid[0].right = rightOffset; |
|||
this.timeSeriesChartOptions.grid[0].bottom = bottomOffset; |
|||
this.timeSeriesChartOptions.grid[0].top = topOffset; |
|||
changed = true; |
|||
} |
|||
if (changed) { |
|||
this.timeSeriesChartOptions.yAxis = this.yAxisList.map(axis => axis.option); |
|||
this.timeSeriesChart.setOption(this.timeSeriesChartOptions, {replaceMerge: ['yAxis', 'xAxis', 'grid'], lazyUpdate: true}); |
|||
} |
|||
if (this.yAxisList.length) { |
|||
const extent = getAxisExtent(this.timeSeriesChart, this.yAxisList[0].id); |
|||
const min = extent[0]; |
|||
const max = extent[1]; |
|||
if (this.yMinSubject.value !== min) { |
|||
this.yMinSubject.next(min); |
|||
} |
|||
if (this.yMaxSubject.value !== max) { |
|||
this.yMaxSubject.next(max); |
|||
} |
|||
} |
|||
} |
|||
|
|||
private updateYAxisOffset(axisList: TimeSeriesChartYAxis[]): {offset: number; changed: boolean} { |
|||
const result = {offset: 0, changed: false}; |
|||
let width = 0; |
|||
for (const yAxis of axisList) { |
|||
const newWidth = calculateYAxisWidth(this.timeSeriesChart, yAxis.id); |
|||
if (width && newWidth) { |
|||
result.offset += 5; |
|||
} |
|||
width = newWidth; |
|||
const showLine = !!width && yAxis.settings.showLine; |
|||
if (yAxis.option.axisLine.show !== showLine) { |
|||
yAxis.option.axisLine.show = showLine; |
|||
result.changed = true; |
|||
} |
|||
if (yAxis.option.offset !== result.offset) { |
|||
yAxis.option.offset = result.offset; |
|||
result.changed = true; |
|||
} |
|||
if (yAxis.settings.label) { |
|||
if (!width) { |
|||
if (yAxis.option.name) { |
|||
yAxis.option.name = null; |
|||
result.changed = true; |
|||
} |
|||
} else { |
|||
if (!yAxis.option.name) { |
|||
yAxis.option.name = yAxis.settings.label; |
|||
result.changed = true; |
|||
} |
|||
const nameGap = width; |
|||
if (yAxis.option.nameGap !== nameGap) { |
|||
yAxis.option.nameGap = nameGap; |
|||
result.changed = true; |
|||
} |
|||
const nameWidth = measureYAxisNameWidth(this.timeSeriesChart, yAxis.id, yAxis.settings.label); |
|||
result.offset += nameWidth; |
|||
} |
|||
} |
|||
result.offset += width; |
|||
} |
|||
return result; |
|||
} |
|||
|
|||
private updateYAxisScale(axisList: TimeSeriesChartYAxis[]): boolean { |
|||
let changed = false; |
|||
for (const yAxis of axisList) { |
|||
const scaleYAxis = this.scaleYAxis(yAxis); |
|||
if (yAxis.option.scale !== scaleYAxis) { |
|||
yAxis.option.scale = scaleYAxis; |
|||
changed = true; |
|||
} |
|||
} |
|||
return changed; |
|||
} |
|||
|
|||
private scaleYAxis(yAxis: TimeSeriesChartYAxis): boolean { |
|||
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 minTopOffset(): number { |
|||
const showTickLabels = |
|||
!!this.yAxisList.find(yAxis => yAxis.settings.show && yAxis.settings.showTickLabels); |
|||
return (this.topPointLabels) ? 20 : |
|||
(showTickLabels ? 10 : 5); |
|||
} |
|||
|
|||
private minBottomOffset(): number { |
|||
return this.settings.dataZoom ? 45 : 5; |
|||
} |
|||
|
|||
private onResize() { |
|||
const shapeWidth = this.chartElement.offsetWidth; |
|||
const shapeHeight = this.chartElement.offsetHeight; |
|||
if (shapeWidth && shapeHeight) { |
|||
if (!this.timeSeriesChart) { |
|||
this.drawChart(); |
|||
} else { |
|||
const width = this.timeSeriesChart.getWidth(); |
|||
const height = this.timeSeriesChart.getHeight(); |
|||
if (width !== shapeWidth || height !== shapeHeight) { |
|||
let barItems: TimeSeriesChartDataItem[]; |
|||
if (this.animationEnabled()) { |
|||
barItems = |
|||
this.dataItems.filter(d => d.enabled && d.data.length && |
|||
d.dataKey.settings.type === TimeSeriesChartSeriesType.bar); |
|||
this.updateBarsAnimation(barItems, false); |
|||
} |
|||
this.timeSeriesChart.resize(); |
|||
if (this.animationEnabled()) { |
|||
this.updateBarsAnimation(barItems, true); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
private animationEnabled(): boolean { |
|||
return this.settings.animation.animation; |
|||
} |
|||
|
|||
private updateBarsAnimation(barItems: TimeSeriesChartDataItem[], animation: boolean) { |
|||
if (barItems.length) { |
|||
barItems.forEach(item => { |
|||
item.option.animation = animation; |
|||
}); |
|||
this.timeSeriesChart.setOption(this.timeSeriesChartOptions); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,67 @@ |
|||
<!-- |
|||
|
|||
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]="barSettingsFormGroup"> |
|||
<div class="tb-form-row"> |
|||
<mat-slide-toggle class="mat-slide" formControlName="showBorder"> |
|||
{{ 'widgets.time-series-chart.series.bar.show-border' | translate }} |
|||
</mat-slide-toggle> |
|||
</div> |
|||
<div class="tb-form-row space-between column-xs"> |
|||
<div translate>widgets.time-series-chart.series.bar.border-width</div> |
|||
<mat-form-field class="medium-width number" appearance="outline" subscriptSizing="dynamic"> |
|||
<input matInput type="number" formControlName="borderWidth" min="0" step="1" placeholder="{{ 'widget-config.set' | translate }}"/> |
|||
</mat-form-field> |
|||
</div> |
|||
<div class="tb-form-row space-between column-xs"> |
|||
<div translate>widgets.time-series-chart.series.bar.border-radius</div> |
|||
<mat-form-field class="medium-width number" appearance="outline" subscriptSizing="dynamic"> |
|||
<input matInput type="number" formControlName="borderRadius" min="0" step="1" placeholder="{{ 'widget-config.set' | translate }}"/> |
|||
</mat-form-field> |
|||
</div> |
|||
<div class="tb-form-row space-between column-lt-md"> |
|||
<mat-slide-toggle class="mat-slide" formControlName="showLabel"> |
|||
<div tb-hint-tooltip-icon="{{'widgets.time-series-chart.series.bar.label-hint' | translate}}"> |
|||
{{ 'widgets.time-series-chart.series.bar.label' | translate }} |
|||
</div> |
|||
</mat-slide-toggle> |
|||
<div fxLayout="row" fxFlex.lt-md fxLayoutAlign="start center" fxLayoutGap="8px"> |
|||
<mat-form-field class="medium-width" fxFlex.lt-md appearance="outline" subscriptSizing="dynamic"> |
|||
<mat-select formControlName="labelPosition"> |
|||
<mat-option *ngFor="let position of seriesLabelPositions" [value]="position"> |
|||
{{ seriesLabelPositionTranslations.get(position) | translate }} |
|||
</mat-option> |
|||
</mat-select> |
|||
</mat-form-field> |
|||
<tb-font-settings formControlName="labelFont" |
|||
clearButton |
|||
disabledLineHeight |
|||
forceSizeUnit="px" |
|||
[previewText]="labelPreviewFn"> |
|||
</tb-font-settings> |
|||
<tb-color-input asBoxInput |
|||
colorClearButton |
|||
formControlName="labelColor"> |
|||
</tb-color-input> |
|||
</div> |
|||
</div> |
|||
<tb-time-series-chart-fill-settings |
|||
formControlName="backgroundSettings" |
|||
title="widgets.time-series-chart.series.background" |
|||
fillNoneTitle="widgets.time-series-chart.series.fill-type-solid"> |
|||
</tb-time-series-chart-fill-settings> |
|||
</ng-container> |
|||
@ -0,0 +1,148 @@ |
|||
///
|
|||
/// 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 { |
|||
BarSeriesSettings, |
|||
seriesLabelPositions, |
|||
seriesLabelPositionTranslations |
|||
} from '@home/components/widget/lib/chart/time-series-chart.models'; |
|||
import { Store } from '@ngrx/store'; |
|||
import { AppState } from '@core/core.state'; |
|||
import { merge } from 'rxjs'; |
|||
import { formatValue, isDefinedAndNotNull } from '@core/utils'; |
|||
import { DataKeyConfigComponent } from '@home/components/widget/config/data-key-config.component'; |
|||
|
|||
@Component({ |
|||
selector: 'tb-time-series-chart-bar-settings', |
|||
templateUrl: './time-series-chart-bar-settings.component.html', |
|||
styleUrls: ['./../widget-settings.scss'], |
|||
providers: [ |
|||
{ |
|||
provide: NG_VALUE_ACCESSOR, |
|||
useExisting: forwardRef(() => TimeSeriesChartBarSettingsComponent), |
|||
multi: true |
|||
} |
|||
] |
|||
}) |
|||
export class TimeSeriesChartBarSettingsComponent implements OnInit, ControlValueAccessor { |
|||
|
|||
seriesLabelPositions = seriesLabelPositions; |
|||
|
|||
seriesLabelPositionTranslations = seriesLabelPositionTranslations; |
|||
|
|||
labelPreviewFn = this._labelPreviewFn.bind(this); |
|||
|
|||
@Input() |
|||
disabled: boolean; |
|||
|
|||
private modelValue: BarSeriesSettings; |
|||
|
|||
private propagateChange = null; |
|||
|
|||
public barSettingsFormGroup: UntypedFormGroup; |
|||
|
|||
constructor(protected store: Store<AppState>, |
|||
private dataKeyConfigComponent: DataKeyConfigComponent, |
|||
private fb: UntypedFormBuilder) { |
|||
} |
|||
|
|||
ngOnInit(): void { |
|||
this.barSettingsFormGroup = this.fb.group({ |
|||
showBorder: [null, []], |
|||
borderWidth: [null, [Validators.min(0)]], |
|||
borderRadius: [null, [Validators.min(0)]], |
|||
showLabel: [null, []], |
|||
labelPosition: [null, []], |
|||
labelFont: [null, []], |
|||
labelColor: [null, []], |
|||
backgroundSettings: [null, []] |
|||
}); |
|||
this.barSettingsFormGroup.valueChanges.subscribe(() => { |
|||
this.updateModel(); |
|||
}); |
|||
merge(this.barSettingsFormGroup.get('showBorder').valueChanges, |
|||
this.barSettingsFormGroup.get('showLabel').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.barSettingsFormGroup.disable({emitEvent: false}); |
|||
} else { |
|||
this.barSettingsFormGroup.enable({emitEvent: false}); |
|||
this.updateValidators(); |
|||
} |
|||
} |
|||
|
|||
writeValue(value: BarSeriesSettings): void { |
|||
this.modelValue = value; |
|||
this.barSettingsFormGroup.patchValue( |
|||
value, {emitEvent: false} |
|||
); |
|||
this.updateValidators(); |
|||
} |
|||
|
|||
private updateValidators() { |
|||
const showBorder: boolean = this.barSettingsFormGroup.get('showBorder').value; |
|||
const showLabel: boolean = this.barSettingsFormGroup.get('showLabel').value; |
|||
if (showBorder) { |
|||
this.barSettingsFormGroup.get('borderWidth').enable({emitEvent: false}); |
|||
} else { |
|||
this.barSettingsFormGroup.get('borderWidth').disable({emitEvent: false}); |
|||
} |
|||
if (showLabel) { |
|||
this.barSettingsFormGroup.get('labelPosition').enable({emitEvent: false}); |
|||
this.barSettingsFormGroup.get('labelFont').enable({emitEvent: false}); |
|||
this.barSettingsFormGroup.get('labelColor').enable({emitEvent: false}); |
|||
} else { |
|||
this.barSettingsFormGroup.get('labelPosition').disable({emitEvent: false}); |
|||
this.barSettingsFormGroup.get('labelFont').disable({emitEvent: false}); |
|||
this.barSettingsFormGroup.get('labelColor').disable({emitEvent: false}); |
|||
} |
|||
} |
|||
|
|||
private updateModel() { |
|||
this.modelValue = this.barSettingsFormGroup.getRawValue(); |
|||
this.propagateChange(this.modelValue); |
|||
} |
|||
|
|||
private _labelPreviewFn(): string { |
|||
const dataKey = this.dataKeyConfigComponent.modelValue; |
|||
const widgetConfig = this.dataKeyConfigComponent.widgetConfig; |
|||
const units = dataKey.units && dataKey.units.length ? dataKey.units : widgetConfig.config.units; |
|||
const decimals = isDefinedAndNotNull(dataKey.decimals) ? dataKey.decimals : |
|||
(isDefinedAndNotNull(widgetConfig.config.decimals) ? widgetConfig.config.decimals : 2); |
|||
return formatValue(22, decimals, units, false); |
|||
} |
|||
} |
|||
@ -0,0 +1,53 @@ |
|||
<!-- |
|||
|
|||
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]="fillSettingsFormGroup"> |
|||
<div class="tb-form-row column"> |
|||
<div class="tb-form-row no-border no-padding space-between"> |
|||
<div>{{ title | translate }}</div> |
|||
<tb-toggle-select formControlName="type"> |
|||
<tb-toggle-option *ngFor="let type of seriesFillTypes" [value]="type">{{ seriesFillTypeTranslationMap.get(type) | translate }}</tb-toggle-option> |
|||
</tb-toggle-select> |
|||
</div> |
|||
<ng-container *ngIf="fillSettingsFormGroup.get('type').value === SeriesFillType.opacity"> |
|||
<div class="tb-form-row space-between column-xs"> |
|||
<div translate>widgets.time-series-chart.series.opacity</div> |
|||
<mat-form-field class="medium-width number" appearance="outline" subscriptSizing="dynamic"> |
|||
<input matInput type="number" formControlName="opacity" min="0" max="100" |
|||
step="1" placeholder="{{ 'widget-config.set' | translate }}"/> |
|||
</mat-form-field> |
|||
</div> |
|||
</ng-container> |
|||
<ng-container *ngIf="fillSettingsFormGroup.get('type').value === SeriesFillType.gradient" formGroupName="gradient"> |
|||
<div class="tb-form-row space-between column-xs"> |
|||
<div translate>widgets.time-series-chart.series.gradient-stops</div> |
|||
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px"> |
|||
<div class="tb-small-label" translate>widgets.time-series-chart.series.gradient-start</div> |
|||
<mat-form-field appearance="outline" class="number" subscriptSizing="dynamic"> |
|||
<input matInput formControlName="start" min="0" max="100" step="1" |
|||
type="number" placeholder="{{ 'widget-config.set' | translate }}"> |
|||
</mat-form-field> |
|||
<div class="tb-small-label" translate>widgets.time-series-chart.series.gradient-end</div> |
|||
<mat-form-field appearance="outline" class="number" subscriptSizing="dynamic"> |
|||
<input matInput formControlName="end" min="0" max="100" step="1" |
|||
type="number" placeholder="{{ 'widget-config.set' | translate }}"> |
|||
</mat-form-field> |
|||
</div> |
|||
</div> |
|||
</ng-container> |
|||
</div> |
|||
</ng-container> |
|||
@ -0,0 +1,143 @@ |
|||
///
|
|||
/// 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 { |
|||
SeriesFillSettings, |
|||
SeriesFillType, |
|||
seriesFillTypes, |
|||
seriesFillTypeTranslations |
|||
} from '@home/components/widget/lib/chart/time-series-chart.models'; |
|||
import { Store } from '@ngrx/store'; |
|||
import { AppState } from '@core/core.state'; |
|||
|
|||
@Component({ |
|||
selector: 'tb-time-series-chart-fill-settings', |
|||
templateUrl: './time-series-chart-fill-settings.component.html', |
|||
styleUrls: ['./../widget-settings.scss'], |
|||
providers: [ |
|||
{ |
|||
provide: NG_VALUE_ACCESSOR, |
|||
useExisting: forwardRef(() => TimeSeriesChartFillSettingsComponent), |
|||
multi: true |
|||
} |
|||
] |
|||
}) |
|||
export class TimeSeriesChartFillSettingsComponent implements OnInit, ControlValueAccessor { |
|||
|
|||
seriesFillTypes = seriesFillTypes; |
|||
|
|||
seriesFillTypeTranslationMap: Map<SeriesFillType, string> = new Map<SeriesFillType, string>([]); |
|||
|
|||
SeriesFillType = SeriesFillType; |
|||
|
|||
@Input() |
|||
disabled: boolean; |
|||
|
|||
@Input() |
|||
title = 'widgets.time-series-chart.series.fill'; |
|||
|
|||
@Input() |
|||
fillNoneTitle = 'widgets.time-series-chart.series.fill-type-none'; |
|||
|
|||
private modelValue: SeriesFillSettings; |
|||
|
|||
private propagateChange = null; |
|||
|
|||
public fillSettingsFormGroup: UntypedFormGroup; |
|||
|
|||
constructor(protected store: Store<AppState>, |
|||
private fb: UntypedFormBuilder) { |
|||
} |
|||
|
|||
ngOnInit(): void { |
|||
this.fillSettingsFormGroup = this.fb.group({ |
|||
type: [null, []], |
|||
opacity: [null, [Validators.min(0), Validators.max(100)]], |
|||
gradient: this.fb.group({ |
|||
start: [null, [Validators.min(0), Validators.max(100)]], |
|||
end: [null, [Validators.min(0), Validators.max(100)]] |
|||
}) |
|||
}); |
|||
this.fillSettingsFormGroup.valueChanges.subscribe(() => { |
|||
this.updateModel(); |
|||
}); |
|||
this.fillSettingsFormGroup.get('type').valueChanges.subscribe(() => { |
|||
this.updateValidators(); |
|||
}); |
|||
for (const type of seriesFillTypes) { |
|||
let translation: string; |
|||
if (type === SeriesFillType.none) { |
|||
translation = this.fillNoneTitle; |
|||
} else { |
|||
translation = seriesFillTypeTranslations.get(type); |
|||
} |
|||
this.seriesFillTypeTranslationMap.set(type, translation); |
|||
} |
|||
} |
|||
|
|||
registerOnChange(fn: any): void { |
|||
this.propagateChange = fn; |
|||
} |
|||
|
|||
registerOnTouched(_fn: any): void { |
|||
} |
|||
|
|||
setDisabledState(isDisabled: boolean): void { |
|||
this.disabled = isDisabled; |
|||
if (isDisabled) { |
|||
this.fillSettingsFormGroup.disable({emitEvent: false}); |
|||
} else { |
|||
this.fillSettingsFormGroup.enable({emitEvent: false}); |
|||
this.updateValidators(); |
|||
} |
|||
} |
|||
|
|||
writeValue(value: SeriesFillSettings): void { |
|||
this.modelValue = value; |
|||
this.fillSettingsFormGroup.patchValue( |
|||
value, {emitEvent: false} |
|||
); |
|||
this.updateValidators(); |
|||
} |
|||
|
|||
private updateValidators() { |
|||
const type: SeriesFillType = this.fillSettingsFormGroup.get('type').value; |
|||
if (type === SeriesFillType.none) { |
|||
this.fillSettingsFormGroup.get('opacity').disable({emitEvent: false}); |
|||
this.fillSettingsFormGroup.get('gradient').disable({emitEvent: false}); |
|||
} else if (type === SeriesFillType.opacity) { |
|||
this.fillSettingsFormGroup.get('opacity').enable({emitEvent: false}); |
|||
this.fillSettingsFormGroup.get('gradient').disable({emitEvent: false}); |
|||
} else if (type === SeriesFillType.gradient) { |
|||
this.fillSettingsFormGroup.get('opacity').disable({emitEvent: false}); |
|||
this.fillSettingsFormGroup.get('gradient').enable({emitEvent: false}); |
|||
} |
|||
} |
|||
|
|||
private updateModel() { |
|||
const value: SeriesFillSettings = this.fillSettingsFormGroup.getRawValue(); |
|||
this.modelValue = value; |
|||
this.propagateChange(this.modelValue); |
|||
} |
|||
} |
|||
@ -0,0 +1,62 @@ |
|||
<!-- |
|||
|
|||
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]="timeSeriesChartKeySettingsForm"> |
|||
<div class="tb-form-panel"> |
|||
<div class="tb-form-panel-title" translate>widgets.time-series-chart.series.legend-settings</div> |
|||
<mat-slide-toggle class="mat-slide" formControlName="showInLegend"> |
|||
<div tb-hint-tooltip-icon="{{'widgets.time-series-chart.series.show-in-legend-hint' | translate}}"> |
|||
{{ 'widgets.time-series-chart.series.show-in-legend' | translate }} |
|||
</div> |
|||
</mat-slide-toggle> |
|||
<mat-slide-toggle class="mat-slide" formControlName="dataHiddenByDefault"> |
|||
<div tb-hint-tooltip-icon="{{'widgets.time-series-chart.series.hidden-by-default-hint' | translate}}"> |
|||
{{ 'widgets.time-series-chart.series.hidden-by-default' | translate }} |
|||
</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> |
|||
<tb-toggle-select formControlName="type"> |
|||
<tb-toggle-option *ngFor="let type of timeSeriesChartSeriesTypes" [value]="type">{{ timeSeriesChartSeriesTypeTranslations.get(type) | translate }}</tb-toggle-option> |
|||
</tb-toggle-select> |
|||
</div> |
|||
<tb-time-series-chart-line-settings [fxShow]="timeSeriesChartKeySettingsForm.get('type').value === TimeSeriesChartSeriesType.line" |
|||
[chartType]="chartType" |
|||
formControlName="lineSettings"> |
|||
</tb-time-series-chart-line-settings> |
|||
<tb-time-series-chart-bar-settings [fxShow]="timeSeriesChartKeySettingsForm.get('type').value === TimeSeriesChartSeriesType.bar" |
|||
formControlName="barSettings"> |
|||
</tb-time-series-chart-bar-settings> |
|||
</div> |
|||
</ng-container> |
|||
<ng-template #chartTypeTitle> |
|||
<div class="tb-form-panel-title">{{ timeSeriesChartTypeTranslations.get(chartType) | translate }}</div> |
|||
</ng-template> |
|||
@ -0,0 +1,116 @@ |
|||
///
|
|||
/// Copyright © 2016-2024 The Thingsboard Authors
|
|||
///
|
|||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
|||
/// you may not use this file except in compliance with the License.
|
|||
/// You may obtain a copy of the License at
|
|||
///
|
|||
/// http://www.apache.org/licenses/LICENSE-2.0
|
|||
///
|
|||
/// Unless required by applicable law or agreed to in writing, software
|
|||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
|||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|||
/// See the License for the specific language governing permissions and
|
|||
/// limitations under the License.
|
|||
///
|
|||
|
|||
import { Component } from '@angular/core'; |
|||
import { WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models'; |
|||
import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; |
|||
import { Store } from '@ngrx/store'; |
|||
import { AppState } from '@core/core.state'; |
|||
import { isDefinedAndNotNull, mergeDeep } from '@core/utils'; |
|||
import { |
|||
timeSeriesChartKeyDefaultSettings, |
|||
TimeSeriesChartKeySettings, |
|||
TimeSeriesChartSeriesType, |
|||
timeSeriesChartSeriesTypes, |
|||
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', |
|||
templateUrl: './time-series-chart-key-settings.component.html', |
|||
styleUrls: ['./../widget-settings.scss'] |
|||
}) |
|||
export class TimeSeriesChartKeySettingsComponent extends WidgetSettingsComponent { |
|||
|
|||
TimeSeriesChartType = TimeSeriesChartType; |
|||
|
|||
timeSeriesChartTypeTranslations = timeSeriesChartTypeTranslations; |
|||
|
|||
TimeSeriesChartSeriesType = TimeSeriesChartSeriesType; |
|||
|
|||
timeSeriesChartSeriesTypes = timeSeriesChartSeriesTypes; |
|||
|
|||
timeSeriesChartSeriesTypeTranslations = timeSeriesChartSeriesTypeTranslations; |
|||
|
|||
timeSeriesChartKeySettingsForm: UntypedFormGroup; |
|||
|
|||
chartType = TimeSeriesChartType.default; |
|||
|
|||
yAxisIds: TimeSeriesChartYAxisId[]; |
|||
|
|||
constructor(protected store: Store<AppState>, |
|||
private fb: UntypedFormBuilder) { |
|||
super(store); |
|||
} |
|||
|
|||
protected settingsForm(): UntypedFormGroup { |
|||
return this.timeSeriesChartKeySettingsForm; |
|||
} |
|||
|
|||
protected onWidgetConfigSet(widgetConfig: WidgetConfigComponentData) { |
|||
const params = widgetConfig.typeParameters as any; |
|||
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 { |
|||
return mergeDeep<TimeSeriesChartKeySettings>({} as TimeSeriesChartKeySettings, |
|||
timeSeriesChartKeyDefaultSettings); |
|||
} |
|||
|
|||
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, []], |
|||
lineSettings: [seriesSettings.lineSettings, []], |
|||
barSettings: [seriesSettings.barSettings, []] |
|||
}); |
|||
} |
|||
|
|||
protected validatorTriggers(): string[] { |
|||
return ['showInLegend', 'type']; |
|||
} |
|||
|
|||
protected updateValidators(_emitEvent: boolean) { |
|||
const showInLegend: boolean = this.timeSeriesChartKeySettingsForm.get('showInLegend').value; |
|||
const type: TimeSeriesChartSeriesType = this.timeSeriesChartKeySettingsForm.get('type').value; |
|||
if (showInLegend) { |
|||
this.timeSeriesChartKeySettingsForm.get('dataHiddenByDefault').enable(); |
|||
} else { |
|||
this.timeSeriesChartKeySettingsForm.get('dataHiddenByDefault').patchValue(false, {emitEvent: false}); |
|||
this.timeSeriesChartKeySettingsForm.get('dataHiddenByDefault').disable(); |
|||
} |
|||
if (type === TimeSeriesChartSeriesType.line) { |
|||
this.timeSeriesChartKeySettingsForm.get('lineSettings').enable(); |
|||
this.timeSeriesChartKeySettingsForm.get('barSettings').disable(); |
|||
} else if (type === TimeSeriesChartSeriesType.bar) { |
|||
this.timeSeriesChartKeySettingsForm.get('lineSettings').disable(); |
|||
this.timeSeriesChartKeySettingsForm.get('barSettings').enable(); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,116 @@ |
|||
<!-- |
|||
|
|||
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]="lineSettingsFormGroup"> |
|||
<div *ngIf="chartType !== TimeSeriesChartType.point" class="tb-form-panel stroked"> |
|||
<div class="tb-form-panel-title" translate>widgets.time-series-chart.series.line.line</div> |
|||
<div class="tb-form-row"> |
|||
<mat-slide-toggle class="mat-slide" formControlName="showLine"> |
|||
{{ 'widgets.time-series-chart.series.line.show-line' | translate }} |
|||
</mat-slide-toggle> |
|||
</div> |
|||
<div class="tb-form-row space-between column-xs"> |
|||
<mat-slide-toggle class="mat-slide" formControlName="step"> |
|||
{{ 'widgets.time-series-chart.series.line.step-line' | translate }} |
|||
</mat-slide-toggle> |
|||
<mat-form-field class="medium-width" appearance="outline" subscriptSizing="dynamic"> |
|||
<mat-select formControlName="stepType"> |
|||
<mat-option *ngFor="let stepType of lineSeriesStepTypes" [value]="stepType"> |
|||
{{ lineSeriesStepTypeTranslations.get(stepType) | translate }} |
|||
</mat-option> |
|||
</mat-select> |
|||
</mat-form-field> |
|||
</div> |
|||
<div class="tb-form-row"> |
|||
<mat-slide-toggle class="mat-slide" formControlName="smooth"> |
|||
{{ 'widgets.time-series-chart.series.line.smooth-line' | translate }} |
|||
</mat-slide-toggle> |
|||
</div> |
|||
<div class="tb-form-row space-between column-xs"> |
|||
<div translate>widgets.time-series-chart.line-type</div> |
|||
<mat-form-field class="medium-width" appearance="outline" subscriptSizing="dynamic"> |
|||
<mat-select formControlName="lineType"> |
|||
<mat-option *ngFor="let lineType of timeSeriesLineTypes" [value]="lineType"> |
|||
{{ timeSeriesLineTypeTranslations.get(lineType) | translate }} |
|||
</mat-option> |
|||
</mat-select> |
|||
</mat-form-field> |
|||
</div> |
|||
<div class="tb-form-row space-between column-xs"> |
|||
<div translate>widgets.time-series-chart.line-width</div> |
|||
<mat-form-field class="medium-width number" appearance="outline" subscriptSizing="dynamic"> |
|||
<input matInput type="number" formControlName="lineWidth" min="0" step="1" placeholder="{{ 'widget-config.set' | translate }}"/> |
|||
</mat-form-field> |
|||
</div> |
|||
</div> |
|||
<div *ngIf="chartType !== TimeSeriesChartType.point; else pointSettings" class="tb-form-panel stroked"> |
|||
<div class="tb-form-panel-title" translate>widgets.time-series-chart.series.point.points</div> |
|||
<div class="tb-form-row"> |
|||
<mat-slide-toggle class="mat-slide" formControlName="showPoints"> |
|||
{{ 'widgets.time-series-chart.series.point.show-points' | translate }} |
|||
</mat-slide-toggle> |
|||
</div> |
|||
<ng-container *ngTemplateOutlet="pointSettings"></ng-container> |
|||
</div> |
|||
<tb-time-series-chart-fill-settings |
|||
formControlName="fillAreaSettings"> |
|||
</tb-time-series-chart-fill-settings> |
|||
</ng-container> |
|||
<ng-template #pointSettings [formGroup]="lineSettingsFormGroup"> |
|||
<div class="tb-form-row space-between column-lt-md"> |
|||
<mat-slide-toggle class="mat-slide" formControlName="showPointLabel"> |
|||
<div tb-hint-tooltip-icon="{{'widgets.time-series-chart.series.point.point-label-hint' | translate}}"> |
|||
{{ 'widgets.time-series-chart.series.point.point-label' | translate }} |
|||
</div> |
|||
</mat-slide-toggle> |
|||
<div fxLayout="row" fxFlex.lt-md fxLayoutAlign="start center" fxLayoutGap="8px"> |
|||
<mat-form-field class="medium-width" fxFlex.lt-md appearance="outline" subscriptSizing="dynamic"> |
|||
<mat-select formControlName="pointLabelPosition"> |
|||
<mat-option *ngFor="let position of seriesLabelPositions" [value]="position"> |
|||
{{ seriesLabelPositionTranslations.get(position) | translate }} |
|||
</mat-option> |
|||
</mat-select> |
|||
</mat-form-field> |
|||
<tb-font-settings formControlName="pointLabelFont" |
|||
clearButton |
|||
disabledLineHeight |
|||
forceSizeUnit="px" |
|||
[previewText]="pointLabelPreviewFn"> |
|||
</tb-font-settings> |
|||
<tb-color-input asBoxInput |
|||
colorClearButton |
|||
formControlName="pointLabelColor"> |
|||
</tb-color-input> |
|||
</div> |
|||
</div> |
|||
<div class="tb-form-row space-between column-xs"> |
|||
<div translate>widgets.time-series-chart.series.point.point-shape</div> |
|||
<mat-form-field class="medium-width" appearance="outline" subscriptSizing="dynamic"> |
|||
<mat-select formControlName="pointShape"> |
|||
<mat-option *ngFor="let shape of timeSeriesChartShapes" [value]="shape"> |
|||
{{ timeSeriesChartShapeTranslations.get(shape) | translate }} |
|||
</mat-option> |
|||
</mat-select> |
|||
</mat-form-field> |
|||
</div> |
|||
<div class="tb-form-row space-between column-xs"> |
|||
<div translate>widgets.time-series-chart.series.point.point-size</div> |
|||
<mat-form-field class="medium-width number" appearance="outline" subscriptSizing="dynamic"> |
|||
<input matInput type="number" formControlName="pointSize" min="0" step="1" placeholder="{{ 'widget-config.set' | translate }}"/> |
|||
</mat-form-field> |
|||
</div> |
|||
</ng-template> |
|||
@ -0,0 +1,192 @@ |
|||
///
|
|||
/// 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 { |
|||
LineSeriesSettings, |
|||
lineSeriesStepTypes, |
|||
lineSeriesStepTypeTranslations, |
|||
seriesLabelPositions, |
|||
seriesLabelPositionTranslations, |
|||
timeSeriesChartShapes, |
|||
timeSeriesChartShapeTranslations, TimeSeriesChartType, |
|||
timeSeriesLineTypes, |
|||
timeSeriesLineTypeTranslations |
|||
} from '@home/components/widget/lib/chart/time-series-chart.models'; |
|||
import { Store } from '@ngrx/store'; |
|||
import { AppState } from '@core/core.state'; |
|||
import { merge } from 'rxjs'; |
|||
import { formatValue, isDefinedAndNotNull } from '@core/utils'; |
|||
import { DataKeyConfigComponent } from '@home/components/widget/config/data-key-config.component'; |
|||
|
|||
@Component({ |
|||
selector: 'tb-time-series-chart-line-settings', |
|||
templateUrl: './time-series-chart-line-settings.component.html', |
|||
styleUrls: ['./../widget-settings.scss'], |
|||
providers: [ |
|||
{ |
|||
provide: NG_VALUE_ACCESSOR, |
|||
useExisting: forwardRef(() => TimeSeriesChartLineSettingsComponent), |
|||
multi: true |
|||
} |
|||
] |
|||
}) |
|||
export class TimeSeriesChartLineSettingsComponent implements OnInit, ControlValueAccessor { |
|||
|
|||
TimeSeriesChartType = TimeSeriesChartType; |
|||
|
|||
lineSeriesStepTypes = lineSeriesStepTypes; |
|||
|
|||
lineSeriesStepTypeTranslations = lineSeriesStepTypeTranslations; |
|||
|
|||
timeSeriesLineTypes = timeSeriesLineTypes; |
|||
|
|||
timeSeriesLineTypeTranslations = timeSeriesLineTypeTranslations; |
|||
|
|||
seriesLabelPositions = seriesLabelPositions; |
|||
|
|||
seriesLabelPositionTranslations = seriesLabelPositionTranslations; |
|||
|
|||
timeSeriesChartShapes = timeSeriesChartShapes; |
|||
|
|||
timeSeriesChartShapeTranslations = timeSeriesChartShapeTranslations; |
|||
|
|||
pointLabelPreviewFn = this._pointLabelPreviewFn.bind(this); |
|||
|
|||
@Input() |
|||
disabled: boolean; |
|||
|
|||
@Input() |
|||
chartType: TimeSeriesChartType; |
|||
|
|||
private modelValue: LineSeriesSettings; |
|||
|
|||
private propagateChange = null; |
|||
|
|||
public lineSettingsFormGroup: UntypedFormGroup; |
|||
|
|||
constructor(protected store: Store<AppState>, |
|||
private dataKeyConfigComponent: DataKeyConfigComponent, |
|||
private fb: UntypedFormBuilder) { |
|||
} |
|||
|
|||
ngOnInit(): void { |
|||
this.lineSettingsFormGroup = this.fb.group({ |
|||
showLine: [null, []], |
|||
step: [null, []], |
|||
stepType: [null, []], |
|||
smooth: [null, []], |
|||
lineType: [null, []], |
|||
lineWidth: [null, [Validators.min(0)]], |
|||
showPoints: [null, []], |
|||
showPointLabel: [null, []], |
|||
pointLabelPosition: [null, []], |
|||
pointLabelFont: [null, []], |
|||
pointLabelColor: [null, []], |
|||
pointShape: [null, []], |
|||
pointSize: [null, [Validators.min(0)]], |
|||
fillAreaSettings: [null, []] |
|||
}); |
|||
this.lineSettingsFormGroup.valueChanges.subscribe(() => { |
|||
this.updateModel(); |
|||
}); |
|||
merge(this.lineSettingsFormGroup.get('showLine').valueChanges, |
|||
this.lineSettingsFormGroup.get('step').valueChanges, |
|||
this.lineSettingsFormGroup.get('showPointLabel').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.lineSettingsFormGroup.disable({emitEvent: false}); |
|||
} else { |
|||
this.lineSettingsFormGroup.enable({emitEvent: false}); |
|||
this.updateValidators(); |
|||
} |
|||
} |
|||
|
|||
writeValue(value: LineSeriesSettings): void { |
|||
this.modelValue = value; |
|||
this.lineSettingsFormGroup.patchValue( |
|||
value, {emitEvent: false} |
|||
); |
|||
this.updateValidators(); |
|||
} |
|||
|
|||
private updateValidators() { |
|||
const showLine: boolean = this.lineSettingsFormGroup.get('showLine').value; |
|||
const step: boolean = this.lineSettingsFormGroup.get('step').value; |
|||
const showPointLabel: boolean = this.lineSettingsFormGroup.get('showPointLabel').value; |
|||
if (showLine) { |
|||
this.lineSettingsFormGroup.get('step').enable({emitEvent: false}); |
|||
if (step) { |
|||
this.lineSettingsFormGroup.get('stepType').enable({emitEvent: false}); |
|||
this.lineSettingsFormGroup.get('smooth').disable({emitEvent: false}); |
|||
} else { |
|||
this.lineSettingsFormGroup.get('stepType').disable({emitEvent: false}); |
|||
this.lineSettingsFormGroup.get('smooth').enable({emitEvent: false}); |
|||
} |
|||
this.lineSettingsFormGroup.get('lineType').enable({emitEvent: false}); |
|||
this.lineSettingsFormGroup.get('lineWidth').enable({emitEvent: false}); |
|||
} else { |
|||
this.lineSettingsFormGroup.get('step').disable({emitEvent: false}); |
|||
this.lineSettingsFormGroup.get('stepType').disable({emitEvent: false}); |
|||
this.lineSettingsFormGroup.get('smooth').disable({emitEvent: false}); |
|||
this.lineSettingsFormGroup.get('lineType').disable({emitEvent: false}); |
|||
this.lineSettingsFormGroup.get('lineWidth').disable({emitEvent: false}); |
|||
} |
|||
if (showPointLabel) { |
|||
this.lineSettingsFormGroup.get('pointLabelPosition').enable({emitEvent: false}); |
|||
this.lineSettingsFormGroup.get('pointLabelFont').enable({emitEvent: false}); |
|||
this.lineSettingsFormGroup.get('pointLabelColor').enable({emitEvent: false}); |
|||
} else { |
|||
this.lineSettingsFormGroup.get('pointLabelPosition').disable({emitEvent: false}); |
|||
this.lineSettingsFormGroup.get('pointLabelFont').disable({emitEvent: false}); |
|||
this.lineSettingsFormGroup.get('pointLabelColor').disable({emitEvent: false}); |
|||
} |
|||
} |
|||
|
|||
private updateModel() { |
|||
this.modelValue = this.lineSettingsFormGroup.getRawValue(); |
|||
this.propagateChange(this.modelValue); |
|||
} |
|||
|
|||
private _pointLabelPreviewFn(): string { |
|||
const dataKey = this.dataKeyConfigComponent.modelValue; |
|||
const widgetConfig = this.dataKeyConfigComponent.widgetConfig; |
|||
const units = dataKey.units && dataKey.units.length ? dataKey.units : widgetConfig.config.units; |
|||
const decimals = isDefinedAndNotNull(dataKey.decimals) ? dataKey.decimals : |
|||
(isDefinedAndNotNull(widgetConfig.config.decimals) ? widgetConfig.config.decimals : 2); |
|||
return formatValue(22, decimals, units, false); |
|||
} |
|||
} |
|||
@ -0,0 +1,179 @@ |
|||
<!-- |
|||
|
|||
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]="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" |
|||
[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> |
|||
<div class="tb-form-row"> |
|||
<mat-slide-toggle class="mat-slide" formControlName="dataZoom"> |
|||
{{ 'widgets.time-series-chart.data-zoom' | translate }} |
|||
</mat-slide-toggle> |
|||
</div> |
|||
<div class="tb-form-row"> |
|||
<mat-slide-toggle class="mat-slide" formControlName="stack"> |
|||
<div tb-hint-tooltip-icon="{{'widgets.time-series-chart.stack-mode-hint' | translate}}"> |
|||
{{ 'widgets.time-series-chart.stack-mode' | translate }} |
|||
</div> |
|||
</mat-slide-toggle> |
|||
</div> |
|||
<div class="tb-form-panel"> |
|||
<div class="tb-form-panel-title" translate>widgets.time-series-chart.axis.x-axis</div> |
|||
<tb-time-series-chart-axis-settings |
|||
formControlName="xAxis" |
|||
advanced |
|||
axisType="xAxis"> |
|||
</tb-time-series-chart-axis-settings> |
|||
</div> |
|||
<tb-time-series-no-aggregation-bar-width-settings |
|||
*ngIf="chartType === TimeSeriesChartType.default || chartType === TimeSeriesChartType.bar" |
|||
formControlName="noAggregationBarWidthSettings"> |
|||
</tb-time-series-no-aggregation-bar-width-settings> |
|||
<div class="tb-form-panel tb-slide-toggle"> |
|||
<mat-expansion-panel class="tb-settings" [expanded]="timeSeriesChartWidgetSettingsForm.get('showLegend').value" |
|||
[disabled]="!timeSeriesChartWidgetSettingsForm.get('showLegend').value"> |
|||
<mat-expansion-panel-header fxLayout="row wrap"> |
|||
<mat-panel-title> |
|||
<mat-slide-toggle class="mat-slide" formControlName="showLegend" (click)="$event.stopPropagation()" |
|||
fxLayoutAlign="center"> |
|||
{{ 'widget-config.legend' | translate }} |
|||
</mat-slide-toggle> |
|||
</mat-panel-title> |
|||
</mat-expansion-panel-header> |
|||
<ng-template matExpansionPanelContent> |
|||
<div class="tb-form-row space-between"> |
|||
<div>{{ 'legend.label' | translate }}</div> |
|||
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px"> |
|||
<tb-font-settings formControlName="legendLabelFont" |
|||
previewText="Temperature"> |
|||
</tb-font-settings> |
|||
<tb-color-input asBoxInput |
|||
colorClearButton |
|||
formControlName="legendLabelColor"> |
|||
</tb-color-input> |
|||
</div> |
|||
</div> |
|||
<tb-legend-config hideDirection |
|||
formControlName="legendConfig"> |
|||
</tb-legend-config> |
|||
</ng-template> |
|||
</mat-expansion-panel> |
|||
</div> |
|||
<div class="tb-form-panel tb-slide-toggle"> |
|||
<mat-expansion-panel class="tb-settings" [expanded]="timeSeriesChartWidgetSettingsForm.get('showTooltip').value" |
|||
[disabled]="!timeSeriesChartWidgetSettingsForm.get('showTooltip').value"> |
|||
<mat-expansion-panel-header fxLayout="row wrap"> |
|||
<mat-panel-title> |
|||
<mat-slide-toggle class="mat-slide" formControlName="showTooltip" (click)="$event.stopPropagation()" |
|||
fxLayoutAlign="center"> |
|||
{{ 'widget-config.tooltip' | translate }} |
|||
</mat-slide-toggle> |
|||
</mat-panel-title> |
|||
</mat-expansion-panel-header> |
|||
<ng-template matExpansionPanelContent> |
|||
<div class="tb-form-row space-between"> |
|||
<div>{{ 'tooltip.trigger' | translate }}</div> |
|||
<mat-chip-listbox class="center-stretch" formControlName="tooltipTrigger"> |
|||
<mat-chip-option [selectable]="timeSeriesChartWidgetSettingsForm.get('tooltipTrigger').value !== EChartsTooltipTrigger.point" |
|||
[value]="EChartsTooltipTrigger.point">{{ 'tooltip.trigger-point' | translate }}</mat-chip-option> |
|||
<mat-chip-option [selectable]="timeSeriesChartWidgetSettingsForm.get('tooltipTrigger').value !== EChartsTooltipTrigger.axis" |
|||
[value]="EChartsTooltipTrigger.axis">{{ 'tooltip.trigger-axis' | translate }}</mat-chip-option> |
|||
</mat-chip-listbox> |
|||
</div> |
|||
<div class="tb-form-row space-between"> |
|||
<div>{{ 'tooltip.value' | translate }}</div> |
|||
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px"> |
|||
<tb-font-settings formControlName="tooltipValueFont" |
|||
[previewText]="tooltipValuePreviewFn"> |
|||
</tb-font-settings> |
|||
<tb-color-input asBoxInput |
|||
colorClearButton |
|||
formControlName="tooltipValueColor"> |
|||
</tb-color-input> |
|||
</div> |
|||
</div> |
|||
<div class="tb-form-row column-xs"> |
|||
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="tooltipShowDate"> |
|||
{{ 'tooltip.date' | translate }} |
|||
</mat-slide-toggle> |
|||
<div fxFlex.gt-xs fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px"> |
|||
<tb-date-format-select fxFlex excludeLastUpdateAgo includeAuto |
|||
formControlName="tooltipDateFormat"></tb-date-format-select> |
|||
<tb-font-settings formControlName="tooltipDateFont" |
|||
[previewText]="tooltipDatePreviewFn"> |
|||
</tb-font-settings> |
|||
<tb-color-input asBoxInput |
|||
colorClearButton |
|||
formControlName="tooltipDateColor"> |
|||
</tb-color-input> |
|||
</div> |
|||
</div> |
|||
<div class="tb-form-row"> |
|||
<mat-slide-toggle class="mat-slide" formControlName="tooltipDateInterval"> |
|||
<div tb-hint-tooltip-icon="{{'tooltip.show-date-time-interval-hint' | translate}}"> |
|||
{{ 'tooltip.show-date-time-interval' | translate }} |
|||
</div> |
|||
</mat-slide-toggle> |
|||
</div> |
|||
<div class="tb-form-row space-between"> |
|||
<div>{{ 'tooltip.background-color' | translate }}</div> |
|||
<tb-color-input asBoxInput |
|||
colorClearButton |
|||
formControlName="tooltipBackgroundColor"> |
|||
</tb-color-input> |
|||
</div> |
|||
<div class="tb-form-row space-between"> |
|||
<div>{{ 'tooltip.background-blur' | translate }}</div> |
|||
<mat-form-field appearance="outline" class="number" subscriptSizing="dynamic"> |
|||
<input matInput formControlName="tooltipBackgroundBlur" type="number" min="0" step="1" placeholder="{{ 'widget-config.set' | translate }}"> |
|||
<div matSuffix>px</div> |
|||
</mat-form-field> |
|||
</div> |
|||
</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"> |
|||
<div>{{ 'widgets.background.background' | translate }}</div> |
|||
<tb-background-settings formControlName="background"> |
|||
</tb-background-settings> |
|||
</div> |
|||
<div class="tb-form-row space-between"> |
|||
<div>{{ 'widget-config.card-padding' | translate }}</div> |
|||
<mat-form-field appearance="outline" subscriptSizing="dynamic"> |
|||
<input matInput formControlName="padding" placeholder="{{ 'widget-config.set' | translate }}"> |
|||
</mat-form-field> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</ng-container> |
|||
@ -0,0 +1,222 @@ |
|||
///
|
|||
/// Copyright © 2016-2024 The Thingsboard Authors
|
|||
///
|
|||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
|||
/// you may not use this file except in compliance with the License.
|
|||
/// You may obtain a copy of the License at
|
|||
///
|
|||
/// http://www.apache.org/licenses/LICENSE-2.0
|
|||
///
|
|||
/// Unless required by applicable law or agreed to in writing, software
|
|||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
|||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|||
/// See the License for the specific language governing permissions and
|
|||
/// limitations under the License.
|
|||
///
|
|||
|
|||
import { Component, Injector } from '@angular/core'; |
|||
import { |
|||
DataKey, |
|||
Datasource, |
|||
legendPositions, |
|||
legendPositionTranslationMap, |
|||
WidgetSettings, |
|||
WidgetSettingsComponent |
|||
} from '@shared/models/widget.models'; |
|||
import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; |
|||
import { Store } from '@ngrx/store'; |
|||
import { AppState } from '@core/core.state'; |
|||
import { formatValue, isDefinedAndNotNull, mergeDeep } from '@core/utils'; |
|||
import { DateFormatProcessor, DateFormatSettings } from '@shared/models/widget-settings.models'; |
|||
import { EChartsTooltipTrigger } from '../../chart/echarts-widget.models'; |
|||
import { |
|||
timeSeriesChartWidgetDefaultSettings, |
|||
TimeSeriesChartWidgetSettings |
|||
} from '@home/components/widget/lib/chart/time-series-chart-widget.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({ |
|||
selector: 'tb-time-series-chart-widget-settings', |
|||
templateUrl: './time-series-chart-widget-settings.component.html', |
|||
styleUrls: ['./../widget-settings.scss'] |
|||
}) |
|||
export class TimeSeriesChartWidgetSettingsComponent extends WidgetSettingsComponent { |
|||
|
|||
public get datasource(): Datasource { |
|||
const datasources: Datasource[] = this.widgetConfig.config.datasources; |
|||
if (datasources && datasources.length) { |
|||
return datasources[0]; |
|||
} else { |
|||
return null; |
|||
} |
|||
} |
|||
|
|||
public get yAxisIds(): TimeSeriesChartYAxisId[] { |
|||
const yAxes: TimeSeriesChartYAxes = this.timeSeriesChartWidgetSettingsForm.get('yAxes').value; |
|||
return Object.keys(yAxes); |
|||
} |
|||
|
|||
TimeSeriesChartType = TimeSeriesChartType; |
|||
|
|||
EChartsTooltipTrigger = EChartsTooltipTrigger; |
|||
|
|||
legendPositions = legendPositions; |
|||
|
|||
legendPositionTranslationMap = legendPositionTranslationMap; |
|||
|
|||
timeSeriesChartWidgetSettingsForm: UntypedFormGroup; |
|||
|
|||
tooltipValuePreviewFn = this._tooltipValuePreviewFn.bind(this); |
|||
|
|||
tooltipDatePreviewFn = this._tooltipDatePreviewFn.bind(this); |
|||
|
|||
chartType: TimeSeriesChartType = TimeSeriesChartType.default; |
|||
|
|||
constructor(protected store: Store<AppState>, |
|||
private $injector: Injector, |
|||
private fb: UntypedFormBuilder) { |
|||
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; |
|||
} |
|||
|
|||
protected onWidgetConfigSet(widgetConfig: WidgetConfigComponentData) { |
|||
const params = widgetConfig.typeParameters as any; |
|||
if (isDefinedAndNotNull(params.chartType)) { |
|||
this.chartType = params.chartType; |
|||
} |
|||
} |
|||
|
|||
protected defaultSettings(): WidgetSettings { |
|||
return mergeDeep({} as TimeSeriesChartWidgetSettings, timeSeriesChartWidgetDefaultSettings); |
|||
} |
|||
|
|||
protected onSettingsSet(settings: WidgetSettings) { |
|||
this.timeSeriesChartWidgetSettingsForm = this.fb.group({ |
|||
|
|||
yAxes: [settings.yAxes, []], |
|||
thresholds: [settings.thresholds, []], |
|||
|
|||
dataZoom: [settings.dataZoom, []], |
|||
stack: [settings.stack, []], |
|||
|
|||
xAxis: [settings.xAxis, []], |
|||
|
|||
noAggregationBarWidthSettings: [settings.noAggregationBarWidthSettings, []], |
|||
|
|||
showLegend: [settings.showLegend, []], |
|||
legendLabelFont: [settings.legendLabelFont, []], |
|||
legendLabelColor: [settings.legendLabelColor, []], |
|||
legendConfig: [settings.legendConfig, []], |
|||
|
|||
showTooltip: [settings.showTooltip, []], |
|||
tooltipTrigger: [settings.tooltipTrigger, []], |
|||
tooltipValueFont: [settings.tooltipValueFont, []], |
|||
tooltipValueColor: [settings.tooltipValueColor, []], |
|||
tooltipShowDate: [settings.tooltipShowDate, []], |
|||
tooltipDateFormat: [settings.tooltipDateFormat, []], |
|||
tooltipDateFont: [settings.tooltipDateFont, []], |
|||
tooltipDateColor: [settings.tooltipDateColor, []], |
|||
tooltipDateInterval: [settings.tooltipDateInterval, []], |
|||
|
|||
tooltipBackgroundColor: [settings.tooltipBackgroundColor, []], |
|||
tooltipBackgroundBlur: [settings.tooltipBackgroundBlur, []], |
|||
|
|||
animation: [settings.animation, []], |
|||
|
|||
background: [settings.background, []], |
|||
padding: [settings.padding, []] |
|||
}); |
|||
} |
|||
|
|||
protected validatorTriggers(): string[] { |
|||
return ['showLegend', 'showTooltip', 'tooltipShowDate']; |
|||
} |
|||
|
|||
protected updateValidators(emitEvent: boolean) { |
|||
const showLegend: boolean = this.timeSeriesChartWidgetSettingsForm.get('showLegend').value; |
|||
const showTooltip: boolean = this.timeSeriesChartWidgetSettingsForm.get('showTooltip').value; |
|||
const tooltipShowDate: boolean = this.timeSeriesChartWidgetSettingsForm.get('tooltipShowDate').value; |
|||
|
|||
if (showLegend) { |
|||
this.timeSeriesChartWidgetSettingsForm.get('legendLabelFont').enable(); |
|||
this.timeSeriesChartWidgetSettingsForm.get('legendLabelColor').enable(); |
|||
this.timeSeriesChartWidgetSettingsForm.get('legendConfig').enable(); |
|||
} else { |
|||
this.timeSeriesChartWidgetSettingsForm.get('legendLabelFont').disable(); |
|||
this.timeSeriesChartWidgetSettingsForm.get('legendLabelColor').disable(); |
|||
this.timeSeriesChartWidgetSettingsForm.get('legendConfig').disable(); |
|||
} |
|||
|
|||
if (showTooltip) { |
|||
this.timeSeriesChartWidgetSettingsForm.get('tooltipTrigger').enable(); |
|||
this.timeSeriesChartWidgetSettingsForm.get('tooltipValueFont').enable(); |
|||
this.timeSeriesChartWidgetSettingsForm.get('tooltipValueColor').enable(); |
|||
this.timeSeriesChartWidgetSettingsForm.get('tooltipShowDate').enable({emitEvent: false}); |
|||
this.timeSeriesChartWidgetSettingsForm.get('tooltipBackgroundColor').enable(); |
|||
this.timeSeriesChartWidgetSettingsForm.get('tooltipBackgroundBlur').enable(); |
|||
if (tooltipShowDate) { |
|||
this.timeSeriesChartWidgetSettingsForm.get('tooltipDateFormat').enable(); |
|||
this.timeSeriesChartWidgetSettingsForm.get('tooltipDateFont').enable(); |
|||
this.timeSeriesChartWidgetSettingsForm.get('tooltipDateColor').enable(); |
|||
this.timeSeriesChartWidgetSettingsForm.get('tooltipDateInterval').enable(); |
|||
} else { |
|||
this.timeSeriesChartWidgetSettingsForm.get('tooltipDateFormat').disable(); |
|||
this.timeSeriesChartWidgetSettingsForm.get('tooltipDateFont').disable(); |
|||
this.timeSeriesChartWidgetSettingsForm.get('tooltipDateColor').disable(); |
|||
this.timeSeriesChartWidgetSettingsForm.get('tooltipDateInterval').disable(); |
|||
} |
|||
} else { |
|||
this.timeSeriesChartWidgetSettingsForm.get('tooltipValueFont').disable(); |
|||
this.timeSeriesChartWidgetSettingsForm.get('tooltipValueColor').disable(); |
|||
this.timeSeriesChartWidgetSettingsForm.get('tooltipShowDate').disable({emitEvent: false}); |
|||
this.timeSeriesChartWidgetSettingsForm.get('tooltipDateFormat').disable(); |
|||
this.timeSeriesChartWidgetSettingsForm.get('tooltipDateFont').disable(); |
|||
this.timeSeriesChartWidgetSettingsForm.get('tooltipDateColor').disable(); |
|||
this.timeSeriesChartWidgetSettingsForm.get('tooltipDateInterval').disable(); |
|||
this.timeSeriesChartWidgetSettingsForm.get('tooltipBackgroundColor').disable(); |
|||
this.timeSeriesChartWidgetSettingsForm.get('tooltipBackgroundBlur').disable(); |
|||
} |
|||
} |
|||
|
|||
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); |
|||
} |
|||
|
|||
private _tooltipDatePreviewFn(): string { |
|||
const dateFormat: DateFormatSettings = this.timeSeriesChartWidgetSettingsForm.get('tooltipDateFormat').value; |
|||
const processor = DateFormatProcessor.fromSettings(this.$injector, dateFormat); |
|||
processor.update(Date.now()); |
|||
return processor.formatted; |
|||
} |
|||
} |
|||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue