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