UNPKG

@c8y/ngx-components

Version:

Angular modules for Cumulocity IoT applications

333 lines (327 loc) 22.6 kB
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