Browse Source

UI: Introduce new aggregation intervals. Improve data intervals processing and aggregation.

pull/9990/head
Igor Kulikov 3 years ago
parent
commit
56792bba97
  1. 2
      application/src/main/data/json/system/widget_types/bar_chart_with_labels.json
  2. 6
      application/src/main/java/org/thingsboard/server/ThingsboardServerApplication.java
  3. 1
      ui-ngx/package.json
  4. 161
      ui-ngx/src/app/core/api/data-aggregator.ts
  5. 96
      ui-ngx/src/app/core/api/entity-data-subscription.ts
  6. 9
      ui-ngx/src/app/core/api/widget-subscription.ts
  7. 67
      ui-ngx/src/app/core/services/time.service.ts
  8. 4
      ui-ngx/src/app/modules/home/components/widget/lib/cards/aggregated-value-card.models.ts
  9. 51
      ui-ngx/src/app/modules/home/components/widget/lib/chart/bar-chart-with-labels-widget.component.ts
  10. 54
      ui-ngx/src/app/modules/home/components/widget/lib/chart/echarts-widget.models.ts
  11. 3
      ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.component.ts
  12. 8
      ui-ngx/src/app/modules/home/components/widget/lib/flot-widget.ts
  13. 5
      ui-ngx/src/app/modules/home/components/widget/lib/indicator/liquid-level-widget.component.ts
  14. 2
      ui-ngx/src/app/shared/components/time/timeinterval.component.html
  15. 70
      ui-ngx/src/app/shared/components/time/timeinterval.component.ts
  16. 2
      ui-ngx/src/app/shared/components/time/timewindow-panel.component.html
  17. 18
      ui-ngx/src/app/shared/models/telemetry/telemetry.models.ts
  18. 178
      ui-ngx/src/app/shared/models/time/time.models.ts
  19. 16
      ui-ngx/src/app/shared/models/widget-settings.models.ts
  20. 8
      ui-ngx/src/app/shared/models/widget.models.ts
  21. 6
      ui-ngx/src/assets/locale/locale.constant-en_US.json
  22. 5
      ui-ngx/yarn.lock

2
application/src/main/data/json/system/widget_types/bar_chart_with_labels.json

@ -20,7 +20,7 @@
"latestDataKeySettingsDirective": "",
"hasBasicMode": true,
"basicModeDirective": "tb-bar-chart-with-labels-basic-config",
"defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Humidity\",\"color\":\"rgb(125, 142, 255)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < 50) {\\n\\tvalue = 50;\\n} else if (value > 80) {\\n\\tvalue = 80;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Soil moisture\",\"color\":\"rgb(249, 111, 255)\",\"settings\":{},\"_hash\":0.9111685461089025,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < 30) {\\n\\tvalue = 30;\\n} else if (value > 90) {\\n\\tvalue = 90;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Leaf wetness\",\"color\":\"rgb(255, 163, 137)\",\"settings\":{},\"_hash\":0.8487533373085416,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < 40) {\\n\\tvalue = 40;\\n} else if (value > 70) {\\n\\tvalue = 70;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Cloud cover\",\"color\":\"#FFED53\",\"settings\":{},\"_hash\":0.7690144858984289,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < 20) {\\n\\tvalue = 20;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"hideInterval\":false,\"hideLastInterval\":false,\"hideQuickInterval\":false,\"hideAggregation\":false,\"hideAggInterval\":false,\"hideTimezone\":false,\"selectedTab\":1,\"history\":{\"historyType\":2,\"timewindowMs\":60000,\"interval\":2592000000,\"fixedTimewindow\":{\"startTimeMs\":1704293713163,\"endTimeMs\":1704380113163},\"quickInterval\":\"CURRENT_HALF_YEAR\"},\"aggregation\":{\"type\":\"AVG\",\"limit\":25000},\"timezone\":null},\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"showBarLabel\":true,\"barLabelFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"12px\"},\"barLabelColor\":\"rgba(0, 0, 0, 0.54)\",\"showBarValue\":true,\"barValueFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"700\",\"lineHeight\":\"12px\"},\"barValueColor\":\"rgba(0, 0, 0, 0.76)\",\"showLegend\":true,\"legendPosition\":\"top\",\"legendLabelFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"16px\"},\"legendLabelColor\":\"rgba(0, 0, 0, 0.76)\",\"showTooltip\":true,\"tooltipValueFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"16px\"},\"tooltipValueColor\":\"rgba(0, 0, 0, 0.76)\",\"tooltipShowDate\":true,\"tooltipDateFormat\":{\"format\":\"MMMM y\",\"lastUpdateAgo\":false,\"custom\":true},\"tooltipDateFont\":{\"family\":\"Roboto\",\"size\":11,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"16px\"},\"tooltipDateColor\":\"rgba(0, 0, 0, 0.76)\",\"tooltipBackgroundColor\":\"rgba(255, 255, 255, 0.76)\",\"tooltipBackgroundBlur\":4,\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Bar chart with labels\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":false,\"titleIcon\":\"public\",\"iconColor\":\"#1F6BDD\",\"useDashboardTimewindow\":false,\"displayTimewindow\":true,\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"units\":\"%\",\"decimals\":0,\"noDataDisplayMessage\":\"\",\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":null,\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true},\"margin\":\"0px\",\"borderRadius\":\"0px\",\"iconSize\":\"24px\"}"
"defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Humidity\",\"color\":\"rgb(125, 142, 255)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < 50) {\\n\\tvalue = 50;\\n} else if (value > 80) {\\n\\tvalue = 80;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Soil moisture\",\"color\":\"rgb(249, 111, 255)\",\"settings\":{},\"_hash\":0.9111685461089025,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < 30) {\\n\\tvalue = 30;\\n} else if (value > 90) {\\n\\tvalue = 90;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Leaf wetness\",\"color\":\"rgb(255, 163, 137)\",\"settings\":{},\"_hash\":0.8487533373085416,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < 40) {\\n\\tvalue = 40;\\n} else if (value > 70) {\\n\\tvalue = 70;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Cloud cover\",\"color\":\"#FFED53\",\"settings\":{},\"_hash\":0.7690144858984289,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < 20) {\\n\\tvalue = 20;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"hideInterval\":false,\"hideLastInterval\":false,\"hideQuickInterval\":false,\"hideAggregation\":false,\"hideAggInterval\":false,\"hideTimezone\":false,\"selectedTab\":1,\"history\":{\"historyType\":2,\"timewindowMs\":60000,\"interval\":\"MONTH\",\"fixedTimewindow\":{\"startTimeMs\":1704293713163,\"endTimeMs\":1704380113163},\"quickInterval\":\"CURRENT_HALF_YEAR\"},\"aggregation\":{\"type\":\"AVG\",\"limit\":25000},\"timezone\":null},\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"showBarLabel\":true,\"barLabelFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"12px\"},\"barLabelColor\":\"rgba(0, 0, 0, 0.54)\",\"showBarValue\":true,\"barValueFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"700\",\"lineHeight\":\"12px\"},\"barValueColor\":\"rgba(0, 0, 0, 0.76)\",\"showLegend\":true,\"legendPosition\":\"top\",\"legendLabelFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"16px\"},\"legendLabelColor\":\"rgba(0, 0, 0, 0.76)\",\"showTooltip\":true,\"tooltipValueFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"16px\"},\"tooltipValueColor\":\"rgba(0, 0, 0, 0.76)\",\"tooltipShowDate\":true,\"tooltipDateFormat\":{\"format\":\"MMMM y\",\"lastUpdateAgo\":false,\"custom\":true},\"tooltipDateFont\":{\"family\":\"Roboto\",\"size\":11,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"16px\"},\"tooltipDateColor\":\"rgba(0, 0, 0, 0.76)\",\"tooltipBackgroundColor\":\"rgba(255, 255, 255, 0.76)\",\"tooltipBackgroundBlur\":4,\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Bar chart with labels\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":false,\"titleIcon\":\"public\",\"iconColor\":\"#1F6BDD\",\"useDashboardTimewindow\":false,\"displayTimewindow\":true,\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"units\":\"%\",\"decimals\":0,\"noDataDisplayMessage\":\"\",\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":null,\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true},\"margin\":\"0px\",\"borderRadius\":\"0px\",\"iconSize\":\"24px\"}"
},
"tags": [
"bar chart",

6
application/src/main/java/org/thingsboard/server/ThingsboardServerApplication.java

@ -21,7 +21,6 @@ import org.springframework.context.annotation.ComponentScan;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.annotation.EnableScheduling;
import java.time.ZoneId;
import java.util.Arrays;
@SpringBootConfiguration
@ -34,10 +33,7 @@ public class ThingsboardServerApplication {
private static final String DEFAULT_SPRING_CONFIG_PARAM = SPRING_CONFIG_NAME_KEY + "=" + "thingsboard";
public static void main(String[] args) {
ZoneId.getAvailableZoneIds().stream().sorted().forEach(System.out::println);
// SpringApplication.run(ThingsboardServerApplication.class, updateArguments(args));
SpringApplication.run(ThingsboardServerApplication.class, updateArguments(args));
}
private static String[] updateArguments(String[] args) {

1
ui-ngx/package.json

@ -97,6 +97,7 @@
"rxjs": "~7.8.0",
"schema-inspector": "^2.0.2",
"screenfull": "^6.0.2",
"sorted-btree": "^1.8.1",
"split.js": "^1.6.5",
"systemjs": "6.14.1",
"tinycolor2": "^1.6.0",

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

@ -19,36 +19,40 @@ import {
IndexedSubscriptionData,
} from '@app/shared/models/telemetry/telemetry.models';
import {
AggregationType,
AggregationType, calculateAggInterval,
calculateIntervalComparisonEndTime,
calculateIntervalEndTime,
calculateIntervalStartEndTime,
getCurrentTime,
getTime,
getTime, IntervalMath,
SubscriptionTimewindow
} from '@shared/models/time/time.models';
import { UtilsService } from '@core/services/utils.service';
import { deepClone, isDefinedAndNotNull, isNumber, isNumeric } from '@core/utils';
import Timeout = NodeJS.Timeout;
import { DataEntry, DataSet, IndexedData } from '@shared/models/widget.models';
import BTree from 'sorted-btree';
export declare type onAggregatedData = (data: IndexedSubscriptionData, detectChanges: boolean) => void;
export declare type onAggregatedData = (data: IndexedData, detectChanges: boolean) => void;
interface AggData {
count: number;
sum: number;
aggValue: any;
ts: number;
interval: [number, number];
}
class AggDataMap {
rangeChanged = false;
private minTs = Number.MAX_SAFE_INTEGER;
private map = new Map<number, AggData>();
private map = new BTree<number, AggData>();
private reusePair: [number, AggData] = [undefined, undefined];
constructor(
private subsTw: SubscriptionTimewindow,
private endTs: number
){};
set(ts: number, data: AggData) {
if (ts < this.minTs) {
this.rangeChanged = true;
this.minTs = ts;
}
this.map.set(ts, data);
}
@ -60,7 +64,38 @@ class AggDataMap {
this.map.delete(ts);
}
forEach(callback: (value: AggData, key: number, map: Map<number, AggData>) => void, thisArg?: any) {
findDataForTs(ts: number): AggData | undefined {
if (ts >= this.endTs) {
this.updateLastInterval(ts + 1);
}
const pair = this.map.getPairOrNextLower(ts, this.reusePair);
if (pair) {
const data = pair[1];
const interval = data.interval;
if (ts < interval[1]) {
return data;
}
}
}
calculateAggInterval(timestamp: number): [number, number] {
return calculateAggInterval(this.subsTw, this.endTs, timestamp);
}
updateLastInterval(endTs: number) {
if (endTs > this.endTs) {
this.endTs = endTs;
const lastTs = this.map.maxKey();
if (lastTs) {
const data = this.map.get(lastTs);
const interval = calculateAggInterval(this.subsTw, endTs, data.ts);
data.interval = interval;
data.ts = interval[0] + Math.floor((interval[1] - interval[0]) / 2);
}
}
}
forEach(callback: (value: AggData, key: number, map: BTree<number, AggData>) => void, thisArg?: any) {
this.map.forEach(callback, thisArg);
}
@ -71,24 +106,6 @@ class AggDataMap {
class AggregationMap {
aggMap: {[id: number]: AggDataMap} = {};
detectRangeChanged(): boolean {
let changed = false;
for (const id of Object.keys(this.aggMap)) {
const aggDataMap = this.aggMap[id];
if (aggDataMap.rangeChanged) {
changed = true;
aggDataMap.rangeChanged = false;
}
}
return changed;
}
clearRangeChangedFlags() {
for (const id of Object.keys(this.aggMap)) {
this.aggMap[id].rangeChanged = false;
}
}
}
declare type AggFunction = (aggData: AggData, value?: any) => void;
@ -156,9 +173,9 @@ export class DataAggregator {
}
}
private dataBuffer: IndexedSubscriptionData = [];
private data: IndexedSubscriptionData;
private readonly lastPrevKvPairData: {[id: number]: [number, any]};
private dataBuffer: IndexedData = [];
private data: IndexedData;
private readonly lastPrevKvPairData: {[id: number]: DataEntry};
private aggregationMap: AggregationMap;
@ -166,7 +183,7 @@ export class DataAggregator {
private resetPending = false;
private updatedData = false;
private aggregationTimeout = this.isLatestDataAgg ? 1000 : Math.max(this.subsTw.aggregation.interval, 1000);
private aggregationTimeout = this.isLatestDataAgg ? 1000 : Math.max(IntervalMath.numberValue(this.subsTw.aggregation.interval), 1000);
private intervalTimeoutHandle: Timeout;
private intervalScheduledTime: number;
@ -216,7 +233,7 @@ export class DataAggregator {
this.intervalScheduledTime = this.utils.currentPerfTime();
this.calculateStartEndTs();
this.elapsed = 0;
this.aggregationTimeout = this.isLatestDataAgg ? 1000 : Math.max(this.subsTw.aggregation.interval, 1000);
this.aggregationTimeout = this.isLatestDataAgg ? 1000 : Math.max(IntervalMath.numberValue(this.subsTw.aggregation.interval), 1000);
this.resetPending = true;
this.updatedData = false;
this.intervalTimeoutHandle = setTimeout(this.onInterval.bind(this), Math.min(this.aggregationTimeout, MAX_INTERVAL_TIMEOUT));
@ -252,7 +269,6 @@ export class DataAggregator {
if (updateIntervalScheduledTime) {
this.intervalScheduledTime = this.utils.currentPerfTime();
}
this.aggregationMap.clearRangeChangedFlags();
this.onInterval(history, detectChanges);
} else {
this.updateAggregatedData(data);
@ -260,9 +276,7 @@ export class DataAggregator {
this.intervalScheduledTime = this.utils.currentPerfTime();
this.onInterval(history, detectChanges);
} else {
if (this.aggregationMap.detectRangeChanged()) {
this.onInterval(false, detectChanges, true);
}
this.onInterval(false, detectChanges, true);
}
}
}
@ -283,7 +297,7 @@ export class DataAggregator {
}
}
private onInterval(history?: boolean, detectChanges?: boolean, rangeChanged?: boolean) {
private onInterval(history?: boolean, detectChanges?: boolean, dataChanged?: boolean) {
const now = this.utils.currentPerfTime();
this.elapsed += now - this.intervalScheduledTime;
this.intervalScheduledTime = now;
@ -291,10 +305,10 @@ export class DataAggregator {
clearTimeout(this.intervalTimeoutHandle);
this.intervalTimeoutHandle = null;
}
const intervalTimeout = rangeChanged ? this.aggregationTimeout - this.elapsed : this.aggregationTimeout;
const intervalTimeout = dataChanged ? this.aggregationTimeout - this.elapsed : this.aggregationTimeout;
if (!history) {
const delta = Math.floor(this.elapsed / this.aggregationTimeout);
if (delta || !this.data || rangeChanged) {
if (delta || !this.data || dataChanged) {
const tickTs = delta * this.aggregationTimeout;
if (this.subsTw.quickInterval) {
const startEndTime = calculateIntervalStartEndTime(this.subsTw.quickInterval, this.subsTw.timezone);
@ -304,6 +318,7 @@ export class DataAggregator {
this.startTs += tickTs;
this.endTs += tickTs;
}
this.updateLastInterval();
this.data = this.updateData();
this.elapsed = this.elapsed - delta * this.aggregationTimeout;
}
@ -319,7 +334,7 @@ export class DataAggregator {
}
}
private updateData(): IndexedSubscriptionData {
private updateData(): IndexedData {
this.dataBuffer = [];
this.tsKeys.forEach((key) => {
if (!this.dataBuffer[key.id]) {
@ -332,19 +347,21 @@ export class DataAggregator {
const aggKey = this.aggKeyById(id);
const noAggregation = aggKey.agg === AggregationType.NONE;
let keyData = this.dataBuffer[id];
aggKeyData.forEach((aggData, aggTimestamp) => {
if (aggTimestamp < this.startTs) {
const deletedKeys: number[] = [];
aggKeyData.forEach((aggData, aggStartTs) => {
if (aggStartTs < this.startTs) {
if (this.subsTw.aggregation.stateData &&
(!this.lastPrevKvPairData[id] || this.lastPrevKvPairData[id][0] < aggTimestamp)) {
this.lastPrevKvPairData[id] = [aggTimestamp, aggData.aggValue];
(!this.lastPrevKvPairData[id] || this.lastPrevKvPairData[id][0] < aggData.ts)) {
this.lastPrevKvPairData[id] = [aggData.ts, aggData.aggValue, aggData.interval];
}
aggKeyData.delete(aggTimestamp);
deletedKeys.push(aggStartTs);
this.updatedData = true;
} else if (aggTimestamp < this.endTs || noAggregation) {
const kvPair: [number, any] = [aggTimestamp, aggData.aggValue];
} else if (aggData.ts < this.endTs || noAggregation) {
const kvPair: DataEntry = [aggData.ts, aggData.aggValue, aggData.interval];
keyData.push(kvPair);
}
});
deletedKeys.forEach(ts => aggKeyData.delete(ts));
keyData.sort((set1, set2) => set1[0] - set2[0]);
if (this.subsTw.aggregation.stateData) {
this.updateStateBounds(keyData, deepClone(this.lastPrevKvPairData[id]));
@ -357,11 +374,11 @@ export class DataAggregator {
return this.dataBuffer;
}
private updateStateBounds(keyData: [number, any, number?][], lastPrevKvPair: [number, any]) {
private updateStateBounds(keyData: DataSet, lastPrevKvPair: DataEntry) {
if (lastPrevKvPair) {
lastPrevKvPair[0] = this.startTs;
}
let firstKvPair;
let firstKvPair: DataEntry;
if (!keyData.length) {
if (lastPrevKvPair) {
firstKvPair = lastPrevKvPair;
@ -395,20 +412,26 @@ export class DataAggregator {
const noAggregation = aggType === AggregationType.NONE;
let aggKeyData = aggregationMap.aggMap[id];
if (!aggKeyData) {
aggKeyData = new AggDataMap();
aggKeyData = new AggDataMap(this.subsTw, this.endTs);
aggregationMap.aggMap[id] = aggKeyData;
}
const keyData = data[id];
keyData.forEach((kvPair) => {
const timestamp = kvPair[0];
const value = DataAggregator.convertValue(kvPair[1], noAggregation);
const tsKey = timestamp;
const aggData = {
let interval: [number, number] = [timestamp, timestamp];
if (!noAggregation) {
interval = aggKeyData.calculateAggInterval(timestamp);
}
const ts = interval[0] + Math.floor((interval[1] - interval[0]) / 2);
const aggData: AggData = {
count: isCount ? value : isDefinedAndNotNull(kvPair[2]) ? kvPair[2] : 1,
sum: value,
aggValue: value
aggValue: value,
ts,
interval
};
aggKeyData.set(tsKey, aggData);
aggKeyData.set(interval[0], aggData);
});
}
return aggregationMap;
@ -423,24 +446,28 @@ export class DataAggregator {
const noAggregation = aggType === AggregationType.NONE;
let aggKeyData = this.aggregationMap.aggMap[id];
if (!aggKeyData) {
aggKeyData = new AggDataMap();
aggKeyData = new AggDataMap(this.subsTw, this.endTs);
this.aggregationMap.aggMap[id] = aggKeyData;
}
const keyData = data[id];
keyData.forEach((kvPair) => {
const timestamp = kvPair[0];
const value = DataAggregator.convertValue(kvPair[1], noAggregation);
const aggTimestamp = noAggregation ? timestamp : (this.startTs +
Math.floor((timestamp - this.startTs) / this.subsTw.aggregation.interval) *
this.subsTw.aggregation.interval + this.subsTw.aggregation.interval / 2);
let aggData = aggKeyData.get(aggTimestamp);
let aggData = aggKeyData.findDataForTs(timestamp);
if (!aggData) {
let interval: [number, number] = [timestamp, timestamp];
if (!noAggregation) {
interval = aggKeyData.calculateAggInterval(timestamp);
}
const ts = interval[0] + Math.floor((interval[1] - interval[0]) / 2);
aggData = {
count: isDefinedAndNotNull(kvPair[2]) ? kvPair[2] : 1,
sum: value,
aggValue: isCount ? 1 : value
aggValue: isCount ? 1 : value,
ts,
interval
};
aggKeyData.set(aggTimestamp, aggData);
aggKeyData.set(interval[0], aggData);
} else {
DataAggregator.getAggFunction(aggType)(aggData, value);
}
@ -448,6 +475,14 @@ export class DataAggregator {
}
}
private updateLastInterval() {
for (const idStr of Object.keys(this.aggregationMap.aggMap)) {
const id = Number(idStr);
const aggKeyData = this.aggregationMap.aggMap[id];
aggKeyData.updateLastInterval(this.endTs);
}
}
private aggKeyById(id: number): AggKey {
return this.tsKeys.find(key => key.id === id);
}

96
ui-ngx/src/app/core/api/entity-data-subscription.ts

@ -14,12 +14,19 @@
/// limitations under the License.
///
import { ComparisonResultType, DataSet, DataSetHolder, DatasourceType, widgetType } from '@shared/models/widget.models';
import {
ComparisonResultType,
DataEntry,
DataSet,
DataSetHolder,
DatasourceType, IndexedData,
widgetType
} from '@shared/models/widget.models';
import {
AggregationType,
ComparisonDuration,
createTimewindowForComparison,
getCurrentTime,
getCurrentTime, IntervalMath,
SubscriptionTimewindow
} from '@shared/models/time/time.models';
import {
@ -42,15 +49,16 @@ import {
EntityCountCmd,
EntityDataCmd,
IndexedSubscriptionData,
IntervalType,
NOT_SUPPORTED,
SubscriptionData,
SubscriptionData, SubscriptionDataEntry,
TelemetrySubscriber
} from '@shared/models/telemetry/telemetry.models';
import { UtilsService } from '@core/services/utils.service';
import { EntityDataListener, EntityDataLoadResult } from '@core/api/entity-data.service';
import { deepClone, isDefined, isDefinedAndNotNull, isNumeric, isObject, objectHashCode } from '@core/utils';
import { PageData } from '@shared/models/page/page-data';
import { DataAggregator } from '@core/api/data-aggregator';
import { DataAggregator, onAggregatedData } from '@core/api/data-aggregator';
import { NULL_UUID } from '@shared/models/id/has-uuid';
import { EntityType } from '@shared/models/entity-type.models';
import { Observable, of, ReplaySubject, Subject } from 'rxjs';
@ -154,7 +162,7 @@ export class EntityDataSubscription {
return val;
}
private static calculateComparisonValue(key: SubscriptionDataKey, comparisonTsValue: ComparisonTsValue): [number, any, number?][] {
private static calculateComparisonValue(key: SubscriptionDataKey, comparisonTsValue: ComparisonTsValue): DataSet {
let timestamp: number;
let value: any;
switch (key.comparisonResultType) {
@ -385,7 +393,13 @@ export class EntityDataSubscription {
if (this.entityDataSubscriptionOptions.type === widgetType.timeseries && this.tsFields.length) {
targetCommand.tsCmd.startTs = this.subsTw.startTs;
targetCommand.tsCmd.timeWindow = this.subsTw.aggregation.timeWindow;
targetCommand.tsCmd.interval = this.subsTw.aggregation.interval;
if (typeof this.subsTw.aggregation.interval === 'number') {
targetCommand.tsCmd.interval = this.subsTw.aggregation.interval;
targetCommand.tsCmd.intervalType = IntervalType.MILLISECONDS;
} else {
targetCommand.tsCmd.intervalType = this.subsTw.aggregation.interval;
}
targetCommand.tsCmd.timeZoneId = this.subsTw.timezone;
targetCommand.tsCmd.limit = this.subsTw.aggregation.limit;
targetCommand.tsCmd.agg = this.subsTw.aggregation.type;
targetCommand.tsCmd.fetchLatestPreviousPoint = this.subsTw.aggregation.stateData;
@ -659,21 +673,35 @@ export class EntityDataSubscription {
keys: this.tsFields.map(key => key.key),
startTs: this.subsTw.fixedWindow.startTimeMs,
endTs: this.subsTw.fixedWindow.endTimeMs,
interval: this.subsTw.aggregation.interval,
interval: 0,
intervalType: IntervalType.MILLISECONDS,
limit: this.subsTw.aggregation.limit,
timeZoneId: this.subsTw.timezone,
agg: this.subsTw.aggregation.type,
fetchLatestPreviousPoint: this.subsTw.aggregation.stateData
};
if (typeof this.subsTw.aggregation.interval === 'number') {
cmd.historyCmd.interval = this.subsTw.aggregation.interval;
} else {
cmd.historyCmd.intervalType = this.subsTw.aggregation.interval;
}
} else {
cmd.tsCmd = {
keys: this.tsFields.map(key => key.key),
startTs: this.subsTw.startTs,
timeWindow: this.subsTw.aggregation.timeWindow,
interval: this.subsTw.aggregation.interval,
interval: 0,
intervalType: IntervalType.MILLISECONDS,
limit: this.subsTw.aggregation.limit,
timeZoneId: this.subsTw.timezone,
agg: this.subsTw.aggregation.type,
fetchLatestPreviousPoint: this.subsTw.aggregation.stateData
};
if (typeof this.subsTw.aggregation.interval === 'number') {
cmd.tsCmd.interval = this.subsTw.aggregation.interval;
} else {
cmd.tsCmd.intervalType = this.subsTw.aggregation.interval;
}
}
}
latestValuesKeys = this.latestValues;
@ -717,7 +745,8 @@ export class EntityDataSubscription {
this.frequency = 1000;
this.latestFrequency = 1000;
if (this.entityDataSubscriptionOptions.type === widgetType.timeseries) {
this.frequency = Math.min(this.entityDataSubscriptionOptions.subscriptionTimewindow.aggregation.interval, 5000);
this.frequency =
Math.min(IntervalMath.numberValue(this.entityDataSubscriptionOptions.subscriptionTimewindow.aggregation.interval), 5000);
}
this.tickScheduledTime = this.utils.currentPerfTime();
this.generateData(true);
@ -785,9 +814,9 @@ export class EntityDataSubscription {
}
private reportNotSupported(keys: AggKey[], isUpdate: boolean) {
const indexedData: IndexedSubscriptionData = [];
const indexedData: IndexedData = [];
for (const key of keys) {
indexedData[key.id] = [[0, NOT_SUPPORTED]];
indexedData[key.id] = [[0, NOT_SUPPORTED, [0,0]]];
}
for (let dataIndex = 0; dataIndex < this.pageData.data.length; dataIndex++) {
this.onIndexedData(indexedData, dataIndex, true,
@ -942,7 +971,7 @@ export class EntityDataSubscription {
}
if (Object.keys(aggData).length > 0 && this.tsLatestDataAggregators && this.tsLatestDataAggregators[dataIndex]) {
const dataAggregator = this.tsLatestDataAggregators[dataIndex];
let prevDataCb;
let prevDataCb: onAggregatedData;
if (!isUpdate) {
prevDataCb = dataAggregator.updateOnDataCb((data, detectChanges) => {
this.onIndexedData(data, dataIndex, detectChanges,
@ -996,7 +1025,7 @@ export class EntityDataSubscription {
for (const dataKey of dataKeys) {
indexedData[dataKey.index] = subscriptionData[dataKey.name];
}
let prevDataCb;
let prevDataCb: onAggregatedData;
if (!isUpdate) {
prevDataCb = dataAggregator.updateOnDataCb((data, detectChanges) => {
this.onIndexedData(data, dataIndex, detectChanges, false, dataUpdatedCb);
@ -1016,12 +1045,12 @@ export class EntityDataSubscription {
isTsLatest: boolean, dataUpdatedCb: DataUpdatedCb) {
for (const key of Object.keys(sourceData)) {
const keyData = sourceData[key];
this.onKeyData(keyData, key, 0, type,
this.onKeyData(keyData.map(entry => [entry[0], entry[1], [entry[0], entry[0]]]), key, 0, type,
dataIndex, detectChanges, isTsLatest, false, dataUpdatedCb);
}
}
private onIndexedData(sourceData: IndexedSubscriptionData, dataIndex: number, detectChanges: boolean,
private onIndexedData(sourceData: IndexedData, dataIndex: number, detectChanges: boolean,
isTsLatest: boolean, dataUpdatedCb: DataUpdatedCb) {
for (const indexStr of Object.keys(sourceData)) {
const id = Number(indexStr);
@ -1037,7 +1066,7 @@ export class EntityDataSubscription {
}
}
private onKeyData(keyData: [number, any, number?][], keyName: string, id: number, type: DataKeyType,
private onKeyData(keyData: DataSet, keyName: string, id: number, type: DataKeyType,
dataIndex: number, detectChanges: boolean,
isTsLatest: boolean, isAggLatest: boolean, dataUpdatedCb: DataUpdatedCb) {
const keyIdSuffix = isAggLatest ? `_${id}` : '';
@ -1048,8 +1077,8 @@ export class EntityDataSubscription {
if (this.datasourceData[dataIndex][datasourceKey].data) {
const dataKey = dataKeyList[keyIndex];
const data: DataSet = [];
let prevSeries: [number, any];
let prevOrigSeries: [number, any];
let prevSeries: DataEntry;
let prevOrigSeries: DataEntry;
let datasourceKeyData: DataSet;
let datasourceOrigKeyData: DataSet;
let update = false;
@ -1064,36 +1093,36 @@ export class EntityDataSubscription {
prevSeries = datasourceKeyData[datasourceKeyData.length - 1];
prevOrigSeries = datasourceOrigKeyData[datasourceOrigKeyData.length - 1];
} else {
prevSeries = [0, 0];
prevOrigSeries = [0, 0];
prevSeries = [0, 0, [0, 0]];
prevOrigSeries = [0, 0, [0, 0]];
}
this.datasourceOrigData[dataIndex][datasourceKey].data = [];
if (this.entityDataSubscriptionOptions.type === widgetType.timeseries && !isTsLatest) {
keyData.forEach((keySeries) => {
let series = keySeries;
const time = series[0];
this.datasourceOrigData[dataIndex][datasourceKey].data.push([series[0], series[1]]);
this.datasourceOrigData[dataIndex][datasourceKey].data.push([series[0], series[1], series[2]]);
let value = EntityDataSubscription.convertValue(series[1]);
if (dataKey.postFunc) {
value = dataKey.postFunc(time, value, prevSeries[1], prevOrigSeries[0], prevOrigSeries[1]);
}
prevOrigSeries = [series[0], series[1]];
series = [series[0], value];
data.push([series[0], series[1]]);
prevSeries = [series[0], series[1]];
prevOrigSeries = [series[0], series[1], series[2]];
series = [series[0], value, series[2]];
data.push([series[0], series[1], series[2]]);
prevSeries = [series[0], series[1], series[2]];
});
update = true;
} else if (this.entityDataSubscriptionOptions.type === widgetType.latest || isTsLatest) {
if (keyData.length > 0) {
let series = keyData[0];
const time = series[0];
this.datasourceOrigData[dataIndex][datasourceKey].data.push([series[0], series[1]]);
this.datasourceOrigData[dataIndex][datasourceKey].data.push([series[0], series[1], series[2]]);
let value = EntityDataSubscription.convertValue(series[1]);
if (dataKey.postFunc) {
value = dataKey.postFunc(time, value, prevSeries[1], prevOrigSeries[0], prevOrigSeries[1]);
}
series = [time, value];
data.push([series[0], series[1]]);
series = [time, value, series[2]];
data.push([series[0], series[1], series[2]]);
}
update = true;
}
@ -1155,13 +1184,14 @@ export class EntityDataSubscription {
return result;
}
private generateSeries(dataKey: SubscriptionDataKey, startTime: number, endTime: number): [number, any][] {
const data: [number, any][] = [];
let prevSeries: [number, any];
private generateSeries(dataKey: SubscriptionDataKey, startTime: number, endTime: number): SubscriptionDataEntry[] {
const data: SubscriptionDataEntry[] = [];
let prevSeries: SubscriptionDataEntry;
const datasourceDataKey = `${dataKey.key}_${dataKey.listIndex}`;
const datasourceKeyData = this.datasourceData[0][datasourceDataKey].data;
if (datasourceKeyData.length > 0) {
prevSeries = datasourceKeyData[datasourceKeyData.length - 1];
const prevDataEntry = datasourceKeyData[datasourceKeyData.length - 1];
prevSeries = [prevDataEntry[0], prevDataEntry[1]];
} else {
prevSeries = [0, 0];
}
@ -1178,7 +1208,7 @@ export class EntityDataSubscription {
}
private generateLatest(dataKey: SubscriptionDataKey, detectChanges: boolean) {
let prevSeries: [number, any];
let prevSeries: DataEntry;
const datasourceKey = dataKey.latest ? `${dataKey.key}_${dataKey.listIndex}` : dataKey.key;
const datasourceKeyData = this.datasourceData[0][datasourceKey].data;
if (datasourceKeyData.length > 0) {

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

@ -1253,14 +1253,19 @@ export class WidgetSubscription implements IWidgetSubscription {
private updateTimewindow() {
this.timeWindow.interval = this.subscriptionTimewindow.aggregation.interval || 1000;
this.timeWindow.timezone = this.subscriptionTimewindow.timezone;
this.timeWindow.tsOffset = this.subscriptionTimewindow.tsOffset;
if (this.subscriptionTimewindow.realtimeWindowMs) {
if (this.subscriptionTimewindow.quickInterval) {
const startEndTime = calculateIntervalStartEndTime(this.subscriptionTimewindow.quickInterval, this.subscriptionTimewindow.timezone);
this.timeWindow.maxTime = startEndTime[1] + this.subscriptionTimewindow.tsOffset;
this.timeWindow.minTime = startEndTime[0] + this.subscriptionTimewindow.tsOffset;
} else {
this.timeWindow.maxTime = moment().valueOf() + this.subscriptionTimewindow.tsOffset + this.timeWindow.stDiff;
this.timeWindow.minTime = this.timeWindow.maxTime - this.subscriptionTimewindow.realtimeWindowMs;
const now = moment().valueOf() + this.subscriptionTimewindow.tsOffset + this.timeWindow.stDiff;
if (!this.timeWindow.maxTime || Math.abs(now - this.timeWindow.maxTime) > 500) {
this.timeWindow.maxTime = now;
this.timeWindow.maxTime -= this.timeWindow.maxTime % 1000;
this.timeWindow.minTime = this.timeWindow.maxTime - this.subscriptionTimewindow.realtimeWindowMs;
}
}
} else if (this.subscriptionTimewindow.fixedWindow) {
this.timeWindow.maxTime = this.subscriptionTimewindow.fixedWindow.endTimeMs + this.subscriptionTimewindow.tsOffset;

67
ui-ngx/src/app/core/services/time.service.ts

@ -19,22 +19,14 @@ import {
AggregationType,
DAY,
defaultTimeIntervals,
defaultTimewindow,
defaultTimewindow, Interval, IntervalMath,
SECOND,
TimeInterval,
Timewindow
} from '@shared/models/time/time.models';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { defaultHttpOptions } from '@core/http/http-utils';
import { map } from 'rxjs/operators';
import { isDefined } from '@core/utils';
export interface TimeInterval {
name: string;
translateParams: { [key: string]: any };
value: number;
}
const MIN_INTERVAL = SECOND;
const MAX_INTERVAL = 365 * 20 * DAY;
@ -60,15 +52,16 @@ export class TimeService {
}
}
public matchesExistingInterval(min: number, max: number, intervalMs: number): boolean {
const intervals = this.getIntervals(min, max);
return intervals.findIndex(interval => interval.value === intervalMs) > -1;
public matchesExistingInterval(min: number, max: number, interval: Interval, useCalendarIntervals = false): boolean {
const intervals = this.getIntervals(min, max, useCalendarIntervals);
return intervals.findIndex(timeInterval => timeInterval.value === interval) > -1;
}
public getIntervals(min: number, max: number): Array<TimeInterval> {
public getIntervals(min: number, max: number, useCalendarIntervals = false): Array<TimeInterval> {
min = this.boundMinInterval(min);
max = this.boundMaxInterval(max);
return defaultTimeIntervals.filter((interval) => interval.value >= min && interval.value <= max);
return defaultTimeIntervals.filter((interval) => (useCalendarIntervals || typeof interval.value === 'number') &&
IntervalMath.numberValue(interval.value) >= min && IntervalMath.numberValue(interval.value) <= max);
}
public boundMinInterval(min: number): number {
@ -85,32 +78,37 @@ export class TimeService {
return this.toBound(max, MIN_INTERVAL, MAX_INTERVAL, MAX_INTERVAL);
}
public boundToPredefinedInterval(min: number, max: number, intervalMs: number): number {
const intervals = this.getIntervals(min, max);
public boundToPredefinedInterval(min: number, max: number, interval: Interval, useCalendarIntervals = false): Interval {
const intervals = this.getIntervals(min, max, useCalendarIntervals);
let minDelta = MAX_INTERVAL;
const boundedInterval = intervalMs || min;
const boundedInterval = interval || min;
if (!intervals.length) {
return boundedInterval;
}
let matchedInterval: TimeInterval = intervals[0];
intervals.forEach((interval) => {
const delta = Math.abs(interval.value - boundedInterval);
if (delta < minDelta) {
matchedInterval = interval;
minDelta = delta;
}
});
return matchedInterval.value;
const found = intervals.find(timeInterval => timeInterval.value === boundedInterval);
if (found) {
return found.value;
} else {
let matchedInterval: TimeInterval = intervals[0];
intervals.forEach((timeInterval) => {
const delta = Math.abs(IntervalMath.numberValue(timeInterval.value) - IntervalMath.numberValue(boundedInterval));
if (delta <= minDelta) {
matchedInterval = timeInterval;
minDelta = delta;
}
});
return matchedInterval.value;
}
}
public boundIntervalToTimewindow(timewindow: number, intervalMs: number, aggType: AggregationType): number {
public boundIntervalToTimewindow(timewindow: number, interval: Interval, aggType: AggregationType): Interval {
if (aggType === AggregationType.NONE) {
return SECOND;
} else {
const min = this.minIntervalLimit(timewindow);
const max = this.maxIntervalLimit(timewindow);
if (intervalMs) {
return this.toBound(intervalMs, min, max, intervalMs);
if (interval) {
return this.toIntervalBound(interval, min, max, interval);
} else {
return this.boundToPredefinedInterval(min, max, this.avgInterval(timewindow));
}
@ -154,4 +152,13 @@ export class TimeService {
}
}
private toIntervalBound(value: Interval, min: number, max: number, defValue: Interval): Interval {
if (isDefined(value)) {
value = IntervalMath.max(value, min);
value = IntervalMath.min(value, max);
return value;
} else {
return defValue;
}
}
}

4
ui-ngx/src/app/modules/home/components/widget/lib/cards/aggregated-value-card.models.ts

@ -27,7 +27,7 @@ import {
lastUpdateAgoDateFormat,
textStyle
} from '@shared/models/widget-settings.models';
import { ComparisonResultType, DataKey, DatasourceData } from '@shared/models/widget.models';
import { ComparisonResultType, DataEntry, DataKey, DatasourceData } from '@shared/models/widget.models';
import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
import { AggregationType } from '@shared/models/time/time.models';
@ -102,7 +102,7 @@ export const computeAggregatedCardValue =
}
};
export const getTsValueByLatestDataKey = (latestData: Array<DatasourceData>, dataKey: DataKey): [number, any] => {
export const getTsValueByLatestDataKey = (latestData: Array<DatasourceData>, dataKey: DataKey): DataEntry => {
if (latestData?.length) {
const dsData = latestData.find(data => data.dataKey === dataKey);
if (dsData?.data?.length) {

51
ui-ngx/src/app/modules/home/components/widget/lib/chart/bar-chart-with-labels-widget.component.ts

@ -58,6 +58,7 @@ import {
NamedDataSet,
toNamedData
} from '@home/components/widget/lib/chart/echarts-widget.models';
import { IntervalMath } from '@shared/models/time/time.models';
interface BarChartDataItem {
id: string;
@ -187,18 +188,23 @@ export class BarChartWithLabelsWidgetComponent implements OnInit, OnDestroy, Aft
this.barRenderItem = (params, api) => {
const interval = this.ctx.defaultSubscription.timeWindow.interval;
const time = api.value(0) as number;
let start = api.value(2) as number;
const end = api.value(3) as number;
let interval = end - start;
if (!start || !end || !interval) {
interval = IntervalMath.numberValue(this.ctx.timeWindow.interval);
start = time - interval / 2;
}
const enabledDataItems = this.dataItems.filter(d => d.enabled);
const barInterval = interval / (enabledDataItems.length + 1);
const intervalGap = barInterval / 2;
const index = enabledDataItems.findIndex(d => d.id === params.seriesId);
const time = api.value(0) as number;
const value = api.value(1);
const start = time - interval / 2;
const startTime = start + intervalGap + barInterval * index;
const delta = barInterval;
const lowerLeft = api.coord([startTime, value]);
const lowerLeft = api.coord([startTime, value >= 0 ? value : 0]);
const height = api.size([delta, value])[1];
const width = api.size([delta, 10])[0];
@ -256,7 +262,7 @@ export class BarChartWithLabelsWidgetComponent implements OnInit, OnDestroy, Aft
this.barLabelLayoutCallback = (params) => {
if (params.rect.width - params.labelRect.width < 2) {
return {
y: '1000%',
y: '100000%',
};
} else {
return {
@ -293,28 +299,13 @@ export class BarChartWithLabelsWidgetComponent implements OnInit, OnDestroy, Aft
}
public onDataUpdated() {
let minTime = this.ctx.defaultSubscription.timeWindow.minTime;
let maxTime = this.ctx.defaultSubscription.timeWindow.maxTime;
let dataMin = Number.MAX_VALUE;
let dataMax = Number.MIN_VALUE;
for (const item of this.dataItems) {
const datasourceData = this.ctx.data ? this.ctx.data.find(d => d.dataKey === item.dataKey) : null;
item.data = datasourceData?.data ? toNamedData(datasourceData.data) : [];
if (datasourceData.data.length) {
dataMin = Math.min(datasourceData.data[0][0], dataMin);
dataMax = Math.max(datasourceData.data[datasourceData.data.length-1][0], dataMax);
}
}
if (dataMin !== Number.MAX_VALUE) {
minTime = dataMin - this.ctx.defaultSubscription.timeWindow.interval / 2;
}
if (dataMax !== Number.MIN_VALUE) {
dataMax = dataMax + this.ctx.defaultSubscription.timeWindow.interval / 2;
maxTime = Math.max(dataMax, maxTime);
}
if (this.barChart) {
(this.barChartOptions.xAxis as any).min = minTime;
(this.barChartOptions.xAxis as any).max = maxTime;
(this.barChartOptions.xAxis as any).min = this.ctx.defaultSubscription.timeWindow.minTime;
(this.barChartOptions.xAxis as any).max = this.ctx.defaultSubscription.timeWindow.maxTime;
(this.barChartOptions.xAxis as any).tbTimewindowInterval = this.ctx.defaultSubscription.timeWindow.interval;
this.barChartOptions.series = this.updateSeries();
this.barChart.setOption(this.barChartOptions);
@ -332,7 +323,15 @@ export class BarChartWithLabelsWidgetComponent implements OnInit, OnDestroy, Aft
color: item.dataKey.color,
data: item.data,
renderItem: this.barRenderItem,
labelLayout: this.barLabelLayoutCallback
labelLayout: this.barLabelLayoutCallback,
dimensions: [
{name: 'intervalStart', type: 'number'},
{name: 'intervalEnd', type: 'number'}
],
encode: {
intervalStart: 2,
intervalEnd: 3
}
};
series.push(seriesOption);
}
@ -407,8 +406,8 @@ export class BarChartWithLabelsWidgetComponent implements OnInit, OnDestroy, Aft
axisLine: {
onZero: false
},
min: this.ctx.defaultSubscription.timeWindow.minTime - this.ctx.defaultSubscription.timeWindow.interval / 2,
max: this.ctx.defaultSubscription.timeWindow.maxTime + this.ctx.defaultSubscription.timeWindow.interval / 2
min: this.ctx.defaultSubscription.timeWindow.minTime,
max: this.ctx.defaultSubscription.timeWindow.maxTime
},
yAxis: {
type: 'value',
@ -425,6 +424,8 @@ export class BarChartWithLabelsWidgetComponent implements OnInit, OnDestroy, Aft
if (this.settings.showTooltip) {
this.barChartOptions.tooltip = {
trigger: 'axis',
confine: true,
appendToBody: true,
axisPointer: {
type: 'shadow'
},

54
ui-ngx/src/app/modules/home/components/widget/lib/chart/echarts-widget.models.ts

@ -17,7 +17,7 @@
import * as echarts from 'echarts/core';
import { Axis } from 'echarts';
import AxisModel from 'echarts/types/src/coord/cartesian/AxisModel';
import { formatValue, isNumber } from '@core/utils';
import { formatValue, isDefinedAndNotNull, isNumber, isString } from '@core/utils';
import TimeScale from 'echarts/types/src/scale/Time';
import {
DataZoomComponent, DataZoomComponentOption,
@ -36,7 +36,8 @@ import {
} from 'echarts/charts';
import { LabelLayout } from 'echarts/features';
import { CanvasRenderer, SVGRenderer } from 'echarts/renderers';
import { DataSet } from '@shared/models/widget.models';
import { DataEntry, DataSet } from '@shared/models/widget.models';
import { Interval, IntervalMath } from '@shared/models/time/time.models';
import { CallbackDataParams } from 'echarts/types/dist/shared';
import { Renderer2 } from '@angular/core';
import { DateFormatProcessor, DateFormatSettings, Font } from '@shared/models/widget-settings.models';
@ -51,13 +52,13 @@ class EChartsModule {
Axis.prototype.getBandWidth = function(){
const model: AxisModel = this.model;
const axisOption = model.option;
const tbTimewindowInterval = (axisOption as any).tbTimewindowInterval;
if (this.scale.type === 'time' && isNumber(tbTimewindowInterval)) {
const tbTimewindowInterval: Interval = (axisOption as any).tbTimewindowInterval;
if (this.scale.type === 'time' && (isNumber(tbTimewindowInterval) || isString(tbTimewindowInterval))) {
const timeScale: TimeScale = this.scale;
const axisExtent: [number, number] = this._extent;
const dataExtent = timeScale.getExtent();
const size = Math.abs(axisExtent[1] - axisExtent[0]);
return tbTimewindowInterval * (size / (dataExtent[1] - dataExtent[0]));
return IntervalMath.numberValue(tbTimewindowInterval) * (size / (dataExtent[1] - dataExtent[0]));
} else {
return axisGetBandWidth.call(this);
}
@ -99,17 +100,31 @@ export type EChartsOption = echarts.ComposeOption<
export type ECharts = echarts.ECharts;
export type NamedDataSet = {name: string; value: [number, any]}[];
export type EChartsDataItem = [number, any, number, number];
export type NamedDataSet = {name: string; value: EChartsDataItem}[];
export const toNamedData = (data: DataSet): NamedDataSet => {
if (!data?.length) {
return [];
} else {
return data.map(d => ({
name: d[0] + '',
value: d
}));
return data.map(d => {
const ts = isDefinedAndNotNull(d[2]) ? d[2][0] : d[0];
return {
name: ts + '',
value: toEChartsDataItem(d)
};
});
}
};
const toEChartsDataItem = (entry: DataEntry): EChartsDataItem => {
const item: EChartsDataItem = [entry[0], entry[1], entry[0], entry[0]];
if (isDefinedAndNotNull(entry[2])) {
item[2] = entry[2][0];
item[3] = entry[2][1];
}
return item;
};
export interface EChartsTooltipWidgetSettings {
@ -141,9 +156,22 @@ export const echartsTooltipFormatter = (renderer: Renderer2,
renderer.setStyle(tooltipElement, 'gap', '4px');
if (settings.tooltipShowDate) {
const dateElement: HTMLElement = renderer.createElement('div');
const ts = params[0].value[0];
tooltipDateFormat.update(ts);
renderer.appendChild(dateElement, renderer.createText(tooltipDateFormat.formatted));
let dateText: string;
const startTs = params[0].value[2];
const endTs = params[0].value[3];
if (startTs && endTs && (endTs - 1) > startTs) {
const startDateText = tooltipDateFormat.update(startTs);
const endDateText = tooltipDateFormat.update(endTs - 1);
if (startDateText === endDateText) {
dateText = startDateText;
} else {
dateText = startDateText + ' - ' + endDateText;
}
} else {
const ts = params[0].value[0];
dateText = tooltipDateFormat.update(ts);
}
renderer.appendChild(dateElement, renderer.createText(dateText));
renderer.setStyle(dateElement, 'font-family', settings.tooltipDateFont.family);
renderer.setStyle(dateElement, 'font-size', settings.tooltipDateFont.size + settings.tooltipDateFont.sizeUnit);
renderer.setStyle(dateElement, 'font-style', settings.tooltipDateFont.style);

3
ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.component.ts

@ -392,6 +392,7 @@ export class RangeChartWidgetComponent implements OnInit, OnDestroy, AfterViewIn
show: false,
type: 'piecewise',
selected: this.selectedRanges,
dimension: 1,
pieces: this.rangeItems.map(item => item.piece),
outOfRange: {
color: this.settings.outOfRangeColor
@ -405,6 +406,8 @@ export class RangeChartWidgetComponent implements OnInit, OnDestroy, AfterViewIn
if (this.settings.showTooltip) {
this.rangeChartOptions.tooltip = {
trigger: 'axis',
confine: true,
appendToBody: true,
formatter: (params: CallbackDataParams[]) => echartsTooltipFormatter(this.renderer, this.tooltipDateFormat,
this.settings, params, this.decimals, this.units, 0),
padding: [8, 12],

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

@ -56,7 +56,7 @@ import {
} from './flot-widget.models';
import * as moment_ from 'moment';
import tinycolor from 'tinycolor2';
import { AggregationType } from '@shared/models/time/time.models';
import { AggregationType, IntervalMath } from '@shared/models/time/time.models';
import { CancelAnimationFrame } from '@core/services/raf.service';
import { UtilsService } from '@core/services/utils.service';
import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
@ -558,7 +558,7 @@ export class TbFlot {
this.subscription.timeWindowConfig.aggregation.type === AggregationType.NONE) {
this.options.series.bars.barWidth = this.defaultBarWidth;
} else {
this.options.series.bars.barWidth = this.subscription.timeWindow.interval * 0.6;
this.options.series.bars.barWidth = IntervalMath.numberValue(this.subscription.timeWindow.interval) * 0.6;
}
}
this.options.xaxes[0].min = this.subscription.timeWindow.minTime;
@ -663,7 +663,7 @@ export class TbFlot {
this.subscription.timeWindowConfig.aggregation.type === AggregationType.NONE) {
this.options.series.bars.barWidth = this.defaultBarWidth;
} else {
this.options.series.bars.barWidth = this.subscription.timeWindow.interval * 0.6;
this.options.series.bars.barWidth = IntervalMath.numberValue(this.subscription.timeWindow.interval) * 0.6;
}
}
@ -681,7 +681,7 @@ export class TbFlot {
this.subscription.timeWindowConfig.aggregation.type === AggregationType.NONE) {
this.plot.getOptions().series.bars.barWidth = this.defaultBarWidth;
} else {
this.plot.getOptions().series.bars.barWidth = this.subscription.timeWindow.interval * 0.6;
this.plot.getOptions().series.bars.barWidth = IntervalMath.numberValue(this.subscription.timeWindow.interval) * 0.6;
}
}
this.updateData();

5
ui-ngx/src/app/modules/home/components/widget/lib/indicator/liquid-level-widget.component.ts

@ -60,6 +60,7 @@ import { TranslateService } from '@ngx-translate/core';
import ITooltipsterInstance = JQueryTooltipster.ITooltipsterInstance;
import { ImagePipe } from '@shared/pipe/image.pipe';
import { DomSanitizer } from '@angular/platform-browser';
import { DataEntry } from '@shared/models/widget.models';
@Component({
selector: 'tb-liquid-level-widget',
@ -399,7 +400,7 @@ export class LiquidLevelWidgetComponent implements OnInit {
return limits.min + (percentage / 100) * (limits.max - limits.min);
}
private updateTooltip(value: [number, any]): void {
private updateTooltip(value: DataEntry): void {
this.tooltipContent = this.getTooltipContent(value);
if (this.tooltip) {
@ -494,7 +495,7 @@ export class LiquidLevelWidgetComponent implements OnInit {
}
}
private getTooltipContent(value?: [number, any]): string {
private getTooltipContent(value?: DataEntry): string {
const contentValue = value || [0, ''];
let tooltipValue: string | number = 'N/A';

2
ui-ngx/src/app/shared/components/time/timeinterval.component.html

@ -43,7 +43,7 @@
<section fxLayout="row" fxFlex [fxShow]="!advanced && (isEdit || !hideFlag)">
<mat-form-field fxFlex [subscriptSizing]="subscriptSizing">
<mat-label *ngIf="predefinedName" translate>{{ predefinedName }}</mat-label>
<mat-select [disabled]="hideFlag || disabled" [(ngModel)]="intervalMs" (ngModelChange)="onIntervalMsChange()" style="min-width: 150px;">
<mat-select [disabled]="hideFlag || disabled" [(ngModel)]="interval" (ngModelChange)="onIntervalChange()" style="min-width: 150px;">
<mat-option *ngFor="let interval of intervals" [value]="interval.value">
{{ interval.name | translate:interval.translateParams }}
</mat-option>

70
ui-ngx/src/app/shared/components/time/timeinterval.component.ts

@ -16,10 +16,11 @@
import { Component, EventEmitter, forwardRef, Input, OnInit, Output } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { TimeInterval, TimeService } from '@core/services/time.service';
import { TimeService } from '@core/services/time.service';
import { coerceNumberProperty } from '@angular/cdk/coercion';
import { SubscriptSizing } from '@angular/material/form-field';
import { coerceBoolean } from '@shared/decorators/coercion';
import { Interval, IntervalMath, TimeInterval } from '@shared/models/time/time.models';
@Component({
selector: 'tb-timeinterval',
@ -72,6 +73,10 @@ export class TimeintervalComponent implements OnInit, ControlValueAccessor {
@coerceBoolean()
disabledAdvanced = false;
@Input()
@coerceBoolean()
useCalendarIntervals = false;
@Output() hideFlagChange = new EventEmitter<boolean>();
@Input() disabled: boolean;
@ -84,8 +89,8 @@ export class TimeintervalComponent implements OnInit, ControlValueAccessor {
mins = 1;
secs = 0;
intervalMs = 0;
modelValue: number;
interval: Interval = 0;
modelValue: Interval;
advanced = false;
rendered = false;
@ -112,26 +117,26 @@ export class TimeintervalComponent implements OnInit, ControlValueAccessor {
this.disabled = isDisabled;
}
writeValue(intervalMs: number): void {
this.modelValue = intervalMs;
writeValue(interval: Interval): void {
this.modelValue = interval;
this.rendered = true;
if (typeof this.modelValue !== 'undefined') {
const min = this.timeService.boundMinInterval(this.minValue);
const max = this.timeService.boundMaxInterval(this.maxValue);
if (this.modelValue >= min && this.modelValue <= max) {
this.advanced = !this.timeService.matchesExistingInterval(this.minValue, this.maxValue, this.modelValue);
this.setIntervalMs(this.modelValue);
if (IntervalMath.numberValue(this.modelValue) >= min && IntervalMath.numberValue(this.modelValue) <= max) {
this.advanced = !this.timeService.matchesExistingInterval(this.minValue, this.maxValue, this.modelValue, this.useCalendarIntervals);
this.setInterval(this.modelValue);
} else {
this.boundInterval();
}
}
}
setIntervalMs(intervalMs: number) {
setInterval(interval: Interval) {
if (!this.advanced) {
this.intervalMs = intervalMs;
this.interval = interval;
}
const intervalSeconds = Math.floor(intervalMs / 1000);
const intervalSeconds = Math.floor(IntervalMath.numberValue(interval) / 1000);
this.days = Math.floor(intervalSeconds / 86400);
this.hours = Math.floor((intervalSeconds % 86400) / 3600);
this.mins = Math.floor(((intervalSeconds % 86400) % 3600) / 60);
@ -141,19 +146,20 @@ export class TimeintervalComponent implements OnInit, ControlValueAccessor {
boundInterval(updateToPreferred = false) {
const min = this.timeService.boundMinInterval(this.minValue);
const max = this.timeService.boundMaxInterval(this.maxValue);
this.intervals = this.timeService.getIntervals(this.minValue, this.maxValue);
this.intervals = this.timeService.getIntervals(this.minValue, this.maxValue, this.useCalendarIntervals);
if (this.rendered) {
let newIntervalMs = this.modelValue;
let newInterval = this.modelValue;
const newIntervalMs = IntervalMath.numberValue(newInterval);
if (newIntervalMs < min) {
newIntervalMs = min;
newInterval = min;
} else if (newIntervalMs >= max && updateToPreferred) {
newIntervalMs = this.timeService.boundMaxInterval(max / 7);
newInterval = this.timeService.boundMaxInterval(max / 7);
}
if (!this.advanced) {
newIntervalMs = this.timeService.boundToPredefinedInterval(min, max, newIntervalMs);
newInterval = this.timeService.boundToPredefinedInterval(min, max, newInterval, this.useCalendarIntervals);
}
if (newIntervalMs !== this.modelValue) {
this.setIntervalMs(newIntervalMs);
if (newInterval !== this.modelValue) {
this.setInterval(newInterval);
this.updateView();
}
}
@ -163,18 +169,18 @@ export class TimeintervalComponent implements OnInit, ControlValueAccessor {
if (!this.rendered) {
return;
}
let value = null;
let intervalMs;
let value: Interval = null;
let interval: Interval;
if (!this.advanced) {
intervalMs = this.intervalMs;
if (!intervalMs || isNaN(intervalMs)) {
intervalMs = this.calculateIntervalMs();
interval = this.interval;
if (!interval || typeof interval === 'number' && isNaN(interval)) {
interval = this.calculateIntervalMs();
}
} else {
intervalMs = this.calculateIntervalMs();
interval = this.calculateIntervalMs();
}
if (!isNaN(intervalMs) && intervalMs > 0) {
value = intervalMs;
if (typeof interval === 'string' || !isNaN(interval) && interval > 0) {
value = interval;
}
this.modelValue = value;
this.propagateChange(this.modelValue);
@ -188,19 +194,19 @@ export class TimeintervalComponent implements OnInit, ControlValueAccessor {
this.secs) * 1000;
}
onIntervalMsChange() {
onIntervalChange() {
this.updateView();
}
onAdvancedChange() {
if (!this.advanced) {
this.intervalMs = this.calculateIntervalMs();
this.interval = this.calculateIntervalMs();
} else {
let intervalMs = this.intervalMs;
if (!intervalMs || isNaN(intervalMs)) {
intervalMs = this.calculateIntervalMs();
let interval = this.interval;
if (!interval || typeof interval === 'number' && isNaN(interval)) {
interval = this.calculateIntervalMs();
}
this.setIntervalMs(intervalMs);
this.setInterval(interval);
}
this.updateView();
}

2
ui-ngx/src/app/shared/components/time/timewindow-panel.component.html

@ -203,6 +203,7 @@
[(hideFlag)]="timewindow.hideAggInterval"
(hideFlagChange)="onHideAggIntervalChanged()"
[min]="minRealtimeAggInterval()" [max]="maxRealtimeAggInterval()"
useCalendarIntervals
predefinedName="aggregation.group-interval">
</tb-timeinterval>
</div>
@ -215,6 +216,7 @@
[(hideFlag)]="timewindow.hideAggInterval"
(hideFlagChange)="onHideAggIntervalChanged()"
[min]="minHistoryAggInterval()" [max]="maxHistoryAggInterval()"
useCalendarIntervals
predefinedName="aggregation.group-interval">
</tb-timeinterval>
</div>

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

@ -171,6 +171,14 @@ export class AttributesSubscriptionCmd extends SubscriptionCmd {
type = WsCmdType.ATTRIBUTES;
}
export enum IntervalType {
MILLISECONDS = 'MILLISECONDS',
WEEK = 'WEEK',
WEEK_ISO = 'WEEK_ISO',
MONTH = 'MONTH',
QUARTER = 'QUARTER'
}
export class TimeseriesSubscriptionCmd extends SubscriptionCmd {
startTs: number;
timeWindow: number;
@ -197,7 +205,9 @@ export interface EntityHistoryCmd {
keys: Array<string>;
startTs: number;
endTs: number;
intervalType: IntervalType;
interval: number;
timeZoneId: string;
limit: number;
agg: AggregationType;
fetchLatestPreviousPoint?: boolean;
@ -211,7 +221,9 @@ export interface TimeSeriesCmd {
keys: Array<string>;
startTs: number;
timeWindow: number;
intervalType: IntervalType;
interval: number;
timeZoneId: string;
limit: number;
agg: AggregationType;
fetchLatestPreviousPoint?: boolean;
@ -382,12 +394,14 @@ export class TelemetryPluginCmdsWrapper implements CmdWrapper {
}
}
export type SubscriptionDataEntry = [number, any, number?];
export interface SubscriptionData {
[key: string]: [number, any, number?][];
[key: string]: SubscriptionDataEntry[];
}
export interface IndexedSubscriptionData {
[id: number]: [number, any, number?][];
[id: number]: SubscriptionDataEntry[];
}
export interface SubscriptionDataHolder {

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

@ -18,6 +18,7 @@ import { TimeService } from '@core/services/time.service';
import { deepClone, isDefined, isNumeric, isUndefined } from '@app/core/utils';
import * as moment_ from 'moment';
import * as momentTz from 'moment-timezone';
import { IntervalType } from '@shared/models/telemetry/telemetry.models';
const moment = moment_;
@ -26,6 +27,11 @@ export const MINUTE = 60 * SECOND;
export const HOUR = 60 * MINUTE;
export const DAY = 24 * HOUR;
export const WEEK = 7 * DAY;
export const AVG_MONTH = Math.floor(30.44 * DAY);
export const AVG_QUARTER = Math.floor(DAY * 365.2425 / 4);
export const YEAR = DAY * 365;
export type ComparisonDuration = moment_.unitOfTime.DurationConstructor | 'previousInterval' | 'customInterval';
@ -47,8 +53,28 @@ export enum HistoryWindowType {
FOR_ALL_TIME
}
export type Interval = number | IntervalType;
export class IntervalMath {
public static max(...values: Interval[]): Interval {
const numberArr = values.map(v => IntervalMath.numberValue(v));
const index = numberArr.indexOf(Math.max(...numberArr));
return values[index];
}
public static min(...values: Interval[]): Interval {
const numberArr = values.map(v => IntervalMath.numberValue(v));
const index = numberArr.indexOf(Math.min(...numberArr));
return values[index];
}
public static numberValue(value: Interval): number {
return typeof value === 'number' ? value : IntervalTypeValuesMap.get(value);
}
}
export interface IntervalWindow {
interval?: number;
interval?: Interval;
timewindowMs?: number;
quickInterval?: QuickTimeInterval;
}
@ -88,7 +114,7 @@ export const aggregationTranslations = new Map<AggregationType, string>(
);
export interface Aggregation {
interval?: number;
interval?: Interval;
type: AggregationType;
limit: number;
}
@ -110,7 +136,7 @@ export interface Timewindow {
}
export interface SubscriptionAggregation extends Aggregation {
interval?: number;
interval?: Interval;
timeWindow?: number;
stateData?: boolean;
}
@ -129,8 +155,9 @@ export interface SubscriptionTimewindow {
export interface WidgetTimewindow {
minTime?: number;
maxTime?: number;
interval?: number;
interval?: Interval;
timezone?: string;
tsOffset?: number;
stDiff?: number;
}
@ -188,6 +215,13 @@ export const QuickTimeIntervalTranslationMap = new Map<QuickTimeInterval, string
[QuickTimeInterval.CURRENT_YEAR_SO_FAR, 'timeinterval.predefined.current-year-so-far']
]);
export const IntervalTypeValuesMap = new Map<IntervalType, number>([
[IntervalType.WEEK, WEEK],
[IntervalType.WEEK_ISO, WEEK],
[IntervalType.MONTH, AVG_MONTH],
[IntervalType.QUARTER, AVG_QUARTER]
]);
export const forAllTimeInterval = (): Timewindow => ({
selectedTab: TimewindowType.HISTORY,
history: {
@ -324,7 +358,7 @@ export const initModelFromDefaultTimewindow = (value: Timewindow, quickIntervalO
};
export const toHistoryTimewindow = (timewindow: Timewindow, startTimeMs: number, endTimeMs: number,
interval: number, timeService: TimeService): Timewindow => {
interval: Interval, timeService: TimeService): Timewindow => {
if (timewindow.history) {
interval = isDefined(interval) ? interval : timewindow.history.interval;
} else if (timewindow.realtime) {
@ -412,23 +446,23 @@ const getSubscriptionRealtimeWindowFromTimeInterval = (interval: QuickTimeInterv
case QuickTimeInterval.CURRENT_MONTH:
case QuickTimeInterval.CURRENT_MONTH_SO_FAR:
currentDate = getCurrentTime(tz);
return currentDate.endOf('month').diff(currentDate.clone().startOf('month'));
return currentDate.endOf('month').add(1, 'milliseconds').diff(currentDate.clone().startOf('month'));
case QuickTimeInterval.CURRENT_QUARTER:
case QuickTimeInterval.CURRENT_QUARTER_SO_FAR:
currentDate = getCurrentTime(tz);
return currentDate.endOf('quarter').diff(currentDate.clone().startOf('quarter'));
return currentDate.endOf('quarter').add(1, 'milliseconds').diff(currentDate.clone().startOf('quarter'));
case QuickTimeInterval.CURRENT_HALF_YEAR:
case QuickTimeInterval.CURRENT_HALF_YEAR_SO_FAR:
currentDate = getCurrentTime(tz);
if (currentDate.get('quarter') < 3) {
return currentDate.clone().set('quarter', 2).endOf('quarter').diff(currentDate.startOf('year'));
return currentDate.clone().set('quarter', 2).endOf('quarter').add(1, 'milliseconds').diff(currentDate.startOf('year'));
} else {
return currentDate.endOf('year').diff(currentDate.clone().set('quarter', 3).startOf('quarter'));
return currentDate.endOf('year').add(1, 'milliseconds').diff(currentDate.clone().set('quarter', 3).startOf('quarter'));
}
case QuickTimeInterval.CURRENT_YEAR:
case QuickTimeInterval.CURRENT_YEAR_SO_FAR:
currentDate = getCurrentTime(tz);
return currentDate.endOf('year').diff(currentDate.clone().startOf('year'));
return currentDate.endOf('year').add(1, 'milliseconds').diff(currentDate.clone().startOf('year'));
}
};
@ -551,10 +585,10 @@ export const createSubscriptionTimewindow = (timewindow: Timewindow, stDiff: num
limit: timeService.getMaxDatapointsLimit(),
type: AggregationType.AVG
},
timezone: timewindow.timezone,
timezone: timewindow.timezone || getDefaultTimezone(),
tsOffset: calculateTsOffset(timewindow.timezone)
};
let aggTimewindow;
let aggTimewindow: number;
if (stateData) {
subscriptionTimewindow.aggregation.type = AggregationType.NONE;
subscriptionTimewindow.aggregation.stateData = true;
@ -587,16 +621,13 @@ export const createSubscriptionTimewindow = (timewindow: Timewindow, stDiff: num
subscriptionTimewindow.startTs = currentDate.valueOf() + stDiff - subscriptionTimewindow.realtimeWindowMs;
}
subscriptionTimewindow.aggregation.interval =
timeService.boundIntervalToTimewindow(subscriptionTimewindow.realtimeWindowMs, timewindow.realtime.interval,
subscriptionTimewindow.aggregation.type);
subscriptionTimewindow.aggregation.type === AggregationType.NONE
? SECOND
: (!!timewindow.realtime.interval ? timewindow.realtime.interval :
timeService.boundIntervalToTimewindow(subscriptionTimewindow.realtimeWindowMs, timewindow.realtime.interval,
subscriptionTimewindow.aggregation.type));
aggTimewindow = subscriptionTimewindow.realtimeWindowMs;
if (realtimeType !== RealtimeWindowType.INTERVAL) {
const startDiff = subscriptionTimewindow.startTs % subscriptionTimewindow.aggregation.interval;
if (startDiff) {
subscriptionTimewindow.startTs -= startDiff;
aggTimewindow += subscriptionTimewindow.aggregation.interval;
}
}
} else {
let historyType = timewindow.history.historyType;
if (isUndefined(historyType)) {
@ -633,12 +664,17 @@ export const createSubscriptionTimewindow = (timewindow: Timewindow, stDiff: num
}
subscriptionTimewindow.startTs = subscriptionTimewindow.fixedWindow.startTimeMs;
subscriptionTimewindow.aggregation.interval =
timeService.boundIntervalToTimewindow(aggTimewindow, timewindow.history.interval, subscriptionTimewindow.aggregation.type);
subscriptionTimewindow.aggregation.type === AggregationType.NONE
? SECOND
: (!!timewindow.history.interval ? timewindow.history.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);
aggregation.limit = calculateIntervalsCount(subscriptionTimewindow.startTs, aggTimewindow,
subscriptionTimewindow.aggregation.interval, timewindow.timezone);
}
return subscriptionTimewindow;
};
@ -759,12 +795,13 @@ export const createTimewindowForComparison = (subscriptionTimewindow: Subscripti
fixedWindow: null,
realtimeWindowMs: null,
aggregation: subscriptionTimewindow.aggregation,
tsOffset: subscriptionTimewindow.tsOffset
tsOffset: subscriptionTimewindow.tsOffset,
timezone: subscriptionTimewindow.timezone
};
if (subscriptionTimewindow.fixedWindow) {
let startTimeMs;
let endTimeMs;
let startTimeMs: number;
let endTimeMs: number;
if (timeUnit === 'previousInterval') {
if (subscriptionTimewindow.quickInterval) {
const startDate = moment(subscriptionTimewindow.fixedWindow.startTimeMs);
@ -829,7 +866,7 @@ export const cloneSelectedTimewindow = (timewindow: Timewindow): Timewindow => {
export interface TimeInterval {
name: string;
translateParams: {[key: string]: any};
value: number;
value: Interval;
}
export const defaultTimeIntervals = new Array<TimeInterval>(
@ -923,10 +960,30 @@ export const defaultTimeIntervals = new Array<TimeInterval>(
translateParams: {days: 7},
value: 7 * DAY
},
{
name: 'timeinterval.type.week',
translateParams: {},
value: IntervalType.WEEK
},
{
name: 'timeinterval.type.week-iso',
translateParams: {},
value: IntervalType.WEEK_ISO
},
{
name: 'timeinterval.days-interval',
translateParams: {days: 30},
value: 30 * DAY
},
{
name: 'timeinterval.type.month',
translateParams: {},
value: IntervalType.MONTH
},
{
name: 'timeinterval.type.quarter',
translateParams: {},
value: IntervalType.QUARTER
}
);
@ -1013,5 +1070,72 @@ export const getTime = (ts: number, tz?: string): moment_.Moment => {
}
};
export const calculateIntervalsCount = (startTs: number, timewindow: number, interval: Interval, tz?: string): number => {
if (typeof interval === 'number') {
return Math.ceil(timewindow / interval);
} else {
const current = getTime(startTs, tz);
const endDate = getTime(startTs + timewindow, tz);
let startInterval = startIntervalDate(current, interval);
let endInterval = endIntervalDate(current, interval);
let count = 0;
while (startInterval.isBefore(endDate)) {
count++;
endInterval.add(1, 'milliseconds');
startInterval = startIntervalDate(endInterval, interval);
endInterval = endIntervalDate(endInterval, interval);
}
return count;
}
};
export const startIntervalDate = (current: moment_.Moment, interval: IntervalType): moment_.Moment => {
switch (interval) {
case IntervalType.WEEK:
return current.clone().startOf('week');
case IntervalType.WEEK_ISO:
return current.clone().startOf('isoWeek');
case IntervalType.MONTH:
return current.clone().startOf('month');
case IntervalType.QUARTER:
return current.clone().startOf('quarter');
}
};
export const endIntervalDate = (current: moment_.Moment, interval: IntervalType): moment_.Moment => {
switch (interval) {
case IntervalType.WEEK:
return current.clone().endOf('week');
case IntervalType.WEEK_ISO:
return current.clone().endOf('isoWeek');
case IntervalType.MONTH:
return current.clone().endOf('month');
case IntervalType.QUARTER:
return current.clone().endOf('quarter');
}
};
export const calculateAggInterval = (subsTw: SubscriptionTimewindow, endTs: number, timestamp: number): [number, number] => {
let startIntervalTs: number;
let endIntervalTs: number;
if (typeof subsTw.aggregation.interval === 'number') {
const startTs = subsTw.startTs + subsTw.tsOffset;
startIntervalTs = startTs + Math.floor((timestamp - startTs) / subsTw.aggregation.interval) * subsTw.aggregation.interval;
endIntervalTs = startIntervalTs + subsTw.aggregation.interval;
} else {
const time = getTime(timestamp, subsTw.timezone);
let startInterval = startIntervalDate(time, subsTw.aggregation.interval);
const start = getTime(subsTw.startTs, subsTw.timezone);
if (start.isAfter(startInterval)) {
startInterval = start;
}
const endInterval = endIntervalDate(time, subsTw.aggregation.interval).add(1, 'milliseconds');
startIntervalTs = startInterval.valueOf() + subsTw.tsOffset;
endIntervalTs = endInterval.valueOf() + subsTw.tsOffset;
}
endIntervalTs = Math.min(endIntervalTs, endTs);
return [startIntervalTs, endIntervalTs];
};
export const getCurrentTimeForComparison = (timeForComparison: moment_.unitOfTime.DurationConstructor, tz?: string): moment_.Moment =>
getCurrentTime(tz).subtract(1, timeForComparison);

16
ui-ngx/src/app/shared/models/widget-settings.models.ts

@ -15,7 +15,7 @@
///
import { isDefinedAndNotNull, isNumber, isNumeric, isUndefinedOrNull, parseFunction } from '@core/utils';
import { DataKey, Datasource, DatasourceData } from '@shared/models/widget.models';
import { DataEntry, DataKey, Datasource, DatasourceData } from '@shared/models/widget.models';
import { Injector } from '@angular/core';
import { DatePipe } from '@angular/common';
import { DateAgoPipe } from '@shared/pipe/date-ago.pipe';
@ -366,7 +366,7 @@ export abstract class DateFormatProcessor {
protected settings: DateFormatSettings) {
}
abstract update(ts: string | number | Date): void;
abstract update(ts: string | number | Date): string;
}
@ -380,12 +380,13 @@ export class SimpleDateFormatProcessor extends DateFormatProcessor {
this.datePipe = $injector.get(DatePipe);
}
update(ts: string| number | Date): void {
update(ts: string| number | Date): string {
if (ts) {
this.formatted = this.datePipe.transform(ts, this.settings.format);
} else {
this.formatted = '&nbsp;';
}
return this.formatted;
}
}
@ -402,7 +403,7 @@ export class LastUpdateAgoDateFormatProcessor extends DateFormatProcessor {
this.translate = $injector.get(TranslateService);
}
update(ts: string| number | Date): void {
update(ts: string| number | Date): string {
if (ts) {
const agoText = this.dateAgoPipe.transform(ts, {applyAgo: true, short: true, textPart: true});
if (this.settings.hideLastUpdatePrefix) {
@ -414,6 +415,7 @@ export class LastUpdateAgoDateFormatProcessor extends DateFormatProcessor {
} else {
this.formatted = '&nbsp;';
}
return this.formatted;
}
}
@ -629,7 +631,7 @@ export const setLabel = (label: string, datasources?: Datasource[]): void => {
}
};
export const getSingleTsValue = (data: Array<DatasourceData>): [number, any] => {
export const getSingleTsValue = (data: Array<DatasourceData>): DataEntry => {
if (data.length) {
const dsData = data[0];
if (dsData.data.length) {
@ -639,7 +641,7 @@ export const getSingleTsValue = (data: Array<DatasourceData>): [number, any] =>
return null;
};
export const getSingleTsValueByDataKey = (data: Array<DatasourceData>, dataKey: DataKey): [number, any] => {
export const getSingleTsValueByDataKey = (data: Array<DatasourceData>, dataKey: DataKey): DataEntry => {
if (data.length) {
const dsData = data.find(d => d.dataKey === dataKey);
if (dsData?.data?.length) {
@ -649,7 +651,7 @@ export const getSingleTsValueByDataKey = (data: Array<DatasourceData>, dataKey:
return null;
};
export const getLatestSingleTsValue = (data: Array<DatasourceData>): [number, any] => {
export const getLatestSingleTsValue = (data: Array<DatasourceData>): DataEntry => {
if (data.length) {
const dsData = data[0];
if (dsData.data.length) {

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

@ -461,7 +461,13 @@ export interface ReplaceInfo {
dataKeyName: string;
}
export type DataSet = [number, any][];
export type DataEntry = [number, any, [number, number]?];
export type DataSet = DataEntry[];
export interface IndexedData {
[id: number]: DataSet;
}
export interface DataSetHolder {
data: DataSet;

6
ui-ngx/src/assets/locale/locale.constant-en_US.json

@ -4220,6 +4220,12 @@
"current-half-year-so-far": "Current half year so far",
"current-year": "Current year",
"current-year-so-far": "Current year so far"
},
"type": {
"week": "Week (Sun - Sat)",
"week-iso": "Week (Mon - Sun)",
"month": "Month",
"quarter": "Quarter"
}
},
"timeunit": {

5
ui-ngx/yarn.lock

@ -9840,6 +9840,11 @@ socks@^2.6.2:
ip "^2.0.0"
smart-buffer "^4.2.0"
sorted-btree@^1.8.1:
version "1.8.1"
resolved "https://registry.yarnpkg.com/sorted-btree/-/sorted-btree-1.8.1.tgz#6e6275f7955e5892bb8737149cbe495be10f426f"
integrity sha512-395+XIP+wqNn3USkFSrNz7G3Ss/MXlZEqesxvzCRFwL14h6e8LukDHdLBePn5pwbm5OQ9vGu8mDyz2lLDIqamQ==
"source-map-js@>=0.6.2 <2.0.0", source-map-js@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c"

Loading…
Cancel
Save