Commit 2f11d4336f03ef461ac3fda43ad420a216ae6387

Authored by Igor Kulikov
1 parent eda23bbf

Merge data comparison feature

@@ -55,6 +55,7 @@ @@ -55,6 +55,7 @@
55 "node_modules/flot/src/plugins/jquery.flot.pie.js", 55 "node_modules/flot/src/plugins/jquery.flot.pie.js",
56 "node_modules/flot/src/plugins/jquery.flot.crosshair.js", 56 "node_modules/flot/src/plugins/jquery.flot.crosshair.js",
57 "node_modules/flot/src/plugins/jquery.flot.stack.js", 57 "node_modules/flot/src/plugins/jquery.flot.stack.js",
  58 + "node_modules/flot/src/plugins/jquery.flot.symbol.js",
58 "node_modules/flot.curvedlines/curvedLines.js", 59 "node_modules/flot.curvedlines/curvedLines.js",
59 "node_modules/tinycolor2/dist/tinycolor-min.js", 60 "node_modules/tinycolor2/dist/tinycolor-min.js",
60 "node_modules/tooltipster/dist/js/tooltipster.bundle.min.js", 61 "node_modules/tooltipster/dist/js/tooltipster.bundle.min.js",
@@ -17,12 +17,15 @@ @@ -17,12 +17,15 @@
17 import { Observable } from 'rxjs'; 17 import { Observable } from 'rxjs';
18 import { EntityId } from '@app/shared/models/id/entity-id'; 18 import { EntityId } from '@app/shared/models/id/entity-id';
19 import { 19 import {
20 - WidgetActionDescriptor,  
21 - widgetType, 20 + DataSet,
  21 + Datasource,
  22 + DatasourceData,
  23 + DatasourceType,
  24 + KeyInfo,
22 LegendConfig, 25 LegendConfig,
23 LegendData, 26 LegendData,
24 - Datasource,  
25 - DatasourceData, DataSet, DatasourceType, KeyInfo 27 + WidgetActionDescriptor,
  28 + widgetType
26 } from '@shared/models/widget.models'; 29 } from '@shared/models/widget.models';
27 import { TimeService } from '../services/time.service'; 30 import { TimeService } from '../services/time.service';
28 import { DeviceService } from '../http/device.service'; 31 import { DeviceService } from '../http/device.service';
@@ -36,10 +39,8 @@ import { DatasourceService } from '@core/api/datasource.service'; @@ -36,10 +39,8 @@ import { DatasourceService } from '@core/api/datasource.service';
36 import { RafService } from '@core/services/raf.service'; 39 import { RafService } from '@core/services/raf.service';
37 import { EntityAliases } from '@shared/models/alias.models'; 40 import { EntityAliases } from '@shared/models/alias.models';
38 import { EntityInfo } from '@app/shared/models/entity.models'; 41 import { EntityInfo } from '@app/shared/models/entity.models';
39 -import { Type } from '@angular/core';  
40 -import { AssetService } from '@core/http/asset.service';  
41 -import { DialogService } from '@core/services/dialog.service';  
42 import { IDashboardComponent } from '@home/models/dashboard-component.models'; 42 import { IDashboardComponent } from '@home/models/dashboard-component.models';
  43 +import * as moment_ from 'moment';
43 44
44 export interface TimewindowFunctions { 45 export interface TimewindowFunctions {
45 onUpdateTimewindow: (startTimeMs: number, endTimeMs: number, interval?: number) => void; 46 onUpdateTimewindow: (startTimeMs: number, endTimeMs: number, interval?: number) => void;
@@ -200,6 +201,8 @@ export interface WidgetSubscriptionOptions { @@ -200,6 +201,8 @@ export interface WidgetSubscriptionOptions {
200 timeWindowConfig?: Timewindow; 201 timeWindowConfig?: Timewindow;
201 dashboardTimewindow?: Timewindow; 202 dashboardTimewindow?: Timewindow;
202 legendConfig?: LegendConfig; 203 legendConfig?: LegendConfig;
  204 + comparisonEnabled?: boolean;
  205 + timeForComparison?: moment_.unitOfTime.DurationConstructor;
203 decimals?: number; 206 decimals?: number;
204 units?: string; 207 units?: string;
205 callbacks?: WidgetSubscriptionCallbacks; 208 callbacks?: WidgetSubscriptionCallbacks;
@@ -227,6 +230,7 @@ export interface IWidgetSubscription { @@ -227,6 +230,7 @@ export interface IWidgetSubscription {
227 hiddenData?: Array<{data: DataSet}>; 230 hiddenData?: Array<{data: DataSet}>;
228 timeWindowConfig?: Timewindow; 231 timeWindowConfig?: Timewindow;
229 timeWindow?: WidgetTimewindow; 232 timeWindow?: WidgetTimewindow;
  233 + comparisonTimeWindow?: WidgetTimewindow;
230 234
231 alarms?: Array<AlarmInfo>; 235 alarms?: Array<AlarmInfo>;
232 alarmSource?: Datasource; 236 alarmSource?: Datasource;
@@ -22,6 +22,7 @@ import { @@ -22,6 +22,7 @@ import {
22 WidgetSubscriptionOptions 22 WidgetSubscriptionOptions
23 } from '@core/api/widget-api.models'; 23 } from '@core/api/widget-api.models';
24 import { 24 import {
  25 + DataKey,
25 DataSet, 26 DataSet,
26 DataSetHolder, 27 DataSetHolder,
27 Datasource, 28 Datasource,
@@ -36,6 +37,7 @@ import { @@ -36,6 +37,7 @@ import {
36 import { HttpErrorResponse } from '@angular/common/http'; 37 import { HttpErrorResponse } from '@angular/common/http';
37 import { 38 import {
38 createSubscriptionTimewindow, 39 createSubscriptionTimewindow,
  40 + createTimewindowForComparison,
39 SubscriptionTimewindow, 41 SubscriptionTimewindow,
40 Timewindow, 42 Timewindow,
41 toHistoryTimewindow, 43 toHistoryTimewindow,
@@ -52,6 +54,9 @@ import * as deepEqual from 'deep-equal'; @@ -52,6 +54,9 @@ import * as deepEqual from 'deep-equal';
52 import { EntityId } from '@app/shared/models/id/entity-id'; 54 import { EntityId } from '@app/shared/models/id/entity-id';
53 import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; 55 import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
54 import { entityFields } from '@shared/models/entity.models'; 56 import { entityFields } from '@shared/models/entity.models';
  57 +import * as moment_ from 'moment';
  58 +
  59 +const moment = moment_;
55 60
56 export class WidgetSubscription implements IWidgetSubscription { 61 export class WidgetSubscription implements IWidgetSubscription {
57 62
@@ -77,6 +82,10 @@ export class WidgetSubscription implements IWidgetSubscription { @@ -77,6 +82,10 @@ export class WidgetSubscription implements IWidgetSubscription {
77 stateData: boolean; 82 stateData: boolean;
78 decimals: number; 83 decimals: number;
79 units: string; 84 units: string;
  85 + comparisonEnabled: boolean;
  86 + timeForComparison: moment_.unitOfTime.DurationConstructor;
  87 + comparisonTimeWindow: WidgetTimewindow;
  88 + timewindowForComparison: SubscriptionTimewindow;
80 89
81 alarms: Array<AlarmInfo>; 90 alarms: Array<AlarmInfo>;
82 alarmSource: Datasource; 91 alarmSource: Datasource;
@@ -204,6 +213,13 @@ export class WidgetSubscription implements IWidgetSubscription { @@ -204,6 +213,13 @@ export class WidgetSubscription implements IWidgetSubscription {
204 } 213 }
205 214
206 this.subscriptionTimewindow = null; 215 this.subscriptionTimewindow = null;
  216 + this.comparisonEnabled = options.comparisonEnabled;
  217 + if (this.comparisonEnabled) {
  218 + this.timeForComparison = options.timeForComparison;
  219 +
  220 + this.comparisonTimeWindow = {};
  221 + this.timewindowForComparison = null;
  222 + }
207 223
208 this.units = options.units || ''; 224 this.units = options.units || '';
209 this.decimals = isDefined(options.decimals) ? options.decimals : 2; 225 this.decimals = isDefined(options.decimals) ? options.decimals : 2;
@@ -345,11 +361,23 @@ export class WidgetSubscription implements IWidgetSubscription { @@ -345,11 +361,23 @@ export class WidgetSubscription implements IWidgetSubscription {
345 } 361 }
346 362
347 private configureData() { 363 private configureData() {
  364 + const additionalDatasources: Datasource[] = [];
348 let dataIndex = 0; 365 let dataIndex = 0;
  366 + let additionalKeysNumber = 0;
349 this.datasources.forEach((datasource) => { 367 this.datasources.forEach((datasource) => {
  368 + const additionalDataKeys: DataKey[] = [];
  369 + let datasourceAdditionalKeysNumber = 0;
350 datasource.dataKeys.forEach((dataKey) => { 370 datasource.dataKeys.forEach((dataKey) => {
351 dataKey.hidden = false; 371 dataKey.hidden = false;
352 dataKey.pattern = dataKey.label; 372 dataKey.pattern = dataKey.label;
  373 + if (this.comparisonEnabled && dataKey.settings.comparisonSettings && dataKey.settings.comparisonSettings.showValuesForComparison) {
  374 + datasourceAdditionalKeysNumber++;
  375 + additionalKeysNumber++;
  376 + const additionalDataKey = this.ctx.utils.createAdditionalDataKey(dataKey, datasource,
  377 + this.timeForComparison, this.datasources, additionalKeysNumber);
  378 + dataKey.settings.comparisonSettings.color = additionalDataKey.color;
  379 + additionalDataKeys.push(additionalDataKey);
  380 + }
353 const datasourceData: DatasourceData = { 381 const datasourceData: DatasourceData = {
354 datasource, 382 datasource,
355 dataKey, 383 dataKey,
@@ -379,7 +407,43 @@ export class WidgetSubscription implements IWidgetSubscription { @@ -379,7 +407,43 @@ export class WidgetSubscription implements IWidgetSubscription {
379 this.legendData.data.push(legendKeyData); 407 this.legendData.data.push(legendKeyData);
380 } 408 }
381 }); 409 });
  410 + if (datasourceAdditionalKeysNumber > 0) {
  411 + const additionalDatasource: Datasource = deepClone(datasource);
  412 + additionalDatasource.dataKeys = additionalDataKeys;
  413 + additionalDatasource.isAdditional = true;
  414 + additionalDatasources.push(additionalDatasource);
  415 + }
  416 + });
  417 +
  418 + additionalDatasources.forEach((additionalDatasource) => {
  419 + additionalDatasource.dataKeys.forEach((additionalDataKey) => {
  420 + const additionalDatasourceData: DatasourceData = {
  421 + datasource: additionalDatasource,
  422 + dataKey: additionalDataKey,
  423 + data: []
  424 + };
  425 + this.data.push(additionalDatasourceData);
  426 + this.hiddenData.push({data: []});
  427 + if (this.displayLegend) {
  428 + const additionalLegendKey: LegendKey = {
  429 + dataKey: additionalDataKey,
  430 + dataIndex: dataIndex++
  431 + };
  432 + this.legendData.keys.push(additionalLegendKey);
  433 + const additionalLegendKeyData: LegendKeyData = {
  434 + min: null,
  435 + max: null,
  436 + avg: null,
  437 + total: null,
  438 + hidden: false
  439 + };
  440 + this.legendData.data.push(additionalLegendKeyData);
  441 + }
  442 + });
382 }); 443 });
  444 +
  445 + this.datasources = this.datasources.concat(additionalDatasources);
  446 +
383 if (this.displayLegend) { 447 if (this.displayLegend) {
384 this.legendData.keys = this.legendData.keys.sort((key1, key2) => key1.dataKey.label.localeCompare(key2.dataKey.label)); 448 this.legendData.keys = this.legendData.keys.sort((key1, key2) => key1.dataKey.label.localeCompare(key2.dataKey.label));
385 } 449 }
@@ -678,6 +742,9 @@ export class WidgetSubscription implements IWidgetSubscription { @@ -678,6 +742,9 @@ export class WidgetSubscription implements IWidgetSubscription {
678 this.notifyDataLoading(); 742 this.notifyDataLoading();
679 if (this.type === widgetType.timeseries && this.timeWindowConfig) { 743 if (this.type === widgetType.timeseries && this.timeWindowConfig) {
680 this.updateRealtimeSubscription(); 744 this.updateRealtimeSubscription();
  745 + if (this.comparisonEnabled) {
  746 + this.updateSubscriptionForComparison();
  747 + }
681 if (this.subscriptionTimewindow.fixedWindow) { 748 if (this.subscriptionTimewindow.fixedWindow) {
682 this.onDataUpdated(); 749 this.onDataUpdated();
683 } 750 }
@@ -702,6 +769,17 @@ export class WidgetSubscription implements IWidgetSubscription { @@ -702,6 +769,17 @@ export class WidgetSubscription implements IWidgetSubscription {
702 datasourceIndex: index 769 datasourceIndex: index
703 }; 770 };
704 771
  772 + if (this.comparisonEnabled && datasource.isAdditional) {
  773 + listener.subscriptionTimewindow = this.timewindowForComparison;
  774 + listener.updateRealtimeSubscription = () => {
  775 + this.subscriptionTimewindow = this.updateSubscriptionForComparison();
  776 + return this.subscriptionTimewindow;
  777 + };
  778 + listener.setRealtimeSubscription = () => {
  779 + this.updateSubscriptionForComparison();
  780 + };
  781 + }
  782 +
705 let entityFieldKey = false; 783 let entityFieldKey = false;
706 784
707 for (let a = 0; a < datasource.dataKeys.length; a++) { 785 for (let a = 0; a < datasource.dataKeys.length; a++) {
@@ -842,7 +920,7 @@ export class WidgetSubscription implements IWidgetSubscription { @@ -842,7 +920,7 @@ export class WidgetSubscription implements IWidgetSubscription {
842 private updateTimewindow() { 920 private updateTimewindow() {
843 this.timeWindow.interval = this.subscriptionTimewindow.aggregation.interval || 1000; 921 this.timeWindow.interval = this.subscriptionTimewindow.aggregation.interval || 1000;
844 if (this.subscriptionTimewindow.realtimeWindowMs) { 922 if (this.subscriptionTimewindow.realtimeWindowMs) {
845 - this.timeWindow.maxTime = Date.now() + this.timeWindow.stDiff; 923 + this.timeWindow.maxTime = moment().valueOf() + this.timeWindow.stDiff;
846 this.timeWindow.minTime = this.timeWindow.maxTime - this.subscriptionTimewindow.realtimeWindowMs; 924 this.timeWindow.minTime = this.timeWindow.maxTime - this.subscriptionTimewindow.realtimeWindowMs;
847 } else if (this.subscriptionTimewindow.fixedWindow) { 925 } else if (this.subscriptionTimewindow.fixedWindow) {
848 this.timeWindow.maxTime = this.subscriptionTimewindow.fixedWindow.endTimeMs; 926 this.timeWindow.maxTime = this.subscriptionTimewindow.fixedWindow.endTimeMs;
@@ -862,6 +940,26 @@ export class WidgetSubscription implements IWidgetSubscription { @@ -862,6 +940,26 @@ export class WidgetSubscription implements IWidgetSubscription {
862 return this.subscriptionTimewindow; 940 return this.subscriptionTimewindow;
863 } 941 }
864 942
  943 + private updateComparisonTimewindow() {
  944 + this.comparisonTimeWindow.interval = this.timewindowForComparison.aggregation.interval || 1000;
  945 + if (this.timewindowForComparison.realtimeWindowMs) {
  946 + this.comparisonTimeWindow.maxTime = moment(this.timeWindow.maxTime).subtract(1, this.timeForComparison).valueOf();
  947 + this.comparisonTimeWindow.minTime = this.comparisonTimeWindow.maxTime - this.timewindowForComparison.realtimeWindowMs;
  948 + } else if (this.timewindowForComparison.fixedWindow) {
  949 + this.comparisonTimeWindow.maxTime = this.timewindowForComparison.fixedWindow.endTimeMs;
  950 + this.comparisonTimeWindow.minTime = this.timewindowForComparison.fixedWindow.startTimeMs;
  951 + }
  952 + }
  953 +
  954 + private updateSubscriptionForComparison() {
  955 + if (!this.subscriptionTimewindow) {
  956 + this.subscriptionTimewindow = this.updateRealtimeSubscription();
  957 + }
  958 + this.timewindowForComparison = createTimewindowForComparison(this.subscriptionTimewindow, this.timeForComparison);
  959 + this.updateComparisonTimewindow();
  960 + return this.timewindowForComparison;
  961 + }
  962 +
865 private dataUpdated(sourceData: DataSetHolder, datasourceIndex: number, dataKeyIndex: number, detectChanges: boolean) { 963 private dataUpdated(sourceData: DataSetHolder, datasourceIndex: number, dataKeyIndex: number, detectChanges: boolean) {
866 for (let x = 0; x < this.datasourceListeners.length; x++) { 964 for (let x = 0; x < this.datasourceListeners.length; x++) {
867 this.datasources[x].dataReceived = this.datasources[x].dataReceived === true; 965 this.datasources[x].dataReceived = this.datasources[x].dataReceived === true;
@@ -892,6 +990,9 @@ export class WidgetSubscription implements IWidgetSubscription { @@ -892,6 +990,9 @@ export class WidgetSubscription implements IWidgetSubscription {
892 if (update) { 990 if (update) {
893 if (this.subscriptionTimewindow && this.subscriptionTimewindow.realtimeWindowMs) { 991 if (this.subscriptionTimewindow && this.subscriptionTimewindow.realtimeWindowMs) {
894 this.updateTimewindow(); 992 this.updateTimewindow();
  993 + if (this.timewindowForComparison && this.timewindowForComparison.realtimeWindowMs) {
  994 + this.updateComparisonTimewindow();
  995 + }
895 } 996 }
896 currentData.data = sourceData.data; 997 currentData.data = sourceData.data;
897 if (this.caulculateLegendData) { 998 if (this.caulculateLegendData) {
@@ -359,6 +359,26 @@ export class UtilsService { @@ -359,6 +359,26 @@ export class UtilsService {
359 return dataKey; 359 return dataKey;
360 } 360 }
361 361
  362 + public createAdditionalDataKey(dataKey: DataKey, datasource: Datasource, timeUnit: string,
  363 + datasources: Datasource[], additionalKeysNumber: number): DataKey {
  364 + const additionalDataKey = deepClone(dataKey);
  365 + if (dataKey.settings.comparisonSettings.comparisonValuesLabel) {
  366 + additionalDataKey.label = this.createLabelFromDatasource(datasource, dataKey.settings.comparisonSettings.comparisonValuesLabel);
  367 + } else {
  368 + additionalDataKey.label = dataKey.label + ' ' + this.translate.instant('legend.comparison-time-ago.'+timeUnit);
  369 + }
  370 + additionalDataKey.pattern = additionalDataKey.label;
  371 + if (dataKey.settings.comparisonSettings.color) {
  372 + additionalDataKey.color = dataKey.settings.comparisonSettings.color;
  373 + } else {
  374 + const index = datasources.map((_datasource) => datasource.dataKeys.length)
  375 + .reduce((previousValue, currentValue) => previousValue + currentValue, 0);
  376 + additionalDataKey.color = this.getMaterialColor(index + additionalKeysNumber);
  377 + }
  378 + additionalDataKey._hash = Math.random();
  379 + return additionalDataKey;
  380 + }
  381 +
362 public createLabelFromDatasource(datasource: Datasource, pattern: string) { 382 public createLabelFromDatasource(datasource: Datasource, pattern: string) {
363 let label = pattern; 383 let label = pattern;
364 if (!datasource) { 384 if (!datasource) {
@@ -17,7 +17,8 @@ @@ -17,7 +17,8 @@
17 // tslint:disable-next-line:no-reference 17 // tslint:disable-next-line:no-reference
18 /// <reference path="../../../../../../../src/typings/jquery.flot.typings.d.ts" /> 18 /// <reference path="../../../../../../../src/typings/jquery.flot.typings.d.ts" />
19 19
20 -import { JsonSettingsSchema, DataKey, DatasourceData } from '@shared/models/widget.models'; 20 +import { JsonSettingsSchema, DataKey, DatasourceData, Datasource } from '@shared/models/widget.models';
  21 +import * as moment_ from 'moment';
21 22
22 export declare type ChartType = 'line' | 'pie' | 'bar' | 'state' | 'graph'; 23 export declare type ChartType = 'line' | 'pie' | 'bar' | 'state' | 'graph';
23 24
@@ -29,6 +30,7 @@ export declare type TbFlotTicksFormatterFunction = (t: number, a?: TbFlotPlotAxi @@ -29,6 +30,7 @@ export declare type TbFlotTicksFormatterFunction = (t: number, a?: TbFlotPlotAxi
29 30
30 export interface TbFlotSeries extends DatasourceData, JQueryPlotSeriesOptions { 31 export interface TbFlotSeries extends DatasourceData, JQueryPlotSeriesOptions {
31 dataKey: TbFlotDataKey; 32 dataKey: TbFlotDataKey;
  33 + xaxisIndex?: number;
32 yaxisIndex?: number; 34 yaxisIndex?: number;
33 yaxis?: number; 35 yaxis?: number;
34 } 36 }
@@ -50,6 +52,7 @@ export interface TbFlotAxisOptions extends JQueryPlotAxisOptions { @@ -50,6 +52,7 @@ export interface TbFlotAxisOptions extends JQueryPlotAxisOptions {
50 } 52 }
51 53
52 export interface TbFlotPlotDataSeries extends JQueryPlotDataSeries { 54 export interface TbFlotPlotDataSeries extends JQueryPlotDataSeries {
  55 + datasource?: Datasource;
53 dataKey?: TbFlotDataKey; 56 dataKey?: TbFlotDataKey;
54 percent?: number; 57 percent?: number;
55 } 58 }
@@ -93,6 +96,12 @@ export interface TbFlotXAxisSettings { @@ -93,6 +96,12 @@ export interface TbFlotXAxisSettings {
93 color: boolean; 96 color: boolean;
94 } 97 }
95 98
  99 +export interface TbFlotSecondXAxisSettings {
  100 + axisPosition: TbFlotXAxisPosition;
  101 + showLabels: boolean;
  102 + title: string;
  103 +}
  104 +
96 export interface TbFlotYAxisSettings { 105 export interface TbFlotYAxisSettings {
97 min: number; 106 min: number;
98 max: number; 107 max: number;
@@ -112,16 +121,23 @@ export interface TbFlotBaseSettings { @@ -112,16 +121,23 @@ export interface TbFlotBaseSettings {
112 tooltipIndividual: boolean; 121 tooltipIndividual: boolean;
113 tooltipCumulative: boolean; 122 tooltipCumulative: boolean;
114 tooltipValueFormatter: string; 123 tooltipValueFormatter: string;
  124 + hideZeros: boolean;
115 grid: TbFlotGridSettings; 125 grid: TbFlotGridSettings;
116 xaxis: TbFlotXAxisSettings; 126 xaxis: TbFlotXAxisSettings;
117 yaxis: TbFlotYAxisSettings; 127 yaxis: TbFlotYAxisSettings;
118 } 128 }
119 129
120 -export interface TbFlotGraphSettings extends TbFlotBaseSettings { 130 +export interface TbFlotComparisonSettings {
  131 + comparisonEnabled: boolean;
  132 + timeForComparison: moment_.unitOfTime.DurationConstructor;
  133 + xaxisSecond: TbFlotSecondXAxisSettings;
  134 +}
  135 +
  136 +export interface TbFlotGraphSettings extends TbFlotBaseSettings, TbFlotComparisonSettings {
121 smoothLines: boolean; 137 smoothLines: boolean;
122 } 138 }
123 139
124 -export interface TbFlotBarSettings extends TbFlotBaseSettings { 140 +export interface TbFlotBarSettings extends TbFlotBaseSettings, TbFlotComparisonSettings {
125 defaultBarWidth: number; 141 defaultBarWidth: number;
126 } 142 }
127 143
@@ -140,11 +156,17 @@ export interface TbFlotPieSettings { @@ -140,11 +156,17 @@ export interface TbFlotPieSettings {
140 } 156 }
141 157
142 export declare type TbFlotYAxisPosition = 'left' | 'right'; 158 export declare type TbFlotYAxisPosition = 'left' | 'right';
  159 +export declare type TbFlotXAxisPosition = 'top' | 'bottom';
143 160
144 export interface TbFlotKeySettings { 161 export interface TbFlotKeySettings {
  162 + excludeFromStacking: boolean;
145 showLines: boolean; 163 showLines: boolean;
146 fillLines: boolean; 164 fillLines: boolean;
147 showPoints: boolean; 165 showPoints: boolean;
  166 + showPointShape: string;
  167 + pointShapeFormatter: string;
  168 + showPointsLineWidth: number;
  169 + showPointsRadius: number;
148 lineWidth: number; 170 lineWidth: number;
149 tooltipValueFormatter: string; 171 tooltipValueFormatter: string;
150 showSeparateAxis: boolean; 172 showSeparateAxis: boolean;
@@ -218,6 +240,11 @@ export function flotSettingsSchema(chartType: ChartType): JsonSettingsSchema { @@ -218,6 +240,11 @@ export function flotSettingsSchema(chartType: ChartType): JsonSettingsSchema {
218 type: 'string', 240 type: 'string',
219 default: '' 241 default: ''
220 }; 242 };
  243 + properties.hideZeros = {
  244 + title: 'Hide zero/false values from tooltip',
  245 + type: 'boolean',
  246 + default: false
  247 + };
221 248
222 properties.grid = { 249 properties.grid = {
223 title: 'Grid settings', 250 title: 'Grid settings',
@@ -345,6 +372,7 @@ export function flotSettingsSchema(chartType: ChartType): JsonSettingsSchema { @@ -345,6 +372,7 @@ export function flotSettingsSchema(chartType: ChartType): JsonSettingsSchema {
345 key: 'tooltipValueFormatter', 372 key: 'tooltipValueFormatter',
346 type: 'javascript' 373 type: 'javascript'
347 }); 374 });
  375 + schema.form.push('hideZeros');
348 schema.form.push({ 376 schema.form.push({
349 key: 'grid', 377 key: 'grid',
350 items: [ 378 items: [
@@ -395,9 +423,112 @@ export function flotSettingsSchema(chartType: ChartType): JsonSettingsSchema { @@ -395,9 +423,112 @@ export function flotSettingsSchema(chartType: ChartType): JsonSettingsSchema {
395 } 423 }
396 ] 424 ]
397 }); 425 });
  426 + if (chartType === 'graph' || chartType === 'bar') {
  427 + schema.groupInfoes = [{
  428 + formIndex: 0,
  429 + GroupTitle: 'Common Settings'
  430 + }];
  431 + schema.form = [schema.form];
  432 + schema.schema.properties = {...schema.schema.properties, ...chartSettingsSchemaForComparison.schema.properties};
  433 + schema.schema.required = schema.schema.required.concat(chartSettingsSchemaForComparison.schema.required);
  434 + schema.form.push(chartSettingsSchemaForComparison.form);
  435 + schema.groupInfoes.push({
  436 + formIndex: schema.groupInfoes.length,
  437 + GroupTitle:'Comparison Settings'
  438 + });
  439 + }
398 return schema; 440 return schema;
399 } 441 }
400 442
  443 +const chartSettingsSchemaForComparison: JsonSettingsSchema = {
  444 + schema: {
  445 + title: 'Comparison Settings',
  446 + type: 'object',
  447 + properties: {
  448 + comparisonEnabled: {
  449 + title: 'Enable comparison',
  450 + type: 'boolean',
  451 + default: false
  452 + },
  453 + timeForComparison: {
  454 + title: 'Time to show historical data',
  455 + type: 'string',
  456 + default: 'months'
  457 + },
  458 + xaxisSecond: {
  459 + title: 'Second X axis',
  460 + type: 'object',
  461 + properties: {
  462 + axisPosition: {
  463 + title: 'Axis position',
  464 + type: 'string',
  465 + default: 'top'
  466 + },
  467 + showLabels: {
  468 + title: 'Show labels',
  469 + type: 'boolean',
  470 + default: true
  471 + },
  472 + title: {
  473 + title: 'Axis title',
  474 + type: 'string',
  475 + default: null
  476 + }
  477 + }
  478 + }
  479 + },
  480 + required: []
  481 + },
  482 + form: [
  483 + 'comparisonEnabled',
  484 + {
  485 + key: 'timeForComparison',
  486 + type: 'rc-select',
  487 + multiple: false,
  488 + items: [
  489 + {
  490 + value: 'days',
  491 + label: 'Day ago'
  492 + },
  493 + {
  494 + value: 'weeks',
  495 + label: 'Week ago'
  496 + },
  497 + {
  498 + value: 'months',
  499 + label: 'Month ago (default)'
  500 + },
  501 + {
  502 + value: 'years',
  503 + label: 'Year ago'
  504 + }
  505 + ]
  506 + },
  507 + {
  508 + key: 'xaxisSecond',
  509 + items: [
  510 + {
  511 + key: 'xaxisSecond.axisPosition',
  512 + type: 'rc-select',
  513 + multiple: false,
  514 + items: [
  515 + {
  516 + value: 'top',
  517 + label: 'Top (default)'
  518 + },
  519 + {
  520 + value: 'bottom',
  521 + label: 'Bottom'
  522 + }
  523 + ]
  524 + },
  525 + 'xaxisSecond.showLabels',
  526 + 'xaxisSecond.title',
  527 + ]
  528 + }
  529 + ]
  530 +};
  531 +
401 export const flotPieSettingsSchema: JsonSettingsSchema = { 532 export const flotPieSettingsSchema: JsonSettingsSchema = {
402 schema: { 533 schema: {
403 type: 'object', 534 type: 'object',
@@ -481,12 +612,17 @@ export const flotPieSettingsSchema: JsonSettingsSchema = { @@ -481,12 +612,17 @@ export const flotPieSettingsSchema: JsonSettingsSchema = {
481 ] 612 ]
482 }; 613 };
483 614
484 -export function flotDatakeySettingsSchema(defaultShowLines: boolean): JsonSettingsSchema {  
485 - return { 615 +export function flotDatakeySettingsSchema(defaultShowLines: boolean, chartType: ChartType): JsonSettingsSchema {
  616 + const schema: JsonSettingsSchema = {
486 schema: { 617 schema: {
487 type: 'object', 618 type: 'object',
488 title: 'DataKeySettings', 619 title: 'DataKeySettings',
489 properties: { 620 properties: {
  621 + excludeFromStacking: {
  622 + title: 'Exclude from stacking(available in "Stacking" mode)',
  623 + type: 'boolean',
  624 + default: false
  625 + },
490 showLines: { 626 showLines: {
491 title: 'Show lines', 627 title: 'Show lines',
492 type: 'boolean', 628 type: 'boolean',
@@ -502,10 +638,29 @@ export function flotDatakeySettingsSchema(defaultShowLines: boolean): JsonSettin @@ -502,10 +638,29 @@ export function flotDatakeySettingsSchema(defaultShowLines: boolean): JsonSettin
502 type: 'boolean', 638 type: 'boolean',
503 default: false 639 default: false
504 }, 640 },
505 - lineWidth: {  
506 - title: 'Line width', 641 + showPointShape: {
  642 + title: 'Select point shape:',
  643 + type: 'string',
  644 + default: 'circle'
  645 + },
  646 + pointShapeFormatter: {
  647 + title: 'Point shape format function, f(ctx, x, y, radius, shadow)',
  648 + type: 'string',
  649 + default: 'var size = radius * Math.sqrt(Math.PI) / 2;\n' +
  650 + 'ctx.moveTo(x - size, y - size);\n' +
  651 + 'ctx.lineTo(x + size, y + size);\n' +
  652 + 'ctx.moveTo(x - size, y + size);\n' +
  653 + 'ctx.lineTo(x + size, y - size);'
  654 + },
  655 + showPointsLineWidth: {
  656 + title: 'Line width of points',
507 type: 'number', 657 type: 'number',
508 - default: null 658 + default: 5
  659 + },
  660 + showPointsRadius: {
  661 + title: 'Radius of points',
  662 + type: 'number',
  663 + default: 3
509 }, 664 },
510 tooltipValueFormatter: { 665 tooltipValueFormatter: {
511 title: 'Tooltip value format function, f(value)', 666 title: 'Tooltip value format function, f(value)',
@@ -556,10 +711,48 @@ export function flotDatakeySettingsSchema(defaultShowLines: boolean): JsonSettin @@ -556,10 +711,48 @@ export function flotDatakeySettingsSchema(defaultShowLines: boolean): JsonSettin
556 required: ['showLines', 'fillLines', 'showPoints'] 711 required: ['showLines', 'fillLines', 'showPoints']
557 }, 712 },
558 form: [ 713 form: [
  714 + 'excludeFromStacking',
559 'showLines', 715 'showLines',
560 'fillLines', 716 'fillLines',
561 'showPoints', 717 'showPoints',
562 { 718 {
  719 + key: 'showPointShape',
  720 + type: 'rc-select',
  721 + multiple: false,
  722 + items: [
  723 + {
  724 + value: 'circle',
  725 + label: 'Circle'
  726 + },
  727 + {
  728 + value: 'cross',
  729 + label: 'Cross'
  730 + },
  731 + {
  732 + value: 'diamond',
  733 + label: 'Diamond'
  734 + },
  735 + {
  736 + value: 'square',
  737 + label: 'Square'
  738 + },
  739 + {
  740 + value: 'triangle',
  741 + label: 'Triangle'
  742 + },
  743 + {
  744 + value: 'custom',
  745 + label: 'Custom function'
  746 + }
  747 + ]
  748 + },
  749 + {
  750 + key: 'pointShapeFormatter',
  751 + type: 'javascript'
  752 + },
  753 + 'showPointsLineWidth',
  754 + 'showPointsRadius',
  755 + {
563 key: 'tooltipValueFormatter', 756 key: 'tooltipValueFormatter',
564 type: 'javascript' 757 type: 'javascript'
565 }, 758 },
@@ -590,4 +783,43 @@ export function flotDatakeySettingsSchema(defaultShowLines: boolean): JsonSettin @@ -590,4 +783,43 @@ export function flotDatakeySettingsSchema(defaultShowLines: boolean): JsonSettin
590 } 783 }
591 ] 784 ]
592 }; 785 };
  786 +
  787 + const properties = schema.schema.properties;
  788 + if (chartType === 'graph' || chartType === 'bar') {
  789 + properties.comparisonSettings = {
  790 + title: 'Comparison Settings',
  791 + type: 'object',
  792 + properties: {
  793 + showValuesForComparison: {
  794 + title: 'Show historical values for comparison',
  795 + type: 'boolean',
  796 + default: true
  797 + },
  798 + comparisonValuesLabel: {
  799 + title: 'Historical values label',
  800 + type: 'string',
  801 + default: ''
  802 + },
  803 + color: {
  804 + title: 'Color',
  805 + type: 'string',
  806 + default: ''
  807 + }
  808 + },
  809 + required: ['showValuesForComparison']
  810 + };
  811 + schema.form.push({
  812 + key: 'comparisonSettings',
  813 + items: [
  814 + 'comparisonSettings.showValuesForComparison',
  815 + 'comparisonSettings.comparisonValuesLabel',
  816 + {
  817 + key: 'comparisonSettings.color',
  818 + type: 'color'
  819 + }
  820 + ]
  821 + });
  822 + }
  823 +
  824 + return schema;
593 } 825 }
@@ -41,6 +41,7 @@ import * as tinycolor_ from 'tinycolor2'; @@ -41,6 +41,7 @@ import * as tinycolor_ from 'tinycolor2';
41 import { AggregationType } from '@shared/models/time/time.models'; 41 import { AggregationType } from '@shared/models/time/time.models';
42 import { CancelAnimationFrame } from '@core/services/raf.service'; 42 import { CancelAnimationFrame } from '@core/services/raf.service';
43 import Timeout = NodeJS.Timeout; 43 import Timeout = NodeJS.Timeout;
  44 +import { UtilsService } from '@core/services/utils.service';
44 45
45 const tinycolor = tinycolor_; 46 const tinycolor = tinycolor_;
46 const moment = moment_; 47 const moment = moment_;
@@ -56,6 +57,7 @@ export class TbFlot { @@ -56,6 +57,7 @@ export class TbFlot {
56 private readonly yAxisTickFormatter: TbFlotTicksFormatterFunction; 57 private readonly yAxisTickFormatter: TbFlotTicksFormatterFunction;
57 private ticksFormatterFunction: TbFlotTicksFormatterFunction; 58 private ticksFormatterFunction: TbFlotTicksFormatterFunction;
58 private readonly yaxis: TbFlotAxisOptions; 59 private readonly yaxis: TbFlotAxisOptions;
  60 + private readonly xaxis: TbFlotAxisOptions;
59 private yaxes: Array<TbFlotAxisOptions>; 61 private yaxes: Array<TbFlotAxisOptions>;
60 62
61 private readonly options: JQueryPlotOptions; 63 private readonly options: JQueryPlotOptions;
@@ -66,6 +68,7 @@ export class TbFlot { @@ -66,6 +68,7 @@ export class TbFlot {
66 private readonly trackDecimals: number; 68 private readonly trackDecimals: number;
67 private readonly tooltipIndividual: boolean; 69 private readonly tooltipIndividual: boolean;
68 private readonly tooltipCumulative: boolean; 70 private readonly tooltipCumulative: boolean;
  71 + private readonly hideZeros: boolean;
69 72
70 private readonly defaultBarWidth: number; 73 private readonly defaultBarWidth: number;
71 74
@@ -106,13 +109,14 @@ export class TbFlot { @@ -106,13 +109,14 @@ export class TbFlot {
106 return flotSettingsSchema(chartType); 109 return flotSettingsSchema(chartType);
107 } 110 }
108 111
109 - static datakeySettingsSchema(defaultShowLines: boolean): JsonSettingsSchema {  
110 - return flotDatakeySettingsSchema(defaultShowLines); 112 + static datakeySettingsSchema(defaultShowLines: boolean, chartType: ChartType): JsonSettingsSchema {
  113 + return flotDatakeySettingsSchema(defaultShowLines, chartType);
111 } 114 }
112 115
113 constructor(private ctx: WidgetContext, private readonly chartType: ChartType) { 116 constructor(private ctx: WidgetContext, private readonly chartType: ChartType) {
114 this.chartType = this.chartType || 'line'; 117 this.chartType = this.chartType || 'line';
115 this.settings = ctx.settings as TbFlotSettings; 118 this.settings = ctx.settings as TbFlotSettings;
  119 + const utils = this.ctx.$injector.get(UtilsService);
116 this.tooltip = $('#flot-series-tooltip'); 120 this.tooltip = $('#flot-series-tooltip');
117 if (this.tooltip.length === 0) { 121 if (this.tooltip.length === 0) {
118 this.tooltip = this.createTooltipElement(); 122 this.tooltip = this.createTooltipElement();
@@ -123,6 +127,7 @@ export class TbFlot { @@ -123,6 +127,7 @@ export class TbFlot {
123 this.tooltipIndividual = this.chartType === 'pie' || (isDefined(this.settings.tooltipIndividual) 127 this.tooltipIndividual = this.chartType === 'pie' || (isDefined(this.settings.tooltipIndividual)
124 ? this.settings.tooltipIndividual : false); 128 ? this.settings.tooltipIndividual : false);
125 this.tooltipCumulative = isDefined(this.settings.tooltipCumulative) ? this.settings.tooltipCumulative : false; 129 this.tooltipCumulative = isDefined(this.settings.tooltipCumulative) ? this.settings.tooltipCumulative : false;
  130 + this.hideZeros = isDefined(this.settings.hideZeros) ? this.settings.hideZeros : false;
126 131
127 const font = { 132 const font = {
128 color: this.settings.fontColor || '#545454', 133 color: this.settings.fontColor || '#545454',
@@ -147,7 +152,8 @@ export class TbFlot { @@ -147,7 +152,8 @@ export class TbFlot {
147 }; 152 };
148 153
149 if (this.chartType === 'line' || this.chartType === 'bar' || this.chartType === 'state') { 154 if (this.chartType === 'line' || this.chartType === 'bar' || this.chartType === 'state') {
150 - this.options.xaxis = { 155 + this.options.xaxes = [];
  156 + this.xaxis = {
151 mode: 'time', 157 mode: 'time',
152 timezone: 'browser', 158 timezone: 'browser',
153 font: deepClone(font), 159 font: deepClone(font),
@@ -158,16 +164,11 @@ export class TbFlot { @@ -158,16 +164,11 @@ export class TbFlot {
158 labelFont: deepClone(font) 164 labelFont: deepClone(font)
159 }; 165 };
160 if (this.settings.xaxis) { 166 if (this.settings.xaxis) {
161 - if (this.settings.xaxis.showLabels === false) {  
162 - this.options.xaxis.tickFormatter = () => {  
163 - return '';  
164 - };  
165 - }  
166 - this.options.xaxis.font.color = this.settings.xaxis.color || this.options.xaxis.font.color;  
167 - this.options.xaxis.label = this.settings.xaxis.title || null;  
168 - this.options.xaxis.labelFont.color = this.options.xaxis.font.color;  
169 - this.options.xaxis.labelFont.size = this.options.xaxis.font.size + 2;  
170 - this.options.xaxis.labelFont.weight = 'bold'; 167 + this.xaxis.font.color = this.settings.xaxis.color || this.xaxis.font.color;
  168 + this.xaxis.label = utils.customTranslation(this.settings.xaxis.title, this.settings.xaxis.title) || null;
  169 + this.xaxis.labelFont.color = this.xaxis.font.color;
  170 + this.xaxis.labelFont.size = this.xaxis.font.size + 2;
  171 + this.xaxis.labelFont.weight = 'bold';
171 } 172 }
172 173
173 this.yAxisTickFormatter = this.formatYAxisTicks.bind(this); 174 this.yAxisTickFormatter = this.formatYAxisTicks.bind(this);
@@ -178,7 +179,7 @@ export class TbFlot { @@ -178,7 +179,7 @@ export class TbFlot {
178 this.yaxis.font.color = this.settings.yaxis.color || this.yaxis.font.color; 179 this.yaxis.font.color = this.settings.yaxis.color || this.yaxis.font.color;
179 this.yaxis.min = isDefined(this.settings.yaxis.min) ? this.settings.yaxis.min : null; 180 this.yaxis.min = isDefined(this.settings.yaxis.min) ? this.settings.yaxis.min : null;
180 this.yaxis.max = isDefined(this.settings.yaxis.max) ? this.settings.yaxis.max : null; 181 this.yaxis.max = isDefined(this.settings.yaxis.max) ? this.settings.yaxis.max : null;
181 - this.yaxis.label = this.settings.yaxis.title || null; 182 + this.yaxis.label = utils.customTranslation(this.settings.yaxis.title, this.settings.yaxis.title) || null;
182 this.yaxis.labelFont.color = this.yaxis.font.color; 183 this.yaxis.labelFont.color = this.yaxis.font.color;
183 this.yaxis.labelFont.size = this.yaxis.font.size + 2; 184 this.yaxis.labelFont.size = this.yaxis.font.size + 2;
184 this.yaxis.labelFont.weight = 'bold'; 185 this.yaxis.labelFont.weight = 'bold';
@@ -212,7 +213,7 @@ export class TbFlot { @@ -212,7 +213,7 @@ export class TbFlot {
212 this. options.grid.borderWidth = isDefined(this.settings.grid.outlineWidth) ? 213 this. options.grid.borderWidth = isDefined(this.settings.grid.outlineWidth) ?
213 this.settings.grid.outlineWidth : 1; 214 this.settings.grid.outlineWidth : 1;
214 if (this.settings.grid.verticalLines === false) { 215 if (this.settings.grid.verticalLines === false) {
215 - this.options.xaxis.tickLength = 0; 216 + this.xaxis.tickLength = 0;
216 } 217 }
217 if (this.settings.grid.horizontalLines === false) { 218 if (this.settings.grid.horizontalLines === false) {
218 this.yaxis.tickLength = 0; 219 this.yaxis.tickLength = 0;
@@ -225,14 +226,41 @@ export class TbFlot { @@ -225,14 +226,41 @@ export class TbFlot {
225 } 226 }
226 } 227 }
227 228
  229 + this.options.xaxes[0] = deepClone(this.xaxis);
  230 + if (this.settings.xaxis && this.settings.xaxis.showLabels === false) {
  231 + this.options.xaxes[0].tickFormatter = () => {
  232 + return '';
  233 + };
  234 + }
  235 +
  236 + if (this.settings.comparisonEnabled) {
  237 + const xaxis = deepClone(this.xaxis);
  238 + xaxis.position = 'top';
  239 + if (this.settings.xaxisSecond) {
  240 + if (this.settings.xaxisSecond.showLabels === false) {
  241 + xaxis.tickFormatter = () => {
  242 + return '';
  243 + };
  244 + }
  245 + xaxis.label = utils.customTranslation(this.settings.xaxisSecond.title, this.settings.xaxisSecond.title) || null;
  246 + xaxis.position = this.settings.xaxisSecond.axisPosition;
  247 + }
  248 + xaxis.tickLength = 0;
  249 + this.options.xaxes.push(xaxis);
  250 +
  251 + this.options.series = {
  252 + stack: false
  253 + };
  254 + } else {
  255 + this.options.series = {
  256 + stack: this.settings.stack === true
  257 + };
  258 + }
  259 +
228 this.options.crosshair = { 260 this.options.crosshair = {
229 mode: 'x' 261 mode: 'x'
230 }; 262 };
231 263
232 - this.options.series = {  
233 - stack: this.settings.stack === true  
234 - };  
235 -  
236 if (this.chartType === 'line' && this.settings.smoothLines) { 264 if (this.chartType === 'line' && this.settings.smoothLines) {
237 this.options.series.curvedLines = { 265 this.options.series.curvedLines = {
238 active: true, 266 active: true,
@@ -339,6 +367,13 @@ export class TbFlot { @@ -339,6 +367,13 @@ export class TbFlot {
339 series.lines = { 367 series.lines = {
340 fill: keySettings.fillLines === true 368 fill: keySettings.fillLines === true
341 }; 369 };
  370 +
  371 + if (this.settings.stack && !this.settings.comparisonEnabled) {
  372 + series.stack = !keySettings.excludeFromStacking;
  373 + } else {
  374 + series.stack = false;
  375 + }
  376 +
342 if (this.chartType === 'line' || this.chartType === 'state') { 377 if (this.chartType === 'line' || this.chartType === 'state') {
343 series.lines.show = keySettings.showLines !== false; 378 series.lines.show = keySettings.showLines !== false;
344 } else { 379 } else {
@@ -353,8 +388,16 @@ export class TbFlot { @@ -353,8 +388,16 @@ export class TbFlot {
353 }; 388 };
354 if (keySettings.showPoints === true) { 389 if (keySettings.showPoints === true) {
355 series.points.show = true; 390 series.points.show = true;
356 - series.points.lineWidth = 5;  
357 - series.points.radius = 3; 391 + series.points.lineWidth = isDefined(keySettings.showPointsLineWidth) ? keySettings.showPointsLineWidth : 5;
  392 + series.points.radius = isDefined(keySettings.showPointsRadius) ? keySettings.showPointsRadius : 3;
  393 + series.points.symbol = isDefined(keySettings.showPointShape) ? keySettings.showPointShape : 'circle';
  394 + if (series.points.symbol === 'custom' && keySettings.pointShapeFormatter) {
  395 + try {
  396 + series.points.symbol = new Function('ctx, x, y, radius, shadow', keySettings.pointShapeFormatter);
  397 + } catch (e) {
  398 + series.points.symbol = 'circle';
  399 + }
  400 + }
358 } 401 }
359 if (this.chartType === 'line' && this.settings.smoothLines && !series.points.show) { 402 if (this.chartType === 'line' && this.settings.smoothLines && !series.points.show) {
360 series.curvedLines = { 403 series.curvedLines = {
@@ -367,6 +410,14 @@ export class TbFlot { @@ -367,6 +410,14 @@ export class TbFlot {
367 410
368 series.highlightColor = lineColor.toRgbString(); 411 series.highlightColor = lineColor.toRgbString();
369 412
  413 + if (series.datasource.isAdditional) {
  414 + series.xaxisIndex = 1;
  415 + series.xaxis = 2;
  416 + } else {
  417 + series.xaxisIndex = 0;
  418 + series.xaxis = 1;
  419 + }
  420 +
370 if (this.yaxis) { 421 if (this.yaxis) {
371 const units = series.dataKey.units && series.dataKey.units.length ? series.dataKey.units : this.trackUnits; 422 const units = series.dataKey.units && series.dataKey.units.length ? series.dataKey.units : this.trackUnits;
372 let yaxis: TbFlotAxisOptions; 423 let yaxis: TbFlotAxisOptions;
@@ -384,7 +435,7 @@ export class TbFlot { @@ -384,7 +435,7 @@ export class TbFlot {
384 series.yaxisIndex = this.yaxes.indexOf(yaxis); 435 series.yaxisIndex = this.yaxes.indexOf(yaxis);
385 series.yaxis = series.yaxisIndex + 1; 436 series.yaxis = series.yaxisIndex + 1;
386 yaxis.keysInfo[i] = {hidden: false}; 437 yaxis.keysInfo[i] = {hidden: false};
387 - yaxis.hidden = false; 438 + yaxis.show = true;
388 } 439 }
389 } 440 }
390 this.options.colors = colors; 441 this.options.colors = colors;
@@ -398,8 +449,12 @@ export class TbFlot { @@ -398,8 +449,12 @@ export class TbFlot {
398 this.options.series.bars.barWidth = this.subscription.timeWindow.interval * 0.6; 449 this.options.series.bars.barWidth = this.subscription.timeWindow.interval * 0.6;
399 } 450 }
400 } 451 }
401 - this.options.xaxis.min = this.subscription.timeWindow.minTime;  
402 - this.options.xaxis.max = this.subscription.timeWindow.maxTime; 452 + this.options.xaxes[0].min = this.subscription.timeWindow.minTime;
  453 + this.options.xaxes[0].max = this.subscription.timeWindow.maxTime;
  454 + if (this.settings.comparisonEnabled) {
  455 + this.options.xaxes[1].min = this.subscription.comparisonTimeWindow.minTime;
  456 + this.options.xaxes[1].max = this.subscription.comparisonTimeWindow.maxTime;
  457 + }
403 } 458 }
404 459
405 this.checkMouseEvents(); 460 this.checkMouseEvents();
@@ -469,8 +524,12 @@ export class TbFlot { @@ -469,8 +524,12 @@ export class TbFlot {
469 } 524 }
470 } 525 }
471 526
472 - this.options.xaxis.min = this.subscription.timeWindow.minTime;  
473 - this.options.xaxis.max = this.subscription.timeWindow.maxTime; 527 + this.options.xaxes[0].min = this.subscription.timeWindow.minTime;
  528 + this.options.xaxes[0].max = this.subscription.timeWindow.maxTime;
  529 + if (this.settings.comparisonEnabled) {
  530 + this.options.xaxes[1].min = this.subscription.comparisonTimeWindow.minTime;
  531 + this.options.xaxes[1].max = this.subscription.comparisonTimeWindow.maxTime;
  532 + }
474 if (this.chartType === 'bar') { 533 if (this.chartType === 'bar') {
475 if (this.subscription.timeWindowConfig.aggregation && 534 if (this.subscription.timeWindowConfig.aggregation &&
476 this.subscription.timeWindowConfig.aggregation.type === AggregationType.NONE) { 535 this.subscription.timeWindowConfig.aggregation.type === AggregationType.NONE) {
@@ -485,6 +544,10 @@ export class TbFlot { @@ -485,6 +544,10 @@ export class TbFlot {
485 } else { 544 } else {
486 this.plot.getOptions().xaxes[0].min = this.subscription.timeWindow.minTime; 545 this.plot.getOptions().xaxes[0].min = this.subscription.timeWindow.minTime;
487 this.plot.getOptions().xaxes[0].max = this.subscription.timeWindow.maxTime; 546 this.plot.getOptions().xaxes[0].max = this.subscription.timeWindow.maxTime;
  547 + if (this.settings.comparisonEnabled) {
  548 + this.plot.getOptions().xaxes[1].min = this.subscription.comparisonTimeWindow.minTime;
  549 + this.plot.getOptions().xaxes[1].max = this.subscription.comparisonTimeWindow.maxTime;
  550 + }
488 if (this.chartType === 'bar') { 551 if (this.chartType === 'bar') {
489 if (this.subscription.timeWindowConfig.aggregation && 552 if (this.subscription.timeWindowConfig.aggregation &&
490 this.subscription.timeWindowConfig.aggregation.type === AggregationType.NONE) { 553 this.subscription.timeWindowConfig.aggregation.type === AggregationType.NONE) {
@@ -653,7 +716,7 @@ export class TbFlot { @@ -653,7 +716,7 @@ export class TbFlot {
653 divElement.css({ 716 divElement.css({
654 display: 'flex', 717 display: 'flex',
655 alignItems: 'center', 718 alignItems: 'center',
656 - justifyContent: 'flex-start' 719 + justifyContent: 'space-between'
657 }); 720 });
658 const lineSpan = $('<span></span>'); 721 const lineSpan = $('<span></span>');
659 lineSpan.css({ 722 lineSpan.css({
@@ -734,57 +797,98 @@ export class TbFlot { @@ -734,57 +797,98 @@ export class TbFlot {
734 return divElement.prop('outerHTML'); 797 return divElement.prop('outerHTML');
735 } 798 }
736 799
737 - private formatChartTooltip(hoverInfo: TbFlotHoverInfo, seriesIndex: number): string { 800 + private formatChartTooltip(hoverInfo: TbFlotHoverInfo[], seriesIndex: number): string {
738 let content = ''; 801 let content = '';
739 - const timestamp = parseInt(hoverInfo.time, 10);  
740 - const date = moment(timestamp).format('YYYY-MM-DD HH:mm:ss');  
741 - const dateDiv = $(`<div>${date}</div>`);  
742 - dateDiv.css({  
743 - display: 'flex',  
744 - alignItems: 'center',  
745 - justifyContent: 'center',  
746 - padding: '4px',  
747 - fontWeight: '700'  
748 - });  
749 - content += dateDiv.prop('outerHTML');  
750 if (this.tooltipIndividual) { 802 if (this.tooltipIndividual) {
751 - const found = hoverInfo.seriesHover.find((seriesHover) => {  
752 - return seriesHover.index === seriesIndex;  
753 - });  
754 - if (found) {  
755 - content += this.seriesInfoDivFromInfo(found, seriesIndex); 803 + let seriesHoverArray: TbFlotSeriesHoverInfo[];
  804 + if (hoverInfo[1] && hoverInfo[1].seriesHover.length) {
  805 + seriesHoverArray = hoverInfo[0].seriesHover.concat(hoverInfo[1].seriesHover);
  806 + } else {
  807 + seriesHoverArray = hoverInfo[0].seriesHover;
756 } 808 }
757 - } else {  
758 - const seriesDiv = $('<div></div>');  
759 - seriesDiv.css({  
760 - display: 'flex',  
761 - flexDirection: 'row' 809 + const found = seriesHoverArray.filter((seriesHover) => {
  810 + return seriesHover.index === seriesIndex;
762 }); 811 });
763 - const maxRows = 15;  
764 - const columns = Math.ceil(hoverInfo.seriesHover.length / maxRows);  
765 - let columnsContent = '';  
766 - for (let c = 0; c < columns; c++) {  
767 - const columnDiv = $('<div></div>');  
768 - columnDiv.css({ 812 + if (found && found.length) {
  813 + let timestamp: number;
  814 + if (!isNumber(hoverInfo[0].time) || (found[0].time < hoverInfo[0].time)) {
  815 + timestamp = parseInt(hoverInfo[1].time, 10);
  816 + } else {
  817 + timestamp = parseInt(hoverInfo[0].time, 10);
  818 + }
  819 + const date = moment(timestamp).format('YYYY-MM-DD HH:mm:ss');
  820 + const dateDiv = $('<div>' + date + '</div>');
  821 + dateDiv.css({
769 display: 'flex', 822 display: 'flex',
770 - flexDirection: 'column' 823 + alignItems: 'center',
  824 + justifyContent: 'center',
  825 + padding: '4px',
  826 + fontWeight: '700'
771 }); 827 });
772 - let columnContent = '';  
773 - for (let i = c * maxRows; i < (c + 1) * maxRows; i++) {  
774 - if (i === hoverInfo.seriesHover.length) {  
775 - break; 828 + content += dateDiv.prop('outerHTML');
  829 + content += this.seriesInfoDivFromInfo(found[0], seriesIndex);
  830 + }
  831 + } else {
  832 + let maxRows: number;
  833 + if (hoverInfo[1] && hoverInfo[1].seriesHover.length) {
  834 + maxRows = 5;
  835 + } else {
  836 + maxRows = 15;
  837 + }
  838 + let columns = 0;
  839 + if (hoverInfo[1] && (hoverInfo[1].seriesHover.length > hoverInfo[0].seriesHover.length)) {
  840 + columns = Math.ceil(hoverInfo[1].seriesHover.length / maxRows);
  841 + } else {
  842 + columns = Math.ceil(hoverInfo[0].seriesHover.length / maxRows);
  843 + }
  844 + hoverInfo.forEach((hoverData) => {
  845 + if (isNumber(hoverData.time)) {
  846 + let columnsContent = '';
  847 + const timestamp = parseInt(hoverData.time, 10);
  848 + const date = moment(timestamp).format('YYYY-MM-DD HH:mm:ss');
  849 + const dateDiv = $('<div>' + date + '</div>');
  850 + dateDiv.css({
  851 + display: 'flex',
  852 + alignItems: 'center',
  853 + justifyContent: 'center',
  854 + padding: '4px',
  855 + fontWeight: '700'
  856 + });
  857 + content += dateDiv.prop('outerHTML');
  858 +
  859 + const seriesDiv = $('<div></div>');
  860 + seriesDiv.css({
  861 + display: 'flex',
  862 + flexDirection: 'row'
  863 + });
  864 + for (let c = 0; c < columns; c++) {
  865 + const columnDiv = $('<div></div>');
  866 + columnDiv.css({
  867 + display: 'flex',
  868 + flexDirection: 'column',
  869 + flex: '1'
  870 + });
  871 + let columnContent = '';
  872 + for (let i = c*maxRows; i < (c+1)*maxRows; i++) {
  873 + if (i >= hoverData.seriesHover.length) {
  874 + break;
  875 + }
  876 + const seriesHoverInfo = hoverData.seriesHover[i];
  877 + columnContent += this.seriesInfoDivFromInfo(seriesHoverInfo, seriesIndex);
  878 + }
  879 + columnDiv.html(columnContent);
  880 +
  881 + if (columnContent) {
  882 + if (c > 0) {
  883 + columnsContent += '<span style="min-width: 20px;"></span>';
  884 + }
  885 + columnsContent += columnDiv.prop('outerHTML');
  886 + }
776 } 887 }
777 - const seriesHoverInfo = hoverInfo.seriesHover[i];  
778 - columnContent += this.seriesInfoDivFromInfo(seriesHoverInfo, seriesIndex);  
779 - }  
780 - columnDiv.html(columnContent);  
781 - if (c > 0) {  
782 - columnsContent += '<span style="width: 20px;"></span>'; 888 + seriesDiv.html(columnsContent);
  889 + content += seriesDiv.prop('outerHTML');
783 } 890 }
784 - columnsContent += columnDiv.prop('outerHTML');  
785 - }  
786 - seriesDiv.html(columnsContent);  
787 - content += seriesDiv.prop('outerHTML'); 891 + });
788 } 892 }
789 return content; 893 return content;
790 } 894 }
@@ -848,16 +952,21 @@ export class TbFlot { @@ -848,16 +952,21 @@ export class TbFlot {
848 const pageY = pos.pageY; 952 const pageY = pos.pageY;
849 953
850 let tooltipHtml; 954 let tooltipHtml;
851 - let hoverInfo: TbFlotHoverInfo; 955 + let hoverInfo: TbFlotHoverInfo[];
852 956
853 if (this.chartType === 'pie') { 957 if (this.chartType === 'pie') {
854 tooltipHtml = this.formatPieTooltip(item); 958 tooltipHtml = this.formatPieTooltip(item);
855 } else { 959 } else {
856 hoverInfo = this.getHoverInfo(this.plot.getData(), pos); 960 hoverInfo = this.getHoverInfo(this.plot.getData(), pos);
857 - if (isNumber(hoverInfo.time)) {  
858 - hoverInfo.seriesHover.sort((a, b) => { 961 + if (isNumber(hoverInfo[0].time) || (hoverInfo[1] && isNumber(hoverInfo[1].time))) {
  962 + hoverInfo[0].seriesHover.sort((a, b) => {
859 return b.value - a.value; 963 return b.value - a.value;
860 }); 964 });
  965 + if (hoverInfo[1] && hoverInfo[1].seriesHover.length) {
  966 + hoverInfo[1].seriesHover.sort((a, b) => {
  967 + return b.value - a.value;
  968 + });
  969 + }
861 tooltipHtml = this.formatChartTooltip(hoverInfo, item ? item.seriesIndex : -1); 970 tooltipHtml = this.formatChartTooltip(hoverInfo, item ? item.seriesIndex : -1);
862 } 971 }
863 } 972 }
@@ -883,8 +992,10 @@ export class TbFlot { @@ -883,8 +992,10 @@ export class TbFlot {
883 left 992 left
884 }); 993 });
885 if (multipleModeTooltip) { 994 if (multipleModeTooltip) {
886 - hoverInfo.seriesHover.forEach((seriesHoverInfo) => {  
887 - this.plot.highlight(seriesHoverInfo.index, seriesHoverInfo.hoverIndex); 995 + hoverInfo.forEach((hoverData) => {
  996 + hoverData.seriesHover.forEach((seriesHoverInfo) => {
  997 + this.plot.highlight(seriesHoverInfo.index, seriesHoverInfo.hoverIndex);
  998 + });
888 }); 999 });
889 } 1000 }
890 } 1001 }
@@ -927,7 +1038,7 @@ export class TbFlot { @@ -927,7 +1038,7 @@ export class TbFlot {
927 this.isMouseInteraction = false; 1038 this.isMouseInteraction = false;
928 } 1039 }
929 1040
930 - private getHoverInfo(seriesList: TbFlotPlotDataSeries[], pos: JQueryPlotPoint): TbFlotHoverInfo { 1041 + private getHoverInfo(seriesList: TbFlotPlotDataSeries[], pos: JQueryPlotPoint): TbFlotHoverInfo[] {
931 let i: number; 1042 let i: number;
932 let series: TbFlotPlotDataSeries; 1043 let series: TbFlotPlotDataSeries;
933 let hoverIndex: number; 1044 let hoverIndex: number;
@@ -935,19 +1046,40 @@ export class TbFlot { @@ -935,19 +1046,40 @@ export class TbFlot {
935 let minDistance: number; 1046 let minDistance: number;
936 let pointTime: any; 1047 let pointTime: any;
937 let minTime: any; 1048 let minTime: any;
  1049 + let minTimeHistorical: any;
  1050 + let hoverData: TbFlotSeriesHoverInfo;
938 let value: any; 1051 let value: any;
939 let lastValue: any; 1052 let lastValue: any;
940 - const results: TbFlotHoverInfo = { 1053 + let minDistanceHistorical: number;
  1054 + const results: TbFlotHoverInfo[] = [{
941 seriesHover: [] 1055 seriesHover: []
942 - }; 1056 + }];
  1057 + if (this.settings.comparisonEnabled) {
  1058 + results.push({
  1059 + seriesHover: []
  1060 + });
  1061 + }
943 for (i = 0; i < seriesList.length; i++) { 1062 for (i = 0; i < seriesList.length; i++) {
944 series = seriesList[i]; 1063 series = seriesList[i];
945 - hoverIndex = this.findHoverIndexFromData(pos.x, series); 1064 + let posx: number;
  1065 + if (series.datasource.isAdditional) {
  1066 + posx = pos.x2;
  1067 + } else {
  1068 + posx = pos.x;
  1069 + }
  1070 + hoverIndex = this.findHoverIndexFromData(posx, series);
946 if (series.data[hoverIndex] && series.data[hoverIndex][0]) { 1071 if (series.data[hoverIndex] && series.data[hoverIndex][0]) {
947 - hoverDistance = pos.x - series.data[hoverIndex][0]; 1072 + hoverDistance = posx - series.data[hoverIndex][0];
948 pointTime = series.data[hoverIndex][0]; 1073 pointTime = series.data[hoverIndex][0];
949 1074
950 - if (!minDistance 1075 + if (series.datasource.isAdditional) {
  1076 + if (!minDistanceHistorical
  1077 + || (hoverDistance >= 0 && (hoverDistance < minDistanceHistorical || minDistanceHistorical < 0))
  1078 + || (hoverDistance < 0 && hoverDistance > minDistanceHistorical)) {
  1079 + minDistanceHistorical = hoverDistance;
  1080 + minTimeHistorical = pointTime;
  1081 + }
  1082 + } else if (!minDistance
951 || (hoverDistance >= 0 && (hoverDistance < minDistance || minDistance < 0)) 1083 || (hoverDistance >= 0 && (hoverDistance < minDistance || minDistance < 0))
952 || (hoverDistance < 0 && hoverDistance > minDistance)) { 1084 || (hoverDistance < 0 && hoverDistance > minDistance)) {
953 minDistance = hoverDistance; 1085 minDistance = hoverDistance;
@@ -964,23 +1096,33 @@ export class TbFlot { @@ -964,23 +1096,33 @@ export class TbFlot {
964 value = series.data[hoverIndex][1]; 1096 value = series.data[hoverIndex][1];
965 } 1097 }
966 if (series.stack || (series.curvedLines && series.curvedLines.apply)) { 1098 if (series.stack || (series.curvedLines && series.curvedLines.apply)) {
967 - hoverIndex = this.findHoverIndexFromDataPoints(pos.x, series, hoverIndex); 1099 + hoverIndex = this.findHoverIndexFromDataPoints(posx, series, hoverIndex);
  1100 + }
  1101 + if (!this.hideZeros || value) {
  1102 + hoverData = {
  1103 + value,
  1104 + hoverIndex,
  1105 + color: series.dataKey.color,
  1106 + label: series.dataKey.label,
  1107 + units: series.dataKey.units,
  1108 + decimals: series.dataKey.decimals,
  1109 + tooltipValueFormatFunction: series.dataKey.tooltipValueFormatFunction,
  1110 + time: pointTime,
  1111 + distance: hoverDistance,
  1112 + index: i
  1113 + };
  1114 + if (series.datasource.isAdditional) {
  1115 + results[1].seriesHover.push(hoverData);
  1116 + } else {
  1117 + results[0].seriesHover.push(hoverData);
  1118 + }
968 } 1119 }
969 - results.seriesHover.push({  
970 - value,  
971 - hoverIndex,  
972 - color: series.dataKey.color,  
973 - label: series.dataKey.label,  
974 - units: series.dataKey.units,  
975 - decimals: series.dataKey.decimals,  
976 - tooltipValueFormatFunction: series.dataKey.tooltipValueFormatFunction,  
977 - time: pointTime,  
978 - distance: hoverDistance,  
979 - index: i  
980 - });  
981 } 1120 }
982 } 1121 }
983 - results.time = minTime; 1122 + if (results[1] && results[1].seriesHover.length) {
  1123 + results[1].time = minTimeHistorical;
  1124 + }
  1125 + results[0].time = minTime;
984 return results; 1126 return results;
985 } 1127 }
986 1128
@@ -23,7 +23,7 @@ import { @@ -23,7 +23,7 @@ import {
23 Datasource, 23 Datasource,
24 DatasourceType, 24 DatasourceType,
25 datasourceTypeTranslationMap, 25 datasourceTypeTranslationMap,
26 - defaultLegendConfig, 26 + defaultLegendConfig, GroupInfo, JsonSchema,
27 widgetType 27 widgetType
28 } from '@shared/models/widget.models'; 28 } from '@shared/models/widget.models';
29 import { 29 import {
@@ -60,11 +60,11 @@ import { WidgetActionsData } from './action/manage-widget-actions.component.mode @@ -60,11 +60,11 @@ import { WidgetActionsData } from './action/manage-widget-actions.component.mode
60 import { DashboardState } from '@shared/models/dashboard.models'; 60 import { DashboardState } from '@shared/models/dashboard.models';
61 import { entityFields } from '@shared/models/entity.models'; 61 import { entityFields } from '@shared/models/entity.models';
62 62
63 -const emptySettingsSchema = { 63 +const emptySettingsSchema: JsonSchema = {
64 type: 'object', 64 type: 'object',
65 properties: {} 65 properties: {}
66 }; 66 };
67 -const emptySettingsGroupInfoes = []; 67 +const emptySettingsGroupInfoes: GroupInfo[] = [];
68 const defaultSettingsForm = [ 68 const defaultSettingsForm = [
69 '*' 69 '*'
70 ]; 70 ];
@@ -594,7 +594,7 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont @@ -594,7 +594,7 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
594 } 594 }
595 595
596 public displayAdvanced(): boolean { 596 public displayAdvanced(): boolean {
597 - return this.modelValue && this.modelValue.settingsSchema && this.modelValue.settingsSchema.schema; 597 + return !!this.modelValue && !!this.modelValue.settingsSchema && !!this.modelValue.settingsSchema.schema;
598 } 598 }
599 599
600 public removeDatasource(index: number) { 600 public removeDatasource(index: number) {
@@ -44,7 +44,7 @@ import { @@ -44,7 +44,7 @@ import {
44 Widget, 44 Widget,
45 WidgetActionDescriptor, 45 WidgetActionDescriptor,
46 widgetActionSources, 46 widgetActionSources,
47 - WidgetActionType, 47 + WidgetActionType, WidgetComparisonSettings,
48 WidgetResource, 48 WidgetResource,
49 widgetType, 49 widgetType,
50 WidgetTypeParameters 50 WidgetTypeParameters
@@ -847,9 +847,12 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI @@ -847,9 +847,12 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
847 const createSubscriptionSubject = new ReplaySubject(); 847 const createSubscriptionSubject = new ReplaySubject();
848 let options: WidgetSubscriptionOptions; 848 let options: WidgetSubscriptionOptions;
849 if (this.widget.type !== widgetType.rpc && this.widget.type !== widgetType.static) { 849 if (this.widget.type !== widgetType.rpc && this.widget.type !== widgetType.static) {
  850 + const comparisonSettings: WidgetComparisonSettings = this.widgetContext.settings;
850 options = { 851 options = {
851 type: this.widget.type, 852 type: this.widget.type,
852 - stateData: this.typeParameters.stateData 853 + stateData: this.typeParameters.stateData,
  854 + comparisonEnabled: comparisonSettings.comparisonEnabled,
  855 + timeForComparison: comparisonSettings.timeForComparison
853 }; 856 };
854 if (this.widget.type === widgetType.alarm) { 857 if (this.widget.type === widgetType.alarm) {
855 options.alarmSource = deepClone(this.widget.config.alarmSource); 858 options.alarmSource = deepClone(this.widget.config.alarmSource);
@@ -27,7 +27,7 @@ import { @@ -27,7 +27,7 @@ import {
27 widgetType, 27 widgetType,
28 WidgetTypeDescriptor, 28 WidgetTypeDescriptor,
29 WidgetTypeParameters, 29 WidgetTypeParameters,
30 - Widget 30 + Widget, JsonSettingsSchema
31 } from '@shared/models/widget.models'; 31 } from '@shared/models/widget.models';
32 import { Timewindow, WidgetTimewindow } from '@shared/models/time/time.models'; 32 import { Timewindow, WidgetTimewindow } from '@shared/models/time/time.models';
33 import { 33 import {
@@ -243,8 +243,8 @@ export interface WidgetConfigComponentData { @@ -243,8 +243,8 @@ export interface WidgetConfigComponentData {
243 typeParameters: WidgetTypeParameters; 243 typeParameters: WidgetTypeParameters;
244 actionSources: {[actionSourceId: string]: WidgetActionSource}; 244 actionSources: {[actionSourceId: string]: WidgetActionSource};
245 isDataEnabled: boolean; 245 isDataEnabled: boolean;
246 - settingsSchema: any;  
247 - dataKeySettingsSchema: any; 246 + settingsSchema: JsonSettingsSchema;
  247 + dataKeySettingsSchema: JsonSettingsSchema;
248 } 248 }
249 249
250 export const MissingWidgetType: WidgetInfo = { 250 export const MissingWidgetType: WidgetInfo = {
@@ -15,9 +15,8 @@ @@ -15,9 +15,8 @@
15 /// 15 ///
16 16
17 17
18 -export interface JsonFormComponentData { 18 +import { JsonSettingsSchema } from '@shared/models/widget.models';
  19 +
  20 +export interface JsonFormComponentData extends JsonSettingsSchema {
19 model?: any; 21 model?: any;
20 - schema?: any;  
21 - form?: any;  
22 - groupInfoes?: any[];  
23 } 22 }
@@ -42,6 +42,7 @@ import * as ReactDOM from 'react-dom'; @@ -42,6 +42,7 @@ import * as ReactDOM from 'react-dom';
42 import ReactSchemaForm from './react/json-form-react'; 42 import ReactSchemaForm from './react/json-form-react';
43 import JsonFormUtils from './react/json-form-utils'; 43 import JsonFormUtils from './react/json-form-utils';
44 import { JsonFormComponentData } from './json-form-component.models'; 44 import { JsonFormComponentData } from './json-form-component.models';
  45 +import { GroupInfo } from '@shared/models/widget.models';
45 46
46 const tinycolor = tinycolor_; 47 const tinycolor = tinycolor_;
47 48
@@ -94,7 +95,7 @@ export class JsonFormComponent implements OnInit, ControlValueAccessor, Validato @@ -94,7 +95,7 @@ export class JsonFormComponent implements OnInit, ControlValueAccessor, Validato
94 model: any; 95 model: any;
95 schema: any; 96 schema: any;
96 form: any; 97 form: any;
97 - groupInfoes: any[]; 98 + groupInfoes: GroupInfo[];
98 99
99 isModelValid = true; 100 isModelValid = true;
100 101
@@ -32,10 +32,11 @@ import ThingsboardImage from './json-form-image'; @@ -32,10 +32,11 @@ import ThingsboardImage from './json-form-image';
32 import ThingsboardCheckbox from './json-form-checkbox'; 32 import ThingsboardCheckbox from './json-form-checkbox';
33 import ThingsboardHelp from './json-form-help'; 33 import ThingsboardHelp from './json-form-help';
34 import ThingsboardFieldSet from './json-form-fieldset'; 34 import ThingsboardFieldSet from './json-form-fieldset';
35 -import { JsonFormProps, GroupInfo, JsonFormData, onChangeFn, OnColorClickFn } from './json-form.models'; 35 +import { JsonFormProps, JsonFormData, onChangeFn, OnColorClickFn } from './json-form.models';
36 36
37 import _ from 'lodash'; 37 import _ from 'lodash';
38 import * as tinycolor_ from 'tinycolor2'; 38 import * as tinycolor_ from 'tinycolor2';
  39 +import { GroupInfo } from '@shared/models/widget.models';
39 const tinycolor = tinycolor_; 40 const tinycolor = tinycolor_;
40 41
41 class ThingsboardSchemaForm extends React.Component<JsonFormProps, any> { 42 class ThingsboardSchemaForm extends React.Component<JsonFormProps, any> {
@@ -19,6 +19,7 @@ import * as equal from 'deep-equal'; @@ -19,6 +19,7 @@ import * as equal from 'deep-equal';
19 import ObjectPath from 'objectpath'; 19 import ObjectPath from 'objectpath';
20 import * as React from 'react'; 20 import * as React from 'react';
21 import * as tinycolor_ from 'tinycolor2'; 21 import * as tinycolor_ from 'tinycolor2';
  22 +import { GroupInfo } from '@shared/models/widget.models';
22 23
23 const tinycolor = tinycolor_; 24 const tinycolor = tinycolor_;
24 25
@@ -45,11 +46,6 @@ export interface DefaultsFormOptions { @@ -45,11 +46,6 @@ export interface DefaultsFormOptions {
45 ignore?: {[key: string]: boolean}; 46 ignore?: {[key: string]: boolean};
46 } 47 }
47 48
48 -export interface GroupInfo {  
49 - formIndex: number;  
50 - GroupTitle: string;  
51 -}  
52 -  
53 export type onChangeFn = (key: (string | number)[], val: any, forceUpdate?: boolean) => void; 49 export type onChangeFn = (key: (string | number)[], val: any, forceUpdate?: boolean) => void;
54 export type OnColorClickFn = (key: (string | number)[], val: tinycolor.ColorFormats.RGBA, 50 export type OnColorClickFn = (key: (string | number)[], val: tinycolor.ColorFormats.RGBA,
55 colorSelectedFn: (color: tinycolor.ColorFormats.RGBA) => void) => void; 51 colorSelectedFn: (color: tinycolor.ColorFormats.RGBA) => void) => void;
@@ -16,6 +16,9 @@ @@ -16,6 +16,9 @@
16 16
17 import { TimeService } from '@core/services/time.service'; 17 import { TimeService } from '@core/services/time.service';
18 import { deepClone, isDefined, isUndefined } from '@app/core/utils'; 18 import { deepClone, isDefined, isUndefined } from '@app/core/utils';
  19 +import * as moment_ from 'moment';
  20 +
  21 +const moment = moment_;
19 22
20 export const SECOND = 1000; 23 export const SECOND = 1000;
21 export const MINUTE = 60 * SECOND; 24 export const MINUTE = 60 * SECOND;
@@ -289,6 +292,31 @@ export function createSubscriptionTimewindow(timewindow: Timewindow, stDiff: num @@ -289,6 +292,31 @@ export function createSubscriptionTimewindow(timewindow: Timewindow, stDiff: num
289 return subscriptionTimewindow; 292 return subscriptionTimewindow;
290 } 293 }
291 294
  295 +export function createTimewindowForComparison(subscriptionTimewindow: SubscriptionTimewindow,
  296 + timeUnit: moment_.unitOfTime.DurationConstructor): SubscriptionTimewindow {
  297 + const timewindowForComparison: SubscriptionTimewindow = {
  298 + fixedWindow: null,
  299 + realtimeWindowMs: null,
  300 + aggregation: subscriptionTimewindow.aggregation
  301 + };
  302 +
  303 + if (subscriptionTimewindow.realtimeWindowMs) {
  304 + timewindowForComparison.startTs = moment(subscriptionTimewindow.startTs).subtract(1, timeUnit).valueOf();
  305 + timewindowForComparison.realtimeWindowMs = subscriptionTimewindow.realtimeWindowMs;
  306 + } else if (subscriptionTimewindow.fixedWindow) {
  307 + const timeInterval = subscriptionTimewindow.fixedWindow.endTimeMs - subscriptionTimewindow.fixedWindow.startTimeMs;
  308 + const endTimeMs = moment(subscriptionTimewindow.fixedWindow.endTimeMs).subtract(1, timeUnit).valueOf();
  309 +
  310 + timewindowForComparison.startTs = endTimeMs - timeInterval;
  311 + timewindowForComparison.fixedWindow = {
  312 + startTimeMs: timewindowForComparison.startTs,
  313 + endTimeMs
  314 + };
  315 + }
  316 +
  317 + return timewindowForComparison;
  318 +}
  319 +
292 export function cloneSelectedTimewindow(timewindow: Timewindow): Timewindow { 320 export function cloneSelectedTimewindow(timewindow: Timewindow): Timewindow {
293 const cloned: Timewindow = {}; 321 const cloned: Timewindow = {};
294 if (isDefined(timewindow.selectedTab)) { 322 if (isDefined(timewindow.selectedTab)) {
@@ -20,9 +20,9 @@ import { WidgetTypeId } from '@shared/models/id/widget-type-id'; @@ -20,9 +20,9 @@ import { WidgetTypeId } from '@shared/models/id/widget-type-id';
20 import { Timewindow } from '@shared/models/time/time.models'; 20 import { Timewindow } from '@shared/models/time/time.models';
21 import { EntityType } from '@shared/models/entity-type.models'; 21 import { EntityType } from '@shared/models/entity-type.models';
22 import { AlarmSearchStatus } from '@shared/models/alarm.models'; 22 import { AlarmSearchStatus } from '@shared/models/alarm.models';
23 -import { Data } from '@angular/router';  
24 import { DataKeyType } from './telemetry/telemetry.models'; 23 import { DataKeyType } from './telemetry/telemetry.models';
25 import { EntityId } from '@shared/models/id/entity-id'; 24 import { EntityId } from '@shared/models/id/entity-id';
  25 +import * as moment_ from 'moment';
26 26
27 export enum widgetType { 27 export enum widgetType {
28 timeseries = 'timeseries', 28 timeseries = 'timeseries',
@@ -261,6 +261,7 @@ export interface Datasource { @@ -261,6 +261,7 @@ export interface Datasource {
261 entityLabel?: string; 261 entityLabel?: string;
262 entityDescription?: string; 262 entityDescription?: string;
263 generated?: boolean; 263 generated?: boolean;
  264 + isAdditional?: boolean;
264 [key: string]: any; 265 [key: string]: any;
265 } 266 }
266 267
@@ -331,6 +332,11 @@ export interface WidgetActionDescriptor extends CustomActionDescriptor { @@ -331,6 +332,11 @@ export interface WidgetActionDescriptor extends CustomActionDescriptor {
331 stateEntityParamName?: string; 332 stateEntityParamName?: string;
332 } 333 }
333 334
  335 +export interface WidgetComparisonSettings {
  336 + comparisonEnabled?: boolean;
  337 + timeForComparison?: moment_.unitOfTime.DurationConstructor;
  338 +}
  339 +
334 export interface WidgetConfig { 340 export interface WidgetConfig {
335 title?: string; 341 title?: string;
336 titleIcon?: string; 342 titleIcon?: string;
@@ -382,14 +388,22 @@ export interface Widget { @@ -382,14 +388,22 @@ export interface Widget {
382 config: WidgetConfig; 388 config: WidgetConfig;
383 } 389 }
384 390
  391 +export interface GroupInfo {
  392 + formIndex: number;
  393 + GroupTitle: string;
  394 +}
  395 +
  396 +export interface JsonSchema {
  397 + type: string;
  398 + title?: string;
  399 + properties: {[key: string]: any};
  400 + required?: string[];
  401 +}
  402 +
385 export interface JsonSettingsSchema { 403 export interface JsonSettingsSchema {
386 - schema?: {  
387 - type: string;  
388 - title: string;  
389 - properties: {[key: string]: any};  
390 - required?: string[];  
391 - }; 404 + schema?: JsonSchema;
392 form?: any[]; 405 form?: any[];
  406 + groupInfoes?: GroupInfo[]
393 } 407 }
394 408
395 export interface WidgetPosition { 409 export interface WidgetPosition {
@@ -24,6 +24,7 @@ interface JQueryPlot extends jquery.flot.plot { @@ -24,6 +24,7 @@ interface JQueryPlot extends jquery.flot.plot {
24 interface JQueryPlotPoint extends jquery.flot.point { 24 interface JQueryPlotPoint extends jquery.flot.point {
25 pageX: number; 25 pageX: number;
26 pageY: number; 26 pageY: number;
  27 + x2: number;
27 } 28 }
28 29
29 interface JQueryPlotDataSeries extends jquery.flot.dataSeries, JQueryPlotSeriesOptions { 30 interface JQueryPlotDataSeries extends jquery.flot.dataSeries, JQueryPlotSeriesOptions {