thingsboard-aplcache

Details

diff --git a/application/src/main/java/org/thingsboard/server/controller/DashboardController.java b/application/src/main/java/org/thingsboard/server/controller/DashboardController.java
index 77898ce..fc419b7 100644
--- a/application/src/main/java/org/thingsboard/server/controller/DashboardController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/DashboardController.java
@@ -32,6 +32,13 @@ import org.thingsboard.server.exception.ThingsboardException;
 @RequestMapping("/api")
 public class DashboardController extends BaseController {
 
+    @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
+    @RequestMapping(value = "/dashboard/serverTime", method = RequestMethod.GET)
+    @ResponseBody
+    public long getServerTime() throws ThingsboardException {
+        return System.currentTimeMillis();
+    }
+
     @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
     @RequestMapping(value = "/dashboard/{dashboardId}", method = RequestMethod.GET)
     @ResponseBody
diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/cmd/TimeseriesSubscriptionCmd.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/cmd/TimeseriesSubscriptionCmd.java
index f4eacf5..20bd3e2 100644
--- a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/cmd/TimeseriesSubscriptionCmd.java
+++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/cmd/TimeseriesSubscriptionCmd.java
@@ -28,6 +28,7 @@ import org.thingsboard.server.extensions.core.plugin.telemetry.sub.SubscriptionT
 @Data
 public class TimeseriesSubscriptionCmd extends SubscriptionCmd {
 
+    private long startTs;
     private long timeWindow;
     private int limit;
     private String agg;
diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryWebsocketMsgHandler.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryWebsocketMsgHandler.java
index 8385bf1..51181fd 100644
--- a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryWebsocketMsgHandler.java
+++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryWebsocketMsgHandler.java
@@ -191,8 +191,8 @@ public class TelemetryWebsocketMsgHandler extends DefaultWebsocketMsgHandler {
                     if (cmd.getTimeWindow() > 0) {
                         List<String> keys = new ArrayList<>(getKeys(cmd).orElse(Collections.emptySet()));
                         log.debug("[{}] fetching timeseries data for last {} ms for keys: ({}) for device : {}", sessionId, cmd.getTimeWindow(), cmd.getKeys(), cmd.getDeviceId());
-                        long endTs = System.currentTimeMillis();
-                        startTs = endTs - cmd.getTimeWindow();
+                        startTs = cmd.getStartTs();
+                        long endTs = cmd.getStartTs() + cmd.getTimeWindow();
                         List<TsKvQuery> queries = keys.stream().map(key -> new BaseTsKvQuery(key, startTs, endTs, getLimit(cmd.getLimit()), getAggregation(cmd.getAgg()))).collect(Collectors.toList());
                         ctx.loadTimeseries(deviceId, queries, getSubscriptionCallback(sessionRef, cmd, sessionId, deviceId, startTs, keys));
                     } else {
@@ -234,7 +234,7 @@ public class TelemetryWebsocketMsgHandler extends DefaultWebsocketMsgHandler {
         return new PluginCallback<List<TsKvEntry>>() {
             @Override
             public void onSuccess(PluginContext ctx, List<TsKvEntry> data) {
-                sendWsMsg(ctx, sessionRef, new SubscriptionUpdate(cmd.getCmdId(), startTs, data));
+                sendWsMsg(ctx, sessionRef, new SubscriptionUpdate(cmd.getCmdId(), data));
 
                 Map<String, Long> subState = new HashMap<>(keys.size());
                 keys.forEach(key -> subState.put(key, startTs));
diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/sub/SubscriptionUpdate.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/sub/SubscriptionUpdate.java
index 8a9e7b2..4d8cf53 100644
--- a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/sub/SubscriptionUpdate.java
+++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/sub/SubscriptionUpdate.java
@@ -26,16 +26,10 @@ public class SubscriptionUpdate {
     private int errorCode;
     private String errorMsg;
     private Map<String, List<Object>> data;
-    private long serverStartTs;
 
     public SubscriptionUpdate(int subscriptionId, List<TsKvEntry> data) {
-        this(subscriptionId, 0L, data);
-    }
-
-    public SubscriptionUpdate(int subscriptionId, long serverStartTs, List<TsKvEntry> data) {
         super();
         this.subscriptionId = subscriptionId;
-        this.serverStartTs = serverStartTs;
         this.data = new TreeMap<>();
         for (TsKvEntry tsEntry : data) {
             List<Object> values = this.data.get(tsEntry.getKey());
@@ -95,13 +89,9 @@ public class SubscriptionUpdate {
         return errorMsg;
     }
 
-    public long getServerStartTs() {
-        return serverStartTs;
-    }
-
     @Override
     public String toString() {
         return "SubscriptionUpdate [subscriptionId=" + subscriptionId + ", errorCode=" + errorCode + ", errorMsg=" + errorMsg + ", data="
-                + data + ", serverStartTs=" + serverStartTs+ "]";
+                + data + "]";
     }
 }
diff --git a/ui/src/app/api/dashboard.service.js b/ui/src/app/api/dashboard.service.js
index 9eb7329..be450ed 100644
--- a/ui/src/app/api/dashboard.service.js
+++ b/ui/src/app/api/dashboard.service.js
@@ -22,6 +22,7 @@ function DashboardService($http, $q) {
     var service = {
         assignDashboardToCustomer: assignDashboardToCustomer,
         getCustomerDashboards: getCustomerDashboards,
+        getServerTimeDiff: getServerTimeDiff,
         getDashboard: getDashboard,
         getTenantDashboards: getTenantDashboards,
         deleteDashboard: deleteDashboard,
@@ -71,6 +72,21 @@ function DashboardService($http, $q) {
         return deferred.promise;
     }
 
+    function getServerTimeDiff() {
+        var deferred = $q.defer();
+        var url = '/api/dashboard/serverTime';
+        var ct1 = Date.now();
+        $http.get(url, null).then(function success(response) {
+            var ct2 = Date.now();
+            var st = response.data;
+            var stDiff = Math.ceil(st - (ct1+ct2)/2);
+            deferred.resolve(stDiff);
+        }, function fail() {
+            deferred.reject();
+        });
+        return deferred.promise;
+    }
+
     function getDashboard(dashboardId) {
         var deferred = $q.defer();
         var url = '/api/dashboard/' + dashboardId;
diff --git a/ui/src/app/api/data-aggregator.js b/ui/src/app/api/data-aggregator.js
index 314ba64..e273a9d 100644
--- a/ui/src/app/api/data-aggregator.js
+++ b/ui/src/app/api/data-aggregator.js
@@ -16,31 +16,26 @@
 
 export default class DataAggregator {
 
-    constructor(onDataCb, limit, aggregationType, timeWindow, types, $timeout, $filter) {
+    constructor(onDataCb, tsKeyNames, startTs, limit, aggregationType, timeWindow, interval, types, $timeout, $filter) {
         this.onDataCb = onDataCb;
+        this.tsKeyNames = tsKeyNames;
+        this.startTs = startTs;
         this.aggregationType = aggregationType;
         this.types = types;
         this.$timeout = $timeout;
         this.$filter = $filter;
         this.dataReceived = false;
         this.noAggregation = aggregationType === types.aggregation.none.value;
-        var interval = Math.floor(timeWindow / limit);
-        if (!this.noAggregation) {
-            this.interval = Math.max(interval, 1000);
-            this.limit = Math.ceil(interval/this.interval * limit);
-            this.timeWindow = this.interval * this.limit;
-        } else {
-            this.limit = limit;
-            this.timeWindow = interval * this.limit;
-            this.interval = 1000;
-        }
+        this.limit = limit;
+        this.timeWindow = timeWindow;
+        this.interval = interval;
         this.aggregationTimeout = this.interval;
         switch (aggregationType) {
             case types.aggregation.min.value:
                 this.aggFunction = min;
                 break;
             case types.aggregation.max.value:
-                this.aggFunction = max
+                this.aggFunction = max;
                 break;
             case types.aggregation.avg.value:
                 this.aggFunction = avg;
@@ -59,42 +54,56 @@ export default class DataAggregator {
         }
     }
 
-    onData(data) {
+    onData(data, update, history) {
         if (!this.dataReceived) {
             this.elapsed = 0;
             this.dataReceived = true;
-            this.startTs = data.serverStartTs;
             this.endTs = this.startTs + this.timeWindow;
-            this.aggregationMap = processAggregatedData(data.data, this.aggregationType === this.types.aggregation.count.value, this.noAggregation);
-            this.onInterval(currentTime());
+            if (update) {
+                this.aggregationMap = {};
+                updateAggregatedData(this.aggregationMap, this.aggregationType === this.types.aggregation.count.value,
+                    this.noAggregation, this.aggFunction, data.data, this.interval, this.startTs);
+            } else {
+                this.aggregationMap = processAggregatedData(data.data, this.aggregationType === this.types.aggregation.count.value, this.noAggregation);
+            }
+            this.onInterval(currentTime(), history);
         } else {
             updateAggregatedData(this.aggregationMap, this.aggregationType === this.types.aggregation.count.value,
                 this.noAggregation, this.aggFunction, data.data, this.interval, this.startTs);
+            if (history) {
+                this.onInterval(currentTime(), history);
+            }
         }
     }
 
-    onInterval(startedTime) {
+    onInterval(startedTime, history) {
         var now = currentTime();
         this.elapsed += now - startedTime;
         if (this.intervalTimeoutHandle) {
             this.$timeout.cancel(this.intervalTimeoutHandle);
             this.intervalTimeoutHandle = null;
         }
-        var delta = Math.floor(this.elapsed / this.interval);
-        if (delta || !this.data) {
-            this.startTs += delta * this.interval;
-            this.endTs += delta * this.interval;
-            this.data = toData(this.aggregationMap, this.startTs, this.endTs, this.$filter, this.limit);
-            this.elapsed = this.elapsed - delta * this.interval;
+        if (!history) {
+            var delta = Math.floor(this.elapsed / this.interval);
+            if (delta || !this.data) {
+                this.startTs += delta * this.interval;
+                this.endTs += delta * this.interval;
+                this.data = toData(this.tsKeyNames, this.aggregationMap, this.startTs, this.endTs, this.$filter, this.limit);
+                this.elapsed = this.elapsed - delta * this.interval;
+            }
+        } else {
+            this.data = toData(this.tsKeyNames, this.aggregationMap, this.startTs, this.endTs, this.$filter, this.limit);
         }
         if (this.onDataCb) {
             this.onDataCb(this.data, this.startTs, this.endTs);
         }
 
         var self = this;
-        this.intervalTimeoutHandle = this.$timeout(function() {
-            self.onInterval(now);
-        }, this.aggregationTimeout, false);
+        if (!history) {
+            this.intervalTimeoutHandle = this.$timeout(function() {
+                self.onInterval(now);
+            }, this.aggregationTimeout, false);
+        }
     }
 
     reset() {
@@ -172,12 +181,12 @@ function updateAggregatedData(aggregationMap, isCount, noAggregation, aggFunctio
     }
 }
 
-function toData(aggregationMap, startTs, endTs, $filter, limit) {
+function toData(tsKeyNames, aggregationMap, startTs, endTs, $filter, limit) {
     var data = {};
+    for (var k in tsKeyNames) {
+        data[tsKeyNames[k]] = [];
+    }
     for (var key in aggregationMap) {
-        if (!data[key]) {
-            data[key] = [];
-        }
         var aggKeyData = aggregationMap[key];
         var keyData = data[key];
         for (var aggTimestamp in aggKeyData) {
@@ -185,7 +194,7 @@ function toData(aggregationMap, startTs, endTs, $filter, limit) {
                 delete aggKeyData[aggTimestamp];
             } else if (aggTimestamp <= endTs) {
                 var aggData = aggKeyData[aggTimestamp];
-                var kvPair = [aggTimestamp, aggData.aggValue];
+                var kvPair = [Number(aggTimestamp), aggData.aggValue];
                 keyData.push(kvPair);
             }
         }
diff --git a/ui/src/app/api/datasource.service.js b/ui/src/app/api/datasource.service.js
index acfe124..b44f85d 100644
--- a/ui/src/app/api/datasource.service.js
+++ b/ui/src/app/api/datasource.service.js
@@ -108,9 +108,9 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic
         datasourceSubscription.subscriptionTimewindow.fixedWindow;
     var realtime = datasourceSubscription.subscriptionTimewindow &&
         datasourceSubscription.subscriptionTimewindow.realtimeWindowMs;
-    var dataGenFunction = null;
     var timer;
     var frequency;
+    var dataAggregator;
 
     var subscription = {
         addListener: addListener,
@@ -131,19 +131,20 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic
             dataKey.index = i;
             var key;
             if (datasourceType === types.datasourceType.function) {
-                key = utils.objectHashCode(dataKey);
                 if (!dataKey.func) {
                     dataKey.func = new Function("time", "prevValue", dataKey.funcBody);
                 }
-                datasourceData[key] = {
-                    data: []
-                };
-                dataKeys[key] = dataKey;
-            } else if (datasourceType === types.datasourceType.device) {
-                key = dataKey.name + '_' + dataKey.type;
+            } else {
                 if (dataKey.postFuncBody && !dataKey.postFunc) {
                     dataKey.postFunc = new Function("time", "value", "prevValue", dataKey.postFuncBody);
                 }
+            }
+            if (datasourceType === types.datasourceType.device || datasourceSubscription.type === types.widgetType.timeseries.value) {
+                if (datasourceType === types.datasourceType.function) {
+                    key = dataKey.name + '_' + dataKey.index + '_' + dataKey.type;
+                } else {
+                    key = dataKey.name + '_' + dataKey.type;
+                }
                 var dataKeysList = dataKeys[key];
                 if (!dataKeysList) {
                     dataKeysList = [];
@@ -153,24 +154,19 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic
                 datasourceData[key + '_' + index] = {
                     data: []
                 };
+            } else {
+                key = utils.objectHashCode(dataKey);
+                datasourceData[key] = {
+                    data: []
+                };
+                dataKeys[key] = dataKey;
             }
             dataKey.key = key;
         }
         if (datasourceType === types.datasourceType.function) {
             frequency = 1000;
             if (datasourceSubscription.type === types.widgetType.timeseries.value) {
-                dataGenFunction = generateSeries;
-                var window;
-                if (realtime) {
-                    window = datasourceSubscription.subscriptionTimewindow.realtimeWindowMs;
-                } else {
-                    window = datasourceSubscription.subscriptionTimewindow.fixedWindow.endTimeMs -
-                        datasourceSubscription.subscriptionTimewindow.fixedWindow.startTimeMs;
-                }
-                frequency = window / 1000 * 20;
-            } else if (datasourceSubscription.type === types.widgetType.latest.value) {
-                dataGenFunction = generateLatest;
-                frequency = 1000;
+                frequency = Math.min(datasourceSubscription.subscriptionTimewindow.aggregation.interval, 5000);
             }
         }
     }
@@ -193,14 +189,7 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic
     function syncListener(listener) {
         var key;
         var dataKey;
-        if (datasourceType === types.datasourceType.function) {
-            for (key in dataKeys) {
-                dataKey = dataKeys[key];
-                listener.dataUpdated(datasourceData[key],
-                    listener.datasourceIndex,
-                    dataKey.index);
-            }
-        } else if (datasourceType === types.datasourceType.device) {
+        if (datasourceType === types.datasourceType.device || datasourceSubscription.type === types.widgetType.timeseries.value) {
             for (key in dataKeys) {
                 var dataKeysList = dataKeys[key];
                 for (var i = 0; i < dataKeysList.length; i++) {
@@ -211,6 +200,13 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic
                         dataKey.index);
                 }
             }
+        } else {
+            for (key in dataKeys) {
+                dataKey = dataKeys[key];
+                listener.dataUpdated(datasourceData[key],
+                    listener.datasourceIndex,
+                    dataKey.index);
+            }
         }
     }
 
@@ -218,7 +214,10 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic
         if (history && !hasListeners()) {
             return;
         }
-        //$log.debug("started!");
+        var subsTw = datasourceSubscription.subscriptionTimewindow;
+        var tsKeyNames = [];
+        var dataKey;
+
         if (datasourceType === types.datasourceType.device) {
 
             //send subscribe command
@@ -228,12 +227,13 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic
 
             for (var key in dataKeys) {
                 var dataKeysList = dataKeys[key];
-                var dataKey = dataKeysList[0];
+                dataKey = dataKeysList[0];
                 if (dataKey.type === types.dataKeyType.timeseries) {
                     if (tsKeys.length > 0) {
                         tsKeys += ',';
                     }
                     tsKeys += dataKey.name;
+                    tsKeyNames.push(dataKey.name);
                 } else if (dataKey.type === types.dataKeyType.attribute) {
                     if (attrKeys.length > 0) {
                         attrKeys += ',';
@@ -252,10 +252,10 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic
                     var historyCommand = {
                         deviceId: datasourceSubscription.deviceId,
                         keys: tsKeys,
-                        startTs: datasourceSubscription.subscriptionTimewindow.fixedWindow.startTimeMs,
-                        endTs: datasourceSubscription.subscriptionTimewindow.fixedWindow.endTimeMs,
-                        limit: datasourceSubscription.subscriptionTimewindow.aggregation.limit,
-                        agg: datasourceSubscription.subscriptionTimewindow.aggregation.type
+                        startTs: subsTw.fixedWindow.startTimeMs,
+                        endTs: subsTw.fixedWindow.endTimeMs,
+                        limit: subsTw.aggregation.limit,
+                        agg: subsTw.aggregation.type
                     };
 
                     subscriber = {
@@ -287,16 +287,20 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic
                     };
 
                     if (datasourceSubscription.type === types.widgetType.timeseries.value) {
-                        subscriptionCommand.timeWindow = datasourceSubscription.subscriptionTimewindow.realtimeWindowMs;
-                        subscriptionCommand.limit = datasourceSubscription.subscriptionTimewindow.aggregation.limit;
-                        subscriptionCommand.agg = datasourceSubscription.subscriptionTimewindow.aggregation.type;
-                        var dataAggregator = new DataAggregator(
+                        subscriptionCommand.startTs = subsTw.startTs;
+                        subscriptionCommand.timeWindow = subsTw.aggregation.timeWindow;
+                        subscriptionCommand.limit = subsTw.aggregation.limit;
+                        subscriptionCommand.agg = subsTw.aggregation.type;
+                        dataAggregator = new DataAggregator(
                             function(data, startTs, endTs) {
                                 onData(data, types.dataKeyType.timeseries, startTs, endTs);
                             },
-                            subscriptionCommand.limit,
-                            subscriptionCommand.agg,
-                            subscriptionCommand.timeWindow,
+                            tsKeyNames,
+                            subsTw.startTs,
+                            subsTw.aggregation.limit,
+                            subsTw.aggregation.type,
+                            subsTw.aggregation.timeWindow,
+                            subsTw.aggregation.interval,
                             types,
                             $timeout,
                             $filter
@@ -308,9 +312,6 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic
                             dataAggregator.reset();
                             onReconnected();
                         }
-                        subscriber.onDestroy = function() {
-                            dataAggregator.destroy();
-                        }
                     } else {
                         subscriber.onReconnected = function() {
                             onReconnected();
@@ -353,7 +354,30 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic
 
             }
 
-        } else if (dataGenFunction) {
+        } else if (datasourceType === types.datasourceType.function) {
+            if (datasourceSubscription.type === types.widgetType.timeseries.value) {
+                for (key in dataKeys) {
+                    var dataKeyList = dataKeys[key];
+                    for (var index = 0; index < dataKeyList.length; index++) {
+                        dataKey = dataKeyList[index];
+                        tsKeyNames.push(dataKey.name+'_'+dataKey.index);
+                    }
+                }
+                dataAggregator = new DataAggregator(
+                    function (data, startTs, endTs) {
+                        onData(data, types.dataKeyType.function, startTs, endTs);
+                    },
+                    tsKeyNames,
+                    subsTw.startTs,
+                    subsTw.aggregation.limit,
+                    subsTw.aggregation.type,
+                    subsTw.aggregation.timeWindow,
+                    subsTw.aggregation.interval,
+                    types,
+                    $timeout,
+                    $filter
+                );
+            }
             if (history) {
                 onTick();
             } else {
@@ -377,30 +401,17 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic
             }
             subscribers = {};
         }
-    }
-
-    function boundToInterval(data, timewindowMs) {
-        if (data.length > 1) {
-            var start = data[0][0];
-            var end = data[data.length - 1][0];
-            var i = 0;
-            var currentInterval = end - start;
-            while (currentInterval > timewindowMs && i < data.length - 2) {
-                i++;
-                start = data[i][0];
-                currentInterval = end - start;
-            }
-            if (i > 1) {
-                data.splice(0, i - 1);
-            }
+        if (dataAggregator) {
+            dataAggregator.destroy();
+            dataAggregator = null;
         }
-        return data;
     }
 
-    function generateSeries(dataKey, startTime, endTime) {
+    function generateSeries(dataKey, index, startTime, endTime) {
         var data = [];
         var prevSeries;
-        var datasourceKeyData = datasourceData[dataKey.key].data;
+        var datasourceDataKey = dataKey.key + '_' + index;
+        var datasourceKeyData = datasourceData[datasourceDataKey].data;
         if (datasourceKeyData.length > 0) {
             prevSeries = datasourceKeyData[datasourceKeyData.length - 1];
         } else {
@@ -417,18 +428,7 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic
         if (data.length > 0) {
             dataKey.lastUpdateTime = data[data.length - 1][0];
         }
-        if (realtime) {
-            datasourceData[dataKey.key].data = boundToInterval(datasourceKeyData.concat(data),
-                datasourceSubscription.subscriptionTimewindow.realtimeWindowMs);
-        } else {
-            datasourceData[dataKey.key].data = data;
-        }
-        for (var i in listeners) {
-            var listener = listeners[i];
-            listener.dataUpdated(datasourceData[dataKey.key],
-                listener.datasourceIndex,
-                dataKey.index);
-        }
+        return data;
     }
 
     function generateLatest(dataKey) {
@@ -458,23 +458,32 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic
         if (datasourceSubscription.type === types.widgetType.timeseries.value) {
             var startTime;
             var endTime;
+            var generatedData = {
+                data: {
+                }
+            };
             for (key in dataKeys) {
-                var dataKey = dataKeys[key];
-                if (!startTime) {
-                    if (realtime) {
-                        endTime = (new Date).getTime();
-                        if (dataKey.lastUpdateTime) {
-                            startTime = dataKey.lastUpdateTime + frequency;
+                var dataKeyList = dataKeys[key];
+                for (var index = 0; index < dataKeyList.length; index ++) {
+                    var dataKey = dataKeyList[index];
+                    if (!startTime) {
+                        if (realtime) {
+                            if (dataKey.lastUpdateTime) {
+                                startTime = dataKey.lastUpdateTime + frequency
+                            } else {
+                                startTime = datasourceSubscription.subscriptionTimewindow.startTs;
+                            }
+                            endTime = startTime + datasourceSubscription.subscriptionTimewindow.realtimeWindowMs;
                         } else {
-                            startTime = endTime - datasourceSubscription.subscriptionTimewindow.realtimeWindowMs;
+                            startTime = datasourceSubscription.subscriptionTimewindow.fixedWindow.startTimeMs;
+                            endTime = datasourceSubscription.subscriptionTimewindow.fixedWindow.endTimeMs;
                         }
-                    } else {
-                        startTime = datasourceSubscription.subscriptionTimewindow.fixedWindow.startTimeMs;
-                        endTime = datasourceSubscription.subscriptionTimewindow.fixedWindow.endTimeMs;
                     }
+                    var data = generateSeries(dataKey, index, startTime, endTime);
+                    generatedData.data[dataKey.name+'_'+dataKey.index] = data;
                 }
-                generateSeries(dataKey, startTime, endTime);
             }
+            dataAggregator.onData(generatedData, true, history);
         } else if (datasourceSubscription.type === types.widgetType.latest.value) {
             for (key in dataKeys) {
                 generateLatest(dataKeys[key]);
@@ -568,8 +577,6 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic
                     }
                     if (data.length > 0 || (startTs && endTs)) {
                         datasourceData[datasourceKey].data = data;
-                        datasourceData[datasourceKey].startTs = startTs;
-                        datasourceData[datasourceKey].endTs = endTs;
                         for (var i2 in listeners) {
                             var listener = listeners[i2];
                             listener.dataUpdated(datasourceData[datasourceKey],
diff --git a/ui/src/app/components/dashboard.directive.js b/ui/src/app/components/dashboard.directive.js
index e67a444..3889339 100644
--- a/ui/src/app/components/dashboard.directive.js
+++ b/ui/src/app/components/dashboard.directive.js
@@ -68,6 +68,7 @@ function Dashboard() {
             prepareDashboardContextMenu: '&?',
             prepareWidgetContextMenu: '&?',
             loadWidgets: '&?',
+            getStDiff: '&?',
             onInit: '&?',
             onInitFailed: '&?',
             dashboardStyle: '=?'
@@ -94,6 +95,8 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, $
 
     vm.gridster = null;
 
+    vm.stDiff = 0;
+
     vm.isMobileDisabled = angular.isDefined(vm.isMobileDisabled) ? vm.isMobileDisabled : false;
 
     vm.dashboardLoading = true;
@@ -302,7 +305,28 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, $
         });
     });
 
-    loadDashboard();
+    loadStDiff();
+
+    function loadStDiff() {
+        if (vm.getStDiff) {
+            var promise = vm.getStDiff();
+            if (promise) {
+                promise.then(function (stDiff) {
+                    vm.stDiff = stDiff;
+                    loadDashboard();
+                }, function () {
+                    vm.stDiff = 0;
+                    loadDashboard();
+                });
+            } else {
+                vm.stDiff = 0;
+                loadDashboard();
+            }
+        } else {
+            vm.stDiff = 0;
+            loadDashboard();
+        }
+    }
 
     function loadDashboard() {
         resetWidgetClick();
diff --git a/ui/src/app/components/dashboard.tpl.html b/ui/src/app/components/dashboard.tpl.html
index 0e367e9..dc46e35 100644
--- a/ui/src/app/components/dashboard.tpl.html
+++ b/ui/src/app/components/dashboard.tpl.html
@@ -93,7 +93,7 @@
 								</div>
 								<div flex layout="column" class="tb-widget-content">
 									<div flex tb-widget
-										 locals="{ visibleRect: vm.visibleRect, widget: widget, deviceAliasList: vm.deviceAliasList, isEdit: vm.isEdit }">
+										 locals="{ visibleRect: vm.visibleRect, widget: widget, deviceAliasList: vm.deviceAliasList, isEdit: vm.isEdit, stDiff: vm.stDiff }">
 									</div>
 								</div>
 							</div>
diff --git a/ui/src/app/components/widget.controller.js b/ui/src/app/components/widget.controller.js
index cdcdbfb..f7e9498 100644
--- a/ui/src/app/components/widget.controller.js
+++ b/ui/src/app/components/widget.controller.js
@@ -20,7 +20,7 @@ import 'javascript-detect-element-resize/detect-element-resize';
 
 /*@ngInject*/
 export default function WidgetController($scope, $timeout, $window, $element, $q, $log, $injector, tbRaf, types, utils,
-                                         datasourceService, deviceService, visibleRect, isEdit, widget, deviceAliasList, widgetType) {
+                                         datasourceService, deviceService, visibleRect, isEdit, stDiff, widget, deviceAliasList, widgetType) {
 
     var vm = this;
 
@@ -46,7 +46,6 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
         realtimeWindowMs: null,
         aggregation: null
     };
-    var dataUpdateTimer = null;
     var dataUpdateCaf = null;
 
     /*
@@ -72,7 +71,9 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
         settings: widget.config.settings,
         datasources: widget.config.datasources,
         data: [],
-        timeWindow: {},
+        timeWindow: {
+            stDiff: stDiff
+        },
         timewindowFunctions: {
             onUpdateTimewindow: onUpdateTimewindow,
             onResetTimewindow: onResetTimewindow
@@ -154,10 +155,11 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
         }
     }
 
-    function updateTimewindow(startTs, endTs) {
+    function updateTimewindow() {
+        widgetContext.timeWindow.interval = subscriptionTimewindow.aggregation.interval || 1000;
         if (subscriptionTimewindow.realtimeWindowMs) {
-            widgetContext.timeWindow.maxTime = endTs || (new Date).getTime();
-            widgetContext.timeWindow.minTime = startTs || (widgetContext.timeWindow.maxTime - subscriptionTimewindow.realtimeWindowMs);
+            widgetContext.timeWindow.maxTime = (new Date).getTime() + widgetContext.timeWindow.stDiff;
+            widgetContext.timeWindow.minTime = widgetContext.timeWindow.maxTime - subscriptionTimewindow.realtimeWindowMs;
         } else if (subscriptionTimewindow.fixedWindow) {
             widgetContext.timeWindow.maxTime = subscriptionTimewindow.fixedWindow.endTimeMs;
             widgetContext.timeWindow.minTime = subscriptionTimewindow.fixedWindow.startTimeMs;
@@ -165,10 +167,6 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
     }
 
     function onDataUpdated() {
-        if (dataUpdateTimer) {
-            $timeout.cancel(dataUpdateTimer);
-            dataUpdateTimer = null;
-        }
         if (widgetContext.inited) {
             if (dataUpdateCaf) {
                 dataUpdateCaf();
@@ -496,7 +494,8 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
                     startTimeMs: startTimeMs,
                     endTimeMs: endTimeMs
                 }
-            }
+            },
+            aggregation: angular.copy(widget.config.timewindow.aggregation)
         };
     }
 
@@ -513,14 +512,10 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
         }
         if (update) {
             if (subscriptionTimewindow.realtimeWindowMs) {
-                updateTimewindow(sourceData.startTs, sourceData.endTs);
+                updateTimewindow();
             }
             widgetContext.data[datasourceIndex + dataKeyIndex].data = sourceData.data;
-            if (widgetContext.data.length > 1 && !dataUpdateTimer) {
-                dataUpdateTimer = $timeout(onDataUpdated, 300, false);
-            } else {
-                onDataUpdated();
-            }
+            onDataUpdated();
         }
     }
 
@@ -552,10 +547,6 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
 
     function unsubscribe() {
         if (widget.type !== types.widgetType.rpc.value) {
-            if (dataUpdateTimer) {
-                $timeout.cancel(dataUpdateTimer);
-                dataUpdateTimer = null;
-            }
             for (var i in datasourceListeners) {
                 var listener = datasourceListeners[i];
                 datasourceService.unsubscribeFromDatasource(listener);
@@ -575,7 +566,7 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
             };
             if (widget.type === types.widgetType.timeseries.value &&
                 angular.isDefined(widget.config.timewindow)) {
-
+                var timeWindow = 0;
                 if (angular.isDefined(widget.config.timewindow.aggregation)) {
                     subscriptionTimewindow.aggregation = {
                         limit: widget.config.timewindow.aggregation.limit || 200,
@@ -585,6 +576,8 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
 
                 if (angular.isDefined(widget.config.timewindow.realtime)) {
                     subscriptionTimewindow.realtimeWindowMs = widget.config.timewindow.realtime.timewindowMs;
+                    subscriptionTimewindow.startTs = (new Date).getTime() + widgetContext.timeWindow.stDiff - subscriptionTimewindow.realtimeWindowMs;
+                    timeWindow = subscriptionTimewindow.realtimeWindowMs;
                 } else if (angular.isDefined(widget.config.timewindow.history)) {
                     if (angular.isDefined(widget.config.timewindow.history.timewindowMs)) {
                         var currentTime = (new Date).getTime();
@@ -592,14 +585,31 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
                             startTimeMs: currentTime - widget.config.timewindow.history.timewindowMs,
                             endTimeMs: currentTime
                         }
+                        timeWindow = widget.config.timewindow.history.timewindowMs;
                     } else {
                         subscriptionTimewindow.fixedWindow = {
                             startTimeMs: widget.config.timewindow.history.fixedTimewindow.startTimeMs,
                             endTimeMs: widget.config.timewindow.history.fixedTimewindow.endTimeMs
                         }
+                        timeWindow = subscriptionTimewindow.fixedWindow.endTimeMs - subscriptionTimewindow.fixedWindow.startTimeMs;
                     }
+                    subscriptionTimewindow.startTs = subscriptionTimewindow.fixedWindow.startTimeMs;
+                }
+                var aggregation = subscriptionTimewindow.aggregation;
+                var noAggregation = aggregation.type === types.aggregation.none.value;
+                var interval = Math.floor(timeWindow / aggregation.limit);
+                if (!noAggregation) {
+                    aggregation.interval = Math.max(interval, 1000);
+                    aggregation.limit = Math.ceil(interval/aggregation.interval * aggregation.limit);
+                    aggregation.timeWindow = aggregation.interval * aggregation.limit;
+                } else {
+                    aggregation.timeWindow = interval * aggregation.limit;
+                    aggregation.interval = 1000;
                 }
                 updateTimewindow();
+                if (subscriptionTimewindow.fixedWindow) {
+                    onDataUpdated();
+                }
             }
             for (var i in widget.config.datasources) {
                 var datasource = widget.config.datasources[i];
diff --git a/ui/src/app/dashboard/dashboard.controller.js b/ui/src/app/dashboard/dashboard.controller.js
index c557ed3..c2a9cf3 100644
--- a/ui/src/app/dashboard/dashboard.controller.js
+++ b/ui/src/app/dashboard/dashboard.controller.js
@@ -61,6 +61,7 @@ export default function DashboardController(types, widgetService, userService,
     vm.isTenantAdmin = isTenantAdmin;
     vm.isSystemAdmin = isSystemAdmin;
     vm.loadDashboard = loadDashboard;
+    vm.getServerTimeDiff = getServerTimeDiff;
     vm.noData = noData;
     vm.onAddWidgetClosed = onAddWidgetClosed;
     vm.onEditWidgetClosed = onEditWidgetClosed;
@@ -94,10 +95,9 @@ export default function DashboardController(types, widgetService, userService,
             widgetService.getBundleWidgetTypes(bundleAlias, isSystem).then(
                 function (widgetTypes) {
 
-                    widgetTypes = $filter('orderBy')(widgetTypes, ['-name']);
+                    widgetTypes = $filter('orderBy')(widgetTypes, ['-createdTime']);
 
                     var top = 0;
-                    var sizeY = 0;
 
                     if (widgetTypes.length > 0) {
                         loadNext(0);
@@ -135,7 +135,7 @@ export default function DashboardController(types, widgetService, userService,
                         } else if (widgetTypeInfo.type === types.widgetType.static.value) {
                             vm.staticWidgetTypes.push(widget);
                         }
-                        top += sizeY;
+                        top += widget.sizeY;
                         loadNextOrComplete(i);
 
                     }
@@ -144,6 +144,10 @@ export default function DashboardController(types, widgetService, userService,
         }
     }
 
+    function getServerTimeDiff() {
+        return dashboardService.getServerTimeDiff();
+    }
+
     function loadDashboard() {
 
         var deferred = $q.defer();
diff --git a/ui/src/app/dashboard/dashboard.tpl.html b/ui/src/app/dashboard/dashboard.tpl.html
index 130a37b..298d365 100644
--- a/ui/src/app/dashboard/dashboard.tpl.html
+++ b/ui/src/app/dashboard/dashboard.tpl.html
@@ -91,6 +91,7 @@
                 prepare-widget-context-menu="vm.prepareWidgetContextMenu(widget)"
                 on-remove-widget="vm.removeWidget(event, widget)"
                 load-widgets="vm.loadDashboard()"
+                get-st-diff="vm.getServerTimeDiff()"
                 on-init="vm.dashboardInited(dashboard)"
                 on-init-failed="vm.dashboardInitFailed(e)">
         </tb-dashboard>
diff --git a/ui/src/app/device/attribute/attribute-table.directive.js b/ui/src/app/device/attribute/attribute-table.directive.js
index d167928..ba3e466 100644
--- a/ui/src/app/device/attribute/attribute-table.directive.js
+++ b/ui/src/app/device/attribute/attribute-table.directive.js
@@ -29,7 +29,7 @@ import EditAttributeValueController from './edit-attribute-value.controller';
 
 /*@ngInject*/
 export default function AttributeTableDirective($compile, $templateCache, $rootScope, $q, $mdEditDialog, $mdDialog,
-                                                $document, $translate, utils, types, deviceService, widgetService) {
+                                                $document, $translate, utils, types, dashboardService, deviceService, widgetService) {
 
     var linker = function (scope, element, attrs) {
 
@@ -357,6 +357,10 @@ export default function AttributeTableDirective($compile, $templateCache, $rootS
             scope.getDeviceAttributes(true);
         }
 
+        scope.getServerTimeDiff = function() {
+            return dashboardService.getServerTimeDiff();
+        }
+
         scope.addWidgetToDashboard = function($event) {
             if (scope.mode === 'widget' && scope.widgetsListCache.length > 0) {
                 var widget = scope.widgetsListCache[scope.widgetsCarousel.index][0];
diff --git a/ui/src/app/device/attribute/attribute-table.tpl.html b/ui/src/app/device/attribute/attribute-table.tpl.html
index 2f88a27..e2efe36 100644
--- a/ui/src/app/device/attribute/attribute-table.tpl.html
+++ b/ui/src/app/device/attribute/attribute-table.tpl.html
@@ -158,8 +158,9 @@
                 <tb-dashboard
                         device-alias-list="deviceAliases"
                         widgets="widgets"
+                        get-st-diff="getServerTimeDiff()"
                         columns="20"
-                        is-edit="true"
+                        is-edit="false"
                         is-mobile-disabled="true"
                         is-edit-action-enabled="false"
                         is-remove-action-enabled="false">
diff --git a/ui/src/app/widget/lib/flot-widget.js b/ui/src/app/widget/lib/flot-widget.js
index 72401f9..c35b363 100644
--- a/ui/src/app/widget/lib/flot-widget.js
+++ b/ui/src/app/widget/lib/flot-widget.js
@@ -22,6 +22,8 @@ import 'flot/src/jquery.flot';
 import 'flot/src/plugins/jquery.flot.time';
 import 'flot/src/plugins/jquery.flot.selection';
 import 'flot/src/plugins/jquery.flot.pie';
+import 'flot/src/plugins/jquery.flot.crosshair';
+import 'flot/src/plugins/jquery.flot.stack';
 
 /* eslint-disable angular/angularelement */
 export default class TbFlot {
@@ -38,8 +40,8 @@ export default class TbFlot {
             var keySettings = series.dataKey.settings;
 
             series.lines = {
-                fill: keySettings.fillLines || false,
-                show: keySettings.showLines || true
+                fill: keySettings.fillLines === true,
+                show: this.chartType === 'line' ? keySettings.showLines !== false : keySettings.showLines === true
             };
 
             series.points = {
@@ -58,36 +60,34 @@ export default class TbFlot {
             series.highlightColor = lineColor.toRgbString();
 
         }
-
-        var tbFlot = this;
-
         ctx.tooltip = $('#flot-series-tooltip');
         if (ctx.tooltip.length === 0) {
             ctx.tooltip = $("<div id=flot-series-tooltip' class='flot-mouse-value'></div>");
             ctx.tooltip.css({
                 fontSize: "12px",
                 fontFamily: "Roboto",
-                lineHeight: "24px",
+                fontWeight: "300",
+                lineHeight: "18px",
                 opacity: "1",
                 backgroundColor: "rgba(0,0,0,0.7)",
-                color: "#fff",
+                color: "#D9DADB",
                 position: "absolute",
                 display: "none",
                 zIndex: "100",
-                padding: "2px 8px",
+                padding: "4px 10px",
                 borderRadius: "4px"
             }).appendTo("body");
         }
 
-        ctx.tooltipFormatter = function(item) {
-            var label = item.series.label;
-            var color = item.series.color;
-            var content = '';
-            if (tbFlot.chartType === 'line') {
-                var timestamp = parseInt(item.datapoint[0]);
-                var date = moment(timestamp).format('YYYY-MM-DD HH:mm:ss');
-                content += '<b>' + date + '</b></br>';
-            }
+        var tbFlot = this;
+
+        function seriesInfoDiv(label, color, value, units, trackDecimals, active, percent) {
+            var divElement = $('<div></div>');
+            divElement.css({
+                display: "flex",
+                alignItems: "center",
+                justifyContent: "center"
+            });
             var lineSpan = $('<span></span>');
             lineSpan.css({
                 backgroundColor: color,
@@ -97,27 +97,76 @@ export default class TbFlot {
                 verticalAlign: "middle",
                 marginRight: "5px"
             });
-            content += lineSpan.prop('outerHTML');
-
+            divElement.append(lineSpan);
             var labelSpan = $('<span>' + label + ':</span>');
             labelSpan.css({
                 marginRight: "10px"
             });
-            content += labelSpan.prop('outerHTML');
-            var value = tbFlot.chartType === 'line' ? item.datapoint[1] : item.datapoint[1][0][1];
-            content += ' <b>' + value.toFixed(ctx.trackDecimals);
-            if (settings.units) {
-                content += ' ' + settings.units;
+            if (active) {
+                labelSpan.css({
+                    color: "#FFF",
+                    fontWeight: "700"
+                });
             }
-            if (tbFlot.chartType === 'pie') {
-                content += ' (' + Math.round(item.series.percent) + ' %)';
+            divElement.append(labelSpan);
+            var valueContent = value.toFixed(trackDecimals);
+            if (units) {
+                valueContent += ' ' + units;
             }
-            content += '</b>';
-            return content;
-        };
+            if (angular.isNumber(percent)) {
+                valueContent += ' (' + Math.round(percent) + ' %)';
+            }
+            var valueSpan =  $('<span>' + valueContent + '</span>');
+            valueSpan.css({
+                marginLeft: "auto",
+                fontWeight: "700"
+            });
+            if (active) {
+                valueSpan.css({
+                    color: "#FFF"
+                });
+            }
+            divElement.append(valueSpan);
+
+            return divElement;
+        }
+
+        if (this.chartType === 'pie') {
+            ctx.tooltipFormatter = function(item) {
+                var divElement = seriesInfoDiv(item.series.label, item.series.color,
+                    item.datapoint[1][0][1], tbFlot.ctx.settings.units, tbFlot.ctx.trackDecimals, true, item.series.percent);
+                return divElement.prop('outerHTML');
+            };
+        } else {
+            ctx.tooltipFormatter = function(hoverInfo, seriesIndex) {
+                var content = '';
+                var timestamp = parseInt(hoverInfo.time);
+                var date = moment(timestamp).format('YYYY-MM-DD HH:mm:ss');
+                var dateDiv = $('<div>' + date + '</div>');
+                dateDiv.css({
+                    display: "flex",
+                    alignItems: "center",
+                    justifyContent: "center",
+                    padding: "4px",
+                    fontWeight: "700"
+                });
+                content += dateDiv.prop('outerHTML');
+                for (var i in hoverInfo.seriesHover) {
+                    var seriesHoverInfo = hoverInfo.seriesHover[i];
+                    if (tbFlot.ctx.tooltipIndividual && seriesHoverInfo.index !== seriesIndex) {
+                        continue;
+                    }
+                    var divElement = seriesInfoDiv(seriesHoverInfo.label, seriesHoverInfo.color,
+                        seriesHoverInfo.value, tbFlot.ctx.settings.units, tbFlot.ctx.trackDecimals, seriesHoverInfo.index === seriesIndex);
+                    content += divElement.prop('outerHTML');
+                }
+                return content;
+            };
+        }
 
         var settings = ctx.settings;
         ctx.trackDecimals = angular.isDefined(settings.decimals) ? settings.decimals : 1;
+        ctx.tooltipIndividual = this.chartType === 'pie' || (angular.isDefined(settings.tooltipIndividual) ? settings.tooltipIndividual : false);
 
         var font = {
             color: settings.fontColor || "#545454",
@@ -134,7 +183,7 @@ export default class TbFlot {
             grid: {
                 hoverable: true,
                 mouseActiveRadius: 10,
-                autoHighlight: true
+                autoHighlight: ctx.tooltipIndividual === true
             },
             selection : { mode : ctx.isMobile ? null : 'x' },
             legend : {
@@ -155,7 +204,7 @@ export default class TbFlot {
                 settings.legend.backgroundOpacity : 0.85;
         }
 
-        if (this.chartType === 'line') {
+        if (this.chartType === 'line' || this.chartType === 'bar') {
             options.xaxis = {
                 mode: 'time',
                 timezone: 'browser',
@@ -208,6 +257,28 @@ export default class TbFlot {
                 }
             }
 
+            options.crosshair = {
+                mode: 'x'
+            }
+
+            options.series = {
+                stack: settings.stack === true
+            }
+
+            if (this.chartType === 'bar') {
+                options.series.lines = {
+                        show: false,
+                        fill: false,
+                        steps: false
+                }
+                options.series.bars ={
+                        show: true,
+                        barWidth: ctx.timeWindow.interval * 0.6,
+                        lineWidth: 0,
+                        fill: 0.9
+                }
+            }
+
             options.xaxis.min = ctx.timeWindow.minTime;
             options.xaxis.max = ctx.timeWindow.maxTime;
         } else if (this.chartType === 'pie') {
@@ -271,11 +342,12 @@ export default class TbFlot {
 
     update() {
         if (!this.isMouseInteraction) {
-            if (this.chartType === 'line') {
+            if (this.chartType === 'line' || this.chartType === 'bar') {
                 this.ctx.plot.getOptions().xaxes[0].min = this.ctx.timeWindow.minTime;
                 this.ctx.plot.getOptions().xaxes[0].max = this.ctx.timeWindow.maxTime;
-            }
-            if (this.chartType === 'line') {
+                if (this.chartType === 'bar') {
+                    this.ctx.plot.getOptions().series.bars.barWidth = this.ctx.timeWindow.interval * 0.6;
+                }
                 this.ctx.plot.setData(this.ctx.data);
                 this.ctx.plot.setupGrid();
                 this.ctx.plot.draw();
@@ -290,75 +362,475 @@ export default class TbFlot {
         }
     }
 
-    pieDataRendered() {
-        for (var i in this.ctx.pieTargetData) {
-            var value = this.ctx.pieTargetData[i] ? this.ctx.pieTargetData[i] : 0;
-            this.ctx.pieRenderedData[i] = value;
-            if (!this.ctx.pieData[i].data[0]) {
-                this.ctx.pieData[i].data[0] = [0,0];
-            }
-            this.ctx.pieData[i].data[0][1] = value;
+    resize() {
+        this.ctx.plot.resize();
+        if (this.chartType !== 'pie') {
+            this.ctx.plot.setupGrid();
         }
+        this.ctx.plot.draw();
     }
 
-    nextPieDataAnimation(start) {
-        if (start) {
-            this.finishPieDataAnimation();
-            this.ctx.pieAnimationStartTime = this.ctx.pieAnimationLastTime = Date.now();
-            for (var i in this.ctx.data) {
-                this.ctx.pieTargetData[i] = (this.ctx.data[i].data && this.ctx.data[i].data[0])
-                    ? this.ctx.data[i].data[0][1] : 0;
-            }
-        }
-        if (this.ctx.pieAnimationCaf) {
-            this.ctx.pieAnimationCaf();
-            this.ctx.pieAnimationCaf = null;
+    static get pieSettingsSchema() {
+        return {
+            "schema": {
+                "type": "object",
+                "title": "Settings",
+                "properties": {
+                    "radius": {
+                        "title": "Radius",
+                        "type": "number",
+                        "default": 1
+                    },
+                    "innerRadius": {
+                        "title": "Inner radius",
+                        "type": "number",
+                        "default": 0
+                    },
+                    "tilt": {
+                        "title": "Tilt",
+                        "type": "number",
+                        "default": 1
+                    },
+                    "animatedPie": {
+                        "title": "Enable pie animation (experimental)",
+                        "type": "boolean",
+                        "default": false
+                    },
+                    "stroke": {
+                        "title": "Stroke",
+                        "type": "object",
+                        "properties": {
+                            "color": {
+                                "title": "Color",
+                                "type": "string",
+                                "default": ""
+                            },
+                            "width": {
+                                "title": "Width (pixels)",
+                                "type": "number",
+                                "default": 0
+                            }
+                        }
+                    },
+                    "showLabels": {
+                        "title": "Show labels",
+                        "type": "boolean",
+                        "default": false
+                    },
+                    "fontColor": {
+                        "title": "Font color",
+                        "type": "string",
+                        "default": "#545454"
+                    },
+                    "fontSize": {
+                        "title": "Font size",
+                        "type": "number",
+                        "default": 10
+                    },
+                    "decimals": {
+                        "title": "Number of digits after floating point",
+                        "type": "number",
+                        "default": 1
+                    },
+                    "units": {
+                        "title": "Special symbol to show next to value",
+                        "type": "string",
+                        "default": ""
+                    },
+                    "legend": {
+                        "title": "Legend settings",
+                        "type": "object",
+                        "properties": {
+                            "show": {
+                                "title": "Show legend",
+                                "type": "boolean",
+                                "default": true
+                            },
+                            "position": {
+                                "title": "Position",
+                                "type": "string",
+                                "default": "nw"
+                            },
+                            "labelBoxBorderColor": {
+                                "title": "Label box border color",
+                                "type": "string",
+                                "default": "#CCCCCC"
+                            },
+                            "backgroundColor": {
+                                "title": "Background color",
+                                "type": "string",
+                                "default": "#F0F0F0"
+                            },
+                            "backgroundOpacity": {
+                                "title": "Background opacity",
+                                "type": "number",
+                                "default": 0.85
+                            }
+                        }
+                    }
+                },
+                "required": []
+            },
+            "form": [
+                "radius",
+                "innerRadius",
+                "animatedPie",
+                "tilt",
+                {
+                    "key": "stroke",
+                    "items": [
+                        {
+                            "key": "stroke.color",
+                            "type": "color"
+                        },
+                        "stroke.width"
+                    ]
+                },
+                "showLabels",
+                {
+                    "key": "fontColor",
+                    "type": "color"
+                },
+                "fontSize",
+                "decimals",
+                "units",
+                {
+                    "key": "legend",
+                    "items": [
+                        "legend.show",
+                        {
+                            "key": "legend.position",
+                            "type": "rc-select",
+                            "multiple": false,
+                            "items": [
+                                {
+                                    "value": "nw",
+                                    "label": "North-west"
+                                },
+                                {
+                                    "value": "ne",
+                                    "label": "North-east"
+                                },
+                                {
+                                    "value": "sw",
+                                    "label": "South-west"
+                                },
+                                {
+                                    "value": "se",
+                                    "label": "Soth-east"
+                                }
+                            ]
+                        },
+                        {
+                            "key": "legend.labelBoxBorderColor",
+                            "type": "color"
+                        },
+                        {
+                            "key": "legend.backgroundColor",
+                            "type": "color"
+                        },
+                        "legend.backgroundOpacity"
+                    ]
+                }
+            ]
         }
-        var self = this;
-        this.ctx.pieAnimationCaf = this.ctx.$scope.tbRaf(
-            function () {
-                self.onPieDataAnimation();
-            }
-        );
     }
 
-    onPieDataAnimation() {
-        var time = Date.now();
-        var elapsed = time - this.ctx.pieAnimationLastTime;//this.ctx.pieAnimationStartTime;
-        var progress = (time - this.ctx.pieAnimationStartTime) / this.ctx.pieDataAnimationDuration;
-        if (progress >= 1) {
-            this.finishPieDataAnimation();
-        } else {
-            if (elapsed >= 40) {
-                for (var i in this.ctx.pieTargetData) {
-                    var prevValue = this.ctx.pieRenderedData[i];
-                    var targetValue = this.ctx.pieTargetData[i];
-                    var value = prevValue + (targetValue - prevValue) * progress;
-                    if (!this.ctx.pieData[i].data[0]) {
-                        this.ctx.pieData[i].data[0] = [0,0];
+    static get settingsSchema() {
+        return {
+            "schema": {
+                "type": "object",
+                "title": "Settings",
+                "properties": {
+                    "stack": {
+                        "title": "Stacking",
+                        "type": "boolean",
+                        "default": false
+                    },
+                    "shadowSize": {
+                        "title": "Shadow size",
+                        "type": "number",
+                        "default": 4
+                    },
+                    "fontColor": {
+                        "title": "Font color",
+                        "type": "string",
+                        "default": "#545454"
+                    },
+                    "fontSize": {
+                        "title": "Font size",
+                        "type": "number",
+                        "default": 10
+                    },
+                    "decimals": {
+                        "title": "Number of digits after floating point",
+                        "type": "number",
+                        "default": 1
+                    },
+                    "units": {
+                        "title": "Special symbol to show next to value",
+                        "type": "string",
+                        "default": ""
+                    },
+                    "tooltipIndividual": {
+                        "title": "Hover individual points",
+                        "type": "boolean",
+                        "default": false
+                    },
+                    "grid": {
+                        "title": "Grid settings",
+                        "type": "object",
+                        "properties": {
+                            "color": {
+                                "title": "Primary color",
+                                "type": "string",
+                                "default": "#545454"
+                            },
+                            "backgroundColor": {
+                                "title": "Background color",
+                                "type": "string",
+                                "default": null
+                            },
+                            "tickColor": {
+                                "title": "Ticks color",
+                                "type": "string",
+                                "default": "#DDDDDD"
+                            },
+                            "outlineWidth": {
+                                "title": "Grid outline/border width (px)",
+                                "type": "number",
+                                "default": 1
+                            },
+                            "verticalLines": {
+                                "title": "Show vertical lines",
+                                "type": "boolean",
+                                "default": true
+                            },
+                            "horizontalLines": {
+                                "title": "Show horizontal lines",
+                                "type": "boolean",
+                                "default": true
+                            }
+                        }
+                    },
+                    "legend": {
+                        "title": "Legend settings",
+                        "type": "object",
+                        "properties": {
+                            "show": {
+                                "title": "Show legend",
+                                "type": "boolean",
+                                "default": true
+                            },
+                            "position": {
+                                "title": "Position",
+                                "type": "string",
+                                "default": "nw"
+                            },
+                            "labelBoxBorderColor": {
+                                "title": "Label box border color",
+                                "type": "string",
+                                "default": "#CCCCCC"
+                            },
+                            "backgroundColor": {
+                                "title": "Background color",
+                                "type": "string",
+                                "default": "#F0F0F0"
+                            },
+                            "backgroundOpacity": {
+                                "title": "Background opacity",
+                                "type": "number",
+                                "default": 0.85
+                            }
+                        }
+                    },
+                    "xaxis": {
+                        "title": "X axis settings",
+                        "type": "object",
+                        "properties": {
+                            "showLabels": {
+                                "title": "Show labels",
+                                "type": "boolean",
+                                "default": true
+                            },
+                            "title": {
+                                "title": "Axis title",
+                                "type": "string",
+                                "default": null
+                            },
+                            "titleAngle": {
+                                "title": "Axis title's angle in degrees",
+                                "type": "number",
+                                "default": 0
+                            },
+                            "color": {
+                                "title": "Ticks color",
+                                "type": "string",
+                                "default": null
+                            }
+                        }
+                    },
+                    "yaxis": {
+                        "title": "Y axis settings",
+                        "type": "object",
+                        "properties": {
+                            "showLabels": {
+                                "title": "Show labels",
+                                "type": "boolean",
+                                "default": true
+                            },
+                            "title": {
+                                "title": "Axis title",
+                                "type": "string",
+                                "default": null
+                            },
+                            "titleAngle": {
+                                "title": "Axis title's angle in degrees",
+                                "type": "number",
+                                "default": 0
+                            },
+                            "color": {
+                                "title": "Ticks color",
+                                "type": "string",
+                                "default": null
+                            }
+                        }
                     }
-                    this.ctx.pieData[i].data[0][1] = value;
+                },
+                "required": []
+            },
+            "form": [
+                "stack",
+                "shadowSize",
+                {
+                    "key": "fontColor",
+                    "type": "color"
+                },
+                "fontSize",
+                "decimals",
+                "units",
+                "tooltipIndividual",
+                {
+                    "key": "grid",
+                    "items": [
+                        {
+                            "key": "grid.color",
+                            "type": "color"
+                        },
+                        {
+                            "key": "grid.backgroundColor",
+                            "type": "color"
+                        },
+                        {
+                            "key": "grid.tickColor",
+                            "type": "color"
+                        },
+                        "grid.outlineWidth",
+                        "grid.verticalLines",
+                        "grid.horizontalLines"
+                    ]
+                },
+                {
+                    "key": "legend",
+                    "items": [
+                        "legend.show",
+                        {
+                            "key": "legend.position",
+                            "type": "rc-select",
+                            "multiple": false,
+                            "items": [
+                                {
+                                    "value": "nw",
+                                    "label": "North-west"
+                                },
+                                {
+                                    "value": "ne",
+                                    "label": "North-east"
+                                },
+                                {
+                                    "value": "sw",
+                                    "label": "South-west"
+                                },
+                                {
+                                    "value": "se",
+                                    "label": "Soth-east"
+                                }
+                            ]
+                        },
+                        {
+                            "key": "legend.labelBoxBorderColor",
+                            "type": "color"
+                        },
+                        {
+                            "key": "legend.backgroundColor",
+                            "type": "color"
+                        },
+                        "legend.backgroundOpacity"
+                    ]
+                },
+                {
+                    "key": "xaxis",
+                    "items": [
+                        "xaxis.showLabels",
+                        "xaxis.title",
+                        "xaxis.titleAngle",
+                        {
+                            "key": "xaxis.color",
+                            "type": "color"
+                        }
+                    ]
+                },
+                {
+                    "key": "yaxis",
+                    "items": [
+                        "yaxis.showLabels",
+                        "yaxis.title",
+                        "yaxis.titleAngle",
+                        {
+                            "key": "yaxis.color",
+                            "type": "color"
+                        }
+                    ]
                 }
-                this.ctx.plot.setData(this.ctx.pieData);
-                this.ctx.plot.draw();
-                this.ctx.pieAnimationLastTime = time;
-            }
-            this.nextPieDataAnimation(false);
+
+            ]
         }
     }
 
-    finishPieDataAnimation() {
-        this.pieDataRendered();
-        this.ctx.plot.setData(this.ctx.pieData);
-        this.ctx.plot.draw();
+    static get pieDatakeySettingsSchema() {
+        return {}
     }
 
-    resize() {
-        this.ctx.plot.resize();
-        if (this.chartType === 'line') {
-            this.ctx.plot.setupGrid();
+    static datakeySettingsSchema(defaultShowLines) {
+        return {
+                "schema": {
+                "type": "object",
+                    "title": "DataKeySettings",
+                    "properties": {
+                    "showLines": {
+                        "title": "Show lines",
+                            "type": "boolean",
+                            "default": defaultShowLines
+                    },
+                    "fillLines": {
+                        "title": "Fill lines",
+                            "type": "boolean",
+                            "default": false
+                    },
+                    "showPoints": {
+                        "title": "Show points",
+                            "type": "boolean",
+                            "default": false
+                    }
+                },
+                "required": ["showLines", "fillLines", "showPoints"]
+            },
+                "form": [
+                "showLines",
+                "fillLines",
+                "showPoints"
+            ]
         }
-        this.ctx.plot.draw();
     }
 
     checkMouseEvents() {
@@ -378,24 +850,58 @@ export default class TbFlot {
 
         if (!this.flotHoverHandler) {
             this.flotHoverHandler =  function (event, pos, item) {
-                if (item) {
-                    var pageX = item.pageX || pos.pageX;
-                    var pageY = item.pageY || pos.pageY;
-                    tbFlot.ctx.tooltip.html(tbFlot.ctx.tooltipFormatter(item))
-                        .css({top: pageY+5, left: 0})
-                        .fadeIn(200);
-                    var windowWidth = $( window ).width();  //eslint-disable-line
-                    var tooltipWidth = tbFlot.ctx.tooltip.width();
-                    var left = pageX+5;
-                    if (windowWidth - pageX < tooltipWidth + 50) {
-                        left = pageX - tooltipWidth - 10;
+                if (!tbFlot.ctx.tooltipIndividual || item) {
+
+                    var multipleModeTooltip = !tbFlot.ctx.tooltipIndividual;
+
+                    if (multipleModeTooltip) {
+                        tbFlot.ctx.plot.unhighlight();
                     }
-                    tbFlot.ctx.tooltip.css({
-                        left: left
-                    });
+
+                    var pageX = pos.pageX;
+                    var pageY = pos.pageY;
+
+                    var tooltipHtml;
+
+                    if (tbFlot.chartType === 'pie') {
+                        tooltipHtml = tbFlot.ctx.tooltipFormatter(item);
+                    } else {
+                        var hoverInfo = tbFlot.getHoverInfo(tbFlot.ctx.plot.getData(), pos);
+                        if (angular.isNumber(hoverInfo.time)) {
+                            hoverInfo.seriesHover.sort(function (a, b) {
+                                return b.value - a.value;
+                            });
+                            tooltipHtml = tbFlot.ctx.tooltipFormatter(hoverInfo, item ? item.seriesIndex : -1);
+                        }
+                    }
+
+                    if (tooltipHtml) {
+                        tbFlot.ctx.tooltip.html(tooltipHtml)
+                            .css({top: pageY+5, left: 0})
+                            .fadeIn(200);
+
+                        var windowWidth = $( window ).width();  //eslint-disable-line
+                        var tooltipWidth = tbFlot.ctx.tooltip.width();
+                        var left = pageX+5;
+                        if (windowWidth - pageX < tooltipWidth + 50) {
+                            left = pageX - tooltipWidth - 10;
+                        }
+                        tbFlot.ctx.tooltip.css({
+                            left: left
+                        });
+
+                        if (multipleModeTooltip) {
+                            for (var i = 0; i < hoverInfo.seriesHover.length; i++) {
+                                var seriesHoverInfo = hoverInfo.seriesHover[i];
+                                tbFlot.ctx.plot.highlight(seriesHoverInfo.index, seriesHoverInfo.hoverIndex);
+                            }
+                        }
+                    }
+
                 } else {
                     tbFlot.ctx.tooltip.stop(true);
                     tbFlot.ctx.tooltip.hide();
+                    tbFlot.ctx.plot.unhighlight();
                 }
             };
             this.ctx.$container.bind('plothover', this.flotHoverHandler);
@@ -430,6 +936,7 @@ export default class TbFlot {
             this.mouseleaveHandler =  function () {
                 tbFlot.ctx.tooltip.stop(true);
                 tbFlot.ctx.tooltip.hide();
+                tbFlot.ctx.plot.unhighlight();
                 tbFlot.isMouseInteraction = false;
             };
             this.ctx.$container.bind('mouseleave', this.mouseleaveHandler);
@@ -467,6 +974,152 @@ export default class TbFlot {
             this.mouseleaveHandler = null;
         }
     }
+
+
+    findHoverIndexFromData (posX, series) {
+        var lower = 0;
+        var upper = series.data.length - 1;
+        var middle;
+        var index = null;
+        while (index === null) {
+            if (lower > upper) {
+                return Math.max(upper, 0);
+            }
+            middle = Math.floor((lower + upper) / 2);
+            if (series.data[middle][0] === posX) {
+                return middle;
+            } else if (series.data[middle][0] < posX) {
+                lower = middle + 1;
+            } else {
+                upper = middle - 1;
+            }
+        }
+    }
+
+    findHoverIndexFromDataPoints (posX, series, last) {
+        var ps = series.datapoints.pointsize;
+        var initial = last*ps;
+        var len = series.datapoints.points.length;
+        for (var j = initial; j < len; j += ps) {
+            if ((!series.lines.steps && series.datapoints.points[initial] != null && series.datapoints.points[j] == null)
+                || series.datapoints.points[j] > posX) {
+                return Math.max(j - ps,  0)/ps;
+            }
+        }
+        return j/ps - 1;
+    }
+
+
+    getHoverInfo (seriesList, pos) {
+        var i, series, value, hoverIndex, hoverDistance, pointTime, minDistance, minTime;
+        var last_value = 0;
+        var results = {
+            seriesHover: []
+        };
+        for (i = 0; i < seriesList.length; i++) {
+            series = seriesList[i];
+            hoverIndex = this.findHoverIndexFromData(pos.x, series);
+            if (series.data[hoverIndex] && series.data[hoverIndex][0]) {
+                hoverDistance = pos.x - series.data[hoverIndex][0];
+                pointTime = series.data[hoverIndex][0];
+
+                if (!minDistance
+                    || (hoverDistance >= 0 && (hoverDistance < minDistance || minDistance < 0))
+                    || (hoverDistance < 0 && hoverDistance > minDistance)) {
+                    minDistance = hoverDistance;
+                    minTime = pointTime;
+                }
+                if (series.stack) {
+                    if (this.ctx.tooltipIndividual) {
+                        value = series.data[hoverIndex][1];
+                    } else {
+                        last_value += series.data[hoverIndex][1];
+                        value = last_value;
+                    }
+                } else {
+                    value = series.data[hoverIndex][1];
+                }
+
+                if (series.stack) {
+                    hoverIndex = this.findHoverIndexFromDataPoints(pos.x, series, hoverIndex);
+                }
+                results.seriesHover.push({
+                    value: value,
+                    hoverIndex: hoverIndex,
+                    color: series.dataKey.color,
+                    label: series.label,
+                    time: pointTime,
+                    distance: hoverDistance,
+                    index: i
+                });
+            }
+        }
+        results.time = minTime;
+        return results;
+    }
+
+    pieDataRendered() {
+        for (var i in this.ctx.pieTargetData) {
+            var value = this.ctx.pieTargetData[i] ? this.ctx.pieTargetData[i] : 0;
+            this.ctx.pieRenderedData[i] = value;
+            if (!this.ctx.pieData[i].data[0]) {
+                this.ctx.pieData[i].data[0] = [0,0];
+            }
+            this.ctx.pieData[i].data[0][1] = value;
+        }
+    }
+
+    nextPieDataAnimation(start) {
+        if (start) {
+            this.finishPieDataAnimation();
+            this.ctx.pieAnimationStartTime = this.ctx.pieAnimationLastTime = Date.now();
+            for (var i in this.ctx.data) {
+                this.ctx.pieTargetData[i] = (this.ctx.data[i].data && this.ctx.data[i].data[0])
+                    ? this.ctx.data[i].data[0][1] : 0;
+            }
+        }
+        if (this.ctx.pieAnimationCaf) {
+            this.ctx.pieAnimationCaf();
+            this.ctx.pieAnimationCaf = null;
+        }
+        var self = this;
+        this.ctx.pieAnimationCaf = this.ctx.$scope.tbRaf(
+            function () {
+                self.onPieDataAnimation();
+            }
+        );
+    }
+
+    onPieDataAnimation() {
+        var time = Date.now();
+        var elapsed = time - this.ctx.pieAnimationLastTime;//this.ctx.pieAnimationStartTime;
+        var progress = (time - this.ctx.pieAnimationStartTime) / this.ctx.pieDataAnimationDuration;
+        if (progress >= 1) {
+            this.finishPieDataAnimation();
+        } else {
+            if (elapsed >= 40) {
+                for (var i in this.ctx.pieTargetData) {
+                    var prevValue = this.ctx.pieRenderedData[i];
+                    var targetValue = this.ctx.pieTargetData[i];
+                    var value = prevValue + (targetValue - prevValue) * progress;
+                    if (!this.ctx.pieData[i].data[0]) {
+                        this.ctx.pieData[i].data[0] = [0,0];
+                    }
+                    this.ctx.pieData[i].data[0][1] = value;
+                }
+                this.ctx.plot.setData(this.ctx.pieData);
+                this.ctx.plot.draw();
+                this.ctx.pieAnimationLastTime = time;
+            }
+            this.nextPieDataAnimation(false);
+        }
+    }
+
+    finishPieDataAnimation() {
+        this.pieDataRendered();
+        this.ctx.plot.setData(this.ctx.pieData);
+        this.ctx.plot.draw();
+    }
 }
 
 /* eslint-enable angular/angularelement */
\ No newline at end of file
diff --git a/ui/src/app/widget/widget-library.controller.js b/ui/src/app/widget/widget-library.controller.js
index f7047e4..d07233a 100644
--- a/ui/src/app/widget/widget-library.controller.js
+++ b/ui/src/app/widget/widget-library.controller.js
@@ -54,7 +54,7 @@ export default function WidgetLibraryController($scope, $rootScope, $q, widgetSe
                     widgetService.getBundleWidgetTypes(bundleAlias, isSystem).then(
                         function (widgetTypes) {
 
-                            widgetTypes = $filter('orderBy')(widgetTypes, ['-descriptor.type','name']);
+                            widgetTypes = $filter('orderBy')(widgetTypes, ['-descriptor.type','-createdTime']);
 
                             var top = 0;
                             var lastTop = [0, 0, 0];