Showing
16 changed files
with
681 additions
and
138 deletions
@@ -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 { |