@c8y/ngx-components
Version:
Angular modules for Cumulocity IoT applications
333 lines (327 loc) • 22.6 kB
JavaScript
import * as i0 from '@angular/core';
import { Injectable, inject, ViewChild, Input, ChangeDetectionStrategy, Component } from '@angular/core';
import { WidgetConfigService } from '@c8y/ngx-components/context-dashboard';
import { of, BehaviorSubject, switchMap, tap, map as map$1, combineLatest, Subject } from 'rxjs';
import * as i2 from '@angular/forms';
import { FormBuilder, FormsModule, ReactiveFormsModule } from '@angular/forms';
import { DatapointSelectorModule } from '@c8y/ngx-components/datapoint-selector';
import * as i1 from '@c8y/ngx-components';
import { DynamicComponentAlertAggregator, DynamicComponentAlert, MeasurementRealtimeService, CommonModule, CoreModule } from '@c8y/ngx-components';
import * as i1$1 from 'ngx-echarts';
import { NgxEchartsModule, NGX_ECHARTS_CONFIG } from 'ngx-echarts';
import { AsyncPipe } from '@angular/common';
import { map, catchError } from 'rxjs/operators';
import { ChartAlertsComponent } from '@c8y/ngx-components/echart';
import { TranslateService } from '@ngx-translate/core';
import { gettext } from '@c8y/ngx-components/gettext';
import * as echarts from 'echarts';
class CurrentMeasurementService {
constructor(realtime) {
this.realtime = realtime;
}
/**
* Fetches the latest measurement value for a given datapoint.
* Combines initial historical value with realtime updates.
*
* @param datapoint - The KPI datapoint configuration
* @returns Observable emitting measurement value, unit, date, and notFound flag
*/
getLatest(datapoint) {
return this.realtime
.latestValueOfSpecificMeasurement$(datapoint.fragment, datapoint.series, datapoint.__target, 1, true)
.pipe(map(m => {
if (!m) {
return {
value: Number.NaN,
unit: datapoint.unit || '',
date: '',
notFound: true
};
}
const v = m[datapoint.fragment][datapoint.series];
return {
value: v.value,
unit: v.unit || datapoint.unit,
date: m.time
};
}), catchError(() => of({
value: Number.NaN,
unit: datapoint.unit || '',
date: '',
notFound: true
})));
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: CurrentMeasurementService, deps: [{ token: i1.MeasurementRealtimeService }], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: CurrentMeasurementService, providedIn: 'root' }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: CurrentMeasurementService, decorators: [{
type: Injectable,
args: [{ providedIn: 'root' }]
}], ctorParameters: () => [{ type: i1.MeasurementRealtimeService }] });
/** Chart layout constants */
const CHART_LAYOUT = {
LEGEND_TOP: '5%',
SERIES_TOP: '5%',
PIE_RADIUS: '80%',
EMPTY_STATE_FONT_SIZE: 30
};
class PieChartWidgetViewComponent {
constructor() {
this.activeDatapoints = [];
this.alerts = new DynamicComponentAlertAggregator();
this.configChanged$ = new BehaviorSubject(undefined);
this.measurements = inject(CurrentMeasurementService);
this.translateService = inject(TranslateService);
this.chartOptions$ = this.configChanged$.pipe(switchMap(() => this.fetchMeasurements()), tap(entries => this.handleNegativeValues(entries)), map$1(entries => this.buildChartOptions(entries)), tap(options => this.updateChartInstance(options)));
}
ngOnChanges(_) {
if (this.config?.datapoints?.length) {
this.configChanged$.next();
}
}
onChartInit(ec) {
this.echartsInstance = ec;
}
/**
* Fetches latest measurements for all configured datapoints.
*/
fetchMeasurements() {
this.activeDatapoints = this.config?.datapoints?.filter(dp => dp.__active);
const streams = this.activeDatapoints.map(dp => this.measurements.getLatest(dp).pipe(map$1(m => this.mapToDatapointValue(dp, m))));
return combineLatest(streams);
}
/**
* Maps a datapoint and its measurement to a DatapointValue.
*/
mapToDatapointValue(datapoint, measurement) {
const rawValue = measurement.value;
return {
label: datapoint.label || '',
value: rawValue < 0 || Number.isNaN(rawValue) ? 0 : rawValue,
rawValue,
unit: datapoint.unit || measurement.unit || '',
color: datapoint.color || ''
};
}
/**
* Handles negative value alerts - clears existing alerts and adds warning if needed.
*/
handleNegativeValues(entries) {
this.alerts.clear();
const negatives = entries.filter(e => e.rawValue < 0);
if (negatives.length === 0) {
return;
}
const negativeDpList = negatives
.map(n => `${this.encodeHtml(n.label)}: ${n.rawValue} ${this.encodeHtml(n.unit)}`)
.join('; ');
const errorMessage = this.translateService.instant(gettext('Negative measurements received from data point(s): {{ datapoints }}'), { datapoints: negativeDpList });
this.alerts.addAlerts(new DynamicComponentAlert({ type: 'warning', text: errorMessage }));
}
/**
* Builds the ECharts options based on datapoint values.
*/
buildChartOptions(datapoints) {
if (this.isEmptyState(datapoints)) {
return this.buildEmptyStateOptions();
}
return this.buildPieChartOptions(datapoints);
}
/**
* Checks if chart should display empty state (no positive data).
*/
isEmptyState(entries) {
const hasPositiveData = entries.some(e => e.rawValue > 0);
const hasNegativeData = entries.some(e => e.rawValue < 0);
return !hasPositiveData && !hasNegativeData;
}
/**
* Builds options for empty state display.
*/
buildEmptyStateOptions() {
return {
title: {
text: gettext('No data available.'),
left: 'center',
top: 'center',
textStyle: { fontSize: CHART_LAYOUT.EMPTY_STATE_FONT_SIZE }
},
series: []
};
}
/**
* Builds the pie chart options with data.
*/
buildPieChartOptions(entries) {
const options = this.config.pieChartOptions;
const total = this.calculateTotal(entries);
return {
tooltip: this.buildTooltipConfig(entries, options),
legend: this.buildLegendConfig(options),
series: [this.buildPieSeriesConfig(entries, total, options)]
};
}
/**
* Calculates total of all entry values.
*/
calculateTotal(entries) {
return entries.reduce((sum, e) => sum + e.value, 0);
}
/**
* Builds tooltip configuration.
*/
buildTooltipConfig(entries, options) {
return {
show: options?.showTooltips ?? false,
formatter: (params) => {
const entry = entries.find(e => e.label === params.name);
const unit = entry?.unit || '';
const value = params.value.toFixed(2);
return `${this.encodeHtml(params.name)}: ${value} ${this.encodeHtml(unit)}`;
}
};
}
/**
* Builds legend configuration.
*/
buildLegendConfig(options) {
return {
top: CHART_LAYOUT.LEGEND_TOP,
left: 'right',
show: options?.showLegend ?? false,
formatter: (name) => {
const match = name.match(/^(.+)_\d+$/);
return match ? match[1] : name;
}
};
}
/**
* Builds pie series configuration.
*/
buildPieSeriesConfig(entries, total, options) {
return {
top: CHART_LAYOUT.SERIES_TOP,
type: 'pie',
radius: CHART_LAYOUT.PIE_RADIUS,
label: {
show: options?.showLabels ?? false,
position: 'inside',
formatter: (params) => this.formatPercentageLabel(params.value, total)
},
data: entries.map((e, index) => ({
name: `${e.label}_${index}`,
value: e.value,
itemStyle: { color: e.color }
}))
};
}
/**
* Formats percentage label for pie slice.
*/
formatPercentageLabel(value, total) {
const percentage = total > 0 ? Math.round((value / total) * 100) : 0;
return percentage === 0 ? '' : `${percentage}%`;
}
/**
* Updates the ECharts instance with new options.
*/
updateChartInstance(options) {
if (this.echartsInstance) {
this.echartsInstance.setOption(options, true);
}
}
/**
* Encodes HTML to prevent XSS attacks.
*/
encodeHtml(text) {
return echarts.format.encodeHTML(text);
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: PieChartWidgetViewComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.19", type: PieChartWidgetViewComponent, isStandalone: true, selector: "c8y-pie-chart", inputs: { config: "config" }, providers: [
MeasurementRealtimeService,
CurrentMeasurementService,
{ provide: NGX_ECHARTS_CONFIG, useFactory: () => ({ echarts: () => import('echarts') }) }
], viewQueries: [{ propertyName: "chart", first: true, predicate: ["chart"], descendants: true }], usesOnChanges: true, ngImport: i0, template: "<div class=\"p-relative fit-h\">\n <div\n class=\"p-absolute fit-w fit-h\"\n #chart\n echarts\n [options]=\"chartOptions$ | async\"\n (chartInit)=\"onChartInit($event)\"\n ></div>\n\n <c8y-chart-alerts [alerts]=\"alerts\"></c8y-chart-alerts>\n</div>\n", dependencies: [{ kind: "ngmodule", type: NgxEchartsModule }, { kind: "directive", type: i1$1.NgxEchartsDirective, selector: "echarts, [echarts]", inputs: ["options", "theme", "initOpts", "merge", "autoResize", "loading", "loadingType", "loadingOpts"], outputs: ["chartInit", "optionsError", "chartClick", "chartDblClick", "chartMouseDown", "chartMouseMove", "chartMouseUp", "chartMouseOver", "chartMouseOut", "chartGlobalOut", "chartContextMenu", "chartHighlight", "chartDownplay", "chartSelectChanged", "chartLegendSelectChanged", "chartLegendSelected", "chartLegendUnselected", "chartLegendLegendSelectAll", "chartLegendLegendInverseSelect", "chartLegendScroll", "chartDataZoom", "chartDataRangeSelected", "chartGraphRoam", "chartGeoRoam", "chartTreeRoam", "chartTimelineChanged", "chartTimelinePlayChanged", "chartRestore", "chartDataViewChanged", "chartMagicTypeChanged", "chartGeoSelectChanged", "chartGeoSelected", "chartGeoUnselected", "chartAxisAreaSelected", "chartBrush", "chartBrushEnd", "chartBrushSelected", "chartGlobalCursorTaken", "chartRendered", "chartFinished"], exportAs: ["echarts"] }, { kind: "component", type: ChartAlertsComponent, selector: "c8y-chart-alerts", inputs: ["alerts"] }, { kind: "pipe", type: AsyncPipe, name: "async" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: PieChartWidgetViewComponent, decorators: [{
type: Component,
args: [{ selector: 'c8y-pie-chart', providers: [
MeasurementRealtimeService,
CurrentMeasurementService,
{ provide: NGX_ECHARTS_CONFIG, useFactory: () => ({ echarts: () => import('echarts') }) }
], imports: [NgxEchartsModule, AsyncPipe, ChartAlertsComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"p-relative fit-h\">\n <div\n class=\"p-absolute fit-w fit-h\"\n #chart\n echarts\n [options]=\"chartOptions$ | async\"\n (chartInit)=\"onChartInit($event)\"\n ></div>\n\n <c8y-chart-alerts [alerts]=\"alerts\"></c8y-chart-alerts>\n</div>\n" }]
}], ctorParameters: () => [], propDecorators: { config: [{
type: Input
}], chart: [{
type: ViewChild,
args: ['chart', { static: false }]
}] } });
class PieChartWidgetConfigComponent {
constructor() {
this.destroy$ = new Subject();
this.widgetConfigService = inject(WidgetConfigService);
this.formBuilder = inject(FormBuilder);
}
set previewMapSet(template) {
if (template) {
this.widgetConfigService.setPreview(template);
return;
}
this.widgetConfigService.setPreview(null);
}
ngOnInit() {
this.formGroup = this.initForm();
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
onBeforeSave(config) {
if (!this.formGroup.valid || !config)
return false;
const formValue = this.formGroup.value;
config.pieChartOptions = config.pieChartOptions || {
showLabels: false,
showLegend: false,
showTooltips: false
};
config.pieChartOptions.showLabels = formValue.pieChartOptions.showLabels;
config.pieChartOptions.showLegend = formValue.pieChartOptions.showLegend;
config.pieChartOptions.showTooltips = formValue.pieChartOptions.showTooltips;
this.widgetConfigService.updateConfig(config);
return true;
}
initForm() {
const form = this.formBuilder.group({
pieChartOptions: this.formBuilder.group({
showLabels: [this.config.pieChartOptions?.showLabels ?? false],
showLegend: [this.config.pieChartOptions?.showLegend ?? false],
showTooltips: [this.config.pieChartOptions?.showTooltips ?? false]
})
});
return form;
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: PieChartWidgetConfigComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.19", type: PieChartWidgetConfigComponent, isStandalone: true, selector: "app-pie-chart-config", inputs: { config: "config" }, viewQueries: [{ propertyName: "previewMapSet", first: true, predicate: ["pieChartPreview"], descendants: true }], ngImport: i0, template: "@if (formGroup) {\n <form\n class=\"p-4\"\n [formGroup]=\"formGroup\"\n >\n <fieldset class=\"c8y-fieldset m-t-16 p-b-8\">\n <legend>{{ 'Pie chart options' | translate }}</legend>\n <div\n class=\"d-flex flex-column gap-8 form-group-sm\"\n formGroupName=\"pieChartOptions\"\n >\n <label class=\"c8y-checkbox\">\n <input\n type=\"checkbox\"\n formControlName=\"showLabels\"\n />\n <span></span>\n <span>\n {{ 'Show labels' | translate }}\n </span>\n </label>\n <label class=\"c8y-checkbox\">\n <input\n type=\"checkbox\"\n formControlName=\"showLegend\"\n />\n <span></span>\n <span>\n {{ 'Show legend' | translate }}\n </span>\n </label>\n <label class=\"c8y-checkbox\">\n <input\n type=\"checkbox\"\n formControlName=\"showTooltips\"\n />\n <span></span>\n <span>\n {{ 'Show tooltips' | translate }}\n </span>\n </label>\n </div>\n </fieldset>\n </form>\n\n <ng-template #pieChartPreview>\n @if (config.datapoints?.length > 0) {\n <c8y-pie-chart\n class=\"w-100 h-100\"\n [config]=\"{\n datapoints: config.datapoints,\n pieChartOptions: {\n showLabels: formGroup.value.pieChartOptions.showLabels,\n showLegend: formGroup.value.pieChartOptions.showLegend,\n showTooltips: formGroup.value.pieChartOptions.showTooltips\n }\n }\"\n ></c8y-pie-chart>\n } @else {\n <div class=\"col-md-6 d-col a-i-start j-c-center\">\n <c8y-ui-empty-state\n [icon]=\"'c8y-data-points'\"\n [title]=\"'No data points selected' | translate\"\n [subtitle]=\"'Select data point to render content' | translate\"\n [horizontal]=\"false\"\n data-cy=\"kpi-widget--empty-state-no-data-point-selected\"\n >\n <p c8y-guide-docs>\n <small\n translate\n ngNonBindable\n >\n Find out more in the\n <a c8y-guide-href=\"/docs/cockpit/widgets-collection/#kpi\">user documentation</a>.\n </small>\n </p>\n </c8y-ui-empty-state>\n </div>\n }\n </ng-template>\n}\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: i1.EmptyStateComponent, selector: "c8y-ui-empty-state", inputs: ["icon", "title", "subtitle", "horizontal"] }, { kind: "directive", type: i1.C8yTranslateDirective, selector: "[translate],[ngx-translate]" }, { kind: "ngmodule", type: CoreModule }, { kind: "directive", type: i2.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i2.CheckboxControlValueAccessor, selector: "input[type=checkbox][formControlName],input[type=checkbox][formControl],input[type=checkbox][ngModel]" }, { kind: "directive", type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1.RequiredInputPlaceholderDirective, selector: "input[required], input[formControlName]" }, { kind: "directive", type: i1.GuideHrefDirective, selector: "[c8y-guide-href]", inputs: ["c8y-guide-href"] }, { kind: "component", type: i1.GuideDocsComponent, selector: "[c8y-guide-docs]" }, { kind: "directive", type: i2.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i2.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "directive", type: i2.FormGroupName, selector: "[formGroupName]", inputs: ["formGroupName"] }, { kind: "ngmodule", type: DatapointSelectorModule }, { kind: "ngmodule", type: FormsModule }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "component", type: PieChartWidgetViewComponent, selector: "c8y-pie-chart", inputs: ["config"] }, { kind: "pipe", type: i1.C8yTranslatePipe, name: "translate" }] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: PieChartWidgetConfigComponent, decorators: [{
type: Component,
args: [{ selector: 'app-pie-chart-config', imports: [
CommonModule,
CoreModule,
DatapointSelectorModule,
FormsModule,
ReactiveFormsModule,
PieChartWidgetViewComponent
], template: "@if (formGroup) {\n <form\n class=\"p-4\"\n [formGroup]=\"formGroup\"\n >\n <fieldset class=\"c8y-fieldset m-t-16 p-b-8\">\n <legend>{{ 'Pie chart options' | translate }}</legend>\n <div\n class=\"d-flex flex-column gap-8 form-group-sm\"\n formGroupName=\"pieChartOptions\"\n >\n <label class=\"c8y-checkbox\">\n <input\n type=\"checkbox\"\n formControlName=\"showLabels\"\n />\n <span></span>\n <span>\n {{ 'Show labels' | translate }}\n </span>\n </label>\n <label class=\"c8y-checkbox\">\n <input\n type=\"checkbox\"\n formControlName=\"showLegend\"\n />\n <span></span>\n <span>\n {{ 'Show legend' | translate }}\n </span>\n </label>\n <label class=\"c8y-checkbox\">\n <input\n type=\"checkbox\"\n formControlName=\"showTooltips\"\n />\n <span></span>\n <span>\n {{ 'Show tooltips' | translate }}\n </span>\n </label>\n </div>\n </fieldset>\n </form>\n\n <ng-template #pieChartPreview>\n @if (config.datapoints?.length > 0) {\n <c8y-pie-chart\n class=\"w-100 h-100\"\n [config]=\"{\n datapoints: config.datapoints,\n pieChartOptions: {\n showLabels: formGroup.value.pieChartOptions.showLabels,\n showLegend: formGroup.value.pieChartOptions.showLegend,\n showTooltips: formGroup.value.pieChartOptions.showTooltips\n }\n }\"\n ></c8y-pie-chart>\n } @else {\n <div class=\"col-md-6 d-col a-i-start j-c-center\">\n <c8y-ui-empty-state\n [icon]=\"'c8y-data-points'\"\n [title]=\"'No data points selected' | translate\"\n [subtitle]=\"'Select data point to render content' | translate\"\n [horizontal]=\"false\"\n data-cy=\"kpi-widget--empty-state-no-data-point-selected\"\n >\n <p c8y-guide-docs>\n <small\n translate\n ngNonBindable\n >\n Find out more in the\n <a c8y-guide-href=\"/docs/cockpit/widgets-collection/#kpi\">user documentation</a>.\n </small>\n </p>\n </c8y-ui-empty-state>\n </div>\n }\n </ng-template>\n}\n" }]
}], propDecorators: { config: [{
type: Input
}], previewMapSet: [{
type: ViewChild,
args: ['pieChartPreview']
}] } });
/**
* Generated bundle index. Do not edit.
*/
export { PieChartWidgetConfigComponent, PieChartWidgetViewComponent };
//# sourceMappingURL=c8y-ngx-components-widgets-implementations-pie-chart.mjs.map