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