Commit a0d7e4be05aafbc8c5cc463ed01a92e9037ed59d

Authored by Igor Kulikov
1 parent e4963de1

UI: Add bars widget. Improve tooltips and aggregation.

@@ -32,6 +32,13 @@ import org.thingsboard.server.exception.ThingsboardException; @@ -32,6 +32,13 @@ import org.thingsboard.server.exception.ThingsboardException;
32 @RequestMapping("/api") 32 @RequestMapping("/api")
33 public class DashboardController extends BaseController { 33 public class DashboardController extends BaseController {
34 34
  35 + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
  36 + @RequestMapping(value = "/dashboard/serverTime", method = RequestMethod.GET)
  37 + @ResponseBody
  38 + public long getServerTime() throws ThingsboardException {
  39 + return System.currentTimeMillis();
  40 + }
  41 +
35 @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") 42 @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
36 @RequestMapping(value = "/dashboard/{dashboardId}", method = RequestMethod.GET) 43 @RequestMapping(value = "/dashboard/{dashboardId}", method = RequestMethod.GET)
37 @ResponseBody 44 @ResponseBody
@@ -28,6 +28,7 @@ import org.thingsboard.server.extensions.core.plugin.telemetry.sub.SubscriptionT @@ -28,6 +28,7 @@ import org.thingsboard.server.extensions.core.plugin.telemetry.sub.SubscriptionT
28 @Data 28 @Data
29 public class TimeseriesSubscriptionCmd extends SubscriptionCmd { 29 public class TimeseriesSubscriptionCmd extends SubscriptionCmd {
30 30
  31 + private long startTs;
31 private long timeWindow; 32 private long timeWindow;
32 private int limit; 33 private int limit;
33 private String agg; 34 private String agg;
@@ -191,8 +191,8 @@ public class TelemetryWebsocketMsgHandler extends DefaultWebsocketMsgHandler { @@ -191,8 +191,8 @@ public class TelemetryWebsocketMsgHandler extends DefaultWebsocketMsgHandler {
191 if (cmd.getTimeWindow() > 0) { 191 if (cmd.getTimeWindow() > 0) {
192 List<String> keys = new ArrayList<>(getKeys(cmd).orElse(Collections.emptySet())); 192 List<String> keys = new ArrayList<>(getKeys(cmd).orElse(Collections.emptySet()));
193 log.debug("[{}] fetching timeseries data for last {} ms for keys: ({}) for device : {}", sessionId, cmd.getTimeWindow(), cmd.getKeys(), cmd.getDeviceId()); 193 log.debug("[{}] fetching timeseries data for last {} ms for keys: ({}) for device : {}", sessionId, cmd.getTimeWindow(), cmd.getKeys(), cmd.getDeviceId());
194 - long endTs = System.currentTimeMillis();  
195 - startTs = endTs - cmd.getTimeWindow(); 194 + startTs = cmd.getStartTs();
  195 + long endTs = cmd.getStartTs() + cmd.getTimeWindow();
196 List<TsKvQuery> queries = keys.stream().map(key -> new BaseTsKvQuery(key, startTs, endTs, getLimit(cmd.getLimit()), getAggregation(cmd.getAgg()))).collect(Collectors.toList()); 196 List<TsKvQuery> queries = keys.stream().map(key -> new BaseTsKvQuery(key, startTs, endTs, getLimit(cmd.getLimit()), getAggregation(cmd.getAgg()))).collect(Collectors.toList());
197 ctx.loadTimeseries(deviceId, queries, getSubscriptionCallback(sessionRef, cmd, sessionId, deviceId, startTs, keys)); 197 ctx.loadTimeseries(deviceId, queries, getSubscriptionCallback(sessionRef, cmd, sessionId, deviceId, startTs, keys));
198 } else { 198 } else {
@@ -234,7 +234,7 @@ public class TelemetryWebsocketMsgHandler extends DefaultWebsocketMsgHandler { @@ -234,7 +234,7 @@ public class TelemetryWebsocketMsgHandler extends DefaultWebsocketMsgHandler {
234 return new PluginCallback<List<TsKvEntry>>() { 234 return new PluginCallback<List<TsKvEntry>>() {
235 @Override 235 @Override
236 public void onSuccess(PluginContext ctx, List<TsKvEntry> data) { 236 public void onSuccess(PluginContext ctx, List<TsKvEntry> data) {
237 - sendWsMsg(ctx, sessionRef, new SubscriptionUpdate(cmd.getCmdId(), startTs, data)); 237 + sendWsMsg(ctx, sessionRef, new SubscriptionUpdate(cmd.getCmdId(), data));
238 238
239 Map<String, Long> subState = new HashMap<>(keys.size()); 239 Map<String, Long> subState = new HashMap<>(keys.size());
240 keys.forEach(key -> subState.put(key, startTs)); 240 keys.forEach(key -> subState.put(key, startTs));
@@ -26,16 +26,10 @@ public class SubscriptionUpdate { @@ -26,16 +26,10 @@ public class SubscriptionUpdate {
26 private int errorCode; 26 private int errorCode;
27 private String errorMsg; 27 private String errorMsg;
28 private Map<String, List<Object>> data; 28 private Map<String, List<Object>> data;
29 - private long serverStartTs;  
30 29
31 public SubscriptionUpdate(int subscriptionId, List<TsKvEntry> data) { 30 public SubscriptionUpdate(int subscriptionId, List<TsKvEntry> data) {
32 - this(subscriptionId, 0L, data);  
33 - }  
34 -  
35 - public SubscriptionUpdate(int subscriptionId, long serverStartTs, List<TsKvEntry> data) {  
36 super(); 31 super();
37 this.subscriptionId = subscriptionId; 32 this.subscriptionId = subscriptionId;
38 - this.serverStartTs = serverStartTs;  
39 this.data = new TreeMap<>(); 33 this.data = new TreeMap<>();
40 for (TsKvEntry tsEntry : data) { 34 for (TsKvEntry tsEntry : data) {
41 List<Object> values = this.data.get(tsEntry.getKey()); 35 List<Object> values = this.data.get(tsEntry.getKey());
@@ -95,13 +89,9 @@ public class SubscriptionUpdate { @@ -95,13 +89,9 @@ public class SubscriptionUpdate {
95 return errorMsg; 89 return errorMsg;
96 } 90 }
97 91
98 - public long getServerStartTs() {  
99 - return serverStartTs;  
100 - }  
101 -  
102 @Override 92 @Override
103 public String toString() { 93 public String toString() {
104 return "SubscriptionUpdate [subscriptionId=" + subscriptionId + ", errorCode=" + errorCode + ", errorMsg=" + errorMsg + ", data=" 94 return "SubscriptionUpdate [subscriptionId=" + subscriptionId + ", errorCode=" + errorCode + ", errorMsg=" + errorMsg + ", data="
105 - + data + ", serverStartTs=" + serverStartTs+ "]"; 95 + + data + "]";
106 } 96 }
107 } 97 }
@@ -22,6 +22,7 @@ function DashboardService($http, $q) { @@ -22,6 +22,7 @@ function DashboardService($http, $q) {
22 var service = { 22 var service = {
23 assignDashboardToCustomer: assignDashboardToCustomer, 23 assignDashboardToCustomer: assignDashboardToCustomer,
24 getCustomerDashboards: getCustomerDashboards, 24 getCustomerDashboards: getCustomerDashboards,
  25 + getServerTimeDiff: getServerTimeDiff,
25 getDashboard: getDashboard, 26 getDashboard: getDashboard,
26 getTenantDashboards: getTenantDashboards, 27 getTenantDashboards: getTenantDashboards,
27 deleteDashboard: deleteDashboard, 28 deleteDashboard: deleteDashboard,
@@ -71,6 +72,21 @@ function DashboardService($http, $q) { @@ -71,6 +72,21 @@ function DashboardService($http, $q) {
71 return deferred.promise; 72 return deferred.promise;
72 } 73 }
73 74
  75 + function getServerTimeDiff() {
  76 + var deferred = $q.defer();
  77 + var url = '/api/dashboard/serverTime';
  78 + var ct1 = Date.now();
  79 + $http.get(url, null).then(function success(response) {
  80 + var ct2 = Date.now();
  81 + var st = response.data;
  82 + var stDiff = Math.ceil(st - (ct1+ct2)/2);
  83 + deferred.resolve(stDiff);
  84 + }, function fail() {
  85 + deferred.reject();
  86 + });
  87 + return deferred.promise;
  88 + }
  89 +
74 function getDashboard(dashboardId) { 90 function getDashboard(dashboardId) {
75 var deferred = $q.defer(); 91 var deferred = $q.defer();
76 var url = '/api/dashboard/' + dashboardId; 92 var url = '/api/dashboard/' + dashboardId;
@@ -16,31 +16,26 @@ @@ -16,31 +16,26 @@
16 16
17 export default class DataAggregator { 17 export default class DataAggregator {
18 18
19 - constructor(onDataCb, limit, aggregationType, timeWindow, types, $timeout, $filter) { 19 + constructor(onDataCb, tsKeyNames, startTs, limit, aggregationType, timeWindow, interval, types, $timeout, $filter) {
20 this.onDataCb = onDataCb; 20 this.onDataCb = onDataCb;
  21 + this.tsKeyNames = tsKeyNames;
  22 + this.startTs = startTs;
21 this.aggregationType = aggregationType; 23 this.aggregationType = aggregationType;
22 this.types = types; 24 this.types = types;
23 this.$timeout = $timeout; 25 this.$timeout = $timeout;
24 this.$filter = $filter; 26 this.$filter = $filter;
25 this.dataReceived = false; 27 this.dataReceived = false;
26 this.noAggregation = aggregationType === types.aggregation.none.value; 28 this.noAggregation = aggregationType === types.aggregation.none.value;
27 - var interval = Math.floor(timeWindow / limit);  
28 - if (!this.noAggregation) {  
29 - this.interval = Math.max(interval, 1000);  
30 - this.limit = Math.ceil(interval/this.interval * limit);  
31 - this.timeWindow = this.interval * this.limit;  
32 - } else {  
33 - this.limit = limit;  
34 - this.timeWindow = interval * this.limit;  
35 - this.interval = 1000;  
36 - } 29 + this.limit = limit;
  30 + this.timeWindow = timeWindow;
  31 + this.interval = interval;
37 this.aggregationTimeout = this.interval; 32 this.aggregationTimeout = this.interval;
38 switch (aggregationType) { 33 switch (aggregationType) {
39 case types.aggregation.min.value: 34 case types.aggregation.min.value:
40 this.aggFunction = min; 35 this.aggFunction = min;
41 break; 36 break;
42 case types.aggregation.max.value: 37 case types.aggregation.max.value:
43 - this.aggFunction = max 38 + this.aggFunction = max;
44 break; 39 break;
45 case types.aggregation.avg.value: 40 case types.aggregation.avg.value:
46 this.aggFunction = avg; 41 this.aggFunction = avg;
@@ -59,42 +54,56 @@ export default class DataAggregator { @@ -59,42 +54,56 @@ export default class DataAggregator {
59 } 54 }
60 } 55 }
61 56
62 - onData(data) { 57 + onData(data, update, history) {
63 if (!this.dataReceived) { 58 if (!this.dataReceived) {
64 this.elapsed = 0; 59 this.elapsed = 0;
65 this.dataReceived = true; 60 this.dataReceived = true;
66 - this.startTs = data.serverStartTs;  
67 this.endTs = this.startTs + this.timeWindow; 61 this.endTs = this.startTs + this.timeWindow;
68 - this.aggregationMap = processAggregatedData(data.data, this.aggregationType === this.types.aggregation.count.value, this.noAggregation);  
69 - this.onInterval(currentTime()); 62 + if (update) {
  63 + this.aggregationMap = {};
  64 + updateAggregatedData(this.aggregationMap, this.aggregationType === this.types.aggregation.count.value,
  65 + this.noAggregation, this.aggFunction, data.data, this.interval, this.startTs);
  66 + } else {
  67 + this.aggregationMap = processAggregatedData(data.data, this.aggregationType === this.types.aggregation.count.value, this.noAggregation);
  68 + }
  69 + this.onInterval(currentTime(), history);
70 } else { 70 } else {
71 updateAggregatedData(this.aggregationMap, this.aggregationType === this.types.aggregation.count.value, 71 updateAggregatedData(this.aggregationMap, this.aggregationType === this.types.aggregation.count.value,
72 this.noAggregation, this.aggFunction, data.data, this.interval, this.startTs); 72 this.noAggregation, this.aggFunction, data.data, this.interval, this.startTs);
  73 + if (history) {
  74 + this.onInterval(currentTime(), history);
  75 + }
73 } 76 }
74 } 77 }
75 78
76 - onInterval(startedTime) { 79 + onInterval(startedTime, history) {
77 var now = currentTime(); 80 var now = currentTime();
78 this.elapsed += now - startedTime; 81 this.elapsed += now - startedTime;
79 if (this.intervalTimeoutHandle) { 82 if (this.intervalTimeoutHandle) {
80 this.$timeout.cancel(this.intervalTimeoutHandle); 83 this.$timeout.cancel(this.intervalTimeoutHandle);
81 this.intervalTimeoutHandle = null; 84 this.intervalTimeoutHandle = null;
82 } 85 }
83 - var delta = Math.floor(this.elapsed / this.interval);  
84 - if (delta || !this.data) {  
85 - this.startTs += delta * this.interval;  
86 - this.endTs += delta * this.interval;  
87 - this.data = toData(this.aggregationMap, this.startTs, this.endTs, this.$filter, this.limit);  
88 - this.elapsed = this.elapsed - delta * this.interval; 86 + if (!history) {
  87 + var delta = Math.floor(this.elapsed / this.interval);
  88 + if (delta || !this.data) {
  89 + this.startTs += delta * this.interval;
  90 + this.endTs += delta * this.interval;
  91 + this.data = toData(this.tsKeyNames, this.aggregationMap, this.startTs, this.endTs, this.$filter, this.limit);
  92 + this.elapsed = this.elapsed - delta * this.interval;
  93 + }
  94 + } else {
  95 + this.data = toData(this.tsKeyNames, this.aggregationMap, this.startTs, this.endTs, this.$filter, this.limit);
89 } 96 }
90 if (this.onDataCb) { 97 if (this.onDataCb) {
91 this.onDataCb(this.data, this.startTs, this.endTs); 98 this.onDataCb(this.data, this.startTs, this.endTs);
92 } 99 }
93 100
94 var self = this; 101 var self = this;
95 - this.intervalTimeoutHandle = this.$timeout(function() {  
96 - self.onInterval(now);  
97 - }, this.aggregationTimeout, false); 102 + if (!history) {
  103 + this.intervalTimeoutHandle = this.$timeout(function() {
  104 + self.onInterval(now);
  105 + }, this.aggregationTimeout, false);
  106 + }
98 } 107 }
99 108
100 reset() { 109 reset() {
@@ -172,12 +181,12 @@ function updateAggregatedData(aggregationMap, isCount, noAggregation, aggFunctio @@ -172,12 +181,12 @@ function updateAggregatedData(aggregationMap, isCount, noAggregation, aggFunctio
172 } 181 }
173 } 182 }
174 183
175 -function toData(aggregationMap, startTs, endTs, $filter, limit) { 184 +function toData(tsKeyNames, aggregationMap, startTs, endTs, $filter, limit) {
176 var data = {}; 185 var data = {};
  186 + for (var k in tsKeyNames) {
  187 + data[tsKeyNames[k]] = [];
  188 + }
177 for (var key in aggregationMap) { 189 for (var key in aggregationMap) {
178 - if (!data[key]) {  
179 - data[key] = [];  
180 - }  
181 var aggKeyData = aggregationMap[key]; 190 var aggKeyData = aggregationMap[key];
182 var keyData = data[key]; 191 var keyData = data[key];
183 for (var aggTimestamp in aggKeyData) { 192 for (var aggTimestamp in aggKeyData) {
@@ -185,7 +194,7 @@ function toData(aggregationMap, startTs, endTs, $filter, limit) { @@ -185,7 +194,7 @@ function toData(aggregationMap, startTs, endTs, $filter, limit) {
185 delete aggKeyData[aggTimestamp]; 194 delete aggKeyData[aggTimestamp];
186 } else if (aggTimestamp <= endTs) { 195 } else if (aggTimestamp <= endTs) {
187 var aggData = aggKeyData[aggTimestamp]; 196 var aggData = aggKeyData[aggTimestamp];
188 - var kvPair = [aggTimestamp, aggData.aggValue]; 197 + var kvPair = [Number(aggTimestamp), aggData.aggValue];
189 keyData.push(kvPair); 198 keyData.push(kvPair);
190 } 199 }
191 } 200 }
@@ -108,9 +108,9 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic @@ -108,9 +108,9 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic
108 datasourceSubscription.subscriptionTimewindow.fixedWindow; 108 datasourceSubscription.subscriptionTimewindow.fixedWindow;
109 var realtime = datasourceSubscription.subscriptionTimewindow && 109 var realtime = datasourceSubscription.subscriptionTimewindow &&
110 datasourceSubscription.subscriptionTimewindow.realtimeWindowMs; 110 datasourceSubscription.subscriptionTimewindow.realtimeWindowMs;
111 - var dataGenFunction = null;  
112 var timer; 111 var timer;
113 var frequency; 112 var frequency;
  113 + var dataAggregator;
114 114
115 var subscription = { 115 var subscription = {
116 addListener: addListener, 116 addListener: addListener,
@@ -131,19 +131,20 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic @@ -131,19 +131,20 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic
131 dataKey.index = i; 131 dataKey.index = i;
132 var key; 132 var key;
133 if (datasourceType === types.datasourceType.function) { 133 if (datasourceType === types.datasourceType.function) {
134 - key = utils.objectHashCode(dataKey);  
135 if (!dataKey.func) { 134 if (!dataKey.func) {
136 dataKey.func = new Function("time", "prevValue", dataKey.funcBody); 135 dataKey.func = new Function("time", "prevValue", dataKey.funcBody);
137 } 136 }
138 - datasourceData[key] = {  
139 - data: []  
140 - };  
141 - dataKeys[key] = dataKey;  
142 - } else if (datasourceType === types.datasourceType.device) {  
143 - key = dataKey.name + '_' + dataKey.type; 137 + } else {
144 if (dataKey.postFuncBody && !dataKey.postFunc) { 138 if (dataKey.postFuncBody && !dataKey.postFunc) {
145 dataKey.postFunc = new Function("time", "value", "prevValue", dataKey.postFuncBody); 139 dataKey.postFunc = new Function("time", "value", "prevValue", dataKey.postFuncBody);
146 } 140 }
  141 + }
  142 + if (datasourceType === types.datasourceType.device || datasourceSubscription.type === types.widgetType.timeseries.value) {
  143 + if (datasourceType === types.datasourceType.function) {
  144 + key = dataKey.name + '_' + dataKey.index + '_' + dataKey.type;
  145 + } else {
  146 + key = dataKey.name + '_' + dataKey.type;
  147 + }
147 var dataKeysList = dataKeys[key]; 148 var dataKeysList = dataKeys[key];
148 if (!dataKeysList) { 149 if (!dataKeysList) {
149 dataKeysList = []; 150 dataKeysList = [];
@@ -153,24 +154,19 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic @@ -153,24 +154,19 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic
153 datasourceData[key + '_' + index] = { 154 datasourceData[key + '_' + index] = {
154 data: [] 155 data: []
155 }; 156 };
  157 + } else {
  158 + key = utils.objectHashCode(dataKey);
  159 + datasourceData[key] = {
  160 + data: []
  161 + };
  162 + dataKeys[key] = dataKey;
156 } 163 }
157 dataKey.key = key; 164 dataKey.key = key;
158 } 165 }
159 if (datasourceType === types.datasourceType.function) { 166 if (datasourceType === types.datasourceType.function) {
160 frequency = 1000; 167 frequency = 1000;
161 if (datasourceSubscription.type === types.widgetType.timeseries.value) { 168 if (datasourceSubscription.type === types.widgetType.timeseries.value) {
162 - dataGenFunction = generateSeries;  
163 - var window;  
164 - if (realtime) {  
165 - window = datasourceSubscription.subscriptionTimewindow.realtimeWindowMs;  
166 - } else {  
167 - window = datasourceSubscription.subscriptionTimewindow.fixedWindow.endTimeMs -  
168 - datasourceSubscription.subscriptionTimewindow.fixedWindow.startTimeMs;  
169 - }  
170 - frequency = window / 1000 * 20;  
171 - } else if (datasourceSubscription.type === types.widgetType.latest.value) {  
172 - dataGenFunction = generateLatest;  
173 - frequency = 1000; 169 + frequency = Math.min(datasourceSubscription.subscriptionTimewindow.aggregation.interval, 5000);
174 } 170 }
175 } 171 }
176 } 172 }
@@ -193,14 +189,7 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic @@ -193,14 +189,7 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic
193 function syncListener(listener) { 189 function syncListener(listener) {
194 var key; 190 var key;
195 var dataKey; 191 var dataKey;
196 - if (datasourceType === types.datasourceType.function) {  
197 - for (key in dataKeys) {  
198 - dataKey = dataKeys[key];  
199 - listener.dataUpdated(datasourceData[key],  
200 - listener.datasourceIndex,  
201 - dataKey.index);  
202 - }  
203 - } else if (datasourceType === types.datasourceType.device) { 192 + if (datasourceType === types.datasourceType.device || datasourceSubscription.type === types.widgetType.timeseries.value) {
204 for (key in dataKeys) { 193 for (key in dataKeys) {
205 var dataKeysList = dataKeys[key]; 194 var dataKeysList = dataKeys[key];
206 for (var i = 0; i < dataKeysList.length; i++) { 195 for (var i = 0; i < dataKeysList.length; i++) {
@@ -211,6 +200,13 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic @@ -211,6 +200,13 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic
211 dataKey.index); 200 dataKey.index);
212 } 201 }
213 } 202 }
  203 + } else {
  204 + for (key in dataKeys) {
  205 + dataKey = dataKeys[key];
  206 + listener.dataUpdated(datasourceData[key],
  207 + listener.datasourceIndex,
  208 + dataKey.index);
  209 + }
214 } 210 }
215 } 211 }
216 212
@@ -218,7 +214,10 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic @@ -218,7 +214,10 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic
218 if (history && !hasListeners()) { 214 if (history && !hasListeners()) {
219 return; 215 return;
220 } 216 }
221 - //$log.debug("started!"); 217 + var subsTw = datasourceSubscription.subscriptionTimewindow;
  218 + var tsKeyNames = [];
  219 + var dataKey;
  220 +
222 if (datasourceType === types.datasourceType.device) { 221 if (datasourceType === types.datasourceType.device) {
223 222
224 //send subscribe command 223 //send subscribe command
@@ -228,12 +227,13 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic @@ -228,12 +227,13 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic
228 227
229 for (var key in dataKeys) { 228 for (var key in dataKeys) {
230 var dataKeysList = dataKeys[key]; 229 var dataKeysList = dataKeys[key];
231 - var dataKey = dataKeysList[0]; 230 + dataKey = dataKeysList[0];
232 if (dataKey.type === types.dataKeyType.timeseries) { 231 if (dataKey.type === types.dataKeyType.timeseries) {
233 if (tsKeys.length > 0) { 232 if (tsKeys.length > 0) {
234 tsKeys += ','; 233 tsKeys += ',';
235 } 234 }
236 tsKeys += dataKey.name; 235 tsKeys += dataKey.name;
  236 + tsKeyNames.push(dataKey.name);
237 } else if (dataKey.type === types.dataKeyType.attribute) { 237 } else if (dataKey.type === types.dataKeyType.attribute) {
238 if (attrKeys.length > 0) { 238 if (attrKeys.length > 0) {
239 attrKeys += ','; 239 attrKeys += ',';
@@ -252,10 +252,10 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic @@ -252,10 +252,10 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic
252 var historyCommand = { 252 var historyCommand = {
253 deviceId: datasourceSubscription.deviceId, 253 deviceId: datasourceSubscription.deviceId,
254 keys: tsKeys, 254 keys: tsKeys,
255 - startTs: datasourceSubscription.subscriptionTimewindow.fixedWindow.startTimeMs,  
256 - endTs: datasourceSubscription.subscriptionTimewindow.fixedWindow.endTimeMs,  
257 - limit: datasourceSubscription.subscriptionTimewindow.aggregation.limit,  
258 - agg: datasourceSubscription.subscriptionTimewindow.aggregation.type 255 + startTs: subsTw.fixedWindow.startTimeMs,
  256 + endTs: subsTw.fixedWindow.endTimeMs,
  257 + limit: subsTw.aggregation.limit,
  258 + agg: subsTw.aggregation.type
259 }; 259 };
260 260
261 subscriber = { 261 subscriber = {
@@ -287,16 +287,20 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic @@ -287,16 +287,20 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic
287 }; 287 };
288 288
289 if (datasourceSubscription.type === types.widgetType.timeseries.value) { 289 if (datasourceSubscription.type === types.widgetType.timeseries.value) {
290 - subscriptionCommand.timeWindow = datasourceSubscription.subscriptionTimewindow.realtimeWindowMs;  
291 - subscriptionCommand.limit = datasourceSubscription.subscriptionTimewindow.aggregation.limit;  
292 - subscriptionCommand.agg = datasourceSubscription.subscriptionTimewindow.aggregation.type;  
293 - var dataAggregator = new DataAggregator( 290 + subscriptionCommand.startTs = subsTw.startTs;
  291 + subscriptionCommand.timeWindow = subsTw.aggregation.timeWindow;
  292 + subscriptionCommand.limit = subsTw.aggregation.limit;
  293 + subscriptionCommand.agg = subsTw.aggregation.type;
  294 + dataAggregator = new DataAggregator(
294 function(data, startTs, endTs) { 295 function(data, startTs, endTs) {
295 onData(data, types.dataKeyType.timeseries, startTs, endTs); 296 onData(data, types.dataKeyType.timeseries, startTs, endTs);
296 }, 297 },
297 - subscriptionCommand.limit,  
298 - subscriptionCommand.agg,  
299 - subscriptionCommand.timeWindow, 298 + tsKeyNames,
  299 + subsTw.startTs,
  300 + subsTw.aggregation.limit,
  301 + subsTw.aggregation.type,
  302 + subsTw.aggregation.timeWindow,
  303 + subsTw.aggregation.interval,
300 types, 304 types,
301 $timeout, 305 $timeout,
302 $filter 306 $filter
@@ -308,9 +312,6 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic @@ -308,9 +312,6 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic
308 dataAggregator.reset(); 312 dataAggregator.reset();
309 onReconnected(); 313 onReconnected();
310 } 314 }
311 - subscriber.onDestroy = function() {  
312 - dataAggregator.destroy();  
313 - }  
314 } else { 315 } else {
315 subscriber.onReconnected = function() { 316 subscriber.onReconnected = function() {
316 onReconnected(); 317 onReconnected();
@@ -353,7 +354,30 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic @@ -353,7 +354,30 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic
353 354
354 } 355 }
355 356
356 - } else if (dataGenFunction) { 357 + } else if (datasourceType === types.datasourceType.function) {
  358 + if (datasourceSubscription.type === types.widgetType.timeseries.value) {
  359 + for (key in dataKeys) {
  360 + var dataKeyList = dataKeys[key];
  361 + for (var index = 0; index < dataKeyList.length; index++) {
  362 + dataKey = dataKeyList[index];
  363 + tsKeyNames.push(dataKey.name+'_'+dataKey.index);
  364 + }
  365 + }
  366 + dataAggregator = new DataAggregator(
  367 + function (data, startTs, endTs) {
  368 + onData(data, types.dataKeyType.function, startTs, endTs);
  369 + },
  370 + tsKeyNames,
  371 + subsTw.startTs,
  372 + subsTw.aggregation.limit,
  373 + subsTw.aggregation.type,
  374 + subsTw.aggregation.timeWindow,
  375 + subsTw.aggregation.interval,
  376 + types,
  377 + $timeout,
  378 + $filter
  379 + );
  380 + }
357 if (history) { 381 if (history) {
358 onTick(); 382 onTick();
359 } else { 383 } else {
@@ -377,30 +401,17 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic @@ -377,30 +401,17 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic
377 } 401 }
378 subscribers = {}; 402 subscribers = {};
379 } 403 }
380 - }  
381 -  
382 - function boundToInterval(data, timewindowMs) {  
383 - if (data.length > 1) {  
384 - var start = data[0][0];  
385 - var end = data[data.length - 1][0];  
386 - var i = 0;  
387 - var currentInterval = end - start;  
388 - while (currentInterval > timewindowMs && i < data.length - 2) {  
389 - i++;  
390 - start = data[i][0];  
391 - currentInterval = end - start;  
392 - }  
393 - if (i > 1) {  
394 - data.splice(0, i - 1);  
395 - } 404 + if (dataAggregator) {
  405 + dataAggregator.destroy();
  406 + dataAggregator = null;
396 } 407 }
397 - return data;  
398 } 408 }
399 409
400 - function generateSeries(dataKey, startTime, endTime) { 410 + function generateSeries(dataKey, index, startTime, endTime) {
401 var data = []; 411 var data = [];
402 var prevSeries; 412 var prevSeries;
403 - var datasourceKeyData = datasourceData[dataKey.key].data; 413 + var datasourceDataKey = dataKey.key + '_' + index;
  414 + var datasourceKeyData = datasourceData[datasourceDataKey].data;
404 if (datasourceKeyData.length > 0) { 415 if (datasourceKeyData.length > 0) {
405 prevSeries = datasourceKeyData[datasourceKeyData.length - 1]; 416 prevSeries = datasourceKeyData[datasourceKeyData.length - 1];
406 } else { 417 } else {
@@ -417,18 +428,7 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic @@ -417,18 +428,7 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic
417 if (data.length > 0) { 428 if (data.length > 0) {
418 dataKey.lastUpdateTime = data[data.length - 1][0]; 429 dataKey.lastUpdateTime = data[data.length - 1][0];
419 } 430 }
420 - if (realtime) {  
421 - datasourceData[dataKey.key].data = boundToInterval(datasourceKeyData.concat(data),  
422 - datasourceSubscription.subscriptionTimewindow.realtimeWindowMs);  
423 - } else {  
424 - datasourceData[dataKey.key].data = data;  
425 - }  
426 - for (var i in listeners) {  
427 - var listener = listeners[i];  
428 - listener.dataUpdated(datasourceData[dataKey.key],  
429 - listener.datasourceIndex,  
430 - dataKey.index);  
431 - } 431 + return data;
432 } 432 }
433 433
434 function generateLatest(dataKey) { 434 function generateLatest(dataKey) {
@@ -458,23 +458,32 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic @@ -458,23 +458,32 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic
458 if (datasourceSubscription.type === types.widgetType.timeseries.value) { 458 if (datasourceSubscription.type === types.widgetType.timeseries.value) {
459 var startTime; 459 var startTime;
460 var endTime; 460 var endTime;
  461 + var generatedData = {
  462 + data: {
  463 + }
  464 + };
461 for (key in dataKeys) { 465 for (key in dataKeys) {
462 - var dataKey = dataKeys[key];  
463 - if (!startTime) {  
464 - if (realtime) {  
465 - endTime = (new Date).getTime();  
466 - if (dataKey.lastUpdateTime) {  
467 - startTime = dataKey.lastUpdateTime + frequency; 466 + var dataKeyList = dataKeys[key];
  467 + for (var index = 0; index < dataKeyList.length; index ++) {
  468 + var dataKey = dataKeyList[index];
  469 + if (!startTime) {
  470 + if (realtime) {
  471 + if (dataKey.lastUpdateTime) {
  472 + startTime = dataKey.lastUpdateTime + frequency
  473 + } else {
  474 + startTime = datasourceSubscription.subscriptionTimewindow.startTs;
  475 + }
  476 + endTime = startTime + datasourceSubscription.subscriptionTimewindow.realtimeWindowMs;
468 } else { 477 } else {
469 - startTime = endTime - datasourceSubscription.subscriptionTimewindow.realtimeWindowMs; 478 + startTime = datasourceSubscription.subscriptionTimewindow.fixedWindow.startTimeMs;
  479 + endTime = datasourceSubscription.subscriptionTimewindow.fixedWindow.endTimeMs;
470 } 480 }
471 - } else {  
472 - startTime = datasourceSubscription.subscriptionTimewindow.fixedWindow.startTimeMs;  
473 - endTime = datasourceSubscription.subscriptionTimewindow.fixedWindow.endTimeMs;  
474 } 481 }
  482 + var data = generateSeries(dataKey, index, startTime, endTime);
  483 + generatedData.data[dataKey.name+'_'+dataKey.index] = data;
475 } 484 }
476 - generateSeries(dataKey, startTime, endTime);  
477 } 485 }
  486 + dataAggregator.onData(generatedData, true, history);
478 } else if (datasourceSubscription.type === types.widgetType.latest.value) { 487 } else if (datasourceSubscription.type === types.widgetType.latest.value) {
479 for (key in dataKeys) { 488 for (key in dataKeys) {
480 generateLatest(dataKeys[key]); 489 generateLatest(dataKeys[key]);
@@ -568,8 +577,6 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic @@ -568,8 +577,6 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic
568 } 577 }
569 if (data.length > 0 || (startTs && endTs)) { 578 if (data.length > 0 || (startTs && endTs)) {
570 datasourceData[datasourceKey].data = data; 579 datasourceData[datasourceKey].data = data;
571 - datasourceData[datasourceKey].startTs = startTs;  
572 - datasourceData[datasourceKey].endTs = endTs;  
573 for (var i2 in listeners) { 580 for (var i2 in listeners) {
574 var listener = listeners[i2]; 581 var listener = listeners[i2];
575 listener.dataUpdated(datasourceData[datasourceKey], 582 listener.dataUpdated(datasourceData[datasourceKey],
@@ -68,6 +68,7 @@ function Dashboard() { @@ -68,6 +68,7 @@ function Dashboard() {
68 prepareDashboardContextMenu: '&?', 68 prepareDashboardContextMenu: '&?',
69 prepareWidgetContextMenu: '&?', 69 prepareWidgetContextMenu: '&?',
70 loadWidgets: '&?', 70 loadWidgets: '&?',
  71 + getStDiff: '&?',
71 onInit: '&?', 72 onInit: '&?',
72 onInitFailed: '&?', 73 onInitFailed: '&?',
73 dashboardStyle: '=?' 74 dashboardStyle: '=?'
@@ -94,6 +95,8 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, $ @@ -94,6 +95,8 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, $
94 95
95 vm.gridster = null; 96 vm.gridster = null;
96 97
  98 + vm.stDiff = 0;
  99 +
97 vm.isMobileDisabled = angular.isDefined(vm.isMobileDisabled) ? vm.isMobileDisabled : false; 100 vm.isMobileDisabled = angular.isDefined(vm.isMobileDisabled) ? vm.isMobileDisabled : false;
98 101
99 vm.dashboardLoading = true; 102 vm.dashboardLoading = true;
@@ -302,7 +305,28 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, $ @@ -302,7 +305,28 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, $
302 }); 305 });
303 }); 306 });
304 307
305 - loadDashboard(); 308 + loadStDiff();
  309 +
  310 + function loadStDiff() {
  311 + if (vm.getStDiff) {
  312 + var promise = vm.getStDiff();
  313 + if (promise) {
  314 + promise.then(function (stDiff) {
  315 + vm.stDiff = stDiff;
  316 + loadDashboard();
  317 + }, function () {
  318 + vm.stDiff = 0;
  319 + loadDashboard();
  320 + });
  321 + } else {
  322 + vm.stDiff = 0;
  323 + loadDashboard();
  324 + }
  325 + } else {
  326 + vm.stDiff = 0;
  327 + loadDashboard();
  328 + }
  329 + }
306 330
307 function loadDashboard() { 331 function loadDashboard() {
308 resetWidgetClick(); 332 resetWidgetClick();
@@ -93,7 +93,7 @@ @@ -93,7 +93,7 @@
93 </div> 93 </div>
94 <div flex layout="column" class="tb-widget-content"> 94 <div flex layout="column" class="tb-widget-content">
95 <div flex tb-widget 95 <div flex tb-widget
96 - locals="{ visibleRect: vm.visibleRect, widget: widget, deviceAliasList: vm.deviceAliasList, isEdit: vm.isEdit }"> 96 + locals="{ visibleRect: vm.visibleRect, widget: widget, deviceAliasList: vm.deviceAliasList, isEdit: vm.isEdit, stDiff: vm.stDiff }">
97 </div> 97 </div>
98 </div> 98 </div>
99 </div> 99 </div>
@@ -20,7 +20,7 @@ import 'javascript-detect-element-resize/detect-element-resize'; @@ -20,7 +20,7 @@ import 'javascript-detect-element-resize/detect-element-resize';
20 20
21 /*@ngInject*/ 21 /*@ngInject*/
22 export default function WidgetController($scope, $timeout, $window, $element, $q, $log, $injector, tbRaf, types, utils, 22 export default function WidgetController($scope, $timeout, $window, $element, $q, $log, $injector, tbRaf, types, utils,
23 - datasourceService, deviceService, visibleRect, isEdit, widget, deviceAliasList, widgetType) { 23 + datasourceService, deviceService, visibleRect, isEdit, stDiff, widget, deviceAliasList, widgetType) {
24 24
25 var vm = this; 25 var vm = this;
26 26
@@ -46,7 +46,6 @@ export default function WidgetController($scope, $timeout, $window, $element, $q @@ -46,7 +46,6 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
46 realtimeWindowMs: null, 46 realtimeWindowMs: null,
47 aggregation: null 47 aggregation: null
48 }; 48 };
49 - var dataUpdateTimer = null;  
50 var dataUpdateCaf = null; 49 var dataUpdateCaf = null;
51 50
52 /* 51 /*
@@ -72,7 +71,9 @@ export default function WidgetController($scope, $timeout, $window, $element, $q @@ -72,7 +71,9 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
72 settings: widget.config.settings, 71 settings: widget.config.settings,
73 datasources: widget.config.datasources, 72 datasources: widget.config.datasources,
74 data: [], 73 data: [],
75 - timeWindow: {}, 74 + timeWindow: {
  75 + stDiff: stDiff
  76 + },
76 timewindowFunctions: { 77 timewindowFunctions: {
77 onUpdateTimewindow: onUpdateTimewindow, 78 onUpdateTimewindow: onUpdateTimewindow,
78 onResetTimewindow: onResetTimewindow 79 onResetTimewindow: onResetTimewindow
@@ -154,10 +155,11 @@ export default function WidgetController($scope, $timeout, $window, $element, $q @@ -154,10 +155,11 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
154 } 155 }
155 } 156 }
156 157
157 - function updateTimewindow(startTs, endTs) { 158 + function updateTimewindow() {
  159 + widgetContext.timeWindow.interval = subscriptionTimewindow.aggregation.interval || 1000;
158 if (subscriptionTimewindow.realtimeWindowMs) { 160 if (subscriptionTimewindow.realtimeWindowMs) {
159 - widgetContext.timeWindow.maxTime = endTs || (new Date).getTime();  
160 - widgetContext.timeWindow.minTime = startTs || (widgetContext.timeWindow.maxTime - subscriptionTimewindow.realtimeWindowMs); 161 + widgetContext.timeWindow.maxTime = (new Date).getTime() + widgetContext.timeWindow.stDiff;
  162 + widgetContext.timeWindow.minTime = widgetContext.timeWindow.maxTime - subscriptionTimewindow.realtimeWindowMs;
161 } else if (subscriptionTimewindow.fixedWindow) { 163 } else if (subscriptionTimewindow.fixedWindow) {
162 widgetContext.timeWindow.maxTime = subscriptionTimewindow.fixedWindow.endTimeMs; 164 widgetContext.timeWindow.maxTime = subscriptionTimewindow.fixedWindow.endTimeMs;
163 widgetContext.timeWindow.minTime = subscriptionTimewindow.fixedWindow.startTimeMs; 165 widgetContext.timeWindow.minTime = subscriptionTimewindow.fixedWindow.startTimeMs;
@@ -165,10 +167,6 @@ export default function WidgetController($scope, $timeout, $window, $element, $q @@ -165,10 +167,6 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
165 } 167 }
166 168
167 function onDataUpdated() { 169 function onDataUpdated() {
168 - if (dataUpdateTimer) {  
169 - $timeout.cancel(dataUpdateTimer);  
170 - dataUpdateTimer = null;  
171 - }  
172 if (widgetContext.inited) { 170 if (widgetContext.inited) {
173 if (dataUpdateCaf) { 171 if (dataUpdateCaf) {
174 dataUpdateCaf(); 172 dataUpdateCaf();
@@ -496,7 +494,8 @@ export default function WidgetController($scope, $timeout, $window, $element, $q @@ -496,7 +494,8 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
496 startTimeMs: startTimeMs, 494 startTimeMs: startTimeMs,
497 endTimeMs: endTimeMs 495 endTimeMs: endTimeMs
498 } 496 }
499 - } 497 + },
  498 + aggregation: angular.copy(widget.config.timewindow.aggregation)
500 }; 499 };
501 } 500 }
502 501
@@ -513,14 +512,10 @@ export default function WidgetController($scope, $timeout, $window, $element, $q @@ -513,14 +512,10 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
513 } 512 }
514 if (update) { 513 if (update) {
515 if (subscriptionTimewindow.realtimeWindowMs) { 514 if (subscriptionTimewindow.realtimeWindowMs) {
516 - updateTimewindow(sourceData.startTs, sourceData.endTs); 515 + updateTimewindow();
517 } 516 }
518 widgetContext.data[datasourceIndex + dataKeyIndex].data = sourceData.data; 517 widgetContext.data[datasourceIndex + dataKeyIndex].data = sourceData.data;
519 - if (widgetContext.data.length > 1 && !dataUpdateTimer) {  
520 - dataUpdateTimer = $timeout(onDataUpdated, 300, false);  
521 - } else {  
522 - onDataUpdated();  
523 - } 518 + onDataUpdated();
524 } 519 }
525 } 520 }
526 521
@@ -552,10 +547,6 @@ export default function WidgetController($scope, $timeout, $window, $element, $q @@ -552,10 +547,6 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
552 547
553 function unsubscribe() { 548 function unsubscribe() {
554 if (widget.type !== types.widgetType.rpc.value) { 549 if (widget.type !== types.widgetType.rpc.value) {
555 - if (dataUpdateTimer) {  
556 - $timeout.cancel(dataUpdateTimer);  
557 - dataUpdateTimer = null;  
558 - }  
559 for (var i in datasourceListeners) { 550 for (var i in datasourceListeners) {
560 var listener = datasourceListeners[i]; 551 var listener = datasourceListeners[i];
561 datasourceService.unsubscribeFromDatasource(listener); 552 datasourceService.unsubscribeFromDatasource(listener);
@@ -575,7 +566,7 @@ export default function WidgetController($scope, $timeout, $window, $element, $q @@ -575,7 +566,7 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
575 }; 566 };
576 if (widget.type === types.widgetType.timeseries.value && 567 if (widget.type === types.widgetType.timeseries.value &&
577 angular.isDefined(widget.config.timewindow)) { 568 angular.isDefined(widget.config.timewindow)) {
578 - 569 + var timeWindow = 0;
579 if (angular.isDefined(widget.config.timewindow.aggregation)) { 570 if (angular.isDefined(widget.config.timewindow.aggregation)) {
580 subscriptionTimewindow.aggregation = { 571 subscriptionTimewindow.aggregation = {
581 limit: widget.config.timewindow.aggregation.limit || 200, 572 limit: widget.config.timewindow.aggregation.limit || 200,
@@ -585,6 +576,8 @@ export default function WidgetController($scope, $timeout, $window, $element, $q @@ -585,6 +576,8 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
585 576
586 if (angular.isDefined(widget.config.timewindow.realtime)) { 577 if (angular.isDefined(widget.config.timewindow.realtime)) {
587 subscriptionTimewindow.realtimeWindowMs = widget.config.timewindow.realtime.timewindowMs; 578 subscriptionTimewindow.realtimeWindowMs = widget.config.timewindow.realtime.timewindowMs;
  579 + subscriptionTimewindow.startTs = (new Date).getTime() + widgetContext.timeWindow.stDiff - subscriptionTimewindow.realtimeWindowMs;
  580 + timeWindow = subscriptionTimewindow.realtimeWindowMs;
588 } else if (angular.isDefined(widget.config.timewindow.history)) { 581 } else if (angular.isDefined(widget.config.timewindow.history)) {
589 if (angular.isDefined(widget.config.timewindow.history.timewindowMs)) { 582 if (angular.isDefined(widget.config.timewindow.history.timewindowMs)) {
590 var currentTime = (new Date).getTime(); 583 var currentTime = (new Date).getTime();
@@ -592,14 +585,31 @@ export default function WidgetController($scope, $timeout, $window, $element, $q @@ -592,14 +585,31 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
592 startTimeMs: currentTime - widget.config.timewindow.history.timewindowMs, 585 startTimeMs: currentTime - widget.config.timewindow.history.timewindowMs,
593 endTimeMs: currentTime 586 endTimeMs: currentTime
594 } 587 }
  588 + timeWindow = widget.config.timewindow.history.timewindowMs;
595 } else { 589 } else {
596 subscriptionTimewindow.fixedWindow = { 590 subscriptionTimewindow.fixedWindow = {
597 startTimeMs: widget.config.timewindow.history.fixedTimewindow.startTimeMs, 591 startTimeMs: widget.config.timewindow.history.fixedTimewindow.startTimeMs,
598 endTimeMs: widget.config.timewindow.history.fixedTimewindow.endTimeMs 592 endTimeMs: widget.config.timewindow.history.fixedTimewindow.endTimeMs
599 } 593 }
  594 + timeWindow = subscriptionTimewindow.fixedWindow.endTimeMs - subscriptionTimewindow.fixedWindow.startTimeMs;
600 } 595 }
  596 + subscriptionTimewindow.startTs = subscriptionTimewindow.fixedWindow.startTimeMs;
  597 + }
  598 + var aggregation = subscriptionTimewindow.aggregation;
  599 + var noAggregation = aggregation.type === types.aggregation.none.value;
  600 + var interval = Math.floor(timeWindow / aggregation.limit);
  601 + if (!noAggregation) {
  602 + aggregation.interval = Math.max(interval, 1000);
  603 + aggregation.limit = Math.ceil(interval/aggregation.interval * aggregation.limit);
  604 + aggregation.timeWindow = aggregation.interval * aggregation.limit;
  605 + } else {
  606 + aggregation.timeWindow = interval * aggregation.limit;
  607 + aggregation.interval = 1000;
601 } 608 }
602 updateTimewindow(); 609 updateTimewindow();
  610 + if (subscriptionTimewindow.fixedWindow) {
  611 + onDataUpdated();
  612 + }
603 } 613 }
604 for (var i in widget.config.datasources) { 614 for (var i in widget.config.datasources) {
605 var datasource = widget.config.datasources[i]; 615 var datasource = widget.config.datasources[i];
@@ -61,6 +61,7 @@ export default function DashboardController(types, widgetService, userService, @@ -61,6 +61,7 @@ export default function DashboardController(types, widgetService, userService,
61 vm.isTenantAdmin = isTenantAdmin; 61 vm.isTenantAdmin = isTenantAdmin;
62 vm.isSystemAdmin = isSystemAdmin; 62 vm.isSystemAdmin = isSystemAdmin;
63 vm.loadDashboard = loadDashboard; 63 vm.loadDashboard = loadDashboard;
  64 + vm.getServerTimeDiff = getServerTimeDiff;
64 vm.noData = noData; 65 vm.noData = noData;
65 vm.onAddWidgetClosed = onAddWidgetClosed; 66 vm.onAddWidgetClosed = onAddWidgetClosed;
66 vm.onEditWidgetClosed = onEditWidgetClosed; 67 vm.onEditWidgetClosed = onEditWidgetClosed;
@@ -94,10 +95,9 @@ export default function DashboardController(types, widgetService, userService, @@ -94,10 +95,9 @@ export default function DashboardController(types, widgetService, userService,
94 widgetService.getBundleWidgetTypes(bundleAlias, isSystem).then( 95 widgetService.getBundleWidgetTypes(bundleAlias, isSystem).then(
95 function (widgetTypes) { 96 function (widgetTypes) {
96 97
97 - widgetTypes = $filter('orderBy')(widgetTypes, ['-name']); 98 + widgetTypes = $filter('orderBy')(widgetTypes, ['-createdTime']);
98 99
99 var top = 0; 100 var top = 0;
100 - var sizeY = 0;  
101 101
102 if (widgetTypes.length > 0) { 102 if (widgetTypes.length > 0) {
103 loadNext(0); 103 loadNext(0);
@@ -135,7 +135,7 @@ export default function DashboardController(types, widgetService, userService, @@ -135,7 +135,7 @@ export default function DashboardController(types, widgetService, userService,
135 } else if (widgetTypeInfo.type === types.widgetType.static.value) { 135 } else if (widgetTypeInfo.type === types.widgetType.static.value) {
136 vm.staticWidgetTypes.push(widget); 136 vm.staticWidgetTypes.push(widget);
137 } 137 }
138 - top += sizeY; 138 + top += widget.sizeY;
139 loadNextOrComplete(i); 139 loadNextOrComplete(i);
140 140
141 } 141 }
@@ -144,6 +144,10 @@ export default function DashboardController(types, widgetService, userService, @@ -144,6 +144,10 @@ export default function DashboardController(types, widgetService, userService,
144 } 144 }
145 } 145 }
146 146
  147 + function getServerTimeDiff() {
  148 + return dashboardService.getServerTimeDiff();
  149 + }
  150 +
147 function loadDashboard() { 151 function loadDashboard() {
148 152
149 var deferred = $q.defer(); 153 var deferred = $q.defer();
@@ -91,6 +91,7 @@ @@ -91,6 +91,7 @@
91 prepare-widget-context-menu="vm.prepareWidgetContextMenu(widget)" 91 prepare-widget-context-menu="vm.prepareWidgetContextMenu(widget)"
92 on-remove-widget="vm.removeWidget(event, widget)" 92 on-remove-widget="vm.removeWidget(event, widget)"
93 load-widgets="vm.loadDashboard()" 93 load-widgets="vm.loadDashboard()"
  94 + get-st-diff="vm.getServerTimeDiff()"
94 on-init="vm.dashboardInited(dashboard)" 95 on-init="vm.dashboardInited(dashboard)"
95 on-init-failed="vm.dashboardInitFailed(e)"> 96 on-init-failed="vm.dashboardInitFailed(e)">
96 </tb-dashboard> 97 </tb-dashboard>
@@ -29,7 +29,7 @@ import EditAttributeValueController from './edit-attribute-value.controller'; @@ -29,7 +29,7 @@ import EditAttributeValueController from './edit-attribute-value.controller';
29 29
30 /*@ngInject*/ 30 /*@ngInject*/
31 export default function AttributeTableDirective($compile, $templateCache, $rootScope, $q, $mdEditDialog, $mdDialog, 31 export default function AttributeTableDirective($compile, $templateCache, $rootScope, $q, $mdEditDialog, $mdDialog,
32 - $document, $translate, utils, types, deviceService, widgetService) { 32 + $document, $translate, utils, types, dashboardService, deviceService, widgetService) {
33 33
34 var linker = function (scope, element, attrs) { 34 var linker = function (scope, element, attrs) {
35 35
@@ -357,6 +357,10 @@ export default function AttributeTableDirective($compile, $templateCache, $rootS @@ -357,6 +357,10 @@ export default function AttributeTableDirective($compile, $templateCache, $rootS
357 scope.getDeviceAttributes(true); 357 scope.getDeviceAttributes(true);
358 } 358 }
359 359
  360 + scope.getServerTimeDiff = function() {
  361 + return dashboardService.getServerTimeDiff();
  362 + }
  363 +
360 scope.addWidgetToDashboard = function($event) { 364 scope.addWidgetToDashboard = function($event) {
361 if (scope.mode === 'widget' && scope.widgetsListCache.length > 0) { 365 if (scope.mode === 'widget' && scope.widgetsListCache.length > 0) {
362 var widget = scope.widgetsListCache[scope.widgetsCarousel.index][0]; 366 var widget = scope.widgetsListCache[scope.widgetsCarousel.index][0];
@@ -158,8 +158,9 @@ @@ -158,8 +158,9 @@
158 <tb-dashboard 158 <tb-dashboard
159 device-alias-list="deviceAliases" 159 device-alias-list="deviceAliases"
160 widgets="widgets" 160 widgets="widgets"
  161 + get-st-diff="getServerTimeDiff()"
161 columns="20" 162 columns="20"
162 - is-edit="true" 163 + is-edit="false"
163 is-mobile-disabled="true" 164 is-mobile-disabled="true"
164 is-edit-action-enabled="false" 165 is-edit-action-enabled="false"
165 is-remove-action-enabled="false"> 166 is-remove-action-enabled="false">
@@ -22,6 +22,8 @@ import 'flot/src/jquery.flot'; @@ -22,6 +22,8 @@ import 'flot/src/jquery.flot';
22 import 'flot/src/plugins/jquery.flot.time'; 22 import 'flot/src/plugins/jquery.flot.time';
23 import 'flot/src/plugins/jquery.flot.selection'; 23 import 'flot/src/plugins/jquery.flot.selection';
24 import 'flot/src/plugins/jquery.flot.pie'; 24 import 'flot/src/plugins/jquery.flot.pie';
  25 +import 'flot/src/plugins/jquery.flot.crosshair';
  26 +import 'flot/src/plugins/jquery.flot.stack';
25 27
26 /* eslint-disable angular/angularelement */ 28 /* eslint-disable angular/angularelement */
27 export default class TbFlot { 29 export default class TbFlot {
@@ -38,8 +40,8 @@ export default class TbFlot { @@ -38,8 +40,8 @@ export default class TbFlot {
38 var keySettings = series.dataKey.settings; 40 var keySettings = series.dataKey.settings;
39 41
40 series.lines = { 42 series.lines = {
41 - fill: keySettings.fillLines || false,  
42 - show: keySettings.showLines || true 43 + fill: keySettings.fillLines === true,
  44 + show: this.chartType === 'line' ? keySettings.showLines !== false : keySettings.showLines === true
43 }; 45 };
44 46
45 series.points = { 47 series.points = {
@@ -58,36 +60,34 @@ export default class TbFlot { @@ -58,36 +60,34 @@ export default class TbFlot {
58 series.highlightColor = lineColor.toRgbString(); 60 series.highlightColor = lineColor.toRgbString();
59 61
60 } 62 }
61 -  
62 - var tbFlot = this;  
63 -  
64 ctx.tooltip = $('#flot-series-tooltip'); 63 ctx.tooltip = $('#flot-series-tooltip');
65 if (ctx.tooltip.length === 0) { 64 if (ctx.tooltip.length === 0) {
66 ctx.tooltip = $("<div id=flot-series-tooltip' class='flot-mouse-value'></div>"); 65 ctx.tooltip = $("<div id=flot-series-tooltip' class='flot-mouse-value'></div>");
67 ctx.tooltip.css({ 66 ctx.tooltip.css({
68 fontSize: "12px", 67 fontSize: "12px",
69 fontFamily: "Roboto", 68 fontFamily: "Roboto",
70 - lineHeight: "24px", 69 + fontWeight: "300",
  70 + lineHeight: "18px",
71 opacity: "1", 71 opacity: "1",
72 backgroundColor: "rgba(0,0,0,0.7)", 72 backgroundColor: "rgba(0,0,0,0.7)",
73 - color: "#fff", 73 + color: "#D9DADB",
74 position: "absolute", 74 position: "absolute",
75 display: "none", 75 display: "none",
76 zIndex: "100", 76 zIndex: "100",
77 - padding: "2px 8px", 77 + padding: "4px 10px",
78 borderRadius: "4px" 78 borderRadius: "4px"
79 }).appendTo("body"); 79 }).appendTo("body");
80 } 80 }
81 81
82 - ctx.tooltipFormatter = function(item) {  
83 - var label = item.series.label;  
84 - var color = item.series.color;  
85 - var content = '';  
86 - if (tbFlot.chartType === 'line') {  
87 - var timestamp = parseInt(item.datapoint[0]);  
88 - var date = moment(timestamp).format('YYYY-MM-DD HH:mm:ss');  
89 - content += '<b>' + date + '</b></br>';  
90 - } 82 + var tbFlot = this;
  83 +
  84 + function seriesInfoDiv(label, color, value, units, trackDecimals, active, percent) {
  85 + var divElement = $('<div></div>');
  86 + divElement.css({
  87 + display: "flex",
  88 + alignItems: "center",
  89 + justifyContent: "center"
  90 + });
91 var lineSpan = $('<span></span>'); 91 var lineSpan = $('<span></span>');
92 lineSpan.css({ 92 lineSpan.css({
93 backgroundColor: color, 93 backgroundColor: color,
@@ -97,27 +97,76 @@ export default class TbFlot { @@ -97,27 +97,76 @@ export default class TbFlot {
97 verticalAlign: "middle", 97 verticalAlign: "middle",
98 marginRight: "5px" 98 marginRight: "5px"
99 }); 99 });
100 - content += lineSpan.prop('outerHTML');  
101 - 100 + divElement.append(lineSpan);
102 var labelSpan = $('<span>' + label + ':</span>'); 101 var labelSpan = $('<span>' + label + ':</span>');
103 labelSpan.css({ 102 labelSpan.css({
104 marginRight: "10px" 103 marginRight: "10px"
105 }); 104 });
106 - content += labelSpan.prop('outerHTML');  
107 - var value = tbFlot.chartType === 'line' ? item.datapoint[1] : item.datapoint[1][0][1];  
108 - content += ' <b>' + value.toFixed(ctx.trackDecimals);  
109 - if (settings.units) {  
110 - content += ' ' + settings.units; 105 + if (active) {
  106 + labelSpan.css({
  107 + color: "#FFF",
  108 + fontWeight: "700"
  109 + });
111 } 110 }
112 - if (tbFlot.chartType === 'pie') {  
113 - content += ' (' + Math.round(item.series.percent) + ' %)'; 111 + divElement.append(labelSpan);
  112 + var valueContent = value.toFixed(trackDecimals);
  113 + if (units) {
  114 + valueContent += ' ' + units;
114 } 115 }
115 - content += '</b>';  
116 - return content;  
117 - }; 116 + if (angular.isNumber(percent)) {
  117 + valueContent += ' (' + Math.round(percent) + ' %)';
  118 + }
  119 + var valueSpan = $('<span>' + valueContent + '</span>');
  120 + valueSpan.css({
  121 + marginLeft: "auto",
  122 + fontWeight: "700"
  123 + });
  124 + if (active) {
  125 + valueSpan.css({
  126 + color: "#FFF"
  127 + });
  128 + }
  129 + divElement.append(valueSpan);
  130 +
  131 + return divElement;
  132 + }
  133 +
  134 + if (this.chartType === 'pie') {
  135 + ctx.tooltipFormatter = function(item) {
  136 + var divElement = seriesInfoDiv(item.series.label, item.series.color,
  137 + item.datapoint[1][0][1], tbFlot.ctx.settings.units, tbFlot.ctx.trackDecimals, true, item.series.percent);
  138 + return divElement.prop('outerHTML');
  139 + };
  140 + } else {
  141 + ctx.tooltipFormatter = function(hoverInfo, seriesIndex) {
  142 + var content = '';
  143 + var timestamp = parseInt(hoverInfo.time);
  144 + var date = moment(timestamp).format('YYYY-MM-DD HH:mm:ss');
  145 + var dateDiv = $('<div>' + date + '</div>');
  146 + dateDiv.css({
  147 + display: "flex",
  148 + alignItems: "center",
  149 + justifyContent: "center",
  150 + padding: "4px",
  151 + fontWeight: "700"
  152 + });
  153 + content += dateDiv.prop('outerHTML');
  154 + for (var i in hoverInfo.seriesHover) {
  155 + var seriesHoverInfo = hoverInfo.seriesHover[i];
  156 + if (tbFlot.ctx.tooltipIndividual && seriesHoverInfo.index !== seriesIndex) {
  157 + continue;
  158 + }
  159 + var divElement = seriesInfoDiv(seriesHoverInfo.label, seriesHoverInfo.color,
  160 + seriesHoverInfo.value, tbFlot.ctx.settings.units, tbFlot.ctx.trackDecimals, seriesHoverInfo.index === seriesIndex);
  161 + content += divElement.prop('outerHTML');
  162 + }
  163 + return content;
  164 + };
  165 + }
118 166
119 var settings = ctx.settings; 167 var settings = ctx.settings;
120 ctx.trackDecimals = angular.isDefined(settings.decimals) ? settings.decimals : 1; 168 ctx.trackDecimals = angular.isDefined(settings.decimals) ? settings.decimals : 1;
  169 + ctx.tooltipIndividual = this.chartType === 'pie' || (angular.isDefined(settings.tooltipIndividual) ? settings.tooltipIndividual : false);
121 170
122 var font = { 171 var font = {
123 color: settings.fontColor || "#545454", 172 color: settings.fontColor || "#545454",
@@ -134,7 +183,7 @@ export default class TbFlot { @@ -134,7 +183,7 @@ export default class TbFlot {
134 grid: { 183 grid: {
135 hoverable: true, 184 hoverable: true,
136 mouseActiveRadius: 10, 185 mouseActiveRadius: 10,
137 - autoHighlight: true 186 + autoHighlight: ctx.tooltipIndividual === true
138 }, 187 },
139 selection : { mode : ctx.isMobile ? null : 'x' }, 188 selection : { mode : ctx.isMobile ? null : 'x' },
140 legend : { 189 legend : {
@@ -155,7 +204,7 @@ export default class TbFlot { @@ -155,7 +204,7 @@ export default class TbFlot {
155 settings.legend.backgroundOpacity : 0.85; 204 settings.legend.backgroundOpacity : 0.85;
156 } 205 }
157 206
158 - if (this.chartType === 'line') { 207 + if (this.chartType === 'line' || this.chartType === 'bar') {
159 options.xaxis = { 208 options.xaxis = {
160 mode: 'time', 209 mode: 'time',
161 timezone: 'browser', 210 timezone: 'browser',
@@ -208,6 +257,28 @@ export default class TbFlot { @@ -208,6 +257,28 @@ export default class TbFlot {
208 } 257 }
209 } 258 }
210 259
  260 + options.crosshair = {
  261 + mode: 'x'
  262 + }
  263 +
  264 + options.series = {
  265 + stack: settings.stack === true
  266 + }
  267 +
  268 + if (this.chartType === 'bar') {
  269 + options.series.lines = {
  270 + show: false,
  271 + fill: false,
  272 + steps: false
  273 + }
  274 + options.series.bars ={
  275 + show: true,
  276 + barWidth: ctx.timeWindow.interval * 0.6,
  277 + lineWidth: 0,
  278 + fill: 0.9
  279 + }
  280 + }
  281 +
211 options.xaxis.min = ctx.timeWindow.minTime; 282 options.xaxis.min = ctx.timeWindow.minTime;
212 options.xaxis.max = ctx.timeWindow.maxTime; 283 options.xaxis.max = ctx.timeWindow.maxTime;
213 } else if (this.chartType === 'pie') { 284 } else if (this.chartType === 'pie') {
@@ -271,11 +342,12 @@ export default class TbFlot { @@ -271,11 +342,12 @@ export default class TbFlot {
271 342
272 update() { 343 update() {
273 if (!this.isMouseInteraction) { 344 if (!this.isMouseInteraction) {
274 - if (this.chartType === 'line') { 345 + if (this.chartType === 'line' || this.chartType === 'bar') {
275 this.ctx.plot.getOptions().xaxes[0].min = this.ctx.timeWindow.minTime; 346 this.ctx.plot.getOptions().xaxes[0].min = this.ctx.timeWindow.minTime;
276 this.ctx.plot.getOptions().xaxes[0].max = this.ctx.timeWindow.maxTime; 347 this.ctx.plot.getOptions().xaxes[0].max = this.ctx.timeWindow.maxTime;
277 - }  
278 - if (this.chartType === 'line') { 348 + if (this.chartType === 'bar') {
  349 + this.ctx.plot.getOptions().series.bars.barWidth = this.ctx.timeWindow.interval * 0.6;
  350 + }
279 this.ctx.plot.setData(this.ctx.data); 351 this.ctx.plot.setData(this.ctx.data);
280 this.ctx.plot.setupGrid(); 352 this.ctx.plot.setupGrid();
281 this.ctx.plot.draw(); 353 this.ctx.plot.draw();
@@ -290,75 +362,475 @@ export default class TbFlot { @@ -290,75 +362,475 @@ export default class TbFlot {
290 } 362 }
291 } 363 }
292 364
293 - pieDataRendered() {  
294 - for (var i in this.ctx.pieTargetData) {  
295 - var value = this.ctx.pieTargetData[i] ? this.ctx.pieTargetData[i] : 0;  
296 - this.ctx.pieRenderedData[i] = value;  
297 - if (!this.ctx.pieData[i].data[0]) {  
298 - this.ctx.pieData[i].data[0] = [0,0];  
299 - }  
300 - this.ctx.pieData[i].data[0][1] = value; 365 + resize() {
  366 + this.ctx.plot.resize();
  367 + if (this.chartType !== 'pie') {
  368 + this.ctx.plot.setupGrid();
301 } 369 }
  370 + this.ctx.plot.draw();
302 } 371 }
303 372
304 - nextPieDataAnimation(start) {  
305 - if (start) {  
306 - this.finishPieDataAnimation();  
307 - this.ctx.pieAnimationStartTime = this.ctx.pieAnimationLastTime = Date.now();  
308 - for (var i in this.ctx.data) {  
309 - this.ctx.pieTargetData[i] = (this.ctx.data[i].data && this.ctx.data[i].data[0])  
310 - ? this.ctx.data[i].data[0][1] : 0;  
311 - }  
312 - }  
313 - if (this.ctx.pieAnimationCaf) {  
314 - this.ctx.pieAnimationCaf();  
315 - this.ctx.pieAnimationCaf = null; 373 + static get pieSettingsSchema() {
  374 + return {
  375 + "schema": {
  376 + "type": "object",
  377 + "title": "Settings",
  378 + "properties": {
  379 + "radius": {
  380 + "title": "Radius",
  381 + "type": "number",
  382 + "default": 1
  383 + },
  384 + "innerRadius": {
  385 + "title": "Inner radius",
  386 + "type": "number",
  387 + "default": 0
  388 + },
  389 + "tilt": {
  390 + "title": "Tilt",
  391 + "type": "number",
  392 + "default": 1
  393 + },
  394 + "animatedPie": {
  395 + "title": "Enable pie animation (experimental)",
  396 + "type": "boolean",
  397 + "default": false
  398 + },
  399 + "stroke": {
  400 + "title": "Stroke",
  401 + "type": "object",
  402 + "properties": {
  403 + "color": {
  404 + "title": "Color",
  405 + "type": "string",
  406 + "default": ""
  407 + },
  408 + "width": {
  409 + "title": "Width (pixels)",
  410 + "type": "number",
  411 + "default": 0
  412 + }
  413 + }
  414 + },
  415 + "showLabels": {
  416 + "title": "Show labels",
  417 + "type": "boolean",
  418 + "default": false
  419 + },
  420 + "fontColor": {
  421 + "title": "Font color",
  422 + "type": "string",
  423 + "default": "#545454"
  424 + },
  425 + "fontSize": {
  426 + "title": "Font size",
  427 + "type": "number",
  428 + "default": 10
  429 + },
  430 + "decimals": {
  431 + "title": "Number of digits after floating point",
  432 + "type": "number",
  433 + "default": 1
  434 + },
  435 + "units": {
  436 + "title": "Special symbol to show next to value",
  437 + "type": "string",
  438 + "default": ""
  439 + },
  440 + "legend": {
  441 + "title": "Legend settings",
  442 + "type": "object",
  443 + "properties": {
  444 + "show": {
  445 + "title": "Show legend",
  446 + "type": "boolean",
  447 + "default": true
  448 + },
  449 + "position": {
  450 + "title": "Position",
  451 + "type": "string",
  452 + "default": "nw"
  453 + },
  454 + "labelBoxBorderColor": {
  455 + "title": "Label box border color",
  456 + "type": "string",
  457 + "default": "#CCCCCC"
  458 + },
  459 + "backgroundColor": {
  460 + "title": "Background color",
  461 + "type": "string",
  462 + "default": "#F0F0F0"
  463 + },
  464 + "backgroundOpacity": {
  465 + "title": "Background opacity",
  466 + "type": "number",
  467 + "default": 0.85
  468 + }
  469 + }
  470 + }
  471 + },
  472 + "required": []
  473 + },
  474 + "form": [
  475 + "radius",
  476 + "innerRadius",
  477 + "animatedPie",
  478 + "tilt",
  479 + {
  480 + "key": "stroke",
  481 + "items": [
  482 + {
  483 + "key": "stroke.color",
  484 + "type": "color"
  485 + },
  486 + "stroke.width"
  487 + ]
  488 + },
  489 + "showLabels",
  490 + {
  491 + "key": "fontColor",
  492 + "type": "color"
  493 + },
  494 + "fontSize",
  495 + "decimals",
  496 + "units",
  497 + {
  498 + "key": "legend",
  499 + "items": [
  500 + "legend.show",
  501 + {
  502 + "key": "legend.position",
  503 + "type": "rc-select",
  504 + "multiple": false,
  505 + "items": [
  506 + {
  507 + "value": "nw",
  508 + "label": "North-west"
  509 + },
  510 + {
  511 + "value": "ne",
  512 + "label": "North-east"
  513 + },
  514 + {
  515 + "value": "sw",
  516 + "label": "South-west"
  517 + },
  518 + {
  519 + "value": "se",
  520 + "label": "Soth-east"
  521 + }
  522 + ]
  523 + },
  524 + {
  525 + "key": "legend.labelBoxBorderColor",
  526 + "type": "color"
  527 + },
  528 + {
  529 + "key": "legend.backgroundColor",
  530 + "type": "color"
  531 + },
  532 + "legend.backgroundOpacity"
  533 + ]
  534 + }
  535 + ]
316 } 536 }
317 - var self = this;  
318 - this.ctx.pieAnimationCaf = this.ctx.$scope.tbRaf(  
319 - function () {  
320 - self.onPieDataAnimation();  
321 - }  
322 - );  
323 } 537 }
324 538
325 - onPieDataAnimation() {  
326 - var time = Date.now();  
327 - var elapsed = time - this.ctx.pieAnimationLastTime;//this.ctx.pieAnimationStartTime;  
328 - var progress = (time - this.ctx.pieAnimationStartTime) / this.ctx.pieDataAnimationDuration;  
329 - if (progress >= 1) {  
330 - this.finishPieDataAnimation();  
331 - } else {  
332 - if (elapsed >= 40) {  
333 - for (var i in this.ctx.pieTargetData) {  
334 - var prevValue = this.ctx.pieRenderedData[i];  
335 - var targetValue = this.ctx.pieTargetData[i];  
336 - var value = prevValue + (targetValue - prevValue) * progress;  
337 - if (!this.ctx.pieData[i].data[0]) {  
338 - this.ctx.pieData[i].data[0] = [0,0]; 539 + static get settingsSchema() {
  540 + return {
  541 + "schema": {
  542 + "type": "object",
  543 + "title": "Settings",
  544 + "properties": {
  545 + "stack": {
  546 + "title": "Stacking",
  547 + "type": "boolean",
  548 + "default": false
  549 + },
  550 + "shadowSize": {
  551 + "title": "Shadow size",
  552 + "type": "number",
  553 + "default": 4
  554 + },
  555 + "fontColor": {
  556 + "title": "Font color",
  557 + "type": "string",
  558 + "default": "#545454"
  559 + },
  560 + "fontSize": {
  561 + "title": "Font size",
  562 + "type": "number",
  563 + "default": 10
  564 + },
  565 + "decimals": {
  566 + "title": "Number of digits after floating point",
  567 + "type": "number",
  568 + "default": 1
  569 + },
  570 + "units": {
  571 + "title": "Special symbol to show next to value",
  572 + "type": "string",
  573 + "default": ""
  574 + },
  575 + "tooltipIndividual": {
  576 + "title": "Hover individual points",
  577 + "type": "boolean",
  578 + "default": false
  579 + },
  580 + "grid": {
  581 + "title": "Grid settings",
  582 + "type": "object",
  583 + "properties": {
  584 + "color": {
  585 + "title": "Primary color",
  586 + "type": "string",
  587 + "default": "#545454"
  588 + },
  589 + "backgroundColor": {
  590 + "title": "Background color",
  591 + "type": "string",
  592 + "default": null
  593 + },
  594 + "tickColor": {
  595 + "title": "Ticks color",
  596 + "type": "string",
  597 + "default": "#DDDDDD"
  598 + },
  599 + "outlineWidth": {
  600 + "title": "Grid outline/border width (px)",
  601 + "type": "number",
  602 + "default": 1
  603 + },
  604 + "verticalLines": {
  605 + "title": "Show vertical lines",
  606 + "type": "boolean",
  607 + "default": true
  608 + },
  609 + "horizontalLines": {
  610 + "title": "Show horizontal lines",
  611 + "type": "boolean",
  612 + "default": true
  613 + }
  614 + }
  615 + },
  616 + "legend": {
  617 + "title": "Legend settings",
  618 + "type": "object",
  619 + "properties": {
  620 + "show": {
  621 + "title": "Show legend",
  622 + "type": "boolean",
  623 + "default": true
  624 + },
  625 + "position": {
  626 + "title": "Position",
  627 + "type": "string",
  628 + "default": "nw"
  629 + },
  630 + "labelBoxBorderColor": {
  631 + "title": "Label box border color",
  632 + "type": "string",
  633 + "default": "#CCCCCC"
  634 + },
  635 + "backgroundColor": {
  636 + "title": "Background color",
  637 + "type": "string",
  638 + "default": "#F0F0F0"
  639 + },
  640 + "backgroundOpacity": {
  641 + "title": "Background opacity",
  642 + "type": "number",
  643 + "default": 0.85
  644 + }
  645 + }
  646 + },
  647 + "xaxis": {
  648 + "title": "X axis settings",
  649 + "type": "object",
  650 + "properties": {
  651 + "showLabels": {
  652 + "title": "Show labels",
  653 + "type": "boolean",
  654 + "default": true
  655 + },
  656 + "title": {
  657 + "title": "Axis title",
  658 + "type": "string",
  659 + "default": null
  660 + },
  661 + "titleAngle": {
  662 + "title": "Axis title's angle in degrees",
  663 + "type": "number",
  664 + "default": 0
  665 + },
  666 + "color": {
  667 + "title": "Ticks color",
  668 + "type": "string",
  669 + "default": null
  670 + }
  671 + }
  672 + },
  673 + "yaxis": {
  674 + "title": "Y axis settings",
  675 + "type": "object",
  676 + "properties": {
  677 + "showLabels": {
  678 + "title": "Show labels",
  679 + "type": "boolean",
  680 + "default": true
  681 + },
  682 + "title": {
  683 + "title": "Axis title",
  684 + "type": "string",
  685 + "default": null
  686 + },
  687 + "titleAngle": {
  688 + "title": "Axis title's angle in degrees",
  689 + "type": "number",
  690 + "default": 0
  691 + },
  692 + "color": {
  693 + "title": "Ticks color",
  694 + "type": "string",
  695 + "default": null
  696 + }
  697 + }
339 } 698 }
340 - this.ctx.pieData[i].data[0][1] = value; 699 + },
  700 + "required": []
  701 + },
  702 + "form": [
  703 + "stack",
  704 + "shadowSize",
  705 + {
  706 + "key": "fontColor",
  707 + "type": "color"
  708 + },
  709 + "fontSize",
  710 + "decimals",
  711 + "units",
  712 + "tooltipIndividual",
  713 + {
  714 + "key": "grid",
  715 + "items": [
  716 + {
  717 + "key": "grid.color",
  718 + "type": "color"
  719 + },
  720 + {
  721 + "key": "grid.backgroundColor",
  722 + "type": "color"
  723 + },
  724 + {
  725 + "key": "grid.tickColor",
  726 + "type": "color"
  727 + },
  728 + "grid.outlineWidth",
  729 + "grid.verticalLines",
  730 + "grid.horizontalLines"
  731 + ]
  732 + },
  733 + {
  734 + "key": "legend",
  735 + "items": [
  736 + "legend.show",
  737 + {
  738 + "key": "legend.position",
  739 + "type": "rc-select",
  740 + "multiple": false,
  741 + "items": [
  742 + {
  743 + "value": "nw",
  744 + "label": "North-west"
  745 + },
  746 + {
  747 + "value": "ne",
  748 + "label": "North-east"
  749 + },
  750 + {
  751 + "value": "sw",
  752 + "label": "South-west"
  753 + },
  754 + {
  755 + "value": "se",
  756 + "label": "Soth-east"
  757 + }
  758 + ]
  759 + },
  760 + {
  761 + "key": "legend.labelBoxBorderColor",
  762 + "type": "color"
  763 + },
  764 + {
  765 + "key": "legend.backgroundColor",
  766 + "type": "color"
  767 + },
  768 + "legend.backgroundOpacity"
  769 + ]
  770 + },
  771 + {
  772 + "key": "xaxis",
  773 + "items": [
  774 + "xaxis.showLabels",
  775 + "xaxis.title",
  776 + "xaxis.titleAngle",
  777 + {
  778 + "key": "xaxis.color",
  779 + "type": "color"
  780 + }
  781 + ]
  782 + },
  783 + {
  784 + "key": "yaxis",
  785 + "items": [
  786 + "yaxis.showLabels",
  787 + "yaxis.title",
  788 + "yaxis.titleAngle",
  789 + {
  790 + "key": "yaxis.color",
  791 + "type": "color"
  792 + }
  793 + ]
341 } 794 }
342 - this.ctx.plot.setData(this.ctx.pieData);  
343 - this.ctx.plot.draw();  
344 - this.ctx.pieAnimationLastTime = time;  
345 - }  
346 - this.nextPieDataAnimation(false); 795 +
  796 + ]
347 } 797 }
348 } 798 }
349 799
350 - finishPieDataAnimation() {  
351 - this.pieDataRendered();  
352 - this.ctx.plot.setData(this.ctx.pieData);  
353 - this.ctx.plot.draw(); 800 + static get pieDatakeySettingsSchema() {
  801 + return {}
354 } 802 }
355 803
356 - resize() {  
357 - this.ctx.plot.resize();  
358 - if (this.chartType === 'line') {  
359 - this.ctx.plot.setupGrid(); 804 + static datakeySettingsSchema(defaultShowLines) {
  805 + return {
  806 + "schema": {
  807 + "type": "object",
  808 + "title": "DataKeySettings",
  809 + "properties": {
  810 + "showLines": {
  811 + "title": "Show lines",
  812 + "type": "boolean",
  813 + "default": defaultShowLines
  814 + },
  815 + "fillLines": {
  816 + "title": "Fill lines",
  817 + "type": "boolean",
  818 + "default": false
  819 + },
  820 + "showPoints": {
  821 + "title": "Show points",
  822 + "type": "boolean",
  823 + "default": false
  824 + }
  825 + },
  826 + "required": ["showLines", "fillLines", "showPoints"]
  827 + },
  828 + "form": [
  829 + "showLines",
  830 + "fillLines",
  831 + "showPoints"
  832 + ]
360 } 833 }
361 - this.ctx.plot.draw();  
362 } 834 }
363 835
364 checkMouseEvents() { 836 checkMouseEvents() {
@@ -378,24 +850,58 @@ export default class TbFlot { @@ -378,24 +850,58 @@ export default class TbFlot {
378 850
379 if (!this.flotHoverHandler) { 851 if (!this.flotHoverHandler) {
380 this.flotHoverHandler = function (event, pos, item) { 852 this.flotHoverHandler = function (event, pos, item) {
381 - if (item) {  
382 - var pageX = item.pageX || pos.pageX;  
383 - var pageY = item.pageY || pos.pageY;  
384 - tbFlot.ctx.tooltip.html(tbFlot.ctx.tooltipFormatter(item))  
385 - .css({top: pageY+5, left: 0})  
386 - .fadeIn(200);  
387 - var windowWidth = $( window ).width(); //eslint-disable-line  
388 - var tooltipWidth = tbFlot.ctx.tooltip.width();  
389 - var left = pageX+5;  
390 - if (windowWidth - pageX < tooltipWidth + 50) {  
391 - left = pageX - tooltipWidth - 10; 853 + if (!tbFlot.ctx.tooltipIndividual || item) {
  854 +
  855 + var multipleModeTooltip = !tbFlot.ctx.tooltipIndividual;
  856 +
  857 + if (multipleModeTooltip) {
  858 + tbFlot.ctx.plot.unhighlight();
392 } 859 }
393 - tbFlot.ctx.tooltip.css({  
394 - left: left  
395 - }); 860 +
  861 + var pageX = pos.pageX;
  862 + var pageY = pos.pageY;
  863 +
  864 + var tooltipHtml;
  865 +
  866 + if (tbFlot.chartType === 'pie') {
  867 + tooltipHtml = tbFlot.ctx.tooltipFormatter(item);
  868 + } else {
  869 + var hoverInfo = tbFlot.getHoverInfo(tbFlot.ctx.plot.getData(), pos);
  870 + if (angular.isNumber(hoverInfo.time)) {
  871 + hoverInfo.seriesHover.sort(function (a, b) {
  872 + return b.value - a.value;
  873 + });
  874 + tooltipHtml = tbFlot.ctx.tooltipFormatter(hoverInfo, item ? item.seriesIndex : -1);
  875 + }
  876 + }
  877 +
  878 + if (tooltipHtml) {
  879 + tbFlot.ctx.tooltip.html(tooltipHtml)
  880 + .css({top: pageY+5, left: 0})
  881 + .fadeIn(200);
  882 +
  883 + var windowWidth = $( window ).width(); //eslint-disable-line
  884 + var tooltipWidth = tbFlot.ctx.tooltip.width();
  885 + var left = pageX+5;
  886 + if (windowWidth - pageX < tooltipWidth + 50) {
  887 + left = pageX - tooltipWidth - 10;
  888 + }
  889 + tbFlot.ctx.tooltip.css({
  890 + left: left
  891 + });
  892 +
  893 + if (multipleModeTooltip) {
  894 + for (var i = 0; i < hoverInfo.seriesHover.length; i++) {
  895 + var seriesHoverInfo = hoverInfo.seriesHover[i];
  896 + tbFlot.ctx.plot.highlight(seriesHoverInfo.index, seriesHoverInfo.hoverIndex);
  897 + }
  898 + }
  899 + }
  900 +
396 } else { 901 } else {
397 tbFlot.ctx.tooltip.stop(true); 902 tbFlot.ctx.tooltip.stop(true);
398 tbFlot.ctx.tooltip.hide(); 903 tbFlot.ctx.tooltip.hide();
  904 + tbFlot.ctx.plot.unhighlight();
399 } 905 }
400 }; 906 };
401 this.ctx.$container.bind('plothover', this.flotHoverHandler); 907 this.ctx.$container.bind('plothover', this.flotHoverHandler);
@@ -430,6 +936,7 @@ export default class TbFlot { @@ -430,6 +936,7 @@ export default class TbFlot {
430 this.mouseleaveHandler = function () { 936 this.mouseleaveHandler = function () {
431 tbFlot.ctx.tooltip.stop(true); 937 tbFlot.ctx.tooltip.stop(true);
432 tbFlot.ctx.tooltip.hide(); 938 tbFlot.ctx.tooltip.hide();
  939 + tbFlot.ctx.plot.unhighlight();
433 tbFlot.isMouseInteraction = false; 940 tbFlot.isMouseInteraction = false;
434 }; 941 };
435 this.ctx.$container.bind('mouseleave', this.mouseleaveHandler); 942 this.ctx.$container.bind('mouseleave', this.mouseleaveHandler);
@@ -467,6 +974,152 @@ export default class TbFlot { @@ -467,6 +974,152 @@ export default class TbFlot {
467 this.mouseleaveHandler = null; 974 this.mouseleaveHandler = null;
468 } 975 }
469 } 976 }
  977 +
  978 +
  979 + findHoverIndexFromData (posX, series) {
  980 + var lower = 0;
  981 + var upper = series.data.length - 1;
  982 + var middle;
  983 + var index = null;
  984 + while (index === null) {
  985 + if (lower > upper) {
  986 + return Math.max(upper, 0);
  987 + }
  988 + middle = Math.floor((lower + upper) / 2);
  989 + if (series.data[middle][0] === posX) {
  990 + return middle;
  991 + } else if (series.data[middle][0] < posX) {
  992 + lower = middle + 1;
  993 + } else {
  994 + upper = middle - 1;
  995 + }
  996 + }
  997 + }
  998 +
  999 + findHoverIndexFromDataPoints (posX, series, last) {
  1000 + var ps = series.datapoints.pointsize;
  1001 + var initial = last*ps;
  1002 + var len = series.datapoints.points.length;
  1003 + for (var j = initial; j < len; j += ps) {
  1004 + if ((!series.lines.steps && series.datapoints.points[initial] != null && series.datapoints.points[j] == null)
  1005 + || series.datapoints.points[j] > posX) {
  1006 + return Math.max(j - ps, 0)/ps;
  1007 + }
  1008 + }
  1009 + return j/ps - 1;
  1010 + }
  1011 +
  1012 +
  1013 + getHoverInfo (seriesList, pos) {
  1014 + var i, series, value, hoverIndex, hoverDistance, pointTime, minDistance, minTime;
  1015 + var last_value = 0;
  1016 + var results = {
  1017 + seriesHover: []
  1018 + };
  1019 + for (i = 0; i < seriesList.length; i++) {
  1020 + series = seriesList[i];
  1021 + hoverIndex = this.findHoverIndexFromData(pos.x, series);
  1022 + if (series.data[hoverIndex] && series.data[hoverIndex][0]) {
  1023 + hoverDistance = pos.x - series.data[hoverIndex][0];
  1024 + pointTime = series.data[hoverIndex][0];
  1025 +
  1026 + if (!minDistance
  1027 + || (hoverDistance >= 0 && (hoverDistance < minDistance || minDistance < 0))
  1028 + || (hoverDistance < 0 && hoverDistance > minDistance)) {
  1029 + minDistance = hoverDistance;
  1030 + minTime = pointTime;
  1031 + }
  1032 + if (series.stack) {
  1033 + if (this.ctx.tooltipIndividual) {
  1034 + value = series.data[hoverIndex][1];
  1035 + } else {
  1036 + last_value += series.data[hoverIndex][1];
  1037 + value = last_value;
  1038 + }
  1039 + } else {
  1040 + value = series.data[hoverIndex][1];
  1041 + }
  1042 +
  1043 + if (series.stack) {
  1044 + hoverIndex = this.findHoverIndexFromDataPoints(pos.x, series, hoverIndex);
  1045 + }
  1046 + results.seriesHover.push({
  1047 + value: value,
  1048 + hoverIndex: hoverIndex,
  1049 + color: series.dataKey.color,
  1050 + label: series.label,
  1051 + time: pointTime,
  1052 + distance: hoverDistance,
  1053 + index: i
  1054 + });
  1055 + }
  1056 + }
  1057 + results.time = minTime;
  1058 + return results;
  1059 + }
  1060 +
  1061 + pieDataRendered() {
  1062 + for (var i in this.ctx.pieTargetData) {
  1063 + var value = this.ctx.pieTargetData[i] ? this.ctx.pieTargetData[i] : 0;
  1064 + this.ctx.pieRenderedData[i] = value;
  1065 + if (!this.ctx.pieData[i].data[0]) {
  1066 + this.ctx.pieData[i].data[0] = [0,0];
  1067 + }
  1068 + this.ctx.pieData[i].data[0][1] = value;
  1069 + }
  1070 + }
  1071 +
  1072 + nextPieDataAnimation(start) {
  1073 + if (start) {
  1074 + this.finishPieDataAnimation();
  1075 + this.ctx.pieAnimationStartTime = this.ctx.pieAnimationLastTime = Date.now();
  1076 + for (var i in this.ctx.data) {
  1077 + this.ctx.pieTargetData[i] = (this.ctx.data[i].data && this.ctx.data[i].data[0])
  1078 + ? this.ctx.data[i].data[0][1] : 0;
  1079 + }
  1080 + }
  1081 + if (this.ctx.pieAnimationCaf) {
  1082 + this.ctx.pieAnimationCaf();
  1083 + this.ctx.pieAnimationCaf = null;
  1084 + }
  1085 + var self = this;
  1086 + this.ctx.pieAnimationCaf = this.ctx.$scope.tbRaf(
  1087 + function () {
  1088 + self.onPieDataAnimation();
  1089 + }
  1090 + );
  1091 + }
  1092 +
  1093 + onPieDataAnimation() {
  1094 + var time = Date.now();
  1095 + var elapsed = time - this.ctx.pieAnimationLastTime;//this.ctx.pieAnimationStartTime;
  1096 + var progress = (time - this.ctx.pieAnimationStartTime) / this.ctx.pieDataAnimationDuration;
  1097 + if (progress >= 1) {
  1098 + this.finishPieDataAnimation();
  1099 + } else {
  1100 + if (elapsed >= 40) {
  1101 + for (var i in this.ctx.pieTargetData) {
  1102 + var prevValue = this.ctx.pieRenderedData[i];
  1103 + var targetValue = this.ctx.pieTargetData[i];
  1104 + var value = prevValue + (targetValue - prevValue) * progress;
  1105 + if (!this.ctx.pieData[i].data[0]) {
  1106 + this.ctx.pieData[i].data[0] = [0,0];
  1107 + }
  1108 + this.ctx.pieData[i].data[0][1] = value;
  1109 + }
  1110 + this.ctx.plot.setData(this.ctx.pieData);
  1111 + this.ctx.plot.draw();
  1112 + this.ctx.pieAnimationLastTime = time;
  1113 + }
  1114 + this.nextPieDataAnimation(false);
  1115 + }
  1116 + }
  1117 +
  1118 + finishPieDataAnimation() {
  1119 + this.pieDataRendered();
  1120 + this.ctx.plot.setData(this.ctx.pieData);
  1121 + this.ctx.plot.draw();
  1122 + }
470 } 1123 }
471 1124
472 /* eslint-enable angular/angularelement */ 1125 /* eslint-enable angular/angularelement */
@@ -54,7 +54,7 @@ export default function WidgetLibraryController($scope, $rootScope, $q, widgetSe @@ -54,7 +54,7 @@ export default function WidgetLibraryController($scope, $rootScope, $q, widgetSe
54 widgetService.getBundleWidgetTypes(bundleAlias, isSystem).then( 54 widgetService.getBundleWidgetTypes(bundleAlias, isSystem).then(
55 function (widgetTypes) { 55 function (widgetTypes) {
56 56
57 - widgetTypes = $filter('orderBy')(widgetTypes, ['-descriptor.type','name']); 57 + widgetTypes = $filter('orderBy')(widgetTypes, ['-descriptor.type','-createdTime']);
58 58
59 var top = 0; 59 var top = 0;
60 var lastTop = [0, 0, 0]; 60 var lastTop = [0, 0, 0];