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 32 @RequestMapping("/api")
33 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 42 @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
36 43 @RequestMapping(value = "/dashboard/{dashboardId}", method = RequestMethod.GET)
37 44 @ResponseBody
... ...
... ... @@ -28,6 +28,7 @@ import org.thingsboard.server.extensions.core.plugin.telemetry.sub.SubscriptionT
28 28 @Data
29 29 public class TimeseriesSubscriptionCmd extends SubscriptionCmd {
30 30
  31 + private long startTs;
31 32 private long timeWindow;
32 33 private int limit;
33 34 private String agg;
... ...
... ... @@ -191,8 +191,8 @@ public class TelemetryWebsocketMsgHandler extends DefaultWebsocketMsgHandler {
191 191 if (cmd.getTimeWindow() > 0) {
192 192 List<String> keys = new ArrayList<>(getKeys(cmd).orElse(Collections.emptySet()));
193 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 196 List<TsKvQuery> queries = keys.stream().map(key -> new BaseTsKvQuery(key, startTs, endTs, getLimit(cmd.getLimit()), getAggregation(cmd.getAgg()))).collect(Collectors.toList());
197 197 ctx.loadTimeseries(deviceId, queries, getSubscriptionCallback(sessionRef, cmd, sessionId, deviceId, startTs, keys));
198 198 } else {
... ... @@ -234,7 +234,7 @@ public class TelemetryWebsocketMsgHandler extends DefaultWebsocketMsgHandler {
234 234 return new PluginCallback<List<TsKvEntry>>() {
235 235 @Override
236 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 239 Map<String, Long> subState = new HashMap<>(keys.size());
240 240 keys.forEach(key -> subState.put(key, startTs));
... ...
... ... @@ -26,16 +26,10 @@ public class SubscriptionUpdate {
26 26 private int errorCode;
27 27 private String errorMsg;
28 28 private Map<String, List<Object>> data;
29   - private long serverStartTs;
30 29
31 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 31 super();
37 32 this.subscriptionId = subscriptionId;
38   - this.serverStartTs = serverStartTs;
39 33 this.data = new TreeMap<>();
40 34 for (TsKvEntry tsEntry : data) {
41 35 List<Object> values = this.data.get(tsEntry.getKey());
... ... @@ -95,13 +89,9 @@ public class SubscriptionUpdate {
95 89 return errorMsg;
96 90 }
97 91
98   - public long getServerStartTs() {
99   - return serverStartTs;
100   - }
101   -
102 92 @Override
103 93 public String toString() {
104 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 22 var service = {
23 23 assignDashboardToCustomer: assignDashboardToCustomer,
24 24 getCustomerDashboards: getCustomerDashboards,
  25 + getServerTimeDiff: getServerTimeDiff,
25 26 getDashboard: getDashboard,
26 27 getTenantDashboards: getTenantDashboards,
27 28 deleteDashboard: deleteDashboard,
... ... @@ -71,6 +72,21 @@ function DashboardService($http, $q) {
71 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 90 function getDashboard(dashboardId) {
75 91 var deferred = $q.defer();
76 92 var url = '/api/dashboard/' + dashboardId;
... ...
... ... @@ -16,31 +16,26 @@
16 16
17 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 20 this.onDataCb = onDataCb;
  21 + this.tsKeyNames = tsKeyNames;
  22 + this.startTs = startTs;
21 23 this.aggregationType = aggregationType;
22 24 this.types = types;
23 25 this.$timeout = $timeout;
24 26 this.$filter = $filter;
25 27 this.dataReceived = false;
26 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 32 this.aggregationTimeout = this.interval;
38 33 switch (aggregationType) {
39 34 case types.aggregation.min.value:
40 35 this.aggFunction = min;
41 36 break;
42 37 case types.aggregation.max.value:
43   - this.aggFunction = max
  38 + this.aggFunction = max;
44 39 break;
45 40 case types.aggregation.avg.value:
46 41 this.aggFunction = avg;
... ... @@ -59,42 +54,56 @@ export default class DataAggregator {
59 54 }
60 55 }
61 56
62   - onData(data) {
  57 + onData(data, update, history) {
63 58 if (!this.dataReceived) {
64 59 this.elapsed = 0;
65 60 this.dataReceived = true;
66   - this.startTs = data.serverStartTs;
67 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 70 } else {
71 71 updateAggregatedData(this.aggregationMap, this.aggregationType === this.types.aggregation.count.value,
72 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 80 var now = currentTime();
78 81 this.elapsed += now - startedTime;
79 82 if (this.intervalTimeoutHandle) {
80 83 this.$timeout.cancel(this.intervalTimeoutHandle);
81 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 97 if (this.onDataCb) {
91 98 this.onDataCb(this.data, this.startTs, this.endTs);
92 99 }
93 100
94 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 109 reset() {
... ... @@ -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 185 var data = {};
  186 + for (var k in tsKeyNames) {
  187 + data[tsKeyNames[k]] = [];
  188 + }
177 189 for (var key in aggregationMap) {
178   - if (!data[key]) {
179   - data[key] = [];
180   - }
181 190 var aggKeyData = aggregationMap[key];
182 191 var keyData = data[key];
183 192 for (var aggTimestamp in aggKeyData) {
... ... @@ -185,7 +194,7 @@ function toData(aggregationMap, startTs, endTs, $filter, limit) {
185 194 delete aggKeyData[aggTimestamp];
186 195 } else if (aggTimestamp <= endTs) {
187 196 var aggData = aggKeyData[aggTimestamp];
188   - var kvPair = [aggTimestamp, aggData.aggValue];
  197 + var kvPair = [Number(aggTimestamp), aggData.aggValue];
189 198 keyData.push(kvPair);
190 199 }
191 200 }
... ...
... ... @@ -108,9 +108,9 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic
108 108 datasourceSubscription.subscriptionTimewindow.fixedWindow;
109 109 var realtime = datasourceSubscription.subscriptionTimewindow &&
110 110 datasourceSubscription.subscriptionTimewindow.realtimeWindowMs;
111   - var dataGenFunction = null;
112 111 var timer;
113 112 var frequency;
  113 + var dataAggregator;
114 114
115 115 var subscription = {
116 116 addListener: addListener,
... ... @@ -131,19 +131,20 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic
131 131 dataKey.index = i;
132 132 var key;
133 133 if (datasourceType === types.datasourceType.function) {
134   - key = utils.objectHashCode(dataKey);
135 134 if (!dataKey.func) {
136 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 138 if (dataKey.postFuncBody && !dataKey.postFunc) {
145 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 148 var dataKeysList = dataKeys[key];
148 149 if (!dataKeysList) {
149 150 dataKeysList = [];
... ... @@ -153,24 +154,19 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic
153 154 datasourceData[key + '_' + index] = {
154 155 data: []
155 156 };
  157 + } else {
  158 + key = utils.objectHashCode(dataKey);
  159 + datasourceData[key] = {
  160 + data: []
  161 + };
  162 + dataKeys[key] = dataKey;
156 163 }
157 164 dataKey.key = key;
158 165 }
159 166 if (datasourceType === types.datasourceType.function) {
160 167 frequency = 1000;
161 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 189 function syncListener(listener) {
194 190 var key;
195 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 193 for (key in dataKeys) {
205 194 var dataKeysList = dataKeys[key];
206 195 for (var i = 0; i < dataKeysList.length; i++) {
... ... @@ -211,6 +200,13 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic
211 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 214 if (history && !hasListeners()) {
219 215 return;
220 216 }
221   - //$log.debug("started!");
  217 + var subsTw = datasourceSubscription.subscriptionTimewindow;
  218 + var tsKeyNames = [];
  219 + var dataKey;
  220 +
222 221 if (datasourceType === types.datasourceType.device) {
223 222
224 223 //send subscribe command
... ... @@ -228,12 +227,13 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic
228 227
229 228 for (var key in dataKeys) {
230 229 var dataKeysList = dataKeys[key];
231   - var dataKey = dataKeysList[0];
  230 + dataKey = dataKeysList[0];
232 231 if (dataKey.type === types.dataKeyType.timeseries) {
233 232 if (tsKeys.length > 0) {
234 233 tsKeys += ',';
235 234 }
236 235 tsKeys += dataKey.name;
  236 + tsKeyNames.push(dataKey.name);
237 237 } else if (dataKey.type === types.dataKeyType.attribute) {
238 238 if (attrKeys.length > 0) {
239 239 attrKeys += ',';
... ... @@ -252,10 +252,10 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic
252 252 var historyCommand = {
253 253 deviceId: datasourceSubscription.deviceId,
254 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 261 subscriber = {
... ... @@ -287,16 +287,20 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic
287 287 };
288 288
289 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 295 function(data, startTs, endTs) {
295 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 304 types,
301 305 $timeout,
302 306 $filter
... ... @@ -308,9 +312,6 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic
308 312 dataAggregator.reset();
309 313 onReconnected();
310 314 }
311   - subscriber.onDestroy = function() {
312   - dataAggregator.destroy();
313   - }
314 315 } else {
315 316 subscriber.onReconnected = function() {
316 317 onReconnected();
... ... @@ -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 381 if (history) {
358 382 onTick();
359 383 } else {
... ... @@ -377,30 +401,17 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic
377 401 }
378 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 411 var data = [];
402 412 var prevSeries;
403   - var datasourceKeyData = datasourceData[dataKey.key].data;
  413 + var datasourceDataKey = dataKey.key + '_' + index;
  414 + var datasourceKeyData = datasourceData[datasourceDataKey].data;
404 415 if (datasourceKeyData.length > 0) {
405 416 prevSeries = datasourceKeyData[datasourceKeyData.length - 1];
406 417 } else {
... ... @@ -417,18 +428,7 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic
417 428 if (data.length > 0) {
418 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 434 function generateLatest(dataKey) {
... ... @@ -458,23 +458,32 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic
458 458 if (datasourceSubscription.type === types.widgetType.timeseries.value) {
459 459 var startTime;
460 460 var endTime;
  461 + var generatedData = {
  462 + data: {
  463 + }
  464 + };
461 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 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 487 } else if (datasourceSubscription.type === types.widgetType.latest.value) {
479 488 for (key in dataKeys) {
480 489 generateLatest(dataKeys[key]);
... ... @@ -568,8 +577,6 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic
568 577 }
569 578 if (data.length > 0 || (startTs && endTs)) {
570 579 datasourceData[datasourceKey].data = data;
571   - datasourceData[datasourceKey].startTs = startTs;
572   - datasourceData[datasourceKey].endTs = endTs;
573 580 for (var i2 in listeners) {
574 581 var listener = listeners[i2];
575 582 listener.dataUpdated(datasourceData[datasourceKey],
... ...
... ... @@ -68,6 +68,7 @@ function Dashboard() {
68 68 prepareDashboardContextMenu: '&?',
69 69 prepareWidgetContextMenu: '&?',
70 70 loadWidgets: '&?',
  71 + getStDiff: '&?',
71 72 onInit: '&?',
72 73 onInitFailed: '&?',
73 74 dashboardStyle: '=?'
... ... @@ -94,6 +95,8 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, $
94 95
95 96 vm.gridster = null;
96 97
  98 + vm.stDiff = 0;
  99 +
97 100 vm.isMobileDisabled = angular.isDefined(vm.isMobileDisabled) ? vm.isMobileDisabled : false;
98 101
99 102 vm.dashboardLoading = true;
... ... @@ -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 331 function loadDashboard() {
308 332 resetWidgetClick();
... ...
... ... @@ -93,7 +93,7 @@
93 93 </div>
94 94 <div flex layout="column" class="tb-widget-content">
95 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 97 </div>
98 98 </div>
99 99 </div>
... ...
... ... @@ -20,7 +20,7 @@ import 'javascript-detect-element-resize/detect-element-resize';
20 20
21 21 /*@ngInject*/
22 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 25 var vm = this;
26 26
... ... @@ -46,7 +46,6 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
46 46 realtimeWindowMs: null,
47 47 aggregation: null
48 48 };
49   - var dataUpdateTimer = null;
50 49 var dataUpdateCaf = null;
51 50
52 51 /*
... ... @@ -72,7 +71,9 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
72 71 settings: widget.config.settings,
73 72 datasources: widget.config.datasources,
74 73 data: [],
75   - timeWindow: {},
  74 + timeWindow: {
  75 + stDiff: stDiff
  76 + },
76 77 timewindowFunctions: {
77 78 onUpdateTimewindow: onUpdateTimewindow,
78 79 onResetTimewindow: onResetTimewindow
... ... @@ -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 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 163 } else if (subscriptionTimewindow.fixedWindow) {
162 164 widgetContext.timeWindow.maxTime = subscriptionTimewindow.fixedWindow.endTimeMs;
163 165 widgetContext.timeWindow.minTime = subscriptionTimewindow.fixedWindow.startTimeMs;
... ... @@ -165,10 +167,6 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
165 167 }
166 168
167 169 function onDataUpdated() {
168   - if (dataUpdateTimer) {
169   - $timeout.cancel(dataUpdateTimer);
170   - dataUpdateTimer = null;
171   - }
172 170 if (widgetContext.inited) {
173 171 if (dataUpdateCaf) {
174 172 dataUpdateCaf();
... ... @@ -496,7 +494,8 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
496 494 startTimeMs: startTimeMs,
497 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 512 }
514 513 if (update) {
515 514 if (subscriptionTimewindow.realtimeWindowMs) {
516   - updateTimewindow(sourceData.startTs, sourceData.endTs);
  515 + updateTimewindow();
517 516 }
518 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 547
553 548 function unsubscribe() {
554 549 if (widget.type !== types.widgetType.rpc.value) {
555   - if (dataUpdateTimer) {
556   - $timeout.cancel(dataUpdateTimer);
557   - dataUpdateTimer = null;
558   - }
559 550 for (var i in datasourceListeners) {
560 551 var listener = datasourceListeners[i];
561 552 datasourceService.unsubscribeFromDatasource(listener);
... ... @@ -575,7 +566,7 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
575 566 };
576 567 if (widget.type === types.widgetType.timeseries.value &&
577 568 angular.isDefined(widget.config.timewindow)) {
578   -
  569 + var timeWindow = 0;
579 570 if (angular.isDefined(widget.config.timewindow.aggregation)) {
580 571 subscriptionTimewindow.aggregation = {
581 572 limit: widget.config.timewindow.aggregation.limit || 200,
... ... @@ -585,6 +576,8 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
585 576
586 577 if (angular.isDefined(widget.config.timewindow.realtime)) {
587 578 subscriptionTimewindow.realtimeWindowMs = widget.config.timewindow.realtime.timewindowMs;
  579 + subscriptionTimewindow.startTs = (new Date).getTime() + widgetContext.timeWindow.stDiff - subscriptionTimewindow.realtimeWindowMs;
  580 + timeWindow = subscriptionTimewindow.realtimeWindowMs;
588 581 } else if (angular.isDefined(widget.config.timewindow.history)) {
589 582 if (angular.isDefined(widget.config.timewindow.history.timewindowMs)) {
590 583 var currentTime = (new Date).getTime();
... ... @@ -592,14 +585,31 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
592 585 startTimeMs: currentTime - widget.config.timewindow.history.timewindowMs,
593 586 endTimeMs: currentTime
594 587 }
  588 + timeWindow = widget.config.timewindow.history.timewindowMs;
595 589 } else {
596 590 subscriptionTimewindow.fixedWindow = {
597 591 startTimeMs: widget.config.timewindow.history.fixedTimewindow.startTimeMs,
598 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 609 updateTimewindow();
  610 + if (subscriptionTimewindow.fixedWindow) {
  611 + onDataUpdated();
  612 + }
603 613 }
604 614 for (var i in widget.config.datasources) {
605 615 var datasource = widget.config.datasources[i];
... ...
... ... @@ -61,6 +61,7 @@ export default function DashboardController(types, widgetService, userService,
61 61 vm.isTenantAdmin = isTenantAdmin;
62 62 vm.isSystemAdmin = isSystemAdmin;
63 63 vm.loadDashboard = loadDashboard;
  64 + vm.getServerTimeDiff = getServerTimeDiff;
64 65 vm.noData = noData;
65 66 vm.onAddWidgetClosed = onAddWidgetClosed;
66 67 vm.onEditWidgetClosed = onEditWidgetClosed;
... ... @@ -94,10 +95,9 @@ export default function DashboardController(types, widgetService, userService,
94 95 widgetService.getBundleWidgetTypes(bundleAlias, isSystem).then(
95 96 function (widgetTypes) {
96 97
97   - widgetTypes = $filter('orderBy')(widgetTypes, ['-name']);
  98 + widgetTypes = $filter('orderBy')(widgetTypes, ['-createdTime']);
98 99
99 100 var top = 0;
100   - var sizeY = 0;
101 101
102 102 if (widgetTypes.length > 0) {
103 103 loadNext(0);
... ... @@ -135,7 +135,7 @@ export default function DashboardController(types, widgetService, userService,
135 135 } else if (widgetTypeInfo.type === types.widgetType.static.value) {
136 136 vm.staticWidgetTypes.push(widget);
137 137 }
138   - top += sizeY;
  138 + top += widget.sizeY;
139 139 loadNextOrComplete(i);
140 140
141 141 }
... ... @@ -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 151 function loadDashboard() {
148 152
149 153 var deferred = $q.defer();
... ...
... ... @@ -91,6 +91,7 @@
91 91 prepare-widget-context-menu="vm.prepareWidgetContextMenu(widget)"
92 92 on-remove-widget="vm.removeWidget(event, widget)"
93 93 load-widgets="vm.loadDashboard()"
  94 + get-st-diff="vm.getServerTimeDiff()"
94 95 on-init="vm.dashboardInited(dashboard)"
95 96 on-init-failed="vm.dashboardInitFailed(e)">
96 97 </tb-dashboard>
... ...
... ... @@ -29,7 +29,7 @@ import EditAttributeValueController from './edit-attribute-value.controller';
29 29
30 30 /*@ngInject*/
31 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 34 var linker = function (scope, element, attrs) {
35 35
... ... @@ -357,6 +357,10 @@ export default function AttributeTableDirective($compile, $templateCache, $rootS
357 357 scope.getDeviceAttributes(true);
358 358 }
359 359
  360 + scope.getServerTimeDiff = function() {
  361 + return dashboardService.getServerTimeDiff();
  362 + }
  363 +
360 364 scope.addWidgetToDashboard = function($event) {
361 365 if (scope.mode === 'widget' && scope.widgetsListCache.length > 0) {
362 366 var widget = scope.widgetsListCache[scope.widgetsCarousel.index][0];
... ...
... ... @@ -158,8 +158,9 @@
158 158 <tb-dashboard
159 159 device-alias-list="deviceAliases"
160 160 widgets="widgets"
  161 + get-st-diff="getServerTimeDiff()"
161 162 columns="20"
162   - is-edit="true"
  163 + is-edit="false"
163 164 is-mobile-disabled="true"
164 165 is-edit-action-enabled="false"
165 166 is-remove-action-enabled="false">
... ...
... ... @@ -22,6 +22,8 @@ import 'flot/src/jquery.flot';
22 22 import 'flot/src/plugins/jquery.flot.time';
23 23 import 'flot/src/plugins/jquery.flot.selection';
24 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 28 /* eslint-disable angular/angularelement */
27 29 export default class TbFlot {
... ... @@ -38,8 +40,8 @@ export default class TbFlot {
38 40 var keySettings = series.dataKey.settings;
39 41
40 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 47 series.points = {
... ... @@ -58,36 +60,34 @@ export default class TbFlot {
58 60 series.highlightColor = lineColor.toRgbString();
59 61
60 62 }
61   -
62   - var tbFlot = this;
63   -
64 63 ctx.tooltip = $('#flot-series-tooltip');
65 64 if (ctx.tooltip.length === 0) {
66 65 ctx.tooltip = $("<div id=flot-series-tooltip' class='flot-mouse-value'></div>");
67 66 ctx.tooltip.css({
68 67 fontSize: "12px",
69 68 fontFamily: "Roboto",
70   - lineHeight: "24px",
  69 + fontWeight: "300",
  70 + lineHeight: "18px",
71 71 opacity: "1",
72 72 backgroundColor: "rgba(0,0,0,0.7)",
73   - color: "#fff",
  73 + color: "#D9DADB",
74 74 position: "absolute",
75 75 display: "none",
76 76 zIndex: "100",
77   - padding: "2px 8px",
  77 + padding: "4px 10px",
78 78 borderRadius: "4px"
79 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 91 var lineSpan = $('<span></span>');
92 92 lineSpan.css({
93 93 backgroundColor: color,
... ... @@ -97,27 +97,76 @@ export default class TbFlot {
97 97 verticalAlign: "middle",
98 98 marginRight: "5px"
99 99 });
100   - content += lineSpan.prop('outerHTML');
101   -
  100 + divElement.append(lineSpan);
102 101 var labelSpan = $('<span>' + label + ':</span>');
103 102 labelSpan.css({
104 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 167 var settings = ctx.settings;
120 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 171 var font = {
123 172 color: settings.fontColor || "#545454",
... ... @@ -134,7 +183,7 @@ export default class TbFlot {
134 183 grid: {
135 184 hoverable: true,
136 185 mouseActiveRadius: 10,
137   - autoHighlight: true
  186 + autoHighlight: ctx.tooltipIndividual === true
138 187 },
139 188 selection : { mode : ctx.isMobile ? null : 'x' },
140 189 legend : {
... ... @@ -155,7 +204,7 @@ export default class TbFlot {
155 204 settings.legend.backgroundOpacity : 0.85;
156 205 }
157 206
158   - if (this.chartType === 'line') {
  207 + if (this.chartType === 'line' || this.chartType === 'bar') {
159 208 options.xaxis = {
160 209 mode: 'time',
161 210 timezone: 'browser',
... ... @@ -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 282 options.xaxis.min = ctx.timeWindow.minTime;
212 283 options.xaxis.max = ctx.timeWindow.maxTime;
213 284 } else if (this.chartType === 'pie') {
... ... @@ -271,11 +342,12 @@ export default class TbFlot {
271 342
272 343 update() {
273 344 if (!this.isMouseInteraction) {
274   - if (this.chartType === 'line') {
  345 + if (this.chartType === 'line' || this.chartType === 'bar') {
275 346 this.ctx.plot.getOptions().xaxes[0].min = this.ctx.timeWindow.minTime;
276 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 351 this.ctx.plot.setData(this.ctx.data);
280 352 this.ctx.plot.setupGrid();
281 353 this.ctx.plot.draw();
... ... @@ -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 836 checkMouseEvents() {
... ... @@ -378,24 +850,58 @@ export default class TbFlot {
378 850
379 851 if (!this.flotHoverHandler) {
380 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 901 } else {
397 902 tbFlot.ctx.tooltip.stop(true);
398 903 tbFlot.ctx.tooltip.hide();
  904 + tbFlot.ctx.plot.unhighlight();
399 905 }
400 906 };
401 907 this.ctx.$container.bind('plothover', this.flotHoverHandler);
... ... @@ -430,6 +936,7 @@ export default class TbFlot {
430 936 this.mouseleaveHandler = function () {
431 937 tbFlot.ctx.tooltip.stop(true);
432 938 tbFlot.ctx.tooltip.hide();
  939 + tbFlot.ctx.plot.unhighlight();
433 940 tbFlot.isMouseInteraction = false;
434 941 };
435 942 this.ctx.$container.bind('mouseleave', this.mouseleaveHandler);
... ... @@ -467,6 +974,152 @@ export default class TbFlot {
467 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 1125 /* eslint-enable angular/angularelement */
\ No newline at end of file
... ...
... ... @@ -54,7 +54,7 @@ export default function WidgetLibraryController($scope, $rootScope, $q, widgetSe
54 54 widgetService.getBundleWidgetTypes(bundleAlias, isSystem).then(
55 55 function (widgetTypes) {
56 56
57   - widgetTypes = $filter('orderBy')(widgetTypes, ['-descriptor.type','name']);
  57 + widgetTypes = $filter('orderBy')(widgetTypes, ['-descriptor.type','-createdTime']);
58 58
59 59 var top = 0;
60 60 var lastTop = [0, 0, 0];
... ...