Browse Source

Implement DataSource service, Data aggregator, Flot widget.

pull/2147/head
Igor Kulikov 7 years ago
parent
commit
2bdde37535
  1. 9
      ui-ngx/angular.json
  2. 28
      ui-ngx/package-lock.json
  3. 5
      ui-ngx/package.json
  4. 332
      ui-ngx/src/app/core/api/data-aggregator.ts
  5. 666
      ui-ngx/src/app/core/api/datasource-subcription.ts
  6. 71
      ui-ngx/src/app/core/api/datasource.service.ts
  7. 19
      ui-ngx/src/app/core/api/widget-api.models.ts
  8. 323
      ui-ngx/src/app/core/api/widget-subscription.ts
  9. 31
      ui-ngx/src/app/core/services/utils.service.ts
  10. 36
      ui-ngx/src/app/core/utils.ts
  11. 7
      ui-ngx/src/app/core/ws/telemetry-websocket.service.ts
  12. 10
      ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.ts
  13. 4
      ui-ngx/src/app/modules/home/components/entity/entities-table.component.ts
  14. 4
      ui-ngx/src/app/modules/home/components/widget/dynamic-widget.component.ts
  15. 4
      ui-ngx/src/app/modules/home/components/widget/legend.component.scss
  16. 6
      ui-ngx/src/app/modules/home/components/widget/legend.component.ts
  17. 592
      ui-ngx/src/app/modules/home/components/widget/lib/flot-widget.models.ts
  18. 1074
      ui-ngx/src/app/modules/home/components/widget/lib/flot-widget.ts
  19. 3
      ui-ngx/src/app/modules/home/components/widget/widget-component.service.ts
  20. 14
      ui-ngx/src/app/modules/home/components/widget/widget.component.html
  21. 46
      ui-ngx/src/app/modules/home/components/widget/widget.component.ts
  22. 3
      ui-ngx/src/app/modules/home/models/dashboard-component.models.ts
  23. 6
      ui-ngx/src/app/modules/home/models/widget-component.models.ts
  24. 13
      ui-ngx/src/app/shared/models/telemetry/telemetry.models.ts
  25. 84
      ui-ngx/src/app/shared/models/time/time.models.ts
  26. 27
      ui-ngx/src/app/shared/models/widget.models.ts
  27. 4
      ui-ngx/src/styles.scss
  28. 2
      ui-ngx/src/tsconfig.app.json
  29. 128
      ui-ngx/src/typings/jquery.flot.typings.d.ts
  30. 0
      ui-ngx/src/typings/jquery.typings.d.ts
  31. 3
      ui-ngx/tsconfig.json

9
ui-ngx/angular.json

@ -38,6 +38,15 @@
"node_modules/javascript-detect-element-resize/detect-element-resize.js",
"node_modules/jquery/dist/jquery.min.js",
"node_modules/jquery.terminal/js/jquery.terminal.min.js",
"node_modules/flot/lib/jquery.colorhelpers.js",
"node_modules/flot/src/jquery.flot.js",
"node_modules/flot/src/plugins/jquery.flot.time.js",
"node_modules/flot/src/plugins/jquery.flot.selection.js",
"node_modules/flot/src/plugins/jquery.flot.pie.js",
"node_modules/flot/src/plugins/jquery.flot.crosshair.js",
"node_modules/flot/src/plugins/jquery.flot.stack.js",
"node_modules/flot.curvedlines/curvedLines.js",
"node_modules/tinycolor2/dist/tinycolor-min.js",
"node_modules/ace-builds/src-min/ace.js",
"node_modules/ace-builds/src-min/ext-language_tools.js",
"node_modules/ace-builds/src-min/ext-searchbox.js",

28
ui-ngx/package-lock.json

@ -1126,6 +1126,15 @@
"integrity": "sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g==",
"dev": true
},
"@types/flot": {
"version": "0.0.31",
"resolved": "https://registry.npmjs.org/@types/flot/-/flot-0.0.31.tgz",
"integrity": "sha512-X+RcMQCqPlQo8zPT6cUFTd/PoYBShMQlHUeOXf05jWlfYnvLuRmluB9z+2EsOKFgUzqzZve5brx+gnFxBaHEUw==",
"dev": true,
"requires": {
"@types/jquery": "*"
}
},
"@types/glob": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.1.1.tgz",
@ -1196,6 +1205,12 @@
"integrity": "sha512-K5K+yml8LTo9bWJI/rECfIPrGgxdpeNbj+d53lwN4QjW1MCwlkhUms+gtdzigTeUyBr09+u8BwOIY3MXvHdcsA==",
"dev": true
},
"@types/tinycolor2": {
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/@types/tinycolor2/-/tinycolor2-1.4.2.tgz",
"integrity": "sha512-PeHg/AtdW6aaIO2a+98Xj7rWY4KC1E6yOy7AFknJQ7VXUGNrMlyxDFxJo7HqLtjQms/ZhhQX52mLVW/EX3JGOw==",
"dev": true
},
"@types/webpack-sources": {
"version": "0.1.5",
"resolved": "https://registry.npmjs.org/@types/webpack-sources/-/webpack-sources-0.1.5.tgz",
@ -4412,6 +4427,14 @@
"integrity": "sha512-a1hQMktqW9Nmqr5aktAux3JMNqaucxGcjtjWnZLHX7yyPCmlSV3M54nGYbqT8K+0GhF3NBgmJCc3ma+WOgX8Jg==",
"dev": true
},
"flot": {
"version": "git://github.com/thingsboard/flot.git#6e1a37095868f174d31d5c627c3659b70f9b92dd",
"from": "git://github.com/thingsboard/flot.git#0.9-work"
},
"flot.curvedlines": {
"version": "git://github.com/MichaelZinsmaier/CurvedLines.git#22ed1fc2a6ccafc816c2d07b36027cc123825c4b",
"from": "git://github.com/MichaelZinsmaier/CurvedLines.git#master"
},
"flush-write-stream": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/flush-write-stream/-/flush-write-stream-1.1.1.tgz",
@ -7358,6 +7381,11 @@
}
}
},
"moment": {
"version": "2.24.0",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.24.0.tgz",
"integrity": "sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg=="
},
"move-concurrently": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz",

5
ui-ngx/package.json

@ -37,6 +37,8 @@
"compass-sass-mixins": "^0.12.7",
"core-js": "^3.1.4",
"deep-equal": "^1.0.1",
"flot": "git://github.com/thingsboard/flot.git#0.9-work",
"flot.curvedlines": "git://github.com/MichaelZinsmaier/CurvedLines.git#master",
"font-awesome": "^4.7.0",
"hammerjs": "^2.0.8",
"javascript-detect-element-resize": "^0.5.3",
@ -44,6 +46,7 @@
"jquery.terminal": "^2.8.0",
"material-design-icons": "^3.0.1",
"messageformat": "^2.3.0",
"moment": "^2.24.0",
"ngx-clipboard": "^12.2.0",
"ngx-translate-messageformat-compiler": "^4.5.0",
"rxjs": "~6.5.2",
@ -59,10 +62,12 @@
"@angular/cli": "~8.2.0",
"@angular/compiler-cli": "~8.2.0",
"@angular/language-service": "~8.2.0",
"@types/flot": "0.0.31",
"@types/jasmine": "~3.4.0",
"@types/jasminewd2": "~2.0.6",
"@types/jquery": "^3.3.31",
"@types/node": "~10.14.15",
"@types/tinycolor2": "^1.4.2",
"codelyzer": "~5.1.0",
"compression-webpack-plugin": "^3.0.0",
"directory-tree": "^2.2.3",

332
ui-ngx/src/app/core/api/data-aggregator.ts

@ -0,0 +1,332 @@
///
/// Copyright © 2016-2019 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { SubscriptionData, SubscriptionUpdateMsg, SubscriptionDataHolder } from '@app/shared/models/telemetry/telemetry.models';
import { AggregationType } from '@shared/models/time/time.models';
import { UtilsService } from '@core/services/utils.service';
import Timeout = NodeJS.Timeout;
import { deepClone } from '@core/utils';
export declare type onAggregatedData = (data: SubscriptionData, detectChanges: boolean) => void;
interface AggData {
count: number;
sum: number;
aggValue: any;
}
interface AggregationMap {
[key: string]: Map<number, AggData>;
}
declare type AggFunction = (aggData: AggData, value?: any) => void;
const avg: AggFunction = (aggData: AggData, value?: any) => {
aggData.count++;
aggData.sum += value;
aggData.aggValue = aggData.sum / aggData.count;
};
const min: AggFunction = (aggData: AggData, value?: any) => {
aggData.aggValue = Math.min(aggData.aggValue, value);
};
const max: AggFunction = (aggData: AggData, value?: any) => {
aggData.aggValue = Math.max(aggData.aggValue, value);
};
const sum: AggFunction = (aggData: AggData, value?: any) => {
aggData.aggValue = aggData.aggValue + value;
};
const count: AggFunction = (aggData: AggData) => {
aggData.count++;
aggData.aggValue = aggData.count;
};
const none: AggFunction = (aggData: AggData, value?: any) => {
aggData.aggValue = value;
};
export class DataAggregator {
private dataBuffer: SubscriptionData = {};
private data: SubscriptionData;
private lastPrevKvPairData: {[key: string]: [number, any]};
private aggregationMap: AggregationMap;
private dataReceived = false;
private resetPending = false;
private noAggregation = this.aggregationType === AggregationType.NONE;
private aggregationTimeout = Math.max(this.interval, 1000);
private aggFunction: AggFunction;
private intervalTimeoutHandle: Timeout;
private intervalScheduledTime: number;
private endTs: number;
private elapsed: number;
constructor(private onDataCb: onAggregatedData,
private tsKeyNames: string[],
private startTs: number,
private limit: number,
private aggregationType: AggregationType,
private timeWindow: number,
private interval: number,
private stateData: boolean,
private utils: UtilsService) {
this.tsKeyNames.forEach((key) => {
this.dataBuffer[key] = [];
});
if (this.stateData) {
this.lastPrevKvPairData = {};
}
switch (this.aggregationType) {
case AggregationType.MIN:
this.aggFunction = min;
break;
case AggregationType.MAX:
this.aggFunction = max;
break;
case AggregationType.AVG:
this.aggFunction = avg;
break;
case AggregationType.SUM:
this.aggFunction = sum;
break;
case AggregationType.COUNT:
this.aggFunction = count;
break;
case AggregationType.NONE:
this.aggFunction = none;
break;
default:
this.aggFunction = avg;
}
}
public reset(startTs: number, timeWindow: number, interval: number) {
if (this.intervalTimeoutHandle) {
clearTimeout(this.intervalTimeoutHandle);
this.intervalTimeoutHandle = null;
}
this.intervalScheduledTime = this.utils.currentPerfTime();
this.startTs = startTs;
this.timeWindow = timeWindow;
this.interval = interval;
this.endTs = this.startTs + this.timeWindow;
this.elapsed = 0;
this.aggregationTimeout = Math.max(this.interval, 1000);
this.resetPending = true;
this.intervalTimeoutHandle = setTimeout(this.onInterval.bind(this), this.aggregationTimeout);
}
public destroy() {
if (this.intervalTimeoutHandle) {
clearTimeout(this.intervalTimeoutHandle);
this.intervalTimeoutHandle = null;
}
this.aggregationMap = null;
}
public onData(data: SubscriptionDataHolder, update: boolean, history: boolean, detectChanges: boolean) {
if (!this.dataReceived || this.resetPending) {
let updateIntervalScheduledTime = true;
if (!this.dataReceived) {
this.elapsed = 0;
this.dataReceived = true;
this.endTs = this.startTs + this.timeWindow;
}
if (this.resetPending) {
this.resetPending = false;
updateIntervalScheduledTime = false;
}
if (update) {
this.aggregationMap = {};
this.updateAggregatedData(data.data);
} else {
this.aggregationMap = this.processAggregatedData(data.data);
}
if (updateIntervalScheduledTime) {
this.intervalScheduledTime = this.utils.currentPerfTime();
}
this.onInterval(history, detectChanges);
} else {
this.updateAggregatedData(data.data);
if (history) {
this.intervalScheduledTime = this.utils.currentPerfTime();
this.onInterval(history, detectChanges);
}
}
}
private onInterval(history?: boolean, detectChanges?: boolean) {
const now = this.utils.currentPerfTime();
this.elapsed += now - this.intervalScheduledTime;
this.intervalScheduledTime = now;
if (this.intervalTimeoutHandle) {
clearTimeout(this.intervalTimeoutHandle);
this.intervalTimeoutHandle = null;
}
if (!history) {
const delta = Math.floor(this.elapsed / this.interval);
if (delta || !this.data) {
this.startTs += delta * this.interval;
this.endTs += delta * this.interval;
this.data = this.updateData();
this.elapsed = this.elapsed - delta * this.interval;
}
} else {
this.data = this.updateData();
}
if (this.onDataCb) {
this.onDataCb(this.data, detectChanges);
}
if (!history) {
this.intervalTimeoutHandle = setTimeout(this.onInterval.bind(this), this.aggregationTimeout);
}
}
private updateData(): SubscriptionData {
this.tsKeyNames.forEach((key) => {
this.dataBuffer[key] = [];
});
for (const key of Object.keys(this.aggregationMap)) {
const aggKeyData = this.aggregationMap[key];
let keyData = this.dataBuffer[key];
aggKeyData.forEach((aggData, aggTimestamp) => {
if (aggTimestamp <= this.startTs) {
if (this.stateData &&
(!this.lastPrevKvPairData[key] || this.lastPrevKvPairData[key][0] < aggTimestamp)) {
this.lastPrevKvPairData[key] = [aggTimestamp, aggData.aggValue];
}
aggKeyData.delete(aggTimestamp);
} else if (aggTimestamp <= this.endTs) {
const kvPair: [number, any] = [aggTimestamp, aggData.aggValue];
keyData.push(kvPair);
}
});
keyData.sort((set1, set2) => set1[0] - set2[0]);
if (this.stateData) {
this.updateStateBounds(keyData, deepClone(this.lastPrevKvPairData[key]));
}
if (keyData.length > this.limit) {
keyData = keyData.slice(keyData.length - this.limit);
}
this.dataBuffer[key] = keyData;
}
return this.dataBuffer;
}
private updateStateBounds(keyData: [number, any][], lastPrevKvPair: [number, any]) {
if (lastPrevKvPair) {
lastPrevKvPair[0] = this.startTs;
}
let firstKvPair;
if (!keyData.length) {
if (lastPrevKvPair) {
firstKvPair = lastPrevKvPair;
keyData.push(firstKvPair);
}
} else {
firstKvPair = keyData[0];
}
if (firstKvPair && firstKvPair[0] > this.startTs) {
if (lastPrevKvPair) {
keyData.unshift(lastPrevKvPair);
}
}
if (keyData.length) {
let lastKvPair = keyData[keyData.length - 1];
if (lastKvPair[0] < this.endTs) {
lastKvPair = deepClone(lastKvPair);
lastKvPair[0] = this.endTs;
keyData.push(lastKvPair);
}
}
}
private processAggregatedData(data: SubscriptionData): AggregationMap {
const isCount = this.aggregationType === AggregationType.COUNT;
const aggregationMap: AggregationMap = {};
for (const key of Object.keys(data)) {
let aggKeyData = aggregationMap[key];
if (!aggKeyData) {
aggKeyData = new Map<number, AggData>();
aggregationMap[key] = aggKeyData;
}
const keyData = data[key];
keyData.forEach((kvPair) => {
const timestamp = kvPair[0];
const value = this.convertValue(kvPair[1]);
const aggKey = timestamp;
const aggData = {
count: isCount ? value : 1,
sum: value,
aggValue: value
};
aggKeyData.set(aggKey, aggData);
});
}
return aggregationMap;
}
private updateAggregatedData(data: SubscriptionData) {
const isCount = this.aggregationType === AggregationType.COUNT;
for (const key of Object.keys(data)) {
let aggKeyData = this.aggregationMap[key];
if (!aggKeyData) {
aggKeyData = new Map<number, AggData>();
this.aggregationMap[key] = aggKeyData;
}
const keyData = data[key];
keyData.forEach((kvPair) => {
const timestamp = kvPair[0];
const value = this.convertValue(kvPair[1]);
const aggTimestamp = this.noAggregation ? timestamp : (this.startTs +
Math.floor((timestamp - this.startTs) / this.interval) * this.interval + this.interval / 2);
let aggData = aggKeyData.get(aggTimestamp);
if (!aggData) {
aggData = {
count: 1,
sum: value,
aggValue: isCount ? 1 : value
};
aggKeyData.set(aggTimestamp, aggData);
} else {
this.aggFunction(aggData, value);
}
});
}
}
private isNumeric(val: any): boolean {
return (val - parseFloat( val ) + 1) >= 0;
}
private convertValue(val: string): any {
if (!this.noAggregation || val && this.isNumeric(val)) {
return Number(val);
} else {
return val;
}
}
}

666
ui-ngx/src/app/core/api/datasource-subcription.ts

@ -0,0 +1,666 @@
///
/// Copyright © 2016-2019 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { DataSet, DataSetHolder, DatasourceType, widgetType } from '@shared/models/widget.models';
import {
AttributesSubscriptionCmd,
DataKeyType,
GetHistoryCmd,
SubscriptionData,
SubscriptionDataHolder,
SubscriptionUpdateMsg,
TelemetryService,
TelemetrySubscriber,
TimeseriesSubscriptionCmd
} from '@shared/models/telemetry/telemetry.models';
import { DatasourceListener } from './datasource.service';
import { AggregationType, SubscriptionTimewindow, YEAR } from '@shared/models/time/time.models';
import { deepClone, isDefined, isObject, isDefinedAndNotNull } from '@core/utils';
import { UtilsService } from '@core/services/utils.service';
import { EntityType } from '@shared/models/entity-type.models';
import { DataAggregator } from '@core/api/data-aggregator';
import Timeout = NodeJS.Timeout;
declare type DataKeyFunction = (time: number, prevValue: any) => any;
declare type DataKeyPostFunction = (time: number, value: any, prevValue: any, timePrev: number, prevOrigValue: any) => any;
export interface SubscriptionDataKey {
name: string;
type: DataKeyType;
funcBody: string;
func?: DataKeyFunction;
postFuncBody: string;
postFunc?: DataKeyPostFunction;
index?: number;
key?: string;
lastUpdateTime?: number;
}
export interface DatasourceSubscriptionOptions {
datasourceType: DatasourceType;
dataKeys: Array<SubscriptionDataKey>;
type: widgetType;
entityType?: EntityType;
entityId?: string;
subscriptionTimewindow?: SubscriptionTimewindow;
}
export class DatasourceSubscription {
private listeners: Array<DatasourceListener> = [];
private datasourceType: DatasourceType = this.datasourceSubscriptionOptions.datasourceType;
private history = this.datasourceSubscriptionOptions.subscriptionTimewindow &&
isObject(this.datasourceSubscriptionOptions.subscriptionTimewindow.fixedWindow);
private realtime = this.datasourceSubscriptionOptions.subscriptionTimewindow &&
isDefinedAndNotNull(this.datasourceSubscriptionOptions.subscriptionTimewindow.realtimeWindowMs);
private subscribers = new Array<TelemetrySubscriber>();
private dataAggregator: DataAggregator;
private dataKeys: {[key: string]: Array<SubscriptionDataKey> | SubscriptionDataKey} = {};
private datasourceData: {[key: string]: DataSetHolder} = {};
private datasourceOrigData: {[key: string]: DataSetHolder} = {};
private frequency: number;
private tickScheduledTime = 0;
private tickElapsed = 0;
private timer: Timeout;
constructor(private datasourceSubscriptionOptions: DatasourceSubscriptionOptions,
private telemetryService: TelemetryService,
private utils: UtilsService) {
this.initializeSubscription();
}
private initializeSubscription() {
for (let i = 0; i < this.datasourceSubscriptionOptions.dataKeys.length; i++) {
const dataKey = deepClone(this.datasourceSubscriptionOptions.dataKeys[i]);
dataKey.index = i;
if (this.datasourceType === DatasourceType.function) {
if (!dataKey.func) {
dataKey.func = new Function('time', 'prevValue', dataKey.funcBody) as DataKeyFunction;
}
} else {
if (dataKey.postFuncBody && !dataKey.postFunc) {
dataKey.postFunc = new Function('time', 'value', 'prevValue', 'timePrev', 'prevOrigValue',
dataKey.postFuncBody) as DataKeyPostFunction;
}
}
let key: string;
if (this.datasourceType === DatasourceType.entity || this.datasourceSubscriptionOptions.type === widgetType.timeseries) {
if (this.datasourceType === DatasourceType.function) {
key = `${dataKey.name}_${dataKey.index}_${dataKey.type}`;
} else {
key = `${dataKey.name}_${dataKey.type}`;
}
let dataKeysList = this.dataKeys[key] as Array<SubscriptionDataKey>;
if (!dataKeysList) {
dataKeysList = [];
this.dataKeys[key] = dataKeysList;
}
const index = dataKeysList.push(dataKey) - 1;
this.datasourceData[key + '_' + index] = {
data: []
};
} else {
key = String(this.utils.objectHashCode(dataKey));
this.datasourceData[key] = {
data: []
};
this.dataKeys[key] = dataKey;
}
dataKey.key = key;
}
this.datasourceOrigData = deepClone(this.datasourceData);
if (this.datasourceType === DatasourceType.function) {
this.frequency = 1000;
if (this.datasourceSubscriptionOptions.type === widgetType.timeseries) {
this.frequency = Math.min(this.datasourceSubscriptionOptions.subscriptionTimewindow.aggregation.interval, 5000);
}
}
}
public addListener(listener: DatasourceListener) {
this.listeners.push(listener);
if (this.history) {
this.start();
}
}
public hasListeners(): boolean {
return this.listeners.length > 0;
}
public removeListener(listener: DatasourceListener) {
this.listeners.splice(this.listeners.indexOf(listener), 1);
}
public syncListener(listener: DatasourceListener) {
let key: string;
let dataKey: SubscriptionDataKey;
if (this.datasourceType === DatasourceType.entity || this.datasourceSubscriptionOptions.type === widgetType.timeseries) {
for (key of Object.keys(this.dataKeys)) {
const dataKeysList = this.dataKeys[key] as Array<SubscriptionDataKey>;
for (let i = 0; i < dataKeysList.length; i++) {
dataKey = dataKeysList[i];
const datasourceKey = `${key}_${i}`;
listener.dataUpdated(this.datasourceData[datasourceKey],
listener.datasourceIndex,
dataKey.index, false);
}
}
} else {
for (key of Object.keys(this.dataKeys)) {
dataKey = this.dataKeys[key] as SubscriptionDataKey;
listener.dataUpdated(this.datasourceData[key],
listener.datasourceIndex,
dataKey.index, false);
}
}
}
public unsubscribe() {
if (this.timer) {
clearTimeout(this.timer);
this.timer = null;
}
if (this.datasourceType === DatasourceType.entity) {
this.subscribers.forEach(
(subscriber) => {
subscriber.unsubscribe();
}
);
this.subscribers.length = 0;
}
if (this.dataAggregator) {
this.dataAggregator.destroy();
this.dataAggregator = null;
}
}
public start() {
if (this.history && !this.hasListeners()) {
return;
}
let subsTw = this.datasourceSubscriptionOptions.subscriptionTimewindow;
const tsKeyNames: string[] = [];
const attrKeyNames: string[] = [];
let dataKey: SubscriptionDataKey;
if (this.datasourceType === DatasourceType.entity) {
let tsKeys = '';
let attrKeys = '';
for (const key of Object.keys(this.dataKeys)) {
const dataKeysList = this.dataKeys[key] as Array<SubscriptionDataKey>;
dataKey = dataKeysList[0];
if (dataKey.type === DataKeyType.timeseries) {
tsKeyNames.push(dataKey.name);
} else if (dataKey.type === DataKeyType.attribute) {
attrKeyNames.push(dataKey.name);
}
}
tsKeys = tsKeyNames.join(',');
attrKeys = attrKeyNames.join(',');
if (tsKeys.length > 0) {
if (this.history) {
const historyCommand = new GetHistoryCmd();
historyCommand.entityType = this.datasourceSubscriptionOptions.entityType;
historyCommand.entityId = this.datasourceSubscriptionOptions.entityId;
historyCommand.keys = tsKeys;
historyCommand.startTs = subsTw.fixedWindow.startTimeMs;
historyCommand.endTs = subsTw.fixedWindow.endTimeMs;
historyCommand.interval = subsTw.aggregation.interval;
historyCommand.limit = subsTw.aggregation.limit;
historyCommand.agg = subsTw.aggregation.type;
const subscriber = new TelemetrySubscriber(this.telemetryService);
subscriber.subscriptionCommands.push(historyCommand);
let firstStateHistoryCommand: GetHistoryCmd;
if (subsTw.aggregation.stateData) {
firstStateHistoryCommand = this.createFirstStateHistoryCommand(subsTw.fixedWindow.startTimeMs, tsKeys);
subscriber.subscriptionCommands.push(firstStateHistoryCommand);
}
let data: SubscriptionUpdateMsg;
let firstStateData: SubscriptionUpdateMsg;
subscriber.data$.subscribe(
(subscriptionUpdate) => {
if (subsTw.aggregation.stateData && firstStateHistoryCommand
&& firstStateHistoryCommand.cmdId === subscriptionUpdate.subscriptionId) {
if (data) {
this.onStateHistoryData(subscriptionUpdate, data, subsTw.aggregation.limit,
subsTw.fixedWindow.startTimeMs, subsTw.fixedWindow.endTimeMs,
(newData) => {
this.onData(newData.data, DataKeyType.timeseries, true);
}
);
} else {
firstStateData = data;
}
} else {
if (subsTw.aggregation.stateData) {
if (firstStateData) {
this.onStateHistoryData(firstStateData, subscriptionUpdate, subsTw.aggregation.limit,
subsTw.fixedWindow.startTimeMs, subsTw.fixedWindow.endTimeMs,
(newData) => {
this.onData(newData.data, DataKeyType.timeseries, true);
});
} else {
data = subscriptionUpdate;
}
} else {
for (const key of Object.keys(data.data)) {
const keyData = data.data[key];
keyData.sort((set1, set2) => set1[0] - set2[0]);
}
this.onData(data.data, DataKeyType.timeseries, true);
}
}
}
);
subscriber.subscribe();
this.subscribers.push(subscriber);
} else {
const subscriptionCommand = new TimeseriesSubscriptionCmd();
subscriptionCommand.entityType = this.datasourceSubscriptionOptions.entityType;
subscriptionCommand.entityId = this.datasourceSubscriptionOptions.entityId;
subscriptionCommand.keys = tsKeys;
const subscriber = new TelemetrySubscriber(this.telemetryService);
subscriber.subscriptionCommands.push(subscriptionCommand);
if (this.datasourceSubscriptionOptions.type === widgetType.timeseries) {
this.updateRealtimeSubscriptionCommand(subscriptionCommand, subsTw);
let firstStateSubscriptionCommand: GetHistoryCmd;
if (subsTw.aggregation.stateData) {
firstStateSubscriptionCommand = this.createFirstStateHistoryCommand(subsTw.startTs, tsKeys);
subscriber.subscriptionCommands.push(firstStateSubscriptionCommand);
}
this.dataAggregator = this.createRealtimeDataAggregator(subsTw, tsKeyNames, DataKeyType.timeseries);
let data: SubscriptionUpdateMsg;
let firstStateData: SubscriptionUpdateMsg;
let stateDataReceived: boolean;
subscriber.data$.subscribe(
(subscriptionUpdate) => {
if (subsTw.aggregation.stateData &&
firstStateSubscriptionCommand && firstStateSubscriptionCommand.cmdId === subscriptionUpdate.subscriptionId) {
if (data) {
this.onStateHistoryData(subscriptionUpdate, data, subsTw.aggregation.limit,
subsTw.startTs, subsTw.startTs + subsTw.aggregation.timeWindow,
(newData) => {
this.dataAggregator.onData(newData, false, false, true);
});
stateDataReceived = true;
} else {
firstStateData = data;
}
} else {
if (subsTw.aggregation.stateData && !stateDataReceived) {
if (firstStateData) {
this.onStateHistoryData(firstStateData, subscriptionUpdate, subsTw.aggregation.limit,
subsTw.startTs, subsTw.startTs + subsTw.aggregation.timeWindow,
(newData) => {
this.dataAggregator.onData(newData, false, false, true);
});
stateDataReceived = true;
} else {
data = subscriptionUpdate;
}
} else {
this.dataAggregator.onData(subscriptionUpdate, false, false, true);
}
}
}
);
subscriber.reconnect$.subscribe(() => {
let newSubsTw: SubscriptionTimewindow = null;
this.listeners.forEach((listener) => {
if (!newSubsTw) {
newSubsTw = listener.updateRealtimeSubscription();
} else {
listener.setRealtimeSubscription(newSubsTw);
}
});
subsTw = newSubsTw;
firstStateData = null;
data = null;
stateDataReceived = false;
this.updateRealtimeSubscriptionCommand(subscriptionCommand, subsTw);
if (subsTw.aggregation.stateData) {
this.updateFirstStateHistoryCommand(firstStateSubscriptionCommand, subsTw.startTs);
}
this.dataAggregator.reset(newSubsTw.startTs, newSubsTw.aggregation.timeWindow, newSubsTw.aggregation.interval);
});
} else {
subscriber.data$.subscribe(
(subscriptionUpdate) => {
if (subscriptionUpdate.data) {
this.onData(subscriptionUpdate.data, DataKeyType.timeseries, true);
}
}
);
}
subscriber.subscribe();
this.subscribers.push(subscriber);
}
}
if (attrKeys.length) {
const attrsSubscriptionCommand = new AttributesSubscriptionCmd();
attrsSubscriptionCommand.entityType = this.datasourceSubscriptionOptions.entityType;
attrsSubscriptionCommand.entityId = this.datasourceSubscriptionOptions.entityId;
attrsSubscriptionCommand.keys = attrKeys;
const subscriber = new TelemetrySubscriber(this.telemetryService);
subscriber.subscriptionCommands.push(attrsSubscriptionCommand);
subscriber.data$.subscribe(
(subscriptionUpdate) => {
if (subscriptionUpdate.data) {
this.onData(subscriptionUpdate.data, DataKeyType.attribute, true);
}
}
);
subscriber.subscribe();
this.subscribers.push(subscriber);
}
} else if (this.datasourceType === DatasourceType.function) {
if (this.datasourceSubscriptionOptions.type === widgetType.timeseries) {
for (const key of Object.keys(this.dataKeys)) {
const dataKeysList = this.dataKeys[key] as Array<SubscriptionDataKey>;
dataKeysList.forEach((subscriptionDataKey) => {
tsKeyNames.push(`${subscriptionDataKey.name}_${subscriptionDataKey.index}`);
});
}
this.dataAggregator = this.createRealtimeDataAggregator(subsTw, tsKeyNames, DataKeyType.function);
}
this.tickScheduledTime = this.utils.currentPerfTime();
if (this.history) {
this.onTick(true);
} else {
this.timer = setTimeout(this.onTick.bind(this, true), 0);
}
}
}
private createFirstStateHistoryCommand(startTs: number, tsKeys: string): GetHistoryCmd {
const command = new GetHistoryCmd();
command.entityType = this.datasourceSubscriptionOptions.entityType;
command.entityId = this.datasourceSubscriptionOptions.entityId;
command.keys = tsKeys;
command.startTs = startTs - YEAR;
command.endTs = startTs;
command.interval = 1000;
command.limit = 1;
command.agg = AggregationType.NONE;
return command;
}
private updateFirstStateHistoryCommand(stateHistoryCommand: GetHistoryCmd, startTs: number) {
stateHistoryCommand.startTs = startTs - YEAR;
stateHistoryCommand.endTs = startTs;
}
private onStateHistoryData(firstStateData: SubscriptionUpdateMsg, data: SubscriptionUpdateMsg,
limit: number, startTs: number, endTs: number, onData: (data: SubscriptionUpdateMsg) => void) {
for (const key of Object.keys(data.data)) {
const keyData = data.data[key];
keyData.sort((set1, set2) => set1[0] - set2[0]);
if (keyData.length < limit) {
let firstStateKeyData = firstStateData.data[key];
if (firstStateKeyData.length) {
const firstStateDataTsKv = firstStateKeyData[0];
firstStateDataTsKv[0] = startTs;
firstStateKeyData = [
[ startTs, firstStateKeyData[0][1] ]
];
keyData.unshift(firstStateDataTsKv);
}
}
if (keyData.length) {
const lastTsKv = deepClone(keyData[keyData.length - 1]);
lastTsKv[0] = endTs;
keyData.push(lastTsKv);
}
}
onData(data);
}
private createRealtimeDataAggregator(subsTw: SubscriptionTimewindow,
tsKeyNames: Array<string>, dataKeyType: DataKeyType): DataAggregator {
return new DataAggregator(
(data, detectChanges) => {
this.onData(data, dataKeyType, detectChanges);
},
tsKeyNames,
subsTw.startTs,
subsTw.aggregation.limit,
subsTw.aggregation.type,
subsTw.aggregation.timeWindow,
subsTw.aggregation.interval,
subsTw.aggregation.stateData,
this.utils
);
}
private updateRealtimeSubscriptionCommand(subscriptionCommand: TimeseriesSubscriptionCmd, subsTw: SubscriptionTimewindow) {
subscriptionCommand.startTs = subsTw.startTs;
subscriptionCommand.timeWindow = subsTw.aggregation.timeWindow;
subscriptionCommand.interval = subsTw.aggregation.interval;
subscriptionCommand.limit = subsTw.aggregation.limit;
subscriptionCommand.agg = subsTw.aggregation.type;
}
private generateSeries(dataKey: SubscriptionDataKey, index: number, startTime: number, endTime: number): [number, any][] {
const data: [number, any][] = [];
let prevSeries: [number, any];
const datasourceDataKey = `${dataKey.key}_${index}`;
const datasourceKeyData = this.datasourceData[datasourceDataKey].data;
if (datasourceKeyData.length > 0) {
prevSeries = datasourceKeyData[datasourceKeyData.length - 1];
} else {
prevSeries = [0, 0];
}
for (let time = startTime; time <= endTime && (this.timer || this.history); time += this.frequency) {
const value = dataKey.func(time, prevSeries[1]);
const series: [number, any] = [time, value];
data.push(series);
prevSeries = series;
}
if (data.length > 0) {
dataKey.lastUpdateTime = data[data.length - 1][0];
}
return data;
}
private generateLatest(dataKey: SubscriptionDataKey, detectChanges: boolean) {
let prevSeries: [number, any];
const datasourceKeyData = this.datasourceData[dataKey.key].data;
if (datasourceKeyData.length > 0) {
prevSeries = datasourceKeyData[datasourceKeyData.length - 1];
} else {
prevSeries = [0, 0];
}
const time = Date.now();
const value = dataKey.func(time, prevSeries[1]);
const series: [number, any] = [time, value];
this.datasourceData[dataKey.key].data = [series];
this.listeners.forEach(
(listener) => {
listener.dataUpdated(this.datasourceData[dataKey.key],
listener.datasourceIndex,
dataKey.index, detectChanges);
}
);
}
private onTick(detectChanges: boolean) {
const now = this.utils.currentPerfTime();
this.tickElapsed += now - this.tickScheduledTime;
this.tickScheduledTime = now;
if (this.timer) {
clearTimeout(this.timer);
}
let key: string;
if (this.datasourceSubscriptionOptions.type === widgetType.timeseries) {
let startTime: number;
let endTime: number;
let delta: number;
const generatedData: SubscriptionDataHolder = {
data: {}
};
if (!this.history) {
delta = Math.floor(this.tickElapsed / this.frequency);
}
const deltaElapsed = this.history ? this.frequency : delta * this.frequency;
this.tickElapsed = this.tickElapsed - deltaElapsed;
for (key of Object.keys(this.dataKeys)) {
const dataKeyList = this.dataKeys[key] as Array<SubscriptionDataKey>;
for (let index = 0; index < dataKeyList.length && (this.timer || this.history); index ++) {
const dataKey = dataKeyList[index];
if (!startTime) {
if (this.realtime) {
if (dataKey.lastUpdateTime) {
startTime = dataKey.lastUpdateTime + this.frequency;
endTime = dataKey.lastUpdateTime + deltaElapsed;
} else {
startTime = this.datasourceSubscriptionOptions.subscriptionTimewindow.startTs;
endTime = startTime + this.datasourceSubscriptionOptions.subscriptionTimewindow.realtimeWindowMs + this.frequency;
if (this.datasourceSubscriptionOptions.subscriptionTimewindow.aggregation.type === AggregationType.NONE) {
const time = endTime - this.frequency * this.datasourceSubscriptionOptions.subscriptionTimewindow.aggregation.limit;
startTime = Math.max(time, startTime);
}
}
} else {
startTime = this.datasourceSubscriptionOptions.subscriptionTimewindow.fixedWindow.startTimeMs;
endTime = this.datasourceSubscriptionOptions.subscriptionTimewindow.fixedWindow.endTimeMs;
}
}
const data = this.generateSeries(dataKey, index, startTime, endTime);
generatedData.data[`${dataKey.name}_${dataKey.index}`] = data;
}
}
if (this.dataAggregator) {
this.dataAggregator.onData(generatedData, true, this.history, detectChanges);
}
} else if (this.datasourceSubscriptionOptions.type === widgetType.latest) {
for (key of Object.keys(this.dataKeys)) {
this.generateLatest(this.dataKeys[key] as SubscriptionDataKey, detectChanges);
}
}
if (!this.history) {
this.timer = setTimeout(this.onTick.bind(this, true), this.frequency);
}
}
private onData(sourceData: SubscriptionData, type: DataKeyType, detectChanges: boolean) {
for (const keyName of Object.keys(sourceData)) {
const keyData = sourceData[keyName];
const key = `${keyName}_${type}`;
const dataKeyList = this.dataKeys[key] as Array<SubscriptionDataKey>;
for (let keyIndex = 0; dataKeyList && keyIndex < dataKeyList.length; keyIndex++) {
const datasourceKey = `${key}_${keyIndex}`;
if (this.datasourceData[datasourceKey].data) {
const dataKey = dataKeyList[keyIndex];
const data: DataSet = [];
let prevSeries: [number, any];
let prevOrigSeries: [number, any];
let datasourceKeyData: DataSet;
let datasourceOrigKeyData: DataSet;
let update = false;
if (this.realtime) {
datasourceKeyData = [];
datasourceOrigKeyData = [];
} else {
datasourceKeyData = this.datasourceData[datasourceKey].data;
datasourceOrigKeyData = this.datasourceOrigData[datasourceKey].data;
}
if (datasourceKeyData.length > 0) {
prevSeries = datasourceKeyData[datasourceKeyData.length - 1];
prevOrigSeries = datasourceOrigKeyData[datasourceOrigKeyData.length - 1];
} else {
prevSeries = [0, 0];
prevOrigSeries = [0, 0];
}
this.datasourceOrigData[datasourceKey].data = [];
if (this.datasourceSubscriptionOptions.type === widgetType.timeseries) {
keyData.forEach((keySeries) => {
let series = keySeries;
const time = series[0];
this.datasourceOrigData[datasourceKey].data.push(series);
let value = this.convertValue(series[1]);
if (dataKey.postFunc) {
value = dataKey.postFunc(time, value, prevSeries[1], prevOrigSeries[0], prevOrigSeries[1]);
}
prevOrigSeries = series;
series = [time, value];
data.push(series);
prevSeries = series;
});
update = true;
} else if (this.datasourceSubscriptionOptions.type === widgetType.latest) {
if (keyData.length > 0) {
let series = keyData[0];
const time = series[0];
this.datasourceOrigData[datasourceKey].data.push(series);
let value = this.convertValue(series[1]);
if (dataKey.postFunc) {
value = dataKey.postFunc(time, value, prevSeries[1], prevOrigSeries[0], prevOrigSeries[1]);
}
series = [time, value];
data.push(series);
}
update = true;
}
if (update) {
this.datasourceData[datasourceKey].data = data;
this.listeners.forEach((listener) => {
listener.dataUpdated(this.datasourceData[datasourceKey],
listener.datasourceIndex,
dataKey.index, detectChanges);
});
}
}
}
}
}
private isNumeric(val: any): boolean {
return (val - parseFloat( val ) + 1) >= 0;
}
private convertValue(val: string): any {
if (val && this.isNumeric(val)) {
return Number(val);
} else {
return val;
}
}
}

71
ui-ngx/src/app/core/api/datasource.service.ts

@ -18,10 +18,26 @@ import { Injectable } from '@angular/core';
import { TelemetryWebsocketService } from '@core/ws/telemetry-websocket.service';
import { UtilsService } from '@core/services/utils.service';
import { EntityType } from '@app/shared/models/entity-type.models';
import { DataSetHolder, Datasource, DatasourceType, widgetType } from '@shared/models/widget.models';
import { SubscriptionTimewindow } from '@shared/models/time/time.models';
import {
DatasourceSubscription,
DatasourceSubscriptionOptions,
SubscriptionDataKey
} from '@core/api/datasource-subcription';
import { deepClone } from '@core/utils';
export interface DatasourceListener {
subscriptionType: widgetType;
subscriptionTimewindow: SubscriptionTimewindow;
datasource: Datasource;
entityType: EntityType;
entityId: string;
datasourceIndex: number;
dataUpdated: (data: DataSetHolder, datasourceIndex: number, dataKeyIndex: number, detectChanges: boolean) => void;
updateRealtimeSubscription: () => SubscriptionTimewindow;
setRealtimeSubscription: (subscriptionTimewindow: SubscriptionTimewindow) => void;
datasourceSubscriptionKey?: number;
}
@Injectable({
@ -29,14 +45,65 @@ export interface DatasourceListener {
})
export class DatasourceService {
private subscriptions: {[datasourceSubscriptionKey: string]: DatasourceSubscription} = {};
constructor(private telemetryService: TelemetryWebsocketService,
private utils: UtilsService) {}
public subscribeToDatasource(listener: DatasourceListener) {
// TODO:
const datasource = listener.datasource;
if (datasource.type === DatasourceType.entity && (!listener.entityId || !listener.entityType)) {
return;
}
const subscriptionDataKeys: Array<SubscriptionDataKey> = [];
datasource.dataKeys.forEach((dataKey) => {
const subscriptionDataKey: SubscriptionDataKey = {
name: dataKey.name,
type: dataKey.type,
funcBody: dataKey.funcBody,
postFuncBody: dataKey.postFuncBody
};
subscriptionDataKeys.push(subscriptionDataKey);
});
const datasourceSubscriptionOptions: DatasourceSubscriptionOptions = {
datasourceType: datasource.type,
dataKeys: subscriptionDataKeys,
type: listener.subscriptionType
};
if (listener.subscriptionType === widgetType.timeseries) {
datasourceSubscriptionOptions.subscriptionTimewindow = deepClone(listener.subscriptionTimewindow);
}
if (datasourceSubscriptionOptions.datasourceType === DatasourceType.entity) {
datasourceSubscriptionOptions.entityType = listener.entityType;
datasourceSubscriptionOptions.entityId = listener.entityId;
}
listener.datasourceSubscriptionKey = this.utils.objectHashCode(datasourceSubscriptionOptions);
let subscription: DatasourceSubscription;
if (this.subscriptions[listener.datasourceSubscriptionKey]) {
subscription = this.subscriptions[listener.datasourceSubscriptionKey];
subscription.syncListener(listener);
} else {
subscription = new DatasourceSubscription(datasourceSubscriptionOptions,
this.telemetryService, this.utils);
this.subscriptions[listener.datasourceSubscriptionKey] = subscription;
subscription.start();
}
subscription.addListener(listener);
}
public unsubscribeFromDatasource(listener: DatasourceListener) {
// TODO:
if (listener.datasourceSubscriptionKey) {
const subscription = this.subscriptions[listener.datasourceSubscriptionKey];
if (subscription) {
subscription.removeListener(listener);
if (!subscription.hasListeners()) {
subscription.unsubscribe();
delete this.subscriptions[listener.datasourceSubscriptionKey];
}
}
listener.datasourceSubscriptionKey = null;
}
}
}

19
ui-ngx/src/app/core/api/widget-api.models.ts

@ -28,14 +28,15 @@ import { TimeService } from '../services/time.service';
import { DeviceService } from '../http/device.service';
import { AlarmService } from '../http/alarm.service';
import { UtilsService } from '@core/services/utils.service';
import { Timewindow } from '@shared/models/time/time.models';
import { Timewindow, WidgetTimewindow } from '@shared/models/time/time.models';
import { EntityType } from '@shared/models/entity-type.models';
import { AlarmSearchStatus } from '@shared/models/alarm.models';
import { HttpErrorResponse } from '@angular/common/http';
import { DatasourceService } from '@core/api/datasource.service';
import { RafService } from '@core/services/raf.service';
export interface TimewindowFunctions {
onUpdateTimewindow: (startTimeMs: number, endTimeMs: number, interval: number) => void;
onUpdateTimewindow: (startTimeMs: number, endTimeMs: number, interval?: number) => void;
onResetTimewindow: () => void;
}
@ -128,6 +129,7 @@ export interface WidgetSubscriptionContext {
alarmService: AlarmService;
datasourceService: DatasourceService;
utils: UtilsService;
raf: RafService;
widgetUtils: IWidgetUtils;
dashboardTimewindowApi: TimewindowFunctions;
getServerTimeDiff: () => Observable<number>;
@ -137,10 +139,10 @@ export interface WidgetSubscriptionContext {
}
export interface WidgetSubscriptionCallbacks {
onDataUpdated?: (subscription: IWidgetSubscription) => void;
onDataUpdated?: (subscription: IWidgetSubscription, detectChanges: boolean) => void;
onDataUpdateError?: (subscription: IWidgetSubscription, e: any) => void;
dataLoading?: (subscription: IWidgetSubscription) => void;
legendDataUpdated?: (subscription: IWidgetSubscription) => void;
legendDataUpdated?: (subscription: IWidgetSubscription, detectChanges: boolean) => void;
timeWindowUpdated?: (subscription: IWidgetSubscription, timeWindowConfig: Timewindow) => void;
rpcStateChanged?: (subscription: IWidgetSubscription) => void;
onRpcSuccess?: (subscription: IWidgetSubscription) => void;
@ -184,7 +186,8 @@ export interface IWidgetSubscription {
datasources?: Array<Datasource>;
data?: Array<DatasourceData>;
hiddenData?: Array<{data: DataSet}>;
timeWindow?: Timewindow;
timeWindowConfig?: Timewindow;
timeWindow?: WidgetTimewindow;
alarmSource?: Datasource;
alarmSearchStatus?: AlarmSearchStatus;
@ -202,7 +205,11 @@ export interface IWidgetSubscription {
onAliasesChanged(aliasIds: Array<string>): boolean;
onUpdateTimewindow(startTimeMs: number, endTimeMs: number, interval: number): void;
onDashboardTimewindowChanged(dashboardTimewindow: Timewindow): void;
updateDataVisibility(index: number): void;
onUpdateTimewindow(startTimeMs: number, endTimeMs: number, interval?: number): void;
onResetTimewindow(): void;
updateTimewindowConfig(newTimewindow: Timewindow): void;

323
ui-ngx/src/app/core/api/widget-subscription.ts

@ -23,8 +23,10 @@ import {
} from '@core/api/widget-api.models';
import {
DataSet,
DataSetHolder,
Datasource,
DatasourceData,
DatasourceType,
LegendConfig,
LegendData,
LegendKey,
@ -32,7 +34,13 @@ import {
widgetType
} from '@app/shared/models/widget.models';
import { HttpErrorResponse } from '@angular/common/http';
import { Timewindow } from '@app/shared/models/time/time.models';
import {
createSubscriptionTimewindow,
SubscriptionTimewindow,
Timewindow,
toHistoryTimewindow,
WidgetTimewindow
} from '@app/shared/models/time/time.models';
import { Observable, of, ReplaySubject, Subject } from 'rxjs';
import { CancelAnimationFrame } from '@core/services/raf.service';
import { EntityType } from '@shared/models/entity-type.models';
@ -40,6 +48,7 @@ import { AlarmInfo, AlarmSearchStatus } from '@shared/models/alarm.models';
import { deepClone, isDefined } from '@core/utils';
import { AlarmSourceListener } from '@core/http/alarm.service';
import { DatasourceListener } from '@core/api/datasource.service';
import * as deepEqual from 'deep-equal';
export class WidgetSubscription implements IWidgetSubscription {
@ -48,16 +57,16 @@ export class WidgetSubscription implements IWidgetSubscription {
type: widgetType;
callbacks: WidgetSubscriptionCallbacks;
timeWindow: Timewindow;
timeWindow: WidgetTimewindow;
originalTimewindow: Timewindow;
timeWindowConfig: Timewindow;
subscriptionTimewindow: Timewindow;
subscriptionTimewindow: SubscriptionTimewindow;
useDashboardTimewindow: boolean;
data: Array<DatasourceData>;
datasources: Array<Datasource>;
datasourceListeners: Array<DatasourceListener>;
hiddenData: Array<{ data: DataSet }>;
hiddenData: Array<DataSetHolder>;
legendData: LegendData;
legendConfig: LegendConfig;
caulculateLegendData: boolean;
@ -323,17 +332,95 @@ export class WidgetSubscription implements IWidgetSubscription {
}
}
private resetData() {
for (let i = 0; i < this.data.length; i++) {
this.data[i].data = [];
this.hiddenData[i].data = [];
if (this.displayLegend) {
this.legendData.data[i].min = null;
this.legendData.data[i].max = null;
this.legendData.data[i].avg = null;
this.legendData.data[i].total = null;
this.legendData.data[i].hidden = false;
}
}
this.onDataUpdated();
}
getFirstEntityInfo(): EntityInfo {
return undefined;
}
onAliasesChanged(aliasIds: Array<string>): boolean {
return false;
}
private onDataUpdated(detectChanges?: boolean) {
if (this.cafs.dataUpdated) {
this.cafs.dataUpdated();
this.cafs.dataUpdated = null;
}
this.cafs.dataUpdated = this.ctx.raf.raf(() => {
try {
this.callbacks.onDataUpdated(this, detectChanges);
} catch (e) {
this.callbacks.onDataUpdateError(this, e);
}
});
}
onDashboardTimewindowChanged(newDashboardTimewindow: Timewindow): void {
if (this.type === widgetType.timeseries || this.type === widgetType.alarm) {
if (this.useDashboardTimewindow) {
if (!deepEqual(this.timeWindowConfig, newDashboardTimewindow) && newDashboardTimewindow) {
this.timeWindowConfig = deepClone(newDashboardTimewindow);
this.update();
}
}
}
}
updateDataVisibility(index: number): void {
if (this.displayLegend) {
const hidden = this.legendData.keys[index].dataKey.hidden;
if (hidden) {
this.hiddenData[index].data = this.data[index].data;
this.data[index].data = [];
} else {
this.data[index].data = this.hiddenData[index].data;
this.hiddenData[index].data = [];
}
this.onDataUpdated();
}
}
updateTimewindowConfig(newTimewindow: Timewindow): void {
}
onResetTimewindow(): void {
if (this.useDashboardTimewindow) {
this.ctx.dashboardTimewindowApi.onResetTimewindow();
} else {
if (this.originalTimewindow) {
this.timeWindowConfig = deepClone(this.originalTimewindow);
this.originalTimewindow = null;
this.callbacks.timeWindowUpdated(this, this.timeWindowConfig);
this.update();
}
}
}
onUpdateTimewindow(startTimeMs: number, endTimeMs: number, interval: number): void {
onUpdateTimewindow(startTimeMs: number, endTimeMs: number, interval?: number): void {
if (this.useDashboardTimewindow) {
this.ctx.dashboardTimewindowApi.onUpdateTimewindow(startTimeMs, endTimeMs);
} else {
if (!this.originalTimewindow) {
this.originalTimewindow = deepClone(this.timeWindowConfig);
}
this.timeWindowConfig = toHistoryTimewindow(this.timeWindowConfig, startTimeMs, endTimeMs, interval, this.ctx.timeService);
this.callbacks.timeWindowUpdated(this, this.timeWindowConfig);
this.update();
}
}
sendOneWayCommand(method: string, params?: any, timeout?: number): Observable<any> {
@ -347,12 +434,109 @@ export class WidgetSubscription implements IWidgetSubscription {
clearRpcError(): void {
}
update() {
this.unsubscribe();
this.subscribe();
}
subscribe(): void {
if (this.cafs.subscribe) {
this.cafs.subscribe();
this.cafs.subscribe = null;
}
this.cafs.subscribe = this.ctx.raf.raf(() => {
this.doSubscribe();
});
}
private doSubscribe() {
if (this.type === widgetType.rpc) {
return;
}
if (this.type === widgetType.alarm) {
this.alarmsSubscribe();
} else {
this.notifyDataLoading();
if (this.type === widgetType.timeseries && this.timeWindowConfig) {
this.updateRealtimeSubscription();
if (this.subscriptionTimewindow.fixedWindow) {
this.onDataUpdated();
}
}
let index = 0;
this.datasources.forEach((datasource) => {
const listener: DatasourceListener = {
subscriptionType: this.type,
subscriptionTimewindow: this.subscriptionTimewindow,
datasource,
entityType: datasource.entityType,
entityId: datasource.entityId,
dataUpdated: this.dataUpdated.bind(this),
updateRealtimeSubscription: () => {
this.subscriptionTimewindow = this.updateRealtimeSubscription();
return this.subscriptionTimewindow;
},
setRealtimeSubscription: (subscriptionTimewindow) => {
this.updateRealtimeSubscription(deepClone(subscriptionTimewindow));
},
datasourceIndex: index
};
for (let a = 0; a < datasource.dataKeys.length; a++) {
this.data[index + a].data = [];
}
index += datasource.dataKeys.length;
this.datasourceListeners.push(listener);
if (datasource.dataKeys.length) {
this.ctx.datasourceService.subscribeToDatasource(listener);
}
let forceUpdate = false;
if (datasource.unresolvedStateEntity ||
!datasource.dataKeys.length ||
(datasource.type === DatasourceType.entity && !datasource.entityId)
) {
forceUpdate = true;
}
if (forceUpdate) {
this.notifyDataLoaded();
this.onDataUpdated();
}
});
}
}
private alarmsSubscribe() {
// TODO:
}
unsubscribe() {
if (this.type !== widgetType.rpc) {
if (this.type === widgetType.alarm) {
this.alarmsUnsubscribe();
} else {
this.datasourceListeners.forEach((listener) => {
this.ctx.datasourceService.unsubscribeFromDatasource(listener);
});
this.datasourceListeners.length = 0;
this.resetData();
}
}
}
private alarmsUnsubscribe() {
// TODO:
this.notifyDataLoaded();
}
destroy(): void {
this.unsubscribe();
for (const cafId of Object.keys(this.cafs)) {
if (this.cafs[cafId]) {
this.cafs[cafId]();
this.cafs[cafId] = null;
}
}
// TODO:
}
private notifyDataLoading() {
@ -365,10 +549,89 @@ export class WidgetSubscription implements IWidgetSubscription {
this.callbacks.dataLoading(this);
}
onAliasesChanged(aliasIds: Array<string>): boolean {
return false;
private updateTimewindow() {
this.timeWindow.interval = this.subscriptionTimewindow.aggregation.interval || 1000;
if (this.subscriptionTimewindow.realtimeWindowMs) {
this.timeWindow.maxTime = Date.now() + this.timeWindow.stDiff;
this.timeWindow.minTime = this.timeWindow.maxTime - this.subscriptionTimewindow.realtimeWindowMs;
} else if (this.subscriptionTimewindow.fixedWindow) {
this.timeWindow.maxTime = this.subscriptionTimewindow.fixedWindow.endTimeMs;
this.timeWindow.minTime = this.subscriptionTimewindow.fixedWindow.startTimeMs;
}
}
private updateRealtimeSubscription(subscriptionTimewindow?: SubscriptionTimewindow) {
if (subscriptionTimewindow) {
this.subscriptionTimewindow = subscriptionTimewindow;
} else {
this.subscriptionTimewindow =
createSubscriptionTimewindow(this.timeWindowConfig, this.timeWindow.stDiff,
this.stateData, this.ctx.timeService);
}
this.updateTimewindow();
return this.subscriptionTimewindow;
}
private dataUpdated(sourceData: DataSetHolder, datasourceIndex: number, dataKeyIndex: number, detectChanges: boolean) {
for (let x = 0; x < this.datasourceListeners.length; x++) {
this.datasources[x].dataReceived = this.datasources[x].dataReceived === true;
if (this.datasourceListeners[x].datasourceIndex === datasourceIndex && sourceData.data.length > 0) {
this.datasources[x].dataReceived = true;
}
}
this.notifyDataLoaded();
let update = true;
let currentData: DataSetHolder;
if (this.displayLegend && this.legendData.keys[datasourceIndex + dataKeyIndex].dataKey.hidden) {
currentData = this.hiddenData[datasourceIndex + dataKeyIndex];
} else {
currentData = this.data[datasourceIndex + dataKeyIndex];
}
if (this.type === widgetType.latest) {
const prevData = currentData.data;
if (!sourceData.data.length) {
update = false;
} else if (prevData && prevData[0] && prevData[0].length > 1 && sourceData.data.length > 0) {
const prevTs = prevData[0][0];
const prevValue = prevData[0][1];
if (prevTs === sourceData.data[0][0] && prevValue === sourceData.data[0][1]) {
update = false;
}
}
}
if (update) {
if (this.subscriptionTimewindow && this.subscriptionTimewindow.realtimeWindowMs) {
this.updateTimewindow();
}
currentData.data = sourceData.data;
if (this.caulculateLegendData) {
this.updateLegend(datasourceIndex + dataKeyIndex, sourceData.data, detectChanges);
}
this.onDataUpdated(detectChanges);
}
}
private updateLegend(dataIndex: number, data: DataSet, detectChanges: boolean) {
const dataKey = this.legendData.keys[dataIndex].dataKey;
const decimals = isDefined(dataKey.decimals) ? dataKey.decimals : this.decimals;
const units = dataKey.units && dataKey.units.length ? dataKey.units : this.units;
const legendKeyData = this.legendData.data[dataIndex];
if (this.legendConfig.showMin) {
legendKeyData.min = this.ctx.widgetUtils.formatValue(calculateMin(data), decimals, units);
}
if (this.legendConfig.showMax) {
legendKeyData.max = this.ctx.widgetUtils.formatValue(calculateMax(data), decimals, units);
}
if (this.legendConfig.showAvg) {
legendKeyData.avg = this.ctx.widgetUtils.formatValue(calculateAvg(data), decimals, units);
}
if (this.legendConfig.showTotal) {
legendKeyData.total = this.ctx.widgetUtils.formatValue(calculateTotal(data), decimals, units);
}
this.callbacks.legendDataUpdated(this, detectChanges !== false);
}
private loadStDiff(): Observable<any> {
const loadSubject = new ReplaySubject(1);
if (this.ctx.getServerTimeDiff && this.timeWindow) {
@ -394,3 +657,47 @@ export class WidgetSubscription implements IWidgetSubscription {
return loadSubject.asObservable();
}
}
function calculateMin(data: DataSet): number {
if (data.length > 0) {
let result = Number(data[0][1]);
for (let i = 1; i < data.length; i++) {
result = Math.min(result, Number(data[i][1]));
}
return result;
} else {
return null;
}
}
function calculateMax(data: DataSet): number {
if (data.length > 0) {
let result = Number(data[0][1]);
for (let i = 1; i < data.length; i++) {
result = Math.max(result, Number(data[i][1]));
}
return result;
} else {
return null;
}
}
function calculateAvg(data: DataSet): number {
if (data.length > 0) {
return calculateTotal(data) / data.length;
} else {
return null;
}
}
function calculateTotal(data: DataSet): number {
if (data.length > 0) {
let result = 0;
data.forEach((dataRow) => {
result += Number(dataRow[1]);
});
return result;
} else {
return null;
}
}

31
ui-ngx/src/app/core/services/utils.service.ts

@ -54,6 +54,32 @@ export class UtilsService {
}
}
public hashCode(str: string): number {
let hash = 0;
let i: number;
let char: number;
if (str.length === 0) {
return hash;
}
for (i = 0; i < str.length; i++) {
char = str.charCodeAt(i);
// tslint:disable-next-line:no-bitwise
hash = ((hash << 5) - hash) + char;
// tslint:disable-next-line:no-bitwise
hash = hash & hash; // Convert to 32bit integer
}
return hash;
}
public objectHashCode(obj: any): number {
let hash = 0;
if (obj) {
const str = JSON.stringify(obj);
hash = this.hashCode(str);
}
return hash;
}
public processWidgetException(exception: any): ExceptionData {
const data = this.parseException(exception, -5);
if (this.widgetEditMode) {
@ -203,4 +229,9 @@ export class UtilsService {
});
}
public currentPerfTime(): number {
return this.window.performance && this.window.performance.now ?
this.window.performance.now() : Date.now();
}
}

36
ui-ngx/src/app/core/utils.ts

@ -79,10 +79,22 @@ export function isDefined(value: any): boolean {
return typeof value !== 'undefined';
}
export function isDefinedAndNotNull(value: any): boolean {
return typeof value !== 'undefined' && value !== null;
}
export function isFunction(value: any): boolean {
return typeof value === 'function';
}
export function isObject(value: any): boolean {
return value !== null && typeof value === 'object';
}
export function isNumber(value: any): boolean {
return typeof value === 'number';
}
export function objToBase64(obj: any): string {
const json = JSON.stringify(obj);
const encoded = utf8Encode(json);
@ -295,10 +307,24 @@ function utf8ToBytes(input: string, units?: number): number[] {
return bytes;
}
export function deepClone<T>(obj: T): T {
if (obj) {
return JSON.parse(JSON.stringify(obj));
} else {
return obj;
export function deepClone<T>(target: T): T {
if (target === null) {
return target;
}
if (target instanceof Date) {
return new Date(target.getTime()) as any;
}
if (target instanceof Array) {
const cp = [] as any[];
(target as any[]).forEach((v) => { cp.push(v); });
return cp.map((n: any) => deepClone<any>(n)) as any;
}
if (typeof target === 'object' && target !== {}) {
const cp = { ...(target as { [key: string]: any }) } as { [key: string]: any };
Object.keys(cp).forEach(k => {
cp[k] = deepClone<any>(cp[k]);
});
return cp as T;
}
return target;
}

7
ui-ngx/src/app/core/ws/telemetry-websocket.service.ts

@ -14,7 +14,7 @@
/// limitations under the License.
///
import { Inject, Injectable } from '@angular/core';
import { Inject, Injectable, NgZone } from '@angular/core';
import {
AttributesSubscriptionCmd,
GetHistoryCmd,
@ -66,6 +66,7 @@ export class TelemetryWebsocketService implements TelemetryService {
constructor(private store: Store<AppState>,
private authService: AuthService,
private ngZone: NgZone,
@Inject(WINDOW) private window: Window) {
this.store.pipe(select(selectIsAuthenticated)).subscribe(
(authenticated: boolean) => {
@ -222,7 +223,9 @@ export class TelemetryWebsocketService implements TelemetryService {
);
this.dataStream.subscribe((message) => {
this.onMessage(message as SubscriptionUpdateMsg);
this.ngZone.runOutsideAngular(() => {
this.onMessage(message as SubscriptionUpdateMsg);
});
},
(error) => {
this.onError(error);

10
ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.ts

@ -39,7 +39,7 @@ import {
IDashboardComponent,
WidgetsData
} from '../../models/dashboard-component.models';
import { merge, Observable } from 'rxjs';
import { merge, Observable, ReplaySubject, Subject } from 'rxjs';
import { map, share, tap } from 'rxjs/operators';
import { WidgetLayout } from '@shared/models/dashboard.models';
import { DialogService } from '@core/services/dialog.service';
@ -117,6 +117,10 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo
@Input()
dashboardTimewindow: Timewindow;
dashboardTimewindowChangedSubject: Subject<Timewindow> = new ReplaySubject<Timewindow>();
dashboardTimewindowChanged = this.dashboardTimewindowChangedSubject.asObservable();
originalDashboardTimewindow: Timewindow;
gridsterOpts: GridsterConfig;
@ -262,18 +266,20 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo
ngAfterViewInit(): void {
}
onUpdateTimewindow(startTimeMs: number, endTimeMs: number, interval: number): void {
onUpdateTimewindow(startTimeMs: number, endTimeMs: number, interval?: number): void {
if (!this.originalDashboardTimewindow) {
this.originalDashboardTimewindow = deepClone(this.dashboardTimewindow);
}
this.dashboardTimewindow = toHistoryTimewindow(this.dashboardTimewindow,
startTimeMs, endTimeMs, interval, this.timeService);
this.dashboardTimewindowChangedSubject.next(this.dashboardTimewindow);
}
onResetTimewindow(): void {
if (this.originalDashboardTimewindow) {
this.dashboardTimewindow = deepClone(this.originalDashboardTimewindow);
this.originalDashboardTimewindow = null;
this.dashboardTimewindowChangedSubject.next(this.dashboardTimewindow);
}
}

4
ui-ngx/src/app/modules/home/components/entity/entities-table.component.ts

@ -158,7 +158,7 @@ export class EntitiesTableComponent extends PageComponent implements AfterViewIn
if (this.entitiesTableConfig.useTimePageLink) {
this.timewindow = historyInterval(24 * 60 * 60 * 1000);
const currentTime = new Date().getTime();
const currentTime = Date.now();
this.pageLink = new TimePageLink(10, 0, null, sortOrder,
currentTime - this.timewindow.history.timewindowMs, currentTime);
} else {
@ -216,7 +216,7 @@ export class EntitiesTableComponent extends PageComponent implements AfterViewIn
if (this.entitiesTableConfig.useTimePageLink) {
const timePageLink = this.pageLink as TimePageLink;
if (this.timewindow.history.timewindowMs) {
const currentTime = new Date().getTime();
const currentTime = Date.now();
timePageLink.startTime = currentTime - this.timewindow.history.timewindowMs;
timePageLink.endTime = currentTime;
} else {

4
ui-ngx/src/app/modules/home/components/widget/dynamic-widget.component.ts

@ -21,6 +21,7 @@ import { AppState } from '@core/core.state';
import { WidgetContext, IDynamicWidgetComponent } from '@home/models/widget-component.models';
import { ExceptionData } from '@shared/models/error.models';
import { HttpErrorResponse } from '@angular/common/http';
import { RafService } from '@core/services/raf.service';
export class DynamicWidgetComponent extends PageComponent implements IDynamicWidgetComponent, OnInit, OnDestroy {
@ -37,7 +38,8 @@ export class DynamicWidgetComponent extends PageComponent implements IDynamicWid
[key: string]: any;
constructor(protected store: Store<AppState>) {
constructor(public raf: RafService,
protected store: Store<AppState>) {
super(store);
}

4
ui-ngx/src/app/modules/home/components/widget/legend.component.scss

@ -29,10 +29,14 @@
padding: 0 10px 1px 0;
color: rgb(255, 110, 64);
white-space: nowrap;
font-size: 12px;
}
}
.tb-legend-keys {
td {
font-size: 12px;
}
td.tb-legend-label,
td.tb-legend-value {
padding: 2px 10px;

6
ui-ngx/src/app/modules/home/components/widget/legend.component.ts

@ -14,7 +14,7 @@
/// limitations under the License.
///
import { Component, Input, OnInit } from '@angular/core';
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { LegendConfig, LegendData, LegendDirection, LegendPosition } from '@shared/models/widget.models';
@Component({
@ -30,6 +30,9 @@ export class LegendComponent implements OnInit {
@Input()
legendData: LegendData;
@Output()
legendKeyHiddenChange = new EventEmitter<number>();
displayHeader: boolean;
isHorizontal: boolean;
@ -50,6 +53,7 @@ export class LegendComponent implements OnInit {
toggleHideData(index: number) {
this.legendData.keys[index].dataKey.hidden = !this.legendData.keys[index].dataKey.hidden;
this.legendKeyHiddenChange.emit(index);
}
}

592
ui-ngx/src/app/modules/home/components/widget/lib/flot-widget.models.ts

@ -0,0 +1,592 @@
///
/// Copyright © 2016-2019 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { JsonSettingsSchema, DataKey, DatasourceData } from '@shared/models/widget.models';
export declare type ChartType = 'line' | 'pie' | 'bar' | 'state' | 'graph';
export declare type TbFlotSettings = TbFlotBaseSettings & TbFlotGraphSettings & TbFlotBarSettings & TbFlotPieSettings;
export declare type TooltipValueFormatFunction = (value: any) => string;
export declare type TbFlotTicksFormatterFunction = (t: number, a?: TbFlotPlotAxis) => string;
export interface TbFlotSeries extends DatasourceData, JQueryPlotSeriesOptions {
dataKey: TbFlotDataKey;
yaxisIndex?: number;
yaxis?: number;
}
export interface TbFlotDataKey extends DataKey {
settings?: TbFlotKeySettings;
tooltipValueFormatFunction?: TooltipValueFormatFunction;
}
export interface TbFlotPlotAxis extends JQueryPlotAxis, TbFlotAxisOptions {
options: TbFlotAxisOptions;
}
export interface TbFlotAxisOptions extends JQueryPlotAxisOptions {
tickUnits?: string;
hidden?: boolean;
keysInfo?: Array<{hidden: boolean}>;
ticksFormatterFunction?: TbFlotTicksFormatterFunction;
}
export interface TbFlotPlotDataSeries extends JQueryPlotDataSeries {
dataKey?: TbFlotDataKey;
percent?: number;
}
export interface TbFlotPlotItem extends jquery.flot.item {
series: TbFlotPlotDataSeries;
}
export interface TbFlotHoverInfo {
seriesHover: Array<TbFlotSeriesHoverInfo>;
time?: any;
}
export interface TbFlotSeriesHoverInfo {
hoverIndex: number;
units: string;
decimals: number;
label: string;
color: string;
index: number;
tooltipValueFormatFunction: TooltipValueFormatFunction;
value: any;
time: any;
distance: number;
}
export interface TbFlotGridSettings {
color: string;
backgroundColor: string;
tickColor: string;
outlineWidth: number;
verticalLines: boolean;
horizontalLines: boolean;
minBorderMargin: number;
margin: number;
}
export interface TbFlotXAxisSettings {
showLabels: boolean;
title: string;
color: boolean;
}
export interface TbFlotYAxisSettings {
min: number;
max: number;
showLabels: boolean;
title: string;
color: string;
ticksFormatter: string;
tickDecimals: number;
tickSize: number;
}
export interface TbFlotBaseSettings {
stack: boolean;
shadowSize: number;
fontColor: string;
fontSize: number;
tooltipIndividual: boolean;
tooltipCumulative: boolean;
tooltipValueFormatter: string;
grid: TbFlotGridSettings;
xaxis: TbFlotXAxisSettings;
yaxis: TbFlotYAxisSettings;
}
export interface TbFlotGraphSettings extends TbFlotBaseSettings {
smoothLines: boolean;
}
export interface TbFlotBarSettings extends TbFlotBaseSettings {
defaultBarWidth: number;
}
export interface TbFlotPieSettings {
radius: number;
innerRadius: number;
tilt: number;
animatedPie: boolean;
stroke: {
color: string;
width: number;
};
showLabels: boolean;
fontColor: string;
fontSize: number;
}
export declare type TbFlotYAxisPosition = 'left' | 'right';
export interface TbFlotKeySettings {
showLines: boolean;
fillLines: boolean;
showPoints: boolean;
lineWidth: number;
tooltipValueFormatter: string;
showSeparateAxis: boolean;
axisMin: number;
axisMax: number;
axisTitle: string;
axisTickDecimals: number;
axisTickSize: number;
axisPosition: TbFlotYAxisPosition;
axisTicksFormatter: string;
}
export function flotSettingsSchema(chartType: ChartType): JsonSettingsSchema {
const schema: JsonSettingsSchema = {
schema: {
type: 'object',
title: 'Settings',
properties: {
}
}
};
const properties: any = schema.schema.properties;
properties.stack = {
title: 'Stacking',
type: 'boolean',
default: false
};
if (chartType === 'graph') {
properties.smoothLines = {
title: 'Display smooth (curved) lines',
type: 'boolean',
default: false
};
}
if (chartType === 'bar') {
properties.defaultBarWidth = {
title: 'Default bar width for non-aggregated data (milliseconds)',
type: 'number',
default: 600
};
}
properties.shadowSize = {
title: 'Shadow size',
type: 'number',
default: 4
};
properties.fontColor = {
title: 'Font color',
type: 'string',
default: '#545454'
};
properties.fontSize = {
title: 'Font size',
type: 'number',
default: 10
};
properties.tooltipIndividual = {
title: 'Hover individual points',
type: 'boolean',
default: false
};
properties.tooltipCumulative = {
title: 'Show cumulative values in stacking mode',
type: 'boolean',
default: false
};
properties.tooltipValueFormatter = {
title: 'Tooltip value format function, f(value)',
type: 'string',
default: ''
};
properties.grid = {
title: 'Grid settings',
type: 'object',
properties: {
color: {
title: 'Primary color',
type: 'string',
default: '#545454'
},
backgroundColor: {
title: 'Background color',
type: 'string',
default: null
},
tickColor: {
title: 'Ticks color',
type: 'string',
default: '#DDDDDD'
},
outlineWidth: {
title: 'Grid outline/border width (px)',
type: 'number',
default: 1
},
verticalLines: {
title: 'Show vertical lines',
type: 'boolean',
default: true
},
horizontalLines: {
title: 'Show horizontal lines',
type: 'boolean',
default: true
}
}
};
properties.xaxis = {
title: 'X axis settings',
type: 'object',
properties: {
showLabels: {
title: 'Show labels',
type: 'boolean',
default: true
},
title: {
title: 'Axis title',
type: 'string',
default: null
},
color: {
title: 'Ticks color',
type: 'string',
default: null
}
}
};
properties.yaxis = {
title: 'Y axis settings',
type: 'object',
properties: {
min: {
title: 'Minimum value on the scale',
type: 'number',
default: null
},
max: {
title: 'Maximum value on the scale',
type: 'number',
default: null
},
showLabels: {
title: 'Show labels',
type: 'boolean',
default: true
},
title: {
title: 'Axis title',
type: 'string',
default: null
},
color: {
title: 'Ticks color',
type: 'string',
default: null
},
ticksFormatter: {
title: 'Ticks formatter function, f(value)',
type: 'string',
default: ''
},
tickDecimals: {
title: 'The number of decimals to display',
type: 'number',
default: 0
},
tickSize: {
title: 'Step size between ticks',
type: 'number',
default: null
}
}
};
schema.schema.required = [];
schema.form = ['stack'];
if (chartType === 'graph') {
schema.form.push('smoothLines');
}
if (chartType === 'bar') {
schema.form.push('defaultBarWidth');
}
schema.form.push('shadowSize');
schema.form.push({
key: 'fontColor',
type: 'color'
});
schema.form.push('fontSize');
schema.form.push('tooltipIndividual');
schema.form.push('tooltipCumulative');
schema.form.push({
key: 'tooltipValueFormatter',
type: 'javascript'
});
schema.form.push({
key: 'grid',
items: [
{
key: 'grid.color',
type: 'color'
},
{
key: 'grid.backgroundColor',
type: 'color'
},
{
key: 'grid.tickColor',
type: 'color'
},
'grid.outlineWidth',
'grid.verticalLines',
'grid.horizontalLines'
]
});
schema.form.push({
key: 'xaxis',
items: [
'xaxis.showLabels',
'xaxis.title',
{
key: 'xaxis.color',
type: 'color'
}
]
});
schema.form.push({
key: 'yaxis',
items: [
'yaxis.min',
'yaxis.max',
'yaxis.tickDecimals',
'yaxis.tickSize',
'yaxis.showLabels',
'yaxis.title',
{
key: 'yaxis.color',
type: 'color'
},
{
key: 'yaxis.ticksFormatter',
type: 'javascript'
}
]
});
return schema;
}
export function flotPieSettingsSchema(): JsonSettingsSchema {
return {
schema: {
type: 'object',
title: 'Settings',
properties: {
radius: {
title: 'Radius',
type: 'number',
default: 1
},
innerRadius: {
title: 'Inner radius',
type: 'number',
default: 0
},
tilt: {
title: 'Tilt',
type: 'number',
default: 1
},
animatedPie: {
title: 'Enable pie animation (experimental)',
type: 'boolean',
default: false
},
stroke: {
title: 'Stroke',
type: 'object',
properties: {
color: {
title: 'Color',
type: 'string',
default: ''
},
width: {
title: 'Width (pixels)',
type: 'number',
default: 0
}
}
},
showLabels: {
title: 'Show labels',
type: 'boolean',
default: false
},
fontColor: {
title: 'Font color',
type: 'string',
default: '#545454'
},
fontSize: {
title: 'Font size',
type: 'number',
default: 10
}
},
required: []
},
form: [
'radius',
'innerRadius',
'animatedPie',
'tilt',
{
key: 'stroke',
items: [
{
key: 'stroke.color',
type: 'color'
},
'stroke.width'
]
},
'showLabels',
{
key: 'fontColor',
type: 'color'
},
'fontSize'
]
};
}
export function flotDatakeySettingsSchema(defaultShowLines: boolean): JsonSettingsSchema {
return {
schema: {
type: 'object',
title: 'DataKeySettings',
properties: {
showLines: {
title: 'Show lines',
type: 'boolean',
default: defaultShowLines
},
fillLines: {
title: 'Fill lines',
type: 'boolean',
default: false
},
showPoints: {
title: 'Show points',
type: 'boolean',
default: false
},
lineWidth: {
title: 'Line width',
type: 'number',
default: null
},
tooltipValueFormatter: {
title: 'Tooltip value format function, f(value)',
type: 'string',
default: ''
},
showSeparateAxis: {
title: 'Show separate axis',
type: 'boolean',
default: false
},
axisMin: {
title: 'Minimum value on the axis scale',
type: 'number',
default: null
},
axisMax: {
title: 'Maximum value on the axis scale',
type: 'number',
default: null
},
axisTitle: {
title: 'Axis title',
type: 'string',
default: ''
},
axisTickDecimals: {
title: 'Axis tick number of digits after floating point',
type: 'number',
default: null
},
axisTickSize: {
title: 'Axis step size between ticks',
type: 'number',
default: null
},
axisPosition: {
title: 'Axis position',
type: 'string',
default: 'left'
},
axisTicksFormatter: {
title: 'Ticks formatter function, f(value)',
type: 'string',
default: ''
}
},
required: ['showLines', 'fillLines', 'showPoints']
},
form: [
'showLines',
'fillLines',
'showPoints',
{
key: 'tooltipValueFormatter',
type: 'javascript'
},
'showSeparateAxis',
'axisMin',
'axisMax',
'axisTitle',
'axisTickDecimals',
'axisTickSize',
{
key: 'axisPosition',
type: 'rc-select',
multiple: false,
items: [
{
value: 'left',
label: 'Left'
},
{
value: 'right',
label: 'Right'
}
]
},
{
key: 'axisTicksFormatter',
type: 'javascript'
}
]
};
}

1074
ui-ngx/src/app/modules/home/components/widget/lib/flot-widget.ts

File diff suppressed because it is too large

3
ui-ngx/src/app/modules/home/components/widget/widget-component.service.ts

@ -36,6 +36,7 @@ import { WidgetComponentsModule } from '@home/components/widget/widget-component
import { WINDOW } from '@core/services/window.service';
import * as tinycolor from 'tinycolor2';
import { TbFlot } from './lib/flot-widget';
// declare var jQuery: any;
@ -63,6 +64,8 @@ export class WidgetComponentService {
this.window.tinycolor = tinycolor;
// @ts-ignore
this.window.cssjs = cssjs;
// @ts-ignore
this.window.TbFlot = TbFlot;
this.cssParser.testMode = false;
this.init();

14
ui-ngx/src/app/modules/home/components/widget/widget.component.html

@ -16,11 +16,21 @@
-->
<div class="tb-absolute-fill" [fxLayout]="legendContainerLayoutType">
<tb-legend *ngIf="displayLegend && isLegendFirst" [ngStyle]="legendStyle" [legendConfig]="legendConfig" [legendData]="legendData"></tb-legend>
<tb-legend *ngIf="displayLegend && isLegendFirst"
[ngStyle]="legendStyle"
[legendConfig]="legendConfig"
[legendData]="legendData"
(legendKeyHiddenChange)="onLegendKeyHiddenChange($event)">
</tb-legend>
<div fxFlex id="widget-container">
<ng-container #widgetContent></ng-container>
</div>
<tb-legend *ngIf="displayLegend && !isLegendFirst" [ngStyle]="legendStyle" [legendConfig]="legendConfig" [legendData]="legendData"></tb-legend>
<tb-legend *ngIf="displayLegend && !isLegendFirst"
[ngStyle]="legendStyle"
[legendConfig]="legendConfig"
[legendData]="legendData"
(legendKeyHiddenChange)="onLegendKeyHiddenChange($event)">
</tb-legend>
</div>
<div class="tb-absolute-fill tb-widget-error" *ngIf="widgetErrorData">
<span>Widget Error: {{ widgetErrorData.name + ": " + widgetErrorData.message}}</span>

46
ui-ngx/src/app/modules/home/components/widget/widget.component.ts

@ -28,7 +28,8 @@ import {
SimpleChanges,
ViewChild,
ViewContainerRef,
ViewEncapsulation
ViewEncapsulation,
ChangeDetectorRef
} from '@angular/core';
import { DashboardWidget, IDashboardComponent } from '@home/models/dashboard-component.models';
import {
@ -157,7 +158,8 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
private dashboardService: DashboardService,
private datasourceService: DatasourceService,
private utils: UtilsService,
private raf: RafService) {
private raf: RafService,
private cd: ChangeDetectorRef) {
super(store);
}
@ -290,7 +292,7 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
}
};
this.widgetContext.utils = {
formatValue: this.formatValue
formatValue: this.formatValue.bind(this)
};
this.widgetContext.actionsApi = {
actionDescriptorsBySourceId,
@ -325,6 +327,7 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
alarmService: this.alarmService,
datasourceService: this.datasourceService,
utils: this.utils,
raf: this.raf,
widgetUtils: this.widgetContext.utils,
dashboardTimewindowApi: {
onResetTimewindow: this.dashboard.onResetTimewindow.bind(this.dashboard),
@ -407,6 +410,13 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
}
}
public onLegendKeyHiddenChange(index: number) {
for (const id of Object.keys(this.widgetContext.subscriptions)) {
const subscription = this.widgetContext.subscriptions[id];
subscription.updateDataVisibility(index);
}
}
private loadFromWidgetInfo() {
const widgetNamespace = `widget-type-${(this.widget.isSystemType ? 'sys-' : '')}${this.widget.bundleAlias}-${this.widget.typeAlias}`;
const elem = this.elementRef.nativeElement;
@ -464,11 +474,17 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
}
if (!this.widgetContext.inited && this.isReady()) {
this.widgetContext.inited = true;
try {
this.widgetTypeInstance.onInit();
} catch (e) {
this.handleWidgetException(e);
if (this.cafs.init) {
this.cafs.init();
this.cafs.init = null;
}
this.cafs.init = this.raf.raf(() => {
try {
this.widgetTypeInstance.onInit();
} catch (e) {
this.handleWidgetException(e);
}
});
if (!this.typeParameters.useCustomDatasources && this.widgetContext.defaultSubscription) {
this.widgetContext.defaultSubscription.subscribe();
}
@ -570,6 +586,15 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
}
));
this.rxSubscriptions.push(this.dashboard.dashboardTimewindowChanged.subscribe(
(dashboardTimewindow) => {
for (const id of Object.keys(this.widgetContext.subscriptions)) {
const subscription = this.widgetContext.subscriptions[id];
subscription.onDashboardTimewindowChanged(dashboardTimewindow);
}
}
));
this.configureDynamicWidgetComponent();
if (!this.typeParameters.useCustomDatasources) {
// this.cre
@ -722,12 +747,17 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
dataLoading: (subscription) => {
if (this.loadingData !== subscription.loadingData) {
this.loadingData = subscription.loadingData;
this.cd.detectChanges();
}
},
legendDataUpdated: (subscription) => {
legendDataUpdated: (subscription, detectChanges) => {
if (detectChanges) {
this.cd.detectChanges();
}
},
timeWindowUpdated: (subscription, timeWindowConfig) => {
this.widget.config.timewindow = timeWindowConfig;
this.cd.detectChanges();
}
};

3
ui-ngx/src/app/modules/home/models/dashboard-component.models.ts

@ -47,9 +47,10 @@ export interface IDashboardComponent {
isMobileSize: boolean;
autofillHeight: boolean;
dashboardTimewindow: Timewindow;
dashboardTimewindowChanged: Observable<Timewindow>;
aliasController: IAliasController;
stateController: IStateController;
onUpdateTimewindow(startTimeMs: number, endTimeMs: number, interval: number): void;
onUpdateTimewindow(startTimeMs: number, endTimeMs: number, interval?: number): void;
onResetTimewindow(): void;
}

6
ui-ngx/src/app/modules/home/models/widget-component.models.ts

@ -29,7 +29,7 @@ import {
WidgetTypeDescriptor,
WidgetTypeParameters
} from '@shared/models/widget.models';
import { Timewindow } from '@shared/models/time/time.models';
import { Timewindow, WidgetTimewindow } from '@shared/models/time/time.models';
import {
EntityInfo,
IAliasController,
@ -43,6 +43,7 @@ import {
} from '@core/api/widget-api.models';
import { ComponentFactory } from '@angular/core';
import { HttpErrorResponse } from '@angular/common/http';
import { RafService } from '@core/services/raf.service';
export interface IWidgetAction {
name: string;
@ -93,7 +94,7 @@ export interface WidgetContext {
datasources?: Array<Datasource>;
data?: Array<DatasourceData>;
hiddenData?: Array<{data: DataSet}>;
timeWindow?: Timewindow;
timeWindow?: WidgetTimewindow;
}
export interface IDynamicWidgetComponent {
@ -103,6 +104,7 @@ export interface IDynamicWidgetComponent {
rpcEnabled: boolean;
rpcErrorText: string;
rpcRejection: HttpErrorResponse;
raf: RafService;
[key: string]: any;
}

13
ui-ngx/src/app/shared/models/telemetry/telemetry.models.ts

@ -158,18 +158,25 @@ export class TelemetryPluginCmdsWrapper {
}
}
export interface SubscriptionUpdateMsg {
export interface SubscriptionData {
[key: string]: [number, any][];
}
export interface SubscriptionDataHolder {
data: SubscriptionData;
}
export interface SubscriptionUpdateMsg extends SubscriptionDataHolder {
subscriptionId: number;
errorCode: number;
errorMsg: string;
data: {[key: string]: [number, string][]};
}
export class SubscriptionUpdate implements SubscriptionUpdateMsg {
subscriptionId: number;
errorCode: number;
errorMsg: string;
data: {[key: string]: [number, string][]};
data: SubscriptionData;
constructor(msg: SubscriptionUpdateMsg) {
this.subscriptionId = msg.subscriptionId;

84
ui-ngx/src/app/shared/models/time/time.models.ts

@ -21,6 +21,7 @@ export const SECOND = 1000;
export const MINUTE = 60 * SECOND;
export const HOUR = 60 * MINUTE;
export const DAY = 24 * HOUR;
export const YEAR = DAY * 365;
export enum TimewindowType {
REALTIME,
@ -68,6 +69,7 @@ export const aggregationTranslations = new Map<AggregationType, string>(
);
export interface Aggregation {
interval?: number;
type: AggregationType;
limit: number;
}
@ -78,6 +80,25 @@ export interface Timewindow {
realtime?: IntervalWindow;
history?: HistoryWindow;
aggregation?: Aggregation;
}
export interface SubscriptionAggregation extends Aggregation {
interval?: number;
timeWindow?: number;
stateData?: boolean;
}
export interface SubscriptionTimewindow {
startTs?: number;
realtimeWindowMs?: number;
fixedWindow?: FixedWindow;
aggregation?: SubscriptionAggregation;
}
export interface WidgetTimewindow {
minTime?: number;
maxTime?: number;
interval?: number;
stDiff?: number;
}
@ -91,7 +112,7 @@ export function historyInterval(timewindowMs: number): Timewindow {
}
export function defaultTimewindow(timeService: TimeService): Timewindow {
const currentTime = new Date().getTime();
const currentTime = Date.now();
const timewindow: Timewindow = {
displayValue: '',
selectedTab: TimewindowType.REALTIME,
@ -183,6 +204,67 @@ export function toHistoryTimewindow(timewindow: Timewindow, startTimeMs: number,
return historyTimewindow;
}
export function createSubscriptionTimewindow(timewindow: Timewindow, stDiff: number, stateData: boolean,
timeService: TimeService): SubscriptionTimewindow {
const subscriptionTimewindow: SubscriptionTimewindow = {
fixedWindow: null,
realtimeWindowMs: null,
aggregation: {
interval: SECOND,
limit: timeService.getMaxDatapointsLimit(),
type: AggregationType.AVG
}
};
let aggTimewindow = 0;
if (stateData) {
subscriptionTimewindow.aggregation.type = AggregationType.NONE;
subscriptionTimewindow.aggregation.stateData = true;
}
if (isDefined(timewindow.aggregation) && !stateData) {
subscriptionTimewindow.aggregation = {
type: timewindow.aggregation.type || AggregationType.AVG,
limit: timewindow.aggregation.limit || timeService.getMaxDatapointsLimit()
};
}
if (isDefined(timewindow.realtime)) {
subscriptionTimewindow.realtimeWindowMs = timewindow.realtime.timewindowMs;
subscriptionTimewindow.aggregation.interval =
timeService.boundIntervalToTimewindow(subscriptionTimewindow.realtimeWindowMs, timewindow.realtime.interval,
subscriptionTimewindow.aggregation.type);
subscriptionTimewindow.startTs = Date.now() + stDiff - subscriptionTimewindow.realtimeWindowMs;
const startDiff = subscriptionTimewindow.startTs % subscriptionTimewindow.aggregation.interval;
aggTimewindow = subscriptionTimewindow.realtimeWindowMs;
if (startDiff) {
subscriptionTimewindow.startTs -= startDiff;
aggTimewindow += subscriptionTimewindow.aggregation.interval;
}
} else if (isDefined(timewindow.history)) {
if (isDefined(timewindow.history.timewindowMs)) {
const currentTime = Date.now();
subscriptionTimewindow.fixedWindow = {
startTimeMs: currentTime - timewindow.history.timewindowMs,
endTimeMs: currentTime
};
aggTimewindow = timewindow.history.timewindowMs;
} else {
subscriptionTimewindow.fixedWindow = {
startTimeMs: timewindow.history.fixedTimewindow.startTimeMs,
endTimeMs: timewindow.history.fixedTimewindow.endTimeMs
};
aggTimewindow = subscriptionTimewindow.fixedWindow.endTimeMs - subscriptionTimewindow.fixedWindow.startTimeMs;
}
subscriptionTimewindow.startTs = subscriptionTimewindow.fixedWindow.startTimeMs;
subscriptionTimewindow.aggregation.interval =
timeService.boundIntervalToTimewindow(aggTimewindow, timewindow.history.interval, subscriptionTimewindow.aggregation.type);
}
const aggregation = subscriptionTimewindow.aggregation;
aggregation.timeWindow = aggTimewindow;
if (aggregation.type !== AggregationType.NONE) {
aggregation.limit = Math.ceil(aggTimewindow / subscriptionTimewindow.aggregation.interval);
}
return subscriptionTimewindow;
}
export function cloneSelectedTimewindow(timewindow: Timewindow): Timewindow {
const cloned: Timewindow = {};
if (isDefined(timewindow.selectedTab)) {

27
ui-ngx/src/app/shared/models/widget.models.ts

@ -221,16 +221,21 @@ export interface Datasource {
entityId?: string;
entityName?: string;
entityAliasId?: string;
unresolvedStateEntity?: boolean;
dataReceived?: boolean;
[key: string]: any;
// TODO:
}
export type DataSet = [number, any][];
export interface DatasourceData {
export interface DataSetHolder {
data: DataSet;
}
export interface DatasourceData extends DataSetHolder {
datasource: Datasource;
dataKey: DataKey;
data: DataSet;
}
export interface LegendKey {
@ -239,10 +244,10 @@ export interface LegendKey {
}
export interface LegendKeyData {
min: number;
max: number;
avg: number;
total: number;
min: string;
max: string;
avg: string;
total: string;
hidden: boolean;
}
@ -340,3 +345,13 @@ export interface Widget {
col: number;
config: WidgetConfig;
}
export interface JsonSettingsSchema {
schema?: {
type: string;
title: string;
properties: {[key: string]: any};
required?: string[];
};
form?: any[];
}

4
ui-ngx/src/styles.scss

@ -209,6 +209,10 @@ label {
}
}
.tb-noselect {
user-select: none;
}
div {
&.tb-small {
font-size: 14px;

2
ui-ngx/src/tsconfig.app.json

@ -2,7 +2,7 @@
"extends": "../tsconfig.json",
"compilerOptions": {
"outDir": "../out-tsc/app",
"types": ["node", "jquery"]
"types": ["node", "jquery", "flot", "tinycolor2"]
},
"exclude": [
"test.ts",

128
ui-ngx/src/typings/jquery.flot.typings.d.ts

@ -0,0 +1,128 @@
///
/// Copyright © 2016-2019 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
interface JQueryPlot extends jquery.flot.plot {
destroy(): void;
highlight(series: jquery.flot.dataSeries | number, datapoint: jquery.flot.item | number): void;
clearSelection(): void;
}
interface JQueryPlotPoint extends jquery.flot.point {
pageX: number;
pageY: number;
}
interface JQueryPlotDataSeries extends jquery.flot.dataSeries, JQueryPlotSeriesOptions {
datapoints?: jquery.flot.datapoints;
}
interface JQueryPlotOptions extends jquery.flot.plotOptions {
title?: string;
subtitile?: string;
shadowSize?: number;
HtmlText?: boolean;
selection?: JQueryPlotSelection;
xaxis?: JQueryPlotAxisOptions;
series?: JQueryPlotSeriesOptions;
crosshair?: JQueryPlotCrosshairOptions;
}
interface JQueryPlotAxisOptions extends jquery.flot.axisOptions {
label?: string;
labelFont?: any;
}
interface JQueryPlotAxis extends jquery.flot.axis, JQueryPlotAxisOptions {
options: JQueryPlotAxisOptions;
}
interface JQueryPlotSeriesOptions extends jquery.flot.seriesOptions {
stack?: boolean;
curvedLines?: JQueryPlotCurvedLinesOptions;
pie?: JQueryPlotPieOptions;
}
declare type JQueryPlotCrosshairMode = 'x' | 'y' | 'xy' | null;
interface JQueryPlotCrosshairOptions {
mode?: JQueryPlotCrosshairMode;
color?: string;
lineWidth?: number;
}
interface JQueryPlotCurvedLinesOptions {
active?: boolean;
apply?: boolean;
monotonicFit?: boolean;
tension?: number;
nrSplinePoints?: number;
legacyOverride?: any;
}
interface JQueryPlotPieOptions {
show?: boolean;
radius?: any;
innerRadius?: any;
startAngle?: number;
tilt?: number;
offset?: {
top?: number;
left?: number;
};
stroke?: {
color?: string;
width?: number;
};
shadow?: {
top?: number;
left?: number;
alpha?: number;
};
label?: {
show?: boolean;
formatter?: (label: string, slice?: any) => string;
radius?: any;
background?: {
color?: string;
opacity?: number;
};
threshold?: number;
};
combine?: {
threshold?: number;
color?: string;
label?: string;
};
highlight?: number;
}
declare type JQueryPlotSelectionMode = 'x' | 'y' | 'xy' | null;
declare type JQueryPlotSelectionShape = 'round' | 'mitter' | 'bevel';
interface JQueryPlotSelection {
mode?: JQueryPlotSelectionMode;
color?: string;
shape?: JQueryPlotSelectionShape;
minSize?: number;
}
interface JQueryPlotSelectionRanges {
[axis: string]: {
from: number;
to: number;
};
}

0
ui-ngx/src/typings.d.ts → ui-ngx/src/typings/jquery.typings.d.ts

3
ui-ngx/tsconfig.json

@ -13,7 +13,8 @@
"target": "es5",
"typeRoots": [
"node_modules/@types",
"src/typings.d.ts"
"src/typings/jquery.typings.d.ts",
"src/typings/jquery.flot.typings.d.ts"
],
"paths": {
"@app/*": ["src/app/*"],

Loading…
Cancel
Save