thingsboard-aplcache
Changes
extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/cmd/TimeseriesSubscriptionCmd.java 1(+1 -0)
extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryWebsocketMsgHandler.java 6(+3 -3)
extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/sub/SubscriptionUpdate.java 12(+1 -11)
ui/src/app/api/dashboard.service.js 16(+16 -0)
ui/src/app/api/data-aggregator.js 71(+40 -31)
ui/src/app/api/datasource.service.js 185(+96 -89)
ui/src/app/components/dashboard.directive.js 26(+25 -1)
ui/src/app/components/widget.controller.js 54(+32 -22)
ui/src/app/widget/lib/flot-widget.js 859(+756 -103)
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 + "]";
}
}
ui/src/app/api/dashboard.service.js 16(+16 -0)
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;
ui/src/app/api/data-aggregator.js 71(+40 -31)
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);
}
}
ui/src/app/api/datasource.service.js 185(+96 -89)
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],
ui/src/app/components/dashboard.directive.js 26(+25 -1)
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>
ui/src/app/components/widget.controller.js 54(+32 -22)
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">
ui/src/app/widget/lib/flot-widget.js 859(+756 -103)
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];