Commit 467d7b5c3c5d9dce13b82e3c2da4dc15fcc0999f

Authored by Chantsova Ekaterina
Committed by Igor Kulikov
1 parent 14c2d2e5

Flots comparison option (#2079)

* Add new hidden widget cards for latest and timeseries value

* Revert "Add new hidden widget cards for latest and timeseries value"

This reverts commit 09b73d5afcc66baf942a776f83a0295700f83d22.

* Allow hiding of zero/false dataKey values from tbFlot widgets tooltips

* comparison option draft

* Added dataKey setting for excluding from Stacking mode in chart widgets.

* Flot comparison option (draft)

* Flot comparison option (draft)

* Fix color generation for additional keys

* Add new time options for comparison

* Add ability to define points symbol and line width in 'line' flot charts

* Change history timeinterval calculation, translations definition
... ... @@ -35,7 +35,7 @@
35 35 "resources": [],
36 36 "templateHtml": "",
37 37 "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",
38   - "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('graph');\n}\n\nself.getDataKeySettingsSchema = function() {\n return TbFlot.datakeySettingsSchema(true);\n}\n\nself.onDestroy = function() {\n self.ctx.flot.destroy();\n}\n",
  38 + "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('graph');\n}\n\nself.getDataKeySettingsSchema = function() {\n return TbFlot.datakeySettingsSchema(true, 'graph');\n}\n\nself.onDestroy = function() {\n self.ctx.flot.destroy();\n}\n",
39 39 "settingsSchema": "{}",
40 40 "dataKeySettingsSchema": "{}",
41 41 "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}"
... ... @@ -147,7 +147,7 @@
147 147 "resources": [],
148 148 "templateHtml": "",
149 149 "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",
150   - "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('bar');\n}\n\nself.getDataKeySettingsSchema = function() {\n return TbFlot.datakeySettingsSchema(false);\n}\n\nself.onDestroy = function() {\n self.ctx.flot.destroy();\n}\n",
  150 + "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('bar');\n}\n\nself.getDataKeySettingsSchema = function() {\n return TbFlot.datakeySettingsSchema(false, 'bar');\n}\n\nself.onDestroy = function() {\n self.ctx.flot.destroy();\n}\n",
151 151 "settingsSchema": "{}",
152 152 "dataKeySettingsSchema": "{}",
153 153 "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},\"stack\":true,\"tooltipIndividual\":false,\"defaultBarWidth\":600},\"title\":\"Timeseries Bars - Flot\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"mobileHeight\":null,\"widgetStyle\":{},\"useDashboardTimewindow\":true,\"showLegend\":true,\"actions\":{}}"
... ... @@ -163,7 +163,7 @@
163 163 "resources": [],
164 164 "templateHtml": "",
165 165 "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",
166   - "controllerScript": "self.onInit = function() {\n self.ctx.flot = new TbFlot(self.ctx, 'state'); \n}\n\nself.onDataUpdated = function() {\n self.ctx.flot.update();\n}\n\nself.onResize = function() {\n self.ctx.flot.resize();\n}\n\nself.typeParameters = function() {\n return {\n stateData: true\n };\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('graph');\n}\n\nself.getDataKeySettingsSchema = function() {\n return TbFlot.datakeySettingsSchema(true);\n}\n\nself.onDestroy = function() {\n self.ctx.flot.destroy();\n}\n",
  166 + "controllerScript": "self.onInit = function() {\n self.ctx.flot = new TbFlot(self.ctx, 'state'); \n}\n\nself.onDataUpdated = function() {\n self.ctx.flot.update();\n}\n\nself.onResize = function() {\n self.ctx.flot.resize();\n}\n\nself.typeParameters = function() {\n return {\n stateData: true\n };\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('graph');\n}\n\nself.getDataKeySettingsSchema = function() {\n return TbFlot.datakeySettingsSchema(true, 'graph');\n}\n\nself.onDestroy = function() {\n self.ctx.flot.destroy();\n}\n",
167 167 "settingsSchema": "{}",
168 168 "dataKeySettingsSchema": "{}",
169 169 "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Switch 1\",\"color\":\"#2196f3\",\"settings\":{\"showLines\":true,\"fillLines\":true,\"showPoints\":false,\"axisPosition\":\"left\",\"showSeparateAxis\":false},\"_hash\":0.8587686344902596,\"funcBody\":\"return Math.random() > 0.5 ? 1 : 0;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Switch 2\",\"color\":\"#ffc107\",\"settings\":{\"showLines\":true,\"fillLines\":false,\"showPoints\":false,\"axisPosition\":\"left\"},\"_hash\":0.12775350966079668,\"funcBody\":\"return Math.random() <= 0.5 ? 1 : 0;\"}]}],\"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\",\"ticksFormatter\":\"if (value > 0 && value <= 1) {\\n return 'On';\\n} else if (value === 0) {\\n return 'Off';\\n} else {\\n return '';\\n}\"},\"grid\":{\"color\":\"#545454\",\"tickColor\":\"#DDDDDD\",\"verticalLines\":true,\"horizontalLines\":true,\"outlineWidth\":1},\"stack\":false,\"tooltipIndividual\":false,\"tooltipValueFormatter\":\"if (value > 0 && value <= 1) {\\n return 'On';\\n} else if (value === 0) {\\n return 'Off';\\n} else {\\n return '';\\n}\",\"smoothLines\":false},\"title\":\"State Chart\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"mobileHeight\":null,\"widgetStyle\":{},\"useDashboardTimewindow\":true,\"showLegend\":true,\"actions\":{},\"legendConfig\":{\"position\":\"bottom\",\"showMin\":false,\"showMax\":false,\"showAvg\":false,\"showTotal\":false}}"
... ...
... ... @@ -132,6 +132,13 @@ export default class Subscription {
132 132 }
133 133
134 134 this.subscriptionTimewindow = null;
  135 + this.comparisonEnabled = options.comparisonEnabled;
  136 + if (this.comparisonEnabled) {
  137 + this.timeForComparison = options.timeForComparison;
  138 +
  139 + this.comparisonTimeWindow = {};
  140 + this.timewindowForComparison = null;
  141 + }
135 142
136 143 this.units = options.units || '';
137 144 this.decimals = angular.isDefined(options.decimals) ? options.decimals : 2;
... ... @@ -311,13 +318,28 @@ export default class Subscription {
311 318 }
312 319
313 320 configureData() {
  321 + var additionalDatasources = [];
314 322 var dataIndex = 0;
  323 + var additionalKeysNumber = 0;
315 324 for (var i = 0; i < this.datasources.length; i++) {
316 325 var datasource = this.datasources[i];
  326 + var additionalDataKeys = [];
  327 + let datasourceAdditionalKeysNumber = 0;
  328 +
317 329 for (var a = 0; a < datasource.dataKeys.length; a++) {
318 330 var dataKey = datasource.dataKeys[a];
319 331 dataKey.hidden = false;
320 332 dataKey.pattern = angular.copy(dataKey.label);
  333 +
  334 + if (this.comparisonEnabled && dataKey.settings.comparisonSettings && dataKey.settings.comparisonSettings.showValuesForComparison) {
  335 + datasourceAdditionalKeysNumber++;
  336 + additionalKeysNumber++;
  337 + let additionalDataKey = this.ctx.utils.createAdditionalDataKey(dataKey,datasource, this.timeForComparison,this.datasources,additionalKeysNumber);
  338 + dataKey.settings.comparisonSettings.color = additionalDataKey.color;
  339 +
  340 + additionalDataKeys.push(additionalDataKey);
  341 + }
  342 +
321 343 var datasourceData = {
322 344 datasource: datasource,
323 345 dataKey: dataKey,
... ... @@ -341,8 +363,46 @@ export default class Subscription {
341 363 this.legendData.data.push(legendKeyData);
342 364 }
343 365 }
  366 +
  367 + if (datasourceAdditionalKeysNumber > 0) {
  368 + let additionalDatasource = angular.copy(datasource);
  369 + additionalDatasource.dataKeys = additionalDataKeys;
  370 + additionalDatasource.isAdditional = true;
  371 + additionalDatasources.push(additionalDatasource);
  372 + }
  373 + }
  374 +
  375 + for (var j=0; j < additionalDatasources.length; j++) {
  376 + let additionalDatasource = additionalDatasources[j];
  377 + for (var k=0; k < additionalDatasource.dataKeys.length; k++) {
  378 + let additionalDataKey = additionalDatasource.dataKeys[k];
  379 + var additionalDatasourceData = {
  380 + datasource: additionalDatasource,
  381 + dataKey: additionalDataKey,
  382 + data: []
  383 + };
  384 + this.data.push(additionalDatasourceData);
  385 + this.hiddenData.push({data: []});
  386 + if (this.displayLegend) {
  387 + var additionalLegendKey = {
  388 + dataKey: additionalDataKey,
  389 + dataIndex: dataIndex++
  390 + };
  391 + this.legendData.keys.push(additionalLegendKey);
  392 + var additionalLegendKeyData = {
  393 + min: null,
  394 + max: null,
  395 + avg: null,
  396 + total: null,
  397 + hidden: false
  398 + };
  399 + this.legendData.data.push(additionalLegendKeyData);
  400 + }
  401 + }
344 402 }
345 403
  404 + this.datasources = this.datasources.concat(additionalDatasources);
  405 +
346 406 var subscription = this;
347 407 var registration;
348 408
... ... @@ -638,7 +698,7 @@ export default class Subscription {
638 698 updateTimewindow() {
639 699 this.timeWindow.interval = this.subscriptionTimewindow.aggregation.interval || 1000;
640 700 if (this.subscriptionTimewindow.realtimeWindowMs) {
641   - this.timeWindow.maxTime = (new Date).getTime() + this.timeWindow.stDiff;
  701 + this.timeWindow.maxTime = (moment()).valueOf() + this.timeWindow.stDiff;//eslint-disable-line
642 702 this.timeWindow.minTime = this.timeWindow.maxTime - this.subscriptionTimewindow.realtimeWindowMs;
643 703 } else if (this.subscriptionTimewindow.fixedWindow) {
644 704 this.timeWindow.maxTime = this.subscriptionTimewindow.fixedWindow.endTimeMs;
... ... @@ -659,6 +719,26 @@ export default class Subscription {
659 719 return this.subscriptionTimewindow;
660 720 }
661 721
  722 + updateComparisonTimewindow() {
  723 + this.comparisonTimeWindow.interval = this.timewindowForComparison.aggregation.interval || 1000;
  724 + if (this.timewindowForComparison.realtimeWindowMs) {
  725 + this.comparisonTimeWindow.maxTime = moment(this.timeWindow.maxTime).subtract(1, this.timeForComparison).valueOf(); //eslint-disable-line
  726 + this.comparisonTimeWindow.minTime = this.comparisonTimeWindow.maxTime - this.timewindowForComparison.realtimeWindowMs;
  727 + } else if (this.timewindowForComparison.fixedWindow) {
  728 + this.comparisonTimeWindow.maxTime = this.timewindowForComparison.fixedWindow.endTimeMs;
  729 + this.comparisonTimeWindow.minTime = this.timewindowForComparison.fixedWindow.startTimeMs;
  730 + }
  731 + }
  732 +
  733 + updateSubscriptionForComparison() {
  734 + if (!this.subscriptionTimewindow) {
  735 + this.subscriptionTimewindow = this.updateRealtimeSubscription();
  736 + }
  737 + this.timewindowForComparison = this.ctx.timeService.createTimewindowForComparison(this.subscriptionTimewindow, this.timeForComparison);
  738 + this.updateComparisonTimewindow();
  739 + return this.timewindowForComparison;
  740 + }
  741 +
662 742 dataUpdated(sourceData, datasourceIndex, dataKeyIndex, apply) {
663 743 for (var x = 0; x < this.datasourceListeners.length; x++) {
664 744 this.datasources[x].dataReceived = this.datasources[x].dataReceived === true;
... ... @@ -689,6 +769,9 @@ export default class Subscription {
689 769 if (update) {
690 770 if (this.subscriptionTimewindow && this.subscriptionTimewindow.realtimeWindowMs) {
691 771 this.updateTimewindow();
  772 + if (this.timewindowForComparison && this.timewindowForComparison.realtimeWindowMs) {
  773 + this.updateComparisonTimewindow();
  774 + }
692 775 }
693 776 currentData.data = sourceData.data;
694 777 if (this.caulculateLegendData) {
... ... @@ -745,6 +828,9 @@ export default class Subscription {
745 828 this.notifyDataLoading();
746 829 if (this.type === this.ctx.types.widgetType.timeseries.value && this.timeWindowConfig) {
747 830 this.updateRealtimeSubscription();
  831 + if (this.comparisonEnabled) {
  832 + this.updateSubscriptionForComparison();
  833 + }
748 834 if (this.subscriptionTimewindow.fixedWindow) {
749 835 this.onDataUpdated();
750 836 }
... ... @@ -776,6 +862,17 @@ export default class Subscription {
776 862 datasourceIndex: index
777 863 };
778 864
  865 + if (this.comparisonEnabled && datasource.isAdditional) {
  866 + listener.subscriptionTimewindow = this.timewindowForComparison;
  867 + listener.updateRealtimeSubscription = function () {
  868 + this.subscriptionTimewindow = subscription.updateSubscriptionForComparison();
  869 + return this.subscriptionTimewindow;
  870 + };
  871 + listener.setRealtimeSubscription = function () {
  872 + subscription.updateSubscriptionForComparison();
  873 + };
  874 + }
  875 +
779 876 for (var a = 0; a < datasource.dataKeys.length; a++) {
780 877 this.data[index + a].data = [];
781 878 }
... ... @@ -908,7 +1005,6 @@ export default class Subscription {
908 1005 });
909 1006 this.registrations = [];
910 1007 }
911   -
912 1008 }
913 1009
914 1010 function calculateMin(data) {
... ...
... ... @@ -47,6 +47,7 @@ function TimeService($translate, $http, $q, types) {
47 47 defaultTimewindow: defaultTimewindow,
48 48 toHistoryTimewindow: toHistoryTimewindow,
49 49 createSubscriptionTimewindow: createSubscriptionTimewindow,
  50 + createTimewindowForComparison: createTimewindowForComparison,
50 51 getMaxDatapointsLimit: function () {
51 52 return maxDatapointsLimit;
52 53 },
... ... @@ -383,5 +384,27 @@ function TimeService($translate, $http, $q, types) {
383 384 }
384 385 }
385 386
  387 + function createTimewindowForComparison(subscriptionTimewindow, timeUnit) {
  388 + var timewindowForComparison = {
  389 + fixedWindow: null,
  390 + realtimeWindowMs: null,
  391 + aggregation: subscriptionTimewindow.aggregation
  392 + };
  393 +
  394 + if (subscriptionTimewindow.realtimeWindowMs) {
  395 + timewindowForComparison.startTs = moment(subscriptionTimewindow.startTs).subtract(1, timeUnit).valueOf(); //eslint-disable-line
  396 + timewindowForComparison.realtimeWindowMs = subscriptionTimewindow.realtimeWindowMs;
  397 + } else if (subscriptionTimewindow.fixedWindow) {
  398 + var timeInterval = subscriptionTimewindow.fixedWindow.endTimeMs - subscriptionTimewindow.fixedWindow.startTimeMs;
  399 + var endTimeMs = moment(subscriptionTimewindow.fixedWindow.endTimeMs).subtract(1, timeUnit).valueOf(); //eslint-disable-line
386 400
  401 + timewindowForComparison.startTs = endTimeMs - timeInterval;
  402 + timewindowForComparison.fixedWindow = {
  403 + startTimeMs: timewindowForComparison.startTs,
  404 + endTimeMs: endTimeMs
  405 + };
  406 + }
  407 +
  408 + return timewindowForComparison;
  409 + }
387 410 }
... ...
... ... @@ -144,6 +144,7 @@ function Utils($mdColorPalette, $rootScope, $window, $translate, $q, $timeout, t
144 144 isLocalUrl: isLocalUrl,
145 145 validateDatasources: validateDatasources,
146 146 createKey: createKey,
  147 + createAdditionalDataKey: createAdditionalDataKey,
147 148 createLabelFromDatasource: createLabelFromDatasource,
148 149 insertVariable: insertVariable,
149 150 customTranslation: customTranslation,
... ... @@ -411,8 +412,8 @@ function Utils($mdColorPalette, $rootScope, $window, $translate, $q, $timeout, t
411 412 return copy;
412 413 }
413 414
414   - function genNextColor(datasources) {
415   - var index = 0;
  415 + function genNextColor(datasources, initialIndex) {
  416 + var index = initialIndex || 0;
416 417 if (datasources) {
417 418 for (var i = 0; i < datasources.length; i++) {
418 419 var datasource = datasources[i];
... ... @@ -491,6 +492,23 @@ function Utils($mdColorPalette, $rootScope, $window, $translate, $q, $timeout, t
491 492 return dataKey;
492 493 }
493 494
  495 + function createAdditionalDataKey(dataKey, datasource, timeUnit, datasources, additionalKeysNumber) {
  496 + let additionalDataKey = angular.copy(dataKey);
  497 + if (dataKey.settings.comparisonSettings.comparisonValuesLabel) {
  498 + additionalDataKey.label = createLabelFromDatasource(datasource, dataKey.settings.comparisonSettings.comparisonValuesLabel);
  499 + } else {
  500 + additionalDataKey.label = dataKey.label + ' ' + $translate.instant('legend.comparison-time-ago.'+timeUnit);
  501 + }
  502 + additionalDataKey.pattern = additionalDataKey.label;
  503 + if (dataKey.settings.comparisonSettings.color) {
  504 + additionalDataKey.color = dataKey.settings.comparisonSettings.color;
  505 + } else {
  506 + additionalDataKey.color = genNextColor(datasources, additionalKeysNumber);
  507 + }
  508 + additionalDataKey._hash = Math.random();
  509 + return additionalDataKey;
  510 + }
  511 +
494 512 function createLabelFromDatasource(datasource, pattern) {
495 513 var label = angular.copy(pattern);
496 514 var match = varsRegex.exec(pattern);
... ...
... ... @@ -357,8 +357,10 @@ export default function WidgetController($scope, $state, $timeout, $window, $ocL
357 357 if (widget.type !== types.widgetType.rpc.value && widget.type !== types.widgetType.static.value) {
358 358 options = {
359 359 type: widget.type,
360   - stateData: vm.typeParameters.stateData
361   - }
  360 + stateData: vm.typeParameters.stateData,
  361 + comparisonEnabled: widgetContext.settings.comparisonEnabled,
  362 + timeForComparison: widgetContext.settings.timeForComparison
  363 + };
362 364 if (widget.type == types.widgetType.alarm.value) {
363 365 options.alarmSource = angular.copy(widget.config.alarmSource);
364 366 options.alarmSearchStatus = angular.isDefined(widget.config.alarmSearchStatus) ?
... ...
... ... @@ -1195,7 +1195,13 @@
1195 1195 "min": "min",
1196 1196 "max": "max",
1197 1197 "avg": "avg",
1198   - "total": "total"
  1198 + "total": "total",
  1199 + "comparison-time-ago": {
  1200 + "days": "(day ago)",
  1201 + "weeks": "(week ago)",
  1202 + "months": "(month ago)",
  1203 + "years": "(year ago)"
  1204 + }
1199 1205 },
1200 1206 "login": {
1201 1207 "login": "Login",
... ...
... ... @@ -1169,7 +1169,13 @@
1169 1169 "min": "Мин",
1170 1170 "max": "Макс",
1171 1171 "avg": "Среднее",
1172   - "total": "Сумма"
  1172 + "total": "Сумма",
  1173 + "comparison-time-ago": {
  1174 + "days": "(день назад)",
  1175 + "weeks": "(неделю назад)",
  1176 + "months": "(месяц назад)",
  1177 + "years": "(год назад)"
  1178 + }
1173 1179 },
1174 1180 "login": {
1175 1181 "login": "Войти",
... ...
... ... @@ -1584,7 +1584,13 @@
1584 1584 "min": "мін",
1585 1585 "max": "макс",
1586 1586 "avg": "середнє",
1587   - "total": "Сума"
  1587 + "total": "Сума",
  1588 + "comparison-time-ago": {
  1589 + "days": "(день тому)",
  1590 + "weeks": "(тиждень тому)",
  1591 + "months": "(місяць тому)",
  1592 + "years": "(рік тому)"
  1593 + }
1588 1594 },
1589 1595 "login": {
1590 1596 "login": "Вхід",
... ...
... ... @@ -23,6 +23,7 @@ import 'flot/src/plugins/jquery.flot.selection';
23 23 import 'flot/src/plugins/jquery.flot.pie';
24 24 import 'flot/src/plugins/jquery.flot.crosshair';
25 25 import 'flot/src/plugins/jquery.flot.stack';
  26 +import 'flot/src/plugins/jquery.flot.symbol';
26 27 import 'flot.curvedlines/curvedLines';
27 28
28 29 /* eslint-disable angular/angularelement */
... ... @@ -32,6 +33,7 @@ export default class TbFlot {
32 33 this.ctx = ctx;
33 34 this.chartType = chartType || 'line';
34 35 var settings = ctx.settings;
  36 + var utils = this.ctx.$scope.$injector.get('utils');
35 37
36 38 ctx.tooltip = $('#flot-series-tooltip');
37 39 if (ctx.tooltip.length === 0) {
... ... @@ -59,7 +61,7 @@ export default class TbFlot {
59 61 divElement.css({
60 62 display: "flex",
61 63 alignItems: "center",
62   - justifyContent: "flex-start"
  64 + justifyContent: "space-between"
63 65 });
64 66 var lineSpan = $('<span></span>');
65 67 lineSpan.css({
... ... @@ -125,55 +127,99 @@ export default class TbFlot {
125 127 } else {
126 128 ctx.tooltipFormatter = function(hoverInfo, seriesIndex) {
127 129 var content = '';
128   - var timestamp = parseInt(hoverInfo.time);
129   - var date = moment(timestamp).format('YYYY-MM-DD HH:mm:ss');
130   - var dateDiv = $('<div>' + date + '</div>');
131   - dateDiv.css({
132   - display: "flex",
133   - alignItems: "center",
134   - justifyContent: "center",
135   - padding: "4px",
136   - fontWeight: "700"
137   - });
138   - content += dateDiv.prop('outerHTML');
  130 +
139 131 if (tbFlot.ctx.tooltipIndividual) {
140   - var found = hoverInfo.seriesHover.filter((seriesHover) => {
  132 + var seriesHoverArray;
  133 + if (hoverInfo[1] && hoverInfo[1].seriesHover.length) {
  134 + seriesHoverArray = hoverInfo[0].seriesHover.concat(hoverInfo[1].seriesHover);
  135 + } else {
  136 + seriesHoverArray = hoverInfo[0].seriesHover;
  137 + }
  138 + var found = seriesHoverArray.filter((seriesHover) => {
141 139 return seriesHover.index === seriesIndex;
142 140 });
143 141 if (found && found.length) {
  142 + let timestamp;
  143 + if (!angular.isNumber(hoverInfo[0].time) || (found[0].time < hoverInfo[0].time)) {
  144 + timestamp = parseInt(hoverInfo[1].time);
  145 + } else {
  146 + timestamp = parseInt(hoverInfo[0].time);
  147 + }
  148 + let date = moment(timestamp).format('YYYY-MM-DD HH:mm:ss');
  149 + let dateDiv = $('<div>' + date + '</div>');
  150 + dateDiv.css({
  151 + display: "flex",
  152 + alignItems: "center",
  153 + justifyContent: "center",
  154 + padding: "4px",
  155 + fontWeight: "700"
  156 + });
  157 + content += dateDiv.prop('outerHTML');
144 158 content += seriesInfoDivFromInfo(found[0], seriesIndex);
145 159 }
146 160 } else {
147   - var seriesDiv = $('<div></div>');
148   - seriesDiv.css({
149   - display: "flex",
150   - flexDirection: "row"
151   - });
152   - const maxRows = 15;
153   - var columns = Math.ceil(hoverInfo.seriesHover.length / maxRows);
154   - var columnsContent = '';
155   - for (var c = 0; c < columns; c++) {
156   - var columnDiv = $('<div></div>');
157   - columnDiv.css({
158   - display: "flex",
159   - flexDirection: "column"
160   - });
161   - var columnContent = '';
162   - for (var i = c*maxRows; i < (c+1)*maxRows; i++) {
163   - if (i == hoverInfo.seriesHover.length) {
164   - break;
  161 + var maxRows;
  162 + if (hoverInfo[1] && hoverInfo[1].seriesHover.length) {
  163 + maxRows = 5;
  164 + } else {
  165 + maxRows = 15;
  166 + }
  167 + var columns = 0;
  168 + if (hoverInfo[1] && (hoverInfo[1].seriesHover.length > hoverInfo[0].seriesHover.length)) {
  169 + columns = Math.ceil(hoverInfo[1].seriesHover.length / maxRows);
  170 + } else {
  171 + columns = Math.ceil(hoverInfo[0].seriesHover.length / maxRows);
  172 + }
  173 +
  174 + for (var j = 0; j < hoverInfo.length; j++) {
  175 + var hoverData = hoverInfo[j];
  176 + if (angular.isNumber(hoverData.time)) {
  177 + var columnsContent = '';
  178 + let timestamp = parseInt(hoverData.time);
  179 + let date = moment(timestamp).format('YYYY-MM-DD HH:mm:ss');
  180 + let dateDiv = $('<div>' + date + '</div>');
  181 + dateDiv.css({
  182 + display: "flex",
  183 + alignItems: "center",
  184 + justifyContent: "center",
  185 + padding: "4px",
  186 + fontWeight: "700"
  187 + });
  188 + content += dateDiv.prop('outerHTML');
  189 +
  190 + var seriesDiv = $('<div></div>');
  191 + seriesDiv.css({
  192 + display: "flex",
  193 + flexDirection: "row"
  194 + });
  195 + for (var c = 0; c < columns; c++) {
  196 + var columnDiv = $('<div></div>');
  197 + columnDiv.css({
  198 + display: "flex",
  199 + flexDirection: "column",
  200 + flex: "1"
  201 + });
  202 + var columnContent = '';
  203 + for (var i = c*maxRows; i < (c+1)*maxRows; i++) {
  204 + if (i >= hoverData.seriesHover.length) {
  205 + break;
  206 + }
  207 + var seriesHoverInfo = hoverData.seriesHover[i];
  208 + columnContent += seriesInfoDivFromInfo(seriesHoverInfo, seriesIndex);
  209 + }
  210 + columnDiv.html(columnContent);
  211 +
  212 + if (columnContent) {
  213 + if (c > 0) {
  214 + columnsContent += '<span style="min-width: 20px;"></span>';
  215 + }
  216 + columnsContent += columnDiv.prop('outerHTML');
  217 + }
165 218 }
166   - var seriesHoverInfo = hoverInfo.seriesHover[i];
167   - columnContent += seriesInfoDivFromInfo(seriesHoverInfo, seriesIndex);
168   - }
169   - columnDiv.html(columnContent);
170   - if (c > 0) {
171   - columnsContent += '<span style="width: 20px;"></span>';
  219 + seriesDiv.html(columnsContent);
  220 + content += seriesDiv.prop('outerHTML');
172 221 }
173   - columnsContent += columnDiv.prop('outerHTML');
174 222 }
175   - seriesDiv.html(columnsContent);
176   - content += seriesDiv.prop('outerHTML');
177 223 }
178 224 return content;
179 225 };
... ... @@ -185,6 +231,7 @@ export default class TbFlot {
185 231
186 232 ctx.tooltipIndividual = this.chartType === 'pie' || (angular.isDefined(settings.tooltipIndividual) ? settings.tooltipIndividual : false);
187 233 ctx.tooltipCumulative = angular.isDefined(settings.tooltipCumulative) ? settings.tooltipCumulative : false;
  234 + ctx.hideZeros = angular.isDefined(settings.hideZeros) ? settings.hideZeros : false;
188 235
189 236 var font = {
190 237 color: settings.fontColor || "#545454",
... ... @@ -209,7 +256,8 @@ export default class TbFlot {
209 256 };
210 257
211 258 if (this.chartType === 'line' || this.chartType === 'bar' || this.chartType === 'state') {
212   - options.xaxis = {
  259 + options.xaxes = [];
  260 + this.xaxis = {
213 261 mode: 'time',
214 262 timezone: 'browser',
215 263 font: angular.copy(font),
... ... @@ -220,16 +268,11 @@ export default class TbFlot {
220 268 labelFont: angular.copy(font)
221 269 };
222 270 if (settings.xaxis) {
223   - if (settings.xaxis.showLabels === false) {
224   - options.xaxis.tickFormatter = function() {
225   - return '';
226   - };
227   - }
228   - options.xaxis.font.color = settings.xaxis.color || options.xaxis.font.color;
229   - options.xaxis.label = settings.xaxis.title || null;
230   - options.xaxis.labelFont.color = options.xaxis.font.color;
231   - options.xaxis.labelFont.size = options.xaxis.font.size+2;
232   - options.xaxis.labelFont.weight = "bold";
  271 + this.xaxis.font.color = settings.xaxis.color || this.xaxis.font.color;
  272 + this.xaxis.label = utils.customTranslation(settings.xaxis.title, settings.xaxis.title) || null;
  273 + this.xaxis.labelFont.color = this.xaxis.font.color;
  274 + this.xaxis.labelFont.size = this.xaxis.font.size+2;
  275 + this.xaxis.labelFont.weight = "bold";
233 276 }
234 277
235 278 ctx.yAxisTickFormatter = function(value/*, axis*/) {
... ... @@ -263,7 +306,7 @@ export default class TbFlot {
263 306 this.yaxis.font.color = settings.yaxis.color || this.yaxis.font.color;
264 307 this.yaxis.min = angular.isDefined(settings.yaxis.min) ? settings.yaxis.min : null;
265 308 this.yaxis.max = angular.isDefined(settings.yaxis.max) ? settings.yaxis.max : null;
266   - this.yaxis.label = settings.yaxis.title || null;
  309 + this.yaxis.label = utils.customTranslation(settings.yaxis.title, settings.yaxis.title) || null;
267 310 this.yaxis.labelFont.color = this.yaxis.font.color;
268 311 this.yaxis.labelFont.size = this.yaxis.font.size+2;
269 312 this.yaxis.labelFont.weight = "bold";
... ... @@ -296,7 +339,7 @@ export default class TbFlot {
296 339 options.grid.borderWidth = angular.isDefined(settings.grid.outlineWidth) ?
297 340 settings.grid.outlineWidth : 1;
298 341 if (settings.grid.verticalLines === false) {
299   - options.xaxis.tickLength = 0;
  342 + this.xaxis.tickLength = 0;
300 343 }
301 344 if (settings.grid.horizontalLines === false) {
302 345 this.yaxis.tickLength = 0;
... ... @@ -309,14 +352,42 @@ export default class TbFlot {
309 352 }
310 353 }
311 354
312   - options.crosshair = {
313   - mode: 'x'
  355 + options.xaxes[0] = angular.copy(this.xaxis);
  356 +
  357 + if (settings.xaxis && settings.xaxis.showLabels === false) {
  358 + options.xaxes[0].tickFormatter = function() {
  359 + return '';
  360 + };
314 361 }
315 362
316   - options.series = {
317   - stack: settings.stack === true
  363 + if (settings.comparisonEnabled) {
  364 + var xaxis = angular.copy(this.xaxis);
  365 + xaxis.position = 'top';
  366 + if (settings.xaxisSecond) {
  367 + if (settings.xaxisSecond.showLabels === false) {
  368 + xaxis.tickFormatter = function() {
  369 + return '';
  370 + };
  371 + }
  372 + xaxis.label = utils.customTranslation(settings.xaxisSecond.title, settings.xaxisSecond.title) || null;
  373 + xaxis.position = settings.xaxisSecond.axisPosition;
  374 + }
  375 + xaxis.tickLength = 0;
  376 + options.xaxes.push(xaxis);
  377 +
  378 + options.series = {
  379 + stack: false
  380 + };
  381 + } else {
  382 + options.series = {
  383 + stack: settings.stack === true
  384 + };
318 385 }
319 386
  387 + options.crosshair = {
  388 + mode: 'x'
  389 + };
  390 +
320 391 if (this.chartType === 'line' && settings.smoothLines) {
321 392 options.series.curvedLines = {
322 393 active: true,
... ... @@ -429,6 +500,13 @@ export default class TbFlot {
429 500 series.lines = {
430 501 fill: keySettings.fillLines === true
431 502 };
  503 +
  504 + if (this.ctx.settings.stack && !this.ctx.settings.comparisonEnabled) {
  505 + series.stack = !keySettings.excludeFromStacking;
  506 + } else {
  507 + series.stack = false;
  508 + }
  509 +
432 510 if (this.chartType === 'line' || this.chartType === 'state') {
433 511 series.lines.show = keySettings.showLines !== false
434 512 } else {
... ... @@ -445,14 +523,23 @@ export default class TbFlot {
445 523 };
446 524 if (keySettings.showPoints === true) {
447 525 series.points.show = true;
448   - series.points.lineWidth = 5;
  526 + series.points.lineWidth = angular.isDefined(keySettings.showPointsLineWidth) ? keySettings.showPointsLineWidth : 5;
449 527 series.points.radius = angular.isDefined(keySettings.showPointsRadius) ? keySettings.showPointsRadius : 3;
  528 + series.points.symbol = angular.isDefined(keySettings.showPointShape) ? keySettings.showPointShape : 'circle';
  529 + if (series.points.symbol == 'custom' && keySettings.pointShapeFormatter) {
  530 + try {
  531 + series.points.symbol = new Function('ctx, x, y, radius, shadow', keySettings.pointShapeFormatter);
  532 + } catch (e) {
  533 + series.points.symbol = 'circle';
  534 + }
  535 + }
  536 +
450 537 }
451 538
452 539 if (this.chartType === 'line' && this.ctx.settings.smoothLines && !series.points.show) {
453 540 series.curvedLines = {
454 541 apply: true
455   - }
  542 + };
456 543 }
457 544
458 545 var lineColor = tinycolor(series.dataKey.color);
... ... @@ -460,6 +547,15 @@ export default class TbFlot {
460 547
461 548 series.highlightColor = lineColor.toRgbString();
462 549
  550 + if (series.datasource.isAdditional) {
  551 + series.xaxisIndex = 1;
  552 + series.xaxis = 2;
  553 + } else {
  554 + series.xaxisIndex = 0;
  555 + series.xaxis = 1;
  556 + }
  557 +
  558 +
463 559 if (this.yaxis) {
464 560 var units = series.dataKey.units && series.dataKey.units.length ? series.dataKey.units : this.ctx.trackUnits;
465 561 var yaxis;
... ... @@ -477,7 +573,7 @@ export default class TbFlot {
477 573 series.yaxisIndex = this.yaxes.indexOf(yaxis);
478 574 series.yaxis = series.yaxisIndex+1;
479 575 yaxis.keysInfo[i] = {hidden: false};
480   - yaxis.hidden = false;
  576 + yaxis.show = true;
481 577 }
482 578 }
483 579
... ... @@ -491,8 +587,12 @@ export default class TbFlot {
491 587 this.options.series.bars.barWidth = this.subscription.timeWindow.interval * 0.6;
492 588 }
493 589 }
494   - this.options.xaxis.min = this.subscription.timeWindow.minTime;
495   - this.options.xaxis.max = this.subscription.timeWindow.maxTime;
  590 + this.options.xaxes[0].min = this.subscription.timeWindow.minTime;
  591 + this.options.xaxes[0].max = this.subscription.timeWindow.maxTime;
  592 + if (this.ctx.settings.comparisonEnabled) {
  593 + this.options.xaxes[1].min = this.subscription.comparisonTimeWindow.minTime;
  594 + this.options.xaxes[1].max = this.subscription.comparisonTimeWindow.maxTime;
  595 + }
496 596 }
497 597
498 598 this.checkMouseEvents();
... ... @@ -500,6 +600,7 @@ export default class TbFlot {
500 600 if (this.ctx.plot) {
501 601 this.ctx.plot.destroy();
502 602 }
  603 +
503 604 if (this.chartType === 'pie' && this.ctx.animatedPie) {
504 605 this.ctx.pieDataAnimationDuration = 250;
505 606 this.pieData = angular.copy(this.subscription.data);
... ... @@ -608,8 +709,12 @@ export default class TbFlot {
608 709 }
609 710 }
610 711
611   - this.options.xaxis.min = this.subscription.timeWindow.minTime;
612   - this.options.xaxis.max = this.subscription.timeWindow.maxTime;
  712 + this.options.xaxes[0].min = this.subscription.timeWindow.minTime;
  713 + this.options.xaxes[0].max = this.subscription.timeWindow.maxTime;
  714 + if (this.ctx.settings.comparisonEnabled) {
  715 + this.options.xaxes[1].min = this.subscription.comparisonTimeWindow.minTime;
  716 + this.options.xaxes[1].max = this.subscription.comparisonTimeWindow.maxTime;
  717 + }
613 718 if (this.chartType === 'bar') {
614 719 if (this.subscription.timeWindowConfig.aggregation && this.subscription.timeWindowConfig.aggregation.type === 'NONE') {
615 720 this.options.series.bars.barWidth = this.ctx.defaultBarWidth;
... ... @@ -623,6 +728,10 @@ export default class TbFlot {
623 728 } else {
624 729 this.ctx.plot.getOptions().xaxes[0].min = this.subscription.timeWindow.minTime;
625 730 this.ctx.plot.getOptions().xaxes[0].max = this.subscription.timeWindow.maxTime;
  731 + if (this.ctx.settings.comparisonEnabled) {
  732 + this.ctx.plot.getOptions().xaxes[1].min = this.subscription.comparisonTimeWindow.minTime;
  733 + this.ctx.plot.getOptions().xaxes[1].max = this.subscription.comparisonTimeWindow.maxTime;
  734 + }
626 735 if (this.chartType === 'bar') {
627 736 if (this.subscription.timeWindowConfig.aggregation && this.subscription.timeWindowConfig.aggregation.type === 'NONE') {
628 737 this.ctx.plot.getOptions().series.bars.barWidth = this.ctx.defaultBarWidth;
... ... @@ -902,6 +1011,11 @@ export default class TbFlot {
902 1011 "type": "string",
903 1012 "default": ""
904 1013 };
  1014 + properties["hideZeros"] = {
  1015 + "title": "Hide zero/false values from tooltip",
  1016 + "type": "boolean",
  1017 + "default": false
  1018 + };
905 1019
906 1020 properties["grid"] = {
907 1021 "title": "Grid settings",
... ... @@ -1029,6 +1143,7 @@ export default class TbFlot {
1029 1143 "key": "tooltipValueFormatter",
1030 1144 "type": "javascript"
1031 1145 });
  1146 + schema["form"].push("hideZeros");
1032 1147 schema["form"].push({
1033 1148 "key": "grid",
1034 1149 "items": [
... ... @@ -1079,6 +1194,21 @@ export default class TbFlot {
1079 1194 }
1080 1195 ]
1081 1196 });
  1197 + if (chartType === 'graph' || chartType === 'bar') {
  1198 + schema.groupInfoes = [{
  1199 + "formIndex":0,
  1200 + "GroupTitle":"Common Settings"
  1201 + }];
  1202 + schema.form = [schema.form];
  1203 + angular.merge(schema.schema.properties, chartSettingsSchemaForComparison.schema.properties);
  1204 + schema.schema.required = schema.schema.required.concat(chartSettingsSchemaForComparison.schema.required);
  1205 + schema.form.push(chartSettingsSchemaForComparison.form);
  1206 + schema.groupInfoes.push({
  1207 + "formIndex":schema.groupInfoes.length,
  1208 + "GroupTitle":"Comparison Settings"
  1209 + });
  1210 + }
  1211 +
1082 1212 return schema;
1083 1213 }
1084 1214
... ... @@ -1086,12 +1216,18 @@ export default class TbFlot {
1086 1216 return {}
1087 1217 }
1088 1218
1089   - static datakeySettingsSchema(defaultShowLines) {
1090   - return {
  1219 + static datakeySettingsSchema(defaultShowLines, chartType) {
  1220 +
  1221 + var schema = {
1091 1222 "schema": {
1092 1223 "type": "object",
1093 1224 "title": "DataKeySettings",
1094 1225 "properties": {
  1226 + "excludeFromStacking": {
  1227 + "title": "Exclude from stacking(available in \"Stacking\" mode)",
  1228 + "type": "boolean",
  1229 + "default": false
  1230 + },
1095 1231 "showLines": {
1096 1232 "title": "Show lines",
1097 1233 "type": "boolean",
... ... @@ -1107,6 +1243,25 @@ export default class TbFlot {
1107 1243 "type": "boolean",
1108 1244 "default": false
1109 1245 },
  1246 + "showPointShape": {
  1247 + "title": "Select point shape:",
  1248 + "type": "string",
  1249 + "default": "circle"
  1250 + },
  1251 + "pointShapeFormatter": {
  1252 + "title": "Point shape format function, f(ctx, x, y, radius, shadow)",
  1253 + "type": "string",
  1254 + "default": "var size = radius * Math.sqrt(Math.PI) / 2;\n" +
  1255 + "ctx.moveTo(x - size, y - size);\n" +
  1256 + "ctx.lineTo(x + size, y + size);\n" +
  1257 + "ctx.moveTo(x - size, y + size);\n" +
  1258 + "ctx.lineTo(x + size, y - size);"
  1259 + },
  1260 + "showPointsLineWidth": {
  1261 + "title": "Line width of points",
  1262 + "type": "number",
  1263 + "default": 5
  1264 + },
1110 1265 "showPointsRadius": {
1111 1266 "title": "Radius of points",
1112 1267 "type": "number",
... ... @@ -1161,9 +1316,46 @@ export default class TbFlot {
1161 1316 "required": ["showLines", "fillLines", "showPoints"]
1162 1317 },
1163 1318 "form": [
  1319 + "excludeFromStacking",
1164 1320 "showLines",
1165 1321 "fillLines",
1166 1322 "showPoints",
  1323 + {
  1324 + "key": "showPointShape",
  1325 + "type": "rc-select",
  1326 + "multiple": false,
  1327 + "items": [
  1328 + {
  1329 + "value": "circle",
  1330 + "label": "Circle"
  1331 + },
  1332 + {
  1333 + "value": "cross",
  1334 + "label": "Cross"
  1335 + },
  1336 + {
  1337 + "value": "diamond",
  1338 + "label": "Diamond"
  1339 + },
  1340 + {
  1341 + "value": "square",
  1342 + "label": "Square"
  1343 + },
  1344 + {
  1345 + "value": "triangle",
  1346 + "label": "Triangle"
  1347 + },
  1348 + {
  1349 + "value": "custom",
  1350 + "label": "Custom function"
  1351 + }
  1352 + ]
  1353 + },
  1354 + {
  1355 + "key": "pointShapeFormatter",
  1356 + "type": "javascript"
  1357 + },
  1358 + "showPointsLineWidth",
1167 1359 "showPointsRadius",
1168 1360 {
1169 1361 "key": "tooltipValueFormatter",
... ... @@ -1195,7 +1387,47 @@ export default class TbFlot {
1195 1387 "type": "javascript"
1196 1388 }
1197 1389 ]
  1390 + };
  1391 +
  1392 + var properties = schema["schema"]["properties"];
  1393 + if (chartType === 'graph' || chartType === 'bar') {
  1394 + properties["comparisonSettings"] = {
  1395 + "title": "Comparison Settings",
  1396 + "type": "object",
  1397 + "properties": {
  1398 + "showValuesForComparison": {
  1399 + "title": "Show historical values for comparison",
  1400 + "type": "boolean",
  1401 + "default": true
  1402 + },
  1403 + "comparisonValuesLabel": {
  1404 + "title": "Historical values label",
  1405 + "type": "string",
  1406 + "default": ""
  1407 + },
  1408 + "color": {
  1409 + "title": "Color",
  1410 + "type": "string",
  1411 + "default": ""
  1412 + }
  1413 + },
  1414 + "required": ["showValuesForComparison"]
  1415 + };
  1416 + schema["form"].push({
  1417 + "key": "comparisonSettings",
  1418 + "items": [
  1419 + "comparisonSettings.showValuesForComparison",
  1420 + "comparisonSettings.comparisonValuesLabel",
  1421 + {
  1422 + "key": "comparisonSettings.color",
  1423 + "type": "color"
  1424 + }
  1425 + ]
  1426 + });
  1427 +
1198 1428 }
  1429 +
  1430 + return schema;
1199 1431 }
1200 1432
1201 1433 enableMouseEvents() {
... ... @@ -1227,10 +1459,15 @@ export default class TbFlot {
1227 1459 tooltipHtml = tbFlot.ctx.tooltipFormatter(item);
1228 1460 } else {
1229 1461 var hoverInfo = tbFlot.getHoverInfo(tbFlot.ctx.plot.getData(), pos);
1230   - if (angular.isNumber(hoverInfo.time)) {
1231   - hoverInfo.seriesHover.sort(function (a, b) {
  1462 + if (angular.isNumber(hoverInfo[0].time) || (hoverInfo[1] && angular.isNumber(hoverInfo[1].time))) {
  1463 + hoverInfo[0].seriesHover.sort(function (a, b) {
1232 1464 return b.value - a.value;
1233 1465 });
  1466 + if (hoverInfo[1] && hoverInfo[1].seriesHover.length) {
  1467 + hoverInfo[1].seriesHover.sort(function (a, b) {
  1468 + return b.value - a.value;
  1469 + });
  1470 + }
1234 1471 tooltipHtml = tbFlot.ctx.tooltipFormatter(hoverInfo, item ? item.seriesIndex : -1);
1235 1472 }
1236 1473 }
... ... @@ -1258,9 +1495,11 @@ export default class TbFlot {
1258 1495 });
1259 1496
1260 1497 if (multipleModeTooltip) {
1261   - for (var i = 0; i < hoverInfo.seriesHover.length; i++) {
1262   - var seriesHoverInfo = hoverInfo.seriesHover[i];
1263   - tbFlot.ctx.plot.highlight(seriesHoverInfo.index, seriesHoverInfo.hoverIndex);
  1498 + for (var j = 0; j < hoverInfo.length; j++) {
  1499 + for (var i = 0; i < hoverInfo[j].seriesHover.length; i++) {
  1500 + var seriesHoverInfo = hoverInfo[j].seriesHover[i];
  1501 + tbFlot.ctx.plot.highlight(seriesHoverInfo.index, seriesHoverInfo.hoverIndex);
  1502 + }
1264 1503 }
1265 1504 }
1266 1505 }
... ... @@ -1399,19 +1638,38 @@ export default class TbFlot {
1399 1638
1400 1639
1401 1640 getHoverInfo (seriesList, pos) {
1402   - var i, series, value, hoverIndex, hoverDistance, pointTime, minDistance, minTime;
  1641 + var i, series, value, hoverIndex, hoverDistance, pointTime, minDistance, minTime, hoverData;
1403 1642 var last_value = 0;
1404   - var results = {
  1643 + var results = [{
1405 1644 seriesHover: []
1406   - };
  1645 + }];
  1646 + if (this.ctx.settings.comparisonEnabled) {
  1647 + results.push({
  1648 + seriesHover: []
  1649 + });
  1650 + var minDistanceHistorical, minTimeHistorical;
  1651 + }
1407 1652 for (i = 0; i < seriesList.length; i++) {
1408 1653 series = seriesList[i];
1409   - hoverIndex = this.findHoverIndexFromData(pos.x, series);
  1654 + var posx;
  1655 + if (series.datasource.isAdditional) {
  1656 + posx = pos.x2;
  1657 + } else {
  1658 + posx = pos.x;
  1659 + }
  1660 + hoverIndex = this.findHoverIndexFromData(posx, series);
1410 1661 if (series.data[hoverIndex] && series.data[hoverIndex][0]) {
1411   - hoverDistance = pos.x - series.data[hoverIndex][0];
  1662 + hoverDistance = posx - series.data[hoverIndex][0];
1412 1663 pointTime = series.data[hoverIndex][0];
1413 1664
1414   - if (!minDistance
  1665 + if (series.datasource.isAdditional) {
  1666 + if (!minDistanceHistorical
  1667 + || (hoverDistance >= 0 && (hoverDistance < minDistanceHistorical || minDistanceHistorical < 0))
  1668 + || (hoverDistance < 0 && hoverDistance > minDistanceHistorical)) {
  1669 + minDistanceHistorical = hoverDistance;
  1670 + minTimeHistorical = pointTime;
  1671 + }
  1672 + } else if (!minDistance
1415 1673 || (hoverDistance >= 0 && (hoverDistance < minDistance || minDistance < 0))
1416 1674 || (hoverDistance < 0 && hoverDistance > minDistance)) {
1417 1675 minDistance = hoverDistance;
... ... @@ -1429,23 +1687,33 @@ export default class TbFlot {
1429 1687 }
1430 1688
1431 1689 if (series.stack || (series.curvedLines && series.curvedLines.apply)) {
1432   - hoverIndex = this.findHoverIndexFromDataPoints(pos.x, series, hoverIndex);
  1690 + hoverIndex = this.findHoverIndexFromDataPoints(posx, series, hoverIndex);
  1691 + }
  1692 + if (!this.ctx.hideZeros || value) {
  1693 + hoverData = {
  1694 + value: value,
  1695 + hoverIndex: hoverIndex,
  1696 + color: series.dataKey.color,
  1697 + label: series.dataKey.label,
  1698 + units: series.dataKey.units,
  1699 + decimals: series.dataKey.decimals,
  1700 + tooltipValueFormatFunction: series.dataKey.tooltipValueFormatFunction,
  1701 + time: pointTime,
  1702 + distance: hoverDistance,
  1703 + index: i
  1704 + };
  1705 + if (series.datasource.isAdditional) {
  1706 + results[1].seriesHover.push(hoverData);
  1707 + } else {
  1708 + results[0].seriesHover.push(hoverData);
  1709 + }
1433 1710 }
1434   - results.seriesHover.push({
1435   - value: value,
1436   - hoverIndex: hoverIndex,
1437   - color: series.dataKey.color,
1438   - label: series.dataKey.label,
1439   - units: series.dataKey.units,
1440   - decimals: series.dataKey.decimals,
1441   - tooltipValueFormatFunction: series.dataKey.tooltipValueFormatFunction,
1442   - time: pointTime,
1443   - distance: hoverDistance,
1444   - index: i
1445   - });
1446 1711 }
1447 1712 }
1448   - results.time = minTime;
  1713 + if (results[1] && results[1].seriesHover.length) {
  1714 + results[1].time = minTimeHistorical;
  1715 + }
  1716 + results[0].time = minTime;
1449 1717 return results;
1450 1718 }
1451 1719
... ... @@ -1524,4 +1792,93 @@ export default class TbFlot {
1524 1792 }
1525 1793 }
1526 1794
  1795 +const chartSettingsSchemaForComparison = {
  1796 + "schema": {
  1797 + "title": "Comparison Settings",
  1798 + "type": "object",
  1799 + "properties": {
  1800 + "comparisonEnabled": {
  1801 + "title": "Enable comparison",
  1802 + "type": "boolean",
  1803 + "default": false
  1804 + },
  1805 + "timeForComparison": {
  1806 + "title": "Time to show historical data",
  1807 + "type": "string",
  1808 + "default": "months"
  1809 + },
  1810 + "xaxisSecond": {
  1811 + "title": "Second X axis",
  1812 + "type": "object",
  1813 + "properties": {
  1814 + "axisPosition": {
  1815 + "title": "Axis position",
  1816 + "type": "string",
  1817 + "default": "top"
  1818 + },
  1819 + "showLabels": {
  1820 + "title": "Show labels",
  1821 + "type": "boolean",
  1822 + "default": true
  1823 + },
  1824 + "title": {
  1825 + "title": "Axis title",
  1826 + "type": "string",
  1827 + "default": null
  1828 + }
  1829 + }
  1830 + }
  1831 + },
  1832 + "required": []
  1833 + },
  1834 + "form": [
  1835 + "comparisonEnabled",
  1836 + {
  1837 + "key": "timeForComparison",
  1838 + "type": "rc-select",
  1839 + "multiple": false,
  1840 + "items": [
  1841 + {
  1842 + "value": "days",
  1843 + "label": "Day ago"
  1844 + },
  1845 + {
  1846 + "value": "weeks",
  1847 + "label": "Week ago"
  1848 + },
  1849 + {
  1850 + "value": "months",
  1851 + "label": "Month ago (default)"
  1852 + },
  1853 + {
  1854 + "value": "years",
  1855 + "label": "Year ago"
  1856 + }
  1857 + ]
  1858 + },
  1859 + {
  1860 + "key": "xaxisSecond",
  1861 + "items": [
  1862 + {
  1863 + "key": "xaxisSecond.axisPosition",
  1864 + "type": "rc-select",
  1865 + "multiple": false,
  1866 + "items": [
  1867 + {
  1868 + "value": "top",
  1869 + "label": "Top (default)"
  1870 + },
  1871 + {
  1872 + "value": "bottom",
  1873 + "label": "Bottom"
  1874 + }
  1875 + ]
  1876 + },
  1877 + "xaxisSecond.showLabels",
  1878 + "xaxisSecond.title",
  1879 + ]
  1880 + }
  1881 + ]
  1882 +};
  1883 +
1527 1884 /* eslint-enable angular/angularelement */
... ...