Commit 2f11d4336f03ef461ac3fda43ad420a216ae6387

Authored by Igor Kulikov
1 parent eda23bbf

Merge data comparison feature

... ... @@ -55,6 +55,7 @@
55 55 "node_modules/flot/src/plugins/jquery.flot.pie.js",
56 56 "node_modules/flot/src/plugins/jquery.flot.crosshair.js",
57 57 "node_modules/flot/src/plugins/jquery.flot.stack.js",
  58 + "node_modules/flot/src/plugins/jquery.flot.symbol.js",
58 59 "node_modules/flot.curvedlines/curvedLines.js",
59 60 "node_modules/tinycolor2/dist/tinycolor-min.js",
60 61 "node_modules/tooltipster/dist/js/tooltipster.bundle.min.js",
... ...
... ... @@ -17,12 +17,15 @@
17 17 import { Observable } from 'rxjs';
18 18 import { EntityId } from '@app/shared/models/id/entity-id';
19 19 import {
20   - WidgetActionDescriptor,
21   - widgetType,
  20 + DataSet,
  21 + Datasource,
  22 + DatasourceData,
  23 + DatasourceType,
  24 + KeyInfo,
22 25 LegendConfig,
23 26 LegendData,
24   - Datasource,
25   - DatasourceData, DataSet, DatasourceType, KeyInfo
  27 + WidgetActionDescriptor,
  28 + widgetType
26 29 } from '@shared/models/widget.models';
27 30 import { TimeService } from '../services/time.service';
28 31 import { DeviceService } from '../http/device.service';
... ... @@ -36,10 +39,8 @@ import { DatasourceService } from '@core/api/datasource.service';
36 39 import { RafService } from '@core/services/raf.service';
37 40 import { EntityAliases } from '@shared/models/alias.models';
38 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 42 import { IDashboardComponent } from '@home/models/dashboard-component.models';
  43 +import * as moment_ from 'moment';
43 44
44 45 export interface TimewindowFunctions {
45 46 onUpdateTimewindow: (startTimeMs: number, endTimeMs: number, interval?: number) => void;
... ... @@ -200,6 +201,8 @@ export interface WidgetSubscriptionOptions {
200 201 timeWindowConfig?: Timewindow;
201 202 dashboardTimewindow?: Timewindow;
202 203 legendConfig?: LegendConfig;
  204 + comparisonEnabled?: boolean;
  205 + timeForComparison?: moment_.unitOfTime.DurationConstructor;
203 206 decimals?: number;
204 207 units?: string;
205 208 callbacks?: WidgetSubscriptionCallbacks;
... ... @@ -227,6 +230,7 @@ export interface IWidgetSubscription {
227 230 hiddenData?: Array<{data: DataSet}>;
228 231 timeWindowConfig?: Timewindow;
229 232 timeWindow?: WidgetTimewindow;
  233 + comparisonTimeWindow?: WidgetTimewindow;
230 234
231 235 alarms?: Array<AlarmInfo>;
232 236 alarmSource?: Datasource;
... ...
... ... @@ -22,6 +22,7 @@ import {
22 22 WidgetSubscriptionOptions
23 23 } from '@core/api/widget-api.models';
24 24 import {
  25 + DataKey,
25 26 DataSet,
26 27 DataSetHolder,
27 28 Datasource,
... ... @@ -36,6 +37,7 @@ import {
36 37 import { HttpErrorResponse } from '@angular/common/http';
37 38 import {
38 39 createSubscriptionTimewindow,
  40 + createTimewindowForComparison,
39 41 SubscriptionTimewindow,
40 42 Timewindow,
41 43 toHistoryTimewindow,
... ... @@ -52,6 +54,9 @@ import * as deepEqual from 'deep-equal';
52 54 import { EntityId } from '@app/shared/models/id/entity-id';
53 55 import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
54 56 import { entityFields } from '@shared/models/entity.models';
  57 +import * as moment_ from 'moment';
  58 +
  59 +const moment = moment_;
55 60
56 61 export class WidgetSubscription implements IWidgetSubscription {
57 62
... ... @@ -77,6 +82,10 @@ export class WidgetSubscription implements IWidgetSubscription {
77 82 stateData: boolean;
78 83 decimals: number;
79 84 units: string;
  85 + comparisonEnabled: boolean;
  86 + timeForComparison: moment_.unitOfTime.DurationConstructor;
  87 + comparisonTimeWindow: WidgetTimewindow;
  88 + timewindowForComparison: SubscriptionTimewindow;
80 89
81 90 alarms: Array<AlarmInfo>;
82 91 alarmSource: Datasource;
... ... @@ -204,6 +213,13 @@ export class WidgetSubscription implements IWidgetSubscription {
204 213 }
205 214
206 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 224 this.units = options.units || '';
209 225 this.decimals = isDefined(options.decimals) ? options.decimals : 2;
... ... @@ -345,11 +361,23 @@ export class WidgetSubscription implements IWidgetSubscription {
345 361 }
346 362
347 363 private configureData() {
  364 + const additionalDatasources: Datasource[] = [];
348 365 let dataIndex = 0;
  366 + let additionalKeysNumber = 0;
349 367 this.datasources.forEach((datasource) => {
  368 + const additionalDataKeys: DataKey[] = [];
  369 + let datasourceAdditionalKeysNumber = 0;
350 370 datasource.dataKeys.forEach((dataKey) => {
351 371 dataKey.hidden = false;
352 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 381 const datasourceData: DatasourceData = {
354 382 datasource,
355 383 dataKey,
... ... @@ -379,7 +407,43 @@ export class WidgetSubscription implements IWidgetSubscription {
379 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 447 if (this.displayLegend) {
384 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 742 this.notifyDataLoading();
679 743 if (this.type === widgetType.timeseries && this.timeWindowConfig) {
680 744 this.updateRealtimeSubscription();
  745 + if (this.comparisonEnabled) {
  746 + this.updateSubscriptionForComparison();
  747 + }
681 748 if (this.subscriptionTimewindow.fixedWindow) {
682 749 this.onDataUpdated();
683 750 }
... ... @@ -702,6 +769,17 @@ export class WidgetSubscription implements IWidgetSubscription {
702 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 783 let entityFieldKey = false;
706 784
707 785 for (let a = 0; a < datasource.dataKeys.length; a++) {
... ... @@ -842,7 +920,7 @@ export class WidgetSubscription implements IWidgetSubscription {
842 920 private updateTimewindow() {
843 921 this.timeWindow.interval = this.subscriptionTimewindow.aggregation.interval || 1000;
844 922 if (this.subscriptionTimewindow.realtimeWindowMs) {
845   - this.timeWindow.maxTime = Date.now() + this.timeWindow.stDiff;
  923 + this.timeWindow.maxTime = moment().valueOf() + this.timeWindow.stDiff;
846 924 this.timeWindow.minTime = this.timeWindow.maxTime - this.subscriptionTimewindow.realtimeWindowMs;
847 925 } else if (this.subscriptionTimewindow.fixedWindow) {
848 926 this.timeWindow.maxTime = this.subscriptionTimewindow.fixedWindow.endTimeMs;
... ... @@ -862,6 +940,26 @@ export class WidgetSubscription implements IWidgetSubscription {
862 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 963 private dataUpdated(sourceData: DataSetHolder, datasourceIndex: number, dataKeyIndex: number, detectChanges: boolean) {
866 964 for (let x = 0; x < this.datasourceListeners.length; x++) {
867 965 this.datasources[x].dataReceived = this.datasources[x].dataReceived === true;
... ... @@ -892,6 +990,9 @@ export class WidgetSubscription implements IWidgetSubscription {
892 990 if (update) {
893 991 if (this.subscriptionTimewindow && this.subscriptionTimewindow.realtimeWindowMs) {
894 992 this.updateTimewindow();
  993 + if (this.timewindowForComparison && this.timewindowForComparison.realtimeWindowMs) {
  994 + this.updateComparisonTimewindow();
  995 + }
895 996 }
896 997 currentData.data = sourceData.data;
897 998 if (this.caulculateLegendData) {
... ...
... ... @@ -359,6 +359,26 @@ export class UtilsService {
359 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 382 public createLabelFromDatasource(datasource: Datasource, pattern: string) {
363 383 let label = pattern;
364 384 if (!datasource) {
... ...
... ... @@ -17,7 +17,8 @@
17 17 // tslint:disable-next-line:no-reference
18 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 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 30
30 31 export interface TbFlotSeries extends DatasourceData, JQueryPlotSeriesOptions {
31 32 dataKey: TbFlotDataKey;
  33 + xaxisIndex?: number;
32 34 yaxisIndex?: number;
33 35 yaxis?: number;
34 36 }
... ... @@ -50,6 +52,7 @@ export interface TbFlotAxisOptions extends JQueryPlotAxisOptions {
50 52 }
51 53
52 54 export interface TbFlotPlotDataSeries extends JQueryPlotDataSeries {
  55 + datasource?: Datasource;
53 56 dataKey?: TbFlotDataKey;
54 57 percent?: number;
55 58 }
... ... @@ -93,6 +96,12 @@ export interface TbFlotXAxisSettings {
93 96 color: boolean;
94 97 }
95 98
  99 +export interface TbFlotSecondXAxisSettings {
  100 + axisPosition: TbFlotXAxisPosition;
  101 + showLabels: boolean;
  102 + title: string;
  103 +}
  104 +
96 105 export interface TbFlotYAxisSettings {
97 106 min: number;
98 107 max: number;
... ... @@ -112,16 +121,23 @@ export interface TbFlotBaseSettings {
112 121 tooltipIndividual: boolean;
113 122 tooltipCumulative: boolean;
114 123 tooltipValueFormatter: string;
  124 + hideZeros: boolean;
115 125 grid: TbFlotGridSettings;
116 126 xaxis: TbFlotXAxisSettings;
117 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 137 smoothLines: boolean;
122 138 }
123 139
124   -export interface TbFlotBarSettings extends TbFlotBaseSettings {
  140 +export interface TbFlotBarSettings extends TbFlotBaseSettings, TbFlotComparisonSettings {
125 141 defaultBarWidth: number;
126 142 }
127 143
... ... @@ -140,11 +156,17 @@ export interface TbFlotPieSettings {
140 156 }
141 157
142 158 export declare type TbFlotYAxisPosition = 'left' | 'right';
  159 +export declare type TbFlotXAxisPosition = 'top' | 'bottom';
143 160
144 161 export interface TbFlotKeySettings {
  162 + excludeFromStacking: boolean;
145 163 showLines: boolean;
146 164 fillLines: boolean;
147 165 showPoints: boolean;
  166 + showPointShape: string;
  167 + pointShapeFormatter: string;
  168 + showPointsLineWidth: number;
  169 + showPointsRadius: number;
148 170 lineWidth: number;
149 171 tooltipValueFormatter: string;
150 172 showSeparateAxis: boolean;
... ... @@ -218,6 +240,11 @@ export function flotSettingsSchema(chartType: ChartType): JsonSettingsSchema {
218 240 type: 'string',
219 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 249 properties.grid = {
223 250 title: 'Grid settings',
... ... @@ -345,6 +372,7 @@ export function flotSettingsSchema(chartType: ChartType): JsonSettingsSchema {
345 372 key: 'tooltipValueFormatter',
346 373 type: 'javascript'
347 374 });
  375 + schema.form.push('hideZeros');
348 376 schema.form.push({
349 377 key: 'grid',
350 378 items: [
... ... @@ -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 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 532 export const flotPieSettingsSchema: JsonSettingsSchema = {
402 533 schema: {
403 534 type: 'object',
... ... @@ -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 617 schema: {
487 618 type: 'object',
488 619 title: 'DataKeySettings',
489 620 properties: {
  621 + excludeFromStacking: {
  622 + title: 'Exclude from stacking(available in "Stacking" mode)',
  623 + type: 'boolean',
  624 + default: false
  625 + },
490 626 showLines: {
491 627 title: 'Show lines',
492 628 type: 'boolean',
... ... @@ -502,10 +638,29 @@ export function flotDatakeySettingsSchema(defaultShowLines: boolean): JsonSettin
502 638 type: 'boolean',
503 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 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 665 tooltipValueFormatter: {
511 666 title: 'Tooltip value format function, f(value)',
... ... @@ -556,10 +711,48 @@ export function flotDatakeySettingsSchema(defaultShowLines: boolean): JsonSettin
556 711 required: ['showLines', 'fillLines', 'showPoints']
557 712 },
558 713 form: [
  714 + 'excludeFromStacking',
559 715 'showLines',
560 716 'fillLines',
561 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 756 key: 'tooltipValueFormatter',
564 757 type: 'javascript'
565 758 },
... ... @@ -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 41 import { AggregationType } from '@shared/models/time/time.models';
42 42 import { CancelAnimationFrame } from '@core/services/raf.service';
43 43 import Timeout = NodeJS.Timeout;
  44 +import { UtilsService } from '@core/services/utils.service';
44 45
45 46 const tinycolor = tinycolor_;
46 47 const moment = moment_;
... ... @@ -56,6 +57,7 @@ export class TbFlot {
56 57 private readonly yAxisTickFormatter: TbFlotTicksFormatterFunction;
57 58 private ticksFormatterFunction: TbFlotTicksFormatterFunction;
58 59 private readonly yaxis: TbFlotAxisOptions;
  60 + private readonly xaxis: TbFlotAxisOptions;
59 61 private yaxes: Array<TbFlotAxisOptions>;
60 62
61 63 private readonly options: JQueryPlotOptions;
... ... @@ -66,6 +68,7 @@ export class TbFlot {
66 68 private readonly trackDecimals: number;
67 69 private readonly tooltipIndividual: boolean;
68 70 private readonly tooltipCumulative: boolean;
  71 + private readonly hideZeros: boolean;
69 72
70 73 private readonly defaultBarWidth: number;
71 74
... ... @@ -106,13 +109,14 @@ export class TbFlot {
106 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 116 constructor(private ctx: WidgetContext, private readonly chartType: ChartType) {
114 117 this.chartType = this.chartType || 'line';
115 118 this.settings = ctx.settings as TbFlotSettings;
  119 + const utils = this.ctx.$injector.get(UtilsService);
116 120 this.tooltip = $('#flot-series-tooltip');
117 121 if (this.tooltip.length === 0) {
118 122 this.tooltip = this.createTooltipElement();
... ... @@ -123,6 +127,7 @@ export class TbFlot {
123 127 this.tooltipIndividual = this.chartType === 'pie' || (isDefined(this.settings.tooltipIndividual)
124 128 ? this.settings.tooltipIndividual : false);
125 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 132 const font = {
128 133 color: this.settings.fontColor || '#545454',
... ... @@ -147,7 +152,8 @@ export class TbFlot {
147 152 };
148 153
149 154 if (this.chartType === 'line' || this.chartType === 'bar' || this.chartType === 'state') {
150   - this.options.xaxis = {
  155 + this.options.xaxes = [];
  156 + this.xaxis = {
151 157 mode: 'time',
152 158 timezone: 'browser',
153 159 font: deepClone(font),
... ... @@ -158,16 +164,11 @@ export class TbFlot {
158 164 labelFont: deepClone(font)
159 165 };
160 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 174 this.yAxisTickFormatter = this.formatYAxisTicks.bind(this);
... ... @@ -178,7 +179,7 @@ export class TbFlot {
178 179 this.yaxis.font.color = this.settings.yaxis.color || this.yaxis.font.color;
179 180 this.yaxis.min = isDefined(this.settings.yaxis.min) ? this.settings.yaxis.min : null;
180 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 183 this.yaxis.labelFont.color = this.yaxis.font.color;
183 184 this.yaxis.labelFont.size = this.yaxis.font.size + 2;
184 185 this.yaxis.labelFont.weight = 'bold';
... ... @@ -212,7 +213,7 @@ export class TbFlot {
212 213 this. options.grid.borderWidth = isDefined(this.settings.grid.outlineWidth) ?
213 214 this.settings.grid.outlineWidth : 1;
214 215 if (this.settings.grid.verticalLines === false) {
215   - this.options.xaxis.tickLength = 0;
  216 + this.xaxis.tickLength = 0;
216 217 }
217 218 if (this.settings.grid.horizontalLines === false) {
218 219 this.yaxis.tickLength = 0;
... ... @@ -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 260 this.options.crosshair = {
229 261 mode: 'x'
230 262 };
231 263
232   - this.options.series = {
233   - stack: this.settings.stack === true
234   - };
235   -
236 264 if (this.chartType === 'line' && this.settings.smoothLines) {
237 265 this.options.series.curvedLines = {
238 266 active: true,
... ... @@ -339,6 +367,13 @@ export class TbFlot {
339 367 series.lines = {
340 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 377 if (this.chartType === 'line' || this.chartType === 'state') {
343 378 series.lines.show = keySettings.showLines !== false;
344 379 } else {
... ... @@ -353,8 +388,16 @@ export class TbFlot {
353 388 };
354 389 if (keySettings.showPoints === true) {
355 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 402 if (this.chartType === 'line' && this.settings.smoothLines && !series.points.show) {
360 403 series.curvedLines = {
... ... @@ -367,6 +410,14 @@ export class TbFlot {
367 410
368 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 421 if (this.yaxis) {
371 422 const units = series.dataKey.units && series.dataKey.units.length ? series.dataKey.units : this.trackUnits;
372 423 let yaxis: TbFlotAxisOptions;
... ... @@ -384,7 +435,7 @@ export class TbFlot {
384 435 series.yaxisIndex = this.yaxes.indexOf(yaxis);
385 436 series.yaxis = series.yaxisIndex + 1;
386 437 yaxis.keysInfo[i] = {hidden: false};
387   - yaxis.hidden = false;
  438 + yaxis.show = true;
388 439 }
389 440 }
390 441 this.options.colors = colors;
... ... @@ -398,8 +449,12 @@ export class TbFlot {
398 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 460 this.checkMouseEvents();
... ... @@ -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 533 if (this.chartType === 'bar') {
475 534 if (this.subscription.timeWindowConfig.aggregation &&
476 535 this.subscription.timeWindowConfig.aggregation.type === AggregationType.NONE) {
... ... @@ -485,6 +544,10 @@ export class TbFlot {
485 544 } else {
486 545 this.plot.getOptions().xaxes[0].min = this.subscription.timeWindow.minTime;
487 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 551 if (this.chartType === 'bar') {
489 552 if (this.subscription.timeWindowConfig.aggregation &&
490 553 this.subscription.timeWindowConfig.aggregation.type === AggregationType.NONE) {
... ... @@ -653,7 +716,7 @@ export class TbFlot {
653 716 divElement.css({
654 717 display: 'flex',
655 718 alignItems: 'center',
656   - justifyContent: 'flex-start'
  719 + justifyContent: 'space-between'
657 720 });
658 721 const lineSpan = $('<span></span>');
659 722 lineSpan.css({
... ... @@ -734,57 +797,98 @@ export class TbFlot {
734 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 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 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 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 893 return content;
790 894 }
... ... @@ -848,16 +952,21 @@ export class TbFlot {
848 952 const pageY = pos.pageY;
849 953
850 954 let tooltipHtml;
851   - let hoverInfo: TbFlotHoverInfo;
  955 + let hoverInfo: TbFlotHoverInfo[];
852 956
853 957 if (this.chartType === 'pie') {
854 958 tooltipHtml = this.formatPieTooltip(item);
855 959 } else {
856 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 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 970 tooltipHtml = this.formatChartTooltip(hoverInfo, item ? item.seriesIndex : -1);
862 971 }
863 972 }
... ... @@ -883,8 +992,10 @@ export class TbFlot {
883 992 left
884 993 });
885 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 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 1042 let i: number;
932 1043 let series: TbFlotPlotDataSeries;
933 1044 let hoverIndex: number;
... ... @@ -935,19 +1046,40 @@ export class TbFlot {
935 1046 let minDistance: number;
936 1047 let pointTime: any;
937 1048 let minTime: any;
  1049 + let minTimeHistorical: any;
  1050 + let hoverData: TbFlotSeriesHoverInfo;
938 1051 let value: any;
939 1052 let lastValue: any;
940   - const results: TbFlotHoverInfo = {
  1053 + let minDistanceHistorical: number;
  1054 + const results: TbFlotHoverInfo[] = [{
941 1055 seriesHover: []
942   - };
  1056 + }];
  1057 + if (this.settings.comparisonEnabled) {
  1058 + results.push({
  1059 + seriesHover: []
  1060 + });
  1061 + }
943 1062 for (i = 0; i < seriesList.length; i++) {
944 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 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 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 1083 || (hoverDistance >= 0 && (hoverDistance < minDistance || minDistance < 0))
952 1084 || (hoverDistance < 0 && hoverDistance > minDistance)) {
953 1085 minDistance = hoverDistance;
... ... @@ -964,23 +1096,33 @@ export class TbFlot {
964 1096 value = series.data[hoverIndex][1];
965 1097 }
966 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 1126 return results;
985 1127 }
986 1128
... ...
... ... @@ -23,7 +23,7 @@ import {
23 23 Datasource,
24 24 DatasourceType,
25 25 datasourceTypeTranslationMap,
26   - defaultLegendConfig,
  26 + defaultLegendConfig, GroupInfo, JsonSchema,
27 27 widgetType
28 28 } from '@shared/models/widget.models';
29 29 import {
... ... @@ -60,11 +60,11 @@ import { WidgetActionsData } from './action/manage-widget-actions.component.mode
60 60 import { DashboardState } from '@shared/models/dashboard.models';
61 61 import { entityFields } from '@shared/models/entity.models';
62 62
63   -const emptySettingsSchema = {
  63 +const emptySettingsSchema: JsonSchema = {
64 64 type: 'object',
65 65 properties: {}
66 66 };
67   -const emptySettingsGroupInfoes = [];
  67 +const emptySettingsGroupInfoes: GroupInfo[] = [];
68 68 const defaultSettingsForm = [
69 69 '*'
70 70 ];
... ... @@ -594,7 +594,7 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
594 594 }
595 595
596 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 600 public removeDatasource(index: number) {
... ...
... ... @@ -44,7 +44,7 @@ import {
44 44 Widget,
45 45 WidgetActionDescriptor,
46 46 widgetActionSources,
47   - WidgetActionType,
  47 + WidgetActionType, WidgetComparisonSettings,
48 48 WidgetResource,
49 49 widgetType,
50 50 WidgetTypeParameters
... ... @@ -847,9 +847,12 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
847 847 const createSubscriptionSubject = new ReplaySubject();
848 848 let options: WidgetSubscriptionOptions;
849 849 if (this.widget.type !== widgetType.rpc && this.widget.type !== widgetType.static) {
  850 + const comparisonSettings: WidgetComparisonSettings = this.widgetContext.settings;
850 851 options = {
851 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 857 if (this.widget.type === widgetType.alarm) {
855 858 options.alarmSource = deepClone(this.widget.config.alarmSource);
... ...
... ... @@ -27,7 +27,7 @@ import {
27 27 widgetType,
28 28 WidgetTypeDescriptor,
29 29 WidgetTypeParameters,
30   - Widget
  30 + Widget, JsonSettingsSchema
31 31 } from '@shared/models/widget.models';
32 32 import { Timewindow, WidgetTimewindow } from '@shared/models/time/time.models';
33 33 import {
... ... @@ -243,8 +243,8 @@ export interface WidgetConfigComponentData {
243 243 typeParameters: WidgetTypeParameters;
244 244 actionSources: {[actionSourceId: string]: WidgetActionSource};
245 245 isDataEnabled: boolean;
246   - settingsSchema: any;
247   - dataKeySettingsSchema: any;
  246 + settingsSchema: JsonSettingsSchema;
  247 + dataKeySettingsSchema: JsonSettingsSchema;
248 248 }
249 249
250 250 export const MissingWidgetType: WidgetInfo = {
... ...
... ... @@ -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 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 42 import ReactSchemaForm from './react/json-form-react';
43 43 import JsonFormUtils from './react/json-form-utils';
44 44 import { JsonFormComponentData } from './json-form-component.models';
  45 +import { GroupInfo } from '@shared/models/widget.models';
45 46
46 47 const tinycolor = tinycolor_;
47 48
... ... @@ -94,7 +95,7 @@ export class JsonFormComponent implements OnInit, ControlValueAccessor, Validato
94 95 model: any;
95 96 schema: any;
96 97 form: any;
97   - groupInfoes: any[];
  98 + groupInfoes: GroupInfo[];
98 99
99 100 isModelValid = true;
100 101
... ...
... ... @@ -32,10 +32,11 @@ import ThingsboardImage from './json-form-image';
32 32 import ThingsboardCheckbox from './json-form-checkbox';
33 33 import ThingsboardHelp from './json-form-help';
34 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 37 import _ from 'lodash';
38 38 import * as tinycolor_ from 'tinycolor2';
  39 +import { GroupInfo } from '@shared/models/widget.models';
39 40 const tinycolor = tinycolor_;
40 41
41 42 class ThingsboardSchemaForm extends React.Component<JsonFormProps, any> {
... ...
... ... @@ -19,6 +19,7 @@ import * as equal from 'deep-equal';
19 19 import ObjectPath from 'objectpath';
20 20 import * as React from 'react';
21 21 import * as tinycolor_ from 'tinycolor2';
  22 +import { GroupInfo } from '@shared/models/widget.models';
22 23
23 24 const tinycolor = tinycolor_;
24 25
... ... @@ -45,11 +46,6 @@ export interface DefaultsFormOptions {
45 46 ignore?: {[key: string]: boolean};
46 47 }
47 48
48   -export interface GroupInfo {
49   - formIndex: number;
50   - GroupTitle: string;
51   -}
52   -
53 49 export type onChangeFn = (key: (string | number)[], val: any, forceUpdate?: boolean) => void;
54 50 export type OnColorClickFn = (key: (string | number)[], val: tinycolor.ColorFormats.RGBA,
55 51 colorSelectedFn: (color: tinycolor.ColorFormats.RGBA) => void) => void;
... ...
... ... @@ -16,6 +16,9 @@
16 16
17 17 import { TimeService } from '@core/services/time.service';
18 18 import { deepClone, isDefined, isUndefined } from '@app/core/utils';
  19 +import * as moment_ from 'moment';
  20 +
  21 +const moment = moment_;
19 22
20 23 export const SECOND = 1000;
21 24 export const MINUTE = 60 * SECOND;
... ... @@ -289,6 +292,31 @@ export function createSubscriptionTimewindow(timewindow: Timewindow, stDiff: num
289 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 320 export function cloneSelectedTimewindow(timewindow: Timewindow): Timewindow {
293 321 const cloned: Timewindow = {};
294 322 if (isDefined(timewindow.selectedTab)) {
... ...
... ... @@ -20,9 +20,9 @@ import { WidgetTypeId } from '@shared/models/id/widget-type-id';
20 20 import { Timewindow } from '@shared/models/time/time.models';
21 21 import { EntityType } from '@shared/models/entity-type.models';
22 22 import { AlarmSearchStatus } from '@shared/models/alarm.models';
23   -import { Data } from '@angular/router';
24 23 import { DataKeyType } from './telemetry/telemetry.models';
25 24 import { EntityId } from '@shared/models/id/entity-id';
  25 +import * as moment_ from 'moment';
26 26
27 27 export enum widgetType {
28 28 timeseries = 'timeseries',
... ... @@ -261,6 +261,7 @@ export interface Datasource {
261 261 entityLabel?: string;
262 262 entityDescription?: string;
263 263 generated?: boolean;
  264 + isAdditional?: boolean;
264 265 [key: string]: any;
265 266 }
266 267
... ... @@ -331,6 +332,11 @@ export interface WidgetActionDescriptor extends CustomActionDescriptor {
331 332 stateEntityParamName?: string;
332 333 }
333 334
  335 +export interface WidgetComparisonSettings {
  336 + comparisonEnabled?: boolean;
  337 + timeForComparison?: moment_.unitOfTime.DurationConstructor;
  338 +}
  339 +
334 340 export interface WidgetConfig {
335 341 title?: string;
336 342 titleIcon?: string;
... ... @@ -382,14 +388,22 @@ export interface Widget {
382 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 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 405 form?: any[];
  406 + groupInfoes?: GroupInfo[]
393 407 }
394 408
395 409 export interface WidgetPosition {
... ...
... ... @@ -24,6 +24,7 @@ interface JQueryPlot extends jquery.flot.plot {
24 24 interface JQueryPlotPoint extends jquery.flot.point {
25 25 pageX: number;
26 26 pageY: number;
  27 + x2: number;
27 28 }
28 29
29 30 interface JQueryPlotDataSeries extends jquery.flot.dataSeries, JQueryPlotSeriesOptions {
... ...