8 changed files with 833 additions and 5 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -0,0 +1,36 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
Copyright © 2016-2023 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-bar-chart-panel" [style]="backgroundStyle$ | async"> |
||||
|
<div class="tb-bar-chart-overlay" [style]="overlayStyle"></div> |
||||
|
<ng-container *ngTemplateOutlet="widgetTitlePanel"></ng-container> |
||||
|
<div class="tb-bar-chart-content" [class]="legendClass"> |
||||
|
<div #chartShape class="tb-bar-chart-shape"> |
||||
|
</div> |
||||
|
<div *ngIf="showLegend" class="tb-bar-chart-legend"> |
||||
|
<div class="tb-bar-chart-legend-item" *ngFor="let legendItem of legendItems" |
||||
|
(mouseenter)="onLegendItemEnter(legendItem)" |
||||
|
(mouseleave)="onLegendItemLeave(legendItem)" |
||||
|
(click)="toggleLegendItem(legendItem)"> |
||||
|
<div class="tb-bar-chart-legend-item-label"> |
||||
|
<div class="tb-bar-chart-legend-item-label-circle" [style]="{background: legendItem.enabled ? legendItem.color : null}"></div> |
||||
|
<div [style]="legendItem.enabled ? legendLabelStyle : disabledLegendLabelStyle">{{ legendItem.label }}</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
@ -0,0 +1,105 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2023 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. |
||||
|
*/ |
||||
|
|
||||
|
.tb-bar-chart-panel { |
||||
|
width: 100%; |
||||
|
height: 100%; |
||||
|
position: relative; |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
gap: 16px; |
||||
|
padding: 20px 24px 24px 24px; |
||||
|
> div:not(.tb-bar-chart-overlay) { |
||||
|
z-index: 1; |
||||
|
} |
||||
|
.tb-bar-chart-overlay { |
||||
|
position: absolute; |
||||
|
top: 12px; |
||||
|
left: 12px; |
||||
|
bottom: 12px; |
||||
|
right: 12px; |
||||
|
} |
||||
|
div.tb-widget-title { |
||||
|
padding: 0; |
||||
|
} |
||||
|
.tb-bar-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-bar-chart-shape { |
||||
|
flex: 1; |
||||
|
min-width: 0; |
||||
|
min-height: 0; |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
justify-content: center; |
||||
|
} |
||||
|
.tb-bar-chart-legend { |
||||
|
display: flex; |
||||
|
justify-content: center; |
||||
|
align-items: center; |
||||
|
align-self: stretch; |
||||
|
flex-wrap: wrap; |
||||
|
column-gap: 24px; |
||||
|
row-gap: 8px; |
||||
|
.tb-bar-chart-legend-item { |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
justify-content: center; |
||||
|
align-items: flex-start; |
||||
|
user-select: none; |
||||
|
cursor: pointer; |
||||
|
.tb-bar-chart-legend-item-label { |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
gap: 4px; |
||||
|
color: #ccc; |
||||
|
.tb-bar-chart-legend-item-label-circle { |
||||
|
width: 8px; |
||||
|
height: 8px; |
||||
|
border-radius: 50%; |
||||
|
background-color: #ccc; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
&.legend-right, &.legend-left { |
||||
|
gap: 24px; |
||||
|
.tb-bar-chart-legend { |
||||
|
flex-direction: column-reverse; |
||||
|
justify-content: flex-end; |
||||
|
align-items: stretch; |
||||
|
.tb-bar-chart-legend-item { |
||||
|
flex-direction: row; |
||||
|
justify-content: space-between; |
||||
|
align-items: center; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,540 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2023 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 { WidgetContext } from '@home/models/widget-component.models'; |
||||
|
import { |
||||
|
backgroundStyle, |
||||
|
ComponentStyle, |
||||
|
DateFormatProcessor, |
||||
|
overlayStyle, |
||||
|
textStyle |
||||
|
} from '@shared/models/widget-settings.models'; |
||||
|
import { ResizeObserver } from '@juggle/resize-observer'; |
||||
|
import * as echarts from 'echarts/core'; |
||||
|
import { Axis } from 'echarts'; |
||||
|
import { formatValue } from '@core/utils'; |
||||
|
import { GridComponent, GridComponentOption, TooltipComponent, TooltipComponentOption } from 'echarts/components'; |
||||
|
import { LabelLayout } from 'echarts/features'; |
||||
|
import { BarChart, BarSeriesOption} from 'echarts/charts'; |
||||
|
import { CanvasRenderer } from 'echarts/renderers'; |
||||
|
import { DataKey, DataSet } from '@shared/models/widget.models'; |
||||
|
import { Observable } from 'rxjs'; |
||||
|
import { ImagePipe } from '@shared/pipe/image.pipe'; |
||||
|
import { DomSanitizer } from '@angular/platform-browser'; |
||||
|
import { |
||||
|
barChartWithLabelsDefaultSettings, |
||||
|
BarChartWithLabelsWidgetSettings |
||||
|
} from '@home/components/widget/lib/chart/bar-chart-with-labels-widget.models'; |
||||
|
import { LabelLayoutOptionCallback } from 'echarts/types/dist/shared'; |
||||
|
import BarSeriesModel from 'echarts/types/src/chart/bar/BarSeries'; |
||||
|
|
||||
|
const axisGetBandWidth = Axis.prototype.getBandWidth; |
||||
|
|
||||
|
Axis.prototype.getBandWidth = function(){ |
||||
|
const series: any[] = this.model?.parentModel?.getSeries(); |
||||
|
if (this.scale.type === 'time' && series?.length && series[0].type === 'series.bar') { |
||||
|
const barWidth = (series[0] as BarSeriesModel).getData().getLayout('size'); |
||||
|
return barWidth * (series.length + 1); |
||||
|
} else { |
||||
|
return axisGetBandWidth.call(this); |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
echarts.use([ |
||||
|
TooltipComponent, |
||||
|
GridComponent, |
||||
|
BarChart, |
||||
|
LabelLayout, |
||||
|
CanvasRenderer |
||||
|
]); |
||||
|
|
||||
|
type EChartsOption = echarts.ComposeOption< |
||||
|
| TooltipComponentOption |
||||
|
| GridComponentOption |
||||
|
| BarSeriesOption |
||||
|
>; |
||||
|
|
||||
|
type ECharts = echarts.ECharts; |
||||
|
|
||||
|
type NamedDataSet = {name: string; value: [number, any]}[]; |
||||
|
|
||||
|
type BarLabelOption = NonNullable<BarSeriesOption['label']>; |
||||
|
|
||||
|
interface BarChartDataItem { |
||||
|
id: string; |
||||
|
dataKey: DataKey; |
||||
|
data: NamedDataSet; |
||||
|
enabled: boolean; |
||||
|
} |
||||
|
|
||||
|
interface BarChartLegendItem { |
||||
|
id: string; |
||||
|
color: string; |
||||
|
label: string; |
||||
|
enabled: boolean; |
||||
|
} |
||||
|
|
||||
|
const toNamedData = (data: DataSet): NamedDataSet => { |
||||
|
if (!data?.length) { |
||||
|
return []; |
||||
|
} else { |
||||
|
return data.map(d => ({ |
||||
|
name: d[0] + '', |
||||
|
value: d |
||||
|
})); |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-bar-chart-with-labels-widget', |
||||
|
templateUrl: './bar-chart-with-labels-widget.component.html', |
||||
|
styleUrls: ['./bar-chart-with-labels-widget.component.scss'], |
||||
|
encapsulation: ViewEncapsulation.None |
||||
|
}) |
||||
|
export class BarChartWithLabelsWidgetComponent implements OnInit, OnDestroy, AfterViewInit { |
||||
|
|
||||
|
@ViewChild('chartShape', {static: false}) |
||||
|
chartShape: ElementRef<HTMLElement>; |
||||
|
|
||||
|
settings: BarChartWithLabelsWidgetSettings; |
||||
|
|
||||
|
@Input() |
||||
|
ctx: WidgetContext; |
||||
|
|
||||
|
@Input() |
||||
|
widgetTitlePanel: TemplateRef<any>; |
||||
|
|
||||
|
showLegend: boolean; |
||||
|
legendClass: string; |
||||
|
|
||||
|
backgroundStyle$: Observable<ComponentStyle>; |
||||
|
overlayStyle: ComponentStyle = {}; |
||||
|
|
||||
|
legendItems: BarChartLegendItem[]; |
||||
|
legendLabelStyle: ComponentStyle; |
||||
|
disabledLegendLabelStyle: ComponentStyle; |
||||
|
|
||||
|
private shapeResize$: ResizeObserver; |
||||
|
|
||||
|
private decimals = 0; |
||||
|
private units = ''; |
||||
|
|
||||
|
private dataItems: BarChartDataItem[] = []; |
||||
|
|
||||
|
private drawChartPending = false; |
||||
|
private barChart: ECharts; |
||||
|
private barChartOptions: EChartsOption; |
||||
|
|
||||
|
private tooltipDateFormat: DateFormatProcessor; |
||||
|
|
||||
|
private barLabelOptions: BarLabelOption; |
||||
|
private barLabelLayoutCallback: LabelLayoutOptionCallback; |
||||
|
|
||||
|
constructor(private imagePipe: ImagePipe, |
||||
|
private sanitizer: DomSanitizer, |
||||
|
private renderer: Renderer2, |
||||
|
private cd: ChangeDetectorRef) { |
||||
|
} |
||||
|
|
||||
|
ngOnInit(): void { |
||||
|
this.ctx.$scope.barChartWidget = this; |
||||
|
this.settings = {...barChartWithLabelsDefaultSettings, ...this.ctx.settings}; |
||||
|
|
||||
|
this.decimals = this.ctx.decimals; |
||||
|
this.units = this.ctx.units; |
||||
|
|
||||
|
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.legendItems = []; |
||||
|
this.legendClass = `legend-${this.settings.legendPosition}`; |
||||
|
this.legendLabelStyle = textStyle(this.settings.legendLabelFont); |
||||
|
this.disabledLegendLabelStyle = textStyle(this.settings.legendLabelFont); |
||||
|
this.legendLabelStyle.color = this.settings.legendLabelColor; |
||||
|
} |
||||
|
let counter = 0; |
||||
|
if (this.ctx.datasources.length) { |
||||
|
for (const datasource of this.ctx.datasources) { |
||||
|
const dataKeys = datasource.dataKeys; |
||||
|
for (const dataKey of dataKeys) { |
||||
|
const id = counter++; |
||||
|
const datasourceData = this.ctx.data ? this.ctx.data.find(d => d.dataKey === dataKey) : null; |
||||
|
const namedData = datasourceData?.data ? toNamedData(datasourceData.data) : []; |
||||
|
this.dataItems.push({ |
||||
|
id: id+'', |
||||
|
dataKey, |
||||
|
data: namedData, |
||||
|
enabled: true |
||||
|
}); |
||||
|
if (this.showLegend) { |
||||
|
this.legendItems.push( |
||||
|
{ |
||||
|
id: id+'', |
||||
|
label: dataKey.label, |
||||
|
color: dataKey.color, |
||||
|
enabled: true |
||||
|
} |
||||
|
); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (this.settings.showTooltip && this.settings.tooltipShowDate) { |
||||
|
this.tooltipDateFormat = DateFormatProcessor.fromSettings(this.ctx.$injector, this.settings.tooltipDateFormat); |
||||
|
} |
||||
|
|
||||
|
const barValueStyle: ComponentStyle = textStyle(this.settings.barValueFont); |
||||
|
delete barValueStyle.lineHeight; |
||||
|
barValueStyle.fontSize = this.settings.barValueFont.size; |
||||
|
barValueStyle.color = this.settings.barValueColor; |
||||
|
|
||||
|
const barLabelStyle: ComponentStyle = textStyle(this.settings.barLabelFont); |
||||
|
delete barLabelStyle.lineHeight; |
||||
|
barLabelStyle.fontSize = this.settings.barLabelFont.size; |
||||
|
barLabelStyle.color = this.settings.barLabelColor; |
||||
|
|
||||
|
this.barLabelOptions = { |
||||
|
show: this.settings.showBarLabel || this.settings.showBarValue, |
||||
|
position: 'insideBottom', |
||||
|
distance: 15, |
||||
|
align: 'left', |
||||
|
verticalAlign: 'middle', |
||||
|
rotate: 90, |
||||
|
formatter: (params) => { |
||||
|
const parts: string[] = []; |
||||
|
if (this.settings.showBarValue) { |
||||
|
const value = formatValue(params.value[1], this.decimals, '', false); |
||||
|
parts.push(`{value|${value}}`); |
||||
|
} |
||||
|
if (this.settings.showBarLabel) { |
||||
|
parts.push(`{label|${params.seriesName}}`); |
||||
|
} |
||||
|
return parts.join(' '); |
||||
|
}, |
||||
|
rich: { |
||||
|
value: barValueStyle, |
||||
|
label: barLabelStyle |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
this.barLabelLayoutCallback = (params) => { |
||||
|
if (params.rect.width - params.labelRect.width < 2) { |
||||
|
return { |
||||
|
y: '-100%' |
||||
|
}; |
||||
|
} else { |
||||
|
return { |
||||
|
hideOverlap: true |
||||
|
}; |
||||
|
} |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
ngAfterViewInit() { |
||||
|
if (this.drawChartPending) { |
||||
|
this.drawChart(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
ngOnDestroy() { |
||||
|
if (this.shapeResize$) { |
||||
|
this.shapeResize$.disconnect(); |
||||
|
} |
||||
|
if (this.barChart) { |
||||
|
this.barChart.dispose(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public onInit() { |
||||
|
const borderRadius = this.ctx.$widgetElement.css('borderRadius'); |
||||
|
this.overlayStyle = {...this.overlayStyle, ...{borderRadius}}; |
||||
|
if (this.chartShape) { |
||||
|
this.drawChart(); |
||||
|
} else { |
||||
|
this.drawChartPending = true; |
||||
|
} |
||||
|
this.cd.detectChanges(); |
||||
|
} |
||||
|
|
||||
|
public onDataUpdated() { |
||||
|
let minTime = this.ctx.defaultSubscription.timeWindow.minTime; |
||||
|
for (const item of this.dataItems) { |
||||
|
const datasourceData = this.ctx.data ? this.ctx.data.find(d => d.dataKey === item.dataKey) : null; |
||||
|
const namedData = datasourceData?.data ? toNamedData(datasourceData.data) : []; |
||||
|
item.data = namedData; |
||||
|
if (datasourceData.data.length) { |
||||
|
minTime = Math.min(datasourceData.data[0][0], minTime); |
||||
|
} |
||||
|
} |
||||
|
if (this.barChart) { |
||||
|
(this.barChartOptions.xAxis as any).min = minTime; |
||||
|
(this.barChartOptions.xAxis as any).max = this.ctx.defaultSubscription.timeWindow.maxTime; |
||||
|
this.barChartOptions.series = this.updateSeries(); |
||||
|
this.barChart.setOption(this.barChartOptions); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private updateSeries(): Array<BarSeriesOption> { |
||||
|
const series: Array<BarSeriesOption> = []; |
||||
|
for (const item of this.dataItems) { |
||||
|
if (item.enabled) { |
||||
|
const seriesOption: BarSeriesOption = { |
||||
|
type: 'bar', |
||||
|
barGap: 0, |
||||
|
barMaxWidth: 40, |
||||
|
id: item.id, |
||||
|
name: item.dataKey.label, |
||||
|
label: this.barLabelOptions, |
||||
|
color: item.dataKey.color, |
||||
|
data: item.data, |
||||
|
emphasis: { |
||||
|
focus: 'series' |
||||
|
}, |
||||
|
labelLayout: this.barLabelLayoutCallback |
||||
|
}; |
||||
|
series.push(seriesOption); |
||||
|
} |
||||
|
} |
||||
|
return series; |
||||
|
} |
||||
|
|
||||
|
public onLegendItemEnter(item: BarChartLegendItem) { |
||||
|
this.barChart.dispatchAction({ |
||||
|
type: 'highlight', |
||||
|
seriesId: item.id |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
public onLegendItemLeave(item: BarChartLegendItem) { |
||||
|
this.barChart.dispatchAction({ |
||||
|
type: 'downplay', |
||||
|
seriesId: item.id |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
public toggleLegendItem(item: BarChartLegendItem) { |
||||
|
const enable = !item.enabled; |
||||
|
const dataItem = this.dataItems.find(d => d.id === item.id); |
||||
|
if (dataItem) { |
||||
|
dataItem.enabled = enable; |
||||
|
this.barChartOptions.series = this.updateSeries(); |
||||
|
this.barChart.setOption(this.barChartOptions, true); |
||||
|
item.enabled = enable; |
||||
|
if (enable) { |
||||
|
this.barChart.dispatchAction({ |
||||
|
type: 'highlight', |
||||
|
seriesId: item.id |
||||
|
}); |
||||
|
} else { |
||||
|
for (const otherItem of this.legendItems.filter(i => i.id !== item.id)) { |
||||
|
this.barChart.dispatchAction({ |
||||
|
type: 'highlight', |
||||
|
seriesId: otherItem.id |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private drawChart() { |
||||
|
this.barChart = echarts.init(this.chartShape.nativeElement, null, { |
||||
|
renderer: 'canvas', |
||||
|
}); |
||||
|
this.barChartOptions = { |
||||
|
tooltip: { |
||||
|
trigger: 'none' |
||||
|
}, |
||||
|
grid: { |
||||
|
containLabel: true, |
||||
|
top: '30', |
||||
|
left: 0, |
||||
|
right: 0, |
||||
|
bottom: 0 |
||||
|
}, |
||||
|
xAxis: { |
||||
|
type: 'time', |
||||
|
axisTick: { |
||||
|
show: false |
||||
|
}, |
||||
|
axisLabel: { |
||||
|
hideOverlap: true, |
||||
|
fontSize: 10 |
||||
|
}, |
||||
|
axisLine: { |
||||
|
onZero: false |
||||
|
}, |
||||
|
min: this.ctx.defaultSubscription.timeWindow.minTime, |
||||
|
max: this.ctx.defaultSubscription.timeWindow.maxTime |
||||
|
}, |
||||
|
yAxis: { |
||||
|
type: 'value', |
||||
|
axisLabel: { |
||||
|
formatter: value => formatValue(value, this.decimals, this.units, false) |
||||
|
} |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
this.barChartOptions.series = this.updateSeries(); |
||||
|
|
||||
|
if (this.settings.showTooltip) { |
||||
|
this.barChartOptions.tooltip = { |
||||
|
trigger: 'axis', |
||||
|
axisPointer: { |
||||
|
type: 'shadow' |
||||
|
}, |
||||
|
formatter: (params: any[]) => { |
||||
|
if (!params.length || !params[0]) { |
||||
|
return null; |
||||
|
} |
||||
|
const tooltipElement: HTMLElement = this.renderer.createElement('div'); |
||||
|
this.renderer.setStyle(tooltipElement, 'display', 'flex'); |
||||
|
this.renderer.setStyle(tooltipElement, 'flex-direction', 'column'); |
||||
|
this.renderer.setStyle(tooltipElement, 'align-items', 'flex-start'); |
||||
|
this.renderer.setStyle(tooltipElement, 'gap', '4px'); |
||||
|
if (this.settings.tooltipShowDate) { |
||||
|
const dateElement: HTMLElement = this.renderer.createElement('div'); |
||||
|
const ts = params[0].value[0]; |
||||
|
this.tooltipDateFormat.update(ts); |
||||
|
this.renderer.appendChild(dateElement, this.renderer.createText(this.tooltipDateFormat.formatted)); |
||||
|
this.renderer.setStyle(dateElement, 'font-family', this.settings.tooltipDateFont.family); |
||||
|
this.renderer.setStyle(dateElement, 'font-size', this.settings.tooltipDateFont.size + this.settings.tooltipDateFont.sizeUnit); |
||||
|
this.renderer.setStyle(dateElement, 'font-style', this.settings.tooltipDateFont.style); |
||||
|
this.renderer.setStyle(dateElement, 'font-weight', this.settings.tooltipDateFont.weight); |
||||
|
this.renderer.setStyle(dateElement, 'line-height', this.settings.tooltipDateFont.lineHeight); |
||||
|
this.renderer.setStyle(dateElement, 'color', this.settings.tooltipDateColor); |
||||
|
this.renderer.appendChild(tooltipElement, dateElement); |
||||
|
} |
||||
|
let seriesParams = null; |
||||
|
const focusedSeriesIndex = this.focusedSeriesIndex(); |
||||
|
if (focusedSeriesIndex > -1) { |
||||
|
seriesParams = params.find(param => param.seriesIndex === focusedSeriesIndex); |
||||
|
} |
||||
|
if (seriesParams) { |
||||
|
this.renderer.appendChild(tooltipElement, this.constructTooltipSeriesElement(seriesParams)); |
||||
|
} else { |
||||
|
for (seriesParams of params) { |
||||
|
this.renderer.appendChild(tooltipElement, this.constructTooltipSeriesElement(seriesParams)); |
||||
|
} |
||||
|
} |
||||
|
return tooltipElement; |
||||
|
}, |
||||
|
padding: [8, 12], |
||||
|
backgroundColor: this.settings.tooltipBackgroundColor, |
||||
|
extraCssText: `line-height: 1; backdrop-filter: blur(${this.settings.tooltipBackgroundBlur}px);` |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
this.barChart.setOption(this.barChartOptions); |
||||
|
|
||||
|
this.shapeResize$ = new ResizeObserver(() => { |
||||
|
this.onResize(); |
||||
|
}); |
||||
|
this.shapeResize$.observe(this.chartShape.nativeElement); |
||||
|
this.onResize(); |
||||
|
} |
||||
|
|
||||
|
private constructTooltipSeriesElement(seriesParams): HTMLElement { |
||||
|
const labelValueElement: HTMLElement = this.renderer.createElement('div'); |
||||
|
this.renderer.setStyle(labelValueElement, 'display', 'flex'); |
||||
|
this.renderer.setStyle(labelValueElement, 'flex-direction', 'row'); |
||||
|
this.renderer.setStyle(labelValueElement, 'align-items', 'center'); |
||||
|
this.renderer.setStyle(labelValueElement, 'align-self', 'stretch'); |
||||
|
this.renderer.setStyle(labelValueElement, 'gap', '12px'); |
||||
|
const labelElement: HTMLElement = this.renderer.createElement('div'); |
||||
|
this.renderer.setStyle(labelElement, 'display', 'flex'); |
||||
|
this.renderer.setStyle(labelElement, 'align-items', 'center'); |
||||
|
this.renderer.setStyle(labelElement, 'gap', '8px'); |
||||
|
this.renderer.appendChild(labelValueElement, labelElement); |
||||
|
const circleElement: HTMLElement = this.renderer.createElement('div'); |
||||
|
this.renderer.setStyle(circleElement, 'width', '8px'); |
||||
|
this.renderer.setStyle(circleElement, 'height', '8px'); |
||||
|
this.renderer.setStyle(circleElement, 'border-radius', '50%'); |
||||
|
this.renderer.setStyle(circleElement, 'background', seriesParams.color); |
||||
|
this.renderer.appendChild(labelElement, circleElement); |
||||
|
const labelTextElement: HTMLElement = this.renderer.createElement('div'); |
||||
|
this.renderer.appendChild(labelTextElement, this.renderer.createText(seriesParams.seriesName)); |
||||
|
this.renderer.setStyle(labelTextElement, 'font-family', 'Roboto'); |
||||
|
this.renderer.setStyle(labelTextElement, 'font-size', '12px'); |
||||
|
this.renderer.setStyle(labelTextElement, 'font-style', 'normal'); |
||||
|
this.renderer.setStyle(labelTextElement, 'font-weight', '400'); |
||||
|
this.renderer.setStyle(labelTextElement, 'line-height', '16px'); |
||||
|
this.renderer.setStyle(labelTextElement, 'letter-spacing', '0.4px'); |
||||
|
this.renderer.setStyle(labelTextElement, 'color', 'rgba(0, 0, 0, 0.76)'); |
||||
|
this.renderer.appendChild(labelElement, labelTextElement); |
||||
|
const valueElement: HTMLElement = this.renderer.createElement('div'); |
||||
|
const value = formatValue(seriesParams.value[1], this.decimals, this.units, false); |
||||
|
this.renderer.appendChild(valueElement, this.renderer.createText(value)); |
||||
|
this.renderer.setStyle(valueElement, 'flex', '1'); |
||||
|
this.renderer.setStyle(valueElement, 'text-align', 'end'); |
||||
|
this.renderer.setStyle(valueElement, 'font-family', this.settings.tooltipValueFont.family); |
||||
|
this.renderer.setStyle(valueElement, 'font-size', this.settings.tooltipValueFont.size + this.settings.tooltipValueFont.sizeUnit); |
||||
|
this.renderer.setStyle(valueElement, 'font-style', this.settings.tooltipValueFont.style); |
||||
|
this.renderer.setStyle(valueElement, 'font-weight', this.settings.tooltipValueFont.weight); |
||||
|
this.renderer.setStyle(valueElement, 'line-height', this.settings.tooltipValueFont.lineHeight); |
||||
|
this.renderer.setStyle(valueElement, 'color', this.settings.tooltipValueColor); |
||||
|
this.renderer.appendChild(labelValueElement, valueElement); |
||||
|
return labelValueElement; |
||||
|
} |
||||
|
|
||||
|
private focusedSeriesIndex(): number { |
||||
|
let index = - 1; |
||||
|
// @ts-ignore
|
||||
|
const views: any[] = this.barChart._chartsViews; |
||||
|
if (views) { |
||||
|
const hasBlurredView = !!views.find(view => { |
||||
|
const graphicEls: any[] = view._data._graphicEls; |
||||
|
return !!graphicEls.find(el => el.currentStates.includes('blur')); |
||||
|
}); |
||||
|
if (hasBlurredView) { |
||||
|
const focusedView = views.find(view => { |
||||
|
const graphicEls: any[] = view._data._graphicEls; |
||||
|
return !!graphicEls.find(el => !el.currentStates.includes('blur')); |
||||
|
}); |
||||
|
if (focusedView) { |
||||
|
index = focusedView._model.seriesIndex; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
return index; |
||||
|
} |
||||
|
|
||||
|
private onResize() { |
||||
|
const width = this.barChart.getWidth(); |
||||
|
const height = this.barChart.getHeight(); |
||||
|
const shapeWidth = this.chartShape.nativeElement.offsetWidth; |
||||
|
const shapeHeight = this.chartShape.nativeElement.offsetHeight; |
||||
|
if (width !== shapeWidth || height !== shapeHeight) { |
||||
|
this.barChart.resize(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,112 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2023 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 { |
||||
|
BackgroundSettings, |
||||
|
BackgroundType, customDateFormat, |
||||
|
DateFormatSettings, |
||||
|
Font |
||||
|
} from '@shared/models/widget-settings.models'; |
||||
|
import { LegendPosition } from '@shared/models/widget.models'; |
||||
|
|
||||
|
export interface BarChartWithLabelsWidgetSettings { |
||||
|
showBarLabel: boolean; |
||||
|
barLabelFont: Font; |
||||
|
barLabelColor: string; |
||||
|
showBarValue: boolean; |
||||
|
barValueFont: Font; |
||||
|
barValueColor: string; |
||||
|
showLegend: boolean; |
||||
|
legendPosition: LegendPosition; |
||||
|
legendLabelFont: Font; |
||||
|
legendLabelColor: string; |
||||
|
showTooltip: boolean; |
||||
|
tooltipValueFont: Font; |
||||
|
tooltipValueColor: string; |
||||
|
tooltipShowDate: boolean; |
||||
|
tooltipDateFormat: DateFormatSettings; |
||||
|
tooltipDateFont: Font; |
||||
|
tooltipDateColor: string; |
||||
|
tooltipBackgroundColor: string; |
||||
|
tooltipBackgroundBlur: number; |
||||
|
background: BackgroundSettings; |
||||
|
} |
||||
|
|
||||
|
export const barChartWithLabelsDefaultSettings: BarChartWithLabelsWidgetSettings = { |
||||
|
showBarLabel: true, |
||||
|
barLabelFont: { |
||||
|
family: 'Roboto', |
||||
|
size: 12, |
||||
|
sizeUnit: 'px', |
||||
|
style: 'normal', |
||||
|
weight: '400', |
||||
|
lineHeight: '12px' |
||||
|
}, |
||||
|
barLabelColor: 'rgba(0, 0, 0, 0.54)', |
||||
|
showBarValue: true, |
||||
|
barValueFont: { |
||||
|
family: 'Roboto', |
||||
|
size: 12, |
||||
|
sizeUnit: 'px', |
||||
|
style: 'normal', |
||||
|
weight: '700', |
||||
|
lineHeight: '12px' |
||||
|
}, |
||||
|
barValueColor: 'rgba(0, 0, 0, 0.76)', |
||||
|
showLegend: true, |
||||
|
legendPosition: LegendPosition.top, |
||||
|
legendLabelFont: { |
||||
|
family: 'Roboto', |
||||
|
size: 12, |
||||
|
sizeUnit: 'px', |
||||
|
style: 'normal', |
||||
|
weight: '400', |
||||
|
lineHeight: '16px' |
||||
|
}, |
||||
|
legendLabelColor: 'rgba(0, 0, 0, 0.76)', |
||||
|
showTooltip: true, |
||||
|
tooltipValueFont: { |
||||
|
family: 'Roboto', |
||||
|
size: 12, |
||||
|
sizeUnit: 'px', |
||||
|
style: 'normal', |
||||
|
weight: '500', |
||||
|
lineHeight: '16px' |
||||
|
}, |
||||
|
tooltipValueColor: 'rgba(0, 0, 0, 0.76)', |
||||
|
tooltipShowDate: true, |
||||
|
tooltipDateFormat: customDateFormat('MMMM y'), |
||||
|
tooltipDateFont: { |
||||
|
family: 'Roboto', |
||||
|
size: 11, |
||||
|
sizeUnit: 'px', |
||||
|
style: 'normal', |
||||
|
weight: '400', |
||||
|
lineHeight: '16px' |
||||
|
}, |
||||
|
tooltipDateColor: 'rgba(0, 0, 0, 0.76)', |
||||
|
tooltipBackgroundColor: 'rgba(255, 255, 255, 0.76)', |
||||
|
tooltipBackgroundBlur: 4, |
||||
|
background: { |
||||
|
type: BackgroundType.color, |
||||
|
color: '#fff', |
||||
|
overlay: { |
||||
|
enabled: false, |
||||
|
color: 'rgba(255,255,255,0.72)', |
||||
|
blur: 3 |
||||
|
} |
||||
|
} |
||||
|
}; |
||||
Loading…
Reference in new issue