31 changed files with 3500 additions and 54 deletions
@ -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; |
|||
} |
|||
} |
|||
|
|||
} |
|||
|
|||
@ -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; |
|||
} |
|||
} |
|||
|
|||
} |
|||
@ -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' |
|||
} |
|||
] |
|||
}; |
|||
} |
|||
File diff suppressed because it is too large
@ -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; |
|||
}; |
|||
} |
|||
Loading…
Reference in new issue