23 changed files with 2752 additions and 128 deletions
File diff suppressed because one or more lines are too long
@ -0,0 +1,208 @@ |
|||||
|
diff --git a/node_modules/echarts/lib/component/dataZoom/DataZoomModel.js b/node_modules/echarts/lib/component/dataZoom/DataZoomModel.js
|
||||
|
index 112ebe3..9afc9b7 100644
|
||||
|
--- a/node_modules/echarts/lib/component/dataZoom/DataZoomModel.js
|
||||
|
+++ b/node_modules/echarts/lib/component/dataZoom/DataZoomModel.js
|
||||
|
@@ -422,7 +422,10 @@ function (_super) {
|
||||
|
return axisProxy.getDataValueWindow(); |
||||
|
} |
||||
|
} else { |
||||
|
- return this.getAxisProxy(axisDim, axisIndex).getDataValueWindow();
|
||||
|
+ var axisProxy = this.getAxisProxy(axisDim, axisIndex);
|
||||
|
+ if (axisProxy) {
|
||||
|
+ return axisProxy.getDataValueWindow();
|
||||
|
+ }
|
||||
|
} |
||||
|
}; |
||||
|
/** |
||||
|
@@ -449,11 +452,11 @@ function (_super) {
|
||||
|
for (var j = 0; j < axisInfo.indexList.length; j++) { |
||||
|
var proxy = this.getAxisProxy(axisDim, axisInfo.indexList[j]); |
||||
|
|
||||
|
- if (proxy.hostedBy(this)) {
|
||||
|
+ if (proxy && proxy.hostedBy(this)) {
|
||||
|
return proxy; |
||||
|
} |
||||
|
|
||||
|
- if (!firstProxy) {
|
||||
|
+ if (proxy && !firstProxy) {
|
||||
|
firstProxy = proxy; |
||||
|
} |
||||
|
} |
||||
|
diff --git a/node_modules/echarts/lib/component/dataZoom/InsideZoomView.js b/node_modules/echarts/lib/component/dataZoom/InsideZoomView.js
|
||||
|
index 7163279..c37f9c2 100644
|
||||
|
--- a/node_modules/echarts/lib/component/dataZoom/InsideZoomView.js
|
||||
|
+++ b/node_modules/echarts/lib/component/dataZoom/InsideZoomView.js
|
||||
|
@@ -112,12 +112,15 @@ var getRangeHandlers = {
|
||||
|
range[0] = (range[0] - percentPoint) * scale + percentPoint; |
||||
|
range[1] = (range[1] - percentPoint) * scale + percentPoint; // Restrict range. |
||||
|
|
||||
|
- var minMaxSpan = this.dataZoomModel.findRepresentativeAxisProxy().getMinMaxSpan();
|
||||
|
- sliderMove(0, range, [0, 100], 0, minMaxSpan.minSpan, minMaxSpan.maxSpan);
|
||||
|
- this.range = range;
|
||||
|
-
|
||||
|
- if (lastRange[0] !== range[0] || lastRange[1] !== range[1]) {
|
||||
|
- return range;
|
||||
|
+ var proxy = this.dataZoomModel.findRepresentativeAxisProxy();
|
||||
|
+ if (proxy) {
|
||||
|
+ var minMaxSpan = proxy.getMinMaxSpan();
|
||||
|
+ sliderMove(0, range, [0, 100], 0, minMaxSpan.minSpan, minMaxSpan.maxSpan);
|
||||
|
+ this.range = range;
|
||||
|
+
|
||||
|
+ if (lastRange[0] !== range[0] || lastRange[1] !== range[1]) {
|
||||
|
+ return range;
|
||||
|
+ }
|
||||
|
} |
||||
|
}, |
||||
|
pan: makeMover(function (range, axisModel, coordSysInfo, coordSysMainType, controller, e) { |
||||
|
diff --git a/node_modules/echarts/lib/component/dataZoom/SliderZoomView.js b/node_modules/echarts/lib/component/dataZoom/SliderZoomView.js
|
||||
|
index 590ef51..aff8a0a 100644
|
||||
|
--- a/node_modules/echarts/lib/component/dataZoom/SliderZoomView.js
|
||||
|
+++ b/node_modules/echarts/lib/component/dataZoom/SliderZoomView.js
|
||||
|
@@ -64,7 +64,7 @@ var DEFAULT_MOVE_HANDLE_SIZE = 7;
|
||||
|
var HORIZONTAL = 'horizontal'; |
||||
|
var VERTICAL = 'vertical'; |
||||
|
var LABEL_GAP = 5; |
||||
|
-var SHOW_DATA_SHADOW_SERIES_TYPE = ['line', 'bar', 'candlestick', 'scatter'];
|
||||
|
+var SHOW_DATA_SHADOW_SERIES_TYPE = ['line', 'bar', 'candlestick', 'scatter', 'custom'];
|
||||
|
var REALTIME_ANIMATION_CONFIG = { |
||||
|
easing: 'cubicOut', |
||||
|
duration: 100, |
||||
|
@@ -406,34 +406,37 @@ function (_super) {
|
||||
|
var result; |
||||
|
var ecModel = this.ecModel; |
||||
|
dataZoomModel.eachTargetAxis(function (axisDim, axisIndex) { |
||||
|
- var seriesModels = dataZoomModel.getAxisProxy(axisDim, axisIndex).getTargetSeriesModels();
|
||||
|
- each(seriesModels, function (seriesModel) {
|
||||
|
- if (result) {
|
||||
|
- return;
|
||||
|
- }
|
||||
|
-
|
||||
|
- if (showDataShadow !== true && indexOf(SHOW_DATA_SHADOW_SERIES_TYPE, seriesModel.get('type')) < 0) {
|
||||
|
- return;
|
||||
|
- }
|
||||
|
-
|
||||
|
- var thisAxis = ecModel.getComponent(getAxisMainType(axisDim), axisIndex).axis;
|
||||
|
- var otherDim = getOtherDim(axisDim);
|
||||
|
- var otherAxisInverse;
|
||||
|
- var coordSys = seriesModel.coordinateSystem;
|
||||
|
-
|
||||
|
- if (otherDim != null && coordSys.getOtherAxis) {
|
||||
|
- otherAxisInverse = coordSys.getOtherAxis(thisAxis).inverse;
|
||||
|
- }
|
||||
|
-
|
||||
|
- otherDim = seriesModel.getData().mapDimension(otherDim);
|
||||
|
- result = {
|
||||
|
- thisAxis: thisAxis,
|
||||
|
- series: seriesModel,
|
||||
|
- thisDim: axisDim,
|
||||
|
- otherDim: otherDim,
|
||||
|
- otherAxisInverse: otherAxisInverse
|
||||
|
- };
|
||||
|
- }, this);
|
||||
|
+ var axisProxy = dataZoomModel.getAxisProxy(axisDim, axisIndex);
|
||||
|
+ if (axisProxy) {
|
||||
|
+ var seriesModels = axisProxy.getTargetSeriesModels();
|
||||
|
+ each(seriesModels, function (seriesModel) {
|
||||
|
+ if (result) {
|
||||
|
+ return;
|
||||
|
+ }
|
||||
|
+
|
||||
|
+ if (showDataShadow !== true && indexOf(SHOW_DATA_SHADOW_SERIES_TYPE, seriesModel.get('type')) < 0) {
|
||||
|
+ return;
|
||||
|
+ }
|
||||
|
+
|
||||
|
+ var thisAxis = ecModel.getComponent(getAxisMainType(axisDim), axisIndex).axis;
|
||||
|
+ var otherDim = getOtherDim(axisDim);
|
||||
|
+ var otherAxisInverse;
|
||||
|
+ var coordSys = seriesModel.coordinateSystem;
|
||||
|
+
|
||||
|
+ if (otherDim != null && coordSys.getOtherAxis) {
|
||||
|
+ otherAxisInverse = coordSys.getOtherAxis(thisAxis).inverse;
|
||||
|
+ }
|
||||
|
+
|
||||
|
+ otherDim = seriesModel.getData().mapDimension(otherDim);
|
||||
|
+ result = {
|
||||
|
+ thisAxis: thisAxis,
|
||||
|
+ series: seriesModel,
|
||||
|
+ thisDim: axisDim,
|
||||
|
+ otherDim: otherDim,
|
||||
|
+ otherAxisInverse: otherAxisInverse
|
||||
|
+ };
|
||||
|
+ }, this);
|
||||
|
+ }
|
||||
|
}, this); |
||||
|
return result; |
||||
|
}; |
||||
|
@@ -595,12 +598,17 @@ function (_super) {
|
||||
|
|
||||
|
var viewExtend = this._getViewExtent(); |
||||
|
|
||||
|
- var minMaxSpan = dataZoomModel.findRepresentativeAxisProxy().getMinMaxSpan();
|
||||
|
- var percentExtent = [0, 100];
|
||||
|
- sliderMove(delta, handleEnds, viewExtend, dataZoomModel.get('zoomLock') ? 'all' : handleIndex, minMaxSpan.minSpan != null ? linearMap(minMaxSpan.minSpan, percentExtent, viewExtend, true) : null, minMaxSpan.maxSpan != null ? linearMap(minMaxSpan.maxSpan, percentExtent, viewExtend, true) : null);
|
||||
|
- var lastRange = this._range;
|
||||
|
- var range = this._range = asc([linearMap(handleEnds[0], viewExtend, percentExtent, true), linearMap(handleEnds[1], viewExtend, percentExtent, true)]);
|
||||
|
- return !lastRange || lastRange[0] !== range[0] || lastRange[1] !== range[1];
|
||||
|
+ var proxy = dataZoomModel.findRepresentativeAxisProxy();
|
||||
|
+ if (proxy) {
|
||||
|
+ var minMaxSpan = proxy.getMinMaxSpan();
|
||||
|
+ var percentExtent = [0, 100];
|
||||
|
+ sliderMove(delta, handleEnds, viewExtend, dataZoomModel.get('zoomLock') ? 'all' : handleIndex, minMaxSpan.minSpan != null ? linearMap(minMaxSpan.minSpan, percentExtent, viewExtend, true) : null, minMaxSpan.maxSpan != null ? linearMap(minMaxSpan.maxSpan, percentExtent, viewExtend, true) : null);
|
||||
|
+ var lastRange = this._range;
|
||||
|
+ var range = this._range = asc([linearMap(handleEnds[0], viewExtend, percentExtent, true), linearMap(handleEnds[1], viewExtend, percentExtent, true)]);
|
||||
|
+ return !lastRange || lastRange[0] !== range[0] || lastRange[1] !== range[1];
|
||||
|
+ } else {
|
||||
|
+ return false;
|
||||
|
+ }
|
||||
|
}; |
||||
|
|
||||
|
SliderZoomView.prototype._updateView = function (nonRealtime) { |
||||
|
diff --git a/node_modules/echarts/lib/component/dataZoom/dataZoomProcessor.js b/node_modules/echarts/lib/component/dataZoom/dataZoomProcessor.js
|
||||
|
index 3871784..9bab428 100644
|
||||
|
--- a/node_modules/echarts/lib/component/dataZoom/dataZoomProcessor.js
|
||||
|
+++ b/node_modules/echarts/lib/component/dataZoom/dataZoomProcessor.js
|
||||
|
@@ -91,7 +91,10 @@ var dataZoomProcessor = {
|
||||
|
// init stage and not after action dispatch handler, because |
||||
|
// reset should be called after seriesData.restoreData. |
||||
|
dataZoomModel.eachTargetAxis(function (axisDim, axisIndex) { |
||||
|
- dataZoomModel.getAxisProxy(axisDim, axisIndex).reset(dataZoomModel);
|
||||
|
+ var axisProxy = dataZoomModel.getAxisProxy(axisDim, axisIndex);
|
||||
|
+ if (axisProxy) {
|
||||
|
+ axisProxy.reset(dataZoomModel);
|
||||
|
+ }
|
||||
|
}); // Caution: data zoom filtering is order sensitive when using |
||||
|
// percent range and no min/max/scale set on axis. |
||||
|
// For example, we have dataZoom definition: |
||||
|
@@ -108,7 +111,10 @@ var dataZoomProcessor = {
|
||||
|
// and then reset y-axis and filter y-axis. |
||||
|
|
||||
|
dataZoomModel.eachTargetAxis(function (axisDim, axisIndex) { |
||||
|
- dataZoomModel.getAxisProxy(axisDim, axisIndex).filterData(dataZoomModel, api);
|
||||
|
+ var axisProxy = dataZoomModel.getAxisProxy(axisDim, axisIndex);
|
||||
|
+ if (axisProxy) {
|
||||
|
+ axisProxy.filterData(dataZoomModel, api);
|
||||
|
+ }
|
||||
|
}); |
||||
|
}); |
||||
|
ecModel.eachComponent('dataZoom', function (dataZoomModel) { |
||||
|
diff --git a/node_modules/echarts/lib/component/toolbox/feature/DataZoom.js b/node_modules/echarts/lib/component/toolbox/feature/DataZoom.js
|
||||
|
index c5ee405..957ffd2 100644
|
||||
|
--- a/node_modules/echarts/lib/component/toolbox/feature/DataZoom.js
|
||||
|
+++ b/node_modules/echarts/lib/component/toolbox/feature/DataZoom.js
|
||||
|
@@ -129,10 +129,13 @@ function (_super) {
|
||||
|
var axisModel = axis.model; |
||||
|
var dataZoomModel = findDataZoom(dimName, axisModel, ecModel); // Restrict range. |
||||
|
|
||||
|
- var minMaxSpan = dataZoomModel.findRepresentativeAxisProxy(axisModel).getMinMaxSpan();
|
||||
|
+ var proxy = dataZoomModel.findRepresentativeAxisProxy(axisModel);
|
||||
|
+ if (proxy) {
|
||||
|
+ var minMaxSpan = proxy.getMinMaxSpan();
|
||||
|
|
||||
|
- if (minMaxSpan.minValueSpan != null || minMaxSpan.maxValueSpan != null) {
|
||||
|
- minMax = sliderMove(0, minMax.slice(), axis.scale.getExtent(), 0, minMaxSpan.minValueSpan, minMaxSpan.maxValueSpan);
|
||||
|
+ if (minMaxSpan.minValueSpan != null || minMaxSpan.maxValueSpan != null) {
|
||||
|
+ minMax = sliderMove(0, minMax.slice(), axis.scale.getExtent(), 0, minMaxSpan.minValueSpan, minMaxSpan.maxValueSpan);
|
||||
|
+ }
|
||||
|
} |
||||
|
|
||||
|
dataZoomModel && (snapshot[dataZoomModel.id] = { |
||||
@ -0,0 +1,134 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2024 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 { LinearGradientObject } from 'zrender/lib/graphic/LinearGradient'; |
||||
|
import { Interval, IntervalMath } from '@shared/models/time/time.models'; |
||||
|
import { LabelFormatterCallback, SeriesLabelOption } from 'echarts/types/src/util/types'; |
||||
|
import { TimeSeriesChartDataItem } from '@home/components/widget/lib/chart/time-series-chart.models'; |
||||
|
import { CustomSeriesRenderItemParams } from 'echarts'; |
||||
|
import { CustomSeriesRenderItemAPI, CustomSeriesRenderItemReturn } from 'echarts/types/dist/shared'; |
||||
|
import { isNumeric } from '@core/utils'; |
||||
|
import * as echarts from 'echarts/core'; |
||||
|
|
||||
|
export interface BarVisualSettings { |
||||
|
color: string | LinearGradientObject; |
||||
|
borderColor: string; |
||||
|
borderWidth: number; |
||||
|
borderRadius: number; |
||||
|
} |
||||
|
|
||||
|
export interface BarRenderContext { |
||||
|
barsCount?: number; |
||||
|
barIndex?: number; |
||||
|
timeInterval?: Interval; |
||||
|
visualSettings?: BarVisualSettings; |
||||
|
labelOption?: SeriesLabelOption; |
||||
|
barStackIndex?: number; |
||||
|
currentStackItems?: TimeSeriesChartDataItem[]; |
||||
|
} |
||||
|
|
||||
|
export const renderTimeSeriesBar = (params: CustomSeriesRenderItemParams, api: CustomSeriesRenderItemAPI, |
||||
|
renderCtx: BarRenderContext): CustomSeriesRenderItemReturn => { |
||||
|
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; |
||||
|
const ts = start ? start : time; |
||||
|
if (!start || !end || !interval) { |
||||
|
interval = IntervalMath.numberValue(renderCtx.timeInterval); |
||||
|
start = time - interval / 2; |
||||
|
} |
||||
|
const gap = 0.3; |
||||
|
const barInterval = interval / (renderCtx.barsCount + gap * (renderCtx.barsCount + 3)); |
||||
|
const intervalGap = barInterval * gap * 2; |
||||
|
const barGap = barInterval * gap; |
||||
|
const value = api.value(1); |
||||
|
const startTime = start + intervalGap + (barInterval + barGap) * renderCtx.barIndex; |
||||
|
const delta = barInterval; |
||||
|
let offset = 0; |
||||
|
if (renderCtx.currentStackItems?.length) { |
||||
|
for (let i = 0; i < renderCtx.barStackIndex; i++) { |
||||
|
const stackItem = renderCtx.currentStackItems[i]; |
||||
|
const dataName = ts + ''; |
||||
|
const data = stackItem.data.find(d => d.name === dataName); |
||||
|
if (data) { |
||||
|
const val = data.value[1]; |
||||
|
if (isNumeric(val)) { |
||||
|
offset += Number(val); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
let lowerLeft: number[]; |
||||
|
if (offset !== 0 && isNumeric(value)) { |
||||
|
lowerLeft = api.coord([startTime, value >= 0 ? Number(value) + offset : offset]); |
||||
|
} else { |
||||
|
lowerLeft = api.coord([startTime, value >= 0 ? value : 0]); |
||||
|
} |
||||
|
const size = api.size([delta, value]); |
||||
|
const width = size[0]; |
||||
|
const height = size[1]; |
||||
|
|
||||
|
const coordSys: {x: number; y: number; width: number; height: number} = params.coordSys as any; |
||||
|
|
||||
|
const rectShape = echarts.graphic.clipRectByRect({ |
||||
|
x: lowerLeft[0], |
||||
|
y: lowerLeft[1], |
||||
|
width, |
||||
|
height |
||||
|
}, { |
||||
|
x: coordSys.x, |
||||
|
y: coordSys.y, |
||||
|
width: coordSys.width, |
||||
|
height: coordSys.height |
||||
|
}); |
||||
|
|
||||
|
const zeroPos = api.coord([0, offset]); |
||||
|
|
||||
|
const style: any = { |
||||
|
fill: renderCtx.visualSettings.color, |
||||
|
stroke: renderCtx.visualSettings.borderColor, |
||||
|
lineWidth: renderCtx.visualSettings.borderWidth |
||||
|
}; |
||||
|
|
||||
|
if (renderCtx.labelOption.show) { |
||||
|
let position = renderCtx.labelOption.position; |
||||
|
if (value < 0) { |
||||
|
if (position === 'top') { |
||||
|
position = 'bottom'; |
||||
|
} else if (position === 'bottom') { |
||||
|
position = 'top'; |
||||
|
} |
||||
|
} |
||||
|
style.text = (renderCtx.labelOption.formatter as LabelFormatterCallback)({value: [null, value]} as any); |
||||
|
style.textDistance = 5; |
||||
|
style.textPosition = position; |
||||
|
style.rich = renderCtx.labelOption.rich; |
||||
|
} |
||||
|
|
||||
|
return rectShape && { |
||||
|
type: 'rect', |
||||
|
id: time + '', |
||||
|
shape: {...rectShape, r: renderCtx.visualSettings.borderRadius}, |
||||
|
style, |
||||
|
focus: 'series', |
||||
|
transition: 'all', |
||||
|
enterFrom: { |
||||
|
style: { opacity: 0 }, |
||||
|
shape: { height: 0, y: zeroPos[1] } |
||||
|
} |
||||
|
}; |
||||
|
}; |
||||
@ -0,0 +1,122 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
Copyright © 2016-2024 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. |
||||
|
|
||||
|
--> |
||||
|
<div class="tb-time-series-chart-panel" [style]="backgroundStyle$ | async"> |
||||
|
<div class="tb-time-series-chart-overlay" [style]="overlayStyle"></div> |
||||
|
<ng-container *ngTemplateOutlet="widgetTitlePanel"></ng-container> |
||||
|
<div class="tb-time-series-chart-content" [class]="legendClass"> |
||||
|
<div #chartShape class="tb-time-series-chart-shape"> |
||||
|
</div> |
||||
|
<div *ngIf="showLegend" class="tb-time-series-chart-legend" [class.tb-simple-legend]="!displayLegendValues"> |
||||
|
<ng-container *ngIf="displayLegendValues; else simpleLegend"> |
||||
|
<ng-container *ngIf="horizontalLegendPosition; else verticalLegend"> |
||||
|
<ng-container *ngTemplateOutlet="horizontalLegend"></ng-container> |
||||
|
</ng-container> |
||||
|
</ng-container> |
||||
|
</div> |
||||
|
<ng-template #horizontalLegend> |
||||
|
<table class="tb-time-series-chart-legend-table"> |
||||
|
<thead> |
||||
|
<tr> |
||||
|
<th></th> |
||||
|
<th *ngFor="let legendKey of legendKeys"> |
||||
|
<ng-container *ngTemplateOutlet="legendItem; context:{legendKey: legendKey}"></ng-container> |
||||
|
</th> |
||||
|
</tr> |
||||
|
</thead> |
||||
|
<tbody> |
||||
|
<ng-container *ngIf="legendConfig.showMin === true"> |
||||
|
<ng-container *ngTemplateOutlet="legendDataRow; context:{label: 'legend.Min', type: 'min'}"></ng-container> |
||||
|
</ng-container> |
||||
|
<ng-container *ngIf="legendConfig.showMax === true"> |
||||
|
<ng-container *ngTemplateOutlet="legendDataRow; context:{label: 'legend.Max', type: 'max'}"></ng-container> |
||||
|
</ng-container> |
||||
|
<ng-container *ngIf="legendConfig.showAvg === true"> |
||||
|
<ng-container *ngTemplateOutlet="legendDataRow; context:{label: 'legend.Avg', type: 'avg'}"></ng-container> |
||||
|
</ng-container> |
||||
|
<ng-container *ngIf="legendConfig.showTotal === true"> |
||||
|
<ng-container *ngTemplateOutlet="legendDataRow; context:{label: 'legend.Total', type: 'total'}"></ng-container> |
||||
|
</ng-container> |
||||
|
<ng-container *ngIf="legendConfig.showLatest === true"> |
||||
|
<ng-container *ngTemplateOutlet="legendDataRow; context:{label: 'legend.Latest', type: 'latest'}"></ng-container> |
||||
|
</ng-container> |
||||
|
</tbody> |
||||
|
</table> |
||||
|
</ng-template> |
||||
|
<ng-template #verticalLegend> |
||||
|
<table class="tb-time-series-chart-legend-table vertical"> |
||||
|
<thead> |
||||
|
<tr> |
||||
|
<th></th> |
||||
|
<th class="tb-time-series-chart-legend-type-label right" *ngIf="legendConfig.showMin === true">{{ 'legend.Min' | translate }}</th> |
||||
|
<th class="tb-time-series-chart-legend-type-label right" *ngIf="legendConfig.showMax === true">{{ 'legend.Max' | translate }}</th> |
||||
|
<th class="tb-time-series-chart-legend-type-label right" *ngIf="legendConfig.showAvg === true">{{ 'legend.Avg' | translate }}</th> |
||||
|
<th class="tb-time-series-chart-legend-type-label right" *ngIf="legendConfig.showTotal === true">{{ 'legend.Total' | translate }}</th> |
||||
|
<th class="tb-time-series-chart-legend-type-label right" *ngIf="legendConfig.showLatest === true">{{ 'legend.Latest' | translate }}</th> |
||||
|
</tr> |
||||
|
</thead> |
||||
|
<tbody> |
||||
|
<tr *ngFor="let legendKey of legendKeys"> |
||||
|
<th> |
||||
|
<ng-container *ngTemplateOutlet="legendItem; context:{legendKey: legendKey, left: true}"></ng-container> |
||||
|
</th> |
||||
|
<td *ngIf="legendConfig.showMin === true" class="tb-time-series-chart-legend-value"> |
||||
|
{{ legendData.data[legendKey.dataIndex].min }} |
||||
|
</td> |
||||
|
<td *ngIf="legendConfig.showMax === true" class="tb-time-series-chart-legend-value"> |
||||
|
{{ legendData.data[legendKey.dataIndex].max }} |
||||
|
</td> |
||||
|
<td *ngIf="legendConfig.showAvg === true" class="tb-time-series-chart-legend-value"> |
||||
|
{{ legendData.data[legendKey.dataIndex].avg }} |
||||
|
</td> |
||||
|
<td *ngIf="legendConfig.showTotal === true" class="tb-time-series-chart-legend-value"> |
||||
|
{{ legendData.data[legendKey.dataIndex].total }} |
||||
|
</td> |
||||
|
<td *ngIf="legendConfig.showLatest === true" class="tb-time-series-chart-legend-value"> |
||||
|
{{ legendData.data[legendKey.dataIndex].latest }} |
||||
|
</td> |
||||
|
</tr> |
||||
|
</tbody> |
||||
|
</table> |
||||
|
</ng-template> |
||||
|
<ng-template #simpleLegend> |
||||
|
<ng-container *ngFor="let legendKey of legendKeys"> |
||||
|
<ng-container *ngTemplateOutlet="legendItem; context:{legendKey: legendKey}"></ng-container> |
||||
|
</ng-container> |
||||
|
</ng-template> |
||||
|
<ng-template #legendItem let-legendKey="legendKey" let-left="left"> |
||||
|
<div class="tb-time-series-chart-legend-item" |
||||
|
[class.left]="left"> |
||||
|
<div class="tb-time-series-chart-legend-item-label" |
||||
|
(mouseenter)="onLegendKeyEnter(legendKey)" |
||||
|
(mouseleave)="onLegendKeyLeave(legendKey)" |
||||
|
(click)="toggleLegendKey(legendKey)"> |
||||
|
<div class="tb-time-series-chart-legend-item-label-circle" [style]="{background: !legendKey.dataKey.hidden ? legendKey.dataKey.color : null}"></div> |
||||
|
<div [style]="!legendKey.dataKey.hidden ? legendLabelStyle : disabledLegendLabelStyle">{{ legendKey.dataKey.label }}</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</ng-template> |
||||
|
<ng-template #legendDataRow let-label="label" let-type="type"> |
||||
|
<tr> |
||||
|
<th class="tb-time-series-chart-legend-type-label">{{ label | translate }}</th> |
||||
|
<td *ngFor="let legendKey of legendKeys" class="tb-time-series-chart-legend-value"> |
||||
|
{{ legendData.data[legendKey.dataIndex][type] }} |
||||
|
</td> |
||||
|
</tr> |
||||
|
</ng-template> |
||||
|
</div> |
||||
|
</div> |
||||
@ -0,0 +1,181 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2024 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. |
||||
|
*/ |
||||
|
|
||||
|
$maxLegendWidth: 25%; |
||||
|
$maxLegendHeight: 35%; |
||||
|
|
||||
|
.tb-time-series-chart-panel { |
||||
|
width: 100%; |
||||
|
height: 100%; |
||||
|
position: relative; |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
gap: 16px; |
||||
|
padding: 20px 24px 24px 24px; |
||||
|
> div:not(.tb-time-series-chart-overlay) { |
||||
|
z-index: 1; |
||||
|
} |
||||
|
.tb-time-series-chart-overlay { |
||||
|
position: absolute; |
||||
|
top: 12px; |
||||
|
left: 12px; |
||||
|
bottom: 12px; |
||||
|
right: 12px; |
||||
|
} |
||||
|
div.tb-widget-title { |
||||
|
padding: 0; |
||||
|
} |
||||
|
.tb-time-series-chart-content { |
||||
|
flex: 1; |
||||
|
min-width: 0; |
||||
|
min-height: 0; |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
gap: 16px; |
||||
|
&.legend-top { |
||||
|
flex-direction: column-reverse; |
||||
|
} |
||||
|
&.legend-right { |
||||
|
flex-direction: row; |
||||
|
} |
||||
|
&.legend-left { |
||||
|
flex-direction: row-reverse; |
||||
|
} |
||||
|
.tb-time-series-chart-shape { |
||||
|
flex: 1; |
||||
|
min-width: 0; |
||||
|
min-height: 0; |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
justify-content: center; |
||||
|
} |
||||
|
&.legend-right, &.legend-left { |
||||
|
.tb-time-series-chart-legend { |
||||
|
display: inline-grid; |
||||
|
grid-auto-flow: column; |
||||
|
grid-template-rows: repeat(auto-fit, minmax(16px, min-content)); |
||||
|
max-width: calc($maxLegendWidth - 8px); |
||||
|
height: fit-content; |
||||
|
max-height: 100%; |
||||
|
} |
||||
|
} |
||||
|
&.legend-top, &.legend-bottom { |
||||
|
.tb-time-series-chart-legend { |
||||
|
align-self: center; |
||||
|
&.tb-simple-legend { |
||||
|
justify-content: center; |
||||
|
} |
||||
|
&:not(.tb-simple-legend) { |
||||
|
width: 100%; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
.tb-time-series-chart-legend { |
||||
|
display: flex; |
||||
|
align-items: flex-start; |
||||
|
align-self: stretch; |
||||
|
column-gap: 16px; |
||||
|
row-gap: 8px; |
||||
|
flex-wrap: wrap; |
||||
|
overflow: auto; |
||||
|
width: fit-content; |
||||
|
max-width: 100%; |
||||
|
max-height: calc($maxLegendHeight - 8px); |
||||
|
.tb-time-series-chart-legend-table { |
||||
|
border-spacing: 0; |
||||
|
table-layout: fixed; |
||||
|
&.vertical { |
||||
|
width: 100%; |
||||
|
table-layout: auto; |
||||
|
} |
||||
|
|
||||
|
thead th, tbody th { |
||||
|
position: sticky; |
||||
|
z-index: 1; |
||||
|
backdrop-filter: blur(5000px); |
||||
|
} |
||||
|
thead th { |
||||
|
top: 0; |
||||
|
&:first-child { |
||||
|
left: 0; |
||||
|
z-index: 2; |
||||
|
} |
||||
|
} |
||||
|
tbody th { |
||||
|
left: 0; |
||||
|
} |
||||
|
th, td { |
||||
|
&:not(:last-child) { |
||||
|
padding-right: 8px; |
||||
|
} |
||||
|
} |
||||
|
thead tr, tbody tr:not(:last-child) { |
||||
|
th, td { |
||||
|
padding-bottom: 8px; |
||||
|
} |
||||
|
} |
||||
|
.tb-time-series-chart-legend-item { |
||||
|
align-items: flex-end; |
||||
|
&.left { |
||||
|
align-items: flex-start; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
.tb-time-series-chart-legend-item { |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
justify-content: center; |
||||
|
align-items: flex-start; |
||||
|
user-select: none; |
||||
|
.tb-time-series-chart-legend-item-label { |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
gap: 4px; |
||||
|
color: #ccc; |
||||
|
white-space: nowrap; |
||||
|
cursor: pointer; |
||||
|
.tb-time-series-chart-legend-item-label-circle { |
||||
|
width: 8px; |
||||
|
height: 8px; |
||||
|
border-radius: 50%; |
||||
|
background-color: #ccc; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
.tb-time-series-chart-legend-type-label { |
||||
|
font-size: 12px; |
||||
|
font-style: normal; |
||||
|
font-weight: 400; |
||||
|
line-height: 16px; |
||||
|
color: rgba(0, 0, 0, 0.38); |
||||
|
white-space: nowrap; |
||||
|
text-align: left; |
||||
|
&.right { |
||||
|
text-align: right; |
||||
|
} |
||||
|
} |
||||
|
.tb-time-series-chart-legend-value { |
||||
|
font-size: 12px; |
||||
|
font-style: normal; |
||||
|
font-weight: 500; |
||||
|
line-height: 16px; |
||||
|
color: rgba(0, 0, 0, 0.87); |
||||
|
white-space: nowrap; |
||||
|
text-align: right; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,163 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2024 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 { |
||||
|
AfterViewInit, |
||||
|
ChangeDetectorRef, |
||||
|
Component, |
||||
|
ElementRef, |
||||
|
Input, |
||||
|
OnDestroy, |
||||
|
OnInit, |
||||
|
Renderer2, |
||||
|
TemplateRef, |
||||
|
ViewChild, |
||||
|
ViewEncapsulation |
||||
|
} from '@angular/core'; |
||||
|
import { timeSeriesChartKeyDefaultSettings, TimeSeriesChartKeySettings } from '@home/components/widget/lib/chart/time-series-chart.models'; |
||||
|
import { WidgetContext } from '@home/models/widget-component.models'; |
||||
|
import { Observable } from 'rxjs'; |
||||
|
import { backgroundStyle, ComponentStyle, overlayStyle, textStyle } from '@shared/models/widget-settings.models'; |
||||
|
import { ImagePipe } from '@shared/pipe/image.pipe'; |
||||
|
import { DomSanitizer } from '@angular/platform-browser'; |
||||
|
import { LegendConfig, LegendData, LegendKey, LegendPosition } from '@shared/models/widget.models'; |
||||
|
import { TbTimeSeriesChart } from '@home/components/widget/lib/chart/time-series-chart'; |
||||
|
import { |
||||
|
timeSeriesChartWidgetDefaultSettings, |
||||
|
TimeSeriesChartWidgetSettings |
||||
|
} from '@home/components/widget/lib/chart/time-series-chart-widget.models'; |
||||
|
import { mergeDeep } from '@core/utils'; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-time-series-chart-widget', |
||||
|
templateUrl: './time-series-chart-widget.component.html', |
||||
|
styleUrls: ['./time-series-chart-widget.component.scss'], |
||||
|
encapsulation: ViewEncapsulation.None |
||||
|
}) |
||||
|
export class TimeSeriesChartWidgetComponent implements OnInit, OnDestroy, AfterViewInit { |
||||
|
|
||||
|
@ViewChild('chartShape', {static: false}) |
||||
|
chartShape: ElementRef<HTMLElement>; |
||||
|
|
||||
|
settings: TimeSeriesChartWidgetSettings; |
||||
|
|
||||
|
@Input() |
||||
|
ctx: WidgetContext; |
||||
|
|
||||
|
@Input() |
||||
|
widgetTitlePanel: TemplateRef<any>; |
||||
|
|
||||
|
horizontalLegendPosition = false; |
||||
|
|
||||
|
showLegend: boolean; |
||||
|
legendClass: string; |
||||
|
legendConfig: LegendConfig; |
||||
|
legendData: LegendData; |
||||
|
legendKeys: LegendKey[]; |
||||
|
|
||||
|
backgroundStyle$: Observable<ComponentStyle>; |
||||
|
overlayStyle: ComponentStyle = {}; |
||||
|
|
||||
|
legendLabelStyle: ComponentStyle; |
||||
|
disabledLegendLabelStyle: ComponentStyle; |
||||
|
|
||||
|
displayLegendValues = false; |
||||
|
|
||||
|
private timeSeriesChart: TbTimeSeriesChart; |
||||
|
|
||||
|
constructor(private imagePipe: ImagePipe, |
||||
|
private sanitizer: DomSanitizer, |
||||
|
private renderer: Renderer2, |
||||
|
private cd: ChangeDetectorRef) { |
||||
|
} |
||||
|
|
||||
|
ngOnInit(): void { |
||||
|
this.ctx.$scope.timeSeriesChartWidget = this; |
||||
|
this.settings = {...timeSeriesChartWidgetDefaultSettings, ...this.ctx.settings}; |
||||
|
|
||||
|
this.backgroundStyle$ = backgroundStyle(this.settings.background, this.imagePipe, this.sanitizer); |
||||
|
this.overlayStyle = overlayStyle(this.settings.background.overlay); |
||||
|
|
||||
|
this.showLegend = this.settings.showLegend; |
||||
|
if (this.showLegend) { |
||||
|
this.legendData = this.ctx.defaultSubscription.legendData; |
||||
|
this.legendConfig = this.settings.legendConfig; |
||||
|
this.legendKeys = this.legendData.keys; |
||||
|
if (this.legendConfig.sortDataKeys) { |
||||
|
this.legendKeys = this.legendData.keys.sort((key1, key2) => key1.dataKey.label.localeCompare(key2.dataKey.label)); |
||||
|
} |
||||
|
this.legendKeys.forEach(legendKey => { |
||||
|
legendKey.dataKey.settings = mergeDeep<TimeSeriesChartKeySettings>({} as TimeSeriesChartKeySettings, |
||||
|
timeSeriesChartKeyDefaultSettings, legendKey.dataKey.settings); |
||||
|
legendKey.dataKey.hidden = legendKey.dataKey.settings.dataHiddenByDefault; |
||||
|
}); |
||||
|
this.legendKeys = this.legendKeys.filter(legendKey => legendKey.dataKey.settings.showInLegend); |
||||
|
if (!this.legendKeys.length) { |
||||
|
this.showLegend = false; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (this.showLegend) { |
||||
|
this.horizontalLegendPosition = [LegendPosition.left, LegendPosition.right].includes(this.legendConfig.position); |
||||
|
this.legendClass = `legend-${this.legendConfig.position}`; |
||||
|
this.legendLabelStyle = textStyle(this.settings.legendLabelFont); |
||||
|
this.disabledLegendLabelStyle = textStyle(this.settings.legendLabelFont); |
||||
|
this.legendLabelStyle.color = this.settings.legendLabelColor; |
||||
|
this.displayLegendValues = this.legendConfig.showMin || this.legendConfig.showMax || |
||||
|
this.legendConfig.showAvg || this.legendConfig.showTotal || this.legendConfig.showLatest; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
ngAfterViewInit() { |
||||
|
this.timeSeriesChart = new TbTimeSeriesChart(this.ctx, this.settings, this.chartShape.nativeElement, this.renderer); |
||||
|
} |
||||
|
|
||||
|
ngOnDestroy() { |
||||
|
if (this.timeSeriesChart) { |
||||
|
this.timeSeriesChart.destroy(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public onInit() { |
||||
|
const borderRadius = this.ctx.$widgetElement.css('borderRadius'); |
||||
|
this.overlayStyle = {...this.overlayStyle, ...{borderRadius}}; |
||||
|
this.cd.detectChanges(); |
||||
|
} |
||||
|
|
||||
|
public onDataUpdated() { |
||||
|
if (this.timeSeriesChart) { |
||||
|
this.timeSeriesChart.update(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public onLatestDataUpdated() { |
||||
|
if (this.timeSeriesChart) { |
||||
|
this.timeSeriesChart.latestUpdated(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public onLegendKeyEnter(legendKey: LegendKey) { |
||||
|
this.timeSeriesChart.keyEnter(legendKey.dataKey); |
||||
|
} |
||||
|
|
||||
|
public onLegendKeyLeave(legendKey: LegendKey) { |
||||
|
this.timeSeriesChart.keyLeave(legendKey.dataKey); |
||||
|
} |
||||
|
|
||||
|
public toggleLegendKey(legendKey: LegendKey) { |
||||
|
this.timeSeriesChart.toggleKey(legendKey.dataKey); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,52 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2024 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 { timeSeriesChartDefaultSettings, TimeSeriesChartSettings } from '@home/components/widget/lib/chart/time-series-chart.models'; |
||||
|
import { BackgroundSettings, BackgroundType, Font } from '@shared/models/widget-settings.models'; |
||||
|
import { defaultLegendConfig, LegendConfig, LegendPosition, widgetType } from '@shared/models/widget.models'; |
||||
|
import { mergeDeep } from '@core/utils'; |
||||
|
|
||||
|
export interface TimeSeriesChartWidgetSettings extends TimeSeriesChartSettings { |
||||
|
showLegend: boolean; |
||||
|
legendLabelFont: Font; |
||||
|
legendLabelColor: string; |
||||
|
legendConfig: LegendConfig; |
||||
|
background: BackgroundSettings; |
||||
|
} |
||||
|
|
||||
|
export const timeSeriesChartWidgetDefaultSettings: TimeSeriesChartWidgetSettings = |
||||
|
mergeDeep({} as TimeSeriesChartWidgetSettings, timeSeriesChartDefaultSettings as TimeSeriesChartWidgetSettings, { |
||||
|
showLegend: true, |
||||
|
legendLabelFont: { |
||||
|
family: 'Roboto', |
||||
|
size: 12, |
||||
|
sizeUnit: 'px', |
||||
|
style: 'normal', |
||||
|
weight: '400', |
||||
|
lineHeight: '16px' |
||||
|
}, |
||||
|
legendLabelColor: 'rgba(0, 0, 0, 0.76)', |
||||
|
legendConfig: {...defaultLegendConfig(widgetType.timeseries), position: LegendPosition.top}, |
||||
|
background: { |
||||
|
type: BackgroundType.color, |
||||
|
color: '#fff', |
||||
|
overlay: { |
||||
|
enabled: false, |
||||
|
color: 'rgba(255,255,255,0.72)', |
||||
|
blur: 3 |
||||
|
} |
||||
|
} |
||||
|
} as TimeSeriesChartWidgetSettings); |
||||
@ -0,0 +1,873 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2024 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 { |
||||
|
ECharts, |
||||
|
EChartsOption, |
||||
|
EChartsSeriesItem, |
||||
|
EChartsTooltipTrigger, |
||||
|
EChartsTooltipWidgetSettings, |
||||
|
measureThresholdLabelOffset |
||||
|
} from '@home/components/widget/lib/chart/echarts-widget.models'; |
||||
|
import { ComponentStyle, Font, simpleDateFormat, textStyle } from '@shared/models/widget-settings.models'; |
||||
|
import { XAXisOption, YAXisOption } from 'echarts/types/dist/shared'; |
||||
|
import { CustomSeriesOption, LineSeriesOption } from 'echarts/charts'; |
||||
|
import { formatValue, isDefinedAndNotNull, parseFunction } from '@core/utils'; |
||||
|
import { LinearGradientObject } from 'zrender/lib/graphic/LinearGradient'; |
||||
|
import tinycolor from 'tinycolor2'; |
||||
|
import Axis2D from 'echarts/types/src/coord/cartesian/Axis2D'; |
||||
|
import { Interval } from '@shared/models/time/time.models'; |
||||
|
import { ValueAxisBaseOption } from 'echarts/types/src/coord/axisCommonTypes'; |
||||
|
import { SeriesLabelOption } from 'echarts/types/src/util/types'; |
||||
|
import { |
||||
|
BarRenderContext, |
||||
|
BarVisualSettings, |
||||
|
renderTimeSeriesBar |
||||
|
} from '@home/components/widget/lib/chart/time-series-chart-bar.models'; |
||||
|
import { DataKey } from '@shared/models/widget.models'; |
||||
|
import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; |
||||
|
import { TbColorScheme } from '@shared/models/color.models'; |
||||
|
|
||||
|
export enum PointLabelPosition { |
||||
|
top = 'top', |
||||
|
bottom = 'bottom' |
||||
|
} |
||||
|
|
||||
|
export enum AxisPosition { |
||||
|
left = 'left', |
||||
|
right = 'right', |
||||
|
top = 'top', |
||||
|
bottom = 'bottom' |
||||
|
} |
||||
|
|
||||
|
export enum TimeSeriesChartShape { |
||||
|
emptyCircle = 'emptyCircle', |
||||
|
circle = 'circle', |
||||
|
rect = 'rect', |
||||
|
roundRect = 'roundRect', |
||||
|
triangle = 'triangle', |
||||
|
diamond = 'diamond', |
||||
|
pin = 'pin', |
||||
|
arrow = 'arrow', |
||||
|
none = 'none' |
||||
|
} |
||||
|
|
||||
|
export enum TimeSeriesChartLineType { |
||||
|
solid = 'solid', |
||||
|
dashed = 'dashed', |
||||
|
dotted = 'dotted' |
||||
|
} |
||||
|
|
||||
|
export enum ThresholdLabelPosition { |
||||
|
start = 'start', |
||||
|
middle = 'middle', |
||||
|
end = 'end', |
||||
|
insideStart = 'insideStart', |
||||
|
insideStartTop = 'insideStartTop', |
||||
|
insideStartBottom = 'insideStartBottom', |
||||
|
insideMiddle = 'insideMiddle', |
||||
|
insideMiddleTop = 'insideMiddleTop', |
||||
|
insideMiddleBottom = 'insideMiddleBottom', |
||||
|
insideEnd = 'insideEnd', |
||||
|
insideEndTop = 'insideEndTop', |
||||
|
insideEndBottom = 'insideEndBottom' |
||||
|
} |
||||
|
|
||||
|
export interface TimeSeriesChartAxisSettings { |
||||
|
show: boolean; |
||||
|
position: AxisPosition; |
||||
|
label?: string; |
||||
|
labelFont?: Font; |
||||
|
labelColor?: string; |
||||
|
showLine: boolean; |
||||
|
lineColor: string; |
||||
|
showTicks: boolean; |
||||
|
ticksColor: string; |
||||
|
showTickLabels: boolean; |
||||
|
tickLabelFont: Font; |
||||
|
tickLabelColor: string; |
||||
|
showSplitLines: boolean; |
||||
|
splitLinesColor: string; |
||||
|
} |
||||
|
|
||||
|
export interface TimeSeriesChartYAxisSettings extends TimeSeriesChartAxisSettings { |
||||
|
min?: number | string; |
||||
|
max?: number | string; |
||||
|
intervalCalculator?: string; |
||||
|
} |
||||
|
|
||||
|
export enum TimeSeriesChartThresholdType { |
||||
|
constant = 'constant', |
||||
|
latestKey = 'latestKey', |
||||
|
entity = 'entity' |
||||
|
} |
||||
|
|
||||
|
export interface TimeSeriesChartThreshold { |
||||
|
type: TimeSeriesChartThresholdType; |
||||
|
value?: number; |
||||
|
latestKeyName?: string; |
||||
|
entityAlias?: string; |
||||
|
entityKeyType?: DataKeyType.attribute | DataKeyType.timeseries; |
||||
|
entityKey?: string; |
||||
|
units?: string; |
||||
|
decimals?: number; |
||||
|
lineWidth: number; |
||||
|
lineType: TimeSeriesChartLineType; |
||||
|
lineColor: string; |
||||
|
startSymbol: TimeSeriesChartShape; |
||||
|
startSymbolSize: number; |
||||
|
endSymbol: TimeSeriesChartShape; |
||||
|
endSymbolSize: number; |
||||
|
showLabel: boolean; |
||||
|
labelPosition: ThresholdLabelPosition; |
||||
|
labelFont: Font; |
||||
|
labelColor: string; |
||||
|
} |
||||
|
|
||||
|
const timeSeriesChartColorScheme: TbColorScheme = { |
||||
|
'threshold.line': { |
||||
|
light: 'rgba(0, 0, 0, 0.76)', |
||||
|
dark: '#eee' |
||||
|
}, |
||||
|
'threshold.label': { |
||||
|
light: 'rgba(0, 0, 0, 0.76)', |
||||
|
dark: '#eee' |
||||
|
}, |
||||
|
'axis.line': { |
||||
|
light: 'rgba(0, 0, 0, 0.54)', |
||||
|
dark: '#B9B8CE' |
||||
|
}, |
||||
|
'axis.label': { |
||||
|
light: 'rgba(0, 0, 0, 0.54)', |
||||
|
dark: '#B9B8CE' |
||||
|
}, |
||||
|
'axis.ticks': { |
||||
|
light: 'rgba(0, 0, 0, 0.54)', |
||||
|
dark: '#B9B8CE' |
||||
|
}, |
||||
|
'axis.tickLabel': { |
||||
|
light: 'rgba(0, 0, 0, 0.54)', |
||||
|
dark: '#B9B8CE' |
||||
|
}, |
||||
|
'axis.splitLine': { |
||||
|
light: 'rgba(0, 0, 0, 0.12)', |
||||
|
dark: '#484753' |
||||
|
}, |
||||
|
'series.pointLabel': { |
||||
|
light: 'rgba(0, 0, 0, 0.76)', |
||||
|
dark: '#eee' |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
export const timeSeriesChartThresholdDefaultSettings: TimeSeriesChartThreshold = { |
||||
|
type: TimeSeriesChartThresholdType.constant, |
||||
|
units: '', |
||||
|
decimals: 0, |
||||
|
lineWidth: 1, |
||||
|
lineType: TimeSeriesChartLineType.solid, |
||||
|
lineColor: timeSeriesChartColorScheme['threshold.line'].light, |
||||
|
startSymbol: TimeSeriesChartShape.none, |
||||
|
startSymbolSize: 5, |
||||
|
endSymbol: TimeSeriesChartShape.arrow, |
||||
|
endSymbolSize: 5, |
||||
|
showLabel: true, |
||||
|
labelPosition: ThresholdLabelPosition.end, |
||||
|
labelFont: { |
||||
|
family: 'Roboto', |
||||
|
size: 12, |
||||
|
sizeUnit: 'px', |
||||
|
style: 'normal', |
||||
|
weight: '400', |
||||
|
lineHeight: '1' |
||||
|
}, |
||||
|
labelColor: timeSeriesChartColorScheme['threshold.label'].light |
||||
|
}; |
||||
|
|
||||
|
export interface TimeSeriesChartSettings extends EChartsTooltipWidgetSettings { |
||||
|
darkMode: boolean; |
||||
|
dataZoom: boolean; |
||||
|
stack: boolean; |
||||
|
thresholds: TimeSeriesChartThreshold[]; |
||||
|
xAxis: TimeSeriesChartAxisSettings; |
||||
|
yAxis: TimeSeriesChartYAxisSettings; |
||||
|
} |
||||
|
|
||||
|
export const timeSeriesChartDefaultSettings: TimeSeriesChartSettings = { |
||||
|
darkMode: false, |
||||
|
dataZoom: true, |
||||
|
stack: false, |
||||
|
thresholds: [], |
||||
|
xAxis: { |
||||
|
show: true, |
||||
|
position: AxisPosition.bottom, |
||||
|
label: '', |
||||
|
labelFont: { |
||||
|
family: 'Roboto', |
||||
|
size: 12, |
||||
|
sizeUnit: 'px', |
||||
|
style: 'normal', |
||||
|
weight: '600', |
||||
|
lineHeight: '1' |
||||
|
}, |
||||
|
labelColor: timeSeriesChartColorScheme['axis.label'].light, |
||||
|
showLine: true, |
||||
|
lineColor: timeSeriesChartColorScheme['axis.line'].light, |
||||
|
showTicks: true, |
||||
|
ticksColor: timeSeriesChartColorScheme['axis.ticks'].light, |
||||
|
showTickLabels: true, |
||||
|
tickLabelFont: { |
||||
|
family: 'Roboto', |
||||
|
size: 10, |
||||
|
sizeUnit: 'px', |
||||
|
style: 'normal', |
||||
|
weight: '400', |
||||
|
lineHeight: '1' |
||||
|
}, |
||||
|
tickLabelColor: timeSeriesChartColorScheme['axis.tickLabel'].light, |
||||
|
showSplitLines: true, |
||||
|
splitLinesColor: timeSeriesChartColorScheme['axis.splitLine'].light |
||||
|
}, |
||||
|
yAxis: { |
||||
|
show: true, |
||||
|
position: AxisPosition.left, |
||||
|
label: '', |
||||
|
labelFont: { |
||||
|
family: 'Roboto', |
||||
|
size: 12, |
||||
|
sizeUnit: 'px', |
||||
|
style: 'normal', |
||||
|
weight: '600', |
||||
|
lineHeight: '1' |
||||
|
}, |
||||
|
labelColor: timeSeriesChartColorScheme['axis.label'].light, |
||||
|
showLine: true, |
||||
|
lineColor: timeSeriesChartColorScheme['axis.line'].light, |
||||
|
showTicks: true, |
||||
|
ticksColor: timeSeriesChartColorScheme['axis.ticks'].light, |
||||
|
showTickLabels: true, |
||||
|
tickLabelFont: { |
||||
|
family: 'Roboto', |
||||
|
size: 12, |
||||
|
sizeUnit: 'px', |
||||
|
style: 'normal', |
||||
|
weight: '400', |
||||
|
lineHeight: '1' |
||||
|
}, |
||||
|
tickLabelColor: timeSeriesChartColorScheme['axis.tickLabel'].light, |
||||
|
showSplitLines: true, |
||||
|
splitLinesColor: timeSeriesChartColorScheme['axis.splitLine'].light |
||||
|
}, |
||||
|
showTooltip: true, |
||||
|
tooltipTrigger: EChartsTooltipTrigger.axis, |
||||
|
tooltipValueFont: { |
||||
|
family: 'Roboto', |
||||
|
size: 12, |
||||
|
sizeUnit: 'px', |
||||
|
style: 'normal', |
||||
|
weight: '500', |
||||
|
lineHeight: '16px' |
||||
|
}, |
||||
|
tooltipValueColor: 'rgba(0, 0, 0, 0.76)', |
||||
|
tooltipShowDate: true, |
||||
|
tooltipDateInterval: true, |
||||
|
tooltipDateFormat: simpleDateFormat('dd MMM yyyy HH:mm:ss'), |
||||
|
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 |
||||
|
}; |
||||
|
|
||||
|
export enum SeriesFillType { |
||||
|
none = 'none', |
||||
|
opacity = 'opacity', |
||||
|
gradient = 'gradient' |
||||
|
} |
||||
|
|
||||
|
export interface SeriesFillSettings { |
||||
|
type: SeriesFillType; |
||||
|
opacity: number; |
||||
|
gradient: { |
||||
|
start: number; |
||||
|
end: number; |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
export interface LineSeriesSettings { |
||||
|
step: false | 'start' | 'end' | 'middle'; |
||||
|
smooth: boolean; |
||||
|
showLine: boolean; |
||||
|
lineWidth: number; |
||||
|
lineType: TimeSeriesChartLineType; |
||||
|
fillAreaSettings: SeriesFillSettings; |
||||
|
showPoints: boolean; |
||||
|
pointShape: TimeSeriesChartShape; |
||||
|
pointSize: number; |
||||
|
} |
||||
|
|
||||
|
export interface BarSeriesSettings { |
||||
|
showBorder: boolean; |
||||
|
borderWidth: number; |
||||
|
borderRadius: number; |
||||
|
backgroundSettings: SeriesFillSettings; |
||||
|
} |
||||
|
|
||||
|
export enum TimeSeriesChartSeriesType { |
||||
|
line = 'line', |
||||
|
bar = 'bar' |
||||
|
} |
||||
|
|
||||
|
export interface TimeSeriesChartKeySettings { |
||||
|
dataHiddenByDefault: boolean; |
||||
|
showInLegend: boolean; |
||||
|
showPointLabel: boolean; |
||||
|
pointLabelPosition: PointLabelPosition; |
||||
|
pointLabelFont: Font; |
||||
|
pointLabelColor: string; |
||||
|
type: TimeSeriesChartSeriesType; |
||||
|
lineSettings: LineSeriesSettings; |
||||
|
barSettings: BarSeriesSettings; |
||||
|
} |
||||
|
|
||||
|
export const timeSeriesChartKeyDefaultSettings: TimeSeriesChartKeySettings = { |
||||
|
showInLegend: true, |
||||
|
dataHiddenByDefault: false, |
||||
|
showPointLabel: false, |
||||
|
pointLabelPosition: PointLabelPosition.top, |
||||
|
pointLabelFont: { |
||||
|
family: 'Roboto', |
||||
|
size: 11, |
||||
|
sizeUnit: 'px', |
||||
|
style: 'normal', |
||||
|
weight: '400', |
||||
|
lineHeight: '1' |
||||
|
}, |
||||
|
pointLabelColor: timeSeriesChartColorScheme['series.pointLabel'].light, |
||||
|
type: TimeSeriesChartSeriesType.line, |
||||
|
lineSettings: { |
||||
|
step: false, |
||||
|
smooth: false, |
||||
|
showLine: true, |
||||
|
lineWidth: 2, |
||||
|
lineType: TimeSeriesChartLineType.solid, |
||||
|
fillAreaSettings: { |
||||
|
type: SeriesFillType.none, |
||||
|
opacity: 0.4, |
||||
|
gradient: { |
||||
|
start: 100, |
||||
|
end: 0 |
||||
|
} |
||||
|
}, |
||||
|
showPoints: false, |
||||
|
pointShape: TimeSeriesChartShape.emptyCircle, |
||||
|
pointSize: 4 |
||||
|
}, |
||||
|
barSettings: { |
||||
|
showBorder: false, |
||||
|
borderWidth: 2, |
||||
|
borderRadius: 0, |
||||
|
backgroundSettings: { |
||||
|
type: SeriesFillType.none, |
||||
|
opacity: 0.4, |
||||
|
gradient: { |
||||
|
start: 100, |
||||
|
end: 0 |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
export interface TimeSeriesChartDataItem extends EChartsSeriesItem { |
||||
|
yAxisIndex: number; |
||||
|
option?: LineSeriesOption | CustomSeriesOption; |
||||
|
barRenderContext?: BarRenderContext; |
||||
|
} |
||||
|
|
||||
|
type TimeSeriesChartThresholdValue = number | string | (number | string)[]; |
||||
|
|
||||
|
export interface TimeSeriesChartThresholdItem { |
||||
|
id: string; |
||||
|
yAxisIndex: number; |
||||
|
latestDataKey?: DataKey; |
||||
|
units?: string; |
||||
|
decimals?: number; |
||||
|
value: TimeSeriesChartThresholdValue; |
||||
|
settings: TimeSeriesChartThreshold; |
||||
|
option?: LineSeriesOption; |
||||
|
} |
||||
|
|
||||
|
export interface TimeSeriesChartYAxis { |
||||
|
id: string; |
||||
|
units: string; |
||||
|
option: YAXisOption & ValueAxisBaseOption; |
||||
|
intervalCalculator?: (axis: Axis2D) => number; |
||||
|
} |
||||
|
|
||||
|
export const createTimeSeriesYAxis = (axisId: string, units: string, |
||||
|
decimals: number, settings: TimeSeriesChartYAxisSettings, |
||||
|
darkMode: boolean): TimeSeriesChartYAxis => { |
||||
|
const yAxisTickLabelStyle = createChartTextStyle(settings.tickLabelFont, |
||||
|
settings.tickLabelColor, darkMode, 'axis.tickLabel'); |
||||
|
const yAxisNameStyle = createChartTextStyle(settings.labelFont, |
||||
|
settings.labelColor, darkMode, 'axis.label'); |
||||
|
const yAxis: TimeSeriesChartYAxis = { |
||||
|
id: axisId, |
||||
|
units, |
||||
|
option: { |
||||
|
show: settings.show, |
||||
|
type: 'value', |
||||
|
position: settings.position, |
||||
|
id: axisId, |
||||
|
offset: 0, |
||||
|
alignTicks: true, |
||||
|
scale: true, |
||||
|
min: settings.min, |
||||
|
max: settings.max, |
||||
|
name: settings.label, |
||||
|
nameLocation: 'middle', |
||||
|
nameRotate: settings.position === AxisPosition.left ? 90 : -90, |
||||
|
nameTextStyle: { |
||||
|
color: yAxisNameStyle.color, |
||||
|
fontStyle: yAxisNameStyle.fontStyle, |
||||
|
fontWeight: yAxisNameStyle.fontWeight, |
||||
|
fontFamily: yAxisNameStyle.fontFamily, |
||||
|
fontSize: yAxisNameStyle.fontSize |
||||
|
}, |
||||
|
axisLine: { |
||||
|
show: settings.showLine, |
||||
|
onZero: false, |
||||
|
lineStyle: { |
||||
|
color: prepareChartThemeColor(settings.lineColor, darkMode, 'axis.line') |
||||
|
} |
||||
|
}, |
||||
|
axisTick: { |
||||
|
show: settings.showTicks, |
||||
|
lineStyle: { |
||||
|
color: prepareChartThemeColor(settings.ticksColor, darkMode, 'axis.ticks') |
||||
|
} |
||||
|
}, |
||||
|
axisLabel: { |
||||
|
show: settings.showTickLabels, |
||||
|
color: yAxisTickLabelStyle.color, |
||||
|
fontStyle: yAxisTickLabelStyle.fontStyle, |
||||
|
fontWeight: yAxisTickLabelStyle.fontWeight, |
||||
|
fontFamily: yAxisTickLabelStyle.fontFamily, |
||||
|
fontSize: yAxisTickLabelStyle.fontSize, |
||||
|
formatter: (value: any) => formatValue(value, decimals, units, false) |
||||
|
}, |
||||
|
splitLine: { |
||||
|
show: settings.showSplitLines, |
||||
|
lineStyle: { |
||||
|
color: prepareChartThemeColor(settings.splitLinesColor, darkMode, 'axis.splitLine') |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
}; |
||||
|
if (settings.intervalCalculator && settings.intervalCalculator.length) { |
||||
|
yAxis.intervalCalculator = parseFunction(settings.intervalCalculator, ['axis']); |
||||
|
} |
||||
|
return yAxis; |
||||
|
}; |
||||
|
|
||||
|
export const createTimeSeriesXAxisOption = (settings: TimeSeriesChartAxisSettings, |
||||
|
min: number, max: number, darkMode: boolean): XAXisOption => { |
||||
|
const xAxisTickLabelStyle = createChartTextStyle(settings.tickLabelFont, |
||||
|
settings.tickLabelColor, darkMode, 'axis.tickLabel'); |
||||
|
const xAxisNameStyle = createChartTextStyle(settings.labelFont, |
||||
|
settings.labelColor, darkMode, 'axis.label'); |
||||
|
return { |
||||
|
show: settings.show, |
||||
|
type: 'time', |
||||
|
scale: true, |
||||
|
position: settings.position, |
||||
|
name: settings.label, |
||||
|
nameLocation: 'middle', |
||||
|
nameTextStyle: { |
||||
|
color: xAxisNameStyle.color, |
||||
|
fontStyle: xAxisNameStyle.fontStyle, |
||||
|
fontWeight: xAxisNameStyle.fontWeight, |
||||
|
fontFamily: xAxisNameStyle.fontFamily, |
||||
|
fontSize: xAxisNameStyle.fontSize |
||||
|
}, |
||||
|
axisTick: { |
||||
|
show: settings.showTicks, |
||||
|
lineStyle: { |
||||
|
color: prepareChartThemeColor(settings.ticksColor, darkMode, 'axis.ticks') |
||||
|
} |
||||
|
}, |
||||
|
axisLabel: { |
||||
|
show: settings.showTickLabels, |
||||
|
color: xAxisTickLabelStyle.color, |
||||
|
fontStyle: xAxisTickLabelStyle.fontStyle, |
||||
|
fontWeight: xAxisTickLabelStyle.fontWeight, |
||||
|
fontFamily: xAxisTickLabelStyle.fontFamily, |
||||
|
fontSize: xAxisTickLabelStyle.fontSize, |
||||
|
hideOverlap: true |
||||
|
}, |
||||
|
axisLine: { |
||||
|
show: settings.showLine, |
||||
|
onZero: false, |
||||
|
lineStyle: { |
||||
|
color: prepareChartThemeColor(settings.lineColor, darkMode, 'axis.line') |
||||
|
} |
||||
|
}, |
||||
|
splitLine: { |
||||
|
show: settings.showSplitLines, |
||||
|
lineStyle: { |
||||
|
color: prepareChartThemeColor(settings.splitLinesColor, darkMode, 'axis.splitLine') |
||||
|
} |
||||
|
}, |
||||
|
min, |
||||
|
max |
||||
|
}; |
||||
|
}; |
||||
|
|
||||
|
export const generateChartData = (dataItems: TimeSeriesChartDataItem[], |
||||
|
thresholdItems: TimeSeriesChartThresholdItem[], |
||||
|
timeInterval: Interval, |
||||
|
stack: boolean, |
||||
|
darkMode: boolean): Array<LineSeriesOption | CustomSeriesOption> => { |
||||
|
let series = generateChartSeries(dataItems, timeInterval, stack, darkMode); |
||||
|
if (thresholdItems.length) { |
||||
|
const thresholds = generateChartThresholds(thresholdItems, darkMode); |
||||
|
series = series.concat(thresholds); |
||||
|
} |
||||
|
return series; |
||||
|
}; |
||||
|
|
||||
|
export const calculateThresholdsOffset = (chart: ECharts, |
||||
|
thresholdItems: TimeSeriesChartThresholdItem[], |
||||
|
yAxisList: TimeSeriesChartYAxis[]): [number, number] => { |
||||
|
const result: [number, number] = [0, 0]; |
||||
|
for (const item of thresholdItems) { |
||||
|
const yAxis = yAxisList[item.yAxisIndex]; |
||||
|
const offset = measureThresholdLabelOffset(chart, yAxis.id, item.id, item.value); |
||||
|
result[0] = Math.max(result[0], offset[0]); |
||||
|
result[1] = Math.max(result[1], offset[1]); |
||||
|
} |
||||
|
return result; |
||||
|
}; |
||||
|
|
||||
|
export const parseThresholdData = (value: any): TimeSeriesChartThresholdValue => { |
||||
|
let thresholdValue: TimeSeriesChartThresholdValue; |
||||
|
if (Array.isArray(value)) { |
||||
|
thresholdValue = value; |
||||
|
} else { |
||||
|
try { |
||||
|
const parsedData = JSON.parse(value); |
||||
|
thresholdValue = Array.isArray(parsedData) ? parsedData : [parsedData]; |
||||
|
} catch (e) { |
||||
|
thresholdValue = [value]; |
||||
|
} |
||||
|
} |
||||
|
return thresholdValue; |
||||
|
}; |
||||
|
|
||||
|
const generateChartThresholds = (thresholdItems: TimeSeriesChartThresholdItem[], darkMode: boolean): Array<LineSeriesOption> => { |
||||
|
const series: Array<LineSeriesOption> = []; |
||||
|
for (const item of thresholdItems) { |
||||
|
if (isDefinedAndNotNull(item.value)) { |
||||
|
let seriesOption = item.option; |
||||
|
if (!item.option) { |
||||
|
const thresholdLabelStyle = createChartTextStyle(item.settings.labelFont, |
||||
|
item.settings.labelColor, darkMode, 'threshold.label'); |
||||
|
seriesOption = { |
||||
|
type: 'line', |
||||
|
id: item.id, |
||||
|
dataGroupId: item.id, |
||||
|
yAxisIndex: item.yAxisIndex, |
||||
|
animation: true, |
||||
|
data: [], |
||||
|
tooltip: { |
||||
|
show: false |
||||
|
}, |
||||
|
markLine: { |
||||
|
animation: true, |
||||
|
symbol: [item.settings.startSymbol, item.settings.endSymbol], |
||||
|
symbolSize: [item.settings.startSymbolSize, item.settings.endSymbolSize], |
||||
|
lineStyle: { |
||||
|
width: item.settings.lineWidth, |
||||
|
color: prepareChartThemeColor(item.settings.lineColor, darkMode, 'threshold.line'), |
||||
|
type: item.settings.lineType |
||||
|
}, |
||||
|
label: { |
||||
|
show: item.settings.showLabel, |
||||
|
position: item.settings.labelPosition, |
||||
|
color: thresholdLabelStyle.color, |
||||
|
fontStyle: thresholdLabelStyle.fontStyle, |
||||
|
fontWeight: thresholdLabelStyle.fontWeight, |
||||
|
fontFamily: thresholdLabelStyle.fontFamily, |
||||
|
fontSize: thresholdLabelStyle.fontSize, |
||||
|
formatter: params => formatValue(params.value, item.decimals, |
||||
|
item.units, false) |
||||
|
}, |
||||
|
emphasis: { |
||||
|
disabled: true |
||||
|
} |
||||
|
} |
||||
|
}; |
||||
|
item.option = seriesOption; |
||||
|
} |
||||
|
seriesOption.markLine.data = []; |
||||
|
if (Array.isArray(item.value)) { |
||||
|
for (const val of item.value) { |
||||
|
seriesOption.markLine.data.push({ |
||||
|
yAxis: val |
||||
|
}); |
||||
|
} |
||||
|
} else { |
||||
|
seriesOption.markLine.data.push({ |
||||
|
yAxis: item.value |
||||
|
}); |
||||
|
} |
||||
|
series.push(seriesOption); |
||||
|
} |
||||
|
} |
||||
|
return series; |
||||
|
}; |
||||
|
|
||||
|
const generateChartSeries = (dataItems: TimeSeriesChartDataItem[], |
||||
|
timeInterval: Interval, |
||||
|
stack: boolean, |
||||
|
darkMode: boolean): Array<LineSeriesOption | CustomSeriesOption> => { |
||||
|
const series: Array<LineSeriesOption | CustomSeriesOption> = []; |
||||
|
const enabledDataItems = dataItems.filter(d => d.enabled); |
||||
|
const barDataItems = enabledDataItems.filter(d => |
||||
|
d.dataKey.settings.type === TimeSeriesChartSeriesType.bar && d.data.length); |
||||
|
let barsCount = barDataItems.length; |
||||
|
const barGroups: number[] = []; |
||||
|
if (stack) { |
||||
|
barDataItems.forEach(item => { |
||||
|
if (barGroups.indexOf(item.yAxisIndex) === -1) { |
||||
|
barGroups.push(item.yAxisIndex); |
||||
|
} |
||||
|
}); |
||||
|
barsCount = barGroups.length; |
||||
|
} |
||||
|
for (const item of enabledDataItems) { |
||||
|
if (item.dataKey.settings.type === TimeSeriesChartSeriesType.bar) { |
||||
|
if (!item.barRenderContext) { |
||||
|
item.barRenderContext = {}; |
||||
|
} |
||||
|
item.barRenderContext.barsCount = barsCount; |
||||
|
item.barRenderContext.barIndex = stack ? barGroups.indexOf(item.yAxisIndex) : barDataItems.indexOf(item); |
||||
|
item.barRenderContext.timeInterval = timeInterval; |
||||
|
if (stack) { |
||||
|
const stackItems = enabledDataItems.filter(d => d.yAxisIndex === item.yAxisIndex); |
||||
|
item.barRenderContext.currentStackItems = stackItems; |
||||
|
item.barRenderContext.barStackIndex = stackItems.indexOf(item); |
||||
|
} |
||||
|
} |
||||
|
const seriesOption = createTimeSeriesChartSeries(item, stack, darkMode); |
||||
|
series.push(seriesOption); |
||||
|
} |
||||
|
return series; |
||||
|
}; |
||||
|
|
||||
|
export const updateDarkMode = (options: EChartsOption, settings: TimeSeriesChartSettings, |
||||
|
dataItems: TimeSeriesChartDataItem[], thresholdDataItems: TimeSeriesChartThresholdItem[], |
||||
|
darkMode: boolean): EChartsOption => { |
||||
|
options.darkMode = darkMode; |
||||
|
if (Array.isArray(options.yAxis)) { |
||||
|
for (const yAxis of options.yAxis) { |
||||
|
yAxis.nameTextStyle.color = prepareChartThemeColor(settings.yAxis.labelColor, darkMode, 'axis.label'); |
||||
|
yAxis.axisLabel.color = prepareChartThemeColor(settings.yAxis.tickLabelColor, darkMode, 'axis.tickLabel'); |
||||
|
yAxis.axisLine.lineStyle.color = prepareChartThemeColor(settings.yAxis.lineColor, darkMode, 'axis.line'); |
||||
|
yAxis.axisTick.lineStyle.color = prepareChartThemeColor(settings.yAxis.ticksColor, darkMode, 'axis.ticks'); |
||||
|
yAxis.splitLine.lineStyle.color = prepareChartThemeColor(settings.yAxis.splitLinesColor, darkMode, 'axis.splitLine'); |
||||
|
} |
||||
|
} |
||||
|
if (Array.isArray(options.xAxis)) { |
||||
|
for (const xAxis of options.xAxis) { |
||||
|
xAxis.nameTextStyle.color = prepareChartThemeColor(settings.xAxis.labelColor, darkMode, 'axis.label'); |
||||
|
xAxis.axisLabel.color = prepareChartThemeColor(settings.xAxis.tickLabelColor, darkMode, 'axis.tickLabel'); |
||||
|
xAxis.axisLine.lineStyle.color = prepareChartThemeColor(settings.xAxis.lineColor, darkMode, 'axis.line'); |
||||
|
xAxis.axisTick.lineStyle.color = prepareChartThemeColor(settings.xAxis.ticksColor, darkMode, 'axis.ticks'); |
||||
|
xAxis.splitLine.lineStyle.color = prepareChartThemeColor(settings.xAxis.splitLinesColor, darkMode, 'axis.splitLine'); |
||||
|
} |
||||
|
} |
||||
|
for (const item of dataItems) { |
||||
|
if (item.dataKey.settings.type === TimeSeriesChartSeriesType.line) { |
||||
|
if (item.option.label?.show) { |
||||
|
item.option.label.rich.value.color = prepareChartThemeColor(item.dataKey.settings.pointLabelColor, darkMode, 'series.pointLabel'); |
||||
|
} |
||||
|
if (Array.isArray(options.series)) { |
||||
|
const series = options.series.find(s => s.id === item.id); |
||||
|
if (series) { |
||||
|
if (series.label?.show) { |
||||
|
series.label.rich.value.color = prepareChartThemeColor(item.dataKey.settings.pointLabelColor, darkMode, 'series.pointLabel'); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} else { |
||||
|
if (item.barRenderContext?.labelOption?.show) { |
||||
|
item.barRenderContext.labelOption.rich.value.color = prepareChartThemeColor(item.dataKey.settings.pointLabelColor, |
||||
|
darkMode, 'series.pointLabel'); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
for (const item of thresholdDataItems) { |
||||
|
if (Array.isArray(options.series)) { |
||||
|
const series = options.series.find(s => s.id === item.id); |
||||
|
if (series) { |
||||
|
series.markLine.lineStyle.color = prepareChartThemeColor(item.settings.lineColor, darkMode, 'threshold.line'); |
||||
|
if (series.markLine?.label?.show) { |
||||
|
series.markLine.label.color = prepareChartThemeColor(item.settings.labelColor, darkMode, 'threshold.label'); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
return options; |
||||
|
}; |
||||
|
|
||||
|
const createTimeSeriesChartSeries = (item: TimeSeriesChartDataItem, |
||||
|
stack: boolean, |
||||
|
darkMode: boolean): LineSeriesOption | CustomSeriesOption => { |
||||
|
let seriesOption = item.option; |
||||
|
if (!seriesOption) { |
||||
|
const dataKey = item.dataKey; |
||||
|
const settings: TimeSeriesChartKeySettings = dataKey.settings; |
||||
|
const seriesColor = item.dataKey.color; |
||||
|
let pointLabelStyle: ComponentStyle = {}; |
||||
|
if (settings.showPointLabel) { |
||||
|
pointLabelStyle = createChartTextStyle(settings.pointLabelFont, settings.pointLabelColor, darkMode, 'series.pointLabel'); |
||||
|
} |
||||
|
const label: SeriesLabelOption = { |
||||
|
show: settings.showPointLabel, |
||||
|
position: settings.pointLabelPosition, |
||||
|
formatter: (params): string => { |
||||
|
const value = formatValue(params.value[1], item.decimals, item.units, false); |
||||
|
return `{value|${value}}`; |
||||
|
}, |
||||
|
rich: { |
||||
|
value: pointLabelStyle |
||||
|
} |
||||
|
}; |
||||
|
seriesOption = { |
||||
|
id: item.id, |
||||
|
dataGroupId: item.id, |
||||
|
yAxisIndex: item.yAxisIndex, |
||||
|
name: item.dataKey.label, |
||||
|
color: seriesColor, |
||||
|
stack: stack ? (item.yAxisIndex + '') : undefined, |
||||
|
stackStrategy: 'all', |
||||
|
emphasis: { |
||||
|
focus: 'series' |
||||
|
}, |
||||
|
animation: true, |
||||
|
dimensions: [ |
||||
|
{name: 'intervalStart', type: 'number'}, |
||||
|
{name: 'intervalEnd', type: 'number'} |
||||
|
], |
||||
|
encode: { |
||||
|
intervalStart: 2, |
||||
|
intervalEnd: 3 |
||||
|
} |
||||
|
}; |
||||
|
item.option = seriesOption; |
||||
|
if (settings.type === TimeSeriesChartSeriesType.line) { |
||||
|
const lineSettings = settings.lineSettings; |
||||
|
const lineSeriesOption = seriesOption as LineSeriesOption; |
||||
|
lineSeriesOption.type = 'line'; |
||||
|
lineSeriesOption.label = label; |
||||
|
lineSeriesOption.step = lineSettings.step; |
||||
|
lineSeriesOption.smooth = lineSettings.smooth; |
||||
|
lineSeriesOption.lineStyle = { |
||||
|
width: lineSettings.showLine ? lineSettings.lineWidth : 0, |
||||
|
type: lineSettings.lineType |
||||
|
}; |
||||
|
if (lineSettings.fillAreaSettings.type !== SeriesFillType.none) { |
||||
|
lineSeriesOption.areaStyle = {}; |
||||
|
if (lineSettings.fillAreaSettings.type === SeriesFillType.opacity) { |
||||
|
lineSeriesOption.areaStyle.opacity = lineSettings.fillAreaSettings.opacity; |
||||
|
} else { |
||||
|
lineSeriesOption.areaStyle.opacity = 1; |
||||
|
lineSeriesOption.areaStyle.color = createLinearOpacityGradient(seriesColor, lineSettings.fillAreaSettings.gradient); |
||||
|
} |
||||
|
} |
||||
|
lineSeriesOption.showSymbol = lineSettings.showPoints; |
||||
|
lineSeriesOption.symbol = lineSettings.pointShape; |
||||
|
lineSeriesOption.symbolSize = lineSettings.pointSize; |
||||
|
} else { |
||||
|
const barSettings = settings.barSettings; |
||||
|
const barSeriesOption = (seriesOption as any) as CustomSeriesOption; |
||||
|
barSeriesOption.type = 'custom'; |
||||
|
const barVisualSettings: BarVisualSettings = { |
||||
|
color: seriesColor, |
||||
|
borderColor: seriesColor, |
||||
|
borderWidth: barSettings.showBorder ? barSettings.borderWidth : 0, |
||||
|
borderRadius: barSettings.borderRadius |
||||
|
}; |
||||
|
if (barSettings.backgroundSettings.type === SeriesFillType.none) { |
||||
|
barVisualSettings.color = seriesColor; |
||||
|
} else if (barSettings.backgroundSettings.type === SeriesFillType.opacity) { |
||||
|
barVisualSettings.color = tinycolor(seriesColor).setAlpha(barSettings.backgroundSettings.opacity).toRgbString(); |
||||
|
} else { |
||||
|
barVisualSettings.color = createLinearOpacityGradient(seriesColor, barSettings.backgroundSettings.gradient); |
||||
|
} |
||||
|
item.barRenderContext.visualSettings = barVisualSettings; |
||||
|
item.barRenderContext.labelOption = label; |
||||
|
barSeriesOption.renderItem = (params, api) => |
||||
|
renderTimeSeriesBar(params, api, item.barRenderContext); |
||||
|
} |
||||
|
} |
||||
|
seriesOption.data = item.data; |
||||
|
return seriesOption; |
||||
|
}; |
||||
|
|
||||
|
const createChartTextStyle = (font: Font, color: string, darkMode: boolean, colorKey?: string): ComponentStyle => { |
||||
|
const style = textStyle(font); |
||||
|
delete style.lineHeight; |
||||
|
style.fontSize = font.size; |
||||
|
style.color = prepareChartThemeColor(color, darkMode, colorKey); |
||||
|
return style; |
||||
|
}; |
||||
|
|
||||
|
const createLinearOpacityGradient = (color: string, gradient: {start: number; end: number}): LinearGradientObject => ({ |
||||
|
type: 'linear', |
||||
|
x: 0, |
||||
|
y: 0, |
||||
|
x2: 0, |
||||
|
y2: 1, |
||||
|
colorStops: [{ |
||||
|
offset: 0, color: tinycolor(color).setAlpha(gradient.start / 100).toRgbString() // color at 0%
|
||||
|
}, { |
||||
|
offset: 1, color: tinycolor(color).setAlpha(gradient.end / 100).toRgbString() // color at 100%
|
||||
|
}], |
||||
|
global: false |
||||
|
}); |
||||
|
|
||||
|
const prepareChartThemeColor = (color: string, darkMode: boolean, colorKey?: string): string => { |
||||
|
if (darkMode) { |
||||
|
let colorInstance = tinycolor(color); |
||||
|
if (colorInstance.isDark()) { |
||||
|
if (colorKey && timeSeriesChartColorScheme[colorKey]) { |
||||
|
return timeSeriesChartColorScheme[colorKey].dark; |
||||
|
} else { |
||||
|
const rgb = colorInstance.toRgb(); |
||||
|
colorInstance = tinycolor({r: 255 - rgb.r, g: 255 - rgb.g, b: 255 - rgb.b, a: rgb.a}); |
||||
|
return colorInstance.toRgbString(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
return color; |
||||
|
}; |
||||
@ -0,0 +1,642 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2024 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 { WidgetContext } from '@home/models/widget-component.models'; |
||||
|
import { |
||||
|
AxisPosition, |
||||
|
calculateThresholdsOffset, |
||||
|
createTimeSeriesXAxisOption, |
||||
|
createTimeSeriesYAxis, |
||||
|
generateChartData, |
||||
|
TimeSeriesChartDataItem, |
||||
|
timeSeriesChartDefaultSettings, |
||||
|
timeSeriesChartKeyDefaultSettings, |
||||
|
TimeSeriesChartKeySettings, |
||||
|
TimeSeriesChartSeriesType, |
||||
|
TimeSeriesChartSettings, |
||||
|
TimeSeriesChartThresholdItem, |
||||
|
TimeSeriesChartThresholdType, |
||||
|
TimeSeriesChartYAxis, |
||||
|
parseThresholdData, |
||||
|
PointLabelPosition, |
||||
|
updateDarkMode |
||||
|
} from '@home/components/widget/lib/chart/time-series-chart.models'; |
||||
|
import { ResizeObserver } from '@juggle/resize-observer'; |
||||
|
import { |
||||
|
calculateXAxisHeight, |
||||
|
calculateYAxisWidth, |
||||
|
ECharts, |
||||
|
echartsModule, |
||||
|
EChartsOption, |
||||
|
echartsTooltipFormatter, |
||||
|
EChartsTooltipTrigger, |
||||
|
getAxisExtent, |
||||
|
getYAxis, |
||||
|
measureXAxisNameHeight, |
||||
|
measureYAxisNameWidth, |
||||
|
toNamedData |
||||
|
} from '@home/components/widget/lib/chart/echarts-widget.models'; |
||||
|
import { DateFormatProcessor } from '@shared/models/widget-settings.models'; |
||||
|
import { isDefinedAndNotNull, mergeDeep } from '@core/utils'; |
||||
|
import { DataKey, Datasource, DatasourceType, widgetType } from '@shared/models/widget.models'; |
||||
|
import * as echarts from 'echarts/core'; |
||||
|
import { CallbackDataParams } from 'echarts/types/dist/shared'; |
||||
|
import { Renderer2 } from '@angular/core'; |
||||
|
import { CustomSeriesOption, LineSeriesOption } from 'echarts/charts'; |
||||
|
import { BehaviorSubject } from 'rxjs'; |
||||
|
import { AggregationType } from '@shared/models/time/time.models'; |
||||
|
import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; |
||||
|
import { WidgetSubscriptionOptions } from '@core/api/widget-api.models'; |
||||
|
|
||||
|
export class TbTimeSeriesChart { |
||||
|
|
||||
|
private readonly shapeResize$: ResizeObserver; |
||||
|
|
||||
|
private dataItems: TimeSeriesChartDataItem[] = []; |
||||
|
private thresholdItems: TimeSeriesChartThresholdItem[] = []; |
||||
|
private yAxisList: TimeSeriesChartYAxis[] = []; |
||||
|
|
||||
|
private timeSeriesChart: ECharts; |
||||
|
private timeSeriesChartOptions: EChartsOption; |
||||
|
|
||||
|
private readonly tooltipDateFormat: DateFormatProcessor; |
||||
|
|
||||
|
private yMinSubject = new BehaviorSubject(-1); |
||||
|
private yMaxSubject = new BehaviorSubject(1); |
||||
|
|
||||
|
private darkMode = false; |
||||
|
|
||||
|
private messageChannel = new BroadcastChannel('tbMessageChannel'); |
||||
|
|
||||
|
private topPointLabels = false; |
||||
|
|
||||
|
private componentIndexCounter = 0; |
||||
|
|
||||
|
private highlightedDataKey: DataKey; |
||||
|
|
||||
|
yMin$ = this.yMinSubject.asObservable(); |
||||
|
yMax$ = this.yMaxSubject.asObservable(); |
||||
|
|
||||
|
constructor(private ctx: WidgetContext, |
||||
|
private readonly settings: TimeSeriesChartSettings, |
||||
|
private chartElement: HTMLElement, |
||||
|
private renderer: Renderer2, |
||||
|
private autoResize = true) { |
||||
|
|
||||
|
this.settings = mergeDeep({} as TimeSeriesChartSettings, timeSeriesChartDefaultSettings, this.settings); |
||||
|
this.darkMode = this.settings.darkMode; |
||||
|
this.setupData(); |
||||
|
this.setupThresholds(); |
||||
|
if (this.settings.showTooltip && this.settings.tooltipShowDate) { |
||||
|
this.tooltipDateFormat = DateFormatProcessor.fromSettings(this.ctx.$injector, this.settings.tooltipDateFormat); |
||||
|
} |
||||
|
this.onResize(); |
||||
|
if (this.autoResize) { |
||||
|
this.shapeResize$ = new ResizeObserver(() => { |
||||
|
this.onResize(); |
||||
|
}); |
||||
|
this.shapeResize$.observe(this.chartElement); |
||||
|
} |
||||
|
this.messageChannel.addEventListener('message', (event) => { |
||||
|
if (event?.data?.type === 'tbDarkMode') { |
||||
|
const darkMode = !!event?.data?.darkMode; |
||||
|
this.setDarkMode(darkMode); |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
public update(): void { |
||||
|
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) : []; |
||||
|
} |
||||
|
this.onResize(); |
||||
|
if (this.timeSeriesChart) { |
||||
|
this.timeSeriesChartOptions.xAxis[0].min = this.ctx.defaultSubscription.timeWindow.minTime; |
||||
|
this.timeSeriesChartOptions.xAxis[0].max = this.ctx.defaultSubscription.timeWindow.maxTime; |
||||
|
this.timeSeriesChartOptions.xAxis[0].tbTimeWindow = this.ctx.defaultSubscription.timeWindow; |
||||
|
if (this.ctx.defaultSubscription.timeWindowConfig?.aggregation?.type === AggregationType.NONE) { |
||||
|
this.timeSeriesChartOptions.tooltip[0].axisPointer.type = 'line'; |
||||
|
} else { |
||||
|
this.timeSeriesChartOptions.tooltip[0].axisPointer.type = 'shadow'; |
||||
|
} |
||||
|
this.updateSeriesData(true); |
||||
|
if (this.highlightedDataKey) { |
||||
|
this.keyEnter(this.highlightedDataKey); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public latestUpdated() { |
||||
|
let update = false; |
||||
|
if (this.ctx.latestData) { |
||||
|
for (const item of this.thresholdItems) { |
||||
|
if (item.settings.type === TimeSeriesChartThresholdType.latestKey && item.latestDataKey) { |
||||
|
const data = this.ctx.latestData.find(d => d.dataKey === item.latestDataKey); |
||||
|
if (data.data[0]) { |
||||
|
item.value = parseThresholdData(data.data[0][1]); |
||||
|
update = true; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
if (this.timeSeriesChart && update) { |
||||
|
this.updateSeriesData(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public keyEnter(dataKey: DataKey): void { |
||||
|
this.highlightedDataKey = dataKey; |
||||
|
const item = this.dataItems.find(d => d.dataKey === dataKey); |
||||
|
if (item) { |
||||
|
this.timeSeriesChart.dispatchAction({ |
||||
|
type: 'highlight', |
||||
|
seriesId: item.id |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public keyLeave(dataKey: DataKey): void { |
||||
|
this.highlightedDataKey = null; |
||||
|
const item = this.dataItems.find(d => d.dataKey === dataKey); |
||||
|
if (item) { |
||||
|
this.timeSeriesChart.dispatchAction({ |
||||
|
type: 'downplay', |
||||
|
seriesId: item.id |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public toggleKey(dataKey: DataKey): void { |
||||
|
const enable = dataKey.hidden; |
||||
|
const dataItem = this.dataItems.find(d => d.dataKey === dataKey); |
||||
|
if (dataItem) { |
||||
|
dataItem.enabled = enable; |
||||
|
if (!enable) { |
||||
|
this.timeSeriesChart.dispatchAction({ |
||||
|
type: 'downplay', |
||||
|
seriesId: dataItem.id |
||||
|
}); |
||||
|
} |
||||
|
this.timeSeriesChartOptions.series = this.updateSeries(); |
||||
|
const mergeList = ['series']; |
||||
|
if (this.updateYAxisScale(this.yAxisList)) { |
||||
|
this.timeSeriesChartOptions.yAxis = this.yAxisList.map(axis => axis.option); |
||||
|
mergeList.push('yAxis'); |
||||
|
} |
||||
|
this.timeSeriesChart.setOption(this.timeSeriesChartOptions, this.settings.stack ? {notMerge: true} : {replaceMerge: mergeList}); |
||||
|
this.updateAxes(); |
||||
|
dataKey.hidden = !enable; |
||||
|
if (enable) { |
||||
|
this.timeSeriesChart.dispatchAction({ |
||||
|
type: 'highlight', |
||||
|
seriesId: dataItem.id |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public destroy(): void { |
||||
|
if (this.shapeResize$) { |
||||
|
this.shapeResize$.disconnect(); |
||||
|
} |
||||
|
if (this.timeSeriesChart) { |
||||
|
this.timeSeriesChart.dispose(); |
||||
|
} |
||||
|
this.yMinSubject.complete(); |
||||
|
this.yMaxSubject.complete(); |
||||
|
this.messageChannel.close(); |
||||
|
} |
||||
|
|
||||
|
public resize(): void { |
||||
|
this.onResize(); |
||||
|
} |
||||
|
|
||||
|
public setDarkMode(darkMode: boolean): void { |
||||
|
if (this.darkMode !== darkMode) { |
||||
|
this.darkMode = darkMode; |
||||
|
if (this.timeSeriesChart) { |
||||
|
this.timeSeriesChartOptions = updateDarkMode(this.timeSeriesChartOptions, this.settings, this.dataItems, |
||||
|
this.thresholdItems, darkMode); |
||||
|
this.timeSeriesChart.setOption(this.timeSeriesChartOptions); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public isDarkMode(): boolean { |
||||
|
return this.darkMode; |
||||
|
} |
||||
|
|
||||
|
private setupData(): void { |
||||
|
if (this.ctx.datasources.length) { |
||||
|
for (const datasource of this.ctx.datasources) { |
||||
|
const dataKeys = datasource.dataKeys; |
||||
|
for (const dataKey of dataKeys) { |
||||
|
const keySettings = mergeDeep<TimeSeriesChartKeySettings>({} as TimeSeriesChartKeySettings, |
||||
|
timeSeriesChartKeyDefaultSettings, dataKey.settings); |
||||
|
if (keySettings.showPointLabel && keySettings.pointLabelPosition === PointLabelPosition.top) { |
||||
|
this.topPointLabels = true; |
||||
|
} |
||||
|
dataKey.settings = keySettings; |
||||
|
const datasourceData = this.ctx.data ? this.ctx.data.find(d => d.dataKey === dataKey) : null; |
||||
|
const namedData = datasourceData?.data ? toNamedData(datasourceData.data) : []; |
||||
|
const units = dataKey.units && dataKey.units.length ? dataKey.units : this.ctx.units; |
||||
|
const decimals = isDefinedAndNotNull(dataKey.decimals) ? dataKey.decimals : |
||||
|
(isDefinedAndNotNull(this.ctx.decimals) ? this.ctx.decimals : 2); |
||||
|
this.dataItems.push({ |
||||
|
id: this.nextComponentId(), |
||||
|
units, |
||||
|
decimals, |
||||
|
yAxisIndex: this.getYAxis(units, decimals), |
||||
|
dataKey, |
||||
|
data: namedData, |
||||
|
enabled: !keySettings.dataHiddenByDefault |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private setupThresholds(): void { |
||||
|
const thresholdDatasources: Datasource[] = []; |
||||
|
for (const threshold of this.settings.thresholds) { |
||||
|
let latestDataKey: DataKey = null; |
||||
|
let entityDataKey: DataKey = null; |
||||
|
let value = null; |
||||
|
if (threshold.type === TimeSeriesChartThresholdType.latestKey) { |
||||
|
if (this.ctx.datasources.length) { |
||||
|
for (const datasource of this.ctx.datasources) { |
||||
|
latestDataKey = datasource.latestDataKeys?.find(d => |
||||
|
(d.type === DataKeyType.function && d.label === threshold.latestKeyName) || |
||||
|
(d.type !== DataKeyType.function && d.name === threshold.latestKeyName)); |
||||
|
if (latestDataKey) { |
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
if (!latestDataKey) { |
||||
|
continue; |
||||
|
} |
||||
|
} else if (threshold.type === TimeSeriesChartThresholdType.entity) { |
||||
|
const entityAliasId = this.ctx.aliasController.getEntityAliasId(threshold.entityAlias); |
||||
|
if (!entityAliasId) { |
||||
|
continue; |
||||
|
} |
||||
|
let datasource = thresholdDatasources.find(d => d.entityAliasId === entityAliasId); |
||||
|
entityDataKey = { |
||||
|
type: threshold.entityKeyType, |
||||
|
name: threshold.entityKey, |
||||
|
label: threshold.entityKey, |
||||
|
settings: {} |
||||
|
}; |
||||
|
if (datasource) { |
||||
|
datasource.dataKeys.push(entityDataKey); |
||||
|
} |
||||
|
datasource = { |
||||
|
type: DatasourceType.entity, |
||||
|
name: threshold.entityAlias, |
||||
|
aliasName: threshold.entityAlias, |
||||
|
entityAliasId, |
||||
|
dataKeys: [ entityDataKey ] |
||||
|
}; |
||||
|
thresholdDatasources.push(datasource); |
||||
|
} else { // constant
|
||||
|
value = threshold.value; |
||||
|
} |
||||
|
const units = threshold.units && threshold.units.length ? threshold.units : this.ctx.units; |
||||
|
const decimals = isDefinedAndNotNull(threshold.decimals) ? threshold.decimals : |
||||
|
(isDefinedAndNotNull(this.ctx.decimals) ? this.ctx.decimals : 2); |
||||
|
const thresholdItem: TimeSeriesChartThresholdItem = { |
||||
|
id: this.nextComponentId(), |
||||
|
units, |
||||
|
decimals, |
||||
|
yAxisIndex: this.getYAxis(units, decimals), |
||||
|
value, |
||||
|
latestDataKey, |
||||
|
settings: threshold |
||||
|
}; |
||||
|
if (entityDataKey) { |
||||
|
entityDataKey.settings.thresholdItemId = thresholdItem.id; |
||||
|
} |
||||
|
this.thresholdItems.push(thresholdItem); |
||||
|
} |
||||
|
this.subscribeForEntityThresholds(thresholdDatasources); |
||||
|
} |
||||
|
|
||||
|
private nextComponentId(): string { |
||||
|
return (this.componentIndexCounter++) + ''; |
||||
|
} |
||||
|
|
||||
|
private getYAxis(units: string, decimals: number): number { |
||||
|
let yAxisIndex = this.yAxisList.findIndex(axis => axis.units === units); |
||||
|
if (yAxisIndex === -1) { |
||||
|
const yAxisId = this.yAxisList.length + ''; |
||||
|
const yAxis = createTimeSeriesYAxis(yAxisId, units, decimals, this.settings.yAxis, this.darkMode); |
||||
|
this.yAxisList.push(yAxis); |
||||
|
yAxisIndex = this.yAxisList.length - 1; |
||||
|
} |
||||
|
return yAxisIndex; |
||||
|
} |
||||
|
|
||||
|
private subscribeForEntityThresholds(datasources: Datasource[]) { |
||||
|
if (datasources.length) { |
||||
|
const thresholdsSourcesSubscriptionOptions: WidgetSubscriptionOptions = { |
||||
|
datasources, |
||||
|
useDashboardTimewindow: false, |
||||
|
type: widgetType.latest, |
||||
|
callbacks: { |
||||
|
onDataUpdated: (subscription) => { |
||||
|
let update = false; |
||||
|
if (subscription.data) { |
||||
|
for (const item of this.thresholdItems) { |
||||
|
if (item.settings.type === TimeSeriesChartThresholdType.entity) { |
||||
|
const data = subscription.data.find(d => d.dataKey.settings?.thresholdItemId === item.id); |
||||
|
if (data.data[0]) { |
||||
|
item.value = parseThresholdData(data.data[0][1]); |
||||
|
update = true; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
if (this.timeSeriesChart && update) { |
||||
|
this.updateSeriesData(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
}; |
||||
|
this.ctx.subscriptionApi.createSubscription(thresholdsSourcesSubscriptionOptions, true).subscribe(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private drawChart() { |
||||
|
echartsModule.init(); |
||||
|
this.timeSeriesChart = echarts.init(this.chartElement, null, { |
||||
|
renderer: 'canvas' |
||||
|
}); |
||||
|
const noAggregation = this.ctx.defaultSubscription.timeWindowConfig?.aggregation?.type === AggregationType.NONE; |
||||
|
this.timeSeriesChartOptions = { |
||||
|
darkMode: this.darkMode, |
||||
|
backgroundColor: 'transparent', |
||||
|
tooltip: [{ |
||||
|
trigger: this.settings.tooltipTrigger === EChartsTooltipTrigger.axis ? 'axis' : 'item', |
||||
|
confine: true, |
||||
|
appendToBody: true, |
||||
|
axisPointer: { |
||||
|
type: noAggregation ? 'line' : 'shadow' |
||||
|
}, |
||||
|
formatter: (params: CallbackDataParams[]) => |
||||
|
this.settings.showTooltip ? echartsTooltipFormatter(this.renderer, this.tooltipDateFormat, |
||||
|
this.settings, params, 0, '', -1, this.dataItems) : undefined, |
||||
|
padding: [8, 12], |
||||
|
backgroundColor: this.settings.tooltipBackgroundColor, |
||||
|
borderWidth: 0, |
||||
|
extraCssText: `line-height: 1; backdrop-filter: blur(${this.settings.tooltipBackgroundBlur}px);` |
||||
|
}], |
||||
|
grid: [{ |
||||
|
top: this.minTopOffset(), |
||||
|
left: this.settings.dataZoom ? 5 : 0, |
||||
|
right: this.settings.dataZoom ? 5 : 0, |
||||
|
bottom: this.minBottomOffset() |
||||
|
}], |
||||
|
xAxis: [ |
||||
|
createTimeSeriesXAxisOption(this.settings.xAxis, this.ctx.defaultSubscription.timeWindow.minTime, |
||||
|
this.ctx.defaultSubscription.timeWindow.maxTime, this.darkMode) |
||||
|
], |
||||
|
yAxis: this.yAxisList.map(axis => axis.option), |
||||
|
dataZoom: [ |
||||
|
{ |
||||
|
type: 'inside', |
||||
|
disabled: !this.settings.dataZoom, |
||||
|
realtime: true |
||||
|
}, |
||||
|
{ |
||||
|
type: 'slider', |
||||
|
show: this.settings.dataZoom, |
||||
|
showDetail: false, |
||||
|
realtime: true, |
||||
|
bottom: 10 |
||||
|
} |
||||
|
] |
||||
|
}; |
||||
|
|
||||
|
this.timeSeriesChartOptions.xAxis[0].tbTimeWindow = this.ctx.defaultSubscription.timeWindow; |
||||
|
|
||||
|
this.timeSeriesChartOptions.series = this.updateSeries(); |
||||
|
if (this.updateYAxisScale(this.yAxisList)) { |
||||
|
this.timeSeriesChartOptions.yAxis = this.yAxisList.map(axis => axis.option); |
||||
|
} |
||||
|
|
||||
|
this.timeSeriesChart.setOption(this.timeSeriesChartOptions); |
||||
|
this.updateAxes(); |
||||
|
|
||||
|
if (this.settings.dataZoom) { |
||||
|
this.timeSeriesChart.on('datazoom', () => { |
||||
|
this.updateAxes(); |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private updateSeriesData(updateScale = false): void { |
||||
|
this.timeSeriesChartOptions.series = this.updateSeries(); |
||||
|
if (updateScale && this.updateYAxisScale(this.yAxisList)) { |
||||
|
this.timeSeriesChartOptions.yAxis = this.yAxisList.map(axis => axis.option); |
||||
|
} |
||||
|
this.timeSeriesChart.setOption(this.timeSeriesChartOptions); |
||||
|
this.updateAxes(); |
||||
|
} |
||||
|
|
||||
|
private updateSeries(): Array<LineSeriesOption | CustomSeriesOption> { |
||||
|
return generateChartData(this.dataItems, this.thresholdItems, |
||||
|
this.ctx.timeWindow.interval, this.settings.stack, this.darkMode); |
||||
|
} |
||||
|
|
||||
|
private updateAxes() { |
||||
|
const leftAxisList = this.yAxisList.filter(axis => axis.option.position === 'left'); |
||||
|
let res = this.updateYAxisOffset(leftAxisList); |
||||
|
let leftOffset = res.offset + (!res.offset && this.settings.dataZoom ? 5 : 0); |
||||
|
let changed = res.changed; |
||||
|
const rightAxisList = this.yAxisList.filter(axis => axis.option.position === 'right'); |
||||
|
res = this.updateYAxisOffset(rightAxisList); |
||||
|
let rightOffset = res.offset + (!res.offset && this.settings.dataZoom ? 5 : 0); |
||||
|
changed = changed || res.changed; |
||||
|
let bottomOffset = this.minBottomOffset(); |
||||
|
const minTopOffset = this.minTopOffset(); |
||||
|
let topOffset = minTopOffset; |
||||
|
if (this.timeSeriesChartOptions.xAxis[0].show) { |
||||
|
const xAxisHeight = calculateXAxisHeight(this.timeSeriesChart); |
||||
|
if (this.timeSeriesChartOptions.xAxis[0].position === AxisPosition.bottom) { |
||||
|
bottomOffset += xAxisHeight; |
||||
|
} else { |
||||
|
topOffset = Math.max(minTopOffset, xAxisHeight); |
||||
|
} |
||||
|
if (this.settings.xAxis.label) { |
||||
|
const nameHeight = measureXAxisNameHeight(this.timeSeriesChart, this.timeSeriesChartOptions.xAxis[0].name); |
||||
|
if (this.timeSeriesChartOptions.xAxis[0].position === AxisPosition.bottom) { |
||||
|
bottomOffset += nameHeight; |
||||
|
} else { |
||||
|
topOffset = Math.max(minTopOffset, xAxisHeight + nameHeight); |
||||
|
} |
||||
|
const nameGap = xAxisHeight; |
||||
|
if (this.timeSeriesChartOptions.xAxis[0].nameGap !== nameGap) { |
||||
|
this.timeSeriesChartOptions.xAxis[0].nameGap = nameGap; |
||||
|
changed = true; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
const thresholdsOffset = calculateThresholdsOffset(this.timeSeriesChart, this.thresholdItems, this.yAxisList); |
||||
|
leftOffset = Math.max(leftOffset, thresholdsOffset[0]); |
||||
|
rightOffset = Math.max(rightOffset, thresholdsOffset[1]); |
||||
|
|
||||
|
if (this.timeSeriesChartOptions.grid[0].left !== leftOffset || |
||||
|
this.timeSeriesChartOptions.grid[0].right !== rightOffset || |
||||
|
this.timeSeriesChartOptions.grid[0].bottom !== bottomOffset || |
||||
|
this.timeSeriesChartOptions.grid[0].top !== topOffset) { |
||||
|
this.timeSeriesChartOptions.grid[0].left = leftOffset; |
||||
|
this.timeSeriesChartOptions.grid[0].right = rightOffset; |
||||
|
this.timeSeriesChartOptions.grid[0].bottom = bottomOffset; |
||||
|
this.timeSeriesChartOptions.grid[0].top = topOffset; |
||||
|
changed = true; |
||||
|
} |
||||
|
if (changed) { |
||||
|
this.timeSeriesChartOptions.yAxis = this.yAxisList.map(axis => axis.option); |
||||
|
this.timeSeriesChart.setOption(this.timeSeriesChartOptions, {replaceMerge: ['yAxis', 'xAxis', 'grid'], lazyUpdate: true}); |
||||
|
} |
||||
|
changed = this.calculateYAxisInterval(this.yAxisList); |
||||
|
if (changed) { |
||||
|
this.timeSeriesChartOptions.yAxis = this.yAxisList.map(axis => axis.option); |
||||
|
this.timeSeriesChart.setOption(this.timeSeriesChartOptions, {replaceMerge: ['yAxis'], lazyUpdate: true}); |
||||
|
} |
||||
|
if (this.yAxisList.length) { |
||||
|
const extent = getAxisExtent(this.timeSeriesChart, this.yAxisList[0].id); |
||||
|
const min = extent[0]; |
||||
|
const max = extent[1]; |
||||
|
if (this.yMinSubject.value !== min) { |
||||
|
this.yMinSubject.next(min); |
||||
|
} |
||||
|
if (this.yMaxSubject.value !== max) { |
||||
|
this.yMaxSubject.next(max); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private updateYAxisScale(axisList: TimeSeriesChartYAxis[]): boolean { |
||||
|
let changed = false; |
||||
|
for (const yAxis of axisList) { |
||||
|
const scaleYAxis = this.scaleYAxis(yAxis); |
||||
|
if (yAxis.option.scale !== scaleYAxis) { |
||||
|
yAxis.option.scale = scaleYAxis; |
||||
|
changed = true; |
||||
|
} |
||||
|
} |
||||
|
return changed; |
||||
|
} |
||||
|
|
||||
|
private updateYAxisOffset(axisList: TimeSeriesChartYAxis[]): {offset: number; changed: boolean} { |
||||
|
const result = {offset: 0, changed: false}; |
||||
|
let width = 0; |
||||
|
for (const yAxis of axisList) { |
||||
|
const newWidth = calculateYAxisWidth(this.timeSeriesChart, yAxis.id); |
||||
|
if (width && newWidth) { |
||||
|
result.offset += 5; |
||||
|
} |
||||
|
width = newWidth; |
||||
|
const showLine = !!width && this.settings.yAxis.showLine; |
||||
|
if (yAxis.option.axisLine.show !== showLine) { |
||||
|
yAxis.option.axisLine.show = showLine; |
||||
|
result.changed = true; |
||||
|
} |
||||
|
if (yAxis.option.offset !== result.offset) { |
||||
|
yAxis.option.offset = result.offset; |
||||
|
result.changed = true; |
||||
|
} |
||||
|
if (this.settings.yAxis.label) { |
||||
|
if (!width) { |
||||
|
if (yAxis.option.name) { |
||||
|
yAxis.option.name = null; |
||||
|
result.changed = true; |
||||
|
} |
||||
|
} else { |
||||
|
if (!yAxis.option.name) { |
||||
|
yAxis.option.name = this.settings.yAxis.label; |
||||
|
result.changed = true; |
||||
|
} |
||||
|
const nameGap = width; |
||||
|
if (yAxis.option.nameGap !== nameGap) { |
||||
|
yAxis.option.nameGap = nameGap; |
||||
|
result.changed = true; |
||||
|
} |
||||
|
const nameWidth = measureYAxisNameWidth(this.timeSeriesChart, yAxis.id, this.settings.yAxis.label); |
||||
|
result.offset += nameWidth; |
||||
|
} |
||||
|
} |
||||
|
result.offset += width; |
||||
|
} |
||||
|
return result; |
||||
|
} |
||||
|
|
||||
|
private scaleYAxis(yAxis: TimeSeriesChartYAxis): boolean { |
||||
|
const yAxisIndex = this.yAxisList.indexOf(yAxis); |
||||
|
const axisBarDataItems = this.dataItems.filter(d => d.yAxisIndex === yAxisIndex && d.enabled && |
||||
|
d.data.length && d.dataKey.settings.type === TimeSeriesChartSeriesType.bar); |
||||
|
return !axisBarDataItems.length; |
||||
|
} |
||||
|
|
||||
|
private calculateYAxisInterval(axisList: TimeSeriesChartYAxis[]): boolean { |
||||
|
let changed = false; |
||||
|
for (const yAxis of axisList) { |
||||
|
if (yAxis.intervalCalculator) { |
||||
|
const axis = getYAxis(this.timeSeriesChart, yAxis.id); |
||||
|
if (axis) { |
||||
|
try { |
||||
|
const interval = yAxis.intervalCalculator(axis); |
||||
|
if ((yAxis.option as any).interval !== interval) { |
||||
|
(yAxis.option as any).interval = interval; |
||||
|
changed = true; |
||||
|
} |
||||
|
} catch (_e) {} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
return changed; |
||||
|
} |
||||
|
|
||||
|
private minTopOffset(): number { |
||||
|
return (this.topPointLabels) ? 20 : |
||||
|
((this.settings.yAxis.show && this.settings.yAxis.showTickLabels) ? 10 : 5); |
||||
|
} |
||||
|
|
||||
|
private minBottomOffset(): number { |
||||
|
return this.settings.dataZoom ? 45 : 5; |
||||
|
} |
||||
|
|
||||
|
private onResize() { |
||||
|
const shapeWidth = this.chartElement.offsetWidth; |
||||
|
const shapeHeight = this.chartElement.offsetHeight; |
||||
|
if (shapeWidth && shapeHeight) { |
||||
|
if (!this.timeSeriesChart) { |
||||
|
this.drawChart(); |
||||
|
} else { |
||||
|
const width = this.timeSeriesChart.getWidth(); |
||||
|
const height = this.timeSeriesChart.getHeight(); |
||||
|
if (width !== shapeWidth || height !== shapeHeight) { |
||||
|
this.timeSeriesChart.resize(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,24 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2024 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.
|
||||
|
///
|
||||
|
|
||||
|
export interface TbColor { |
||||
|
light: string; |
||||
|
dark: string; |
||||
|
} |
||||
|
|
||||
|
export interface TbColorScheme { |
||||
|
[key: string]: TbColor; |
||||
|
} |
||||
Loading…
Reference in new issue