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,19 +23,21 @@ public class BaseTsKvQuery implements TsKvQuery {
23 private final String key; 23 private final String key;
24 private final long startTs; 24 private final long startTs;
25 private final long endTs; 25 private final long endTs;
  26 + private final long interval;
26 private final int limit; 27 private final int limit;
27 private final Aggregation aggregation; 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 this.key = key; 31 this.key = key;
31 this.startTs = startTs; 32 this.startTs = startTs;
32 this.endTs = endTs; 33 this.endTs = endTs;
  34 + this.interval = interval;
33 this.limit = limit; 35 this.limit = limit;
34 this.aggregation = aggregation; 36 this.aggregation = aggregation;
35 } 37 }
36 38
37 public BaseTsKvQuery(String key, long startTs, long endTs) { 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,6 +25,8 @@ public interface TsKvQuery {
25 25
26 long getEndTs(); 26 long getEndTs();
27 27
  28 + long getInterval();
  29 +
28 int getLimit(); 30 int getLimit();
29 31
30 Aggregation getAggregation(); 32 Aggregation getAggregation();
@@ -112,13 +112,13 @@ public class BaseTimeseriesDao extends AbstractAsyncDao implements TimeseriesDao @@ -112,13 +112,13 @@ public class BaseTimeseriesDao extends AbstractAsyncDao implements TimeseriesDao
112 if (query.getAggregation() == Aggregation.NONE) { 112 if (query.getAggregation() == Aggregation.NONE) {
113 return findAllAsyncWithLimit(entityType, entityId, query); 113 return findAllAsyncWithLimit(entityType, entityId, query);
114 } else { 114 } else {
115 - long step = Math.max((query.getEndTs() - query.getStartTs()) / query.getLimit(), minAggregationStepMs); 115 + long step = Math.max(query.getInterval(), minAggregationStepMs);
116 long stepTs = query.getStartTs(); 116 long stepTs = query.getStartTs();
117 List<ListenableFuture<Optional<TsKvEntry>>> futures = new ArrayList<>(); 117 List<ListenableFuture<Optional<TsKvEntry>>> futures = new ArrayList<>();
118 while (stepTs < query.getEndTs()) { 118 while (stepTs < query.getEndTs()) {
119 long startTs = stepTs; 119 long startTs = stepTs;
120 long endTs = stepTs + step; 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 futures.add(findAndAggregateAsync(entityType, entityId, subQuery, toPartitionTs(startTs), toPartitionTs(endTs))); 122 futures.add(findAndAggregateAsync(entityType, entityId, subQuery, toPartitionTs(startTs), toPartitionTs(endTs)));
123 stepTs = endTs; 123 stepTs = endTs;
124 } 124 }
@@ -272,17 +272,17 @@ VALUES ( now ( ), minTimeuuid ( 0 ), 'maps', 'route_map', @@ -272,17 +272,17 @@ VALUES ( now ( ), minTimeuuid ( 0 ), 'maps', 'route_map',
272 272
273 INSERT INTO "thingsboard"."widget_type" ( "id", "tenant_id", "bundle_alias", "alias", "descriptor", "name" ) 273 INSERT INTO "thingsboard"."widget_type" ( "id", "tenant_id", "bundle_alias", "alias", "descriptor", "name" )
274 VALUES ( now ( ), minTimeuuid ( 0 ), 'charts', 'pie', 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 'Pie - Flot' ); 276 'Pie - Flot' );
277 277
278 INSERT INTO "thingsboard"."widget_type" ( "id", "tenant_id", "bundle_alias", "alias", "descriptor", "name" ) 278 INSERT INTO "thingsboard"."widget_type" ( "id", "tenant_id", "bundle_alias", "alias", "descriptor", "name" )
279 VALUES ( now ( ), minTimeuuid ( 0 ), 'charts', 'timeseries_bars_flot', 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 'Timeseries Bars - Flot' ); 281 'Timeseries Bars - Flot' );
282 282
283 INSERT INTO "thingsboard"."widget_type" ( "id", "tenant_id", "bundle_alias", "alias", "descriptor", "name" ) 283 INSERT INTO "thingsboard"."widget_type" ( "id", "tenant_id", "bundle_alias", "alias", "descriptor", "name" )
284 VALUES ( now ( ), minTimeuuid ( 0 ), 'charts', 'basic_timeseries', 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 'Timeseries - Flot' ); 286 'Timeseries - Flot' );
287 287
288 /** System plugins and rules **/ 288 /** System plugins and rules **/
@@ -115,7 +115,7 @@ public class TimeseriesServiceTest extends AbstractServiceTest { @@ -115,7 +115,7 @@ public class TimeseriesServiceTest extends AbstractServiceTest {
115 entries.add(save(deviceId, 55000, 600)); 115 entries.add(save(deviceId, 55000, 600));
116 116
117 List<TsKvEntry> list = tsService.findAll(DataConstants.DEVICE, deviceId, Collections.singletonList(new BaseTsKvQuery(LONG_KEY, 0, 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 assertEquals(3, list.size()); 119 assertEquals(3, list.size());
120 assertEquals(55000, list.get(0).getTs()); 120 assertEquals(55000, list.get(0).getTs());
121 assertEquals(java.util.Optional.of(600L), list.get(0).getLongValue()); 121 assertEquals(java.util.Optional.of(600L), list.get(0).getLongValue());
@@ -127,7 +127,7 @@ public class TimeseriesServiceTest extends AbstractServiceTest { @@ -127,7 +127,7 @@ public class TimeseriesServiceTest extends AbstractServiceTest {
127 assertEquals(java.util.Optional.of(400L), list.get(2).getLongValue()); 127 assertEquals(java.util.Optional.of(400L), list.get(2).getLongValue());
128 128
129 list = tsService.findAll(DataConstants.DEVICE, deviceId, Collections.singletonList(new BaseTsKvQuery(LONG_KEY, 0, 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 assertEquals(3, list.size()); 131 assertEquals(3, list.size());
132 assertEquals(10000, list.get(0).getTs()); 132 assertEquals(10000, list.get(0).getTs());
133 assertEquals(java.util.Optional.of(150L), list.get(0).getLongValue()); 133 assertEquals(java.util.Optional.of(150L), list.get(0).getLongValue());
@@ -139,7 +139,7 @@ public class TimeseriesServiceTest extends AbstractServiceTest { @@ -139,7 +139,7 @@ public class TimeseriesServiceTest extends AbstractServiceTest {
139 assertEquals(java.util.Optional.of(550L), list.get(2).getLongValue()); 139 assertEquals(java.util.Optional.of(550L), list.get(2).getLongValue());
140 140
141 list = tsService.findAll(DataConstants.DEVICE, deviceId, Collections.singletonList(new BaseTsKvQuery(LONG_KEY, 0, 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 assertEquals(3, list.size()); 144 assertEquals(3, list.size());
145 assertEquals(10000, list.get(0).getTs()); 145 assertEquals(10000, list.get(0).getTs());
@@ -152,7 +152,7 @@ public class TimeseriesServiceTest extends AbstractServiceTest { @@ -152,7 +152,7 @@ public class TimeseriesServiceTest extends AbstractServiceTest {
152 assertEquals(java.util.Optional.of(1100L), list.get(2).getLongValue()); 152 assertEquals(java.util.Optional.of(1100L), list.get(2).getLongValue());
153 153
154 list = tsService.findAll(DataConstants.DEVICE, deviceId, Collections.singletonList(new BaseTsKvQuery(LONG_KEY, 0, 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 assertEquals(3, list.size()); 157 assertEquals(3, list.size());
158 assertEquals(10000, list.get(0).getTs()); 158 assertEquals(10000, list.get(0).getTs());
@@ -165,7 +165,7 @@ public class TimeseriesServiceTest extends AbstractServiceTest { @@ -165,7 +165,7 @@ public class TimeseriesServiceTest extends AbstractServiceTest {
165 assertEquals(java.util.Optional.of(500L), list.get(2).getLongValue()); 165 assertEquals(java.util.Optional.of(500L), list.get(2).getLongValue());
166 166
167 list = tsService.findAll(DataConstants.DEVICE, deviceId, Collections.singletonList(new BaseTsKvQuery(LONG_KEY, 0, 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 assertEquals(3, list.size()); 170 assertEquals(3, list.size());
171 assertEquals(10000, list.get(0).getTs()); 171 assertEquals(10000, list.get(0).getTs());
@@ -178,7 +178,7 @@ public class TimeseriesServiceTest extends AbstractServiceTest { @@ -178,7 +178,7 @@ public class TimeseriesServiceTest extends AbstractServiceTest {
178 assertEquals(java.util.Optional.of(600L), list.get(2).getLongValue()); 178 assertEquals(java.util.Optional.of(600L), list.get(2).getLongValue());
179 179
180 list = tsService.findAll(DataConstants.DEVICE, deviceId, Collections.singletonList(new BaseTsKvQuery(LONG_KEY, 0, 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 assertEquals(3, list.size()); 183 assertEquals(3, list.size());
184 assertEquals(10000, list.get(0).getTs()); 184 assertEquals(10000, list.get(0).getTs());
@@ -32,6 +32,7 @@ public class GetHistoryCmd implements TelemetryPluginCmd { @@ -32,6 +32,7 @@ public class GetHistoryCmd implements TelemetryPluginCmd {
32 private String keys; 32 private String keys;
33 private long startTs; 33 private long startTs;
34 private long endTs; 34 private long endTs;
  35 + private long interval;
35 private int limit; 36 private int limit;
36 private String agg; 37 private String agg;
37 38
@@ -30,6 +30,7 @@ public class TimeseriesSubscriptionCmd extends SubscriptionCmd { @@ -30,6 +30,7 @@ public class TimeseriesSubscriptionCmd extends SubscriptionCmd {
30 30
31 private long startTs; 31 private long startTs;
32 private long timeWindow; 32 private long timeWindow;
  33 + private long interval;
33 private int limit; 34 private int limit;
34 private String agg; 35 private String agg;
35 36
@@ -89,11 +89,12 @@ public class TelemetryRestMsgHandler extends DefaultRestMsgHandler { @@ -89,11 +89,12 @@ public class TelemetryRestMsgHandler extends DefaultRestMsgHandler {
89 String keysStr = request.getParameter("keys"); 89 String keysStr = request.getParameter("keys");
90 Optional<Long> startTs = request.getLongParamValue("startTs"); 90 Optional<Long> startTs = request.getLongParamValue("startTs");
91 Optional<Long> endTs = request.getLongParamValue("endTs"); 91 Optional<Long> endTs = request.getLongParamValue("endTs");
  92 + Optional<Long> interval = request.getLongParamValue("interval");
92 Optional<Integer> limit = request.getIntParamValue("limit"); 93 Optional<Integer> limit = request.getIntParamValue("limit");
93 Aggregation agg = Aggregation.valueOf(request.getParameter("agg", Aggregation.NONE.name())); 94 Aggregation agg = Aggregation.valueOf(request.getParameter("agg", Aggregation.NONE.name()));
94 95
95 List<String> keys = Arrays.asList(keysStr.split(",")); 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 ctx.loadTimeseries(deviceId, queries, new PluginCallback<List<TsKvEntry>>() { 98 ctx.loadTimeseries(deviceId, queries, new PluginCallback<List<TsKvEntry>>() {
98 @Override 99 @Override
99 public void onSuccess(PluginContext ctx, List<TsKvEntry> data) { 100 public void onSuccess(PluginContext ctx, List<TsKvEntry> data) {
@@ -193,7 +193,7 @@ public class TelemetryWebsocketMsgHandler extends DefaultWebsocketMsgHandler { @@ -193,7 +193,7 @@ public class TelemetryWebsocketMsgHandler extends DefaultWebsocketMsgHandler {
193 log.debug("[{}] fetching timeseries data for last {} ms for keys: ({}) for device : {}", sessionId, cmd.getTimeWindow(), cmd.getKeys(), cmd.getDeviceId()); 193 log.debug("[{}] fetching timeseries data for last {} ms for keys: ({}) for device : {}", sessionId, cmd.getTimeWindow(), cmd.getKeys(), cmd.getDeviceId());
194 startTs = cmd.getStartTs(); 194 startTs = cmd.getStartTs();
195 long endTs = cmd.getStartTs() + cmd.getTimeWindow(); 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 ctx.loadTimeseries(deviceId, queries, getSubscriptionCallback(sessionRef, cmd, sessionId, deviceId, startTs, keys)); 197 ctx.loadTimeseries(deviceId, queries, getSubscriptionCallback(sessionRef, cmd, sessionId, deviceId, startTs, keys));
198 } else { 198 } else {
199 List<String> keys = new ArrayList<>(getKeys(cmd).orElse(Collections.emptySet())); 199 List<String> keys = new ArrayList<>(getKeys(cmd).orElse(Collections.emptySet()));
@@ -277,7 +277,7 @@ public class TelemetryWebsocketMsgHandler extends DefaultWebsocketMsgHandler { @@ -277,7 +277,7 @@ public class TelemetryWebsocketMsgHandler extends DefaultWebsocketMsgHandler {
277 } 277 }
278 DeviceId deviceId = DeviceId.fromString(cmd.getDeviceId()); 278 DeviceId deviceId = DeviceId.fromString(cmd.getDeviceId());
279 List<String> keys = new ArrayList<>(getKeys(cmd).orElse(Collections.emptySet())); 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 ctx.loadTimeseries(deviceId, queries, new PluginCallback<List<TsKvEntry>>() { 281 ctx.loadTimeseries(deviceId, queries, new PluginCallback<List<TsKvEntry>>() {
282 @Override 282 @Override
283 public void onSuccess(PluginContext ctx, List<TsKvEntry> data) { 283 public void onSuccess(PluginContext ctx, List<TsKvEntry> data) {
@@ -25,11 +25,12 @@ export default class DataAggregator { @@ -25,11 +25,12 @@ export default class DataAggregator {
25 this.$timeout = $timeout; 25 this.$timeout = $timeout;
26 this.$filter = $filter; 26 this.$filter = $filter;
27 this.dataReceived = false; 27 this.dataReceived = false;
  28 + this.resetPending = false;
28 this.noAggregation = aggregationType === types.aggregation.none.value; 29 this.noAggregation = aggregationType === types.aggregation.none.value;
29 this.limit = limit; 30 this.limit = limit;
30 this.timeWindow = timeWindow; 31 this.timeWindow = timeWindow;
31 this.interval = interval; 32 this.interval = interval;
32 - this.aggregationTimeout = this.interval; 33 + this.aggregationTimeout = Math.max(this.interval, 1000);
33 switch (aggregationType) { 34 switch (aggregationType) {
34 case types.aggregation.min.value: 35 case types.aggregation.min.value:
35 this.aggFunction = min; 36 this.aggFunction = min;
@@ -54,11 +55,37 @@ export default class DataAggregator { @@ -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 onData(data, update, history) { 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 if (update) { 89 if (update) {
63 this.aggregationMap = {}; 90 this.aggregationMap = {};
64 updateAggregatedData(this.aggregationMap, this.aggregationType === this.types.aggregation.count.value, 91 updateAggregatedData(this.aggregationMap, this.aggregationType === this.types.aggregation.count.value,
@@ -66,19 +93,24 @@ export default class DataAggregator { @@ -66,19 +93,24 @@ export default class DataAggregator {
66 } else { 93 } else {
67 this.aggregationMap = processAggregatedData(data.data, this.aggregationType === this.types.aggregation.count.value, this.noAggregation); 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 } else { 100 } else {
71 updateAggregatedData(this.aggregationMap, this.aggregationType === this.types.aggregation.count.value, 101 updateAggregatedData(this.aggregationMap, this.aggregationType === this.types.aggregation.count.value,
72 this.noAggregation, this.aggFunction, data.data, this.interval, this.startTs); 102 this.noAggregation, this.aggFunction, data.data, this.interval, this.startTs);
73 if (history) { 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 var now = currentTime(); 111 var now = currentTime();
81 - this.elapsed += now - startedTime; 112 + this.elapsed += now - this.intervalScheduledTime;
  113 + this.intervalScheduledTime = now;
82 if (this.intervalTimeoutHandle) { 114 if (this.intervalTimeoutHandle) {
83 this.$timeout.cancel(this.intervalTimeoutHandle); 115 this.$timeout.cancel(this.intervalTimeoutHandle);
84 this.intervalTimeoutHandle = null; 116 this.intervalTimeoutHandle = null;
@@ -101,16 +133,11 @@ export default class DataAggregator { @@ -101,16 +133,11 @@ export default class DataAggregator {
101 var self = this; 133 var self = this;
102 if (!history) { 134 if (!history) {
103 this.intervalTimeoutHandle = this.$timeout(function() { 135 this.intervalTimeoutHandle = this.$timeout(function() {
104 - self.onInterval(now); 136 + self.onInterval();
105 }, this.aggregationTimeout, false); 137 }, this.aggregationTimeout, false);
106 } 138 }
107 } 139 }
108 140
109 - reset() {  
110 - this.destroy();  
111 - this.dataReceived = false;  
112 - }  
113 -  
114 destroy() { 141 destroy() {
115 if (this.intervalTimeoutHandle) { 142 if (this.intervalTimeoutHandle) {
116 this.$timeout.cancel(this.intervalTimeoutHandle); 143 this.$timeout.cancel(this.intervalTimeoutHandle);
@@ -254,6 +254,7 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic @@ -254,6 +254,7 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic
254 keys: tsKeys, 254 keys: tsKeys,
255 startTs: subsTw.fixedWindow.startTimeMs, 255 startTs: subsTw.fixedWindow.startTimeMs,
256 endTs: subsTw.fixedWindow.endTimeMs, 256 endTs: subsTw.fixedWindow.endTimeMs,
  257 + interval: subsTw.aggregation.interval,
257 limit: subsTw.aggregation.limit, 258 limit: subsTw.aggregation.limit,
258 agg: subsTw.aggregation.type 259 agg: subsTw.aggregation.type
259 }; 260 };
@@ -266,9 +267,7 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic @@ -266,9 +267,7 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic
266 onData(data.data, types.dataKeyType.timeseries); 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 telemetryWebsocketService.subscribe(subscriber); 273 telemetryWebsocketService.subscribe(subscriber);
@@ -287,35 +286,26 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic @@ -287,35 +286,26 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic
287 }; 286 };
288 287
289 if (datasourceSubscription.type === types.widgetType.timeseries.value) { 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 subscriber.onData = function(data) { 291 subscriber.onData = function(data) {
309 dataAggregator.onData(data); 292 dataAggregator.onData(data);
310 } 293 }
311 subscriber.onReconnected = function() { 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 } else { 307 } else {
316 - subscriber.onReconnected = function() {  
317 - onReconnected();  
318 - } 308 + subscriber.onReconnected = function() {}
319 subscriber.onData = function(data) { 309 subscriber.onData = function(data) {
320 if (data.data) { 310 if (data.data) {
321 onData(data.data, types.dataKeyType.timeseries); 311 onData(data.data, types.dataKeyType.timeseries);
@@ -344,9 +334,7 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic @@ -344,9 +334,7 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic
344 onData(data.data, types.dataKeyType.attribute); 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 telemetryWebsocketService.subscribe(subscriber); 340 telemetryWebsocketService.subscribe(subscriber);
@@ -384,7 +372,31 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic @@ -384,7 +372,31 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic
384 timer = $timeout(onTick, 0, false); 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 function unsubscribe() { 402 function unsubscribe() {
@@ -495,27 +507,6 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic @@ -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 function isNumeric(val) { 510 function isNumeric(val) {
520 return (val - parseFloat( val ) + 1) >= 0; 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 +}
@@ -129,7 +129,7 @@ function WidgetService($rootScope, $http, $q, $filter, $ocLazyLoad, $window, typ @@ -129,7 +129,7 @@ function WidgetService($rootScope, $http, $q, $filter, $ocLazyLoad, $window, typ
129 resources: [], 129 resources: [],
130 templateHtml: '<div class="tb-widget-error-container"><div translate class="tb-widget-error-msg">widget.widget-type-not-found</div></div>', 130 templateHtml: '<div class="tb-widget-error-container"><div translate class="tb-widget-error-msg">widget.widget-type-not-found</div></div>',
131 templateCss: '', 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 settingsSchema: '{}\n', 133 settingsSchema: '{}\n',
134 dataKeySettingsSchema: '{}\n', 134 dataKeySettingsSchema: '{}\n',
135 defaultConfig: '{\n' + 135 defaultConfig: '{\n' +
@@ -147,7 +147,7 @@ function WidgetService($rootScope, $http, $q, $filter, $ocLazyLoad, $window, typ @@ -147,7 +147,7 @@ function WidgetService($rootScope, $http, $q, $filter, $ocLazyLoad, $window, typ
147 resources: [], 147 resources: [],
148 templateHtml: '<div class="tb-widget-error-container"><div translate class="tb-widget-error-msg">widget.widget-type-load-error</div>', 148 templateHtml: '<div class="tb-widget-error-container"><div translate class="tb-widget-error-msg">widget.widget-type-load-error</div>',
149 templateCss: '', 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 settingsSchema: '{}\n', 151 settingsSchema: '{}\n',
152 dataKeySettingsSchema: '{}\n', 152 dataKeySettingsSchema: '{}\n',
153 defaultConfig: '{\n' + 153 defaultConfig: '{\n' +
@@ -51,6 +51,7 @@ import thingsboardMenu from './services/menu.service'; @@ -51,6 +51,7 @@ import thingsboardMenu from './services/menu.service';
51 import thingsboardRaf from './common/raf.provider'; 51 import thingsboardRaf from './common/raf.provider';
52 import thingsboardUtils from './common/utils.service'; 52 import thingsboardUtils from './common/utils.service';
53 import thingsboardTypes from './common/types.constant'; 53 import thingsboardTypes from './common/types.constant';
  54 +import thingsboardApiTime from './api/time.service';
54 import thingsboardKeyboardShortcut from './components/keyboard-shortcut.filter'; 55 import thingsboardKeyboardShortcut from './components/keyboard-shortcut.filter';
55 import thingsboardHelp from './help/help.directive'; 56 import thingsboardHelp from './help/help.directive';
56 import thingsboardToast from './services/toast'; 57 import thingsboardToast from './services/toast';
@@ -101,6 +102,7 @@ angular.module('thingsboard', [ @@ -101,6 +102,7 @@ angular.module('thingsboard', [
101 thingsboardRaf, 102 thingsboardRaf,
102 thingsboardUtils, 103 thingsboardUtils,
103 thingsboardTypes, 104 thingsboardTypes,
  105 + thingsboardApiTime,
104 thingsboardKeyboardShortcut, 106 thingsboardKeyboardShortcut,
105 thingsboardHelp, 107 thingsboardHelp,
106 thingsboardToast, 108 thingsboardToast,
@@ -26,7 +26,7 @@ export default angular.module('thingsboard.directives.timeinterval', []) @@ -26,7 +26,7 @@ export default angular.module('thingsboard.directives.timeinterval', [])
26 .name; 26 .name;
27 27
28 /*@ngInject*/ 28 /*@ngInject*/
29 -function Timeinterval($compile, $templateCache, $translate) { 29 +function Timeinterval($compile, $templateCache, timeService) {
30 30
31 var linker = function (scope, element, attrs, ngModelCtrl) { 31 var linker = function (scope, element, attrs, ngModelCtrl) {
32 32
@@ -39,62 +39,33 @@ function Timeinterval($compile, $templateCache, $translate) { @@ -39,62 +39,33 @@ function Timeinterval($compile, $templateCache, $translate) {
39 scope.mins = 1; 39 scope.mins = 1;
40 scope.secs = 0; 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 scope.setIntervalMs = function (intervalMs) { 65 scope.setIntervalMs = function (intervalMs) {
  66 + if (!scope.advanced) {
  67 + scope.intervalMs = intervalMs;
  68 + }
98 var intervalSeconds = Math.floor(intervalMs / 1000); 69 var intervalSeconds = Math.floor(intervalMs / 1000);
99 scope.days = Math.floor(intervalSeconds / 86400); 70 scope.days = Math.floor(intervalSeconds / 86400);
100 scope.hours = Math.floor((intervalSeconds % 86400) / 3600); 71 scope.hours = Math.floor((intervalSeconds % 86400) / 3600);
@@ -105,6 +76,9 @@ function Timeinterval($compile, $templateCache, $translate) { @@ -105,6 +76,9 @@ function Timeinterval($compile, $templateCache, $translate) {
105 ngModelCtrl.$render = function () { 76 ngModelCtrl.$render = function () {
106 if (ngModelCtrl.$viewValue) { 77 if (ngModelCtrl.$viewValue) {
107 var intervalMs = ngModelCtrl.$viewValue; 78 var intervalMs = ngModelCtrl.$viewValue;
  79 + if (!scope.rendered) {
  80 + scope.advanced = !timeService.matchesExistingInterval(scope.min, scope.max, intervalMs);
  81 + }
108 scope.setIntervalMs(intervalMs); 82 scope.setIntervalMs(intervalMs);
109 } 83 }
110 scope.rendered = true; 84 scope.rendered = true;
@@ -115,10 +89,15 @@ function Timeinterval($compile, $templateCache, $translate) { @@ -115,10 +89,15 @@ function Timeinterval($compile, $templateCache, $translate) {
115 return; 89 return;
116 } 90 }
117 var value = null; 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 scope.hours * 3600 + 97 scope.hours * 3600 +
120 scope.mins * 60 + 98 scope.mins * 60 +
121 scope.secs) * 1000; 99 scope.secs) * 1000;
  100 + }
122 if (!isNaN(intervalMs) && intervalMs > 0) { 101 if (!isNaN(intervalMs) && intervalMs > 0) {
123 value = intervalMs; 102 value = intervalMs;
124 ngModelCtrl.$setValidity('tb-timeinterval', true); 103 ngModelCtrl.$setValidity('tb-timeinterval', true);
@@ -126,6 +105,7 @@ function Timeinterval($compile, $templateCache, $translate) { @@ -126,6 +105,7 @@ function Timeinterval($compile, $templateCache, $translate) {
126 ngModelCtrl.$setValidity('tb-timeinterval', !scope.required); 105 ngModelCtrl.$setValidity('tb-timeinterval', !scope.required);
127 } 106 }
128 ngModelCtrl.$setViewValue(value); 107 ngModelCtrl.$setViewValue(value);
  108 + scope.boundInterval();
129 } 109 }
130 110
131 scope.$watch('required', function (newRequired, prevRequired) { 111 scope.$watch('required', function (newRequired, prevRequired) {
@@ -134,6 +114,38 @@ function Timeinterval($compile, $templateCache, $translate) { @@ -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 scope.$watch('secs', function (newSecs) { 149 scope.$watch('secs', function (newSecs) {
138 if (angular.isUndefined(newSecs)) { 150 if (angular.isUndefined(newSecs)) {
139 return; 151 return;
@@ -198,6 +210,8 @@ function Timeinterval($compile, $templateCache, $translate) { @@ -198,6 +210,8 @@ function Timeinterval($compile, $templateCache, $translate) {
198 scope.updateView(); 210 scope.updateView();
199 }); 211 });
200 212
  213 + scope.boundInterval();
  214 +
201 $compile(element.contents())(scope); 215 $compile(element.contents())(scope);
202 216
203 } 217 }
@@ -206,7 +220,10 @@ function Timeinterval($compile, $templateCache, $translate) { @@ -206,7 +220,10 @@ function Timeinterval($compile, $templateCache, $translate) {
206 restrict: "E", 220 restrict: "E",
207 require: "^ngModel", 221 require: "^ngModel",
208 scope: { 222 scope: {
209 - required: '=ngRequired' 223 + required: '=ngRequired',
  224 + min: '=?',
  225 + max: '=?',
  226 + predefinedName: '=?'
210 }, 227 },
211 link: linker 228 link: linker
212 }; 229 };
@@ -14,6 +14,7 @@ @@ -14,6 +14,7 @@
14 * limitations under the License. 14 * limitations under the License.
15 */ 15 */
16 tb-timeinterval { 16 tb-timeinterval {
  17 + min-width: 355px;
17 md-input-container { 18 md-input-container {
18 margin-bottom: 0px; 19 margin-bottom: 0px;
19 .md-errors-spacer { 20 .md-errors-spacer {
@@ -25,10 +26,13 @@ tb-timeinterval { @@ -25,10 +26,13 @@ tb-timeinterval {
25 width: 150px; 26 width: 150px;
26 } 27 }
27 } 28 }
28 -}  
29 -  
30 -tb-timeinterval {  
31 .md-input { 29 .md-input {
32 width: 70px !important; 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,33 +15,41 @@
15 limitations under the License. 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 </section> 55 </section>
@@ -14,7 +14,7 @@ @@ -14,7 +14,7 @@
14 * limitations under the License. 14 * limitations under the License.
15 */ 15 */
16 /*@ngInject*/ 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 var vm = this; 19 var vm = this;
20 20
@@ -24,6 +24,13 @@ export default function TimewindowPanelController(mdPanelRef, $scope, types, tim @@ -24,6 +24,13 @@ export default function TimewindowPanelController(mdPanelRef, $scope, types, tim
24 vm.aggregation = aggregation; 24 vm.aggregation = aggregation;
25 vm.onTimewindowUpdate = onTimewindowUpdate; 25 vm.onTimewindowUpdate = onTimewindowUpdate;
26 vm.aggregationTypes = types.aggregation; 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 if (vm.historyOnly) { 35 if (vm.historyOnly) {
29 vm.timewindow.selectedTab = 1; 36 vm.timewindow.selectedTab = 1;
@@ -48,4 +55,45 @@ export default function TimewindowPanelController(mdPanelRef, $scope, types, tim @@ -48,4 +55,45 @@ export default function TimewindowPanelController(mdPanelRef, $scope, types, tim
48 vm.onTimewindowUpdate && vm.onTimewindowUpdate(vm.timewindow); 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,61 +17,70 @@
17 --> 17 -->
18 <form name="theForm" ng-submit="vm.update()"> 18 <form name="theForm" ng-submit="vm.update()">
19 <fieldset ng-disabled="loading"> 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 <md-input-container> 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 </md-input-container> 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 <section layout="row" layout-alignment="start center"> 82 <section layout="row" layout-alignment="start center">
74 - <span flex></span> 83 + <span flex></span>
75 <md-button ng-disabled="loading || theForm.$invalid || !theForm.$dirty" type="submit" class="md-raised md-primary"> 84 <md-button ng-disabled="loading || theForm.$invalid || !theForm.$dirty" type="submit" class="md-raised md-primary">
76 {{ 'action.update' | translate }} 85 {{ 'action.update' | translate }}
77 </md-button> 86 </md-button>
@@ -79,6 +88,6 @@ @@ -79,6 +88,6 @@
79 {{ 'action.cancel' | translate }} 88 {{ 'action.cancel' | translate }}
80 </md-button> 89 </md-button>
81 </section> 90 </section>
82 - </section> 91 + </md-content>
83 </fieldset> 92 </fieldset>
84 </form> 93 </form>
@@ -37,16 +37,18 @@ export default angular.module('thingsboard.directives.timewindow', [thingsboardT @@ -37,16 +37,18 @@ export default angular.module('thingsboard.directives.timewindow', [thingsboardT
37 37
38 /* eslint-disable angular/angularelement */ 38 /* eslint-disable angular/angularelement */
39 /*@ngInject*/ 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 var linker = function (scope, element, attrs, ngModelCtrl) { 42 var linker = function (scope, element, attrs, ngModelCtrl) {
43 43
44 /* tbTimewindow (ng-model) 44 /* tbTimewindow (ng-model)
45 * { 45 * {
46 * realtime: { 46 * realtime: {
  47 + * interval: 0,
47 * timewindowMs: 0 48 * timewindowMs: 0
48 * }, 49 * },
49 * history: { 50 * history: {
  51 + * interval: 0,
50 * timewindowMs: 0, 52 * timewindowMs: 0,
51 * fixedTimewindow: { 53 * fixedTimewindow: {
52 * startTimeMs: 0, 54 * startTimeMs: 0,
@@ -54,8 +56,8 @@ function Timewindow($compile, $templateCache, $filter, $mdPanel, $document, $mdM @@ -54,8 +56,8 @@ function Timewindow($compile, $templateCache, $filter, $mdPanel, $document, $mdM
54 * } 56 * }
55 * }, 57 * },
56 * aggregation: { 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,16 +83,6 @@ function Timewindow($compile, $templateCache, $filter, $mdPanel, $document, $mdM
81 } 83 }
82 element.html(template); 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 scope.openEditMode = function (event) { 86 scope.openEditMode = function (event) {
95 var position; 87 var position;
96 var isGtSm = $mdMedia('gt-sm'); 88 var isGtSm = $mdMedia('gt-sm');
@@ -143,15 +135,18 @@ function Timewindow($compile, $templateCache, $filter, $mdPanel, $document, $mdM @@ -143,15 +135,18 @@ function Timewindow($compile, $templateCache, $filter, $mdPanel, $document, $mdM
143 var model = scope.model; 135 var model = scope.model;
144 if (model.selectedTab === 0) { 136 if (model.selectedTab === 0) {
145 value.realtime = { 137 value.realtime = {
  138 + interval: model.realtime.interval,
146 timewindowMs: model.realtime.timewindowMs 139 timewindowMs: model.realtime.timewindowMs
147 }; 140 };
148 } else { 141 } else {
149 if (model.history.historyType === 0) { 142 if (model.history.historyType === 0) {
150 value.history = { 143 value.history = {
  144 + interval: model.history.interval,
151 timewindowMs: model.history.timewindowMs 145 timewindowMs: model.history.timewindowMs
152 }; 146 };
153 } else { 147 } else {
154 value.history = { 148 value.history = {
  149 + interval: model.history.interval,
155 fixedTimewindow: { 150 fixedTimewindow: {
156 startTimeMs: model.history.fixedTimewindow.startTimeMs, 151 startTimeMs: model.history.fixedTimewindow.startTimeMs,
157 endTimeMs: model.history.fixedTimewindow.endTimeMs 152 endTimeMs: model.history.fixedTimewindow.endTimeMs
@@ -160,8 +155,8 @@ function Timewindow($compile, $templateCache, $filter, $mdPanel, $document, $mdM @@ -160,8 +155,8 @@ function Timewindow($compile, $templateCache, $filter, $mdPanel, $document, $mdM
160 } 155 }
161 } 156 }
162 value.aggregation = { 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 ngModelCtrl.$setViewValue(value); 161 ngModelCtrl.$setViewValue(value);
167 scope.updateDisplayValue(); 162 scope.updateDisplayValue();
@@ -190,34 +185,17 @@ function Timewindow($compile, $templateCache, $filter, $mdPanel, $document, $mdM @@ -190,34 +185,17 @@ function Timewindow($compile, $templateCache, $filter, $mdPanel, $document, $mdM
190 } 185 }
191 186
192 ngModelCtrl.$render = function () { 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 if (ngModelCtrl.$viewValue) { 189 if (ngModelCtrl.$viewValue) {
214 var value = ngModelCtrl.$viewValue; 190 var value = ngModelCtrl.$viewValue;
215 var model = scope.model; 191 var model = scope.model;
216 if (angular.isDefined(value.realtime)) { 192 if (angular.isDefined(value.realtime)) {
217 model.selectedTab = 0; 193 model.selectedTab = 0;
  194 + model.realtime.interval = value.realtime.interval;
218 model.realtime.timewindowMs = value.realtime.timewindowMs; 195 model.realtime.timewindowMs = value.realtime.timewindowMs;
219 } else { 196 } else {
220 model.selectedTab = 1; 197 model.selectedTab = 1;
  198 + model.history.interval = value.history.interval;
221 if (angular.isDefined(value.history.timewindowMs)) { 199 if (angular.isDefined(value.history.timewindowMs)) {
222 model.history.historyType = 0; 200 model.history.historyType = 0;
223 model.history.timewindowMs = value.history.timewindowMs; 201 model.history.timewindowMs = value.history.timewindowMs;
@@ -228,10 +206,10 @@ function Timewindow($compile, $templateCache, $filter, $mdPanel, $document, $mdM @@ -228,10 +206,10 @@ function Timewindow($compile, $templateCache, $filter, $mdPanel, $document, $mdM
228 } 206 }
229 } 207 }
230 if (angular.isDefined(value.aggregation)) { 208 if (angular.isDefined(value.aggregation)) {
231 - model.aggregation.limit = value.aggregation.limit || 200;  
232 if (angular.isDefined(value.aggregation.type) && value.aggregation.type.length > 0) { 209 if (angular.isDefined(value.aggregation.type) && value.aggregation.type.length > 0) {
233 model.aggregation.type = value.aggregation.type; 210 model.aggregation.type = value.aggregation.type;
234 } 211 }
  212 + model.aggregation.limit = value.aggregation.limit || timeService.avgAggregationLimit();
235 } 213 }
236 } 214 }
237 scope.updateDisplayValue(); 215 scope.updateDisplayValue();
@@ -21,14 +21,39 @@ @@ -21,14 +21,39 @@
21 } 21 }
22 22
23 .tb-timewindow-panel { 23 .tb-timewindow-panel {
24 - min-height: 375px; 24 + max-height: 440px;
  25 + min-width: 417px;
25 background: white; 26 background: white;
26 border-radius: 4px; 27 border-radius: 4px;
27 box-shadow: 0 7px 8px -4px rgba(0, 0, 0, 0.2), 28 box-shadow: 0 7px 8px -4px rgba(0, 0, 0, 0.2),
28 0 13px 19px 2px rgba(0, 0, 0, 0.14), 29 0 13px 19px 2px rgba(0, 0, 0, 0.14),
29 0 5px 24px 4px rgba(0, 0, 0, 0.12); 30 0 5px 24px 4px rgba(0, 0, 0, 0.12);
30 overflow: hidden; 31 overflow: hidden;
  32 + form, fieldset {
  33 + height: 100%;
  34 + }
31 md-content { 35 md-content {
32 background-color: #fff; 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,9 +15,9 @@
15 limitations under the License. 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 <span ng-click="openEditMode($event)">{{model.displayValue}}</span> 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 <md-icon ng-style="{ color: buttonColor }" aria-label="{{ 'timewindow.date-range' | translate }}" class="material-icons">date_range</md-icon> 21 <md-icon ng-style="{ color: buttonColor }" aria-label="{{ 'timewindow.date-range' | translate }}" class="material-icons">date_range</md-icon>
22 </md-button> 22 </md-button>
23 </section> 23 </section>
@@ -19,7 +19,7 @@ import 'javascript-detect-element-resize/detect-element-resize'; @@ -19,7 +19,7 @@ import 'javascript-detect-element-resize/detect-element-resize';
19 /* eslint-disable angular/angularelement */ 19 /* eslint-disable angular/angularelement */
20 20
21 /*@ngInject*/ 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 datasourceService, deviceService, visibleRect, isEdit, stDiff, widget, deviceAliasList, widgetType) { 23 datasourceService, deviceService, visibleRect, isEdit, stDiff, widget, deviceAliasList, widgetType) {
24 24
25 var vm = this; 25 var vm = this;
@@ -41,11 +41,7 @@ export default function WidgetController($scope, $timeout, $window, $element, $q @@ -41,11 +41,7 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
41 var targetDeviceAliasId = null; 41 var targetDeviceAliasId = null;
42 var targetDeviceId = null; 42 var targetDeviceId = null;
43 var originalTimewindow = null; 43 var originalTimewindow = null;
44 - var subscriptionTimewindow = {  
45 - fixedWindow: null,  
46 - realtimeWindowMs: null,  
47 - aggregation: null  
48 - }; 44 + var subscriptionTimewindow = null;
49 var dataUpdateCaf = null; 45 var dataUpdateCaf = null;
50 46
51 /* 47 /*
@@ -488,15 +484,7 @@ export default function WidgetController($scope, $timeout, $window, $element, $q @@ -488,15 +484,7 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
488 if (!originalTimewindow) { 484 if (!originalTimewindow) {
489 originalTimewindow = angular.copy(widget.config.timewindow); 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 function dataUpdated(sourceData, datasourceIndex, dataKeyIndex) { 490 function dataUpdated(sourceData, datasourceIndex, dataKeyIndex) {
@@ -511,7 +499,7 @@ export default function WidgetController($scope, $timeout, $window, $element, $q @@ -511,7 +499,7 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
511 } 499 }
512 } 500 }
513 if (update) { 501 if (update) {
514 - if (subscriptionTimewindow.realtimeWindowMs) { 502 + if (subscriptionTimewindow && subscriptionTimewindow.realtimeWindowMs) {
515 updateTimewindow(); 503 updateTimewindow();
516 } 504 }
517 widgetContext.data[datasourceIndex + dataKeyIndex].data = sourceData.data; 505 widgetContext.data[datasourceIndex + dataKeyIndex].data = sourceData.data;
@@ -555,62 +543,26 @@ export default function WidgetController($scope, $timeout, $window, $element, $q @@ -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 function subscribe() { 556 function subscribe() {
559 if (widget.type !== types.widgetType.rpc.value) { 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 if (widget.type === types.widgetType.timeseries.value && 558 if (widget.type === types.widgetType.timeseries.value &&
568 angular.isDefined(widget.config.timewindow)) { 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 if (subscriptionTimewindow.fixedWindow) { 561 if (subscriptionTimewindow.fixedWindow) {
611 onDataUpdated(); 562 onDataUpdated();
612 } 563 }
613 } 564 }
  565 + var index = 0;
614 for (var i in widget.config.datasources) { 566 for (var i in widget.config.datasources) {
615 var datasource = widget.config.datasources[i]; 567 var datasource = widget.config.datasources[i];
616 var deviceId = null; 568 var deviceId = null;
@@ -630,6 +582,13 @@ export default function WidgetController($scope, $timeout, $window, $element, $q @@ -630,6 +582,13 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
630 dataUpdated: function (data, datasourceIndex, dataKeyIndex) { 582 dataUpdated: function (data, datasourceIndex, dataKeyIndex) {
631 dataUpdated(data, datasourceIndex, dataKeyIndex); 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 datasourceIndex: index 592 datasourceIndex: index
634 }; 593 };
635 594
@@ -29,7 +29,7 @@ import EditAttributeValueController from './edit-attribute-value.controller'; @@ -29,7 +29,7 @@ import EditAttributeValueController from './edit-attribute-value.controller';
29 29
30 /*@ngInject*/ 30 /*@ngInject*/
31 export default function AttributeTableDirective($compile, $templateCache, $rootScope, $q, $mdEditDialog, $mdDialog, 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 var linker = function (scope, element, attrs) { 34 var linker = function (scope, element, attrs) {
35 35
@@ -303,6 +303,9 @@ export default function AttributeTableDirective($compile, $templateCache, $rootS @@ -303,6 +303,9 @@ export default function AttributeTableDirective($compile, $templateCache, $rootS
303 var isSystem = scope.widgetsBundle.tenantId.id === types.id.nullUid; 303 var isSystem = scope.widgetsBundle.tenantId.id === types.id.nullUid;
304 widgetService.getBundleWidgetTypes(scope.widgetsBundle.alias, isSystem).then( 304 widgetService.getBundleWidgetTypes(scope.widgetsBundle.alias, isSystem).then(
305 function success(widgetTypes) { 305 function success(widgetTypes) {
  306 +
  307 + widgetTypes = $filter('orderBy')(widgetTypes, ['-descriptor.type','-createdTime']);
  308 +
306 for (var i = 0; i < widgetTypes.length; i++) { 309 for (var i = 0; i < widgetTypes.length; i++) {
307 var widgetType = widgetTypes[i]; 310 var widgetType = widgetTypes[i];
308 var widgetInfo = widgetService.toWidgetInfo(widgetType); 311 var widgetInfo = widgetService.toWidgetInfo(widgetType);
@@ -67,6 +67,7 @@ export default angular.module('thingsboard.locale', []) @@ -67,6 +67,7 @@ export default angular.module('thingsboard.locale', [])
67 "aggregation": "Aggregation", 67 "aggregation": "Aggregation",
68 "function": "Data aggregation function", 68 "function": "Data aggregation function",
69 "limit": "Max values", 69 "limit": "Max values",
  70 + "group-interval": "Grouping interval",
70 "min": "Min", 71 "min": "Min",
71 "max": "Max", 72 "max": "Max",
72 "avg": "Average", 73 "avg": "Average",
@@ -558,7 +559,8 @@ export default angular.module('thingsboard.locale', []) @@ -558,7 +559,8 @@ export default angular.module('thingsboard.locale', [])
558 "days": "Days", 559 "days": "Days",
559 "hours": "Hours", 560 "hours": "Hours",
560 "minutes": "Minutes", 561 "minutes": "Minutes",
561 - "seconds": "Seconds" 562 + "seconds": "Seconds",
  563 + "advanced": "Advanced"
562 }, 564 },
563 "timewindow": { 565 "timewindow": {
564 "days": "{ days, select, 1 { day } other {# days } }", 566 "days": "{ days, select, 1 { day } other {# days } }",
@@ -167,6 +167,7 @@ export default class TbFlot { @@ -167,6 +167,7 @@ export default class TbFlot {
167 var settings = ctx.settings; 167 var settings = ctx.settings;
168 ctx.trackDecimals = angular.isDefined(settings.decimals) ? settings.decimals : 1; 168 ctx.trackDecimals = angular.isDefined(settings.decimals) ? settings.decimals : 1;
169 ctx.tooltipIndividual = this.chartType === 'pie' || (angular.isDefined(settings.tooltipIndividual) ? settings.tooltipIndividual : false); 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 var font = { 172 var font = {
172 color: settings.fontColor || "#545454", 173 color: settings.fontColor || "#545454",
@@ -232,6 +233,21 @@ export default class TbFlot { @@ -232,6 +233,21 @@ export default class TbFlot {
232 options.yaxis.tickFormatter = function() { 233 options.yaxis.tickFormatter = function() {
233 return ''; 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 options.yaxis.font.color = settings.yaxis.color || options.yaxis.font.color; 252 options.yaxis.font.color = settings.yaxis.color || options.yaxis.font.color;
237 options.yaxis.label = settings.yaxis.title || null; 253 options.yaxis.label = settings.yaxis.title || null;
@@ -323,6 +339,8 @@ export default class TbFlot { @@ -323,6 +339,8 @@ export default class TbFlot {
323 339
324 this.options = options; 340 this.options = options;
325 341
  342 + this.checkMouseEvents();
  343 +
326 if (this.chartType === 'pie' && this.ctx.animatedPie) { 344 if (this.chartType === 'pie' && this.ctx.animatedPie) {
327 this.ctx.pieDataAnimationDuration = 250; 345 this.ctx.pieDataAnimationDuration = 250;
328 this.ctx.pieData = angular.copy(this.ctx.data); 346 this.ctx.pieData = angular.copy(this.ctx.data);
@@ -337,7 +355,6 @@ export default class TbFlot { @@ -337,7 +355,6 @@ export default class TbFlot {
337 } else { 355 } else {
338 this.ctx.plot = $.plot(this.ctx.$container, this.ctx.data, this.options); 356 this.ctx.plot = $.plot(this.ctx.$container, this.ctx.data, this.options);
339 } 357 }
340 - this.checkMouseEvents();  
341 } 358 }
342 359
343 update() { 360 update() {
@@ -577,6 +594,11 @@ export default class TbFlot { @@ -577,6 +594,11 @@ export default class TbFlot {
577 "type": "boolean", 594 "type": "boolean",
578 "default": false 595 "default": false
579 }, 596 },
  597 + "tooltipCumulative": {
  598 + "title": "Show cumulative values in stacking mode",
  599 + "type": "boolean",
  600 + "default": false
  601 + },
580 "grid": { 602 "grid": {
581 "title": "Grid settings", 603 "title": "Grid settings",
582 "type": "object", 604 "type": "object",
@@ -710,6 +732,7 @@ export default class TbFlot { @@ -710,6 +732,7 @@ export default class TbFlot {
710 "decimals", 732 "decimals",
711 "units", 733 "units",
712 "tooltipIndividual", 734 "tooltipIndividual",
  735 + "tooltipCumulative",
713 { 736 {
714 "key": "grid", 737 "key": "grid",
715 "items": [ 738 "items": [
@@ -834,10 +857,28 @@ export default class TbFlot { @@ -834,10 +857,28 @@ export default class TbFlot {
834 } 857 }
835 858
836 checkMouseEvents() { 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,7 +1071,7 @@ export default class TbFlot {
1030 minTime = pointTime; 1071 minTime = pointTime;
1031 } 1072 }
1032 if (series.stack) { 1073 if (series.stack) {
1033 - if (this.ctx.tooltipIndividual) { 1074 + if (this.ctx.tooltipIndividual || !this.ctx.tooltipCumulative) {
1034 value = series.data[hoverIndex][1]; 1075 value = series.data[hoverIndex][1];
1035 } else { 1076 } else {
1036 last_value += series.data[hoverIndex][1]; 1077 last_value += series.data[hoverIndex][1];
@@ -201,6 +201,14 @@ md-sidenav { @@ -201,6 +201,14 @@ md-sidenav {
201 color: rgba(0,0,0,0.54); 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 * Prompt 213 * Prompt
206 ***********************/ 214 ***********************/