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