Browse Source

UI: Add bars widget. Improve tooltips and aggregation.

pull/56/head
Igor Kulikov 10 years ago
parent
commit
a0d7e4be05
  1. 7
      application/src/main/java/org/thingsboard/server/controller/DashboardController.java
  2. 1
      extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/cmd/TimeseriesSubscriptionCmd.java
  3. 6
      extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryWebsocketMsgHandler.java
  4. 12
      extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/sub/SubscriptionUpdate.java
  5. 16
      ui/src/app/api/dashboard.service.js
  6. 71
      ui/src/app/api/data-aggregator.js
  7. 185
      ui/src/app/api/datasource.service.js
  8. 26
      ui/src/app/components/dashboard.directive.js
  9. 2
      ui/src/app/components/dashboard.tpl.html
  10. 54
      ui/src/app/components/widget.controller.js
  11. 10
      ui/src/app/dashboard/dashboard.controller.js
  12. 1
      ui/src/app/dashboard/dashboard.tpl.html
  13. 6
      ui/src/app/device/attribute/attribute-table.directive.js
  14. 3
      ui/src/app/device/attribute/attribute-table.tpl.html
  15. 859
      ui/src/app/widget/lib/flot-widget.js
  16. 2
      ui/src/app/widget/widget-library.controller.js

7
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

1
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;

6
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));

12
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 + "]";
}
}

16
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;

71
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);
}
}

185
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],

26
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();

2
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>

54
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];

10
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();

1
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>

6
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];

3
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">

859
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 */

2
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];

Loading…
Cancel
Save