Commit 087cd95bb50fd3c25408645caba7c0ef4b539443

Authored by Igor Kulikov
1 parent 7769dd1c

UI: Improve aggregation interval configuration. Minor bug fixes.

Showing 27 changed files with 796 additions and 337 deletions
... ... @@ -23,19 +23,21 @@ public class BaseTsKvQuery implements TsKvQuery {
23 23 private final String key;
24 24 private final long startTs;
25 25 private final long endTs;
  26 + private final long interval;
26 27 private final int limit;
27 28 private final Aggregation aggregation;
28 29
29   - public BaseTsKvQuery(String key, long startTs, long endTs, int limit, Aggregation aggregation) {
  30 + public BaseTsKvQuery(String key, long startTs, long endTs, long interval, int limit, Aggregation aggregation) {
30 31 this.key = key;
31 32 this.startTs = startTs;
32 33 this.endTs = endTs;
  34 + this.interval = interval;
33 35 this.limit = limit;
34 36 this.aggregation = aggregation;
35 37 }
36 38
37 39 public BaseTsKvQuery(String key, long startTs, long endTs) {
38   - this(key, startTs, endTs, 1, Aggregation.AVG);
  40 + this(key, startTs, endTs, endTs-startTs, 1, Aggregation.AVG);
39 41 }
40 42
41 43 }
... ...
... ... @@ -25,6 +25,8 @@ public interface TsKvQuery {
25 25
26 26 long getEndTs();
27 27
  28 + long getInterval();
  29 +
28 30 int getLimit();
29 31
30 32 Aggregation getAggregation();
... ...
... ... @@ -112,13 +112,13 @@ public class BaseTimeseriesDao extends AbstractAsyncDao implements TimeseriesDao
112 112 if (query.getAggregation() == Aggregation.NONE) {
113 113 return findAllAsyncWithLimit(entityType, entityId, query);
114 114 } else {
115   - long step = Math.max((query.getEndTs() - query.getStartTs()) / query.getLimit(), minAggregationStepMs);
  115 + long step = Math.max(query.getInterval(), minAggregationStepMs);
116 116 long stepTs = query.getStartTs();
117 117 List<ListenableFuture<Optional<TsKvEntry>>> futures = new ArrayList<>();
118 118 while (stepTs < query.getEndTs()) {
119 119 long startTs = stepTs;
120 120 long endTs = stepTs + step;
121   - TsKvQuery subQuery = new BaseTsKvQuery(query.getKey(), startTs, endTs, 1, query.getAggregation());
  121 + TsKvQuery subQuery = new BaseTsKvQuery(query.getKey(), startTs, endTs, step, 1, query.getAggregation());
122 122 futures.add(findAndAggregateAsync(entityType, entityId, subQuery, toPartitionTs(startTs), toPartitionTs(endTs)));
123 123 stepTs = endTs;
124 124 }
... ...
... ... @@ -272,17 +272,17 @@ VALUES ( now ( ), minTimeuuid ( 0 ), 'maps', 'route_map',
272 272
273 273 INSERT INTO "thingsboard"."widget_type" ( "id", "tenant_id", "bundle_alias", "alias", "descriptor", "name" )
274 274 VALUES ( now ( ), minTimeuuid ( 0 ), 'charts', 'pie',
275   -'{"type":"latest","sizeX":8,"sizeY":5,"resources":[],"templateHtml":"","templateCss":".legend {\n font-size: 13px;\n line-height: 10px;\n}\n\n.legend table { \n border-spacing: 0px;\n border-collapse: separate;\n}\n\n.pie-label {\n font-size: 12px;\n font-family: ''Roboto'';\n font-weight: bold;\n text-align: center;\n padding: 2px;\n color: white;\n}\n","controllerScript":"self.onInit = function() {\n self.ctx.flot = new TbFlot(self.ctx, ''pie''); \n}\n\nself.onDataUpdated = function() {\n self.ctx.flot.update();\n}\n\nself.onResize = function() {\n self.ctx.flot.resize();\n}\n\nself.onEditModeChanged = function() {\n self.ctx.flot.checkMouseEvents();\n}\n\nself.onMobileModeChanged = function() {\n self.ctx.flot.checkMouseEvents();\n}\n\nself.getSettingsSchema = function() {\n return TbFlot.pieSettingsSchema;\n}\n\nself.getDataKeySettingsSchema = function() {\n return TbFlot.pieDatakeySettingsSchema;\n}\n\nself.onDestroy = function() {\n}\n","settingsSchema":"{}\n","dataKeySettingsSchema":"{}\n","defaultConfig":"{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"First\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = (prevValue-50) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+50;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Second\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.6114638304362894,\"funcBody\":\"var value = (prevValue-20) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+20;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Third\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.9955906536344441,\"funcBody\":\"var value = (prevValue-40) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+40;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Fourth\",\"color\":\"#ffc107\",\"settings\":{},\"_hash\":0.9430835931647599,\"funcBody\":\"var value = (prevValue-50) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+50;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"radius\":1,\"fontColor\":\"#545454\",\"fontSize\":10,\"decimals\":1,\"legend\":{\"show\":true,\"position\":\"nw\",\"labelBoxBorderColor\":\"#CCCCCC\",\"backgroundColor\":\"#F0F0F0\",\"backgroundOpacity\":0.85},\"innerRadius\":0,\"showLabels\":true,\"stroke\":{\"width\":5},\"tilt\":1,\"animatedPie\":false},\"title\":\"Pie - Flot\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400}}"}',
  275 +'{"type":"latest","sizeX":8,"sizeY":5,"resources":[],"templateHtml":"","templateCss":".legend {\n font-size: 13px;\n line-height: 10px;\n}\n\n.legend table { \n border-spacing: 0px;\n border-collapse: separate;\n}\n\n.pie-label {\n font-size: 12px;\n font-family: ''Roboto'';\n font-weight: bold;\n text-align: center;\n padding: 2px;\n color: white;\n}\n","controllerScript":"self.onInit = function() {\n self.ctx.flot = new TbFlot(self.ctx, ''pie''); \n}\n\nself.onDataUpdated = function() {\n self.ctx.flot.update();\n}\n\nself.onResize = function() {\n self.ctx.flot.resize();\n}\n\nself.onEditModeChanged = function() {\n self.ctx.flot.checkMouseEvents();\n}\n\nself.onMobileModeChanged = function() {\n self.ctx.flot.checkMouseEvents();\n}\n\nself.getSettingsSchema = function() {\n return TbFlot.pieSettingsSchema;\n}\n\nself.getDataKeySettingsSchema = function() {\n return TbFlot.pieDatakeySettingsSchema;\n}\n\nself.onDestroy = function() {\n self.ctx.flot.destroy();\n}\n","settingsSchema":"{}\n","dataKeySettingsSchema":"{}\n","defaultConfig":"{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"First\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = (prevValue-50) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+50;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Second\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.6114638304362894,\"funcBody\":\"var value = (prevValue-20) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+20;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Third\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.9955906536344441,\"funcBody\":\"var value = (prevValue-40) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+40;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Fourth\",\"color\":\"#ffc107\",\"settings\":{},\"_hash\":0.9430835931647599,\"funcBody\":\"var value = (prevValue-50) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+50;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"radius\":1,\"fontColor\":\"#545454\",\"fontSize\":10,\"decimals\":1,\"legend\":{\"show\":true,\"position\":\"nw\",\"labelBoxBorderColor\":\"#CCCCCC\",\"backgroundColor\":\"#F0F0F0\",\"backgroundOpacity\":0.85},\"innerRadius\":0,\"showLabels\":true,\"stroke\":{\"width\":5},\"tilt\":1,\"animatedPie\":false},\"title\":\"Pie - Flot\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400}}"}',
276 276 'Pie - Flot' );
277 277
278 278 INSERT INTO "thingsboard"."widget_type" ( "id", "tenant_id", "bundle_alias", "alias", "descriptor", "name" )
279 279 VALUES ( now ( ), minTimeuuid ( 0 ), 'charts', 'timeseries_bars_flot',
280   -'{"type":"timeseries","sizeX":8,"sizeY":5,"resources":[],"templateHtml":"","templateCss":".legend {\n font-size: 13px;\n line-height: 10px;\n}\n\n.legend table { \n border-spacing: 0px;\n border-collapse: separate;\n}\n\n.mouse-events .flot-overlay {\n cursor: crosshair; \n}\n\n","controllerScript":"self.onInit = function() {\n self.ctx.flot = new TbFlot(self.ctx, ''bar''); \n}\n\nself.onDataUpdated = function() {\n self.ctx.flot.update();\n}\n\nself.onResize = function() {\n self.ctx.flot.resize();\n}\n\nself.onEditModeChanged = function() {\n self.ctx.flot.checkMouseEvents();\n}\n\nself.onMobileModeChanged = function() {\n self.ctx.flot.checkMouseEvents();\n}\n\nself.getSettingsSchema = function() {\n return TbFlot.settingsSchema;\n}\n\nself.getDataKeySettingsSchema = function() {\n return TbFlot.datakeySettingsSchema(false);\n}\n\nself.onDestroy = function() {\n}\n","settingsSchema":"{}","dataKeySettingsSchema":"{}","defaultConfig":"{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"First\",\"color\":\"#2196f3\",\"settings\":{\"showLines\":false,\"fillLines\":false,\"showPoints\":false},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Second\",\"color\":\"#ffc107\",\"settings\":{\"showLines\":false,\"fillLines\":false,\"showPoints\":false},\"_hash\":0.12775350966079668,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000},\"aggregation\":{\"limit\":200,\"type\":\"AVG\"}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"shadowSize\":4,\"fontColor\":\"#545454\",\"fontSize\":10,\"xaxis\":{\"showLabels\":true,\"color\":\"#545454\"},\"yaxis\":{\"showLabels\":true,\"color\":\"#545454\"},\"grid\":{\"color\":\"#545454\",\"tickColor\":\"#DDDDDD\",\"verticalLines\":true,\"horizontalLines\":true,\"outlineWidth\":1},\"legend\":{\"show\":true,\"position\":\"nw\",\"backgroundColor\":\"#f0f0f0\",\"backgroundOpacity\":0.85,\"labelBoxBorderColor\":\"rgba(1, 1, 1, 0.45)\"},\"decimals\":1,\"stack\":true,\"tooltipIndividual\":false},\"title\":\"Timeseries Bars - Flot\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"mobileHeight\":null}"}',
  280 +'{"type":"timeseries","sizeX":8,"sizeY":5,"resources":[],"templateHtml":"","templateCss":".legend {\n font-size: 13px;\n line-height: 10px;\n}\n\n.legend table { \n border-spacing: 0px;\n border-collapse: separate;\n}\n\n.mouse-events .flot-overlay {\n cursor: crosshair; \n}\n\n","controllerScript":"self.onInit = function() {\n self.ctx.flot = new TbFlot(self.ctx, ''bar''); \n}\n\nself.onDataUpdated = function() {\n self.ctx.flot.update();\n}\n\nself.onResize = function() {\n self.ctx.flot.resize();\n}\n\nself.onEditModeChanged = function() {\n self.ctx.flot.checkMouseEvents();\n}\n\nself.onMobileModeChanged = function() {\n self.ctx.flot.checkMouseEvents();\n}\n\nself.getSettingsSchema = function() {\n return TbFlot.settingsSchema;\n}\n\nself.getDataKeySettingsSchema = function() {\n return TbFlot.datakeySettingsSchema(false);\n}\n\nself.onDestroy = function() {\n self.ctx.flot.destroy();\n}\n","settingsSchema":"{}","dataKeySettingsSchema":"{}","defaultConfig":"{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"First\",\"color\":\"#2196f3\",\"settings\":{\"showLines\":false,\"fillLines\":false,\"showPoints\":false},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Second\",\"color\":\"#ffc107\",\"settings\":{\"showLines\":false,\"fillLines\":false,\"showPoints\":false},\"_hash\":0.12775350966079668,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000},\"aggregation\":{\"limit\":200,\"type\":\"AVG\"}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"shadowSize\":4,\"fontColor\":\"#545454\",\"fontSize\":10,\"xaxis\":{\"showLabels\":true,\"color\":\"#545454\"},\"yaxis\":{\"showLabels\":true,\"color\":\"#545454\"},\"grid\":{\"color\":\"#545454\",\"tickColor\":\"#DDDDDD\",\"verticalLines\":true,\"horizontalLines\":true,\"outlineWidth\":1},\"legend\":{\"show\":true,\"position\":\"nw\",\"backgroundColor\":\"#f0f0f0\",\"backgroundOpacity\":0.85,\"labelBoxBorderColor\":\"rgba(1, 1, 1, 0.45)\"},\"decimals\":1,\"stack\":true,\"tooltipIndividual\":false},\"title\":\"Timeseries Bars - Flot\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"mobileHeight\":null}"}',
281 281 'Timeseries Bars - Flot' );
282 282
283 283 INSERT INTO "thingsboard"."widget_type" ( "id", "tenant_id", "bundle_alias", "alias", "descriptor", "name" )
284 284 VALUES ( now ( ), minTimeuuid ( 0 ), 'charts', 'basic_timeseries',
285   -'{"type":"timeseries","sizeX":8,"sizeY":5,"resources":[],"templateHtml":"","templateCss":".legend {\n font-size: 13px;\n line-height: 10px;\n}\n\n.legend table { \n border-spacing: 0px;\n border-collapse: separate;\n}\n\n.mouse-events .flot-overlay {\n cursor: crosshair; \n}\n\n","controllerScript":"self.onInit = function() {\n self.ctx.flot = new TbFlot(self.ctx); \n}\n\nself.onDataUpdated = function() {\n self.ctx.flot.update();\n}\n\nself.onResize = function() {\n self.ctx.flot.resize();\n}\n\nself.onEditModeChanged = function() {\n self.ctx.flot.checkMouseEvents();\n}\n\nself.onMobileModeChanged = function() {\n self.ctx.flot.checkMouseEvents();\n}\n\nself.getSettingsSchema = function() {\n return TbFlot.settingsSchema;\n}\n\nself.getDataKeySettingsSchema = function() {\n return TbFlot.datakeySettingsSchema(true);\n}\n\nself.onDestroy = function() {\n}\n","settingsSchema":"{}","dataKeySettingsSchema":"{}","defaultConfig":"{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"First\",\"color\":\"#2196f3\",\"settings\":{\"showLines\":true,\"fillLines\":true,\"showPoints\":false},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Second\",\"color\":\"#ffc107\",\"settings\":{\"showLines\":true,\"fillLines\":false,\"showPoints\":false},\"_hash\":0.12775350966079668,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"shadowSize\":4,\"fontColor\":\"#545454\",\"fontSize\":10,\"xaxis\":{\"showLabels\":true,\"color\":\"#545454\"},\"yaxis\":{\"showLabels\":true,\"color\":\"#545454\"},\"grid\":{\"color\":\"#545454\",\"tickColor\":\"#DDDDDD\",\"verticalLines\":true,\"horizontalLines\":true,\"outlineWidth\":1},\"legend\":{\"show\":true,\"position\":\"nw\",\"backgroundColor\":\"#f0f0f0\",\"backgroundOpacity\":0.85,\"labelBoxBorderColor\":\"rgba(1, 1, 1, 0.45)\"},\"decimals\":1,\"stack\":false,\"tooltipIndividual\":false},\"title\":\"Timeseries - Flot\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"mobileHeight\":null}"}',
  285 +'{"type":"timeseries","sizeX":8,"sizeY":5,"resources":[],"templateHtml":"","templateCss":".legend {\n font-size: 13px;\n line-height: 10px;\n}\n\n.legend table { \n border-spacing: 0px;\n border-collapse: separate;\n}\n\n.mouse-events .flot-overlay {\n cursor: crosshair; \n}\n\n","controllerScript":"self.onInit = function() {\n self.ctx.flot = new TbFlot(self.ctx); \n}\n\nself.onDataUpdated = function() {\n self.ctx.flot.update();\n}\n\nself.onResize = function() {\n self.ctx.flot.resize();\n}\n\nself.onEditModeChanged = function() {\n self.ctx.flot.checkMouseEvents();\n}\n\nself.onMobileModeChanged = function() {\n self.ctx.flot.checkMouseEvents();\n}\n\nself.getSettingsSchema = function() {\n return TbFlot.settingsSchema;\n}\n\nself.getDataKeySettingsSchema = function() {\n return TbFlot.datakeySettingsSchema(true);\n}\n\nself.onDestroy = function() {\n self.ctx.flot.destroy();\n}\n","settingsSchema":"{}","dataKeySettingsSchema":"{}","defaultConfig":"{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"First\",\"color\":\"#2196f3\",\"settings\":{\"showLines\":true,\"fillLines\":true,\"showPoints\":false},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Second\",\"color\":\"#ffc107\",\"settings\":{\"showLines\":true,\"fillLines\":false,\"showPoints\":false},\"_hash\":0.12775350966079668,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"shadowSize\":4,\"fontColor\":\"#545454\",\"fontSize\":10,\"xaxis\":{\"showLabels\":true,\"color\":\"#545454\"},\"yaxis\":{\"showLabels\":true,\"color\":\"#545454\"},\"grid\":{\"color\":\"#545454\",\"tickColor\":\"#DDDDDD\",\"verticalLines\":true,\"horizontalLines\":true,\"outlineWidth\":1},\"legend\":{\"show\":true,\"position\":\"nw\",\"backgroundColor\":\"#f0f0f0\",\"backgroundOpacity\":0.85,\"labelBoxBorderColor\":\"rgba(1, 1, 1, 0.45)\"},\"decimals\":1,\"stack\":false,\"tooltipIndividual\":false},\"title\":\"Timeseries - Flot\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"mobileHeight\":null}"}',
286 286 'Timeseries - Flot' );
287 287
288 288 /** System plugins and rules **/
... ...
... ... @@ -115,7 +115,7 @@ public class TimeseriesServiceTest extends AbstractServiceTest {
115 115 entries.add(save(deviceId, 55000, 600));
116 116
117 117 List<TsKvEntry> list = tsService.findAll(DataConstants.DEVICE, deviceId, Collections.singletonList(new BaseTsKvQuery(LONG_KEY, 0,
118   - 60000, 3, Aggregation.NONE))).get();
  118 + 60000, 20000, 3, Aggregation.NONE))).get();
119 119 assertEquals(3, list.size());
120 120 assertEquals(55000, list.get(0).getTs());
121 121 assertEquals(java.util.Optional.of(600L), list.get(0).getLongValue());
... ... @@ -127,7 +127,7 @@ public class TimeseriesServiceTest extends AbstractServiceTest {
127 127 assertEquals(java.util.Optional.of(400L), list.get(2).getLongValue());
128 128
129 129 list = tsService.findAll(DataConstants.DEVICE, deviceId, Collections.singletonList(new BaseTsKvQuery(LONG_KEY, 0,
130   - 60000, 3, Aggregation.AVG))).get();
  130 + 60000, 20000, 3, Aggregation.AVG))).get();
131 131 assertEquals(3, list.size());
132 132 assertEquals(10000, list.get(0).getTs());
133 133 assertEquals(java.util.Optional.of(150L), list.get(0).getLongValue());
... ... @@ -139,7 +139,7 @@ public class TimeseriesServiceTest extends AbstractServiceTest {
139 139 assertEquals(java.util.Optional.of(550L), list.get(2).getLongValue());
140 140
141 141 list = tsService.findAll(DataConstants.DEVICE, deviceId, Collections.singletonList(new BaseTsKvQuery(LONG_KEY, 0,
142   - 60000, 3, Aggregation.SUM))).get();
  142 + 60000, 20000, 3, Aggregation.SUM))).get();
143 143
144 144 assertEquals(3, list.size());
145 145 assertEquals(10000, list.get(0).getTs());
... ... @@ -152,7 +152,7 @@ public class TimeseriesServiceTest extends AbstractServiceTest {
152 152 assertEquals(java.util.Optional.of(1100L), list.get(2).getLongValue());
153 153
154 154 list = tsService.findAll(DataConstants.DEVICE, deviceId, Collections.singletonList(new BaseTsKvQuery(LONG_KEY, 0,
155   - 60000, 3, Aggregation.MIN))).get();
  155 + 60000, 20000, 3, Aggregation.MIN))).get();
156 156
157 157 assertEquals(3, list.size());
158 158 assertEquals(10000, list.get(0).getTs());
... ... @@ -165,7 +165,7 @@ public class TimeseriesServiceTest extends AbstractServiceTest {
165 165 assertEquals(java.util.Optional.of(500L), list.get(2).getLongValue());
166 166
167 167 list = tsService.findAll(DataConstants.DEVICE, deviceId, Collections.singletonList(new BaseTsKvQuery(LONG_KEY, 0,
168   - 60000, 3, Aggregation.MAX))).get();
  168 + 60000, 20000, 3, Aggregation.MAX))).get();
169 169
170 170 assertEquals(3, list.size());
171 171 assertEquals(10000, list.get(0).getTs());
... ... @@ -178,7 +178,7 @@ public class TimeseriesServiceTest extends AbstractServiceTest {
178 178 assertEquals(java.util.Optional.of(600L), list.get(2).getLongValue());
179 179
180 180 list = tsService.findAll(DataConstants.DEVICE, deviceId, Collections.singletonList(new BaseTsKvQuery(LONG_KEY, 0,
181   - 60000, 3, Aggregation.COUNT))).get();
  181 + 60000, 20000, 3, Aggregation.COUNT))).get();
182 182
183 183 assertEquals(3, list.size());
184 184 assertEquals(10000, list.get(0).getTs());
... ...
... ... @@ -32,6 +32,7 @@ public class GetHistoryCmd implements TelemetryPluginCmd {
32 32 private String keys;
33 33 private long startTs;
34 34 private long endTs;
  35 + private long interval;
35 36 private int limit;
36 37 private String agg;
37 38
... ...
... ... @@ -30,6 +30,7 @@ public class TimeseriesSubscriptionCmd extends SubscriptionCmd {
30 30
31 31 private long startTs;
32 32 private long timeWindow;
  33 + private long interval;
33 34 private int limit;
34 35 private String agg;
35 36
... ...
... ... @@ -89,11 +89,12 @@ public class TelemetryRestMsgHandler extends DefaultRestMsgHandler {
89 89 String keysStr = request.getParameter("keys");
90 90 Optional<Long> startTs = request.getLongParamValue("startTs");
91 91 Optional<Long> endTs = request.getLongParamValue("endTs");
  92 + Optional<Long> interval = request.getLongParamValue("interval");
92 93 Optional<Integer> limit = request.getIntParamValue("limit");
93 94 Aggregation agg = Aggregation.valueOf(request.getParameter("agg", Aggregation.NONE.name()));
94 95
95 96 List<String> keys = Arrays.asList(keysStr.split(","));
96   - List<TsKvQuery> queries = keys.stream().map(key -> new BaseTsKvQuery(key, startTs.get(), endTs.get(), limit.orElse(TelemetryWebsocketMsgHandler.DEFAULT_LIMIT), agg)).collect(Collectors.toList());
  97 + List<TsKvQuery> queries = keys.stream().map(key -> new BaseTsKvQuery(key, startTs.get(), endTs.get(), interval.get(), limit.orElse(TelemetryWebsocketMsgHandler.DEFAULT_LIMIT), agg)).collect(Collectors.toList());
97 98 ctx.loadTimeseries(deviceId, queries, new PluginCallback<List<TsKvEntry>>() {
98 99 @Override
99 100 public void onSuccess(PluginContext ctx, List<TsKvEntry> data) {
... ...
... ... @@ -193,7 +193,7 @@ public class TelemetryWebsocketMsgHandler extends DefaultWebsocketMsgHandler {
193 193 log.debug("[{}] fetching timeseries data for last {} ms for keys: ({}) for device : {}", sessionId, cmd.getTimeWindow(), cmd.getKeys(), cmd.getDeviceId());
194 194 startTs = cmd.getStartTs();
195 195 long endTs = cmd.getStartTs() + cmd.getTimeWindow();
196   - List<TsKvQuery> queries = keys.stream().map(key -> new BaseTsKvQuery(key, startTs, endTs, getLimit(cmd.getLimit()), getAggregation(cmd.getAgg()))).collect(Collectors.toList());
  196 + List<TsKvQuery> queries = keys.stream().map(key -> new BaseTsKvQuery(key, startTs, endTs, cmd.getInterval(), 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 {
199 199 List<String> keys = new ArrayList<>(getKeys(cmd).orElse(Collections.emptySet()));
... ... @@ -277,7 +277,7 @@ public class TelemetryWebsocketMsgHandler extends DefaultWebsocketMsgHandler {
277 277 }
278 278 DeviceId deviceId = DeviceId.fromString(cmd.getDeviceId());
279 279 List<String> keys = new ArrayList<>(getKeys(cmd).orElse(Collections.emptySet()));
280   - List<TsKvQuery> queries = keys.stream().map(key -> new BaseTsKvQuery(key, cmd.getStartTs(), cmd.getEndTs(), getLimit(cmd.getLimit()), getAggregation(cmd.getAgg()))).collect(Collectors.toList());
  280 + List<TsKvQuery> queries = keys.stream().map(key -> new BaseTsKvQuery(key, cmd.getStartTs(), cmd.getEndTs(), cmd.getInterval(), getLimit(cmd.getLimit()), getAggregation(cmd.getAgg()))).collect(Collectors.toList());
281 281 ctx.loadTimeseries(deviceId, queries, new PluginCallback<List<TsKvEntry>>() {
282 282 @Override
283 283 public void onSuccess(PluginContext ctx, List<TsKvEntry> data) {
... ...
... ... @@ -25,11 +25,12 @@ export default class DataAggregator {
25 25 this.$timeout = $timeout;
26 26 this.$filter = $filter;
27 27 this.dataReceived = false;
  28 + this.resetPending = false;
28 29 this.noAggregation = aggregationType === types.aggregation.none.value;
29 30 this.limit = limit;
30 31 this.timeWindow = timeWindow;
31 32 this.interval = interval;
32   - this.aggregationTimeout = this.interval;
  33 + this.aggregationTimeout = Math.max(this.interval, 1000);
33 34 switch (aggregationType) {
34 35 case types.aggregation.min.value:
35 36 this.aggFunction = min;
... ... @@ -54,11 +55,37 @@ export default class DataAggregator {
54 55 }
55 56 }
56 57
  58 + reset(startTs, timeWindow, interval) {
  59 + if (this.intervalTimeoutHandle) {
  60 + this.$timeout.cancel(this.intervalTimeoutHandle);
  61 + this.intervalTimeoutHandle = null;
  62 + }
  63 + this.intervalScheduledTime = currentTime();
  64 + this.startTs = startTs;
  65 + this.timeWindow = timeWindow;
  66 + this.interval = interval;
  67 + this.endTs = this.startTs + this.timeWindow;
  68 + this.elapsed = 0;
  69 + this.aggregationTimeout = Math.max(this.interval, 1000);
  70 + this.resetPending = true;
  71 + var self = this;
  72 + this.intervalTimeoutHandle = this.$timeout(function() {
  73 + self.onInterval();
  74 + }, this.aggregationTimeout, false);
  75 + }
  76 +
57 77 onData(data, update, history) {
58   - if (!this.dataReceived) {
59   - this.elapsed = 0;
60   - this.dataReceived = true;
61   - this.endTs = this.startTs + this.timeWindow;
  78 + if (!this.dataReceived || this.resetPending) {
  79 + var updateIntervalScheduledTime = true;
  80 + if (!this.dataReceived) {
  81 + this.elapsed = 0;
  82 + this.dataReceived = true;
  83 + this.endTs = this.startTs + this.timeWindow;
  84 + }
  85 + if (this.resetPending) {
  86 + this.resetPending = false;
  87 + updateIntervalScheduledTime = false;
  88 + }
62 89 if (update) {
63 90 this.aggregationMap = {};
64 91 updateAggregatedData(this.aggregationMap, this.aggregationType === this.types.aggregation.count.value,
... ... @@ -66,19 +93,24 @@ export default class DataAggregator {
66 93 } else {
67 94 this.aggregationMap = processAggregatedData(data.data, this.aggregationType === this.types.aggregation.count.value, this.noAggregation);
68 95 }
69   - this.onInterval(currentTime(), history);
  96 + if (updateIntervalScheduledTime) {
  97 + this.intervalScheduledTime = currentTime();
  98 + }
  99 + this.onInterval(history);
70 100 } else {
71 101 updateAggregatedData(this.aggregationMap, this.aggregationType === this.types.aggregation.count.value,
72 102 this.noAggregation, this.aggFunction, data.data, this.interval, this.startTs);
73 103 if (history) {
74   - this.onInterval(currentTime(), history);
  104 + this.intervalScheduledTime = currentTime();
  105 + this.onInterval(history);
75 106 }
76 107 }
77 108 }
78 109
79   - onInterval(startedTime, history) {
  110 + onInterval(history) {
80 111 var now = currentTime();
81   - this.elapsed += now - startedTime;
  112 + this.elapsed += now - this.intervalScheduledTime;
  113 + this.intervalScheduledTime = now;
82 114 if (this.intervalTimeoutHandle) {
83 115 this.$timeout.cancel(this.intervalTimeoutHandle);
84 116 this.intervalTimeoutHandle = null;
... ... @@ -101,16 +133,11 @@ export default class DataAggregator {
101 133 var self = this;
102 134 if (!history) {
103 135 this.intervalTimeoutHandle = this.$timeout(function() {
104   - self.onInterval(now);
  136 + self.onInterval();
105 137 }, this.aggregationTimeout, false);
106 138 }
107 139 }
108 140
109   - reset() {
110   - this.destroy();
111   - this.dataReceived = false;
112   - }
113   -
114 141 destroy() {
115 142 if (this.intervalTimeoutHandle) {
116 143 this.$timeout.cancel(this.intervalTimeoutHandle);
... ...
... ... @@ -254,6 +254,7 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic
254 254 keys: tsKeys,
255 255 startTs: subsTw.fixedWindow.startTimeMs,
256 256 endTs: subsTw.fixedWindow.endTimeMs,
  257 + interval: subsTw.aggregation.interval,
257 258 limit: subsTw.aggregation.limit,
258 259 agg: subsTw.aggregation.type
259 260 };
... ... @@ -266,9 +267,7 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic
266 267 onData(data.data, types.dataKeyType.timeseries);
267 268 }
268 269 },
269   - onReconnected: function() {
270   - onReconnected();
271   - }
  270 + onReconnected: function() {}
272 271 };
273 272
274 273 telemetryWebsocketService.subscribe(subscriber);
... ... @@ -287,35 +286,26 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic
287 286 };
288 287
289 288 if (datasourceSubscription.type === types.widgetType.timeseries.value) {
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(
295   - function(data, startTs, endTs) {
296   - onData(data, types.dataKeyType.timeseries, startTs, endTs);
297   - },
298   - tsKeyNames,
299   - subsTw.startTs,
300   - subsTw.aggregation.limit,
301   - subsTw.aggregation.type,
302   - subsTw.aggregation.timeWindow,
303   - subsTw.aggregation.interval,
304   - types,
305   - $timeout,
306   - $filter
307   - );
  289 + updateRealtimeSubscriptionCommand(subscriptionCommand, subsTw);
  290 + dataAggregator = createRealtimeDataAggregator(subsTw, tsKeyNames);
308 291 subscriber.onData = function(data) {
309 292 dataAggregator.onData(data);
310 293 }
311 294 subscriber.onReconnected = function() {
312   - dataAggregator.reset();
313   - onReconnected();
  295 + var newSubsTw = null;
  296 + for (var i2 in listeners) {
  297 + var listener = listeners[i2];
  298 + if (!newSubsTw) {
  299 + newSubsTw = listener.updateRealtimeSubscription();
  300 + } else {
  301 + listener.setRealtimeSubscription(newSubsTw);
  302 + }
  303 + }
  304 + updateRealtimeSubscriptionCommand(this.subscriptionCommand, newSubsTw);
  305 + dataAggregator.reset(newSubsTw.startTs, newSubsTw.aggregation.timeWindow, newSubsTw.aggregation.interval);
314 306 }
315 307 } else {
316   - subscriber.onReconnected = function() {
317   - onReconnected();
318   - }
  308 + subscriber.onReconnected = function() {}
319 309 subscriber.onData = function(data) {
320 310 if (data.data) {
321 311 onData(data.data, types.dataKeyType.timeseries);
... ... @@ -344,9 +334,7 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic
344 334 onData(data.data, types.dataKeyType.attribute);
345 335 }
346 336 },
347   - onReconnected: function() {
348   - onReconnected();
349   - }
  337 + onReconnected: function() {}
350 338 };
351 339
352 340 telemetryWebsocketService.subscribe(subscriber);
... ... @@ -384,7 +372,31 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic
384 372 timer = $timeout(onTick, 0, false);
385 373 }
386 374 }
  375 + }
  376 +
  377 + function createRealtimeDataAggregator(subsTw, tsKeyNames) {
  378 + return new DataAggregator(
  379 + function(data, startTs, endTs) {
  380 + onData(data, types.dataKeyType.timeseries, startTs, endTs);
  381 + },
  382 + tsKeyNames,
  383 + subsTw.startTs,
  384 + subsTw.aggregation.limit,
  385 + subsTw.aggregation.type,
  386 + subsTw.aggregation.timeWindow,
  387 + subsTw.aggregation.interval,
  388 + types,
  389 + $timeout,
  390 + $filter
  391 + );
  392 + }
387 393
  394 + function updateRealtimeSubscriptionCommand(subscriptionCommand, subsTw) {
  395 + subscriptionCommand.startTs = subsTw.startTs;
  396 + subscriptionCommand.timeWindow = subsTw.aggregation.timeWindow;
  397 + subscriptionCommand.interval = subsTw.aggregation.interval;
  398 + subscriptionCommand.limit = subsTw.aggregation.limit;
  399 + subscriptionCommand.agg = subsTw.aggregation.type;
388 400 }
389 401
390 402 function unsubscribe() {
... ... @@ -495,27 +507,6 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic
495 507 }
496 508 }
497 509
498   - function onReconnected() {
499   - if (datasourceType === types.datasourceType.device) {
500   - for (var key in dataKeys) {
501   - var dataKeysList = dataKeys[key];
502   - for (var i = 0; i < dataKeysList.length; i++) {
503   - var dataKey = dataKeysList[i];
504   - var datasourceKey = key + '_' + i;
505   - datasourceData[datasourceKey] = {
506   - data: []
507   - };
508   - for (var l in listeners) {
509   - var listener = listeners[l];
510   - listener.dataUpdated(datasourceData[datasourceKey],
511   - listener.datasourceIndex,
512   - dataKey.index);
513   - }
514   - }
515   - }
516   - }
517   - }
518   -
519 510 function isNumeric(val) {
520 511 return (val - parseFloat( val ) + 1) >= 0;
521 512 }
... ...
  1 +/*
  2 + * Copyright © 2016-2017 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +export default angular.module('thingsboard.api.time', [])
  17 + .factory('timeService', TimeService)
  18 + .name;
  19 +
  20 +const SECOND = 1000;
  21 +const MINUTE = 60 * SECOND;
  22 +const HOUR = 60 * MINUTE;
  23 +const DAY = 24 * HOUR;
  24 +
  25 +const MIN_INTERVAL = SECOND;
  26 +const MAX_INTERVAL = 365 * 20 * DAY;
  27 +
  28 +const MIN_LIMIT = 10;
  29 +const AVG_LIMIT = 200;
  30 +const MAX_LIMIT = 500;
  31 +
  32 +/*@ngInject*/
  33 +function TimeService($translate, types) {
  34 +
  35 + var predefIntervals = [
  36 + {
  37 + name: $translate.instant('timeinterval.seconds-interval', {seconds: 1}, 'messageformat'),
  38 + value: 1 * SECOND
  39 + },
  40 + {
  41 + name: $translate.instant('timeinterval.seconds-interval', {seconds: 5}, 'messageformat'),
  42 + value: 5 * SECOND
  43 + },
  44 + {
  45 + name: $translate.instant('timeinterval.seconds-interval', {seconds: 10}, 'messageformat'),
  46 + value: 10 * SECOND
  47 + },
  48 + {
  49 + name: $translate.instant('timeinterval.seconds-interval', {seconds: 15}, 'messageformat'),
  50 + value: 15 * SECOND
  51 + },
  52 + {
  53 + name: $translate.instant('timeinterval.seconds-interval', {seconds: 30}, 'messageformat'),
  54 + value: 30 * SECOND
  55 + },
  56 + {
  57 + name: $translate.instant('timeinterval.minutes-interval', {minutes: 1}, 'messageformat'),
  58 + value: 1 * MINUTE
  59 + },
  60 + {
  61 + name: $translate.instant('timeinterval.minutes-interval', {minutes: 2}, 'messageformat'),
  62 + value: 2 * MINUTE
  63 + },
  64 + {
  65 + name: $translate.instant('timeinterval.minutes-interval', {minutes: 5}, 'messageformat'),
  66 + value: 5 * MINUTE
  67 + },
  68 + {
  69 + name: $translate.instant('timeinterval.minutes-interval', {minutes: 10}, 'messageformat'),
  70 + value: 10 * MINUTE
  71 + },
  72 + {
  73 + name: $translate.instant('timeinterval.minutes-interval', {minutes: 15}, 'messageformat'),
  74 + value: 15 * MINUTE
  75 + },
  76 + {
  77 + name: $translate.instant('timeinterval.minutes-interval', {minutes: 30}, 'messageformat'),
  78 + value: 30 * MINUTE
  79 + },
  80 + {
  81 + name: $translate.instant('timeinterval.hours-interval', {hours: 1}, 'messageformat'),
  82 + value: 1 * HOUR
  83 + },
  84 + {
  85 + name: $translate.instant('timeinterval.hours-interval', {hours: 2}, 'messageformat'),
  86 + value: 2 * HOUR
  87 + },
  88 + {
  89 + name: $translate.instant('timeinterval.hours-interval', {hours: 5}, 'messageformat'),
  90 + value: 5 * HOUR
  91 + },
  92 + {
  93 + name: $translate.instant('timeinterval.hours-interval', {hours: 10}, 'messageformat'),
  94 + value: 10 * HOUR
  95 + },
  96 + {
  97 + name: $translate.instant('timeinterval.hours-interval', {hours: 12}, 'messageformat'),
  98 + value: 12 * HOUR
  99 + },
  100 + {
  101 + name: $translate.instant('timeinterval.days-interval', {days: 1}, 'messageformat'),
  102 + value: 1 * DAY
  103 + },
  104 + {
  105 + name: $translate.instant('timeinterval.days-interval', {days: 7}, 'messageformat'),
  106 + value: 7 * DAY
  107 + },
  108 + {
  109 + name: $translate.instant('timeinterval.days-interval', {days: 30}, 'messageformat'),
  110 + value: 30 * DAY
  111 + }
  112 + ];
  113 +
  114 + var service = {
  115 + minIntervalLimit: minIntervalLimit,
  116 + maxIntervalLimit: maxIntervalLimit,
  117 + boundMinInterval: boundMinInterval,
  118 + boundMaxInterval: boundMaxInterval,
  119 + getIntervals: getIntervals,
  120 + matchesExistingInterval: matchesExistingInterval,
  121 + boundToPredefinedInterval: boundToPredefinedInterval,
  122 + defaultTimewindow: defaultTimewindow,
  123 + toHistoryTimewindow: toHistoryTimewindow,
  124 + createSubscriptionTimewindow: createSubscriptionTimewindow,
  125 + avgAggregationLimit: function () {
  126 + return AVG_LIMIT;
  127 + }
  128 + }
  129 +
  130 + return service;
  131 +
  132 + function minIntervalLimit(timewindow) {
  133 + var min = timewindow / MAX_LIMIT;
  134 + return boundMinInterval(min);
  135 + }
  136 +
  137 + function avgInterval(timewindow) {
  138 + var avg = timewindow / AVG_LIMIT;
  139 + return boundMinInterval(avg);
  140 + }
  141 +
  142 + function maxIntervalLimit(timewindow) {
  143 + var max = timewindow / MIN_LIMIT;
  144 + return boundMaxInterval(max);
  145 + }
  146 +
  147 + function boundMinInterval(min) {
  148 + return toBound(min, MIN_INTERVAL, MAX_INTERVAL, MIN_INTERVAL);
  149 + }
  150 +
  151 + function boundMaxInterval(max) {
  152 + return toBound(max, MIN_INTERVAL, MAX_INTERVAL, MAX_INTERVAL);
  153 + }
  154 +
  155 + function toBound(value, min, max, defValue) {
  156 + if (angular.isDefined(value)) {
  157 + value = Math.max(value, min);
  158 + value = Math.min(value, max);
  159 + return value;
  160 + } else {
  161 + return defValue;
  162 + }
  163 + }
  164 +
  165 + function getIntervals(min, max) {
  166 + min = boundMinInterval(min);
  167 + max = boundMaxInterval(max);
  168 + var intervals = [];
  169 + for (var i in predefIntervals) {
  170 + var interval = predefIntervals[i];
  171 + if (interval.value >= min && interval.value <= max) {
  172 + intervals.push(interval);
  173 + }
  174 + }
  175 + return intervals;
  176 + }
  177 +
  178 + function matchesExistingInterval(min, max, intervalMs) {
  179 + var intervals = getIntervals(min, max);
  180 + for (var i in intervals) {
  181 + var interval = intervals[i];
  182 + if (intervalMs === interval.value) {
  183 + return true;
  184 + }
  185 + }
  186 + return false;
  187 + }
  188 +
  189 + function boundToPredefinedInterval(min, max, intervalMs) {
  190 + var intervals = getIntervals(min, max);
  191 + var minDelta = MAX_INTERVAL;
  192 + var boundedInterval = intervalMs || min;
  193 + var matchedInterval;
  194 + for (var i in intervals) {
  195 + var interval = intervals[i];
  196 + var delta = Math.abs(interval.value - boundedInterval);
  197 + if (delta < minDelta) {
  198 + matchedInterval = interval;
  199 + minDelta = delta;
  200 + }
  201 + }
  202 + boundedInterval = matchedInterval.value;
  203 + return boundedInterval;
  204 + }
  205 +
  206 + function defaultTimewindow() {
  207 + var currentTime = (new Date).getTime();
  208 + var timewindow = {
  209 + displayValue: "",
  210 + selectedTab: 0,
  211 + realtime: {
  212 + interval: SECOND,
  213 + timewindowMs: MINUTE // 1 min by default
  214 + },
  215 + history: {
  216 + historyType: 0,
  217 + interval: SECOND,
  218 + timewindowMs: MINUTE, // 1 min by default
  219 + fixedTimewindow: {
  220 + startTimeMs: currentTime - DAY, // 1 day by default
  221 + endTimeMs: currentTime
  222 + }
  223 + },
  224 + aggregation: {
  225 + type: types.aggregation.avg.value,
  226 + limit: AVG_LIMIT
  227 + }
  228 + }
  229 + return timewindow;
  230 + }
  231 +
  232 + function toHistoryTimewindow(timewindow, startTimeMs, endTimeMs) {
  233 +
  234 + var interval = 0;
  235 + if (timewindow.history) {
  236 + interval = timewindow.history.interval;
  237 + } else if (timewindow.realtime) {
  238 + interval = timewindow.realtime.interval;
  239 + }
  240 +
  241 + var historyTimewindow = {
  242 + history: {
  243 + fixedTimewindow: {
  244 + startTimeMs: startTimeMs,
  245 + endTimeMs: endTimeMs
  246 + },
  247 + interval: boundIntervalToTimewindow(endTimeMs - startTimeMs, interval)
  248 + },
  249 + aggregation: {
  250 +
  251 + }
  252 + }
  253 + if (timewindow.aggregation) {
  254 + historyTimewindow.aggregation.type = timewindow.aggregation.type || types.aggregation.avg.value;
  255 + } else {
  256 + historyTimewindow.aggregation.type = types.aggregation.avg.value;
  257 + }
  258 +
  259 + return historyTimewindow;
  260 + }
  261 +
  262 + function createSubscriptionTimewindow(timewindow, stDiff) {
  263 +
  264 + var subscriptionTimewindow = {
  265 + fixedWindow: null,
  266 + realtimeWindowMs: null,
  267 + aggregation: {
  268 + interval: SECOND,
  269 + limit: AVG_LIMIT,
  270 + type: types.aggregation.avg.value
  271 + }
  272 + };
  273 + var aggTimewindow = 0;
  274 +
  275 + if (angular.isDefined(timewindow.aggregation)) {
  276 + subscriptionTimewindow.aggregation = {
  277 + type: timewindow.aggregation.type || types.aggregation.avg.value,
  278 + limit: timewindow.aggregation.limit || AVG_LIMIT
  279 + };
  280 + }
  281 + if (angular.isDefined(timewindow.realtime)) {
  282 + subscriptionTimewindow.realtimeWindowMs = timewindow.realtime.timewindowMs;
  283 + subscriptionTimewindow.aggregation.interval =
  284 + boundIntervalToTimewindow(subscriptionTimewindow.realtimeWindowMs, timewindow.realtime.interval);
  285 + subscriptionTimewindow.startTs = (new Date).getTime() + stDiff - subscriptionTimewindow.realtimeWindowMs;
  286 + var startDiff = subscriptionTimewindow.startTs % subscriptionTimewindow.aggregation.interval;
  287 + aggTimewindow = subscriptionTimewindow.realtimeWindowMs;
  288 + if (startDiff) {
  289 + subscriptionTimewindow.startTs -= startDiff;
  290 + aggTimewindow += subscriptionTimewindow.aggregation.interval;
  291 + }
  292 + } else if (angular.isDefined(timewindow.history)) {
  293 + if (angular.isDefined(timewindow.history.timewindowMs)) {
  294 + var currentTime = (new Date).getTime();
  295 + subscriptionTimewindow.fixedWindow = {
  296 + startTimeMs: currentTime - timewindow.history.timewindowMs,
  297 + endTimeMs: currentTime
  298 + }
  299 + aggTimewindow = timewindow.history.timewindowMs;
  300 +
  301 + } else {
  302 + subscriptionTimewindow.fixedWindow = {
  303 + startTimeMs: timewindow.history.fixedTimewindow.startTimeMs,
  304 + endTimeMs: timewindow.history.fixedTimewindow.endTimeMs
  305 + }
  306 + aggTimewindow = subscriptionTimewindow.fixedWindow.endTimeMs - subscriptionTimewindow.fixedWindow.startTimeMs;
  307 + }
  308 + subscriptionTimewindow.startTs = subscriptionTimewindow.fixedWindow.startTimeMs;
  309 + subscriptionTimewindow.aggregation.interval = boundIntervalToTimewindow(aggTimewindow, timewindow.history.interval);
  310 + }
  311 + var aggregation = subscriptionTimewindow.aggregation;
  312 + aggregation.timeWindow = aggTimewindow;
  313 + if (aggregation.type !== types.aggregation.none.value) {
  314 + aggregation.limit = Math.ceil(aggTimewindow / subscriptionTimewindow.aggregation.interval);
  315 + }
  316 + return subscriptionTimewindow;
  317 + }
  318 +
  319 + function boundIntervalToTimewindow(timewindow, intervalMs) {
  320 + var min = minIntervalLimit(timewindow);
  321 + var max = maxIntervalLimit(timewindow);
  322 + if (intervalMs) {
  323 + return toBound(intervalMs, min, max, intervalMs);
  324 + } else {
  325 + return boundToPredefinedInterval(min, max, avgInterval(timewindow));
  326 + }
  327 + }
  328 +
  329 +
  330 +}
\ No newline at end of file
... ...
... ... @@ -129,7 +129,7 @@ function WidgetService($rootScope, $http, $q, $filter, $ocLazyLoad, $window, typ
129 129 resources: [],
130 130 templateHtml: '<div class="tb-widget-error-container"><div translate class="tb-widget-error-msg">widget.widget-type-not-found</div></div>',
131 131 templateCss: '',
132   - controllerScript: 'fns.init = function(containerElement, settings, datasources,\n data) {}\n\n\nfns.redraw = function(containerElement, width, height, data) {};',
  132 + controllerScript: 'self.onInit = function() {}',
133 133 settingsSchema: '{}\n',
134 134 dataKeySettingsSchema: '{}\n',
135 135 defaultConfig: '{\n' +
... ... @@ -147,7 +147,7 @@ function WidgetService($rootScope, $http, $q, $filter, $ocLazyLoad, $window, typ
147 147 resources: [],
148 148 templateHtml: '<div class="tb-widget-error-container"><div translate class="tb-widget-error-msg">widget.widget-type-load-error</div>',
149 149 templateCss: '',
150   - controllerScript: 'fns.init = function(containerElement, settings, datasources,\n data) {}\n\n\nfns.redraw = function(containerElement, width, height, data) {};',
  150 + controllerScript: 'self.onInit = function() {}',
151 151 settingsSchema: '{}\n',
152 152 dataKeySettingsSchema: '{}\n',
153 153 defaultConfig: '{\n' +
... ...
... ... @@ -51,6 +51,7 @@ import thingsboardMenu from './services/menu.service';
51 51 import thingsboardRaf from './common/raf.provider';
52 52 import thingsboardUtils from './common/utils.service';
53 53 import thingsboardTypes from './common/types.constant';
  54 +import thingsboardApiTime from './api/time.service';
54 55 import thingsboardKeyboardShortcut from './components/keyboard-shortcut.filter';
55 56 import thingsboardHelp from './help/help.directive';
56 57 import thingsboardToast from './services/toast';
... ... @@ -101,6 +102,7 @@ angular.module('thingsboard', [
101 102 thingsboardRaf,
102 103 thingsboardUtils,
103 104 thingsboardTypes,
  105 + thingsboardApiTime,
104 106 thingsboardKeyboardShortcut,
105 107 thingsboardHelp,
106 108 thingsboardToast,
... ...
... ... @@ -26,7 +26,7 @@ export default angular.module('thingsboard.directives.timeinterval', [])
26 26 .name;
27 27
28 28 /*@ngInject*/
29   -function Timeinterval($compile, $templateCache, $translate) {
  29 +function Timeinterval($compile, $templateCache, timeService) {
30 30
31 31 var linker = function (scope, element, attrs, ngModelCtrl) {
32 32
... ... @@ -39,62 +39,33 @@ function Timeinterval($compile, $templateCache, $translate) {
39 39 scope.mins = 1;
40 40 scope.secs = 0;
41 41
42   - scope.predefIntervals = [
43   - {
44   - name: $translate.instant('timeinterval.seconds-interval', {seconds: 10}, 'messageformat'),
45   - value: 10 * 1000
46   - },
47   - {
48   - name: $translate.instant('timeinterval.seconds-interval', {seconds: 30}, 'messageformat'),
49   - value: 30 * 1000
50   - },
51   - {
52   - name: $translate.instant('timeinterval.minutes-interval', {minutes: 1}, 'messageformat'),
53   - value: 60 * 1000
54   - },
55   - {
56   - name: $translate.instant('timeinterval.minutes-interval', {minutes: 2}, 'messageformat'),
57   - value: 2 * 60 * 1000
58   - },
59   - {
60   - name: $translate.instant('timeinterval.minutes-interval', {minutes: 5}, 'messageformat'),
61   - value: 5 * 60 * 1000
62   - },
63   - {
64   - name: $translate.instant('timeinterval.minutes-interval', {minutes: 10}, 'messageformat'),
65   - value: 10 * 60 * 1000
66   - },
67   - {
68   - name: $translate.instant('timeinterval.minutes-interval', {minutes: 30}, 'messageformat'),
69   - value: 30 * 60 * 1000
70   - },
71   - {
72   - name: $translate.instant('timeinterval.hours-interval', {hours: 1}, 'messageformat'),
73   - value: 60 * 60 * 1000
74   - },
75   - {
76   - name: $translate.instant('timeinterval.hours-interval', {hours: 2}, 'messageformat'),
77   - value: 2 * 60 * 60 * 1000
78   - },
79   - {
80   - name: $translate.instant('timeinterval.hours-interval', {hours: 10}, 'messageformat'),
81   - value: 10 * 60 * 60 * 1000
82   - },
83   - {
84   - name: $translate.instant('timeinterval.days-interval', {days: 1}, 'messageformat'),
85   - value: 24 * 60 * 60 * 1000
86   - },
87   - {
88   - name: $translate.instant('timeinterval.days-interval', {days: 7}, 'messageformat'),
89   - value: 7 * 24 * 60 * 60 * 1000
90   - },
91   - {
92   - name: $translate.instant('timeinterval.days-interval', {days: 30}, 'messageformat'),
93   - value: 30 * 24 * 60 * 60 * 1000
94   - }
95   - ];
  42 + scope.advanced = false;
  43 +
  44 + scope.boundInterval = function() {
  45 + var min = timeService.boundMinInterval(scope.min);
  46 + var max = timeService.boundMaxInterval(scope.max);
  47 + scope.intervals = timeService.getIntervals(scope.min, scope.max);
  48 + if (scope.rendered) {
  49 + var newIntervalMs = ngModelCtrl.$viewValue;
  50 + if (newIntervalMs < min) {
  51 + newIntervalMs = min;
  52 + } else if (newIntervalMs > max) {
  53 + newIntervalMs = max;
  54 + }
  55 + if (!scope.advanced) {
  56 + newIntervalMs = timeService.boundToPredefinedInterval(min, max, newIntervalMs);
  57 + }
  58 + if (newIntervalMs !== ngModelCtrl.$viewValue) {
  59 + scope.setIntervalMs(newIntervalMs);
  60 + scope.updateView();
  61 + }
  62 + }
  63 + }
96 64
97 65 scope.setIntervalMs = function (intervalMs) {
  66 + if (!scope.advanced) {
  67 + scope.intervalMs = intervalMs;
  68 + }
98 69 var intervalSeconds = Math.floor(intervalMs / 1000);
99 70 scope.days = Math.floor(intervalSeconds / 86400);
100 71 scope.hours = Math.floor((intervalSeconds % 86400) / 3600);
... ... @@ -105,6 +76,9 @@ function Timeinterval($compile, $templateCache, $translate) {
105 76 ngModelCtrl.$render = function () {
106 77 if (ngModelCtrl.$viewValue) {
107 78 var intervalMs = ngModelCtrl.$viewValue;
  79 + if (!scope.rendered) {
  80 + scope.advanced = !timeService.matchesExistingInterval(scope.min, scope.max, intervalMs);
  81 + }
108 82 scope.setIntervalMs(intervalMs);
109 83 }
110 84 scope.rendered = true;
... ... @@ -115,10 +89,15 @@ function Timeinterval($compile, $templateCache, $translate) {
115 89 return;
116 90 }
117 91 var value = null;
118   - var intervalMs = (scope.days * 86400 +
  92 + var intervalMs;
  93 + if (!scope.advanced) {
  94 + intervalMs = scope.intervalMs;
  95 + } else {
  96 + intervalMs = (scope.days * 86400 +
119 97 scope.hours * 3600 +
120 98 scope.mins * 60 +
121 99 scope.secs) * 1000;
  100 + }
122 101 if (!isNaN(intervalMs) && intervalMs > 0) {
123 102 value = intervalMs;
124 103 ngModelCtrl.$setValidity('tb-timeinterval', true);
... ... @@ -126,6 +105,7 @@ function Timeinterval($compile, $templateCache, $translate) {
126 105 ngModelCtrl.$setValidity('tb-timeinterval', !scope.required);
127 106 }
128 107 ngModelCtrl.$setViewValue(value);
  108 + scope.boundInterval();
129 109 }
130 110
131 111 scope.$watch('required', function (newRequired, prevRequired) {
... ... @@ -134,6 +114,38 @@ function Timeinterval($compile, $templateCache, $translate) {
134 114 }
135 115 });
136 116
  117 + scope.$watch('min', function (newMin, prevMin) {
  118 + if (angular.isDefined(newMin) && newMin !== prevMin) {
  119 + scope.updateView();
  120 + }
  121 + });
  122 +
  123 + scope.$watch('max', function (newMax, prevMax) {
  124 + if (angular.isDefined(newMax) && newMax !== prevMax) {
  125 + scope.updateView();
  126 + }
  127 + });
  128 +
  129 + scope.$watch('intervalMs', function (newIntervalMs, prevIntervalMs) {
  130 + if (angular.isDefined(newIntervalMs) && newIntervalMs !== prevIntervalMs) {
  131 + scope.updateView();
  132 + }
  133 + });
  134 +
  135 + scope.$watch('advanced', function (newAdvanced, prevAdvanced) {
  136 + if (angular.isDefined(newAdvanced) && newAdvanced !== prevAdvanced) {
  137 + if (!scope.advanced) {
  138 + scope.intervalMs = (scope.days * 86400 +
  139 + scope.hours * 3600 +
  140 + scope.mins * 60 +
  141 + scope.secs) * 1000;
  142 + } else {
  143 + scope.setIntervalMs(scope.intervalMs);
  144 + }
  145 + scope.updateView();
  146 + }
  147 + });
  148 +
137 149 scope.$watch('secs', function (newSecs) {
138 150 if (angular.isUndefined(newSecs)) {
139 151 return;
... ... @@ -198,6 +210,8 @@ function Timeinterval($compile, $templateCache, $translate) {
198 210 scope.updateView();
199 211 });
200 212
  213 + scope.boundInterval();
  214 +
201 215 $compile(element.contents())(scope);
202 216
203 217 }
... ... @@ -206,7 +220,10 @@ function Timeinterval($compile, $templateCache, $translate) {
206 220 restrict: "E",
207 221 require: "^ngModel",
208 222 scope: {
209   - required: '=ngRequired'
  223 + required: '=ngRequired',
  224 + min: '=?',
  225 + max: '=?',
  226 + predefinedName: '=?'
210 227 },
211 228 link: linker
212 229 };
... ...
... ... @@ -14,6 +14,7 @@
14 14 * limitations under the License.
15 15 */
16 16 tb-timeinterval {
  17 + min-width: 355px;
17 18 md-input-container {
18 19 margin-bottom: 0px;
19 20 .md-errors-spacer {
... ... @@ -25,10 +26,13 @@ tb-timeinterval {
25 26 width: 150px;
26 27 }
27 28 }
28   -}
29   -
30   -tb-timeinterval {
31 29 .md-input {
32 30 width: 70px !important;
33 31 }
  32 + .advanced-switch {
  33 + margin-top: 0;
  34 + }
  35 + .advanced-label {
  36 + margin: 5px 0;
  37 + }
34 38 }
... ...
... ... @@ -15,33 +15,41 @@
15 15 limitations under the License.
16 16
17 17 -->
18   -<section layout="row" layout-align="start start">
19   - <md-input-container>
20   - <label translate>timeinterval.days</label>
21   - <input type="number" ng-model="days" step="1" aria-label="{{ 'timeinterval.days' | translate }}">
22   - </md-input-container>
23   - <md-input-container>
24   - <label translate>timeinterval.hours</label>
25   - <input type="number" ng-model="hours" step="1" aria-label="{{ 'timeinterval.hours' | translate }}">
26   - </md-input-container>
27   - <md-input-container>
28   - <label translate>timeinterval.minutes</label>
29   - <input type="number" ng-model="mins" step="1" aria-label="{{ 'timeinterval.minutes' | translate }}">
30   - </md-input-container>
31   - <md-input-container>
32   - <label translate>timeinterval.seconds</label>
33   - <input type="number" ng-model="secs" step="1" aria-label="{{ 'timeinterval.seconds' | translate }}">
34   - </md-input-container>
35   - <md-menu md-position-mode="target-right target">
36   - <md-button class="md-icon-button" aria-label="Open intervals" ng-click="$mdOpenMenu($event)">
37   - <md-icon md-menu-origin aria-label="arrow_drop_down" class="material-icons">arrow_drop_down</md-icon>
38   - </md-button>
39   - <md-menu-content width="4">
40   - <md-menu-item ng-repeat="interval in predefIntervals" >
41   - <md-button ng-click="setIntervalMs(interval.value)">
42   - <span>{{interval.name}}</span>
43   - </md-button>
44   - </md-menu-item>
45   - </md-menu-content>
46   - </md-menu>
  18 +<section layout="row">
  19 + <section layout="column" flex ng-show="advanced">
  20 + <label class="tb-small" translate>{{ predefinedName }}</label>
  21 + <section layout="row" layout-align="start start" flex>
  22 + <md-input-container>
  23 + <label translate>timeinterval.days</label>
  24 + <input type="number" ng-model="days" step="1" aria-label="{{ 'timeinterval.days' | translate }}">
  25 + </md-input-container>
  26 + <md-input-container>
  27 + <label translate>timeinterval.hours</label>
  28 + <input type="number" ng-model="hours" step="1" aria-label="{{ 'timeinterval.hours' | translate }}">
  29 + </md-input-container>
  30 + <md-input-container>
  31 + <label translate>timeinterval.minutes</label>
  32 + <input type="number" ng-model="mins" step="1" aria-label="{{ 'timeinterval.minutes' | translate }}">
  33 + </md-input-container>
  34 + <md-input-container>
  35 + <label translate>timeinterval.seconds</label>
  36 + <input type="number" ng-model="secs" step="1" aria-label="{{ 'timeinterval.seconds' | translate }}">
  37 + </md-input-container>
  38 + </section>
  39 + </section>
  40 + <section layout="row" flex ng-show="!advanced">
  41 + <md-input-container flex>
  42 + <label translate>{{ predefinedName }}</label>
  43 + <md-select ng-model="intervalMs" style="min-width: 150px;" aria-label="predefined-interval">
  44 + <md-option ng-repeat="interval in intervals" ng-value="interval.value">
  45 + {{interval.name}}
  46 + </md-option>
  47 + </md-select>
  48 + </md-input-container>
  49 + </section>
  50 + <section layout="column" layout-align="center center">
  51 + <label class="tb-small advanced-label" translate>timeinterval.advanced</label>
  52 + <md-switch class="advanced-switch" ng-model="advanced" aria-label="predefined-switcher">
  53 + </md-switch>
  54 + </section>
47 55 </section>
... ...
... ... @@ -14,7 +14,7 @@
14 14 * limitations under the License.
15 15 */
16 16 /*@ngInject*/
17   -export default function TimewindowPanelController(mdPanelRef, $scope, types, timewindow, historyOnly, aggregation, onTimewindowUpdate) {
  17 +export default function TimewindowPanelController(mdPanelRef, $scope, timeService, types, timewindow, historyOnly, aggregation, onTimewindowUpdate) {
18 18
19 19 var vm = this;
20 20
... ... @@ -24,6 +24,13 @@ export default function TimewindowPanelController(mdPanelRef, $scope, types, tim
24 24 vm.aggregation = aggregation;
25 25 vm.onTimewindowUpdate = onTimewindowUpdate;
26 26 vm.aggregationTypes = types.aggregation;
  27 + vm.showLimit = showLimit;
  28 + vm.showRealtimeAggInterval = showRealtimeAggInterval;
  29 + vm.showHistoryAggInterval = showHistoryAggInterval;
  30 + vm.minRealtimeAggInterval = minRealtimeAggInterval;
  31 + vm.maxRealtimeAggInterval = maxRealtimeAggInterval;
  32 + vm.minHistoryAggInterval = minHistoryAggInterval;
  33 + vm.maxHistoryAggInterval = maxHistoryAggInterval;
27 34
28 35 if (vm.historyOnly) {
29 36 vm.timewindow.selectedTab = 1;
... ... @@ -48,4 +55,45 @@ export default function TimewindowPanelController(mdPanelRef, $scope, types, tim
48 55 vm.onTimewindowUpdate && vm.onTimewindowUpdate(vm.timewindow);
49 56 });
50 57 };
  58 +
  59 + function showLimit() {
  60 + return vm.timewindow.aggregation.type === vm.aggregationTypes.none.value;
  61 + }
  62 +
  63 + function showRealtimeAggInterval() {
  64 + return vm.timewindow.aggregation.type !== vm.aggregationTypes.none.value &&
  65 + vm.timewindow.selectedTab === 0;
  66 + }
  67 +
  68 + function showHistoryAggInterval() {
  69 + return vm.timewindow.aggregation.type !== vm.aggregationTypes.none.value &&
  70 + vm.timewindow.selectedTab === 1;
  71 + }
  72 +
  73 + function minRealtimeAggInterval () {
  74 + return timeService.minIntervalLimit(vm.timewindow.realtime.timewindowMs);
  75 + }
  76 +
  77 + function maxRealtimeAggInterval () {
  78 + return timeService.maxIntervalLimit(vm.timewindow.realtime.timewindowMs);
  79 + }
  80 +
  81 + function minHistoryAggInterval () {
  82 + return timeService.minIntervalLimit(currentHistoryTimewindow());
  83 + }
  84 +
  85 + function maxHistoryAggInterval () {
  86 + return timeService.maxIntervalLimit(currentHistoryTimewindow());
  87 + }
  88 +
  89 + function currentHistoryTimewindow() {
  90 + if (vm.timewindow.history.historyType === 0) {
  91 + return vm.timewindow.history.timewindowMs;
  92 + } else {
  93 + return vm.timewindow.history.fixedTimewindow.endTimeMs -
  94 + vm.timewindow.history.fixedTimewindow.startTimeMs;
  95 + }
  96 + }
  97 +
51 98 }
  99 +
... ...
... ... @@ -17,61 +17,70 @@
17 17 -->
18 18 <form name="theForm" ng-submit="vm.update()">
19 19 <fieldset ng-disabled="loading">
20   - <md-content layout="column">
21   - <md-tabs ng-class="{'tb-headless': vm.historyOnly}" flex md-dynamic-height md-selected="vm.timewindow.selectedTab" md-border-bottom>
22   - <md-tab label="{{ 'timewindow.realtime' | translate }}">
23   - <md-content class="md-padding" layout="column">
24   - <span translate>timewindow.last</span>
25   - <tb-timeinterval
26   - ng-required="vm.timewindow.selectedTab === 0"
27   - ng-model="vm.timewindow.realtime.timewindowMs" style="padding-top: 8px;"></tb-timeinterval>
28   - </md-content>
29   - </md-tab>
30   - <md-tab label="{{ 'timewindow.history' | translate }}">
31   - <md-content class="md-padding" layout="column">
32   - <md-radio-group ng-model="vm.timewindow.history.historyType" class="md-primary">
33   - <md-radio-button ng-value=0 class="md-primary md-align-top-left md-radio-interactive">
34   - <section layout="column">
35   - <span translate>timewindow.last</span>
36   - <tb-timeinterval
37   - ng-required="vm.timewindow.selectedTab === 1 && vm.timewindow.history.historyType === 0"
38   - ng-show="vm.timewindow.history.historyType === 0"
39   - ng-model="vm.timewindow.history.timewindowMs" style="padding-top: 8px;"></tb-timeinterval>
40   - </section>
41   - </md-radio-button>
42   - <md-radio-button ng-value=1 class="md-primary md-align-top-left md-radio-interactive">
43   - <section layout="column">
44   - <span translate>timewindow.time-period</span>
45   - <tb-datetime-period
46   - ng-required="vm.timewindow.selectedTab === 1 && vm.timewindow.history.historyType === 1"
47   - ng-show="vm.timewindow.history.historyType === 1"
48   - ng-model="vm.timewindow.history.fixedTimewindow" style="padding-top: 8px;"></tb-datetime-period>
49   - </section>
50   - </md-radio-button>
51   - </md-radio-group>
52   - </md-content>
53   - </md-tab>
54   - </md-tabs>
55   - <md-content ng-if="vm.aggregation" class="md-padding" layout="column">
56   - <md-input-container>
57   - <label translate>aggregation.function</label>
58   - <md-select ng-model="vm.timewindow.aggregation.type" style="min-width: 150px;">
59   - <md-option ng-repeat="type in vm.aggregationTypes" ng-value="type.value">
60   - {{type.name | translate}}
61   - </md-option>
62   - </md-select>
63   - </md-input-container>
64   - <md-slider-container>
65   - <span translate>aggregation.limit</span>
66   - <md-slider flex min="10" max="500" ng-model="vm.timewindow.aggregation.limit" aria-label="limit" id="limit-slider">
67   - </md-slider>
  20 + <md-content style="height: 100%" flex layout="column">
  21 + <section layout="column">
  22 + <md-tabs ng-class="{'tb-headless': vm.historyOnly}" flex md-dynamic-height md-selected="vm.timewindow.selectedTab" md-border-bottom>
  23 + <md-tab label="{{ 'timewindow.realtime' | translate }}">
  24 + <md-content class="md-padding" layout="column">
  25 + <tb-timeinterval predefined-name="'timewindow.last'"
  26 + ng-required="vm.timewindow.selectedTab === 0"
  27 + ng-model="vm.timewindow.realtime.timewindowMs" style="padding-top: 8px;"></tb-timeinterval>
  28 + </md-content>
  29 + </md-tab>
  30 + <md-tab label="{{ 'timewindow.history' | translate }}">
  31 + <md-content class="md-padding" layout="column" style="padding-top: 8px;">
  32 + <md-radio-group ng-model="vm.timewindow.history.historyType" class="md-primary">
  33 + <md-radio-button ng-value=0 aria-label="{{ 'timewindow.last' | translate }}" class="md-primary md-align-top-left md-radio-interactive">
  34 + <section layout="column">
  35 + <tb-timeinterval predefined-name="'timewindow.last'"
  36 + ng-required="vm.timewindow.selectedTab === 1 && vm.timewindow.history.historyType === 0"
  37 + ng-show="vm.timewindow.history.historyType === 0"
  38 + ng-model="vm.timewindow.history.timewindowMs" style="padding-top: 8px;"></tb-timeinterval>
  39 + </section>
  40 + </md-radio-button>
  41 + <md-radio-button ng-value=1 aria-label="{{ 'timewindow.time-period' | translate }}" class="md-primary md-align-top-left md-radio-interactive">
  42 + <section layout="column">
  43 + <span translate>timewindow.time-period</span>
  44 + <tb-datetime-period
  45 + ng-required="vm.timewindow.selectedTab === 1 && vm.timewindow.history.historyType === 1"
  46 + ng-show="vm.timewindow.history.historyType === 1"
  47 + ng-model="vm.timewindow.history.fixedTimewindow" style="padding-top: 8px;"></tb-datetime-period>
  48 + </section>
  49 + </md-radio-button>
  50 + </md-radio-group>
  51 + </md-content>
  52 + </md-tab>
  53 + </md-tabs>
  54 + <md-content ng-if="vm.aggregation" class="md-padding" layout="column">
68 55 <md-input-container>
69   - <input flex type="number" ng-model="vm.timewindow.aggregation.limit" aria-label="limit" aria-controls="limit-slider">
  56 + <label translate>aggregation.function</label>
  57 + <md-select ng-model="vm.timewindow.aggregation.type" style="min-width: 150px;">
  58 + <md-option ng-repeat="type in vm.aggregationTypes" ng-value="type.value">
  59 + {{type.name | translate}}
  60 + </md-option>
  61 + </md-select>
70 62 </md-input-container>
71   - </md-slider-container>
72   - </md-content>
  63 + <md-slider-container ng-show="vm.showLimit()">
  64 + <span translate>aggregation.limit</span>
  65 + <md-slider flex min="10" max="500" ng-model="vm.timewindow.aggregation.limit" aria-label="limit" id="limit-slider">
  66 + </md-slider>
  67 + <md-input-container>
  68 + <input flex type="number" ng-model="vm.timewindow.aggregation.limit" aria-label="limit" aria-controls="limit-slider">
  69 + </md-input-container>
  70 + </md-slider-container>
  71 + <tb-timeinterval ng-show="vm.showRealtimeAggInterval()" min="vm.minRealtimeAggInterval()" max="vm.maxRealtimeAggInterval()"
  72 + predefined-name="'aggregation.group-interval'"
  73 + ng-model="vm.timewindow.realtime.interval">
  74 + </tb-timeinterval>
  75 + <tb-timeinterval ng-show="vm.showHistoryAggInterval()" min="vm.minHistoryAggInterval()" max="vm.maxHistoryAggInterval()"
  76 + predefined-name="'aggregation.group-interval'"
  77 + ng-model="vm.timewindow.history.interval">
  78 + </tb-timeinterval>
  79 + </md-content>
  80 + </section>
  81 + <span flex></span>
73 82 <section layout="row" layout-alignment="start center">
74   - <span flex></span>
  83 + <span flex></span>
75 84 <md-button ng-disabled="loading || theForm.$invalid || !theForm.$dirty" type="submit" class="md-raised md-primary">
76 85 {{ 'action.update' | translate }}
77 86 </md-button>
... ... @@ -79,6 +88,6 @@
79 88 {{ 'action.cancel' | translate }}
80 89 </md-button>
81 90 </section>
82   - </section>
  91 + </md-content>
83 92 </fieldset>
84 93 </form>
\ No newline at end of file
... ...
... ... @@ -37,16 +37,18 @@ export default angular.module('thingsboard.directives.timewindow', [thingsboardT
37 37
38 38 /* eslint-disable angular/angularelement */
39 39 /*@ngInject*/
40   -function Timewindow($compile, $templateCache, $filter, $mdPanel, $document, $mdMedia, $translate, types) {
  40 +function Timewindow($compile, $templateCache, $filter, $mdPanel, $document, $mdMedia, $translate, timeService) {
41 41
42 42 var linker = function (scope, element, attrs, ngModelCtrl) {
43 43
44 44 /* tbTimewindow (ng-model)
45 45 * {
46 46 * realtime: {
  47 + * interval: 0,
47 48 * timewindowMs: 0
48 49 * },
49 50 * history: {
  51 + * interval: 0,
50 52 * timewindowMs: 0,
51 53 * fixedTimewindow: {
52 54 * startTimeMs: 0,
... ... @@ -54,8 +56,8 @@ function Timewindow($compile, $templateCache, $filter, $mdPanel, $document, $mdM
54 56 * }
55 57 * },
56 58 * aggregation: {
57   - * limit: 200,
58   - * type: types.aggregation.avg.value
  59 + * type: types.aggregation.avg.value,
  60 + * limit: 200
59 61 * }
60 62 * }
61 63 */
... ... @@ -81,16 +83,6 @@ function Timewindow($compile, $templateCache, $filter, $mdPanel, $document, $mdM
81 83 }
82 84 element.html(template);
83 85
84   - scope.isHovered = false;
85   -
86   - scope.onHoverIn = function () {
87   - scope.isHovered = true;
88   - }
89   -
90   - scope.onHoverOut = function () {
91   - scope.isHovered = false;
92   - }
93   -
94 86 scope.openEditMode = function (event) {
95 87 var position;
96 88 var isGtSm = $mdMedia('gt-sm');
... ... @@ -143,15 +135,18 @@ function Timewindow($compile, $templateCache, $filter, $mdPanel, $document, $mdM
143 135 var model = scope.model;
144 136 if (model.selectedTab === 0) {
145 137 value.realtime = {
  138 + interval: model.realtime.interval,
146 139 timewindowMs: model.realtime.timewindowMs
147 140 };
148 141 } else {
149 142 if (model.history.historyType === 0) {
150 143 value.history = {
  144 + interval: model.history.interval,
151 145 timewindowMs: model.history.timewindowMs
152 146 };
153 147 } else {
154 148 value.history = {
  149 + interval: model.history.interval,
155 150 fixedTimewindow: {
156 151 startTimeMs: model.history.fixedTimewindow.startTimeMs,
157 152 endTimeMs: model.history.fixedTimewindow.endTimeMs
... ... @@ -160,8 +155,8 @@ function Timewindow($compile, $templateCache, $filter, $mdPanel, $document, $mdM
160 155 }
161 156 }
162 157 value.aggregation = {
163   - limit: model.aggregation.limit,
164   - type: model.aggregation.type
  158 + type: model.aggregation.type,
  159 + limit: model.aggregation.limit
165 160 };
166 161 ngModelCtrl.$setViewValue(value);
167 162 scope.updateDisplayValue();
... ... @@ -190,34 +185,17 @@ function Timewindow($compile, $templateCache, $filter, $mdPanel, $document, $mdM
190 185 }
191 186
192 187 ngModelCtrl.$render = function () {
193   - var currentTime = (new Date).getTime();
194   - scope.model = {
195   - displayValue: "",
196   - selectedTab: 0,
197   - realtime: {
198   - timewindowMs: 60000 // 1 min by default
199   - },
200   - history: {
201   - historyType: 0,
202   - timewindowMs: 60000, // 1 min by default
203   - fixedTimewindow: {
204   - startTimeMs: currentTime - 24 * 60 * 60 * 1000, // 1 day by default
205   - endTimeMs: currentTime
206   - }
207   - },
208   - aggregation: {
209   - limit: 200,
210   - type: types.aggregation.avg.value
211   - }
212   - };
  188 + scope.model = timeService.defaultTimewindow();
213 189 if (ngModelCtrl.$viewValue) {
214 190 var value = ngModelCtrl.$viewValue;
215 191 var model = scope.model;
216 192 if (angular.isDefined(value.realtime)) {
217 193 model.selectedTab = 0;
  194 + model.realtime.interval = value.realtime.interval;
218 195 model.realtime.timewindowMs = value.realtime.timewindowMs;
219 196 } else {
220 197 model.selectedTab = 1;
  198 + model.history.interval = value.history.interval;
221 199 if (angular.isDefined(value.history.timewindowMs)) {
222 200 model.history.historyType = 0;
223 201 model.history.timewindowMs = value.history.timewindowMs;
... ... @@ -228,10 +206,10 @@ function Timewindow($compile, $templateCache, $filter, $mdPanel, $document, $mdM
228 206 }
229 207 }
230 208 if (angular.isDefined(value.aggregation)) {
231   - model.aggregation.limit = value.aggregation.limit || 200;
232 209 if (angular.isDefined(value.aggregation.type) && value.aggregation.type.length > 0) {
233 210 model.aggregation.type = value.aggregation.type;
234 211 }
  212 + model.aggregation.limit = value.aggregation.limit || timeService.avgAggregationLimit();
235 213 }
236 214 }
237 215 scope.updateDisplayValue();
... ...
... ... @@ -21,14 +21,39 @@
21 21 }
22 22
23 23 .tb-timewindow-panel {
24   - min-height: 375px;
  24 + max-height: 440px;
  25 + min-width: 417px;
25 26 background: white;
26 27 border-radius: 4px;
27 28 box-shadow: 0 7px 8px -4px rgba(0, 0, 0, 0.2),
28 29 0 13px 19px 2px rgba(0, 0, 0, 0.14),
29 30 0 5px 24px 4px rgba(0, 0, 0, 0.12);
30 31 overflow: hidden;
  32 + form, fieldset {
  33 + height: 100%;
  34 + }
31 35 md-content {
32 36 background-color: #fff;
  37 + overflow: hidden;
  38 + }
  39 + .md-padding {
  40 + padding: 0 16px;
  41 + }
  42 + .md-radio-interactive {
  43 + md-select, md-switch {
  44 + pointer-events: all;
  45 + }
  46 + }
  47 + md-radio-button {
  48 + .md-label {
  49 + width: 100%;
  50 + }
  51 + tb-timeinterval {
  52 + width: 355px;
  53 + .advanced-switch {
  54 + min-height: 30px;
  55 + max-width: 44px;
  56 + }
  57 + }
33 58 }
34 59 }
... ...
... ... @@ -15,9 +15,9 @@
15 15 limitations under the License.
16 16
17 17 -->
18   -<section ng-mouseover="onHoverIn()" ng-mouseleave="onHoverOut()" layout='row' layout-align="start center" style="min-height: 32px;">
  18 +<section layout='row' layout-align="start center" style="min-height: 32px;">
19 19 <span ng-click="openEditMode($event)">{{model.displayValue}}</span>
20   - <md-button class="md-icon-button tb-md-32" aria-label="{{ 'timewindow.edit' | translate }}" ng-show="isHovered" ng-click="openEditMode($event)">
  20 + <md-button class="md-icon-button tb-md-32" aria-label="{{ 'timewindow.edit' | translate }}" ng-click="openEditMode($event)">
21 21 <md-icon ng-style="{ color: buttonColor }" aria-label="{{ 'timewindow.date-range' | translate }}" class="material-icons">date_range</md-icon>
22 22 </md-button>
23 23 </section>
\ No newline at end of file
... ...
... ... @@ -19,7 +19,7 @@ import 'javascript-detect-element-resize/detect-element-resize';
19 19 /* eslint-disable angular/angularelement */
20 20
21 21 /*@ngInject*/
22   -export default function WidgetController($scope, $timeout, $window, $element, $q, $log, $injector, tbRaf, types, utils,
  22 +export default function WidgetController($scope, $timeout, $window, $element, $q, $log, $injector, tbRaf, types, utils, timeService,
23 23 datasourceService, deviceService, visibleRect, isEdit, stDiff, widget, deviceAliasList, widgetType) {
24 24
25 25 var vm = this;
... ... @@ -41,11 +41,7 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
41 41 var targetDeviceAliasId = null;
42 42 var targetDeviceId = null;
43 43 var originalTimewindow = null;
44   - var subscriptionTimewindow = {
45   - fixedWindow: null,
46   - realtimeWindowMs: null,
47   - aggregation: null
48   - };
  44 + var subscriptionTimewindow = null;
49 45 var dataUpdateCaf = null;
50 46
51 47 /*
... ... @@ -488,15 +484,7 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
488 484 if (!originalTimewindow) {
489 485 originalTimewindow = angular.copy(widget.config.timewindow);
490 486 }
491   - widget.config.timewindow = {
492   - history: {
493   - fixedTimewindow: {
494   - startTimeMs: startTimeMs,
495   - endTimeMs: endTimeMs
496   - }
497   - },
498   - aggregation: angular.copy(widget.config.timewindow.aggregation)
499   - };
  487 + widget.config.timewindow = timeService.toHistoryTimewindow(widget.config.timewindow, startTimeMs, endTimeMs);
500 488 }
501 489
502 490 function dataUpdated(sourceData, datasourceIndex, dataKeyIndex) {
... ... @@ -511,7 +499,7 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
511 499 }
512 500 }
513 501 if (update) {
514   - if (subscriptionTimewindow.realtimeWindowMs) {
  502 + if (subscriptionTimewindow && subscriptionTimewindow.realtimeWindowMs) {
515 503 updateTimewindow();
516 504 }
517 505 widgetContext.data[datasourceIndex + dataKeyIndex].data = sourceData.data;
... ... @@ -555,62 +543,26 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
555 543 }
556 544 }
557 545
  546 + function updateRealtimeSubscription(_subscriptionTimewindow) {
  547 + if (_subscriptionTimewindow) {
  548 + subscriptionTimewindow = _subscriptionTimewindow;
  549 + } else {
  550 + subscriptionTimewindow = timeService.createSubscriptionTimewindow(widget.config.timewindow, widgetContext.timeWindow.stDiff);
  551 + }
  552 + updateTimewindow();
  553 + return subscriptionTimewindow;
  554 + }
  555 +
558 556 function subscribe() {
559 557 if (widget.type !== types.widgetType.rpc.value) {
560   - var index = 0;
561   - subscriptionTimewindow.fixedWindow = null;
562   - subscriptionTimewindow.realtimeWindowMs = null;
563   - subscriptionTimewindow.aggregation = {
564   - limit: 200,
565   - type: types.aggregation.avg.value
566   - };
567 558 if (widget.type === types.widgetType.timeseries.value &&
568 559 angular.isDefined(widget.config.timewindow)) {
569   - var timeWindow = 0;
570   - if (angular.isDefined(widget.config.timewindow.aggregation)) {
571   - subscriptionTimewindow.aggregation = {
572   - limit: widget.config.timewindow.aggregation.limit || 200,
573   - type: widget.config.timewindow.aggregation.type || types.aggregation.avg.value
574   - };
575   - }
576   -
577   - if (angular.isDefined(widget.config.timewindow.realtime)) {
578   - subscriptionTimewindow.realtimeWindowMs = widget.config.timewindow.realtime.timewindowMs;
579   - subscriptionTimewindow.startTs = (new Date).getTime() + widgetContext.timeWindow.stDiff - subscriptionTimewindow.realtimeWindowMs;
580   - timeWindow = subscriptionTimewindow.realtimeWindowMs;
581   - } else if (angular.isDefined(widget.config.timewindow.history)) {
582   - if (angular.isDefined(widget.config.timewindow.history.timewindowMs)) {
583   - var currentTime = (new Date).getTime();
584   - subscriptionTimewindow.fixedWindow = {
585   - startTimeMs: currentTime - widget.config.timewindow.history.timewindowMs,
586   - endTimeMs: currentTime
587   - }
588   - timeWindow = widget.config.timewindow.history.timewindowMs;
589   - } else {
590   - subscriptionTimewindow.fixedWindow = {
591   - startTimeMs: widget.config.timewindow.history.fixedTimewindow.startTimeMs,
592   - endTimeMs: widget.config.timewindow.history.fixedTimewindow.endTimeMs
593   - }
594   - timeWindow = subscriptionTimewindow.fixedWindow.endTimeMs - subscriptionTimewindow.fixedWindow.startTimeMs;
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;
608   - }
609   - updateTimewindow();
  560 + updateRealtimeSubscription();
610 561 if (subscriptionTimewindow.fixedWindow) {
611 562 onDataUpdated();
612 563 }
613 564 }
  565 + var index = 0;
614 566 for (var i in widget.config.datasources) {
615 567 var datasource = widget.config.datasources[i];
616 568 var deviceId = null;
... ... @@ -630,6 +582,13 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
630 582 dataUpdated: function (data, datasourceIndex, dataKeyIndex) {
631 583 dataUpdated(data, datasourceIndex, dataKeyIndex);
632 584 },
  585 + updateRealtimeSubscription: function() {
  586 + this.subscriptionTimewindow = updateRealtimeSubscription();
  587 + return this.subscriptionTimewindow;
  588 + },
  589 + setRealtimeSubscription: function(subscriptionTimewindow) {
  590 + updateRealtimeSubscription(angular.copy(subscriptionTimewindow));
  591 + },
633 592 datasourceIndex: index
634 593 };
635 594
... ...
... ... @@ -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, dashboardService, deviceService, widgetService) {
  32 + $document, $translate, $filter, utils, types, dashboardService, deviceService, widgetService) {
33 33
34 34 var linker = function (scope, element, attrs) {
35 35
... ... @@ -303,6 +303,9 @@ export default function AttributeTableDirective($compile, $templateCache, $rootS
303 303 var isSystem = scope.widgetsBundle.tenantId.id === types.id.nullUid;
304 304 widgetService.getBundleWidgetTypes(scope.widgetsBundle.alias, isSystem).then(
305 305 function success(widgetTypes) {
  306 +
  307 + widgetTypes = $filter('orderBy')(widgetTypes, ['-descriptor.type','-createdTime']);
  308 +
306 309 for (var i = 0; i < widgetTypes.length; i++) {
307 310 var widgetType = widgetTypes[i];
308 311 var widgetInfo = widgetService.toWidgetInfo(widgetType);
... ...
... ... @@ -67,6 +67,7 @@ export default angular.module('thingsboard.locale', [])
67 67 "aggregation": "Aggregation",
68 68 "function": "Data aggregation function",
69 69 "limit": "Max values",
  70 + "group-interval": "Grouping interval",
70 71 "min": "Min",
71 72 "max": "Max",
72 73 "avg": "Average",
... ... @@ -558,7 +559,8 @@ export default angular.module('thingsboard.locale', [])
558 559 "days": "Days",
559 560 "hours": "Hours",
560 561 "minutes": "Minutes",
561   - "seconds": "Seconds"
  562 + "seconds": "Seconds",
  563 + "advanced": "Advanced"
562 564 },
563 565 "timewindow": {
564 566 "days": "{ days, select, 1 { day } other {# days } }",
... ...
... ... @@ -167,6 +167,7 @@ export default class TbFlot {
167 167 var settings = ctx.settings;
168 168 ctx.trackDecimals = angular.isDefined(settings.decimals) ? settings.decimals : 1;
169 169 ctx.tooltipIndividual = this.chartType === 'pie' || (angular.isDefined(settings.tooltipIndividual) ? settings.tooltipIndividual : false);
  170 + ctx.tooltipCumulative = angular.isDefined(settings.tooltipCumulative) ? settings.tooltipCumulative : false;
170 171
171 172 var font = {
172 173 color: settings.fontColor || "#545454",
... ... @@ -232,6 +233,21 @@ export default class TbFlot {
232 233 options.yaxis.tickFormatter = function() {
233 234 return '';
234 235 };
  236 + } else if (settings.units && settings.units.length > 0) {
  237 + options.yaxis.tickFormatter = function(value, axis) {
  238 + var factor = axis.tickDecimals ? Math.pow(10, axis.tickDecimals) : 1,
  239 + formatted = "" + Math.round(value * factor) / factor;
  240 + if (axis.tickDecimals != null) {
  241 + var decimal = formatted.indexOf("."),
  242 + precision = decimal === -1 ? 0 : formatted.length - decimal - 1;
  243 +
  244 + if (precision < axis.tickDecimals) {
  245 + formatted = (precision ? formatted : formatted + ".") + ("" + factor).substr(1, axis.tickDecimals - precision);
  246 + }
  247 + }
  248 + formatted += ' ' + tbFlot.ctx.settings.units;
  249 + return formatted;
  250 + };
235 251 }
236 252 options.yaxis.font.color = settings.yaxis.color || options.yaxis.font.color;
237 253 options.yaxis.label = settings.yaxis.title || null;
... ... @@ -323,6 +339,8 @@ export default class TbFlot {
323 339
324 340 this.options = options;
325 341
  342 + this.checkMouseEvents();
  343 +
326 344 if (this.chartType === 'pie' && this.ctx.animatedPie) {
327 345 this.ctx.pieDataAnimationDuration = 250;
328 346 this.ctx.pieData = angular.copy(this.ctx.data);
... ... @@ -337,7 +355,6 @@ export default class TbFlot {
337 355 } else {
338 356 this.ctx.plot = $.plot(this.ctx.$container, this.ctx.data, this.options);
339 357 }
340   - this.checkMouseEvents();
341 358 }
342 359
343 360 update() {
... ... @@ -577,6 +594,11 @@ export default class TbFlot {
577 594 "type": "boolean",
578 595 "default": false
579 596 },
  597 + "tooltipCumulative": {
  598 + "title": "Show cumulative values in stacking mode",
  599 + "type": "boolean",
  600 + "default": false
  601 + },
580 602 "grid": {
581 603 "title": "Grid settings",
582 604 "type": "object",
... ... @@ -710,6 +732,7 @@ export default class TbFlot {
710 732 "decimals",
711 733 "units",
712 734 "tooltipIndividual",
  735 + "tooltipCumulative",
713 736 {
714 737 "key": "grid",
715 738 "items": [
... ... @@ -834,10 +857,28 @@ export default class TbFlot {
834 857 }
835 858
836 859 checkMouseEvents() {
837   - if (this.ctx.isMobile || this.ctx.isEdit) {
838   - this.disableMouseEvents();
839   - } else if (!this.ctx.isEdit) {
840   - this.enableMouseEvents();
  860 + var enabled = !this.ctx.isMobile && !this.ctx.isEdit;
  861 + if (angular.isUndefined(this.mouseEventsEnabled) || this.mouseEventsEnabled != enabled) {
  862 + this.mouseEventsEnabled = enabled;
  863 + if (enabled) {
  864 + this.enableMouseEvents();
  865 + } else {
  866 + this.disableMouseEvents();
  867 + }
  868 + if (this.ctx.plot) {
  869 + this.ctx.plot.destroy();
  870 + if (this.chartType === 'pie' && this.ctx.animatedPie) {
  871 + this.ctx.plot = $.plot(this.ctx.$container, this.ctx.pieData, this.options);
  872 + } else {
  873 + this.ctx.plot = $.plot(this.ctx.$container, this.ctx.data, this.options);
  874 + }
  875 + }
  876 + }
  877 + }
  878 +
  879 + destroy() {
  880 + if (this.ctx.plot) {
  881 + this.ctx.plot.destroy();
841 882 }
842 883 }
843 884
... ... @@ -1030,7 +1071,7 @@ export default class TbFlot {
1030 1071 minTime = pointTime;
1031 1072 }
1032 1073 if (series.stack) {
1033   - if (this.ctx.tooltipIndividual) {
  1074 + if (this.ctx.tooltipIndividual || !this.ctx.tooltipCumulative) {
1034 1075 value = series.data[hoverIndex][1];
1035 1076 } else {
1036 1077 last_value += series.data[hoverIndex][1];
... ...
... ... @@ -201,6 +201,14 @@ md-sidenav {
201 201 color: rgba(0,0,0,0.54);
202 202 }
203 203
  204 +label {
  205 + &.tb-small {
  206 + pointer-events: none;
  207 + color: rgba(0,0,0,0.54);
  208 + font-size: 12px;
  209 + }
  210 +}
  211 +
204 212 /***********************
205 213 * Prompt
206 214 ***********************/
... ...