@praxisui/charts 3.0.0-beta.3 → 3.0.0-beta.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,18 +1,36 @@
1
1
  import { CommonModule } from '@angular/common';
2
2
  import * as i0 from '@angular/core';
3
- import { Injectable, Inject, InjectionToken, input, output, viewChild, inject, DestroyRef, signal, computed, effect, ChangeDetectionStrategy, Component, ENVIRONMENT_INITIALIZER } from '@angular/core';
4
- import * as i1$1 from '@praxisui/core';
5
- import { buildApiUrl, API_URL, PraxisI18nService, ComponentMetadataRegistry, createDefaultTableConfig, DynamicWidgetPageComponent, DynamicGridPageComponent } from '@praxisui/core';
3
+ import { Injectable, Inject, InjectionToken, input, booleanAttribute, output, viewChild, inject, ElementRef, DestroyRef, signal, computed, afterNextRender, effect, ChangeDetectionStrategy, Component, ENVIRONMENT_INITIALIZER } from '@angular/core';
4
+ import * as i1$2 from '@praxisui/core';
5
+ import { buildApiUrl, API_URL, PraxisI18nService, SETTINGS_PANEL_BRIDGE, ComponentMetadataRegistry, createDefaultTableConfig, DynamicWidgetPageComponent, DynamicGridPageComponent, providePraxisI18n, SETTINGS_PANEL_DATA } from '@praxisui/core';
6
6
  import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
7
+ import * as i1$1 from '@angular/material/button';
8
+ import { MatButtonModule } from '@angular/material/button';
9
+ import * as i2 from '@angular/material/icon';
10
+ import { MatIconModule } from '@angular/material/icon';
11
+ import * as i3 from '@angular/material/tooltip';
12
+ import { MatTooltipModule } from '@angular/material/tooltip';
7
13
  import { use, init } from 'echarts/core';
8
14
  import { BarChart, LineChart, PieChart, ScatterChart } from 'echarts/charts';
9
15
  import { AriaComponent, DatasetComponent, GridComponent, LegendComponent, TitleComponent, TooltipComponent, TransformComponent } from 'echarts/components';
10
16
  import { CanvasRenderer } from 'echarts/renderers';
11
17
  import * as i1 from '@angular/common/http';
12
18
  import { HttpErrorResponse } from '@angular/common/http';
13
- import { throwError, map } from 'rxjs';
19
+ import { throwError, map, BehaviorSubject } from 'rxjs';
14
20
  import { catchError } from 'rxjs/operators';
15
21
  import { PraxisTable } from '@praxisui/table';
22
+ import * as i1$3 from '@angular/forms';
23
+ import { FormsModule } from '@angular/forms';
24
+ import * as i3$1 from '@angular/material/card';
25
+ import { MatCardModule } from '@angular/material/card';
26
+ import * as i4 from '@angular/material/form-field';
27
+ import { MatFormFieldModule } from '@angular/material/form-field';
28
+ import * as i5 from '@angular/material/input';
29
+ import { MatInputModule } from '@angular/material/input';
30
+ import * as i6 from '@angular/material/select';
31
+ import { MatSelectModule } from '@angular/material/select';
32
+ import * as i7 from '@angular/material/slide-toggle';
33
+ import { MatSlideToggleModule } from '@angular/material/slide-toggle';
16
34
 
17
35
  class PraxisChartDataTransformerService {
18
36
  transform(config, rows) {
@@ -484,1516 +502,1974 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
484
502
  type: Injectable
485
503
  }], ctorParameters: () => [{ type: PraxisChartOptionBuilderService }] });
486
504
 
487
- class PraxisChartStatsApiService {
488
- http;
489
- apiUrl;
490
- constructor(http, apiUrl) {
491
- this.http = http;
492
- this.apiUrl = apiUrl;
505
+ class ChartContractNormalizerService {
506
+ normalize(input) {
507
+ let document = structuredClone(input);
508
+ document = this.normalizeMotion(document);
509
+ document = this.normalizeKindSpecificFields(document);
510
+ document = this.normalizeOperationSpecificFields(document);
511
+ document = this.normalizeSourceSpecificFields(document);
512
+ return document;
493
513
  }
494
- execute(event, config) {
495
- const query = event.query;
496
- const statsRequest = query?.statsRequest;
497
- if (!query?.statsPath || !statsRequest) {
498
- return throwError(() => new Error('PraxisChartStatsApiService requires query.statsPath and query.statsRequest for praxis.stats execution.'));
514
+ normalizeMotion(document) {
515
+ if (!document.motion) {
516
+ return document;
499
517
  }
500
- const categoryField = this.resolveCategoryField(config, statsRequest);
501
- const url = this.buildStatsUrl(query.statsPath);
502
- return this.http
503
- .post(url, statsRequest)
504
- .pipe(map((response) => this.toChartRows(response?.data, statsRequest, categoryField, config)), catchError((error) => this.handleHttpError(error)));
518
+ if (document.motion.enabled === false) {
519
+ return {
520
+ ...document,
521
+ motion: {
522
+ enabled: false,
523
+ },
524
+ };
525
+ }
526
+ return {
527
+ ...document,
528
+ motion: {
529
+ enabled: document.motion.enabled ?? true,
530
+ preset: document.motion.preset ?? 'standard',
531
+ },
532
+ };
505
533
  }
506
- toChartRows(response, request, categoryField, config) {
507
- if (!response) {
508
- return [];
534
+ normalizeKindSpecificFields(document) {
535
+ let nextDocument = document;
536
+ if (document.kind === 'horizontal-bar') {
537
+ nextDocument = {
538
+ ...nextDocument,
539
+ orientation: 'horizontal',
540
+ };
509
541
  }
510
- const metricBindings = this.resolveMetricBindings(config, request, response);
511
- if ('points' in response) {
512
- return response.points.map((point) => {
513
- const category = point.label ?? point.start ?? point.end ?? '';
514
- return {
515
- [categoryField]: category,
516
- ...this.projectMetricValues(metricBindings, point.values, point.value, point.count),
517
- key: point.start ?? point.label ?? point.end ?? category,
518
- label: point.label ?? category,
519
- value: point.value ?? null,
520
- count: point.count ?? null,
521
- start: point.start ?? null,
522
- end: point.end ?? null,
523
- granularity: response.granularity ?? null,
524
- };
525
- });
542
+ if (document.kind !== 'combo' && document.metrics?.length) {
543
+ nextDocument = {
544
+ ...nextDocument,
545
+ metrics: document.metrics.map((metric) => ({
546
+ ...metric,
547
+ axis: undefined,
548
+ seriesKind: undefined,
549
+ })),
550
+ };
526
551
  }
527
- if ('buckets' in response) {
528
- return response.buckets.map((bucket) => {
529
- const category = this.resolveBucketCategory(bucket);
530
- return {
531
- [categoryField]: category,
532
- ...this.projectMetricValues(metricBindings, bucket.values, bucket.value, bucket.count),
533
- key: bucket.key ?? null,
534
- label: bucket.label ?? category,
535
- value: bucket.value ?? null,
536
- count: bucket.count ?? null,
537
- from: bucket.from ?? null,
538
- to: bucket.to ?? null,
539
- mode: 'mode' in response ? response.mode ?? null : null,
540
- requestMode: 'mode' in request ? request.mode : null,
541
- };
542
- });
552
+ if ((document.kind === 'pie' || document.kind === 'donut' || this.isDistribution(document)) && document.metrics?.length) {
553
+ nextDocument = {
554
+ ...nextDocument,
555
+ metrics: document.metrics.slice(0, 1),
556
+ };
543
557
  }
544
- return [];
558
+ return nextDocument;
545
559
  }
546
- resolveBucketCategory(bucket) {
547
- if (bucket.label !== null && bucket.label !== undefined && bucket.label !== '') {
548
- return String(bucket.label);
560
+ normalizeOperationSpecificFields(document) {
561
+ if (document.source.kind !== 'praxis.stats') {
562
+ return document;
549
563
  }
550
- if (bucket.key !== null && bucket.key !== undefined && bucket.key !== '') {
551
- return String(bucket.key);
564
+ const options = { ...document.source.options };
565
+ if (document.source.operation !== 'timeseries') {
566
+ options.granularity = undefined;
567
+ options.fillGaps = undefined;
552
568
  }
553
- if (bucket.from !== null || bucket.to !== null) {
554
- return `${bucket.from ?? ''} - ${bucket.to ?? ''}`.trim();
569
+ if (document.source.operation !== 'distribution') {
570
+ options.mode = undefined;
571
+ options.bucketSize = undefined;
572
+ options.bucketCount = undefined;
555
573
  }
556
- return '';
574
+ return {
575
+ ...document,
576
+ source: {
577
+ ...document.source,
578
+ options,
579
+ },
580
+ };
557
581
  }
558
- resolveMetricValue(value, count) {
559
- if (typeof value === 'number' && Number.isFinite(value)) {
560
- return value;
561
- }
562
- if (typeof count === 'number' && Number.isFinite(count)) {
563
- return count;
582
+ normalizeSourceSpecificFields(document) {
583
+ if (document.source.kind === 'derived') {
584
+ return {
585
+ ...document,
586
+ source: {
587
+ kind: 'derived',
588
+ },
589
+ };
564
590
  }
565
- return 0;
591
+ return document;
566
592
  }
567
- resolveCategoryField(config, request) {
568
- const firstSeries = config.series[0];
569
- return (firstSeries?.categoryField
570
- || config.axes?.x?.field
571
- || request.field
572
- || 'category');
593
+ isDistribution(document) {
594
+ return document.source.kind === 'praxis.stats' && document.source.operation === 'distribution';
573
595
  }
574
- resolveMetricBindings(config, request, response) {
575
- const queryMetrics = config.dataSource?.kind === 'remote'
576
- ? (config.dataSource.query?.metrics ?? [])
577
- : [];
578
- if (queryMetrics.length) {
579
- return queryMetrics.map((metric, index) => ({
580
- field: metric.field || `value${index + 1}`,
581
- alias: metric.alias || metric.field || `value${index + 1}`,
582
- }));
596
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: ChartContractNormalizerService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
597
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: ChartContractNormalizerService, providedIn: 'root' });
598
+ }
599
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: ChartContractNormalizerService, decorators: [{
600
+ type: Injectable,
601
+ args: [{ providedIn: 'root' }]
602
+ }] });
603
+
604
+ class ChartContractValidationService {
605
+ validate(document) {
606
+ const issues = [];
607
+ this.validateSource(document, issues);
608
+ this.validateTheme(document, issues);
609
+ this.validateMetrics(document, issues);
610
+ this.validateKinds(document, issues);
611
+ this.validateEvents(document, issues);
612
+ return {
613
+ valid: !issues.some((issue) => issue.severity === 'error'),
614
+ issues,
615
+ };
616
+ }
617
+ validateSource(document, issues) {
618
+ if (document.source.kind !== 'praxis.stats' && document.source.kind !== 'derived') {
619
+ issues.push(this.error('unsupported-source-kind', 'source.kind', `x-ui.chart source.kind="${document.source.kind}" is not supported in @praxisui/charts.`));
583
620
  }
584
- const responseMetrics = 'metrics' in response ? response.metrics : undefined;
585
- if (responseMetrics?.length) {
586
- return responseMetrics.map((metric, index) => ({
587
- field: metric.alias || metric.field || `value${index + 1}`,
588
- alias: metric.alias || metric.field || `value${index + 1}`,
589
- }));
621
+ if (document.source.kind === 'praxis.stats' && !document.source.resource?.trim()) {
622
+ issues.push(this.error('missing-resource', 'source.resource', 'x-ui.chart source.resource is required for source.kind="praxis.stats".'));
590
623
  }
591
- const requestMetrics = 'metrics' in request ? request.metrics : undefined;
592
- if (requestMetrics?.length) {
593
- return requestMetrics.map((metric, index) => ({
594
- field: metric.alias || metric.field || `value${index + 1}`,
595
- alias: metric.alias || metric.field || `value${index + 1}`,
596
- }));
624
+ if (document.source.kind === 'praxis.stats' && !document.source.operation) {
625
+ issues.push(this.error('missing-operation', 'source.operation', 'x-ui.chart source.operation is required for source.kind="praxis.stats".'));
597
626
  }
598
- const metric = ('metric' in response && response.metric) || ('metric' in request ? request.metric : undefined);
599
- return [
600
- {
601
- field: metric?.alias || metric?.field || config.series[0]?.metric?.field || 'value',
602
- alias: metric?.alias || metric?.field || config.series[0]?.metric?.field || 'value',
603
- },
604
- ];
605
627
  }
606
- projectMetricValues(bindings, values, primaryValue, count) {
607
- return bindings.reduce((acc, binding, index) => {
608
- const rawValue = values?.[binding.alias];
609
- acc[binding.field] = index === 0
610
- ? this.resolveMetricValue(rawValue ?? primaryValue, count)
611
- : this.resolveMetricValue(rawValue, null);
612
- return acc;
613
- }, {});
614
- }
615
- buildStatsUrl(statsPath) {
616
- const base = this.buildDefaultApiBase();
617
- const normalizedStatsPath = this.normalizePath(statsPath);
618
- const resolvedStatsPath = this.shouldStripLeadingApiSegment(base)
619
- ? this.stripLeadingApiSegment(normalizedStatsPath)
620
- : normalizedStatsPath;
621
- return `${base}/${resolvedStatsPath}`;
622
- }
623
- buildDefaultApiBase() {
624
- const entry = this.apiUrl?.default ?? {};
625
- return buildApiUrl(entry).replace(/\/+$/, '');
628
+ validateTheme(document, issues) {
629
+ if (document.theme?.palette && typeof document.theme.palette === 'string') {
630
+ issues.push(this.error('palette-token-unsupported', 'theme.palette', 'x-ui.chart theme.palette as palette token reference is not yet implemented in @praxisui/charts.'));
631
+ }
632
+ if (document.theme?.variant) {
633
+ issues.push(this.error('theme-variant-unsupported', 'theme.variant', 'x-ui.chart theme.variant is not yet implemented in @praxisui/charts.'));
634
+ }
626
635
  }
627
- normalizePath(value) {
628
- return String(value || '').trim().replace(/^\/+|\/+$/g, '');
636
+ validateMetrics(document, issues) {
637
+ if (!document.metrics?.length) {
638
+ issues.push(this.error('missing-metric', 'metrics', 'x-ui.chart requires at least one metric for the current @praxisui/charts runtime.'));
639
+ }
640
+ const aggregations = [
641
+ ...(document.metrics?.map((metric) => metric.aggregation).filter(Boolean) ?? []),
642
+ ...(document.aggregations?.map((aggregation) => aggregation.operation) ?? []),
643
+ ];
644
+ if (aggregations.includes('distinct-count')) {
645
+ issues.push(this.error('distinct-count-unsupported', 'metrics', 'x-ui.chart aggregation "distinct-count" is not yet implemented in @praxisui/charts.'));
646
+ }
629
647
  }
630
- stripLeadingApiSegment(value) {
631
- return value.replace(/^api\/+/i, '');
648
+ validateKinds(document, issues) {
649
+ const metricCount = document.metrics?.length ?? 0;
650
+ if (document.kind !== 'pie' && document.kind !== 'donut' && !document.dimensions?.length) {
651
+ issues.push(this.error('missing-dimension', 'dimensions', 'x-ui.chart cartesian charts require at least one dimension in the current @praxisui/charts runtime.'));
652
+ }
653
+ if ((document.kind === 'pie' || document.kind === 'donut') && !document.dimensions?.[0]?.field) {
654
+ issues.push(this.error('pie-missing-dimension', 'dimensions[0].field', 'x-ui.chart pie/donut charts require a first dimension for category mapping.'));
655
+ }
656
+ if ((document.kind === 'pie' || document.kind === 'donut') && metricCount > 1) {
657
+ issues.push(this.error('pie-multi-metric', 'metrics', 'x-ui.chart pie/donut charts with multiple metrics are not yet implemented in @praxisui/charts.'));
658
+ }
659
+ if (document.kind === 'combo' && metricCount < 2) {
660
+ issues.push(this.error('combo-min-metrics', 'metrics', 'x-ui.chart combo charts require at least two metrics.'));
661
+ }
662
+ if (document.kind !== 'combo' && document.metrics?.some((metric) => metric.axis === 'secondary')) {
663
+ issues.push(this.error('secondary-axis-non-combo', 'metrics', 'x-ui.chart axis="secondary" is supported only for combo charts in @praxisui/charts.'));
664
+ }
665
+ if (document.source.kind === 'praxis.stats'
666
+ && document.source.operation === 'distribution'
667
+ && metricCount > 1) {
668
+ issues.push(this.error('distribution-single-metric', 'metrics', 'x-ui.chart praxis.stats distribution currently supports only a single metric in @praxisui/charts.'));
669
+ }
670
+ if (document.kind === 'horizontal-bar' && document.orientation && document.orientation !== 'horizontal') {
671
+ issues.push(this.error('horizontal-bar-orientation', 'orientation', 'x-ui.chart kind="horizontal-bar" requires orientation="horizontal" when orientation is provided.'));
672
+ }
673
+ if (document.kind === 'scatter' && !document.dimensions?.[0]?.field) {
674
+ issues.push(this.error('scatter-missing-dimension', 'dimensions[0].field', 'x-ui.chart scatter charts require dimensions[0].field for the x axis.'));
675
+ }
676
+ if (document.kind === 'scatter' && !document.metrics?.[0]?.field) {
677
+ issues.push(this.error('scatter-missing-metric', 'metrics[0].field', 'x-ui.chart scatter charts require metrics[0].field for the y axis.'));
678
+ }
679
+ if (document.kind === 'combo'
680
+ && document.source.kind === 'praxis.stats'
681
+ && document.source.operation !== 'group-by'
682
+ && document.source.operation !== 'timeseries') {
683
+ issues.push(this.error('combo-operation-unsupported', 'source.operation', 'x-ui.chart combo charts over praxis.stats currently support only group-by or timeseries operations in @praxisui/charts.'));
684
+ }
632
685
  }
633
- shouldStripLeadingApiSegment(base) {
634
- return /\/api$/i.test(base);
686
+ validateEvents(document, issues) {
687
+ this.validateEventAction('pointClick', document.events?.pointClick, issues);
688
+ this.validateEventAction('drillDown', document.events?.drillDown, issues);
689
+ if (document.events?.selectionChange) {
690
+ issues.push(this.error('selection-change-unsupported', 'events.selectionChange', 'x-ui.chart selectionChange/crossFilter declarative runtime actions are not yet implemented in @praxisui/charts.'));
691
+ }
692
+ if (document.events?.crossFilter) {
693
+ issues.push(this.error('cross-filter-unsupported', 'events.crossFilter', 'x-ui.chart selectionChange/crossFilter declarative runtime actions are not yet implemented in @praxisui/charts.'));
694
+ }
635
695
  }
636
- handleHttpError(error) {
637
- if (error instanceof HttpErrorResponse) {
638
- const detail = typeof error.message === 'string' && error.message.trim()
639
- ? error.message
640
- : `HTTP ${error.status || 0}`;
641
- return throwError(() => new Error(detail));
696
+ validateEventAction(eventKey, eventAction, issues) {
697
+ if (!eventAction?.action) {
698
+ return;
699
+ }
700
+ if (eventAction.action !== 'emit' && !eventAction.target?.trim()) {
701
+ issues.push(this.error(`${eventKey}-missing-target`, `events.${eventKey}.target`, `x-ui.chart events.${eventKey}.target is required when action="${eventAction.action}".`));
642
702
  }
643
- return throwError(() => error instanceof Error ? error : new Error('Unexpected failure during praxis.stats execution.'));
644
703
  }
645
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: PraxisChartStatsApiService, deps: [{ token: i1.HttpClient }, { token: API_URL }], target: i0.ɵɵFactoryTarget.Injectable });
646
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: PraxisChartStatsApiService, providedIn: 'root' });
704
+ error(code, field, message) {
705
+ return {
706
+ severity: 'error',
707
+ code,
708
+ field,
709
+ message,
710
+ };
711
+ }
712
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: ChartContractValidationService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
713
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: ChartContractValidationService, providedIn: 'root' });
647
714
  }
648
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: PraxisChartStatsApiService, decorators: [{
715
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: ChartContractValidationService, decorators: [{
649
716
  type: Injectable,
650
717
  args: [{ providedIn: 'root' }]
651
- }], ctorParameters: () => [{ type: i1.HttpClient }, { type: undefined, decorators: [{
652
- type: Inject,
653
- args: [API_URL]
654
- }] }] });
655
-
656
- const PRAXIS_CHART_ENGINE = new InjectionToken('PRAXIS_CHART_ENGINE');
718
+ }] });
657
719
 
658
- class PraxisChartComponent {
659
- config = input.required(...(ngDevMode ? [{ debugName: "config" }] : []));
660
- data = input(null, ...(ngDevMode ? [{ debugName: "data" }] : []));
661
- pointClick = output();
662
- queryRequest = output();
663
- loadStateChange = output();
664
- chartHost = viewChild('chartHost', ...(ngDevMode ? [{ debugName: "chartHost" }] : []));
665
- engine = inject(PRAXIS_CHART_ENGINE);
666
- transformer = inject(PraxisChartDataTransformerService);
667
- statsApi = inject(PraxisChartStatsApiService);
668
- i18n = inject(PraxisI18nService);
669
- destroyRef = inject(DestroyRef);
670
- resizeObserver = signal(null, ...(ngDevMode ? [{ debugName: "resizeObserver" }] : []));
671
- currentLoadState = signal('idle', ...(ngDevMode ? [{ debugName: "currentLoadState" }] : []));
672
- remoteResolvedData = signal(null, ...(ngDevMode ? [{ debugName: "remoteResolvedData" }] : []));
673
- remoteRuntimeState = signal('idle', ...(ngDevMode ? [{ debugName: "remoteRuntimeState" }] : []));
674
- remoteTechnicalError = signal(null, ...(ngDevMode ? [{ debugName: "remoteTechnicalError" }] : []));
675
- previousRemoteSignature = null;
676
- resolvedData = computed(() => {
677
- const inputData = this.data();
678
- if (inputData !== null && inputData !== undefined) {
679
- return inputData;
680
- }
681
- const remoteData = this.remoteResolvedData();
682
- if (remoteData !== null) {
683
- return remoteData;
720
+ class PraxisChartCanonicalContractMapperService {
721
+ normalizer;
722
+ validator;
723
+ constructor(normalizer = new ChartContractNormalizerService(), validator = new ChartContractValidationService()) {
724
+ this.normalizer = normalizer;
725
+ this.validator = validator;
726
+ }
727
+ toPraxisChartConfig(contract) {
728
+ const normalizedContract = this.normalizer.normalize(contract);
729
+ const validation = this.validator.validate(normalizedContract);
730
+ const firstError = validation.issues.find((issue) => issue.severity === 'error');
731
+ if (firstError) {
732
+ throw new Error(firstError.message);
684
733
  }
685
- const dataSource = this.config().dataSource;
686
- if (dataSource?.kind === 'local') {
687
- return dataSource.items ?? [];
734
+ return {
735
+ id: normalizedContract.chartId,
736
+ type: normalizedContract.kind,
737
+ orientation: this.resolveOrientation(normalizedContract),
738
+ title: this.mapTextValue(normalizedContract.title),
739
+ subtitle: this.mapTextValue(normalizedContract.subtitle),
740
+ height: normalizedContract.height,
741
+ axes: this.buildAxes(normalizedContract),
742
+ series: this.buildSeries(normalizedContract),
743
+ dataSource: this.buildDataSource(normalizedContract),
744
+ interactions: this.buildInteractions(normalizedContract),
745
+ theme: this.buildTheme(normalizedContract),
746
+ motion: this.buildMotion(normalizedContract.motion),
747
+ emptyState: normalizedContract.state?.empty
748
+ ? {
749
+ title: this.mapTextValue(normalizedContract.state.empty.title),
750
+ description: this.mapTextValue(normalizedContract.state.empty.description),
751
+ }
752
+ : undefined,
753
+ state: {
754
+ loadingLabel: this.mapTextValue(normalizedContract.state?.loading?.title),
755
+ error: normalizedContract.state?.error
756
+ ? {
757
+ title: this.mapTextValue(normalizedContract.state.error.title),
758
+ description: this.mapTextValue(normalizedContract.state.error.description),
759
+ }
760
+ : undefined,
761
+ },
762
+ };
763
+ }
764
+ buildAxes(contract) {
765
+ const firstDimension = contract.dimensions?.[0];
766
+ const firstMetric = contract.metrics?.[0];
767
+ const secondaryMetric = contract.metrics?.find((metric) => metric.axis === 'secondary');
768
+ const metricCount = contract.metrics?.length ?? 0;
769
+ if (contract.kind === 'pie' || contract.kind === 'donut') {
770
+ return {
771
+ x: {
772
+ field: firstDimension?.field,
773
+ label: this.toLabel(firstDimension?.label),
774
+ },
775
+ };
688
776
  }
689
- return [];
690
- }, ...(ngDevMode ? [{ debugName: "resolvedData" }] : []));
691
- resolvedHeight = computed(() => {
692
- const value = this.config().height;
693
- if (typeof value === 'number')
694
- return `${value}px`;
695
- return value || '320px';
696
- }, ...(ngDevMode ? [{ debugName: "resolvedHeight" }] : []));
697
- renderConfig = computed(() => {
698
- const config = this.config();
699
- const explicitData = this.data();
700
- const remoteState = this.remoteRuntimeState();
701
- const remoteData = this.remoteResolvedData();
702
- if (explicitData !== null && explicitData !== undefined) {
703
- return config;
704
- }
705
- if (config.dataSource?.kind !== 'remote') {
706
- return config;
707
- }
708
- if (remoteState === 'loading') {
777
+ if (contract.kind === 'scatter') {
709
778
  return {
710
- ...config,
711
- preferredLoadState: 'loading',
779
+ x: {
780
+ field: firstDimension?.field,
781
+ label: this.toLabel(firstDimension?.label),
782
+ type: firstDimension?.role === 'time' ? 'time' : 'value',
783
+ },
784
+ y: {
785
+ field: firstMetric?.field,
786
+ label: this.toLabel(firstMetric?.label),
787
+ type: 'value',
788
+ },
712
789
  };
713
790
  }
714
- if (remoteState === 'error') {
715
- return {
716
- ...config,
717
- preferredLoadState: 'error',
718
- state: config.state?.error
719
- ? {
720
- ...config.state,
721
- error: {
722
- ...config.state.error,
723
- technicalDetails: this.remoteTechnicalError() ?? config.state.error.technicalDetails,
724
- },
725
- }
726
- : config.state,
727
- };
791
+ return {
792
+ x: {
793
+ field: firstDimension?.field,
794
+ label: this.toLabel(firstDimension?.label),
795
+ type: firstDimension?.role === 'time' ? 'time' : 'category',
796
+ },
797
+ y: {
798
+ label: metricCount > 1 ? undefined : this.toLabel(firstMetric?.label),
799
+ type: 'value',
800
+ },
801
+ ySecondary: contract.kind === 'combo' && secondaryMetric
802
+ ? {
803
+ label: this.toLabel(secondaryMetric.label),
804
+ type: 'value',
805
+ position: 'right',
806
+ }
807
+ : undefined,
808
+ };
809
+ }
810
+ buildSeries(contract) {
811
+ const firstDimension = contract.dimensions?.[0];
812
+ const labelsVisible = this.resolveToggle(contract.labels);
813
+ return (contract.metrics ?? []).map((metric, index) => ({
814
+ id: `${metric.field}-${index + 1}`,
815
+ name: this.toLabel(metric.label) ?? metric.field,
816
+ type: this.resolveSeriesType(contract.kind, metric.seriesKind, index),
817
+ axis: metric.axis ?? 'primary',
818
+ categoryField: contract.kind === 'pie' || contract.kind === 'donut' ? firstDimension?.field : undefined,
819
+ metric: {
820
+ field: metric.field,
821
+ aggregation: this.mapAggregation(metric.aggregation),
822
+ label: this.toLabel(metric.label),
823
+ },
824
+ color: metric.color,
825
+ stackId: contract.kind === 'stacked-bar' || contract.kind === 'stacked-area' ? 'stack-1' : undefined,
826
+ labels: labelsVisible ? { visible: true } : undefined,
827
+ smooth: this.shouldSmoothSeries(contract.kind, metric.seriesKind),
828
+ }));
829
+ }
830
+ buildDataSource(contract) {
831
+ if (contract.source.kind === 'derived') {
832
+ return undefined;
728
833
  }
729
- if (remoteData !== null) {
730
- return {
731
- ...config,
732
- preferredLoadState: undefined,
733
- series: config.series.map((series) => ({
734
- ...series,
735
- metric: {
736
- ...series.metric,
737
- aggregation: 'sum',
738
- field: series.metric?.field || 'value',
739
- },
740
- })),
741
- };
834
+ return {
835
+ kind: 'remote',
836
+ resourcePath: contract.source.resource,
837
+ schemaId: contract.source.resource,
838
+ query: this.buildQuery(contract),
839
+ };
840
+ }
841
+ buildQuery(contract) {
842
+ if (contract.source.kind !== 'praxis.stats') {
843
+ return undefined;
742
844
  }
743
- return config;
744
- }, ...(ngDevMode ? [{ debugName: "renderConfig" }] : []));
745
- loadingLabel = computed(() => this.i18n.resolve(this.renderConfig().state?.loadingLabel), ...(ngDevMode ? [{ debugName: "loadingLabel" }] : []));
746
- emptyTitle = computed(() => this.i18n.resolve(this.renderConfig().emptyState?.title), ...(ngDevMode ? [{ debugName: "emptyTitle" }] : []));
747
- emptyDescription = computed(() => this.i18n.resolve(this.renderConfig().emptyState?.description), ...(ngDevMode ? [{ debugName: "emptyDescription" }] : []));
748
- errorTitle = computed(() => this.i18n.resolve(this.renderConfig().state?.error?.title), ...(ngDevMode ? [{ debugName: "errorTitle" }] : []));
749
- errorDescription = computed(() => this.i18n.resolve(this.renderConfig().state?.error?.description), ...(ngDevMode ? [{ debugName: "errorDescription" }] : []));
750
- loadState = computed(() => {
751
- const config = this.renderConfig();
752
- if (config.preferredLoadState === 'loading')
753
- return 'loading';
754
- if (config.preferredLoadState === 'error')
755
- return 'error';
756
- const dataSource = config.dataSource;
757
- const explicitData = this.data();
758
- if (dataSource?.kind === 'remote' && (explicitData === null || explicitData === undefined)) {
759
- return this.remoteRuntimeState() === 'ready' ? 'ready' : 'loading';
845
+ const filtersFromContract = this.toQueryFilterMap(contract.filters);
846
+ const combinedFilters = {
847
+ ...filtersFromContract,
848
+ };
849
+ return {
850
+ sourceKind: 'praxis.stats',
851
+ statsOperation: contract.source.operation,
852
+ granularity: contract.source.options?.granularity,
853
+ fillGaps: contract.source.options?.fillGaps,
854
+ distributionMode: contract.source.options?.mode,
855
+ bucketSize: contract.source.options?.bucketSize,
856
+ bucketCount: contract.source.options?.bucketCount,
857
+ statsOrderBy: this.mapStatsOrderBy(contract.source.options?.orderBy),
858
+ statsPath: this.buildStatsPath(contract),
859
+ statsRequest: this.buildStatsRequest(contract, combinedFilters),
860
+ dimensions: contract.dimensions?.map((dimension) => dimension.field),
861
+ metrics: contract.metrics?.map((metric) => ({
862
+ field: metric.field,
863
+ aggregation: this.mapAggregation(metric.aggregation),
864
+ alias: metric.field,
865
+ })),
866
+ filters: Object.keys(combinedFilters).length ? combinedFilters : undefined,
867
+ sort: contract.sort?.map((item) => `${item.field}:${item.direction}`),
868
+ limit: contract.limit ?? contract.source.options?.limit,
869
+ };
870
+ }
871
+ buildInteractions(contract) {
872
+ return {
873
+ pointClick: Boolean(contract.events?.pointClick || contract.events?.drillDown),
874
+ selection: Boolean(contract.events?.selectionChange),
875
+ drillDown: Boolean(contract.events?.drillDown),
876
+ };
877
+ }
878
+ buildTheme(contract) {
879
+ return {
880
+ palette: Array.isArray(contract.theme?.palette) ? contract.theme.palette : undefined,
881
+ legend: { visible: this.resolveToggle(contract.legend, true) },
882
+ tooltip: {
883
+ enabled: this.resolveToggle(contract.tooltip, true),
884
+ trigger: contract.kind === 'pie' || contract.kind === 'donut' || contract.kind === 'scatter' ? 'item' : 'axis',
885
+ },
886
+ };
887
+ }
888
+ buildMotion(motion) {
889
+ if (!motion) {
890
+ return undefined;
760
891
  }
761
- const transformed = this.transformer.transform(config, this.resolvedData());
762
- return transformed.hasData ? 'ready' : 'empty';
763
- }, ...(ngDevMode ? [{ debugName: "loadState" }] : []));
764
- constructor() {
765
- effect(() => {
766
- const config = this.config();
767
- const explicitData = this.data();
768
- const nextSignature = explicitData === null || explicitData === undefined
769
- ? this.buildRemoteSignature(config)
770
- : null;
771
- if (nextSignature === this.previousRemoteSignature) {
772
- return;
773
- }
774
- this.previousRemoteSignature = nextSignature;
775
- this.remoteResolvedData.set(null);
776
- this.remoteRuntimeState.set('idle');
777
- this.remoteTechnicalError.set(null);
778
- });
779
- effect(() => {
780
- const nextState = this.loadState();
781
- if (this.currentLoadState() !== nextState) {
782
- this.currentLoadState.set(nextState);
783
- this.loadStateChange.emit(nextState);
784
- }
785
- });
786
- effect(() => {
787
- const config = this.config();
788
- const explicitData = this.data();
789
- if (config.dataSource?.kind !== 'remote' || (explicitData !== null && explicitData !== undefined)) {
790
- return;
791
- }
792
- if (this.remoteRuntimeState() === 'loading' || this.remoteResolvedData() !== null) {
793
- return;
794
- }
795
- const event = {
796
- chartId: config.id,
797
- dataSource: config.dataSource,
798
- query: config.dataSource.query,
799
- };
800
- this.queryRequest.emit(event);
801
- this.remoteRuntimeState.set('loading');
802
- this.remoteResolvedData.set([]);
803
- this.remoteTechnicalError.set(null);
804
- this.statsApi.execute(event, config)
805
- .pipe(takeUntilDestroyed(this.destroyRef))
806
- .subscribe({
807
- next: (rows) => {
808
- this.remoteResolvedData.set(rows);
809
- this.remoteRuntimeState.set('ready');
810
- this.remoteTechnicalError.set(null);
811
- },
812
- error: (error) => {
813
- this.remoteResolvedData.set([]);
814
- this.remoteRuntimeState.set('error');
815
- this.remoteTechnicalError.set(error instanceof Error && error.message.trim()
816
- ? error.message
817
- : 'praxis.stats request failed');
818
- },
819
- });
820
- });
821
- effect(() => {
822
- if (this.loadState() !== 'ready') {
823
- this.engine.destroy();
824
- return;
825
- }
826
- const host = this.chartHost()?.nativeElement;
827
- if (!host)
828
- return;
829
- this.engine.render(host, {
830
- config: this.renderConfig(),
831
- data: this.resolvedData(),
832
- onPointClick: (event) => this.pointClick.emit(event),
833
- });
834
- this.ensureResizeObserver(host);
835
- });
836
- this.destroyRef.onDestroy(() => {
837
- this.resizeObserver()?.disconnect();
838
- this.engine.destroy();
839
- });
892
+ return {
893
+ enabled: motion.enabled,
894
+ preset: motion.preset,
895
+ };
840
896
  }
841
- ensureResizeObserver(host) {
842
- if (this.resizeObserver()) {
843
- return;
897
+ resolveOrientation(contract) {
898
+ if (contract.kind === 'horizontal-bar') {
899
+ return 'horizontal';
844
900
  }
845
- const observer = new ResizeObserver(() => {
846
- this.engine.resize();
847
- });
848
- observer.observe(host);
849
- this.resizeObserver.set(observer);
901
+ return contract.orientation;
850
902
  }
851
- buildRemoteSignature(config) {
852
- if (config.dataSource?.kind !== 'remote') {
853
- return null;
903
+ resolveSeriesType(chartKind, seriesKind, index) {
904
+ if (chartKind === 'combo') {
905
+ return seriesKind ?? (index === 0 ? 'bar' : 'line');
854
906
  }
855
- return JSON.stringify({
856
- id: config.id,
857
- resourcePath: config.dataSource.resourcePath,
858
- query: config.dataSource.query,
859
- });
907
+ return chartKind === 'stacked-bar' ? 'bar' : chartKind;
860
908
  }
861
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: PraxisChartComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
862
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.17", type: PraxisChartComponent, isStandalone: true, selector: "praxis-chart", inputs: { config: { classPropertyName: "config", publicName: "config", isSignal: true, isRequired: true, transformFunction: null }, data: { classPropertyName: "data", publicName: "data", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { pointClick: "pointClick", queryRequest: "queryRequest", loadStateChange: "loadStateChange" }, providers: [
863
- EChartsEngineAdapter,
864
- {
865
- provide: PRAXIS_CHART_ENGINE,
866
- useExisting: EChartsEngineAdapter,
867
- },
868
- ], viewQueries: [{ propertyName: "chartHost", first: true, predicate: ["chartHost"], descendants: true, isSignal: true }], ngImport: i0, template: `
869
- <section class="praxis-chart-shell" [style.height]="resolvedHeight()">
870
- @if (loadState() === 'loading') {
871
- <div class="praxis-chart-state praxis-chart-state-loading">
872
- <div class="praxis-chart-spinner" aria-hidden="true"></div>
873
- @if (loadingLabel()) {
874
- <div class="praxis-chart-state-title">{{ loadingLabel() }}</div>
875
- }
876
- </div>
877
- } @else if (loadState() === 'error') {
878
- <div class="praxis-chart-state praxis-chart-state-error">
879
- @if (errorTitle()) {
880
- <div class="praxis-chart-state-title">{{ errorTitle() }}</div>
881
- }
882
- @if (errorDescription()) {
883
- <div class="praxis-chart-state-description">{{ errorDescription() }}</div>
884
- }
885
- </div>
886
- } @else if (loadState() === 'empty') {
887
- <div class="praxis-chart-state praxis-chart-state-empty">
888
- @if (emptyTitle()) {
889
- <div class="praxis-chart-state-title">{{ emptyTitle() }}</div>
890
- }
891
- @if (emptyDescription()) {
892
- <div class="praxis-chart-state-description">{{ emptyDescription() }}</div>
893
- }
894
- </div>
895
- } @else {
896
- <div #chartHost class="praxis-chart-host"></div>
897
- }
898
- </section>
899
- `, isInline: true, styles: [":host{display:block;min-width:0}.praxis-chart-shell{position:relative;width:100%;min-height:240px;border-radius:18px;overflow:hidden;background:radial-gradient(circle at top left,rgba(18,99,180,.12),transparent 38%),linear-gradient(180deg,#1263b408,#1263b400);border:1px solid color-mix(in srgb,var(--md-sys-color-outline, #c5c7ce) 72%,transparent)}.praxis-chart-host{width:100%;height:100%}.praxis-chart-state{height:100%;min-height:240px;display:grid;place-content:center;gap:10px;padding:24px;text-align:center}.praxis-chart-state-title{font-size:1rem;font-weight:600;color:var(--md-sys-color-on-surface, #1a1b20)}.praxis-chart-state-description{font-size:.925rem;color:var(--md-sys-color-on-surface-variant, #5a5d67);max-width:36rem}.praxis-chart-spinner{width:34px;height:34px;margin-inline:auto;border-radius:999px;border:3px solid rgba(18,99,180,.18);border-top-color:#1263b4d1;animation:praxis-chart-spin .8s linear infinite}@keyframes praxis-chart-spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
900
- }
901
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: PraxisChartComponent, decorators: [{
902
- type: Component,
903
- args: [{ selector: 'praxis-chart', standalone: true, imports: [CommonModule], providers: [
904
- EChartsEngineAdapter,
905
- {
906
- provide: PRAXIS_CHART_ENGINE,
907
- useExisting: EChartsEngineAdapter,
908
- },
909
- ], changeDetection: ChangeDetectionStrategy.OnPush, template: `
910
- <section class="praxis-chart-shell" [style.height]="resolvedHeight()">
911
- @if (loadState() === 'loading') {
912
- <div class="praxis-chart-state praxis-chart-state-loading">
913
- <div class="praxis-chart-spinner" aria-hidden="true"></div>
914
- @if (loadingLabel()) {
915
- <div class="praxis-chart-state-title">{{ loadingLabel() }}</div>
916
- }
917
- </div>
918
- } @else if (loadState() === 'error') {
919
- <div class="praxis-chart-state praxis-chart-state-error">
920
- @if (errorTitle()) {
921
- <div class="praxis-chart-state-title">{{ errorTitle() }}</div>
922
- }
923
- @if (errorDescription()) {
924
- <div class="praxis-chart-state-description">{{ errorDescription() }}</div>
925
- }
926
- </div>
927
- } @else if (loadState() === 'empty') {
928
- <div class="praxis-chart-state praxis-chart-state-empty">
929
- @if (emptyTitle()) {
930
- <div class="praxis-chart-state-title">{{ emptyTitle() }}</div>
931
- }
932
- @if (emptyDescription()) {
933
- <div class="praxis-chart-state-description">{{ emptyDescription() }}</div>
934
- }
935
- </div>
936
- } @else {
937
- <div #chartHost class="praxis-chart-host"></div>
938
- }
939
- </section>
940
- `, styles: [":host{display:block;min-width:0}.praxis-chart-shell{position:relative;width:100%;min-height:240px;border-radius:18px;overflow:hidden;background:radial-gradient(circle at top left,rgba(18,99,180,.12),transparent 38%),linear-gradient(180deg,#1263b408,#1263b400);border:1px solid color-mix(in srgb,var(--md-sys-color-outline, #c5c7ce) 72%,transparent)}.praxis-chart-host{width:100%;height:100%}.praxis-chart-state{height:100%;min-height:240px;display:grid;place-content:center;gap:10px;padding:24px;text-align:center}.praxis-chart-state-title{font-size:1rem;font-weight:600;color:var(--md-sys-color-on-surface, #1a1b20)}.praxis-chart-state-description{font-size:.925rem;color:var(--md-sys-color-on-surface-variant, #5a5d67);max-width:36rem}.praxis-chart-spinner{width:34px;height:34px;margin-inline:auto;border-radius:999px;border:3px solid rgba(18,99,180,.18);border-top-color:#1263b4d1;animation:praxis-chart-spin .8s linear infinite}@keyframes praxis-chart-spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}\n"] }]
941
- }], ctorParameters: () => [], propDecorators: { config: [{ type: i0.Input, args: [{ isSignal: true, alias: "config", required: true }] }], data: [{ type: i0.Input, args: [{ isSignal: true, alias: "data", required: false }] }], pointClick: [{ type: i0.Output, args: ["pointClick"] }], queryRequest: [{ type: i0.Output, args: ["queryRequest"] }], loadStateChange: [{ type: i0.Output, args: ["loadStateChange"] }], chartHost: [{ type: i0.ViewChild, args: ['chartHost', { isSignal: true }] }] } });
942
-
943
- const PRAXIS_CHART_DRILLDOWN_DATA_BY_MONTH = {
944
- Jan: [
945
- { segment: 'Enterprise', total: 52000 },
946
- { segment: 'Mid-market', total: 38000 },
947
- { segment: 'SMB', total: 30000 },
948
- ],
949
- Fev: [
950
- { segment: 'Enterprise', total: 61000 },
951
- { segment: 'Mid-market', total: 42000 },
952
- { segment: 'SMB', total: 35000 },
953
- ],
954
- Mar: [
955
- { segment: 'Enterprise', total: 68000 },
956
- { segment: 'Mid-market', total: 47000 },
957
- { segment: 'SMB', total: 36000 },
958
- ],
959
- Abr: [
960
- { segment: 'Enterprise', total: 64000 },
961
- { segment: 'Mid-market', total: 49500 },
962
- { segment: 'SMB', total: 36000 },
963
- ],
964
- };
965
-
966
- class PraxisChartDrilldownPanelComponent {
967
- title = input('Detalhamento por segmento', ...(ngDevMode ? [{ debugName: "title" }] : []));
968
- selection = input(null, ...(ngDevMode ? [{ debugName: "selection" }] : []));
969
- activeCategory = computed(() => {
970
- const raw = this.selection()?.category;
971
- return typeof raw === 'string' ? raw : null;
972
- }, ...(ngDevMode ? [{ debugName: "activeCategory" }] : []));
973
- detailData = computed(() => {
974
- const category = this.activeCategory();
975
- if (!category)
976
- return [];
977
- return PRAXIS_CHART_DRILLDOWN_DATA_BY_MONTH[category] ?? [];
978
- }, ...(ngDevMode ? [{ debugName: "detailData" }] : []));
979
- detailChartConfig = computed(() => {
980
- const category = this.activeCategory();
909
+ shouldSmoothSeries(chartKind, seriesKind) {
910
+ if (chartKind === 'combo') {
911
+ return seriesKind === 'line' || seriesKind === 'area' ? true : undefined;
912
+ }
913
+ return chartKind === 'line' || chartKind === 'area' || chartKind === 'stacked-area' ? true : undefined;
914
+ }
915
+ mapTextValue(value) {
916
+ if (!value)
917
+ return undefined;
918
+ if (typeof value === 'string')
919
+ return value;
981
920
  return {
982
- id: 'chart-drilldown-detail',
983
- type: 'donut',
984
- title: { text: category ? `Mix de receita em ${category}` : 'Aguardando selecao' },
985
- subtitle: { text: category ? 'Drill-down local com JSON mockado' : 'Selecione um ponto no chart principal' },
986
- height: 280,
987
- series: [
988
- {
989
- id: 'segmentMix',
990
- categoryField: 'segment',
991
- metric: { field: 'total', aggregation: 'sum' },
992
- labels: { visible: true },
993
- },
994
- ],
995
- emptyState: {
996
- title: { text: 'Nenhum recorte selecionado' },
997
- description: { text: 'O painel de drill-down usa mocks locais e reage ao pointClick do chart principal.' },
998
- },
999
- theme: {
1000
- palette: ['#1263b4', '#2b8a3e', '#f08c00', '#7b61ff'],
1001
- },
921
+ key: value.key,
922
+ text: value.fallback,
1002
923
  };
1003
- }, ...(ngDevMode ? [{ debugName: "detailChartConfig" }] : []));
1004
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: PraxisChartDrilldownPanelComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1005
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.17", type: PraxisChartDrilldownPanelComponent, isStandalone: true, selector: "praxis-chart-drilldown-panel", inputs: { title: { classPropertyName: "title", publicName: "title", isSignal: true, isRequired: false, transformFunction: null }, selection: { classPropertyName: "selection", publicName: "selection", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: `
1006
- <section class="drilldown-shell">
1007
- <header class="drilldown-header">
1008
- <p class="drilldown-eyebrow">Drill-down local</p>
1009
- <h3>{{ title() }}</h3>
1010
- @if (activeCategory()) {
1011
- <p class="drilldown-description">Recorte atual: {{ activeCategory() }}</p>
1012
- } @else {
1013
- <p class="drilldown-description">Clique em uma barra do chart principal para abrir o detalhamento local.</p>
1014
- }
1015
- </header>
1016
-
1017
- <praxis-chart
1018
- [config]="detailChartConfig()"
1019
- [data]="detailData()"
1020
- ></praxis-chart>
1021
- </section>
1022
- `, isInline: true, styles: [":host{display:block}.drilldown-shell{display:grid;gap:14px}.drilldown-header h3,.drilldown-eyebrow,.drilldown-description{margin:0}.drilldown-eyebrow{font-size:.75rem;letter-spacing:.14em;text-transform:uppercase;color:var(--md-sys-color-on-surface-variant, #5a5d67)}.drilldown-header h3{font-size:1.2rem;color:var(--md-sys-color-on-surface, #1a1b20)}.drilldown-description{color:var(--md-sys-color-on-surface-variant, #5a5d67)}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: PraxisChartComponent, selector: "praxis-chart", inputs: ["config", "data"], outputs: ["pointClick", "queryRequest", "loadStateChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1023
- }
1024
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: PraxisChartDrilldownPanelComponent, decorators: [{
1025
- type: Component,
1026
- args: [{ selector: 'praxis-chart-drilldown-panel', standalone: true, imports: [CommonModule, PraxisChartComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: `
1027
- <section class="drilldown-shell">
1028
- <header class="drilldown-header">
1029
- <p class="drilldown-eyebrow">Drill-down local</p>
1030
- <h3>{{ title() }}</h3>
1031
- @if (activeCategory()) {
1032
- <p class="drilldown-description">Recorte atual: {{ activeCategory() }}</p>
1033
- } @else {
1034
- <p class="drilldown-description">Clique em uma barra do chart principal para abrir o detalhamento local.</p>
924
+ }
925
+ toLabel(value) {
926
+ if (!value)
927
+ return undefined;
928
+ if (typeof value === 'string')
929
+ return value;
930
+ return value.fallback || value.key;
931
+ }
932
+ resolveToggle(value, defaultValue = false) {
933
+ if (typeof value === 'boolean')
934
+ return value;
935
+ if (typeof value === 'object' && value)
936
+ return value.enabled;
937
+ return defaultValue;
938
+ }
939
+ mapAggregation(aggregation) {
940
+ switch (aggregation) {
941
+ case 'avg':
942
+ case 'min':
943
+ case 'max':
944
+ case 'count':
945
+ case 'sum':
946
+ return aggregation;
947
+ case undefined:
948
+ return undefined;
949
+ default:
950
+ throw new Error(`x-ui.chart aggregation "${aggregation}" is not yet implemented in @praxisui/charts.`);
1035
951
  }
1036
- </header>
1037
-
1038
- <praxis-chart
1039
- [config]="detailChartConfig()"
1040
- [data]="detailData()"
1041
- ></praxis-chart>
1042
- </section>
1043
- `, styles: [":host{display:block}.drilldown-shell{display:grid;gap:14px}.drilldown-header h3,.drilldown-eyebrow,.drilldown-description{margin:0}.drilldown-eyebrow{font-size:.75rem;letter-spacing:.14em;text-transform:uppercase;color:var(--md-sys-color-on-surface-variant, #5a5d67)}.drilldown-header h3{font-size:1.2rem;color:var(--md-sys-color-on-surface, #1a1b20)}.drilldown-description{color:var(--md-sys-color-on-surface-variant, #5a5d67)}\n"] }]
1044
- }], propDecorators: { title: [{ type: i0.Input, args: [{ isSignal: true, alias: "title", required: false }] }], selection: [{ type: i0.Input, args: [{ isSignal: true, alias: "selection", required: false }] }] } });
1045
-
1046
- class PraxisChartStateProbeComponent {
1047
- title = input('Chart Runtime Probe', ...(ngDevMode ? [{ debugName: "title" }] : []));
1048
- value = input(null, ...(ngDevMode ? [{ debugName: "value" }] : []));
1049
- serializedValue = computed(() => JSON.stringify(this.value(), null, 2), ...(ngDevMode ? [{ debugName: "serializedValue" }] : []));
1050
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: PraxisChartStateProbeComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1051
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.17", type: PraxisChartStateProbeComponent, isStandalone: true, selector: "praxis-chart-state-probe", inputs: { title: { classPropertyName: "title", publicName: "title", isSignal: true, isRequired: false, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: `
1052
- <section class="probe-shell">
1053
- <header class="probe-header">
1054
- <p class="probe-eyebrow">Probe</p>
1055
- <h3>{{ title() }}</h3>
1056
- </header>
1057
-
1058
- @if (value() === null || value() === undefined || value() === '') {
1059
- <div class="probe-empty">Aguardando eventos do runtime.</div>
1060
- } @else {
1061
- <pre>{{ serializedValue() }}</pre>
1062
- }
1063
- </section>
1064
- `, isInline: true, styles: [":host{display:block}.probe-shell{display:grid;gap:12px;min-height:220px;padding:18px;border-radius:20px;background:linear-gradient(180deg,#0b111ff5,#0b111fe0),radial-gradient(circle at top right,rgba(18,99,180,.32),transparent 35%);color:#d7e6ff}.probe-header h3,.probe-eyebrow{margin:0}.probe-eyebrow{font-size:.75rem;letter-spacing:.12em;text-transform:uppercase;color:#d7e6ffb3}.probe-empty{color:#d7e6ffc7}pre{margin:0;overflow:auto;white-space:pre-wrap;word-break:break-word;font-size:.84rem;line-height:1.45}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1065
- }
1066
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: PraxisChartStateProbeComponent, decorators: [{
1067
- type: Component,
1068
- args: [{ selector: 'praxis-chart-state-probe', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `
1069
- <section class="probe-shell">
1070
- <header class="probe-header">
1071
- <p class="probe-eyebrow">Probe</p>
1072
- <h3>{{ title() }}</h3>
1073
- </header>
1074
-
1075
- @if (value() === null || value() === undefined || value() === '') {
1076
- <div class="probe-empty">Aguardando eventos do runtime.</div>
1077
- } @else {
1078
- <pre>{{ serializedValue() }}</pre>
1079
- }
1080
- </section>
1081
- `, styles: [":host{display:block}.probe-shell{display:grid;gap:12px;min-height:220px;padding:18px;border-radius:20px;background:linear-gradient(180deg,#0b111ff5,#0b111fe0),radial-gradient(circle at top right,rgba(18,99,180,.32),transparent 35%);color:#d7e6ff}.probe-header h3,.probe-eyebrow{margin:0}.probe-eyebrow{font-size:.75rem;letter-spacing:.12em;text-transform:uppercase;color:#d7e6ffb3}.probe-empty{color:#d7e6ffc7}pre{margin:0;overflow:auto;white-space:pre-wrap;word-break:break-word;font-size:.84rem;line-height:1.45}\n"] }]
1082
- }], propDecorators: { title: [{ type: i0.Input, args: [{ isSignal: true, alias: "title", required: false }] }], value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }] } });
1083
-
1084
- const PRAXIS_CHART_COMPONENT_METADATA = {
1085
- id: 'praxis-chart',
1086
- componentType: 'praxis-chart',
1087
- displayName: 'Praxis Chart',
1088
- selector: 'praxis-chart',
1089
- component: PraxisChartComponent,
1090
- friendlyName: 'Praxis Chart',
1091
- description: 'Chart component for analytics and metadata-driven visualizations in Praxis UI.',
1092
- icon: 'bar_chart',
1093
- tags: ['chart', 'analytics', 'widget', 'visualization'],
1094
- lib: '@praxisui/charts',
1095
- inputs: [
1096
- {
1097
- name: 'config',
1098
- type: 'PraxisChartConfig',
1099
- description: 'Declarative chart configuration with metadata-oriented semantics.',
1100
- },
1101
- {
1102
- name: 'data',
1103
- type: 'PraxisChartDataRow[]',
1104
- description: 'Optional local dataset that takes precedence over local datasource items.',
1105
- },
1106
- ],
1107
- outputs: [
1108
- {
1109
- name: 'pointClick',
1110
- type: 'PraxisChartPointEvent',
1111
- description: 'Emitted when the host wants to react to a point/series click.',
1112
- },
1113
- {
1114
- name: 'queryRequest',
1115
- type: 'PraxisChartQueryRequestEvent',
1116
- description: 'Emitted before a remote praxis.stats datasource is resolved, allowing host-side observability of the outgoing request.',
1117
- },
1118
- {
1119
- name: 'loadStateChange',
1120
- type: 'PraxisChartLoadState',
1121
- description: 'Emitted when the chart state changes.',
1122
- },
1123
- ],
1124
- };
1125
- function providePraxisChartsMetadata() {
1126
- return {
1127
- provide: ENVIRONMENT_INITIALIZER,
1128
- multi: true,
1129
- useFactory: (registry) => () => {
1130
- registry.register(PRAXIS_CHART_COMPONENT_METADATA);
1131
- },
1132
- deps: [ComponentMetadataRegistry],
1133
- };
952
+ }
953
+ toQueryFilterMap(filters) {
954
+ if (!filters?.length)
955
+ return undefined;
956
+ return filters.reduce((acc, filter) => {
957
+ if (filter.value !== undefined) {
958
+ acc[filter.field] = filter.value;
959
+ return acc;
960
+ }
961
+ if (filter.values !== undefined) {
962
+ acc[filter.field] = filter.values;
963
+ }
964
+ return acc;
965
+ }, {});
966
+ }
967
+ buildStatsPath(contract) {
968
+ return `${contract.source.resource.replace(/\/+$/, '')}/stats/${contract.source.operation}`;
969
+ }
970
+ buildStatsRequest(contract, filters) {
971
+ const field = this.resolveStatsField(contract);
972
+ const metrics = this.buildStatsMetricRequests(contract);
973
+ const metric = metrics[0];
974
+ const limit = contract.limit ?? contract.source.options?.limit;
975
+ const orderBy = this.mapStatsOrderByToBackend(contract.source.options?.orderBy);
976
+ const requestMetrics = metrics.length > 1 ? metrics : undefined;
977
+ switch (contract.source.operation) {
978
+ case 'group-by':
979
+ return {
980
+ filter: filters,
981
+ field,
982
+ metric,
983
+ metrics: requestMetrics,
984
+ limit,
985
+ orderBy,
986
+ };
987
+ case 'timeseries':
988
+ return {
989
+ filter: filters,
990
+ field,
991
+ granularity: this.mapStatsGranularityToBackend(contract.source.options?.granularity),
992
+ metric,
993
+ metrics: requestMetrics,
994
+ fillGaps: contract.source.options?.fillGaps,
995
+ };
996
+ case 'distribution':
997
+ return {
998
+ filter: filters,
999
+ field,
1000
+ mode: this.mapDistributionModeToBackend(contract.source.options?.mode),
1001
+ metric,
1002
+ bucketSize: contract.source.options?.bucketSize,
1003
+ bucketCount: contract.source.options?.bucketCount,
1004
+ limit,
1005
+ orderBy,
1006
+ };
1007
+ default:
1008
+ throw new Error(`x-ui.chart source.operation "${contract.source.operation}" is not supported in @praxisui/charts.`);
1009
+ }
1010
+ }
1011
+ resolveStatsField(contract) {
1012
+ const dimensionField = contract.dimensions?.[0]?.field?.trim();
1013
+ if (dimensionField) {
1014
+ return dimensionField;
1015
+ }
1016
+ throw new Error('x-ui.chart requires dimensions[0].field to derive the canonical praxis.stats request.');
1017
+ }
1018
+ buildStatsMetricRequest(contract) {
1019
+ const metric = contract.metrics?.[0];
1020
+ if (!metric) {
1021
+ throw new Error('x-ui.chart requires metrics[0] to derive the canonical praxis.stats request.');
1022
+ }
1023
+ const operation = this.mapStatsMetricOperation(metric.aggregation);
1024
+ return {
1025
+ operation,
1026
+ field: operation === 'COUNT' ? undefined : metric.field,
1027
+ alias: metric.field,
1028
+ };
1029
+ }
1030
+ buildStatsMetricRequests(contract) {
1031
+ if (!contract.metrics?.length) {
1032
+ throw new Error('x-ui.chart requires at least one metric to derive the canonical praxis.stats request.');
1033
+ }
1034
+ return contract.metrics.map((metric) => {
1035
+ const operation = this.mapStatsMetricOperation(metric.aggregation);
1036
+ return {
1037
+ operation,
1038
+ field: operation === 'COUNT' ? undefined : metric.field,
1039
+ alias: metric.field,
1040
+ };
1041
+ });
1042
+ }
1043
+ mapStatsOrderBy(value) {
1044
+ if (!value)
1045
+ return undefined;
1046
+ switch (value) {
1047
+ case 'key-asc':
1048
+ case 'key-desc':
1049
+ case 'value-asc':
1050
+ case 'value-desc':
1051
+ return value;
1052
+ default:
1053
+ throw new Error(`x-ui.chart source.options.orderBy "${value}" is not supported in @praxisui/charts.`);
1054
+ }
1055
+ }
1056
+ mapStatsOrderByToBackend(value) {
1057
+ switch (value) {
1058
+ case 'key-asc':
1059
+ return 'KEY_ASC';
1060
+ case 'key-desc':
1061
+ return 'KEY_DESC';
1062
+ case 'value-asc':
1063
+ return 'VALUE_ASC';
1064
+ case 'value-desc':
1065
+ return 'VALUE_DESC';
1066
+ case undefined:
1067
+ return undefined;
1068
+ default:
1069
+ throw new Error(`x-ui.chart source.options.orderBy "${value}" is not supported in @praxisui/charts.`);
1070
+ }
1071
+ }
1072
+ mapStatsGranularityToBackend(value) {
1073
+ switch (value) {
1074
+ case 'hour':
1075
+ return 'HOUR';
1076
+ case 'day':
1077
+ return 'DAY';
1078
+ case 'week':
1079
+ return 'WEEK';
1080
+ case 'month':
1081
+ case undefined:
1082
+ return 'MONTH';
1083
+ case 'quarter':
1084
+ return 'QUARTER';
1085
+ case 'year':
1086
+ return 'YEAR';
1087
+ default:
1088
+ throw new Error(`x-ui.chart source.options.granularity "${value}" is not supported in @praxisui/charts.`);
1089
+ }
1090
+ }
1091
+ mapDistributionModeToBackend(value) {
1092
+ switch (value) {
1093
+ case 'histogram':
1094
+ return 'HISTOGRAM';
1095
+ case 'terms':
1096
+ case undefined:
1097
+ return 'TERMS';
1098
+ default:
1099
+ throw new Error(`x-ui.chart source.options.mode "${value}" is not supported in @praxisui/charts.`);
1100
+ }
1101
+ }
1102
+ mapStatsMetricOperation(aggregation) {
1103
+ switch (aggregation) {
1104
+ case undefined:
1105
+ case 'count':
1106
+ return 'COUNT';
1107
+ case 'sum':
1108
+ return 'SUM';
1109
+ case 'avg':
1110
+ return 'AVG';
1111
+ case 'min':
1112
+ return 'MIN';
1113
+ case 'max':
1114
+ return 'MAX';
1115
+ default:
1116
+ throw new Error(`x-ui.chart aggregation "${aggregation}" is not yet implemented in @praxisui/charts.`);
1117
+ }
1118
+ }
1119
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: PraxisChartCanonicalContractMapperService, deps: [{ token: ChartContractNormalizerService }, { token: ChartContractValidationService }], target: i0.ɵɵFactoryTarget.Injectable });
1120
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: PraxisChartCanonicalContractMapperService, providedIn: 'root' });
1134
1121
  }
1122
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: PraxisChartCanonicalContractMapperService, decorators: [{
1123
+ type: Injectable,
1124
+ args: [{ providedIn: 'root' }]
1125
+ }], ctorParameters: () => [{ type: ChartContractNormalizerService }, { type: ChartContractValidationService }] });
1135
1126
 
1136
- const PRAXIS_CHART_DRILLDOWN_PANEL_METADATA = {
1137
- id: 'praxis-chart-drilldown-panel',
1138
- componentType: 'praxis-chart-drilldown-panel',
1139
- displayName: 'Praxis Chart Drilldown Panel',
1140
- selector: 'praxis-chart-drilldown-panel',
1141
- component: PraxisChartDrilldownPanelComponent,
1142
- friendlyName: 'Praxis Chart Drilldown Panel',
1143
- description: 'Local drill-down panel that consumes chart point events and renders a derived detail chart.',
1144
- icon: 'query_stats',
1145
- tags: ['chart', 'drilldown', 'widget', 'analytics'],
1146
- lib: '@praxisui/charts',
1147
- inputs: [
1148
- {
1149
- name: 'title',
1150
- type: 'string',
1151
- description: 'Panel title for the drill-down view.',
1152
- },
1153
- {
1154
- name: 'selection',
1155
- type: 'PraxisChartPointEvent',
1156
- description: 'Event emitted by a source chart and used to resolve local detail datasets.',
1157
- },
1158
- ],
1159
- };
1160
- function providePraxisChartDrilldownPanelMetadata() {
1161
- return {
1162
- provide: ENVIRONMENT_INITIALIZER,
1163
- multi: true,
1164
- useFactory: (registry) => () => {
1165
- registry.register(PRAXIS_CHART_DRILLDOWN_PANEL_METADATA);
1166
- },
1167
- deps: [ComponentMetadataRegistry],
1168
- };
1127
+ class PraxisChartStatsApiService {
1128
+ http;
1129
+ apiUrl;
1130
+ constructor(http, apiUrl) {
1131
+ this.http = http;
1132
+ this.apiUrl = apiUrl;
1133
+ }
1134
+ execute(event, config) {
1135
+ const query = event.query;
1136
+ const statsRequest = query?.statsRequest;
1137
+ if (!query?.statsPath || !statsRequest) {
1138
+ return throwError(() => new Error('PraxisChartStatsApiService requires query.statsPath and query.statsRequest for praxis.stats execution.'));
1139
+ }
1140
+ const categoryField = this.resolveCategoryField(config, statsRequest);
1141
+ const url = this.buildStatsUrl(query.statsPath);
1142
+ return this.http
1143
+ .post(url, statsRequest)
1144
+ .pipe(map((response) => this.toChartRows(response?.data, statsRequest, categoryField, config)), catchError((error) => this.handleHttpError(error)));
1145
+ }
1146
+ toChartRows(response, request, categoryField, config) {
1147
+ if (!response) {
1148
+ return [];
1149
+ }
1150
+ const metricBindings = this.resolveMetricBindings(config, request, response);
1151
+ if ('points' in response) {
1152
+ return response.points.map((point) => {
1153
+ const category = point.label ?? point.start ?? point.end ?? '';
1154
+ return {
1155
+ [categoryField]: category,
1156
+ ...this.projectMetricValues(metricBindings, point.values, point.value, point.count),
1157
+ key: point.start ?? point.label ?? point.end ?? category,
1158
+ label: point.label ?? category,
1159
+ value: point.value ?? null,
1160
+ count: point.count ?? null,
1161
+ start: point.start ?? null,
1162
+ end: point.end ?? null,
1163
+ granularity: response.granularity ?? null,
1164
+ };
1165
+ });
1166
+ }
1167
+ if ('buckets' in response) {
1168
+ return response.buckets.map((bucket) => {
1169
+ const category = this.resolveBucketCategory(bucket);
1170
+ return {
1171
+ [categoryField]: category,
1172
+ ...this.projectMetricValues(metricBindings, bucket.values, bucket.value, bucket.count),
1173
+ key: bucket.key ?? null,
1174
+ label: bucket.label ?? category,
1175
+ value: bucket.value ?? null,
1176
+ count: bucket.count ?? null,
1177
+ from: bucket.from ?? null,
1178
+ to: bucket.to ?? null,
1179
+ mode: 'mode' in response ? response.mode ?? null : null,
1180
+ requestMode: 'mode' in request ? request.mode : null,
1181
+ };
1182
+ });
1183
+ }
1184
+ return [];
1185
+ }
1186
+ resolveBucketCategory(bucket) {
1187
+ if (bucket.label !== null && bucket.label !== undefined && bucket.label !== '') {
1188
+ return String(bucket.label);
1189
+ }
1190
+ if (bucket.key !== null && bucket.key !== undefined && bucket.key !== '') {
1191
+ return String(bucket.key);
1192
+ }
1193
+ if (bucket.from !== null || bucket.to !== null) {
1194
+ return `${bucket.from ?? ''} - ${bucket.to ?? ''}`.trim();
1195
+ }
1196
+ return '';
1197
+ }
1198
+ resolveMetricValue(value, count) {
1199
+ if (typeof value === 'number' && Number.isFinite(value)) {
1200
+ return value;
1201
+ }
1202
+ if (typeof count === 'number' && Number.isFinite(count)) {
1203
+ return count;
1204
+ }
1205
+ return 0;
1206
+ }
1207
+ resolveCategoryField(config, request) {
1208
+ const firstSeries = config.series[0];
1209
+ return (firstSeries?.categoryField
1210
+ || config.axes?.x?.field
1211
+ || request.field
1212
+ || 'category');
1213
+ }
1214
+ resolveMetricBindings(config, request, response) {
1215
+ const queryMetrics = config.dataSource?.kind === 'remote'
1216
+ ? (config.dataSource.query?.metrics ?? [])
1217
+ : [];
1218
+ if (queryMetrics.length) {
1219
+ return queryMetrics.map((metric, index) => ({
1220
+ field: metric.field || `value${index + 1}`,
1221
+ alias: metric.alias || metric.field || `value${index + 1}`,
1222
+ }));
1223
+ }
1224
+ const responseMetrics = 'metrics' in response ? response.metrics : undefined;
1225
+ if (responseMetrics?.length) {
1226
+ return responseMetrics.map((metric, index) => ({
1227
+ field: metric.alias || metric.field || `value${index + 1}`,
1228
+ alias: metric.alias || metric.field || `value${index + 1}`,
1229
+ }));
1230
+ }
1231
+ const requestMetrics = 'metrics' in request ? request.metrics : undefined;
1232
+ if (requestMetrics?.length) {
1233
+ return requestMetrics.map((metric, index) => ({
1234
+ field: metric.alias || metric.field || `value${index + 1}`,
1235
+ alias: metric.alias || metric.field || `value${index + 1}`,
1236
+ }));
1237
+ }
1238
+ const metric = ('metric' in response && response.metric) || ('metric' in request ? request.metric : undefined);
1239
+ return [
1240
+ {
1241
+ field: metric?.alias || metric?.field || config.series[0]?.metric?.field || 'value',
1242
+ alias: metric?.alias || metric?.field || config.series[0]?.metric?.field || 'value',
1243
+ },
1244
+ ];
1245
+ }
1246
+ projectMetricValues(bindings, values, primaryValue, count) {
1247
+ return bindings.reduce((acc, binding, index) => {
1248
+ const rawValue = values?.[binding.alias];
1249
+ acc[binding.field] = index === 0
1250
+ ? this.resolveMetricValue(rawValue ?? primaryValue, count)
1251
+ : this.resolveMetricValue(rawValue, null);
1252
+ return acc;
1253
+ }, {});
1254
+ }
1255
+ buildStatsUrl(statsPath) {
1256
+ const base = this.buildDefaultApiBase();
1257
+ const normalizedStatsPath = this.normalizePath(statsPath);
1258
+ const resolvedStatsPath = this.shouldStripLeadingApiSegment(base)
1259
+ ? this.stripLeadingApiSegment(normalizedStatsPath)
1260
+ : normalizedStatsPath;
1261
+ return `${base}/${resolvedStatsPath}`;
1262
+ }
1263
+ buildDefaultApiBase() {
1264
+ const entry = this.apiUrl?.default ?? {};
1265
+ return buildApiUrl(entry).replace(/\/+$/, '');
1266
+ }
1267
+ normalizePath(value) {
1268
+ return String(value || '').trim().replace(/^\/+|\/+$/g, '');
1269
+ }
1270
+ stripLeadingApiSegment(value) {
1271
+ return value.replace(/^api\/+/i, '');
1272
+ }
1273
+ shouldStripLeadingApiSegment(base) {
1274
+ return /\/api$/i.test(base);
1275
+ }
1276
+ handleHttpError(error) {
1277
+ if (error instanceof HttpErrorResponse) {
1278
+ const detail = typeof error.message === 'string' && error.message.trim()
1279
+ ? error.message
1280
+ : `HTTP ${error.status || 0}`;
1281
+ return throwError(() => new Error(detail));
1282
+ }
1283
+ return throwError(() => error instanceof Error ? error : new Error('Unexpected failure during praxis.stats execution.'));
1284
+ }
1285
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: PraxisChartStatsApiService, deps: [{ token: i1.HttpClient }, { token: API_URL }], target: i0.ɵɵFactoryTarget.Injectable });
1286
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: PraxisChartStatsApiService, providedIn: 'root' });
1169
1287
  }
1288
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: PraxisChartStatsApiService, decorators: [{
1289
+ type: Injectable,
1290
+ args: [{ providedIn: 'root' }]
1291
+ }], ctorParameters: () => [{ type: i1.HttpClient }, { type: undefined, decorators: [{
1292
+ type: Inject,
1293
+ args: [API_URL]
1294
+ }] }] });
1170
1295
 
1171
- const PRAXIS_CHART_STATE_PROBE_COMPONENT_METADATA = {
1172
- id: 'praxis-chart-state-probe',
1173
- componentType: 'praxis-chart-state-probe',
1174
- displayName: 'Praxis Chart State Probe',
1175
- selector: 'praxis-chart-state-probe',
1176
- component: PraxisChartStateProbeComponent,
1177
- friendlyName: 'Praxis Chart State Probe',
1178
- description: 'Diagnostic widget used to inspect chart events and runtime payloads during local validation.',
1179
- icon: 'monitoring',
1180
- tags: ['chart', 'probe', 'debug', 'widget'],
1181
- lib: '@praxisui/charts',
1182
- inputs: [
1183
- {
1184
- name: 'title',
1185
- type: 'string',
1186
- description: 'Probe panel title.',
1187
- },
1188
- {
1189
- name: 'value',
1190
- type: 'unknown',
1191
- description: 'Payload rendered as formatted JSON.',
1192
- },
1193
- ],
1194
- };
1195
- function providePraxisChartStateProbeMetadata() {
1196
- return {
1197
- provide: ENVIRONMENT_INITIALIZER,
1198
- multi: true,
1199
- useFactory: (registry) => () => {
1200
- registry.register(PRAXIS_CHART_STATE_PROBE_COMPONENT_METADATA);
1201
- },
1202
- deps: [ComponentMetadataRegistry],
1203
- };
1204
- }
1296
+ const PRAXIS_CHART_ENGINE = new InjectionToken('PRAXIS_CHART_ENGINE');
1205
1297
 
1206
- class PraxisChartShowcaseWidgetComponent {
1298
+ class PraxisChartComponent {
1207
1299
  config = input.required(...(ngDevMode ? [{ debugName: "config" }] : []));
1208
1300
  data = input(null, ...(ngDevMode ? [{ debugName: "data" }] : []));
1209
- viewMode = input('chart', ...(ngDevMode ? [{ debugName: "viewMode" }] : []));
1301
+ chartDocument = input(null, ...(ngDevMode ? [{ debugName: "chartDocument" }] : []));
1302
+ filterCriteria = input(null, ...(ngDevMode ? [{ debugName: "filterCriteria" }] : []));
1303
+ enableCustomization = input(false, ...(ngDevMode ? [{ debugName: "enableCustomization", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
1304
+ availableResources = input([], ...(ngDevMode ? [{ debugName: "availableResources" }] : []));
1305
+ availableFields = input([], ...(ngDevMode ? [{ debugName: "availableFields" }] : []));
1306
+ availableTargets = input([], ...(ngDevMode ? [{ debugName: "availableTargets" }] : []));
1210
1307
  pointClick = output();
1211
1308
  queryRequest = output();
1212
1309
  loadStateChange = output();
1310
+ chartDocumentApplied = output();
1311
+ chartDocumentSaved = output();
1312
+ chartHost = viewChild('chartHost', ...(ngDevMode ? [{ debugName: "chartHost" }] : []));
1313
+ hostElement = inject(ElementRef);
1314
+ engine = inject(PRAXIS_CHART_ENGINE);
1315
+ transformer = inject(PraxisChartDataTransformerService);
1316
+ canonicalMapper = inject(PraxisChartCanonicalContractMapperService);
1213
1317
  statsApi = inject(PraxisChartStatsApiService);
1318
+ i18n = inject(PraxisI18nService);
1319
+ settingsPanel = inject(SETTINGS_PANEL_BRIDGE, { optional: true });
1214
1320
  destroyRef = inject(DestroyRef);
1215
- remoteResolvedRows = signal(null, ...(ngDevMode ? [{ debugName: "remoteResolvedRows" }] : []));
1321
+ resizeObserver = signal(null, ...(ngDevMode ? [{ debugName: "resizeObserver" }] : []));
1322
+ shellObserver = signal(null, ...(ngDevMode ? [{ debugName: "shellObserver" }] : []));
1323
+ currentLoadState = signal('idle', ...(ngDevMode ? [{ debugName: "currentLoadState" }] : []));
1324
+ remoteResolvedData = signal(null, ...(ngDevMode ? [{ debugName: "remoteResolvedData" }] : []));
1216
1325
  remoteRuntimeState = signal('idle', ...(ngDevMode ? [{ debugName: "remoteRuntimeState" }] : []));
1217
1326
  remoteTechnicalError = signal(null, ...(ngDevMode ? [{ debugName: "remoteTechnicalError" }] : []));
1327
+ runtimeChartDocument = signal(null, ...(ngDevMode ? [{ debugName: "runtimeChartDocument" }] : []));
1328
+ mappedRuntimeConfig = signal(null, ...(ngDevMode ? [{ debugName: "mappedRuntimeConfig" }] : []));
1329
+ chartDocumentMappingError = signal(null, ...(ngDevMode ? [{ debugName: "chartDocumentMappingError" }] : []));
1330
+ fillContainerHeight = signal(false, ...(ngDevMode ? [{ debugName: "fillContainerHeight" }] : []));
1218
1331
  previousRemoteSignature = null;
1219
- resolvedRows = computed(() => {
1332
+ previousDocumentSignature = null;
1333
+ editorSessionSubscriptions = [];
1334
+ effectiveConfig = computed(() => {
1335
+ const base = this.mappedRuntimeConfig() ?? this.config();
1336
+ const runtimeFilters = normalizeFilterCriteria(this.filterCriteria());
1337
+ if (!runtimeFilters || base.dataSource?.kind !== 'remote' || !base.dataSource.query) {
1338
+ return base;
1339
+ }
1340
+ return mergeRemoteFilterCriteria(base, runtimeFilters);
1341
+ }, ...(ngDevMode ? [{ debugName: "effectiveConfig" }] : []));
1342
+ resolvedData = computed(() => {
1343
+ const inputData = this.data();
1344
+ if (inputData !== null && inputData !== undefined) {
1345
+ return inputData;
1346
+ }
1347
+ const remoteData = this.remoteResolvedData();
1348
+ if (remoteData !== null) {
1349
+ return remoteData;
1350
+ }
1351
+ const dataSource = this.effectiveConfig().dataSource;
1352
+ if (dataSource?.kind === 'local') {
1353
+ return dataSource.items ?? [];
1354
+ }
1355
+ return [];
1356
+ }, ...(ngDevMode ? [{ debugName: "resolvedData" }] : []));
1357
+ resolvedHeight = computed(() => {
1358
+ if (this.fillContainerHeight()) {
1359
+ return '100%';
1360
+ }
1361
+ const value = this.effectiveConfig().height;
1362
+ if (typeof value === 'number')
1363
+ return `${value}px`;
1364
+ return value || 'var(--praxis-chart-runtime-height, 320px)';
1365
+ }, ...(ngDevMode ? [{ debugName: "resolvedHeight" }] : []));
1366
+ renderConfig = computed(() => {
1367
+ const config = this.effectiveConfig();
1220
1368
  const explicitData = this.data();
1369
+ const remoteState = this.remoteRuntimeState();
1370
+ const remoteData = this.remoteResolvedData();
1221
1371
  if (explicitData !== null && explicitData !== undefined) {
1222
- return explicitData;
1372
+ return config;
1223
1373
  }
1224
- return this.remoteResolvedRows() ?? [];
1225
- }, ...(ngDevMode ? [{ debugName: "resolvedRows" }] : []));
1226
- tableId = computed(() => `${this.config().id || 'praxis-chart'}-showcase-table`, ...(ngDevMode ? [{ debugName: "tableId" }] : []));
1227
- resolvedTitle = computed(() => resolveText(this.config().title) || 'Tabela analitica', ...(ngDevMode ? [{ debugName: "resolvedTitle" }] : []));
1228
- resolvedSubtitle = computed(() => resolveText(this.config().subtitle) || 'Mesmo dataset em visualizacao tabular', ...(ngDevMode ? [{ debugName: "resolvedSubtitle" }] : []));
1229
- loadingLabel = computed(() => resolveText(this.config().state?.loadingLabel) || 'Carregando analytics...', ...(ngDevMode ? [{ debugName: "loadingLabel" }] : []));
1230
- errorTitle = computed(() => this.remoteTechnicalError()
1231
- || resolveText(this.config().state?.error?.title)
1232
- || 'Falha no contrato analitico', ...(ngDevMode ? [{ debugName: "errorTitle" }] : []));
1233
- errorDescription = computed(() => resolveText(this.config().state?.error?.description), ...(ngDevMode ? [{ debugName: "errorDescription" }] : []));
1234
- stateMode = computed(() => resolveShowcaseStateMode(this.config(), this.remoteRuntimeState()), ...(ngDevMode ? [{ debugName: "stateMode" }] : []));
1235
- tableConfig = computed(() => buildTableConfig(this.config(), this.resolvedRows()), ...(ngDevMode ? [{ debugName: "tableConfig" }] : []));
1374
+ if (config.dataSource?.kind !== 'remote') {
1375
+ return config;
1376
+ }
1377
+ if (remoteState === 'loading') {
1378
+ return {
1379
+ ...config,
1380
+ preferredLoadState: 'loading',
1381
+ };
1382
+ }
1383
+ if (remoteState === 'error') {
1384
+ return {
1385
+ ...config,
1386
+ preferredLoadState: 'error',
1387
+ state: config.state?.error
1388
+ ? {
1389
+ ...config.state,
1390
+ error: {
1391
+ ...config.state.error,
1392
+ technicalDetails: this.remoteTechnicalError() ?? config.state.error.technicalDetails,
1393
+ },
1394
+ }
1395
+ : config.state,
1396
+ };
1397
+ }
1398
+ if (remoteData !== null) {
1399
+ return {
1400
+ ...config,
1401
+ preferredLoadState: undefined,
1402
+ series: config.series.map((series) => ({
1403
+ ...series,
1404
+ metric: {
1405
+ ...series.metric,
1406
+ aggregation: 'sum',
1407
+ field: series.metric?.field || 'value',
1408
+ },
1409
+ })),
1410
+ };
1411
+ }
1412
+ return config;
1413
+ }, ...(ngDevMode ? [{ debugName: "renderConfig" }] : []));
1414
+ loadingLabel = computed(() => this.i18n.resolve(this.renderConfig().state?.loadingLabel), ...(ngDevMode ? [{ debugName: "loadingLabel" }] : []));
1415
+ emptyTitle = computed(() => this.i18n.resolve(this.renderConfig().emptyState?.title), ...(ngDevMode ? [{ debugName: "emptyTitle" }] : []));
1416
+ emptyDescription = computed(() => this.i18n.resolve(this.renderConfig().emptyState?.description), ...(ngDevMode ? [{ debugName: "emptyDescription" }] : []));
1417
+ errorTitle = computed(() => this.chartDocumentMappingError()
1418
+ ? this.i18n.resolve({
1419
+ key: 'praxis.charts.runtime.invalidDocumentTitle',
1420
+ text: 'Chart configuration is invalid',
1421
+ })
1422
+ : this.i18n.resolve(this.renderConfig().state?.error?.title), ...(ngDevMode ? [{ debugName: "errorTitle" }] : []));
1423
+ errorDescription = computed(() => this.chartDocumentMappingError()
1424
+ ? this.i18n.resolve({
1425
+ key: 'praxis.charts.runtime.invalidDocumentDescription',
1426
+ text: 'The canonical chart document could not be mapped to the current Praxis chart runtime. Review the chart contract before continuing.',
1427
+ })
1428
+ : this.i18n.resolve(this.renderConfig().state?.error?.description), ...(ngDevMode ? [{ debugName: "errorDescription" }] : []));
1429
+ editChartLabel = computed(() => this.i18n.resolve({ key: 'praxis.charts.runtime.editChart', text: 'Edit chart settings' }), ...(ngDevMode ? [{ debugName: "editChartLabel" }] : []));
1430
+ loadState = computed(() => {
1431
+ if (this.chartDocumentMappingError()) {
1432
+ return 'error';
1433
+ }
1434
+ const config = this.renderConfig();
1435
+ if (config.preferredLoadState === 'loading')
1436
+ return 'loading';
1437
+ if (config.preferredLoadState === 'error')
1438
+ return 'error';
1439
+ const dataSource = config.dataSource;
1440
+ const explicitData = this.data();
1441
+ if (dataSource?.kind === 'remote' && (explicitData === null || explicitData === undefined)) {
1442
+ return this.remoteRuntimeState() === 'ready' ? 'ready' : 'loading';
1443
+ }
1444
+ const transformed = this.transformer.transform(config, this.resolvedData());
1445
+ return transformed.hasData ? 'ready' : 'empty';
1446
+ }, ...(ngDevMode ? [{ debugName: "loadState" }] : []));
1236
1447
  constructor() {
1448
+ afterNextRender(() => {
1449
+ this.observeShellSizingContext();
1450
+ });
1237
1451
  effect(() => {
1238
- const config = this.config();
1452
+ const document = this.chartDocument();
1453
+ const signature = document ? JSON.stringify(document) : null;
1454
+ if (signature === this.previousDocumentSignature) {
1455
+ return;
1456
+ }
1457
+ this.previousDocumentSignature = signature;
1458
+ this.runtimeChartDocument.set(document ? structuredClone(document) : null);
1459
+ });
1460
+ effect(() => {
1461
+ const runtimeDocument = this.runtimeChartDocument();
1462
+ if (!runtimeDocument) {
1463
+ this.mappedRuntimeConfig.set(null);
1464
+ this.chartDocumentMappingError.set(null);
1465
+ return;
1466
+ }
1467
+ try {
1468
+ this.mappedRuntimeConfig.set(this.canonicalMapper.toPraxisChartConfig(runtimeDocument));
1469
+ this.chartDocumentMappingError.set(null);
1470
+ }
1471
+ catch (error) {
1472
+ this.mappedRuntimeConfig.set(null);
1473
+ this.chartDocumentMappingError.set(error instanceof Error && error.message.trim()
1474
+ ? error.message
1475
+ : 'Invalid x-ui.chart document for Praxis chart runtime.');
1476
+ }
1477
+ });
1478
+ effect(() => {
1479
+ const config = this.effectiveConfig();
1239
1480
  const explicitData = this.data();
1240
1481
  const nextSignature = explicitData === null || explicitData === undefined
1241
- ? buildRemoteSignature(config)
1482
+ ? this.buildRemoteSignature(config)
1242
1483
  : null;
1243
1484
  if (nextSignature === this.previousRemoteSignature) {
1244
1485
  return;
1245
1486
  }
1246
1487
  this.previousRemoteSignature = nextSignature;
1247
- this.remoteResolvedRows.set(null);
1488
+ this.remoteResolvedData.set(null);
1248
1489
  this.remoteRuntimeState.set('idle');
1249
1490
  this.remoteTechnicalError.set(null);
1250
1491
  });
1251
1492
  effect(() => {
1252
- const config = this.config();
1493
+ const nextState = this.loadState();
1494
+ if (this.currentLoadState() !== nextState) {
1495
+ this.currentLoadState.set(nextState);
1496
+ this.loadStateChange.emit(nextState);
1497
+ }
1498
+ });
1499
+ effect(() => {
1500
+ const effectiveConfig = this.effectiveConfig();
1253
1501
  const explicitData = this.data();
1254
- if (this.viewMode() !== 'table') {
1502
+ if (effectiveConfig.dataSource?.kind !== 'remote' || (explicitData !== null && explicitData !== undefined)) {
1255
1503
  return;
1256
1504
  }
1257
- if (config.dataSource?.kind !== 'remote' || (explicitData !== null && explicitData !== undefined)) {
1505
+ if (this.remoteRuntimeState() === 'loading' || this.remoteResolvedData() !== null) {
1506
+ return;
1507
+ }
1508
+ const event = {
1509
+ chartId: effectiveConfig.id,
1510
+ dataSource: effectiveConfig.dataSource,
1511
+ query: effectiveConfig.dataSource.query,
1512
+ };
1513
+ this.queryRequest.emit(event);
1514
+ this.remoteRuntimeState.set('loading');
1515
+ this.remoteResolvedData.set([]);
1516
+ this.remoteTechnicalError.set(null);
1517
+ this.statsApi.execute(event, effectiveConfig)
1518
+ .pipe(takeUntilDestroyed(this.destroyRef))
1519
+ .subscribe({
1520
+ next: (rows) => {
1521
+ this.remoteResolvedData.set(rows);
1522
+ this.remoteRuntimeState.set('ready');
1523
+ this.remoteTechnicalError.set(null);
1524
+ },
1525
+ error: (error) => {
1526
+ this.remoteResolvedData.set([]);
1527
+ this.remoteRuntimeState.set('error');
1528
+ this.remoteTechnicalError.set(error instanceof Error && error.message.trim()
1529
+ ? error.message
1530
+ : 'praxis.stats request failed');
1531
+ },
1532
+ });
1533
+ });
1534
+ effect(() => {
1535
+ if (this.loadState() !== 'ready') {
1536
+ this.engine.destroy();
1537
+ return;
1538
+ }
1539
+ const host = this.chartHost()?.nativeElement;
1540
+ if (!host)
1541
+ return;
1542
+ this.engine.render(host, {
1543
+ config: this.renderConfig(),
1544
+ data: this.resolvedData(),
1545
+ onPointClick: (event) => this.pointClick.emit(event),
1546
+ });
1547
+ this.ensureResizeObserver(host);
1548
+ });
1549
+ this.destroyRef.onDestroy(() => {
1550
+ this.clearEditorSessionSubscriptions();
1551
+ this.resizeObserver()?.disconnect();
1552
+ this.shellObserver()?.disconnect();
1553
+ this.engine.destroy();
1554
+ });
1555
+ }
1556
+ canOpenConfigEditor() {
1557
+ return this.enableCustomization() && !!this.settingsPanel && !!this.runtimeChartDocument();
1558
+ }
1559
+ async openConfigEditor() {
1560
+ if (!this.settingsPanel) {
1561
+ return;
1562
+ }
1563
+ const document = this.runtimeChartDocument();
1564
+ if (!document) {
1565
+ return;
1566
+ }
1567
+ const { PraxisChartConfigEditor } = await Promise.resolve().then(function () { return praxisChartConfigEditor; });
1568
+ this.clearEditorSessionSubscriptions();
1569
+ const ref = this.settingsPanel.open({
1570
+ id: `chart.${document.chartId || this.effectiveConfig().id || 'praxis-chart'}`,
1571
+ title: this.editChartLabel(),
1572
+ content: {
1573
+ component: PraxisChartConfigEditor,
1574
+ inputs: {
1575
+ chartDocument: document,
1576
+ document,
1577
+ mode: 'edit',
1578
+ readonly: false,
1579
+ availableResources: this.availableResources(),
1580
+ availableFields: this.availableFields(),
1581
+ availableTargets: this.availableTargets(),
1582
+ },
1583
+ },
1584
+ });
1585
+ const appliedSubscription = ref.applied$
1586
+ .subscribe((nextDocument) => {
1587
+ if (!nextDocument) {
1258
1588
  return;
1259
1589
  }
1260
- if (this.remoteRuntimeState() === 'loading' || this.remoteResolvedRows() !== null) {
1590
+ const clone = structuredClone(nextDocument);
1591
+ this.runtimeChartDocument.set(clone);
1592
+ this.chartDocumentApplied.emit(clone);
1593
+ });
1594
+ const savedSubscription = ref.saved$
1595
+ .subscribe((nextDocument) => {
1596
+ if (!nextDocument) {
1261
1597
  return;
1262
1598
  }
1263
- const event = {
1264
- chartId: config.id,
1265
- dataSource: config.dataSource,
1266
- query: config.dataSource.query,
1267
- };
1268
- this.queryRequest.emit(event);
1269
- this.remoteRuntimeState.set('loading');
1270
- this.remoteResolvedRows.set([]);
1271
- this.remoteTechnicalError.set(null);
1272
- this.statsApi.execute(event, config)
1273
- .pipe(takeUntilDestroyed(this.destroyRef))
1274
- .subscribe({
1275
- next: (rows) => {
1276
- this.remoteResolvedRows.set(rows);
1277
- this.remoteRuntimeState.set('ready');
1278
- this.remoteTechnicalError.set(null);
1279
- },
1280
- error: (error) => {
1281
- this.remoteResolvedRows.set([]);
1282
- this.remoteRuntimeState.set('error');
1283
- this.remoteTechnicalError.set(error instanceof Error && error.message.trim()
1284
- ? error.message
1285
- : 'praxis.stats request failed');
1286
- },
1287
- });
1599
+ const clone = structuredClone(nextDocument);
1600
+ this.runtimeChartDocument.set(clone);
1601
+ this.chartDocumentSaved.emit(clone);
1288
1602
  });
1603
+ this.editorSessionSubscriptions = [appliedSubscription, savedSubscription];
1289
1604
  }
1290
- handleRowClick(event) {
1291
- const row = event?.row;
1292
- if (!row) {
1605
+ ensureResizeObserver(host) {
1606
+ if (this.resizeObserver()) {
1293
1607
  return;
1294
1608
  }
1295
- const config = this.config();
1296
- const primarySeries = config.series[0];
1297
- const categoryField = primarySeries?.categoryField || config.axes?.x?.field;
1298
- const metricField = primarySeries?.metric?.field;
1299
- this.pointClick.emit({
1300
- chartId: config.id,
1301
- seriesId: primarySeries?.id,
1302
- seriesName: primarySeries?.name,
1303
- category: categoryField ? stringifyCell(row[categoryField]) : undefined,
1304
- value: metricField ? row[metricField] : undefined,
1305
- data: row,
1609
+ const observer = new ResizeObserver(() => {
1610
+ this.engine.resize();
1306
1611
  });
1612
+ observer.observe(host);
1613
+ this.resizeObserver.set(observer);
1307
1614
  }
1308
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: PraxisChartShowcaseWidgetComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1309
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.17", type: PraxisChartShowcaseWidgetComponent, isStandalone: true, selector: "praxis-chart-showcase-widget", inputs: { config: { classPropertyName: "config", publicName: "config", isSignal: true, isRequired: true, transformFunction: null }, data: { classPropertyName: "data", publicName: "data", isSignal: true, isRequired: false, transformFunction: null }, viewMode: { classPropertyName: "viewMode", publicName: "viewMode", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { pointClick: "pointClick", queryRequest: "queryRequest", loadStateChange: "loadStateChange" }, ngImport: i0, template: `
1310
- @if (viewMode() === 'chart') {
1311
- <praxis-chart
1312
- [config]="config()"
1313
- [data]="data()"
1314
- (pointClick)="pointClick.emit($event)"
1315
- (queryRequest)="queryRequest.emit($event)"
1316
- (loadStateChange)="loadStateChange.emit($event)"
1317
- ></praxis-chart>
1318
- } @else if (stateMode() === 'loading') {
1319
- <section class="showcase-state-card">
1320
- <h4>{{ loadingLabel() }}</h4>
1321
- </section>
1322
- } @else if (stateMode() === 'error') {
1323
- <section class="showcase-state-card showcase-state-card-error">
1324
- <h4>{{ errorTitle() }}</h4>
1325
- @if (errorDescription()) {
1326
- <p>{{ errorDescription() }}</p>
1615
+ buildRemoteSignature(config) {
1616
+ if (config.dataSource?.kind !== 'remote') {
1617
+ return null;
1327
1618
  }
1328
- </section>
1329
- } @else {
1330
- <praxis-table
1331
- [config]="tableConfig()"
1332
- [data]="resolvedRows()"
1333
- [tableId]="tableId()"
1334
- [title]="resolvedTitle()"
1335
- [subtitle]="resolvedSubtitle()"
1336
- [icon]="'table_view'"
1337
- (rowClick)="handleRowClick($event)"
1338
- ></praxis-table>
1619
+ return JSON.stringify({
1620
+ id: config.id,
1621
+ resourcePath: config.dataSource.resourcePath,
1622
+ query: config.dataSource.query,
1623
+ });
1624
+ }
1625
+ clearEditorSessionSubscriptions() {
1626
+ for (const subscription of this.editorSessionSubscriptions) {
1627
+ subscription.unsubscribe();
1628
+ }
1629
+ this.editorSessionSubscriptions = [];
1630
+ }
1631
+ observeShellSizingContext() {
1632
+ const host = this.hostElement.nativeElement;
1633
+ const shell = host.closest('.pdx-shell');
1634
+ if (!shell) {
1635
+ this.fillContainerHeight.set(false);
1636
+ return;
1637
+ }
1638
+ const syncShellMode = () => {
1639
+ this.fillContainerHeight.set(shell.classList.contains('expanded') || shell.classList.contains('fullscreen'));
1640
+ this.scheduleResizeAfterShellModeChange();
1641
+ };
1642
+ syncShellMode();
1643
+ const observer = new MutationObserver(() => {
1644
+ syncShellMode();
1645
+ });
1646
+ observer.observe(shell, {
1647
+ attributes: true,
1648
+ attributeFilter: ['class'],
1649
+ });
1650
+ this.shellObserver.set(observer);
1651
+ }
1652
+ scheduleResizeAfterShellModeChange() {
1653
+ const runResize = () => {
1654
+ this.engine.resize();
1655
+ };
1656
+ if (typeof requestAnimationFrame === 'function') {
1657
+ requestAnimationFrame(() => {
1658
+ runResize();
1659
+ requestAnimationFrame(() => {
1660
+ runResize();
1661
+ });
1662
+ });
1663
+ return;
1664
+ }
1665
+ setTimeout(() => {
1666
+ runResize();
1667
+ }, 0);
1339
1668
  }
1340
- `, isInline: true, styles: [":host{display:block;min-width:0}.showcase-state-card{min-height:240px;display:grid;place-content:center;gap:8px;padding:24px;border-radius:18px;border:1px solid color-mix(in srgb,var(--md-sys-color-outline, #c5c7ce) 72%,transparent);background:linear-gradient(180deg,#fffffff5,#f4f7fbfa);text-align:center}.showcase-state-card h4{margin:0;font-size:1rem;font-weight:600;color:var(--md-sys-color-on-surface, #1a1b20)}.showcase-state-card p{margin:0;color:var(--md-sys-color-on-surface-variant, #5a5d67)}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: PraxisChartComponent, selector: "praxis-chart", inputs: ["config", "data"], outputs: ["pointClick", "queryRequest", "loadStateChange"] }, { kind: "component", type: PraxisTable, selector: "praxis-table", inputs: ["config", "resourcePath", "data", "tableId", "componentInstanceId", "title", "subtitle", "icon", "autoDelete", "notifyIfOutdated", "snoozeMs", "autoOpenSettingsOnOutdated", "crudContext", "dslFunctionRegistry", "editModeEnabled", "dense"], outputs: ["rowClick", "rowDoubleClick", "rowExpansionChange", "rowAction", "toolbarAction", "bulkAction", "columnReorder", "columnReorderAttempt", "beforeDelete", "afterDelete", "deleteError", "beforeBulkDelete", "afterBulkDelete", "bulkDeleteError", "schemaStatusChange", "metadataChange", "loadingStateChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1669
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: PraxisChartComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1670
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.17", type: PraxisChartComponent, isStandalone: true, selector: "praxis-chart", inputs: { config: { classPropertyName: "config", publicName: "config", isSignal: true, isRequired: true, transformFunction: null }, data: { classPropertyName: "data", publicName: "data", isSignal: true, isRequired: false, transformFunction: null }, chartDocument: { classPropertyName: "chartDocument", publicName: "chartDocument", isSignal: true, isRequired: false, transformFunction: null }, filterCriteria: { classPropertyName: "filterCriteria", publicName: "filterCriteria", isSignal: true, isRequired: false, transformFunction: null }, enableCustomization: { classPropertyName: "enableCustomization", publicName: "enableCustomization", isSignal: true, isRequired: false, transformFunction: null }, availableResources: { classPropertyName: "availableResources", publicName: "availableResources", isSignal: true, isRequired: false, transformFunction: null }, availableFields: { classPropertyName: "availableFields", publicName: "availableFields", isSignal: true, isRequired: false, transformFunction: null }, availableTargets: { classPropertyName: "availableTargets", publicName: "availableTargets", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { pointClick: "pointClick", queryRequest: "queryRequest", loadStateChange: "loadStateChange", chartDocumentApplied: "chartDocumentApplied", chartDocumentSaved: "chartDocumentSaved" }, providers: [
1671
+ EChartsEngineAdapter,
1672
+ {
1673
+ provide: PRAXIS_CHART_ENGINE,
1674
+ useExisting: EChartsEngineAdapter,
1675
+ },
1676
+ ], viewQueries: [{ propertyName: "chartHost", first: true, predicate: ["chartHost"], descendants: true, isSignal: true }], ngImport: i0, template: `
1677
+ <section class="praxis-chart-shell" [style.height]="resolvedHeight()">
1678
+ @if (canOpenConfigEditor()) {
1679
+ <button
1680
+ class="praxis-chart-settings-trigger"
1681
+ mat-icon-button
1682
+ type="button"
1683
+ [attr.aria-label]="editChartLabel()"
1684
+ [matTooltip]="editChartLabel()"
1685
+ (click)="openConfigEditor()"
1686
+ >
1687
+ <mat-icon>tune</mat-icon>
1688
+ </button>
1689
+ }
1690
+
1691
+ @if (loadState() === 'loading') {
1692
+ <div class="praxis-chart-state praxis-chart-state-loading">
1693
+ <div class="praxis-chart-spinner" aria-hidden="true"></div>
1694
+ @if (loadingLabel()) {
1695
+ <div class="praxis-chart-state-title">{{ loadingLabel() }}</div>
1696
+ }
1697
+ </div>
1698
+ } @else if (loadState() === 'error') {
1699
+ <div class="praxis-chart-state praxis-chart-state-error">
1700
+ @if (errorTitle()) {
1701
+ <div class="praxis-chart-state-title">{{ errorTitle() }}</div>
1702
+ }
1703
+ @if (errorDescription()) {
1704
+ <div class="praxis-chart-state-description">{{ errorDescription() }}</div>
1705
+ }
1706
+ </div>
1707
+ } @else if (loadState() === 'empty') {
1708
+ <div class="praxis-chart-state praxis-chart-state-empty">
1709
+ @if (emptyTitle()) {
1710
+ <div class="praxis-chart-state-title">{{ emptyTitle() }}</div>
1711
+ }
1712
+ @if (emptyDescription()) {
1713
+ <div class="praxis-chart-state-description">{{ emptyDescription() }}</div>
1714
+ }
1715
+ </div>
1716
+ } @else {
1717
+ <div #chartHost class="praxis-chart-host"></div>
1718
+ }
1719
+ </section>
1720
+ `, isInline: true, styles: [":host{display:block;height:100%;min-width:0}:host-context(.pdx-shell.expanded) .praxis-chart-shell,:host-context(.pdx-shell.fullscreen) .praxis-chart-shell{height:100%!important}.praxis-chart-shell{position:relative;width:100%;height:100%;min-height:240px;border-radius:18px;overflow:hidden;background:radial-gradient(circle at top left,rgba(18,99,180,.12),transparent 38%),linear-gradient(180deg,#1263b408,#1263b400);border:1px solid color-mix(in srgb,var(--md-sys-color-outline, #c5c7ce) 72%,transparent)}.praxis-chart-settings-trigger{position:absolute;top:10px;right:10px;z-index:3;background:color-mix(in srgb,var(--md-sys-color-surface, #fff) 88%,rgba(18,99,180,.12));-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px)}.praxis-chart-host{width:100%;height:100%}.praxis-chart-state{height:100%;min-height:240px;display:grid;place-content:center;gap:10px;padding:24px;text-align:center}.praxis-chart-state-title{font-size:1rem;font-weight:600;color:var(--md-sys-color-on-surface, #1a1b20)}.praxis-chart-state-description{font-size:.925rem;color:var(--md-sys-color-on-surface-variant, #5a5d67);max-width:36rem}.praxis-chart-spinner{width:34px;height:34px;margin-inline:auto;border-radius:999px;border:3px solid rgba(18,99,180,.18);border-top-color:#1263b4d1;animation:praxis-chart-spin .8s linear infinite}@keyframes praxis-chart-spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i1$1.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i2.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i3.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1341
1721
  }
1342
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: PraxisChartShowcaseWidgetComponent, decorators: [{
1722
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: PraxisChartComponent, decorators: [{
1343
1723
  type: Component,
1344
- args: [{ selector: 'praxis-chart-showcase-widget', standalone: true, imports: [CommonModule, PraxisChartComponent, PraxisTable], changeDetection: ChangeDetectionStrategy.OnPush, template: `
1345
- @if (viewMode() === 'chart') {
1724
+ args: [{ selector: 'praxis-chart', standalone: true, imports: [CommonModule, MatButtonModule, MatIconModule, MatTooltipModule], providers: [
1725
+ EChartsEngineAdapter,
1726
+ {
1727
+ provide: PRAXIS_CHART_ENGINE,
1728
+ useExisting: EChartsEngineAdapter,
1729
+ },
1730
+ ], changeDetection: ChangeDetectionStrategy.OnPush, template: `
1731
+ <section class="praxis-chart-shell" [style.height]="resolvedHeight()">
1732
+ @if (canOpenConfigEditor()) {
1733
+ <button
1734
+ class="praxis-chart-settings-trigger"
1735
+ mat-icon-button
1736
+ type="button"
1737
+ [attr.aria-label]="editChartLabel()"
1738
+ [matTooltip]="editChartLabel()"
1739
+ (click)="openConfigEditor()"
1740
+ >
1741
+ <mat-icon>tune</mat-icon>
1742
+ </button>
1743
+ }
1744
+
1745
+ @if (loadState() === 'loading') {
1746
+ <div class="praxis-chart-state praxis-chart-state-loading">
1747
+ <div class="praxis-chart-spinner" aria-hidden="true"></div>
1748
+ @if (loadingLabel()) {
1749
+ <div class="praxis-chart-state-title">{{ loadingLabel() }}</div>
1750
+ }
1751
+ </div>
1752
+ } @else if (loadState() === 'error') {
1753
+ <div class="praxis-chart-state praxis-chart-state-error">
1754
+ @if (errorTitle()) {
1755
+ <div class="praxis-chart-state-title">{{ errorTitle() }}</div>
1756
+ }
1757
+ @if (errorDescription()) {
1758
+ <div class="praxis-chart-state-description">{{ errorDescription() }}</div>
1759
+ }
1760
+ </div>
1761
+ } @else if (loadState() === 'empty') {
1762
+ <div class="praxis-chart-state praxis-chart-state-empty">
1763
+ @if (emptyTitle()) {
1764
+ <div class="praxis-chart-state-title">{{ emptyTitle() }}</div>
1765
+ }
1766
+ @if (emptyDescription()) {
1767
+ <div class="praxis-chart-state-description">{{ emptyDescription() }}</div>
1768
+ }
1769
+ </div>
1770
+ } @else {
1771
+ <div #chartHost class="praxis-chart-host"></div>
1772
+ }
1773
+ </section>
1774
+ `, styles: [":host{display:block;height:100%;min-width:0}:host-context(.pdx-shell.expanded) .praxis-chart-shell,:host-context(.pdx-shell.fullscreen) .praxis-chart-shell{height:100%!important}.praxis-chart-shell{position:relative;width:100%;height:100%;min-height:240px;border-radius:18px;overflow:hidden;background:radial-gradient(circle at top left,rgba(18,99,180,.12),transparent 38%),linear-gradient(180deg,#1263b408,#1263b400);border:1px solid color-mix(in srgb,var(--md-sys-color-outline, #c5c7ce) 72%,transparent)}.praxis-chart-settings-trigger{position:absolute;top:10px;right:10px;z-index:3;background:color-mix(in srgb,var(--md-sys-color-surface, #fff) 88%,rgba(18,99,180,.12));-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px)}.praxis-chart-host{width:100%;height:100%}.praxis-chart-state{height:100%;min-height:240px;display:grid;place-content:center;gap:10px;padding:24px;text-align:center}.praxis-chart-state-title{font-size:1rem;font-weight:600;color:var(--md-sys-color-on-surface, #1a1b20)}.praxis-chart-state-description{font-size:.925rem;color:var(--md-sys-color-on-surface-variant, #5a5d67);max-width:36rem}.praxis-chart-spinner{width:34px;height:34px;margin-inline:auto;border-radius:999px;border:3px solid rgba(18,99,180,.18);border-top-color:#1263b4d1;animation:praxis-chart-spin .8s linear infinite}@keyframes praxis-chart-spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}\n"] }]
1775
+ }], ctorParameters: () => [], propDecorators: { config: [{ type: i0.Input, args: [{ isSignal: true, alias: "config", required: true }] }], data: [{ type: i0.Input, args: [{ isSignal: true, alias: "data", required: false }] }], chartDocument: [{ type: i0.Input, args: [{ isSignal: true, alias: "chartDocument", required: false }] }], filterCriteria: [{ type: i0.Input, args: [{ isSignal: true, alias: "filterCriteria", required: false }] }], enableCustomization: [{ type: i0.Input, args: [{ isSignal: true, alias: "enableCustomization", required: false }] }], availableResources: [{ type: i0.Input, args: [{ isSignal: true, alias: "availableResources", required: false }] }], availableFields: [{ type: i0.Input, args: [{ isSignal: true, alias: "availableFields", required: false }] }], availableTargets: [{ type: i0.Input, args: [{ isSignal: true, alias: "availableTargets", required: false }] }], pointClick: [{ type: i0.Output, args: ["pointClick"] }], queryRequest: [{ type: i0.Output, args: ["queryRequest"] }], loadStateChange: [{ type: i0.Output, args: ["loadStateChange"] }], chartDocumentApplied: [{ type: i0.Output, args: ["chartDocumentApplied"] }], chartDocumentSaved: [{ type: i0.Output, args: ["chartDocumentSaved"] }], chartHost: [{ type: i0.ViewChild, args: ['chartHost', { isSignal: true }] }] } });
1776
+ function normalizeFilterCriteria(criteria) {
1777
+ if (!criteria)
1778
+ return null;
1779
+ const next = Object.entries(criteria).reduce((acc, [key, value]) => {
1780
+ if (value === null || value === undefined)
1781
+ return acc;
1782
+ if (Array.isArray(value) && value.length === 0)
1783
+ return acc;
1784
+ if (typeof value === 'string' && value.trim() === '')
1785
+ return acc;
1786
+ acc[key] = value;
1787
+ return acc;
1788
+ }, {});
1789
+ return Object.keys(next).length ? next : null;
1790
+ }
1791
+ function mergeRemoteFilterCriteria(config, runtimeFilters) {
1792
+ const dataSource = config.dataSource;
1793
+ if (dataSource?.kind !== 'remote' || !dataSource.query)
1794
+ return config;
1795
+ const query = dataSource.query;
1796
+ const nextFilters = {
1797
+ ...(query.filters || {}),
1798
+ ...runtimeFilters,
1799
+ };
1800
+ const statsRequest = query.statsRequest && typeof query.statsRequest === 'object'
1801
+ ? {
1802
+ ...query.statsRequest,
1803
+ filter: {
1804
+ ...((query.statsRequest.filter) || {}),
1805
+ ...runtimeFilters,
1806
+ },
1807
+ }
1808
+ : query.statsRequest;
1809
+ return {
1810
+ ...config,
1811
+ dataSource: {
1812
+ ...dataSource,
1813
+ query: {
1814
+ ...query,
1815
+ filters: nextFilters,
1816
+ statsRequest,
1817
+ },
1818
+ },
1819
+ };
1820
+ }
1821
+
1822
+ const PRAXIS_CHART_DRILLDOWN_DATA_BY_MONTH = {
1823
+ Jan: [
1824
+ { segment: 'Enterprise', total: 52000 },
1825
+ { segment: 'Mid-market', total: 38000 },
1826
+ { segment: 'SMB', total: 30000 },
1827
+ ],
1828
+ Fev: [
1829
+ { segment: 'Enterprise', total: 61000 },
1830
+ { segment: 'Mid-market', total: 42000 },
1831
+ { segment: 'SMB', total: 35000 },
1832
+ ],
1833
+ Mar: [
1834
+ { segment: 'Enterprise', total: 68000 },
1835
+ { segment: 'Mid-market', total: 47000 },
1836
+ { segment: 'SMB', total: 36000 },
1837
+ ],
1838
+ Abr: [
1839
+ { segment: 'Enterprise', total: 64000 },
1840
+ { segment: 'Mid-market', total: 49500 },
1841
+ { segment: 'SMB', total: 36000 },
1842
+ ],
1843
+ };
1844
+
1845
+ class PraxisChartDrilldownPanelComponent {
1846
+ title = input('Detalhamento por segmento', ...(ngDevMode ? [{ debugName: "title" }] : []));
1847
+ selection = input(null, ...(ngDevMode ? [{ debugName: "selection" }] : []));
1848
+ activeCategory = computed(() => {
1849
+ const raw = this.selection()?.category;
1850
+ return typeof raw === 'string' ? raw : null;
1851
+ }, ...(ngDevMode ? [{ debugName: "activeCategory" }] : []));
1852
+ detailData = computed(() => {
1853
+ const category = this.activeCategory();
1854
+ if (!category)
1855
+ return [];
1856
+ return PRAXIS_CHART_DRILLDOWN_DATA_BY_MONTH[category] ?? [];
1857
+ }, ...(ngDevMode ? [{ debugName: "detailData" }] : []));
1858
+ detailChartConfig = computed(() => {
1859
+ const category = this.activeCategory();
1860
+ return {
1861
+ id: 'chart-drilldown-detail',
1862
+ type: 'donut',
1863
+ title: { text: category ? `Mix de receita em ${category}` : 'Aguardando selecao' },
1864
+ subtitle: { text: category ? 'Drill-down local com JSON mockado' : 'Selecione um ponto no chart principal' },
1865
+ height: 280,
1866
+ series: [
1867
+ {
1868
+ id: 'segmentMix',
1869
+ categoryField: 'segment',
1870
+ metric: { field: 'total', aggregation: 'sum' },
1871
+ labels: { visible: true },
1872
+ },
1873
+ ],
1874
+ emptyState: {
1875
+ title: { text: 'Nenhum recorte selecionado' },
1876
+ description: { text: 'O painel de drill-down usa mocks locais e reage ao pointClick do chart principal.' },
1877
+ },
1878
+ theme: {
1879
+ palette: ['#1263b4', '#2b8a3e', '#f08c00', '#7b61ff'],
1880
+ },
1881
+ };
1882
+ }, ...(ngDevMode ? [{ debugName: "detailChartConfig" }] : []));
1883
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: PraxisChartDrilldownPanelComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1884
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.17", type: PraxisChartDrilldownPanelComponent, isStandalone: true, selector: "praxis-chart-drilldown-panel", inputs: { title: { classPropertyName: "title", publicName: "title", isSignal: true, isRequired: false, transformFunction: null }, selection: { classPropertyName: "selection", publicName: "selection", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: `
1885
+ <section class="drilldown-shell">
1886
+ <header class="drilldown-header">
1887
+ <p class="drilldown-eyebrow">Drill-down local</p>
1888
+ <h3>{{ title() }}</h3>
1889
+ @if (activeCategory()) {
1890
+ <p class="drilldown-description">Recorte atual: {{ activeCategory() }}</p>
1891
+ } @else {
1892
+ <p class="drilldown-description">Clique em uma barra do chart principal para abrir o detalhamento local.</p>
1893
+ }
1894
+ </header>
1895
+
1346
1896
  <praxis-chart
1347
- [config]="config()"
1348
- [data]="data()"
1349
- (pointClick)="pointClick.emit($event)"
1350
- (queryRequest)="queryRequest.emit($event)"
1351
- (loadStateChange)="loadStateChange.emit($event)"
1897
+ [config]="detailChartConfig()"
1898
+ [data]="detailData()"
1352
1899
  ></praxis-chart>
1353
- } @else if (stateMode() === 'loading') {
1354
- <section class="showcase-state-card">
1355
- <h4>{{ loadingLabel() }}</h4>
1356
- </section>
1357
- } @else if (stateMode() === 'error') {
1358
- <section class="showcase-state-card showcase-state-card-error">
1359
- <h4>{{ errorTitle() }}</h4>
1360
- @if (errorDescription()) {
1361
- <p>{{ errorDescription() }}</p>
1362
- }
1363
- </section>
1364
- } @else {
1365
- <praxis-table
1366
- [config]="tableConfig()"
1367
- [data]="resolvedRows()"
1368
- [tableId]="tableId()"
1369
- [title]="resolvedTitle()"
1370
- [subtitle]="resolvedSubtitle()"
1371
- [icon]="'table_view'"
1372
- (rowClick)="handleRowClick($event)"
1373
- ></praxis-table>
1374
- }
1375
- `, styles: [":host{display:block;min-width:0}.showcase-state-card{min-height:240px;display:grid;place-content:center;gap:8px;padding:24px;border-radius:18px;border:1px solid color-mix(in srgb,var(--md-sys-color-outline, #c5c7ce) 72%,transparent);background:linear-gradient(180deg,#fffffff5,#f4f7fbfa);text-align:center}.showcase-state-card h4{margin:0;font-size:1rem;font-weight:600;color:var(--md-sys-color-on-surface, #1a1b20)}.showcase-state-card p{margin:0;color:var(--md-sys-color-on-surface-variant, #5a5d67)}\n"] }]
1376
- }], ctorParameters: () => [], propDecorators: { config: [{ type: i0.Input, args: [{ isSignal: true, alias: "config", required: true }] }], data: [{ type: i0.Input, args: [{ isSignal: true, alias: "data", required: false }] }], viewMode: [{ type: i0.Input, args: [{ isSignal: true, alias: "viewMode", required: false }] }], pointClick: [{ type: i0.Output, args: ["pointClick"] }], queryRequest: [{ type: i0.Output, args: ["queryRequest"] }], loadStateChange: [{ type: i0.Output, args: ["loadStateChange"] }] } });
1377
- function resolveShowcaseStateMode(config, remoteRuntimeState = 'idle') {
1378
- if (config.preferredLoadState === 'loading') {
1379
- return 'loading';
1380
- }
1381
- if (config.preferredLoadState === 'error') {
1382
- return 'error';
1383
- }
1384
- if (config.dataSource?.kind === 'remote' && remoteRuntimeState === 'error') {
1385
- return 'error';
1386
- }
1387
- if (config.dataSource?.kind === 'remote' && remoteRuntimeState !== 'ready') {
1388
- return 'loading';
1389
- }
1390
- return 'ready';
1900
+ </section>
1901
+ `, isInline: true, styles: [":host{display:block}.drilldown-shell{display:grid;gap:14px}.drilldown-header h3,.drilldown-eyebrow,.drilldown-description{margin:0}.drilldown-eyebrow{font-size:.75rem;letter-spacing:.14em;text-transform:uppercase;color:var(--md-sys-color-on-surface-variant, #5a5d67)}.drilldown-header h3{font-size:1.2rem;color:var(--md-sys-color-on-surface, #1a1b20)}.drilldown-description{color:var(--md-sys-color-on-surface-variant, #5a5d67)}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: PraxisChartComponent, selector: "praxis-chart", inputs: ["config", "data", "chartDocument", "filterCriteria", "enableCustomization", "availableResources", "availableFields", "availableTargets"], outputs: ["pointClick", "queryRequest", "loadStateChange", "chartDocumentApplied", "chartDocumentSaved"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1391
1902
  }
1392
- function buildTableConfig(config, rows) {
1393
- const tableConfig = createDefaultTableConfig();
1394
- const toolbar = tableConfig.toolbar;
1395
- const behavior = tableConfig.behavior;
1396
- const pagination = behavior.pagination;
1397
- const sorting = behavior.sorting;
1398
- const filtering = behavior.filtering;
1399
- const selection = behavior.selection;
1400
- const localDataMode = behavior.localDataMode;
1401
- const actions = tableConfig.actions;
1402
- const rowActions = actions.row;
1403
- const bulkActions = actions.bulk;
1404
- const appearance = tableConfig.appearance;
1405
- const categoryField = config.axes?.x?.field || config.series[0]?.categoryField;
1406
- const categoryLabel = config.axes?.x?.label || categoryField;
1407
- const sampleRow = rows[0] || {};
1408
- const seriesColumns = config.series
1409
- .map((series) => {
1410
- const field = series.metric?.field || series.categoryField;
1411
- if (!field) {
1412
- return null;
1903
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: PraxisChartDrilldownPanelComponent, decorators: [{
1904
+ type: Component,
1905
+ args: [{ selector: 'praxis-chart-drilldown-panel', standalone: true, imports: [CommonModule, PraxisChartComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: `
1906
+ <section class="drilldown-shell">
1907
+ <header class="drilldown-header">
1908
+ <p class="drilldown-eyebrow">Drill-down local</p>
1909
+ <h3>{{ title() }}</h3>
1910
+ @if (activeCategory()) {
1911
+ <p class="drilldown-description">Recorte atual: {{ activeCategory() }}</p>
1912
+ } @else {
1913
+ <p class="drilldown-description">Clique em uma barra do chart principal para abrir o detalhamento local.</p>
1413
1914
  }
1414
- return {
1415
- field,
1416
- header: series.name || series.metric?.label || field,
1417
- };
1418
- })
1419
- .filter((column) => column !== null);
1420
- const fallbackColumns = Object.keys(sampleRow).map((field) => ({
1421
- field,
1422
- header: field,
1423
- }));
1424
- const categoryColumn = categoryField
1425
- ? [{
1426
- field: categoryField,
1427
- header: categoryLabel || categoryField,
1428
- }]
1429
- : [];
1430
- const dedupedColumns = [...categoryColumn, ...seriesColumns, ...fallbackColumns].filter((column, index, source) => source.findIndex((candidate) => candidate.field === column.field) === index);
1431
- tableConfig.columns = dedupedColumns;
1432
- toolbar.visible = false;
1433
- pagination.enabled = rows.length > 10;
1434
- sorting.enabled = true;
1435
- filtering.enabled = false;
1436
- selection.enabled = false;
1437
- localDataMode.enabled = true;
1438
- rowActions.enabled = false;
1439
- rowActions.actions = [];
1440
- bulkActions.enabled = false;
1441
- bulkActions.actions = [];
1442
- appearance.density = 'comfortable';
1443
- return tableConfig;
1444
- }
1445
- function resolveText(value) {
1446
- if (typeof value === 'string') {
1447
- return value;
1448
- }
1449
- if (value && typeof value === 'object' && 'text' in value) {
1450
- const text = value.text;
1451
- return typeof text === 'string' ? text : '';
1452
- }
1453
- return '';
1454
- }
1455
- function stringifyCell(value) {
1456
- if (value === null || value === undefined) {
1457
- return undefined;
1458
- }
1459
- return String(value);
1460
- }
1461
- function buildRemoteSignature(config) {
1462
- if (config.dataSource?.kind !== 'remote') {
1463
- return null;
1464
- }
1465
- return JSON.stringify({
1466
- id: config.id,
1467
- resourcePath: config.dataSource.resourcePath,
1468
- query: config.dataSource.query,
1469
- });
1915
+ </header>
1916
+
1917
+ <praxis-chart
1918
+ [config]="detailChartConfig()"
1919
+ [data]="detailData()"
1920
+ ></praxis-chart>
1921
+ </section>
1922
+ `, styles: [":host{display:block}.drilldown-shell{display:grid;gap:14px}.drilldown-header h3,.drilldown-eyebrow,.drilldown-description{margin:0}.drilldown-eyebrow{font-size:.75rem;letter-spacing:.14em;text-transform:uppercase;color:var(--md-sys-color-on-surface-variant, #5a5d67)}.drilldown-header h3{font-size:1.2rem;color:var(--md-sys-color-on-surface, #1a1b20)}.drilldown-description{color:var(--md-sys-color-on-surface-variant, #5a5d67)}\n"] }]
1923
+ }], propDecorators: { title: [{ type: i0.Input, args: [{ isSignal: true, alias: "title", required: false }] }], selection: [{ type: i0.Input, args: [{ isSignal: true, alias: "selection", required: false }] }] } });
1924
+
1925
+ class PraxisChartStateProbeComponent {
1926
+ title = input('Chart Runtime Probe', ...(ngDevMode ? [{ debugName: "title" }] : []));
1927
+ value = input(null, ...(ngDevMode ? [{ debugName: "value" }] : []));
1928
+ serializedValue = computed(() => JSON.stringify(this.value(), null, 2), ...(ngDevMode ? [{ debugName: "serializedValue" }] : []));
1929
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: PraxisChartStateProbeComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1930
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.17", type: PraxisChartStateProbeComponent, isStandalone: true, selector: "praxis-chart-state-probe", inputs: { title: { classPropertyName: "title", publicName: "title", isSignal: true, isRequired: false, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: `
1931
+ <section class="probe-shell">
1932
+ <header class="probe-header">
1933
+ <p class="probe-eyebrow">Probe</p>
1934
+ <h3>{{ title() }}</h3>
1935
+ </header>
1936
+
1937
+ @if (value() === null || value() === undefined || value() === '') {
1938
+ <div class="probe-empty">Aguardando eventos do runtime.</div>
1939
+ } @else {
1940
+ <pre>{{ serializedValue() }}</pre>
1941
+ }
1942
+ </section>
1943
+ `, isInline: true, styles: [":host{display:block}.probe-shell{display:grid;gap:12px;min-height:220px;padding:18px;border-radius:20px;background:linear-gradient(180deg,#0b111ff5,#0b111fe0),radial-gradient(circle at top right,rgba(18,99,180,.32),transparent 35%);color:#d7e6ff}.probe-header h3,.probe-eyebrow{margin:0}.probe-eyebrow{font-size:.75rem;letter-spacing:.12em;text-transform:uppercase;color:#d7e6ffb3}.probe-empty{color:#d7e6ffc7}pre{margin:0;overflow:auto;white-space:pre-wrap;word-break:break-word;font-size:.84rem;line-height:1.45}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1470
1944
  }
1945
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: PraxisChartStateProbeComponent, decorators: [{
1946
+ type: Component,
1947
+ args: [{ selector: 'praxis-chart-state-probe', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `
1948
+ <section class="probe-shell">
1949
+ <header class="probe-header">
1950
+ <p class="probe-eyebrow">Probe</p>
1951
+ <h3>{{ title() }}</h3>
1952
+ </header>
1471
1953
 
1472
- const PRAXIS_CHART_SHOWCASE_WIDGET_METADATA = {
1473
- id: 'praxis-chart-showcase-widget',
1474
- componentType: 'praxis-chart-showcase-widget',
1475
- displayName: 'Praxis Chart Showcase Widget',
1476
- selector: 'praxis-chart-showcase-widget',
1477
- component: PraxisChartShowcaseWidgetComponent,
1478
- friendlyName: 'Praxis Chart Showcase Widget',
1479
- description: 'Showcase-only widget that toggles between Praxis chart and Praxis table using the same dataset.',
1480
- icon: 'swap_horiz',
1481
- tags: ['chart', 'table', 'showcase', 'analytics'],
1954
+ @if (value() === null || value() === undefined || value() === '') {
1955
+ <div class="probe-empty">Aguardando eventos do runtime.</div>
1956
+ } @else {
1957
+ <pre>{{ serializedValue() }}</pre>
1958
+ }
1959
+ </section>
1960
+ `, styles: [":host{display:block}.probe-shell{display:grid;gap:12px;min-height:220px;padding:18px;border-radius:20px;background:linear-gradient(180deg,#0b111ff5,#0b111fe0),radial-gradient(circle at top right,rgba(18,99,180,.32),transparent 35%);color:#d7e6ff}.probe-header h3,.probe-eyebrow{margin:0}.probe-eyebrow{font-size:.75rem;letter-spacing:.12em;text-transform:uppercase;color:#d7e6ffb3}.probe-empty{color:#d7e6ffc7}pre{margin:0;overflow:auto;white-space:pre-wrap;word-break:break-word;font-size:.84rem;line-height:1.45}\n"] }]
1961
+ }], propDecorators: { title: [{ type: i0.Input, args: [{ isSignal: true, alias: "title", required: false }] }], value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }] } });
1962
+
1963
+ const PRAXIS_CHART_COMPONENT_METADATA = {
1964
+ id: 'praxis-chart',
1965
+ componentType: 'praxis-chart',
1966
+ displayName: 'Praxis Chart',
1967
+ selector: 'praxis-chart',
1968
+ component: PraxisChartComponent,
1969
+ friendlyName: 'Praxis Chart',
1970
+ description: 'Chart component for analytics and metadata-driven visualizations in Praxis UI.',
1971
+ icon: 'bar_chart',
1972
+ tags: ['chart', 'analytics', 'widget', 'visualization'],
1482
1973
  lib: '@praxisui/charts',
1483
1974
  inputs: [
1484
1975
  {
1485
1976
  name: 'config',
1486
1977
  type: 'PraxisChartConfig',
1487
- description: 'Chart configuration preserved across chart and table views.',
1978
+ description: 'Declarative chart configuration with metadata-oriented semantics.',
1488
1979
  },
1489
1980
  {
1490
1981
  name: 'data',
1491
1982
  type: 'PraxisChartDataRow[]',
1492
- description: 'Local dataset shared by the chart and table renderers.',
1983
+ description: 'Optional local dataset that takes precedence over local datasource items.',
1493
1984
  },
1494
1985
  {
1495
- name: 'viewMode',
1496
- type: "'chart' | 'table'",
1497
- description: 'Current showcase view mode.',
1986
+ name: 'chartDocument',
1987
+ type: 'PraxisXUiChartContract | null',
1988
+ description: 'Optional canonical x-ui.chart document used as the authoring source of truth for settings-panel integration.',
1989
+ },
1990
+ {
1991
+ name: 'filterCriteria',
1992
+ type: 'Record<string, unknown> | null',
1993
+ description: 'Declarative filter criteria merged into remote datasource queries, enabling dynamic-page connections without host orchestration.',
1994
+ },
1995
+ {
1996
+ name: 'enableCustomization',
1997
+ type: 'boolean',
1998
+ description: 'When true and a settings-panel bridge is available, exposes the chart config editor from the runtime.',
1999
+ default: false,
1498
2000
  },
1499
2001
  ],
1500
2002
  outputs: [
1501
2003
  {
1502
2004
  name: 'pointClick',
1503
2005
  type: 'PraxisChartPointEvent',
1504
- description: 'Forwards point selection from the chart or mapped row selection from the table.',
2006
+ description: 'Emitted when the host wants to react to a point/series click.',
2007
+ },
2008
+ {
2009
+ name: 'queryRequest',
2010
+ type: 'PraxisChartQueryRequestEvent',
2011
+ description: 'Emitted before a remote praxis.stats datasource is resolved, allowing host-side observability of the outgoing request.',
2012
+ },
2013
+ {
2014
+ name: 'loadStateChange',
2015
+ type: 'PraxisChartLoadState',
2016
+ description: 'Emitted when the chart state changes.',
2017
+ },
2018
+ {
2019
+ name: 'chartDocumentApplied',
2020
+ type: 'PraxisXUiChartContract',
2021
+ description: 'Emitted when the runtime chart editor applies a canonical x-ui.chart document through the settings panel bridge.',
2022
+ },
2023
+ {
2024
+ name: 'chartDocumentSaved',
2025
+ type: 'PraxisXUiChartContract',
2026
+ description: 'Emitted when the runtime chart editor saves a canonical x-ui.chart document through the settings panel bridge.',
2027
+ },
2028
+ ],
2029
+ actions: [
2030
+ {
2031
+ id: 'open-chart-editor',
2032
+ label: 'Open Chart Editor',
2033
+ icon: 'tune',
2034
+ description: 'Opens the canonical chart config editor when the host provides a settings panel bridge and chartDocument.',
2035
+ command: 'openConfigEditor',
2036
+ scope: 'toolbar',
1505
2037
  },
2038
+ ],
2039
+ };
2040
+ function providePraxisChartsMetadata() {
2041
+ return {
2042
+ provide: ENVIRONMENT_INITIALIZER,
2043
+ multi: true,
2044
+ useFactory: (registry) => () => {
2045
+ registry.register(PRAXIS_CHART_COMPONENT_METADATA);
2046
+ },
2047
+ deps: [ComponentMetadataRegistry],
2048
+ };
2049
+ }
2050
+
2051
+ const PRAXIS_CHART_DRILLDOWN_PANEL_METADATA = {
2052
+ id: 'praxis-chart-drilldown-panel',
2053
+ componentType: 'praxis-chart-drilldown-panel',
2054
+ displayName: 'Praxis Chart Drilldown Panel',
2055
+ selector: 'praxis-chart-drilldown-panel',
2056
+ component: PraxisChartDrilldownPanelComponent,
2057
+ friendlyName: 'Praxis Chart Drilldown Panel',
2058
+ description: 'Local drill-down panel that consumes chart point events and renders a derived detail chart.',
2059
+ icon: 'query_stats',
2060
+ tags: ['chart', 'drilldown', 'widget', 'analytics'],
2061
+ lib: '@praxisui/charts',
2062
+ inputs: [
1506
2063
  {
1507
- name: 'queryRequest',
1508
- type: 'PraxisChartQueryRequestEvent',
1509
- description: 'Forwards remote chart data requests.',
2064
+ name: 'title',
2065
+ type: 'string',
2066
+ description: 'Panel title for the drill-down view.',
1510
2067
  },
1511
2068
  {
1512
- name: 'loadStateChange',
1513
- type: 'PraxisChartLoadState',
1514
- description: 'Forwards chart runtime state transitions.',
2069
+ name: 'selection',
2070
+ type: 'PraxisChartPointEvent',
2071
+ description: 'Event emitted by a source chart and used to resolve local detail datasets.',
1515
2072
  },
1516
2073
  ],
1517
2074
  };
1518
- function providePraxisChartShowcaseWidgetMetadata() {
2075
+ function providePraxisChartDrilldownPanelMetadata() {
1519
2076
  return {
1520
2077
  provide: ENVIRONMENT_INITIALIZER,
1521
2078
  multi: true,
1522
2079
  useFactory: (registry) => () => {
1523
- registry.register(PRAXIS_CHART_SHOWCASE_WIDGET_METADATA);
2080
+ registry.register(PRAXIS_CHART_DRILLDOWN_PANEL_METADATA);
1524
2081
  },
1525
2082
  deps: [ComponentMetadataRegistry],
1526
2083
  };
1527
2084
  }
1528
2085
 
1529
- function providePraxisCharts() {
1530
- return [
1531
- EChartsEngineAdapter,
1532
- {
1533
- provide: PRAXIS_CHART_ENGINE,
1534
- useExisting: EChartsEngineAdapter,
1535
- },
1536
- providePraxisChartsMetadata(),
1537
- providePraxisChartDrilldownPanelMetadata(),
1538
- providePraxisChartStateProbeMetadata(),
1539
- providePraxisChartShowcaseWidgetMetadata(),
1540
- ];
1541
- }
1542
-
1543
- class PraxisChartCanonicalContractMapperService {
1544
- toPraxisChartConfig(contract) {
1545
- this.assertSupportedContract(contract);
1546
- return {
1547
- id: contract.chartId,
1548
- type: contract.kind,
1549
- orientation: this.resolveOrientation(contract),
1550
- title: this.mapTextValue(contract.title),
1551
- subtitle: this.mapTextValue(contract.subtitle),
1552
- height: contract.height,
1553
- axes: this.buildAxes(contract),
1554
- series: this.buildSeries(contract),
1555
- dataSource: this.buildDataSource(contract),
1556
- interactions: this.buildInteractions(contract),
1557
- theme: this.buildTheme(contract),
1558
- emptyState: contract.state?.empty
1559
- ? {
1560
- title: this.mapTextValue(contract.state.empty.title),
1561
- description: this.mapTextValue(contract.state.empty.description),
1562
- }
1563
- : undefined,
1564
- state: {
1565
- loadingLabel: this.mapTextValue(contract.state?.loading?.title),
1566
- error: contract.state?.error
1567
- ? {
1568
- title: this.mapTextValue(contract.state.error.title),
1569
- description: this.mapTextValue(contract.state.error.description),
1570
- }
1571
- : undefined,
1572
- },
1573
- };
1574
- }
1575
- assertSupportedContract(contract) {
1576
- if (contract.source.kind !== 'praxis.stats' && contract.source.kind !== 'derived') {
1577
- throw new Error(`x-ui.chart source.kind="${contract.source.kind}" is not supported in @praxisui/charts.`);
1578
- }
1579
- if (contract.source.kind === 'praxis.stats' && !contract.source.resource?.trim()) {
1580
- throw new Error('x-ui.chart source.resource is required for source.kind="praxis.stats".');
1581
- }
1582
- if (contract.source.kind === 'praxis.stats' && !contract.source.operation) {
1583
- throw new Error('x-ui.chart source.operation is required for source.kind="praxis.stats".');
1584
- }
1585
- if (contract.theme?.palette && typeof contract.theme.palette === 'string') {
1586
- throw new Error('x-ui.chart theme.palette as palette token reference is not yet implemented in @praxisui/charts.');
1587
- }
1588
- if (contract.theme?.variant) {
1589
- throw new Error('x-ui.chart theme.variant is not yet implemented in @praxisui/charts.');
1590
- }
1591
- const aggregations = [
1592
- ...(contract.metrics?.map((metric) => metric.aggregation).filter(Boolean) ?? []),
1593
- ...(contract.aggregations?.map((aggregation) => aggregation.operation) ?? []),
1594
- ];
1595
- if (aggregations.includes('distinct-count')) {
1596
- throw new Error('x-ui.chart aggregation "distinct-count" is not yet implemented in @praxisui/charts.');
1597
- }
1598
- if (!contract.metrics?.length) {
1599
- throw new Error('x-ui.chart requires at least one metric for the current @praxisui/charts runtime.');
1600
- }
1601
- if (contract.kind !== 'pie' && contract.kind !== 'donut' && !contract.dimensions?.length) {
1602
- throw new Error('x-ui.chart cartesian charts require at least one dimension in the current @praxisui/charts runtime.');
1603
- }
1604
- if ((contract.kind === 'pie' || contract.kind === 'donut') && !contract.dimensions?.[0]?.field) {
1605
- throw new Error('x-ui.chart pie/donut charts require a first dimension for category mapping.');
1606
- }
1607
- if (contract.metrics.length > 1 && (contract.kind === 'pie' || contract.kind === 'donut')) {
1608
- throw new Error('x-ui.chart pie/donut charts with multiple metrics are not yet implemented in @praxisui/charts.');
1609
- }
1610
- if (contract.kind === 'combo') {
1611
- if (contract.metrics.length < 2) {
1612
- throw new Error('x-ui.chart combo charts require at least two metrics.');
1613
- }
1614
- if (contract.source.kind === 'praxis.stats'
1615
- && contract.source.operation !== 'group-by'
1616
- && contract.source.operation !== 'timeseries') {
1617
- throw new Error('x-ui.chart combo charts over praxis.stats currently support only group-by or timeseries operations in @praxisui/charts.');
1618
- }
1619
- }
1620
- if (contract.kind !== 'combo' && contract.metrics.some((metric) => metric.axis === 'secondary')) {
1621
- throw new Error('x-ui.chart axis="secondary" is supported only for combo charts in @praxisui/charts.');
1622
- }
1623
- if (contract.source.kind === 'praxis.stats'
1624
- && contract.source.operation === 'distribution'
1625
- && contract.metrics.length > 1) {
1626
- throw new Error('x-ui.chart praxis.stats distribution currently supports only a single metric in @praxisui/charts.');
1627
- }
1628
- if (contract.kind === 'horizontal-bar' && contract.orientation && contract.orientation !== 'horizontal') {
1629
- throw new Error('x-ui.chart kind="horizontal-bar" requires orientation="horizontal" when orientation is provided.');
1630
- }
1631
- if (contract.kind === 'scatter') {
1632
- const firstDimension = contract.dimensions?.[0];
1633
- if (!firstDimension?.field) {
1634
- throw new Error('x-ui.chart scatter charts require dimensions[0].field for the x axis.');
1635
- }
1636
- if (!contract.metrics?.[0]?.field) {
1637
- throw new Error('x-ui.chart scatter charts require metrics[0].field for the y axis.');
1638
- }
1639
- }
1640
- if (contract.events?.selectionChange || contract.events?.crossFilter) {
1641
- throw new Error('x-ui.chart selectionChange/crossFilter declarative runtime actions are not yet implemented in @praxisui/charts.');
1642
- }
1643
- }
1644
- buildAxes(contract) {
1645
- const firstDimension = contract.dimensions?.[0];
1646
- const firstMetric = contract.metrics?.[0];
1647
- const secondaryMetric = contract.metrics?.find((metric) => metric.axis === 'secondary');
1648
- const metricCount = contract.metrics?.length ?? 0;
1649
- if (contract.kind === 'pie' || contract.kind === 'donut') {
1650
- return {
1651
- x: {
1652
- field: firstDimension?.field,
1653
- label: this.toLabel(firstDimension?.label),
1654
- },
1655
- };
1656
- }
1657
- if (contract.kind === 'scatter') {
1658
- return {
1659
- x: {
1660
- field: firstDimension?.field,
1661
- label: this.toLabel(firstDimension?.label),
1662
- type: firstDimension?.role === 'time' ? 'time' : 'value',
1663
- },
1664
- y: {
1665
- field: firstMetric?.field,
1666
- label: this.toLabel(firstMetric?.label),
1667
- type: 'value',
1668
- },
1669
- };
1670
- }
1671
- return {
1672
- x: {
1673
- field: firstDimension?.field,
1674
- label: this.toLabel(firstDimension?.label),
1675
- type: firstDimension?.role === 'time' ? 'time' : 'category',
1676
- },
1677
- y: {
1678
- label: metricCount > 1 ? undefined : this.toLabel(firstMetric?.label),
1679
- type: 'value',
1680
- },
1681
- ySecondary: contract.kind === 'combo' && secondaryMetric
1682
- ? {
1683
- label: this.toLabel(secondaryMetric.label),
1684
- type: 'value',
1685
- position: 'right',
1686
- }
1687
- : undefined,
1688
- };
1689
- }
1690
- buildSeries(contract) {
1691
- const firstDimension = contract.dimensions?.[0];
1692
- const labelsVisible = this.resolveToggle(contract.labels);
1693
- return (contract.metrics ?? []).map((metric, index) => ({
1694
- id: `${metric.field}-${index + 1}`,
1695
- name: this.toLabel(metric.label) ?? metric.field,
1696
- type: this.resolveSeriesType(contract.kind, metric.seriesKind, index),
1697
- axis: metric.axis ?? 'primary',
1698
- categoryField: contract.kind === 'pie' || contract.kind === 'donut' ? firstDimension?.field : undefined,
1699
- metric: {
1700
- field: metric.field,
1701
- aggregation: this.mapAggregation(metric.aggregation),
1702
- label: this.toLabel(metric.label),
1703
- },
1704
- color: metric.color,
1705
- stackId: contract.kind === 'stacked-bar' || contract.kind === 'stacked-area' ? 'stack-1' : undefined,
1706
- labels: labelsVisible ? { visible: true } : undefined,
1707
- smooth: this.shouldSmoothSeries(contract.kind, metric.seriesKind),
1708
- }));
1709
- }
1710
- buildDataSource(contract) {
1711
- if (contract.source.kind === 'derived') {
1712
- return undefined;
1713
- }
1714
- return {
1715
- kind: 'remote',
1716
- resourcePath: contract.source.resource,
1717
- schemaId: contract.source.resource,
1718
- query: this.buildQuery(contract),
1719
- };
1720
- }
1721
- buildQuery(contract) {
1722
- if (contract.source.kind !== 'praxis.stats') {
1723
- return undefined;
1724
- }
1725
- const filtersFromContract = this.toQueryFilterMap(contract.filters);
1726
- const combinedFilters = {
1727
- ...filtersFromContract,
1728
- };
1729
- return {
1730
- sourceKind: 'praxis.stats',
1731
- statsOperation: contract.source.operation,
1732
- granularity: contract.source.options?.granularity,
1733
- fillGaps: contract.source.options?.fillGaps,
1734
- distributionMode: contract.source.options?.mode,
1735
- bucketSize: contract.source.options?.bucketSize,
1736
- bucketCount: contract.source.options?.bucketCount,
1737
- statsOrderBy: this.mapStatsOrderBy(contract.source.options?.orderBy),
1738
- statsPath: this.buildStatsPath(contract),
1739
- statsRequest: this.buildStatsRequest(contract, combinedFilters),
1740
- dimensions: contract.dimensions?.map((dimension) => dimension.field),
1741
- metrics: contract.metrics?.map((metric) => ({
1742
- field: metric.field,
1743
- aggregation: this.mapAggregation(metric.aggregation),
1744
- alias: metric.field,
1745
- })),
1746
- filters: Object.keys(combinedFilters).length ? combinedFilters : undefined,
1747
- sort: contract.sort?.map((item) => `${item.field}:${item.direction}`),
1748
- limit: contract.limit ?? contract.source.options?.limit,
1749
- };
1750
- }
1751
- buildInteractions(contract) {
1752
- return {
1753
- pointClick: Boolean(contract.events?.pointClick || contract.events?.drillDown),
1754
- selection: Boolean(contract.events?.selectionChange),
1755
- drillDown: Boolean(contract.events?.drillDown),
1756
- };
1757
- }
1758
- buildTheme(contract) {
1759
- return {
1760
- palette: Array.isArray(contract.theme?.palette) ? contract.theme.palette : undefined,
1761
- legend: { visible: this.resolveToggle(contract.legend, true) },
1762
- tooltip: {
1763
- enabled: this.resolveToggle(contract.tooltip, true),
1764
- trigger: contract.kind === 'pie' || contract.kind === 'donut' || contract.kind === 'scatter' ? 'item' : 'axis',
1765
- },
1766
- };
1767
- }
1768
- resolveOrientation(contract) {
1769
- if (contract.kind === 'horizontal-bar') {
1770
- return 'horizontal';
1771
- }
1772
- return contract.orientation;
1773
- }
1774
- resolveSeriesType(chartKind, seriesKind, index) {
1775
- if (chartKind === 'combo') {
1776
- return seriesKind ?? (index === 0 ? 'bar' : 'line');
1777
- }
1778
- return chartKind === 'stacked-bar' ? 'bar' : chartKind;
1779
- }
1780
- shouldSmoothSeries(chartKind, seriesKind) {
1781
- if (chartKind === 'combo') {
1782
- return seriesKind === 'line' || seriesKind === 'area' ? true : undefined;
1783
- }
1784
- return chartKind === 'line' || chartKind === 'area' || chartKind === 'stacked-area' ? true : undefined;
1785
- }
1786
- mapTextValue(value) {
1787
- if (!value)
1788
- return undefined;
1789
- if (typeof value === 'string')
1790
- return value;
1791
- return {
1792
- key: value.key,
1793
- text: value.fallback,
1794
- };
1795
- }
1796
- toLabel(value) {
1797
- if (!value)
1798
- return undefined;
1799
- if (typeof value === 'string')
1800
- return value;
1801
- return value.fallback || value.key;
1802
- }
1803
- resolveToggle(value, defaultValue = false) {
1804
- if (typeof value === 'boolean')
1805
- return value;
1806
- if (typeof value === 'object' && value)
1807
- return value.enabled;
1808
- return defaultValue;
1809
- }
1810
- mapAggregation(aggregation) {
1811
- switch (aggregation) {
1812
- case 'avg':
1813
- case 'min':
1814
- case 'max':
1815
- case 'count':
1816
- case 'sum':
1817
- return aggregation;
1818
- case undefined:
1819
- return undefined;
1820
- default:
1821
- throw new Error(`x-ui.chart aggregation "${aggregation}" is not yet implemented in @praxisui/charts.`);
2086
+ const PRAXIS_CHART_STATE_PROBE_COMPONENT_METADATA = {
2087
+ id: 'praxis-chart-state-probe',
2088
+ componentType: 'praxis-chart-state-probe',
2089
+ displayName: 'Praxis Chart State Probe',
2090
+ selector: 'praxis-chart-state-probe',
2091
+ component: PraxisChartStateProbeComponent,
2092
+ friendlyName: 'Praxis Chart State Probe',
2093
+ description: 'Diagnostic widget used to inspect chart events and runtime payloads during local validation.',
2094
+ icon: 'monitoring',
2095
+ tags: ['chart', 'probe', 'debug', 'widget'],
2096
+ lib: '@praxisui/charts',
2097
+ inputs: [
2098
+ {
2099
+ name: 'title',
2100
+ type: 'string',
2101
+ description: 'Probe panel title.',
2102
+ },
2103
+ {
2104
+ name: 'value',
2105
+ type: 'unknown',
2106
+ description: 'Payload rendered as formatted JSON.',
2107
+ },
2108
+ ],
2109
+ };
2110
+ function providePraxisChartStateProbeMetadata() {
2111
+ return {
2112
+ provide: ENVIRONMENT_INITIALIZER,
2113
+ multi: true,
2114
+ useFactory: (registry) => () => {
2115
+ registry.register(PRAXIS_CHART_STATE_PROBE_COMPONENT_METADATA);
2116
+ },
2117
+ deps: [ComponentMetadataRegistry],
2118
+ };
2119
+ }
2120
+
2121
+ class PraxisChartShowcaseWidgetComponent {
2122
+ config = input.required(...(ngDevMode ? [{ debugName: "config" }] : []));
2123
+ data = input(null, ...(ngDevMode ? [{ debugName: "data" }] : []));
2124
+ chartDocument = input(null, ...(ngDevMode ? [{ debugName: "chartDocument" }] : []));
2125
+ enableCustomization = input(false, ...(ngDevMode ? [{ debugName: "enableCustomization" }] : []));
2126
+ viewMode = input('chart', ...(ngDevMode ? [{ debugName: "viewMode" }] : []));
2127
+ pointClick = output();
2128
+ queryRequest = output();
2129
+ loadStateChange = output();
2130
+ statsApi = inject(PraxisChartStatsApiService);
2131
+ destroyRef = inject(DestroyRef);
2132
+ remoteResolvedRows = signal(null, ...(ngDevMode ? [{ debugName: "remoteResolvedRows" }] : []));
2133
+ remoteRuntimeState = signal('idle', ...(ngDevMode ? [{ debugName: "remoteRuntimeState" }] : []));
2134
+ remoteTechnicalError = signal(null, ...(ngDevMode ? [{ debugName: "remoteTechnicalError" }] : []));
2135
+ previousRemoteSignature = null;
2136
+ resolvedRows = computed(() => {
2137
+ const explicitData = this.data();
2138
+ if (explicitData !== null && explicitData !== undefined) {
2139
+ return explicitData;
1822
2140
  }
1823
- }
1824
- toQueryFilterMap(filters) {
1825
- if (!filters?.length)
1826
- return undefined;
1827
- return filters.reduce((acc, filter) => {
1828
- if (filter.value !== undefined) {
1829
- acc[filter.field] = filter.value;
1830
- return acc;
2141
+ return this.remoteResolvedRows() ?? [];
2142
+ }, ...(ngDevMode ? [{ debugName: "resolvedRows" }] : []));
2143
+ tableId = computed(() => `${this.config().id || 'praxis-chart'}-showcase-table`, ...(ngDevMode ? [{ debugName: "tableId" }] : []));
2144
+ resolvedTitle = computed(() => resolveText(this.config().title) || 'Tabela analitica', ...(ngDevMode ? [{ debugName: "resolvedTitle" }] : []));
2145
+ resolvedSubtitle = computed(() => resolveText(this.config().subtitle) || 'Mesmo dataset em visualizacao tabular', ...(ngDevMode ? [{ debugName: "resolvedSubtitle" }] : []));
2146
+ loadingLabel = computed(() => resolveText(this.config().state?.loadingLabel) || 'Carregando analytics...', ...(ngDevMode ? [{ debugName: "loadingLabel" }] : []));
2147
+ errorTitle = computed(() => this.remoteTechnicalError()
2148
+ || resolveText(this.config().state?.error?.title)
2149
+ || 'Falha no contrato analitico', ...(ngDevMode ? [{ debugName: "errorTitle" }] : []));
2150
+ errorDescription = computed(() => resolveText(this.config().state?.error?.description), ...(ngDevMode ? [{ debugName: "errorDescription" }] : []));
2151
+ stateMode = computed(() => resolveShowcaseStateMode(this.config(), this.remoteRuntimeState()), ...(ngDevMode ? [{ debugName: "stateMode" }] : []));
2152
+ tableConfig = computed(() => buildTableConfig(this.config(), this.resolvedRows()), ...(ngDevMode ? [{ debugName: "tableConfig" }] : []));
2153
+ constructor() {
2154
+ effect(() => {
2155
+ const config = this.config();
2156
+ const explicitData = this.data();
2157
+ const nextSignature = explicitData === null || explicitData === undefined
2158
+ ? buildRemoteSignature(config)
2159
+ : null;
2160
+ if (nextSignature === this.previousRemoteSignature) {
2161
+ return;
1831
2162
  }
1832
- if (filter.values !== undefined) {
1833
- acc[filter.field] = filter.values;
2163
+ this.previousRemoteSignature = nextSignature;
2164
+ this.remoteResolvedRows.set(null);
2165
+ this.remoteRuntimeState.set('idle');
2166
+ this.remoteTechnicalError.set(null);
2167
+ });
2168
+ effect(() => {
2169
+ const config = this.config();
2170
+ const explicitData = this.data();
2171
+ if (this.viewMode() !== 'table') {
2172
+ return;
1834
2173
  }
1835
- return acc;
1836
- }, {});
1837
- }
1838
- buildStatsPath(contract) {
1839
- return `${contract.source.resource.replace(/\/+$/, '')}/stats/${contract.source.operation}`;
2174
+ if (config.dataSource?.kind !== 'remote' || (explicitData !== null && explicitData !== undefined)) {
2175
+ return;
2176
+ }
2177
+ if (this.remoteRuntimeState() === 'loading' || this.remoteResolvedRows() !== null) {
2178
+ return;
2179
+ }
2180
+ const event = {
2181
+ chartId: config.id,
2182
+ dataSource: config.dataSource,
2183
+ query: config.dataSource.query,
2184
+ };
2185
+ this.queryRequest.emit(event);
2186
+ this.remoteRuntimeState.set('loading');
2187
+ this.remoteResolvedRows.set([]);
2188
+ this.remoteTechnicalError.set(null);
2189
+ this.statsApi.execute(event, config)
2190
+ .pipe(takeUntilDestroyed(this.destroyRef))
2191
+ .subscribe({
2192
+ next: (rows) => {
2193
+ this.remoteResolvedRows.set(rows);
2194
+ this.remoteRuntimeState.set('ready');
2195
+ this.remoteTechnicalError.set(null);
2196
+ },
2197
+ error: (error) => {
2198
+ this.remoteResolvedRows.set([]);
2199
+ this.remoteRuntimeState.set('error');
2200
+ this.remoteTechnicalError.set(error instanceof Error && error.message.trim()
2201
+ ? error.message
2202
+ : 'praxis.stats request failed');
2203
+ },
2204
+ });
2205
+ });
1840
2206
  }
1841
- buildStatsRequest(contract, filters) {
1842
- const field = this.resolveStatsField(contract);
1843
- const metrics = this.buildStatsMetricRequests(contract);
1844
- const metric = metrics[0];
1845
- const limit = contract.limit ?? contract.source.options?.limit;
1846
- const orderBy = this.mapStatsOrderByToBackend(contract.source.options?.orderBy);
1847
- const requestMetrics = metrics.length > 1 ? metrics : undefined;
1848
- switch (contract.source.operation) {
1849
- case 'group-by':
1850
- return {
1851
- filter: filters,
1852
- field,
1853
- metric,
1854
- metrics: requestMetrics,
1855
- limit,
1856
- orderBy,
1857
- };
1858
- case 'timeseries':
1859
- return {
1860
- filter: filters,
1861
- field,
1862
- granularity: this.mapStatsGranularityToBackend(contract.source.options?.granularity),
1863
- metric,
1864
- metrics: requestMetrics,
1865
- fillGaps: contract.source.options?.fillGaps,
1866
- };
1867
- case 'distribution':
1868
- return {
1869
- filter: filters,
1870
- field,
1871
- mode: this.mapDistributionModeToBackend(contract.source.options?.mode),
1872
- metric,
1873
- bucketSize: contract.source.options?.bucketSize,
1874
- bucketCount: contract.source.options?.bucketCount,
1875
- limit,
1876
- orderBy,
1877
- };
1878
- default:
1879
- throw new Error(`x-ui.chart source.operation "${contract.source.operation}" is not supported in @praxisui/charts.`);
2207
+ handleRowClick(event) {
2208
+ const row = event?.row;
2209
+ if (!row) {
2210
+ return;
1880
2211
  }
2212
+ const config = this.config();
2213
+ const primarySeries = config.series[0];
2214
+ const categoryField = primarySeries?.categoryField || config.axes?.x?.field;
2215
+ const metricField = primarySeries?.metric?.field;
2216
+ this.pointClick.emit({
2217
+ chartId: config.id,
2218
+ seriesId: primarySeries?.id,
2219
+ seriesName: primarySeries?.name,
2220
+ category: categoryField ? stringifyCell(row[categoryField]) : undefined,
2221
+ value: metricField ? row[metricField] : undefined,
2222
+ data: row,
2223
+ });
1881
2224
  }
1882
- resolveStatsField(contract) {
1883
- const dimensionField = contract.dimensions?.[0]?.field?.trim();
1884
- if (dimensionField) {
1885
- return dimensionField;
2225
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: PraxisChartShowcaseWidgetComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2226
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.17", type: PraxisChartShowcaseWidgetComponent, isStandalone: true, selector: "praxis-chart-showcase-widget", inputs: { config: { classPropertyName: "config", publicName: "config", isSignal: true, isRequired: true, transformFunction: null }, data: { classPropertyName: "data", publicName: "data", isSignal: true, isRequired: false, transformFunction: null }, chartDocument: { classPropertyName: "chartDocument", publicName: "chartDocument", isSignal: true, isRequired: false, transformFunction: null }, enableCustomization: { classPropertyName: "enableCustomization", publicName: "enableCustomization", isSignal: true, isRequired: false, transformFunction: null }, viewMode: { classPropertyName: "viewMode", publicName: "viewMode", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { pointClick: "pointClick", queryRequest: "queryRequest", loadStateChange: "loadStateChange" }, ngImport: i0, template: `
2227
+ @if (viewMode() === 'chart') {
2228
+ <praxis-chart
2229
+ [config]="config()"
2230
+ [data]="data()"
2231
+ [chartDocument]="chartDocument()"
2232
+ [enableCustomization]="enableCustomization()"
2233
+ (pointClick)="pointClick.emit($event)"
2234
+ (queryRequest)="queryRequest.emit($event)"
2235
+ (loadStateChange)="loadStateChange.emit($event)"
2236
+ ></praxis-chart>
2237
+ } @else if (stateMode() === 'loading') {
2238
+ <section class="showcase-state-card">
2239
+ <h4>{{ loadingLabel() }}</h4>
2240
+ </section>
2241
+ } @else if (stateMode() === 'error') {
2242
+ <section class="showcase-state-card showcase-state-card-error">
2243
+ <h4>{{ errorTitle() }}</h4>
2244
+ @if (errorDescription()) {
2245
+ <p>{{ errorDescription() }}</p>
1886
2246
  }
1887
- throw new Error('x-ui.chart requires dimensions[0].field to derive the canonical praxis.stats request.');
2247
+ </section>
2248
+ } @else {
2249
+ <praxis-table
2250
+ [config]="tableConfig()"
2251
+ [data]="resolvedRows()"
2252
+ [tableId]="tableId()"
2253
+ [title]="resolvedTitle()"
2254
+ [subtitle]="resolvedSubtitle()"
2255
+ [icon]="'table_view'"
2256
+ (rowClick)="handleRowClick($event)"
2257
+ ></praxis-table>
1888
2258
  }
1889
- buildStatsMetricRequest(contract) {
1890
- const metric = contract.metrics?.[0];
1891
- if (!metric) {
1892
- throw new Error('x-ui.chart requires metrics[0] to derive the canonical praxis.stats request.');
2259
+ `, isInline: true, styles: [":host{display:block;height:100%;min-width:0}.showcase-state-card{min-height:240px;display:grid;place-content:center;gap:8px;padding:24px;border-radius:18px;border:1px solid color-mix(in srgb,var(--md-sys-color-outline, #c5c7ce) 72%,transparent);background:linear-gradient(180deg,#fffffff5,#f4f7fbfa);text-align:center}.showcase-state-card h4{margin:0;font-size:1rem;font-weight:600;color:var(--md-sys-color-on-surface, #1a1b20)}.showcase-state-card p{margin:0;color:var(--md-sys-color-on-surface-variant, #5a5d67)}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: PraxisChartComponent, selector: "praxis-chart", inputs: ["config", "data", "chartDocument", "filterCriteria", "enableCustomization", "availableResources", "availableFields", "availableTargets"], outputs: ["pointClick", "queryRequest", "loadStateChange", "chartDocumentApplied", "chartDocumentSaved"] }, { kind: "component", type: PraxisTable, selector: "praxis-table", inputs: ["config", "resourcePath", "data", "tableId", "componentInstanceId", "title", "subtitle", "icon", "autoDelete", "notifyIfOutdated", "snoozeMs", "autoOpenSettingsOnOutdated", "crudContext", "dslFunctionRegistry", "editModeEnabled", "dense"], outputs: ["rowClick", "rowDoubleClick", "rowExpansionChange", "rowAction", "toolbarAction", "bulkAction", "columnReorder", "columnReorderAttempt", "beforeDelete", "afterDelete", "deleteError", "beforeBulkDelete", "afterBulkDelete", "bulkDeleteError", "schemaStatusChange", "metadataChange", "loadingStateChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
2260
+ }
2261
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: PraxisChartShowcaseWidgetComponent, decorators: [{
2262
+ type: Component,
2263
+ args: [{ selector: 'praxis-chart-showcase-widget', standalone: true, imports: [CommonModule, PraxisChartComponent, PraxisTable], changeDetection: ChangeDetectionStrategy.OnPush, template: `
2264
+ @if (viewMode() === 'chart') {
2265
+ <praxis-chart
2266
+ [config]="config()"
2267
+ [data]="data()"
2268
+ [chartDocument]="chartDocument()"
2269
+ [enableCustomization]="enableCustomization()"
2270
+ (pointClick)="pointClick.emit($event)"
2271
+ (queryRequest)="queryRequest.emit($event)"
2272
+ (loadStateChange)="loadStateChange.emit($event)"
2273
+ ></praxis-chart>
2274
+ } @else if (stateMode() === 'loading') {
2275
+ <section class="showcase-state-card">
2276
+ <h4>{{ loadingLabel() }}</h4>
2277
+ </section>
2278
+ } @else if (stateMode() === 'error') {
2279
+ <section class="showcase-state-card showcase-state-card-error">
2280
+ <h4>{{ errorTitle() }}</h4>
2281
+ @if (errorDescription()) {
2282
+ <p>{{ errorDescription() }}</p>
1893
2283
  }
1894
- const operation = this.mapStatsMetricOperation(metric.aggregation);
1895
- return {
1896
- operation,
1897
- field: operation === 'COUNT' ? undefined : metric.field,
1898
- alias: metric.field,
1899
- };
2284
+ </section>
2285
+ } @else {
2286
+ <praxis-table
2287
+ [config]="tableConfig()"
2288
+ [data]="resolvedRows()"
2289
+ [tableId]="tableId()"
2290
+ [title]="resolvedTitle()"
2291
+ [subtitle]="resolvedSubtitle()"
2292
+ [icon]="'table_view'"
2293
+ (rowClick)="handleRowClick($event)"
2294
+ ></praxis-table>
1900
2295
  }
1901
- buildStatsMetricRequests(contract) {
1902
- if (!contract.metrics?.length) {
1903
- throw new Error('x-ui.chart requires at least one metric to derive the canonical praxis.stats request.');
1904
- }
1905
- return contract.metrics.map((metric) => {
1906
- const operation = this.mapStatsMetricOperation(metric.aggregation);
1907
- return {
1908
- operation,
1909
- field: operation === 'COUNT' ? undefined : metric.field,
1910
- alias: metric.field,
1911
- };
1912
- });
2296
+ `, styles: [":host{display:block;height:100%;min-width:0}.showcase-state-card{min-height:240px;display:grid;place-content:center;gap:8px;padding:24px;border-radius:18px;border:1px solid color-mix(in srgb,var(--md-sys-color-outline, #c5c7ce) 72%,transparent);background:linear-gradient(180deg,#fffffff5,#f4f7fbfa);text-align:center}.showcase-state-card h4{margin:0;font-size:1rem;font-weight:600;color:var(--md-sys-color-on-surface, #1a1b20)}.showcase-state-card p{margin:0;color:var(--md-sys-color-on-surface-variant, #5a5d67)}\n"] }]
2297
+ }], ctorParameters: () => [], propDecorators: { config: [{ type: i0.Input, args: [{ isSignal: true, alias: "config", required: true }] }], data: [{ type: i0.Input, args: [{ isSignal: true, alias: "data", required: false }] }], chartDocument: [{ type: i0.Input, args: [{ isSignal: true, alias: "chartDocument", required: false }] }], enableCustomization: [{ type: i0.Input, args: [{ isSignal: true, alias: "enableCustomization", required: false }] }], viewMode: [{ type: i0.Input, args: [{ isSignal: true, alias: "viewMode", required: false }] }], pointClick: [{ type: i0.Output, args: ["pointClick"] }], queryRequest: [{ type: i0.Output, args: ["queryRequest"] }], loadStateChange: [{ type: i0.Output, args: ["loadStateChange"] }] } });
2298
+ function resolveShowcaseStateMode(config, remoteRuntimeState = 'idle') {
2299
+ if (config.preferredLoadState === 'loading') {
2300
+ return 'loading';
1913
2301
  }
1914
- mapStatsOrderBy(value) {
1915
- if (!value)
1916
- return undefined;
1917
- switch (value) {
1918
- case 'key-asc':
1919
- case 'key-desc':
1920
- case 'value-asc':
1921
- case 'value-desc':
1922
- return value;
1923
- default:
1924
- throw new Error(`x-ui.chart source.options.orderBy "${value}" is not supported in @praxisui/charts.`);
1925
- }
2302
+ if (config.preferredLoadState === 'error') {
2303
+ return 'error';
1926
2304
  }
1927
- mapStatsOrderByToBackend(value) {
1928
- switch (value) {
1929
- case 'key-asc':
1930
- return 'KEY_ASC';
1931
- case 'key-desc':
1932
- return 'KEY_DESC';
1933
- case 'value-asc':
1934
- return 'VALUE_ASC';
1935
- case 'value-desc':
1936
- return 'VALUE_DESC';
1937
- case undefined:
1938
- return undefined;
1939
- default:
1940
- throw new Error(`x-ui.chart source.options.orderBy "${value}" is not supported in @praxisui/charts.`);
1941
- }
2305
+ if (config.dataSource?.kind === 'remote' && remoteRuntimeState === 'error') {
2306
+ return 'error';
1942
2307
  }
1943
- mapStatsGranularityToBackend(value) {
1944
- switch (value) {
1945
- case 'hour':
1946
- return 'HOUR';
1947
- case 'day':
1948
- return 'DAY';
1949
- case 'week':
1950
- return 'WEEK';
1951
- case 'month':
1952
- case undefined:
1953
- return 'MONTH';
1954
- case 'quarter':
1955
- return 'QUARTER';
1956
- case 'year':
1957
- return 'YEAR';
1958
- default:
1959
- throw new Error(`x-ui.chart source.options.granularity "${value}" is not supported in @praxisui/charts.`);
1960
- }
2308
+ if (config.dataSource?.kind === 'remote' && remoteRuntimeState !== 'ready') {
2309
+ return 'loading';
1961
2310
  }
1962
- mapDistributionModeToBackend(value) {
1963
- switch (value) {
1964
- case 'histogram':
1965
- return 'HISTOGRAM';
1966
- case 'terms':
1967
- case undefined:
1968
- return 'TERMS';
1969
- default:
1970
- throw new Error(`x-ui.chart source.options.mode "${value}" is not supported in @praxisui/charts.`);
2311
+ return 'ready';
2312
+ }
2313
+ function buildTableConfig(config, rows) {
2314
+ const tableConfig = createDefaultTableConfig();
2315
+ const toolbar = tableConfig.toolbar;
2316
+ const behavior = tableConfig.behavior;
2317
+ const pagination = behavior.pagination;
2318
+ const sorting = behavior.sorting;
2319
+ const filtering = behavior.filtering;
2320
+ const selection = behavior.selection;
2321
+ const localDataMode = behavior.localDataMode;
2322
+ const actions = tableConfig.actions;
2323
+ const rowActions = actions.row;
2324
+ const bulkActions = actions.bulk;
2325
+ const appearance = tableConfig.appearance;
2326
+ const categoryField = config.axes?.x?.field || config.series[0]?.categoryField;
2327
+ const categoryLabel = config.axes?.x?.label || categoryField;
2328
+ const sampleRow = rows[0] || {};
2329
+ const seriesColumns = config.series
2330
+ .map((series) => {
2331
+ const field = series.metric?.field || series.categoryField;
2332
+ if (!field) {
2333
+ return null;
1971
2334
  }
2335
+ return {
2336
+ field,
2337
+ header: series.name || series.metric?.label || field,
2338
+ };
2339
+ })
2340
+ .filter((column) => column !== null);
2341
+ const fallbackColumns = Object.keys(sampleRow).map((field) => ({
2342
+ field,
2343
+ header: field,
2344
+ }));
2345
+ const categoryColumn = categoryField
2346
+ ? [{
2347
+ field: categoryField,
2348
+ header: categoryLabel || categoryField,
2349
+ }]
2350
+ : [];
2351
+ const dedupedColumns = [...categoryColumn, ...seriesColumns, ...fallbackColumns].filter((column, index, source) => source.findIndex((candidate) => candidate.field === column.field) === index);
2352
+ tableConfig.columns = dedupedColumns;
2353
+ toolbar.visible = false;
2354
+ pagination.enabled = rows.length > 10;
2355
+ sorting.enabled = true;
2356
+ filtering.enabled = false;
2357
+ selection.enabled = false;
2358
+ localDataMode.enabled = true;
2359
+ rowActions.enabled = false;
2360
+ rowActions.actions = [];
2361
+ bulkActions.enabled = false;
2362
+ bulkActions.actions = [];
2363
+ appearance.density = 'comfortable';
2364
+ return tableConfig;
2365
+ }
2366
+ function resolveText(value) {
2367
+ if (typeof value === 'string') {
2368
+ return value;
1972
2369
  }
1973
- mapStatsMetricOperation(aggregation) {
1974
- switch (aggregation) {
1975
- case undefined:
1976
- case 'count':
1977
- return 'COUNT';
1978
- case 'sum':
1979
- return 'SUM';
1980
- case 'avg':
1981
- return 'AVG';
1982
- case 'min':
1983
- return 'MIN';
1984
- case 'max':
1985
- return 'MAX';
1986
- default:
1987
- throw new Error(`x-ui.chart aggregation "${aggregation}" is not yet implemented in @praxisui/charts.`);
1988
- }
2370
+ if (value && typeof value === 'object' && 'text' in value) {
2371
+ const text = value.text;
2372
+ return typeof text === 'string' ? text : '';
1989
2373
  }
1990
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: PraxisChartCanonicalContractMapperService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
1991
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: PraxisChartCanonicalContractMapperService, providedIn: 'root' });
2374
+ return '';
2375
+ }
2376
+ function stringifyCell(value) {
2377
+ if (value === null || value === undefined) {
2378
+ return undefined;
2379
+ }
2380
+ return String(value);
2381
+ }
2382
+ function buildRemoteSignature(config) {
2383
+ if (config.dataSource?.kind !== 'remote') {
2384
+ return null;
2385
+ }
2386
+ return JSON.stringify({
2387
+ id: config.id,
2388
+ resourcePath: config.dataSource.resourcePath,
2389
+ query: config.dataSource.query,
2390
+ });
2391
+ }
2392
+
2393
+ const PRAXIS_CHART_SHOWCASE_WIDGET_METADATA = {
2394
+ id: 'praxis-chart-showcase-widget',
2395
+ componentType: 'praxis-chart-showcase-widget',
2396
+ displayName: 'Praxis Chart Showcase Widget',
2397
+ selector: 'praxis-chart-showcase-widget',
2398
+ component: PraxisChartShowcaseWidgetComponent,
2399
+ friendlyName: 'Praxis Chart Showcase Widget',
2400
+ description: 'Showcase-only widget that toggles between Praxis chart and Praxis table using the same dataset.',
2401
+ icon: 'swap_horiz',
2402
+ tags: ['chart', 'table', 'showcase', 'analytics'],
2403
+ lib: '@praxisui/charts',
2404
+ inputs: [
2405
+ {
2406
+ name: 'config',
2407
+ type: 'PraxisChartConfig',
2408
+ description: 'Chart configuration preserved across chart and table views.',
2409
+ },
2410
+ {
2411
+ name: 'data',
2412
+ type: 'PraxisChartDataRow[]',
2413
+ description: 'Local dataset shared by the chart and table renderers.',
2414
+ },
2415
+ {
2416
+ name: 'chartDocument',
2417
+ type: 'PraxisXUiChartContract | null',
2418
+ description: 'Canonical x-ui.chart contract forwarded to the runtime chart editor affordance.',
2419
+ },
2420
+ {
2421
+ name: 'enableCustomization',
2422
+ type: 'boolean',
2423
+ description: 'Enables runtime chart editor affordances when the host is in customization mode.',
2424
+ },
2425
+ {
2426
+ name: 'viewMode',
2427
+ type: "'chart' | 'table'",
2428
+ description: 'Current showcase view mode.',
2429
+ },
2430
+ ],
2431
+ outputs: [
2432
+ {
2433
+ name: 'pointClick',
2434
+ type: 'PraxisChartPointEvent',
2435
+ description: 'Forwards point selection from the chart or mapped row selection from the table.',
2436
+ },
2437
+ {
2438
+ name: 'queryRequest',
2439
+ type: 'PraxisChartQueryRequestEvent',
2440
+ description: 'Forwards remote chart data requests.',
2441
+ },
2442
+ {
2443
+ name: 'loadStateChange',
2444
+ type: 'PraxisChartLoadState',
2445
+ description: 'Forwards chart runtime state transitions.',
2446
+ },
2447
+ ],
2448
+ };
2449
+ function providePraxisChartShowcaseWidgetMetadata() {
2450
+ return {
2451
+ provide: ENVIRONMENT_INITIALIZER,
2452
+ multi: true,
2453
+ useFactory: (registry) => () => {
2454
+ registry.register(PRAXIS_CHART_SHOWCASE_WIDGET_METADATA);
2455
+ },
2456
+ deps: [ComponentMetadataRegistry],
2457
+ };
2458
+ }
2459
+
2460
+ function providePraxisCharts() {
2461
+ return [
2462
+ EChartsEngineAdapter,
2463
+ {
2464
+ provide: PRAXIS_CHART_ENGINE,
2465
+ useExisting: EChartsEngineAdapter,
2466
+ },
2467
+ providePraxisChartsMetadata(),
2468
+ providePraxisChartDrilldownPanelMetadata(),
2469
+ providePraxisChartStateProbeMetadata(),
2470
+ providePraxisChartShowcaseWidgetMetadata(),
2471
+ ];
1992
2472
  }
1993
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: PraxisChartCanonicalContractMapperService, decorators: [{
1994
- type: Injectable,
1995
- args: [{ providedIn: 'root' }]
1996
- }] });
1997
2473
 
1998
2474
  class PraxisChartMetadataRegistrationService {
1999
2475
  registry;
@@ -2012,13 +2488,13 @@ class PraxisChartMetadataRegistrationService {
2012
2488
  this.registry.register(PRAXIS_CHART_SHOWCASE_WIDGET_METADATA);
2013
2489
  this.registered = true;
2014
2490
  }
2015
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: PraxisChartMetadataRegistrationService, deps: [{ token: i1$1.ComponentMetadataRegistry }], target: i0.ɵɵFactoryTarget.Injectable });
2491
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: PraxisChartMetadataRegistrationService, deps: [{ token: i1$2.ComponentMetadataRegistry }], target: i0.ɵɵFactoryTarget.Injectable });
2016
2492
  static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: PraxisChartMetadataRegistrationService, providedIn: 'root' });
2017
2493
  }
2018
2494
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: PraxisChartMetadataRegistrationService, decorators: [{
2019
2495
  type: Injectable,
2020
2496
  args: [{ providedIn: 'root' }]
2021
- }], ctorParameters: () => [{ type: i1$1.ComponentMetadataRegistry }] });
2497
+ }], ctorParameters: () => [{ type: i1$2.ComponentMetadataRegistry }] });
2022
2498
 
2023
2499
  class PraxisChartSchemaMapperService {
2024
2500
  metadataRegistration;
@@ -2173,6 +2649,7 @@ class PraxisChartBackendPayloadAdapterService {
2173
2649
  inputs: {
2174
2650
  ...(widget.definition.inputs || {}),
2175
2651
  config: this.toChartConfig(payload),
2652
+ chartDocument: payload.widget.chart ?? null,
2176
2653
  },
2177
2654
  },
2178
2655
  };
@@ -2200,6 +2677,7 @@ class PraxisChartBackendPayloadAdapterService {
2200
2677
  inputs: {
2201
2678
  ...(widget.definition.inputs || {}),
2202
2679
  config: this.toChartConfig(payload),
2680
+ chartDocument: payload.widget.chart ?? null,
2203
2681
  },
2204
2682
  },
2205
2683
  };
@@ -2924,9 +3402,10 @@ function withShowcaseViewToggle(widget, initialViewMode = 'chart') {
2924
3402
  id: 'praxis-chart-showcase-widget',
2925
3403
  inputs: {
2926
3404
  ...(widget.definition.inputs || {}),
3405
+ enableCustomization: '${enableCustomization}',
2927
3406
  viewMode: initialViewMode,
2928
3407
  },
2929
- bindingOrder: ['config', 'data', 'viewMode'],
3408
+ bindingOrder: ['config', 'chartDocument', 'data', 'enableCustomization', 'viewMode'],
2930
3409
  },
2931
3410
  shell: {
2932
3411
  ...(widget.shell || {}),
@@ -2962,15 +3441,17 @@ function buildShowcaseToggleActions() {
2962
3441
  }
2963
3442
 
2964
3443
  class PraxisChartCompositionShowcaseComponent {
3444
+ enableCustomization = input(false, ...(ngDevMode ? [{ debugName: "enableCustomization", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
2965
3445
  layoutMode = signal('widget', ...(ngDevMode ? [{ debugName: "layoutMode" }] : []));
2966
3446
  payloadMode = signal('group-by', ...(ngDevMode ? [{ debugName: "payloadMode" }] : []));
2967
3447
  scenarioMode = signal('baseline', ...(ngDevMode ? [{ debugName: "scenarioMode" }] : []));
2968
- runtimeContext = {
3448
+ backendPayloadAdapter = inject(PraxisChartBackendPayloadAdapterService);
3449
+ runtimeContext = computed(() => ({
2969
3450
  tenantId: 'demo-enterprise',
2970
3451
  locale: 'pt-BR',
2971
3452
  environment: 'published-api',
2972
- };
2973
- backendPayloadAdapter = inject(PraxisChartBackendPayloadAdapterService);
3453
+ enableCustomization: this.enableCustomization(),
3454
+ }), ...(ngDevMode ? [{ debugName: "runtimeContext" }] : []));
2974
3455
  widgetPage = computed(() => {
2975
3456
  switch (this.scenarioMode()) {
2976
3457
  case 'interactive':
@@ -3123,7 +3604,7 @@ class PraxisChartCompositionShowcaseComponent {
3123
3604
  }
3124
3605
  }, ...(ngDevMode ? [{ debugName: "selectedPayload" }] : []));
3125
3606
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: PraxisChartCompositionShowcaseComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
3126
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.17", type: PraxisChartCompositionShowcaseComponent, isStandalone: true, selector: "praxis-chart-composition-showcase", providers: [providePraxisCharts()], ngImport: i0, template: `
3607
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.17", type: PraxisChartCompositionShowcaseComponent, isStandalone: true, selector: "praxis-chart-composition-showcase", inputs: { enableCustomization: { classPropertyName: "enableCustomization", publicName: "enableCustomization", isSignal: true, isRequired: false, transformFunction: null } }, providers: [providePraxisCharts()], ngImport: i0, template: `
3127
3608
  <section class="showcase-shell">
3128
3609
  <header class="showcase-hero">
3129
3610
  <div class="showcase-copy">
@@ -3215,11 +3696,14 @@ class PraxisChartCompositionShowcaseComponent {
3215
3696
  @if (layoutMode() === 'widget') {
3216
3697
  <praxis-dynamic-page
3217
3698
  [page]="widgetPage()"
3218
- [context]="runtimeContext"
3699
+ [context]="runtimeContext()"
3219
3700
  [autoPersist]="false"
3220
3701
  ></praxis-dynamic-page>
3221
3702
  } @else {
3222
- <praxis-dynamic-grid-page [page]="gridPage()" [context]="runtimeContext"></praxis-dynamic-grid-page>
3703
+ <praxis-dynamic-grid-page
3704
+ [page]="gridPage()"
3705
+ [context]="runtimeContext()"
3706
+ ></praxis-dynamic-grid-page>
3223
3707
  }
3224
3708
  </article>
3225
3709
 
@@ -3332,11 +3816,14 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
3332
3816
  @if (layoutMode() === 'widget') {
3333
3817
  <praxis-dynamic-page
3334
3818
  [page]="widgetPage()"
3335
- [context]="runtimeContext"
3819
+ [context]="runtimeContext()"
3336
3820
  [autoPersist]="false"
3337
3821
  ></praxis-dynamic-page>
3338
3822
  } @else {
3339
- <praxis-dynamic-grid-page [page]="gridPage()" [context]="runtimeContext"></praxis-dynamic-grid-page>
3823
+ <praxis-dynamic-grid-page
3824
+ [page]="gridPage()"
3825
+ [context]="runtimeContext()"
3826
+ ></praxis-dynamic-grid-page>
3340
3827
  }
3341
3828
  </article>
3342
3829
 
@@ -3354,8 +3841,1048 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
3354
3841
  </div>
3355
3842
  </section>
3356
3843
  `, styles: [":host{display:block}.showcase-shell{display:grid;gap:24px}.showcase-hero{display:grid;gap:20px;padding:28px;border-radius:28px;background:radial-gradient(circle at top right,rgba(18,99,180,.18),transparent 30%),linear-gradient(135deg,#071836f2,#1263b4c7);color:#f7fbff}.showcase-copy h2{margin:0 0 10px;font-size:clamp(1.8rem,3vw,2.6rem);line-height:1.05}.showcase-copy p{margin:0;max-width:52rem;color:#f7fbffe0}.showcase-eyebrow,.panel-kicker{margin:0 0 8px;font-size:.78rem;letter-spacing:.14em;text-transform:uppercase;color:#f7fbffb8}.showcase-controls{display:flex;flex-wrap:wrap;gap:16px}.control-group{display:grid;gap:8px}.control-group span{font-size:.84rem;color:#f7fbffc2}.control-buttons{display:flex;flex-wrap:wrap;gap:8px}.control-buttons button{border:1px solid rgba(247,251,255,.22);background:#f7fbff14;color:#f7fbff;border-radius:999px;padding:10px 14px;cursor:pointer;transition:background .16s ease,transform .16s ease}.control-buttons button.active{background:#f7fbff;color:#0d2d5f}.showcase-grid{display:grid;gap:20px;grid-template-columns:minmax(0,1.3fr) minmax(320px,.9fr)}.runtime-panel,.payload-panel{display:grid;gap:16px;padding:20px;border-radius:24px;border:1px solid color-mix(in srgb,var(--md-sys-color-outline, #c5c7ce) 76%,transparent);background:linear-gradient(180deg,#fffffff0,#f4f7fbf5)}.panel-header{display:flex;align-items:start;justify-content:space-between;gap:12px}.panel-header h3{margin:0;color:#142033}.panel-chip{border-radius:999px;padding:6px 10px;font-size:.76rem;background:#1263b41f;color:#1263b4}.payload-panel pre{margin:0;padding:16px;border-radius:18px;background:#09111f;color:#d7e6ff;overflow:auto;font-size:.84rem;line-height:1.5}@media(max-width:1080px){.showcase-grid{grid-template-columns:1fr}}\n"] }]
3844
+ }], propDecorators: { enableCustomization: [{ type: i0.Input, args: [{ isSignal: true, alias: "enableCustomization", required: false }] }] } });
3845
+
3846
+ const PRAXIS_CHARTS_EN_US = {
3847
+ 'praxis.charts.runtime.editChart': 'Edit chart settings',
3848
+ 'praxis.charts.runtime.invalidDocumentTitle': 'Invalid canonical configuration',
3849
+ 'praxis.charts.runtime.invalidDocumentDescription': 'The canonical chart document could not be mapped to the current Praxis Charts runtime. Review the chart contract before continuing.',
3850
+ 'praxis.charts.editor.section.general': 'General',
3851
+ 'praxis.charts.editor.section.data': 'Data',
3852
+ 'praxis.charts.editor.section.analytics': 'Analytics',
3853
+ 'praxis.charts.editor.section.appearance': 'Appearance',
3854
+ 'praxis.charts.editor.section.motion': 'Motion',
3855
+ 'praxis.charts.editor.section.events': 'Events',
3856
+ 'praxis.charts.editor.section.preview': 'Preview',
3857
+ 'praxis.charts.editor.field.chartId': 'Chart ID',
3858
+ 'praxis.charts.editor.field.kind': 'Kind',
3859
+ 'praxis.charts.editor.field.title': 'Title',
3860
+ 'praxis.charts.editor.field.subtitle': 'Subtitle',
3861
+ 'praxis.charts.editor.field.height': 'Height',
3862
+ 'praxis.charts.editor.field.sourceKind': 'Source',
3863
+ 'praxis.charts.editor.field.resource': 'Resource',
3864
+ 'praxis.charts.editor.field.operation': 'Operation',
3865
+ 'praxis.charts.editor.field.granularity': 'Granularity',
3866
+ 'praxis.charts.editor.field.fillGaps': 'Fill missing intervals',
3867
+ 'praxis.charts.editor.field.distributionMode': 'Distribution mode',
3868
+ 'praxis.charts.editor.field.bucketSize': 'Bucket size',
3869
+ 'praxis.charts.editor.field.bucketCount': 'Bucket count',
3870
+ 'praxis.charts.editor.field.dimension': 'Dimension',
3871
+ 'praxis.charts.editor.field.dimensionRole': 'Dimension role',
3872
+ 'praxis.charts.editor.field.metric': 'Metric',
3873
+ 'praxis.charts.editor.field.metricLabel': 'Metric label',
3874
+ 'praxis.charts.editor.field.metricAggregation': 'Aggregation',
3875
+ 'praxis.charts.editor.field.metricAxis': 'Axis',
3876
+ 'praxis.charts.editor.field.metricSeriesKind': 'Series kind',
3877
+ 'praxis.charts.editor.field.legendEnabled': 'Show legend',
3878
+ 'praxis.charts.editor.field.labelsEnabled': 'Show labels',
3879
+ 'praxis.charts.editor.field.tooltipEnabled': 'Show tooltip',
3880
+ 'praxis.charts.editor.field.palette': 'Palette colors',
3881
+ 'praxis.charts.editor.field.emptyTitle': 'Empty title',
3882
+ 'praxis.charts.editor.field.emptyDescription': 'Empty description',
3883
+ 'praxis.charts.editor.field.loadingTitle': 'Loading title',
3884
+ 'praxis.charts.editor.field.loadingDescription': 'Loading description',
3885
+ 'praxis.charts.editor.field.errorTitle': 'Error title',
3886
+ 'praxis.charts.editor.field.errorDescription': 'Error description',
3887
+ 'praxis.charts.editor.field.motionEnabled': 'Enable animations',
3888
+ 'praxis.charts.editor.field.motionPreset': 'Motion preset',
3889
+ 'praxis.charts.editor.field.eventAction': 'Action',
3890
+ 'praxis.charts.editor.field.eventTarget': 'Target',
3891
+ 'praxis.charts.editor.field.eventMapping': 'Mapping',
3892
+ 'praxis.charts.editor.preview.title': 'Chart preview',
3893
+ 'praxis.charts.editor.preview.caption': 'Local preview derived from the canonical contract without remote calls.',
3894
+ 'praxis.charts.editor.preview.invalid': 'Preview is unavailable while the contract has blocking errors.',
3895
+ 'praxis.charts.editor.issues.title': 'Validation issues',
3896
+ 'praxis.charts.editor.issues.empty': 'No issues were identified.',
3897
+ 'praxis.charts.editor.appearance.featuresTitle': 'Display features',
3898
+ 'praxis.charts.editor.appearance.paletteTitle': 'Palette',
3899
+ 'praxis.charts.editor.appearance.paletteHint': 'Use comma or line separated colors to persist theme.palette as a canonical array.',
3900
+ 'praxis.charts.editor.appearance.statesTitle': 'State messages',
3901
+ 'praxis.charts.editor.analytics.dimensionsTitle': 'Dimensions',
3902
+ 'praxis.charts.editor.analytics.metricsTitle': 'Metrics',
3903
+ 'praxis.charts.editor.analytics.addDimension': 'Add dimension',
3904
+ 'praxis.charts.editor.analytics.addMetric': 'Add metric',
3905
+ 'praxis.charts.editor.analytics.removeDimension': 'Remove dimension',
3906
+ 'praxis.charts.editor.analytics.removeMetric': 'Remove metric',
3907
+ 'praxis.charts.editor.specialization.timeseriesTitle': 'Timeseries options',
3908
+ 'praxis.charts.editor.specialization.distributionTitle': 'Distribution options',
3909
+ 'praxis.charts.editor.specialization.comboTitle': 'Combo guidance',
3910
+ 'praxis.charts.editor.specialization.comboHint': 'Combo charts require at least two metrics and allow per-metric axis and series kind mapping.',
3911
+ 'praxis.charts.editor.specialization.pieDonutTitle': 'Composition guidance',
3912
+ 'praxis.charts.editor.specialization.pieDonutHint': 'Pie and donut charts keep only the first metric and use the first dimension as the category segment.',
3913
+ 'praxis.charts.editor.specialization.scatterTitle': 'Scatter guidance',
3914
+ 'praxis.charts.editor.specialization.scatterHint': 'Scatter charts use the first dimension as X and the first metric as Y.',
3915
+ 'praxis.charts.editor.events.pointClickTitle': 'Point click',
3916
+ 'praxis.charts.editor.events.drillDownTitle': 'Drill down',
3917
+ 'praxis.charts.editor.events.none': 'None',
3918
+ 'praxis.charts.editor.sourceKind.praxisStats': 'praxis.stats',
3919
+ 'praxis.charts.editor.sourceKind.derived': 'derived',
3920
+ 'praxis.charts.editor.operation.groupBy': 'group-by',
3921
+ 'praxis.charts.editor.operation.timeseries': 'timeseries',
3922
+ 'praxis.charts.editor.operation.distribution': 'distribution',
3923
+ 'praxis.charts.editor.granularity.hour': 'hour',
3924
+ 'praxis.charts.editor.granularity.day': 'day',
3925
+ 'praxis.charts.editor.granularity.week': 'week',
3926
+ 'praxis.charts.editor.granularity.month': 'month',
3927
+ 'praxis.charts.editor.granularity.quarter': 'quarter',
3928
+ 'praxis.charts.editor.granularity.year': 'year',
3929
+ 'praxis.charts.editor.distributionMode.terms': 'terms',
3930
+ 'praxis.charts.editor.distributionMode.histogram': 'histogram',
3931
+ 'praxis.charts.editor.motionPreset.subtle': 'subtle',
3932
+ 'praxis.charts.editor.motionPreset.standard': 'standard',
3933
+ 'praxis.charts.editor.motionPreset.expressive': 'expressive',
3934
+ 'praxis.charts.editor.dimensionRole.category': 'category',
3935
+ 'praxis.charts.editor.dimensionRole.series': 'series',
3936
+ 'praxis.charts.editor.dimensionRole.segment': 'segment',
3937
+ 'praxis.charts.editor.dimensionRole.time': 'time',
3938
+ 'praxis.charts.editor.dimensionRole.value': 'value',
3939
+ 'praxis.charts.editor.metricAggregation.sum': 'sum',
3940
+ 'praxis.charts.editor.metricAggregation.count': 'count',
3941
+ 'praxis.charts.editor.metricAggregation.avg': 'avg',
3942
+ 'praxis.charts.editor.metricAggregation.min': 'min',
3943
+ 'praxis.charts.editor.metricAggregation.max': 'max',
3944
+ 'praxis.charts.editor.metricAxis.primary': 'primary',
3945
+ 'praxis.charts.editor.metricAxis.secondary': 'secondary',
3946
+ 'praxis.charts.editor.metricSeriesKind.bar': 'bar',
3947
+ 'praxis.charts.editor.metricSeriesKind.line': 'line',
3948
+ 'praxis.charts.editor.metricSeriesKind.area': 'area',
3949
+ 'praxis.charts.editor.eventAction.filter-widget': 'filter-widget',
3950
+ 'praxis.charts.editor.eventAction.open-detail': 'open-detail',
3951
+ 'praxis.charts.editor.eventAction.navigate': 'navigate',
3952
+ 'praxis.charts.editor.eventAction.update-context': 'update-context',
3953
+ 'praxis.charts.editor.eventAction.emit': 'emit',
3954
+ 'praxis.charts.editor.kind.bar': 'Bar',
3955
+ 'praxis.charts.editor.kind.horizontal-bar': 'Horizontal Bar',
3956
+ 'praxis.charts.editor.kind.line': 'Line',
3957
+ 'praxis.charts.editor.kind.area': 'Area',
3958
+ 'praxis.charts.editor.kind.stacked-bar': 'Stacked Bar',
3959
+ 'praxis.charts.editor.kind.stacked-area': 'Stacked Area',
3960
+ 'praxis.charts.editor.kind.combo': 'Combo',
3961
+ 'praxis.charts.editor.kind.pie': 'Pie',
3962
+ 'praxis.charts.editor.kind.donut': 'Donut',
3963
+ 'praxis.charts.editor.kind.scatter': 'Scatter',
3964
+ };
3965
+
3966
+ const PRAXIS_CHARTS_PT_BR = {
3967
+ 'praxis.charts.runtime.editChart': 'Editar configuracoes do chart',
3968
+ 'praxis.charts.runtime.invalidDocumentTitle': 'Configuracao canonica invalida',
3969
+ 'praxis.charts.runtime.invalidDocumentDescription': 'O documento canonico do chart nao pode ser mapeado para o runtime atual do Praxis Charts. Revise o contrato do chart antes de continuar.',
3970
+ 'praxis.charts.editor.section.general': 'Geral',
3971
+ 'praxis.charts.editor.section.data': 'Dados',
3972
+ 'praxis.charts.editor.section.analytics': 'Estrutura',
3973
+ 'praxis.charts.editor.section.appearance': 'Aparencia',
3974
+ 'praxis.charts.editor.section.motion': 'Motion',
3975
+ 'praxis.charts.editor.section.events': 'Eventos',
3976
+ 'praxis.charts.editor.section.preview': 'Preview',
3977
+ 'praxis.charts.editor.field.chartId': 'Chart ID',
3978
+ 'praxis.charts.editor.field.kind': 'Tipo',
3979
+ 'praxis.charts.editor.field.title': 'Titulo',
3980
+ 'praxis.charts.editor.field.subtitle': 'Subtitulo',
3981
+ 'praxis.charts.editor.field.height': 'Altura',
3982
+ 'praxis.charts.editor.field.sourceKind': 'Fonte',
3983
+ 'praxis.charts.editor.field.resource': 'Recurso',
3984
+ 'praxis.charts.editor.field.operation': 'Operacao',
3985
+ 'praxis.charts.editor.field.granularity': 'Granularidade',
3986
+ 'praxis.charts.editor.field.fillGaps': 'Preencher intervalos sem dados',
3987
+ 'praxis.charts.editor.field.distributionMode': 'Modo da distribuicao',
3988
+ 'praxis.charts.editor.field.bucketSize': 'Tamanho do bucket',
3989
+ 'praxis.charts.editor.field.bucketCount': 'Quantidade de buckets',
3990
+ 'praxis.charts.editor.field.dimension': 'Dimensao',
3991
+ 'praxis.charts.editor.field.dimensionRole': 'Papel da dimensao',
3992
+ 'praxis.charts.editor.field.metric': 'Metrica',
3993
+ 'praxis.charts.editor.field.metricLabel': 'Label da metrica',
3994
+ 'praxis.charts.editor.field.metricAggregation': 'Agregacao',
3995
+ 'praxis.charts.editor.field.metricAxis': 'Eixo',
3996
+ 'praxis.charts.editor.field.metricSeriesKind': 'Tipo da serie',
3997
+ 'praxis.charts.editor.field.legendEnabled': 'Exibir legenda',
3998
+ 'praxis.charts.editor.field.labelsEnabled': 'Exibir labels',
3999
+ 'praxis.charts.editor.field.tooltipEnabled': 'Exibir tooltip',
4000
+ 'praxis.charts.editor.field.palette': 'Cores da paleta',
4001
+ 'praxis.charts.editor.field.emptyTitle': 'Titulo do estado vazio',
4002
+ 'praxis.charts.editor.field.emptyDescription': 'Descricao do estado vazio',
4003
+ 'praxis.charts.editor.field.loadingTitle': 'Titulo do loading',
4004
+ 'praxis.charts.editor.field.loadingDescription': 'Descricao do loading',
4005
+ 'praxis.charts.editor.field.errorTitle': 'Titulo do erro',
4006
+ 'praxis.charts.editor.field.errorDescription': 'Descricao do erro',
4007
+ 'praxis.charts.editor.field.motionEnabled': 'Ativar animacoes',
4008
+ 'praxis.charts.editor.field.motionPreset': 'Preset de motion',
4009
+ 'praxis.charts.editor.field.eventAction': 'Acao',
4010
+ 'praxis.charts.editor.field.eventTarget': 'Destino',
4011
+ 'praxis.charts.editor.field.eventMapping': 'Mapeamento',
4012
+ 'praxis.charts.editor.preview.title': 'Preview do chart',
4013
+ 'praxis.charts.editor.preview.caption': 'Preview local derivado do contrato canonico, sem chamadas remotas.',
4014
+ 'praxis.charts.editor.preview.invalid': 'O preview fica indisponivel enquanto houver erro bloqueante no contrato.',
4015
+ 'praxis.charts.editor.issues.title': 'Issues de validacao',
4016
+ 'praxis.charts.editor.issues.empty': 'Nenhum issue identificado.',
4017
+ 'praxis.charts.editor.appearance.featuresTitle': 'Recursos visuais',
4018
+ 'praxis.charts.editor.appearance.paletteTitle': 'Paleta',
4019
+ 'praxis.charts.editor.appearance.paletteHint': 'Use cores separadas por virgula ou linha para persistir `theme.palette` como array canonico.',
4020
+ 'praxis.charts.editor.appearance.statesTitle': 'Mensagens de estado',
4021
+ 'praxis.charts.editor.analytics.dimensionsTitle': 'Dimensoes',
4022
+ 'praxis.charts.editor.analytics.metricsTitle': 'Metricas',
4023
+ 'praxis.charts.editor.analytics.addDimension': 'Adicionar dimensao',
4024
+ 'praxis.charts.editor.analytics.addMetric': 'Adicionar metrica',
4025
+ 'praxis.charts.editor.analytics.removeDimension': 'Remover dimensao',
4026
+ 'praxis.charts.editor.analytics.removeMetric': 'Remover metrica',
4027
+ 'praxis.charts.editor.specialization.timeseriesTitle': 'Opcoes de series temporais',
4028
+ 'praxis.charts.editor.specialization.distributionTitle': 'Opcoes de distribuicao',
4029
+ 'praxis.charts.editor.specialization.comboTitle': 'Guia de combo',
4030
+ 'praxis.charts.editor.specialization.comboHint': 'Charts combo exigem pelo menos duas metricas e permitem configurar eixo e tipo de serie por metrica.',
4031
+ 'praxis.charts.editor.specialization.pieDonutTitle': 'Guia de composicao',
4032
+ 'praxis.charts.editor.specialization.pieDonutHint': 'Charts pie e donut mantem apenas a primeira metrica e usam a primeira dimensao como segmento categorico.',
4033
+ 'praxis.charts.editor.specialization.scatterTitle': 'Guia de scatter',
4034
+ 'praxis.charts.editor.specialization.scatterHint': 'Charts scatter usam a primeira dimensao como eixo X e a primeira metrica como eixo Y.',
4035
+ 'praxis.charts.editor.events.pointClickTitle': 'Clique no ponto',
4036
+ 'praxis.charts.editor.events.drillDownTitle': 'Drill down',
4037
+ 'praxis.charts.editor.events.none': 'Nenhuma',
4038
+ 'praxis.charts.editor.sourceKind.praxisStats': 'praxis.stats',
4039
+ 'praxis.charts.editor.sourceKind.derived': 'derived',
4040
+ 'praxis.charts.editor.operation.groupBy': 'group-by',
4041
+ 'praxis.charts.editor.operation.timeseries': 'timeseries',
4042
+ 'praxis.charts.editor.operation.distribution': 'distribution',
4043
+ 'praxis.charts.editor.granularity.hour': 'hour',
4044
+ 'praxis.charts.editor.granularity.day': 'day',
4045
+ 'praxis.charts.editor.granularity.week': 'week',
4046
+ 'praxis.charts.editor.granularity.month': 'month',
4047
+ 'praxis.charts.editor.granularity.quarter': 'quarter',
4048
+ 'praxis.charts.editor.granularity.year': 'year',
4049
+ 'praxis.charts.editor.distributionMode.terms': 'terms',
4050
+ 'praxis.charts.editor.distributionMode.histogram': 'histogram',
4051
+ 'praxis.charts.editor.motionPreset.subtle': 'sutil',
4052
+ 'praxis.charts.editor.motionPreset.standard': 'padrao',
4053
+ 'praxis.charts.editor.motionPreset.expressive': 'expressivo',
4054
+ 'praxis.charts.editor.dimensionRole.category': 'category',
4055
+ 'praxis.charts.editor.dimensionRole.series': 'series',
4056
+ 'praxis.charts.editor.dimensionRole.segment': 'segment',
4057
+ 'praxis.charts.editor.dimensionRole.time': 'time',
4058
+ 'praxis.charts.editor.dimensionRole.value': 'value',
4059
+ 'praxis.charts.editor.metricAggregation.sum': 'sum',
4060
+ 'praxis.charts.editor.metricAggregation.count': 'count',
4061
+ 'praxis.charts.editor.metricAggregation.avg': 'avg',
4062
+ 'praxis.charts.editor.metricAggregation.min': 'min',
4063
+ 'praxis.charts.editor.metricAggregation.max': 'max',
4064
+ 'praxis.charts.editor.metricAxis.primary': 'primary',
4065
+ 'praxis.charts.editor.metricAxis.secondary': 'secondary',
4066
+ 'praxis.charts.editor.metricSeriesKind.bar': 'bar',
4067
+ 'praxis.charts.editor.metricSeriesKind.line': 'line',
4068
+ 'praxis.charts.editor.metricSeriesKind.area': 'area',
4069
+ 'praxis.charts.editor.eventAction.filter-widget': 'filter-widget',
4070
+ 'praxis.charts.editor.eventAction.open-detail': 'open-detail',
4071
+ 'praxis.charts.editor.eventAction.navigate': 'navigate',
4072
+ 'praxis.charts.editor.eventAction.update-context': 'update-context',
4073
+ 'praxis.charts.editor.eventAction.emit': 'emit',
4074
+ 'praxis.charts.editor.kind.bar': 'Bar',
4075
+ 'praxis.charts.editor.kind.horizontal-bar': 'Horizontal Bar',
4076
+ 'praxis.charts.editor.kind.line': 'Line',
4077
+ 'praxis.charts.editor.kind.area': 'Area',
4078
+ 'praxis.charts.editor.kind.stacked-bar': 'Stacked Bar',
4079
+ 'praxis.charts.editor.kind.stacked-area': 'Stacked Area',
4080
+ 'praxis.charts.editor.kind.combo': 'Combo',
4081
+ 'praxis.charts.editor.kind.pie': 'Pie',
4082
+ 'praxis.charts.editor.kind.donut': 'Donut',
4083
+ 'praxis.charts.editor.kind.scatter': 'Scatter',
4084
+ };
4085
+
4086
+ const PRAXIS_CHARTS_I18N = new InjectionToken('PRAXIS_CHARTS_I18N', {
4087
+ factory: () => ({}),
4088
+ });
4089
+ function createPraxisChartsI18nConfig(options = {}) {
4090
+ const localeDictionaries = {
4091
+ 'pt-BR': {
4092
+ ...PRAXIS_CHARTS_PT_BR,
4093
+ ...(options.dictionaries?.['pt-BR'] ?? {}),
4094
+ },
4095
+ 'en-US': {
4096
+ ...PRAXIS_CHARTS_EN_US,
4097
+ ...(options.dictionaries?.['en-US'] ?? {}),
4098
+ },
4099
+ };
4100
+ for (const [locale, dictionary] of Object.entries(options.dictionaries ?? {})) {
4101
+ if (locale === 'pt-BR' || locale === 'en-US') {
4102
+ continue;
4103
+ }
4104
+ localeDictionaries[locale] = {
4105
+ ...(localeDictionaries[locale] ?? {}),
4106
+ ...dictionary,
4107
+ };
4108
+ }
4109
+ return {
4110
+ locale: options.locale,
4111
+ fallbackLocale: options.fallbackLocale ?? 'pt-BR',
4112
+ namespaces: {
4113
+ charts: localeDictionaries,
4114
+ },
4115
+ };
4116
+ }
4117
+ function providePraxisChartsI18n(options = {}) {
4118
+ return [
4119
+ {
4120
+ provide: PRAXIS_CHARTS_I18N,
4121
+ useValue: {},
4122
+ },
4123
+ providePraxisI18n(createPraxisChartsI18nConfig(options)),
4124
+ ];
4125
+ }
4126
+ function resolvePraxisChartsText(value, fallback) {
4127
+ if (typeof value === 'string') {
4128
+ return { text: value };
4129
+ }
4130
+ if (value?.key || value?.text) {
4131
+ return value;
4132
+ }
4133
+ return { text: fallback ?? '' };
4134
+ }
4135
+
4136
+ class ChartEditorDefaultsService {
4137
+ create() {
4138
+ return {
4139
+ version: '1.0.0',
4140
+ kind: 'bar',
4141
+ title: 'Untitled chart',
4142
+ source: {
4143
+ kind: 'derived',
4144
+ },
4145
+ dimensions: [{ field: 'category', role: 'category' }],
4146
+ metrics: [{ field: 'value', aggregation: 'sum', label: 'Value' }],
4147
+ legend: { enabled: true },
4148
+ labels: { enabled: false },
4149
+ tooltip: { enabled: true },
4150
+ motion: {
4151
+ enabled: true,
4152
+ preset: 'standard',
4153
+ },
4154
+ };
4155
+ }
4156
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: ChartEditorDefaultsService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
4157
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: ChartEditorDefaultsService, providedIn: 'root' });
4158
+ }
4159
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: ChartEditorDefaultsService, decorators: [{
4160
+ type: Injectable,
4161
+ args: [{ providedIn: 'root' }]
3357
4162
  }] });
3358
4163
 
4164
+ class ChartEditorPreviewMapperService {
4165
+ mapper;
4166
+ constructor(mapper) {
4167
+ this.mapper = mapper;
4168
+ }
4169
+ build(document) {
4170
+ const config = this.mapper.toPraxisChartConfig(document);
4171
+ return {
4172
+ config,
4173
+ data: this.buildRows(document),
4174
+ };
4175
+ }
4176
+ buildRows(document) {
4177
+ const dimensionField = document.dimensions?.[0]?.field?.trim();
4178
+ const metrics = document.metrics ?? [];
4179
+ if (!dimensionField || !metrics.length) {
4180
+ return [];
4181
+ }
4182
+ if (document.kind === 'scatter') {
4183
+ return metrics[0]?.field
4184
+ ? [
4185
+ { [dimensionField]: 10, [metrics[0].field]: 22 },
4186
+ { [dimensionField]: 18, [metrics[0].field]: 31 },
4187
+ { [dimensionField]: 27, [metrics[0].field]: 45 },
4188
+ ]
4189
+ : [];
4190
+ }
4191
+ const categories = document.source.kind === 'praxis.stats' && document.source.operation === 'timeseries'
4192
+ ? ['2026-01', '2026-02', '2026-03']
4193
+ : ['Alpha', 'Beta', 'Gamma'];
4194
+ return categories.map((category, index) => {
4195
+ const row = {
4196
+ [dimensionField]: category,
4197
+ };
4198
+ metrics.forEach((metric, metricIndex) => {
4199
+ if (!metric.field) {
4200
+ return;
4201
+ }
4202
+ row[metric.field] = (index + 1) * 10 * (metricIndex + 1) + metricIndex * 5;
4203
+ });
4204
+ return row;
4205
+ });
4206
+ }
4207
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: ChartEditorPreviewMapperService, deps: [{ token: PraxisChartCanonicalContractMapperService }], target: i0.ɵɵFactoryTarget.Injectable });
4208
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: ChartEditorPreviewMapperService, providedIn: 'root' });
4209
+ }
4210
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: ChartEditorPreviewMapperService, decorators: [{
4211
+ type: Injectable,
4212
+ args: [{ providedIn: 'root' }]
4213
+ }], ctorParameters: () => [{ type: PraxisChartCanonicalContractMapperService }] });
4214
+
4215
+ class PraxisChartConfigEditor {
4216
+ documentInput = input(null, ...(ngDevMode ? [{ debugName: "documentInput", alias: 'document' }] : [{ alias: 'document' }]));
4217
+ modeInput = input('edit', ...(ngDevMode ? [{ debugName: "modeInput", alias: 'mode' }] : [{ alias: 'mode' }]));
4218
+ readonlyInput = input(false, ...(ngDevMode ? [{ debugName: "readonlyInput", alias: 'readonly' }] : [{ alias: 'readonly' }]));
4219
+ availableResourcesInput = input([], ...(ngDevMode ? [{ debugName: "availableResourcesInput", alias: 'availableResources' }] : [{ alias: 'availableResources' }]));
4220
+ availableFieldsInput = input([], ...(ngDevMode ? [{ debugName: "availableFieldsInput", alias: 'availableFields' }] : [{ alias: 'availableFields' }]));
4221
+ availableTargetsInput = input([], ...(ngDevMode ? [{ debugName: "availableTargetsInput", alias: 'availableTargets' }] : [{ alias: 'availableTargets' }]));
4222
+ apply = output();
4223
+ save = output();
4224
+ resetChange = output();
4225
+ documentChange = output();
4226
+ isDirty$ = new BehaviorSubject(false);
4227
+ isValid$ = new BehaviorSubject(true);
4228
+ isBusy$ = new BehaviorSubject(false);
4229
+ sections = [
4230
+ { id: 'general', labelKey: 'praxis.charts.editor.section.general', fallback: 'General' },
4231
+ { id: 'data', labelKey: 'praxis.charts.editor.section.data', fallback: 'Data' },
4232
+ { id: 'analytics', labelKey: 'praxis.charts.editor.section.analytics', fallback: 'Analytics' },
4233
+ { id: 'appearance', labelKey: 'praxis.charts.editor.section.appearance', fallback: 'Appearance' },
4234
+ { id: 'motion', labelKey: 'praxis.charts.editor.section.motion', fallback: 'Motion' },
4235
+ { id: 'events', labelKey: 'praxis.charts.editor.section.events', fallback: 'Events' },
4236
+ { id: 'preview', labelKey: 'praxis.charts.editor.section.preview', fallback: 'Preview' },
4237
+ ];
4238
+ chartKinds = [
4239
+ 'bar',
4240
+ 'horizontal-bar',
4241
+ 'line',
4242
+ 'area',
4243
+ 'stacked-bar',
4244
+ 'stacked-area',
4245
+ 'combo',
4246
+ 'pie',
4247
+ 'donut',
4248
+ 'scatter',
4249
+ ];
4250
+ sourceKinds = ['praxis.stats', 'derived'];
4251
+ operations = ['group-by', 'timeseries', 'distribution'];
4252
+ timeGranularities = ['hour', 'day', 'week', 'month', 'quarter', 'year'];
4253
+ distributionModes = ['terms', 'histogram'];
4254
+ dimensionRoles = ['category', 'series', 'segment', 'time', 'value'];
4255
+ metricAggregations = ['sum', 'count', 'avg', 'min', 'max'];
4256
+ metricAxes = ['primary', 'secondary'];
4257
+ metricSeriesKinds = ['bar', 'line', 'area'];
4258
+ motionPresets = ['subtle', 'standard', 'expressive'];
4259
+ eventActionOptions = ['filter-widget', 'open-detail', 'navigate', 'update-context', 'emit'];
4260
+ activeSection = signal('general', ...(ngDevMode ? [{ debugName: "activeSection" }] : []));
4261
+ injectedData = inject(SETTINGS_PANEL_DATA, { optional: true });
4262
+ defaults = inject(ChartEditorDefaultsService);
4263
+ normalizer = inject(ChartContractNormalizerService);
4264
+ validator = inject(ChartContractValidationService);
4265
+ previewMapper = inject(ChartEditorPreviewMapperService);
4266
+ i18n = inject(PraxisI18nService);
4267
+ currentDocument = signal(this.defaults.create(), ...(ngDevMode ? [{ debugName: "currentDocument" }] : []));
4268
+ initialDocument = signal(this.defaults.create(), ...(ngDevMode ? [{ debugName: "initialDocument" }] : []));
4269
+ lastExternalSignature = null;
4270
+ normalizedDocument = computed(() => this.normalizer.normalize(this.currentDocument()), ...(ngDevMode ? [{ debugName: "normalizedDocument" }] : []));
4271
+ validation = computed(() => this.validator.validate(this.normalizedDocument()), ...(ngDevMode ? [{ debugName: "validation" }] : []));
4272
+ issues = computed(() => this.validation().issues, ...(ngDevMode ? [{ debugName: "issues" }] : []));
4273
+ availableResources = computed(() => this.availableResourcesInput().length
4274
+ ? this.availableResourcesInput()
4275
+ : (this.injectedData?.availableResources ?? []), ...(ngDevMode ? [{ debugName: "availableResources" }] : []));
4276
+ availableFields = computed(() => this.availableFieldsInput().length
4277
+ ? this.availableFieldsInput()
4278
+ : (this.injectedData?.availableFields ?? []), ...(ngDevMode ? [{ debugName: "availableFields" }] : []));
4279
+ availableTargets = computed(() => this.availableTargetsInput().length
4280
+ ? this.availableTargetsInput()
4281
+ : (this.injectedData?.availableTargets ?? []), ...(ngDevMode ? [{ debugName: "availableTargets" }] : []));
4282
+ preview = computed(() => {
4283
+ if (!this.validation().valid) {
4284
+ return null;
4285
+ }
4286
+ return this.previewMapper.build(this.normalizedDocument());
4287
+ }, ...(ngDevMode ? [{ debugName: "preview" }] : []));
4288
+ constructor() {
4289
+ effect(() => {
4290
+ const externalDocument = this.documentInput()
4291
+ ?? this.injectedData?.chartDocument
4292
+ ?? this.injectedData?.document
4293
+ ?? null;
4294
+ if (!externalDocument) {
4295
+ return;
4296
+ }
4297
+ const signature = JSON.stringify(externalDocument);
4298
+ if (signature === this.lastExternalSignature || this.isDirty$.getValue()) {
4299
+ return;
4300
+ }
4301
+ const normalized = this.normalizer.normalize(externalDocument);
4302
+ this.currentDocument.set(structuredClone(normalized));
4303
+ this.initialDocument.set(structuredClone(normalized));
4304
+ this.lastExternalSignature = signature;
4305
+ this.refreshState(false);
4306
+ });
4307
+ }
4308
+ getSettingsValue() {
4309
+ return structuredClone(this.normalizedDocument());
4310
+ }
4311
+ onSave() {
4312
+ return this.saveChanges().document;
4313
+ }
4314
+ reset() {
4315
+ const snapshot = structuredClone(this.initialDocument());
4316
+ this.currentDocument.set(snapshot);
4317
+ this.refreshState(false);
4318
+ this.resetChange.emit({ document: snapshot });
4319
+ }
4320
+ applyChanges() {
4321
+ const payload = this.createApplyPayload();
4322
+ this.apply.emit(payload);
4323
+ return payload;
4324
+ }
4325
+ saveChanges() {
4326
+ const payload = this.createSavePayload();
4327
+ this.initialDocument.set(structuredClone(payload.document));
4328
+ this.refreshState(false);
4329
+ this.save.emit(payload);
4330
+ return payload;
4331
+ }
4332
+ setSection(section) {
4333
+ this.activeSection.set(section);
4334
+ }
4335
+ setChartId(value) {
4336
+ this.patchDocument((document) => ({
4337
+ ...document,
4338
+ chartId: value.trim() || undefined,
4339
+ }));
4340
+ }
4341
+ setKind(value) {
4342
+ this.patchDocument((document) => ({
4343
+ ...document,
4344
+ kind: value,
4345
+ }));
4346
+ }
4347
+ setTitle(value) {
4348
+ this.patchDocument((document) => ({
4349
+ ...document,
4350
+ title: value.trim() || undefined,
4351
+ }));
4352
+ }
4353
+ setSubtitle(value) {
4354
+ this.patchDocument((document) => ({
4355
+ ...document,
4356
+ subtitle: value.trim() || undefined,
4357
+ }));
4358
+ }
4359
+ setHeight(value) {
4360
+ this.patchDocument((document) => ({
4361
+ ...document,
4362
+ height: value.trim() || undefined,
4363
+ }));
4364
+ }
4365
+ setSourceKind(value) {
4366
+ this.patchDocument((document) => ({
4367
+ ...document,
4368
+ source: value === 'derived'
4369
+ ? { kind: 'derived' }
4370
+ : {
4371
+ kind: 'praxis.stats',
4372
+ resource: document.source.kind === 'praxis.stats' ? document.source.resource : '',
4373
+ operation: document.source.kind === 'praxis.stats' ? document.source.operation ?? 'group-by' : 'group-by',
4374
+ options: document.source.kind === 'praxis.stats' ? document.source.options : undefined,
4375
+ },
4376
+ }));
4377
+ }
4378
+ setResource(value) {
4379
+ this.patchDocument((document) => ({
4380
+ ...document,
4381
+ source: document.source.kind === 'praxis.stats'
4382
+ ? {
4383
+ ...document.source,
4384
+ resource: value.trim() || undefined,
4385
+ }
4386
+ : document.source,
4387
+ }));
4388
+ }
4389
+ setOperation(value) {
4390
+ this.patchDocument((document) => ({
4391
+ ...document,
4392
+ source: document.source.kind === 'praxis.stats'
4393
+ ? {
4394
+ ...document.source,
4395
+ operation: value,
4396
+ }
4397
+ : document.source,
4398
+ }));
4399
+ }
4400
+ setGranularity(value) {
4401
+ this.patchDocument((document) => ({
4402
+ ...document,
4403
+ source: document.source.kind === 'praxis.stats'
4404
+ ? {
4405
+ ...document.source,
4406
+ options: {
4407
+ ...(document.source.options ?? {}),
4408
+ granularity: value,
4409
+ },
4410
+ }
4411
+ : document.source,
4412
+ }));
4413
+ }
4414
+ setFillGaps(value) {
4415
+ this.patchDocument((document) => ({
4416
+ ...document,
4417
+ source: document.source.kind === 'praxis.stats'
4418
+ ? {
4419
+ ...document.source,
4420
+ options: {
4421
+ ...(document.source.options ?? {}),
4422
+ fillGaps: value,
4423
+ },
4424
+ }
4425
+ : document.source,
4426
+ }));
4427
+ }
4428
+ setDistributionMode(value) {
4429
+ this.patchDocument((document) => ({
4430
+ ...document,
4431
+ source: document.source.kind === 'praxis.stats'
4432
+ ? {
4433
+ ...document.source,
4434
+ options: {
4435
+ ...(document.source.options ?? {}),
4436
+ mode: value,
4437
+ },
4438
+ }
4439
+ : document.source,
4440
+ }));
4441
+ }
4442
+ setBucketSize(value) {
4443
+ const parsed = Number(value);
4444
+ this.patchDocument((document) => ({
4445
+ ...document,
4446
+ source: document.source.kind === 'praxis.stats'
4447
+ ? {
4448
+ ...document.source,
4449
+ options: {
4450
+ ...(document.source.options ?? {}),
4451
+ bucketSize: Number.isFinite(parsed) && value.trim() !== '' ? parsed : undefined,
4452
+ },
4453
+ }
4454
+ : document.source,
4455
+ }));
4456
+ }
4457
+ setBucketCount(value) {
4458
+ const parsed = Number(value);
4459
+ this.patchDocument((document) => ({
4460
+ ...document,
4461
+ source: document.source.kind === 'praxis.stats'
4462
+ ? {
4463
+ ...document.source,
4464
+ options: {
4465
+ ...(document.source.options ?? {}),
4466
+ bucketCount: Number.isFinite(parsed) && value.trim() !== '' ? parsed : undefined,
4467
+ },
4468
+ }
4469
+ : document.source,
4470
+ }));
4471
+ }
4472
+ addDimension() {
4473
+ this.patchDocument((document) => ({
4474
+ ...document,
4475
+ dimensions: [
4476
+ ...(document.dimensions ?? []),
4477
+ { field: '', role: 'category' },
4478
+ ],
4479
+ }));
4480
+ }
4481
+ removeDimension(index) {
4482
+ this.patchDocument((document) => ({
4483
+ ...document,
4484
+ dimensions: (document.dimensions ?? []).filter((_, currentIndex) => currentIndex !== index),
4485
+ }));
4486
+ }
4487
+ setDimensionField(index, field) {
4488
+ this.patchDocument((document) => ({
4489
+ ...document,
4490
+ dimensions: (document.dimensions ?? []).map((dimension, currentIndex) => currentIndex === index
4491
+ ? { ...dimension, field: field.trim() || '' }
4492
+ : dimension),
4493
+ }));
4494
+ }
4495
+ setDimensionRole(index, role) {
4496
+ this.patchDocument((document) => ({
4497
+ ...document,
4498
+ dimensions: (document.dimensions ?? []).map((dimension, currentIndex) => currentIndex === index
4499
+ ? { ...dimension, role }
4500
+ : dimension),
4501
+ }));
4502
+ }
4503
+ addMetric() {
4504
+ this.patchDocument((document) => ({
4505
+ ...document,
4506
+ metrics: [
4507
+ ...(document.metrics ?? []),
4508
+ { field: '', aggregation: 'sum', label: '' },
4509
+ ],
4510
+ }));
4511
+ }
4512
+ removeMetric(index) {
4513
+ this.patchDocument((document) => ({
4514
+ ...document,
4515
+ metrics: (document.metrics ?? []).filter((_, currentIndex) => currentIndex !== index),
4516
+ }));
4517
+ }
4518
+ setMetricField(index, field) {
4519
+ this.patchDocument((document) => ({
4520
+ ...document,
4521
+ metrics: (document.metrics ?? []).map((metric, currentIndex) => currentIndex === index
4522
+ ? { ...metric, field: field.trim() || '' }
4523
+ : metric),
4524
+ }));
4525
+ }
4526
+ setMetricLabel(index, label) {
4527
+ this.patchDocument((document) => ({
4528
+ ...document,
4529
+ metrics: (document.metrics ?? []).map((metric, currentIndex) => currentIndex === index
4530
+ ? { ...metric, label: label.trim() || undefined }
4531
+ : metric),
4532
+ }));
4533
+ }
4534
+ setMetricAggregation(index, aggregation) {
4535
+ this.patchDocument((document) => ({
4536
+ ...document,
4537
+ metrics: (document.metrics ?? []).map((metric, currentIndex) => currentIndex === index
4538
+ ? { ...metric, aggregation }
4539
+ : metric),
4540
+ }));
4541
+ }
4542
+ setMetricAxis(index, axis) {
4543
+ this.patchDocument((document) => ({
4544
+ ...document,
4545
+ metrics: (document.metrics ?? []).map((metric, currentIndex) => currentIndex === index
4546
+ ? { ...metric, axis }
4547
+ : metric),
4548
+ }));
4549
+ }
4550
+ setMetricSeriesKind(index, seriesKind) {
4551
+ this.patchDocument((document) => ({
4552
+ ...document,
4553
+ metrics: (document.metrics ?? []).map((metric, currentIndex) => currentIndex === index
4554
+ ? { ...metric, seriesKind }
4555
+ : metric),
4556
+ }));
4557
+ }
4558
+ setMotionEnabled(enabled) {
4559
+ this.patchDocument((document) => ({
4560
+ ...document,
4561
+ motion: {
4562
+ ...(document.motion ?? {}),
4563
+ enabled,
4564
+ },
4565
+ }));
4566
+ }
4567
+ setMotionPreset(preset) {
4568
+ this.patchDocument((document) => ({
4569
+ ...document,
4570
+ motion: {
4571
+ ...(document.motion ?? {}),
4572
+ enabled: document.motion?.enabled ?? true,
4573
+ preset,
4574
+ },
4575
+ }));
4576
+ }
4577
+ setFeatureEnabled(feature, enabled) {
4578
+ this.patchDocument((document) => ({
4579
+ ...document,
4580
+ [feature]: {
4581
+ enabled,
4582
+ },
4583
+ }));
4584
+ }
4585
+ setPalette(value) {
4586
+ const palette = value
4587
+ .split(/[\n,]/)
4588
+ .map((item) => item.trim())
4589
+ .filter(Boolean);
4590
+ this.patchDocument((document) => ({
4591
+ ...document,
4592
+ theme: {
4593
+ ...(document.theme ?? {}),
4594
+ palette: palette.length ? palette : undefined,
4595
+ },
4596
+ }));
4597
+ }
4598
+ setStateTitle(stateKey, value) {
4599
+ this.patchStateDescriptor(stateKey, (descriptor) => ({
4600
+ ...descriptor,
4601
+ title: value.trim() || undefined,
4602
+ }));
4603
+ }
4604
+ setStateDescription(stateKey, value) {
4605
+ this.patchStateDescriptor(stateKey, (descriptor) => ({
4606
+ ...descriptor,
4607
+ description: value.trim() || undefined,
4608
+ }));
4609
+ }
4610
+ setEventAction(eventKey, action) {
4611
+ this.patchDocument((document) => {
4612
+ const currentEvent = document.events?.[eventKey];
4613
+ if (!action) {
4614
+ return {
4615
+ ...document,
4616
+ events: {
4617
+ ...(document.events ?? {}),
4618
+ [eventKey]: undefined,
4619
+ },
4620
+ };
4621
+ }
4622
+ return {
4623
+ ...document,
4624
+ events: {
4625
+ ...(document.events ?? {}),
4626
+ [eventKey]: {
4627
+ ...(currentEvent ?? {}),
4628
+ action,
4629
+ },
4630
+ },
4631
+ };
4632
+ });
4633
+ }
4634
+ setEventTarget(eventKey, target) {
4635
+ this.patchEvent(eventKey, (currentEvent) => ({
4636
+ ...currentEvent,
4637
+ target: target.trim() || undefined,
4638
+ }));
4639
+ }
4640
+ setEventMapping(eventKey, value) {
4641
+ const mapping = this.parseMappingText(value);
4642
+ this.patchEvent(eventKey, (currentEvent) => ({
4643
+ ...currentEvent,
4644
+ mapping: Object.keys(mapping).length ? mapping : undefined,
4645
+ }));
4646
+ }
4647
+ isReadonly() {
4648
+ return this.readonlyInput() || this.injectedData?.readonly === true;
4649
+ }
4650
+ titleValue() {
4651
+ const value = this.currentDocument().title;
4652
+ if (typeof value === 'string') {
4653
+ return value;
4654
+ }
4655
+ return value?.fallback ?? '';
4656
+ }
4657
+ subtitleValue() {
4658
+ const value = this.currentDocument().subtitle;
4659
+ if (typeof value === 'string') {
4660
+ return value;
4661
+ }
4662
+ return value?.fallback ?? '';
4663
+ }
4664
+ resourceValue() {
4665
+ return this.currentDocument().source.kind === 'praxis.stats'
4666
+ ? this.currentDocument().source.resource ?? ''
4667
+ : '';
4668
+ }
4669
+ resourceOptions() {
4670
+ return this.availableResources();
4671
+ }
4672
+ granularityValue() {
4673
+ return this.doc().source.kind === 'praxis.stats'
4674
+ ? (this.doc().source.options?.granularity ?? 'day')
4675
+ : 'day';
4676
+ }
4677
+ fillGapsValue() {
4678
+ return this.doc().source.kind === 'praxis.stats'
4679
+ ? this.doc().source.options?.fillGaps ?? false
4680
+ : false;
4681
+ }
4682
+ distributionModeValue() {
4683
+ return this.doc().source.kind === 'praxis.stats'
4684
+ ? (this.doc().source.options?.mode ?? 'terms')
4685
+ : 'terms';
4686
+ }
4687
+ bucketSizeValue() {
4688
+ const value = this.doc().source.kind === 'praxis.stats'
4689
+ ? this.doc().source.options?.bucketSize
4690
+ : undefined;
4691
+ return value === undefined ? '' : String(value);
4692
+ }
4693
+ bucketCountValue() {
4694
+ const value = this.doc().source.kind === 'praxis.stats'
4695
+ ? this.doc().source.options?.bucketCount
4696
+ : undefined;
4697
+ return value === undefined ? '' : String(value);
4698
+ }
4699
+ heightValue() {
4700
+ const height = this.currentDocument().height;
4701
+ return height === undefined || height === null ? '' : String(height);
4702
+ }
4703
+ featureEnabled(feature) {
4704
+ const value = this.doc()[feature];
4705
+ if (typeof value === 'boolean') {
4706
+ return value;
4707
+ }
4708
+ return value?.enabled ?? false;
4709
+ }
4710
+ paletteValue() {
4711
+ const palette = this.doc().theme?.palette;
4712
+ return Array.isArray(palette) ? palette.join(', ') : '';
4713
+ }
4714
+ stateTitle(stateKey) {
4715
+ const value = this.doc().state?.[stateKey]?.title;
4716
+ if (typeof value === 'string') {
4717
+ return value;
4718
+ }
4719
+ return value?.fallback ?? '';
4720
+ }
4721
+ stateDescription(stateKey) {
4722
+ const value = this.doc().state?.[stateKey]?.description;
4723
+ if (typeof value === 'string') {
4724
+ return value;
4725
+ }
4726
+ return value?.fallback ?? '';
4727
+ }
4728
+ eventAction(eventKey) {
4729
+ return this.doc().events?.[eventKey]?.action ?? '';
4730
+ }
4731
+ eventTarget(eventKey) {
4732
+ return this.doc().events?.[eventKey]?.target ?? '';
4733
+ }
4734
+ targetOptions() {
4735
+ return this.availableTargets();
4736
+ }
4737
+ eventMappingText(eventKey) {
4738
+ const mapping = this.doc().events?.[eventKey]?.mapping;
4739
+ if (!mapping) {
4740
+ return '';
4741
+ }
4742
+ return Object.entries(mapping)
4743
+ .map(([key, value]) => `${key}=${value}`)
4744
+ .join('\n');
4745
+ }
4746
+ dimensions() {
4747
+ return this.doc().dimensions ?? [];
4748
+ }
4749
+ metrics() {
4750
+ return this.doc().metrics ?? [];
4751
+ }
4752
+ fieldOptions(role) {
4753
+ const fields = this.availableFields();
4754
+ if (!fields.length) {
4755
+ return [];
4756
+ }
4757
+ return fields.filter((field) => {
4758
+ if (!field.roles?.length) {
4759
+ return role === 'dimension' ? field.aggregable !== true : true;
4760
+ }
4761
+ return field.roles.includes(role);
4762
+ });
4763
+ }
4764
+ showMetricAxisControls() {
4765
+ return this.doc().kind === 'combo';
4766
+ }
4767
+ showMetricSeriesKindControls() {
4768
+ return this.doc().kind === 'combo';
4769
+ }
4770
+ showTimeseriesControls() {
4771
+ return this.doc().source.kind === 'praxis.stats' && this.doc().source.operation === 'timeseries';
4772
+ }
4773
+ showDistributionControls() {
4774
+ return this.doc().source.kind === 'praxis.stats' && this.doc().source.operation === 'distribution';
4775
+ }
4776
+ showComboPanel() {
4777
+ return this.doc().kind === 'combo';
4778
+ }
4779
+ showPieDonutPanel() {
4780
+ return this.doc().kind === 'pie' || this.doc().kind === 'donut';
4781
+ }
4782
+ showScatterPanel() {
4783
+ return this.doc().kind === 'scatter';
4784
+ }
4785
+ issueTrackBy(_, issue) {
4786
+ return `${issue.code}:${issue.field}`;
4787
+ }
4788
+ t(key, fallback) {
4789
+ return this.i18n.resolve(resolvePraxisChartsText({ key, text: fallback }, fallback));
4790
+ }
4791
+ doc() {
4792
+ return this.currentDocument();
4793
+ }
4794
+ patchStateDescriptor(stateKey, updater) {
4795
+ this.patchDocument((document) => ({
4796
+ ...document,
4797
+ state: {
4798
+ ...(document.state ?? {}),
4799
+ [stateKey]: updater(document.state?.[stateKey] ?? {}),
4800
+ },
4801
+ }));
4802
+ }
4803
+ patchEvent(eventKey, updater) {
4804
+ this.patchDocument((document) => {
4805
+ const currentEvent = document.events?.[eventKey];
4806
+ if (!currentEvent?.action) {
4807
+ return document;
4808
+ }
4809
+ return {
4810
+ ...document,
4811
+ events: {
4812
+ ...(document.events ?? {}),
4813
+ [eventKey]: updater(currentEvent),
4814
+ },
4815
+ };
4816
+ });
4817
+ }
4818
+ parseMappingText(value) {
4819
+ return value
4820
+ .split('\n')
4821
+ .map((line) => line.trim())
4822
+ .filter(Boolean)
4823
+ .reduce((accumulator, line) => {
4824
+ const separatorIndex = line.indexOf('=');
4825
+ if (separatorIndex <= 0) {
4826
+ return accumulator;
4827
+ }
4828
+ const key = line.slice(0, separatorIndex).trim();
4829
+ const mappedValue = line.slice(separatorIndex + 1).trim();
4830
+ if (!key || !mappedValue) {
4831
+ return accumulator;
4832
+ }
4833
+ accumulator[key] = mappedValue;
4834
+ return accumulator;
4835
+ }, {});
4836
+ }
4837
+ createApplyPayload() {
4838
+ return {
4839
+ document: this.getSettingsValue(),
4840
+ issues: [...this.issues()],
4841
+ dirty: this.isDirty$.getValue(),
4842
+ };
4843
+ }
4844
+ createSavePayload() {
4845
+ return {
4846
+ document: this.getSettingsValue(),
4847
+ issues: [...this.issues()],
4848
+ dirty: this.isDirty$.getValue(),
4849
+ };
4850
+ }
4851
+ patchDocument(updater) {
4852
+ if (this.isReadonly()) {
4853
+ return;
4854
+ }
4855
+ this.currentDocument.set(updater(structuredClone(this.currentDocument())));
4856
+ this.refreshState(true);
4857
+ }
4858
+ refreshState(markDirty) {
4859
+ this.isDirty$.next(markDirty);
4860
+ this.isValid$.next(this.validation().valid);
4861
+ this.documentChange.emit(structuredClone(this.normalizedDocument()));
4862
+ }
4863
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: PraxisChartConfigEditor, deps: [], target: i0.ɵɵFactoryTarget.Component });
4864
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.17", type: PraxisChartConfigEditor, isStandalone: true, selector: "praxis-chart-config-editor", inputs: { documentInput: { classPropertyName: "documentInput", publicName: "document", isSignal: true, isRequired: false, transformFunction: null }, modeInput: { classPropertyName: "modeInput", publicName: "mode", isSignal: true, isRequired: false, transformFunction: null }, readonlyInput: { classPropertyName: "readonlyInput", publicName: "readonly", isSignal: true, isRequired: false, transformFunction: null }, availableResourcesInput: { classPropertyName: "availableResourcesInput", publicName: "availableResources", isSignal: true, isRequired: false, transformFunction: null }, availableFieldsInput: { classPropertyName: "availableFieldsInput", publicName: "availableFields", isSignal: true, isRequired: false, transformFunction: null }, availableTargetsInput: { classPropertyName: "availableTargetsInput", publicName: "availableTargets", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { apply: "apply", save: "save", resetChange: "resetChange", documentChange: "documentChange" }, providers: [providePraxisChartsI18n()], ngImport: i0, template: "<div class=\"editor-shell\">\n <div class=\"editor-nav\">\n @for (section of sections; track section.id) {\n <button\n mat-stroked-button\n type=\"button\"\n [class.active]=\"activeSection() === section.id\"\n (click)=\"setSection(section.id)\"\n >\n {{ t(section.labelKey, section.fallback) }}\n </button>\n }\n </div>\n\n <div class=\"editor-layout\">\n <div class=\"editor-form\">\n <mat-card class=\"editor-card\">\n <mat-card-content>\n @switch (activeSection()) {\n @case ('general') {\n <div class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.chartId', 'Chart ID') }}</mat-label>\n <input matInput [ngModel]=\"doc().chartId || ''\" (ngModelChange)=\"setChartId($event)\" [disabled]=\"isReadonly()\" />\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.kind', 'Kind') }}</mat-label>\n <mat-select [ngModel]=\"doc().kind\" (ngModelChange)=\"setKind($event)\" [disabled]=\"isReadonly()\">\n @for (kind of chartKinds; track kind) {\n <mat-option [value]=\"kind\">\n {{ t('praxis.charts.editor.kind.' + kind, kind) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.title', 'Title') }}</mat-label>\n <input matInput [ngModel]=\"titleValue()\" (ngModelChange)=\"setTitle($event)\" [disabled]=\"isReadonly()\" />\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.subtitle', 'Subtitle') }}</mat-label>\n <input matInput [ngModel]=\"subtitleValue()\" (ngModelChange)=\"setSubtitle($event)\" [disabled]=\"isReadonly()\" />\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.height', 'Height') }}</mat-label>\n <input matInput [ngModel]=\"heightValue()\" (ngModelChange)=\"setHeight($event)\" [disabled]=\"isReadonly()\" />\n </mat-form-field>\n </div>\n }\n\n @case ('data') {\n <div class=\"editor-stack\">\n <div class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.sourceKind', 'Source') }}</mat-label>\n <mat-select [ngModel]=\"doc().source.kind\" (ngModelChange)=\"setSourceKind($event)\" [disabled]=\"isReadonly()\">\n @for (sourceKind of sourceKinds; track sourceKind) {\n <mat-option [value]=\"sourceKind\">\n {{ t('praxis.charts.editor.sourceKind.' + (sourceKind === 'praxis.stats' ? 'praxisStats' : 'derived'), sourceKind) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n @if (doc().source.kind === 'praxis.stats') {\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.resource', 'Resource') }}</mat-label>\n @if (resourceOptions().length) {\n <mat-select [ngModel]=\"resourceValue()\" (ngModelChange)=\"setResource($event)\" [disabled]=\"isReadonly()\">\n @for (resource of resourceOptions(); track resource.id) {\n <mat-option [value]=\"resource.path\">{{ resource.label }}</mat-option>\n }\n </mat-select>\n } @else {\n <input matInput [ngModel]=\"resourceValue()\" (ngModelChange)=\"setResource($event)\" [disabled]=\"isReadonly()\" />\n }\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.operation', 'Operation') }}</mat-label>\n <mat-select\n [ngModel]=\"doc().source.operation || 'group-by'\"\n (ngModelChange)=\"setOperation($event)\"\n [disabled]=\"isReadonly()\"\n >\n @for (operation of operations; track operation) {\n <mat-option [value]=\"operation\">\n {{ t('praxis.charts.editor.operation.' + (operation === 'group-by' ? 'groupBy' : operation), operation) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n }\n </div>\n\n @if (showTimeseriesControls()) {\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.specialization.timeseriesTitle', 'Timeseries options') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.granularity', 'Granularity') }}</mat-label>\n <mat-select [ngModel]=\"granularityValue()\" (ngModelChange)=\"setGranularity($event)\" [disabled]=\"isReadonly()\">\n @for (granularity of timeGranularities; track granularity) {\n <mat-option [value]=\"granularity\">\n {{ t('praxis.charts.editor.granularity.' + granularity, granularity) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n <mat-slide-toggle\n [ngModel]=\"fillGapsValue()\"\n (ngModelChange)=\"setFillGaps($event)\"\n [disabled]=\"isReadonly()\"\n >\n {{ t('praxis.charts.editor.field.fillGaps', 'Fill missing intervals') }}\n </mat-slide-toggle>\n </mat-card-content>\n </mat-card>\n }\n\n @if (showDistributionControls()) {\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.specialization.distributionTitle', 'Distribution options') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.distributionMode', 'Distribution mode') }}</mat-label>\n <mat-select [ngModel]=\"distributionModeValue()\" (ngModelChange)=\"setDistributionMode($event)\" [disabled]=\"isReadonly()\">\n @for (mode of distributionModes; track mode) {\n <mat-option [value]=\"mode\">\n {{ t('praxis.charts.editor.distributionMode.' + mode, mode) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.bucketSize', 'Bucket size') }}</mat-label>\n <input matInput [ngModel]=\"bucketSizeValue()\" (ngModelChange)=\"setBucketSize($event)\" [disabled]=\"isReadonly()\" />\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.bucketCount', 'Bucket count') }}</mat-label>\n <input matInput [ngModel]=\"bucketCountValue()\" (ngModelChange)=\"setBucketCount($event)\" [disabled]=\"isReadonly()\" />\n </mat-form-field>\n </mat-card-content>\n </mat-card>\n }\n </div>\n }\n\n @case ('motion') {\n <div class=\"editor-grid\">\n <mat-slide-toggle\n [ngModel]=\"normalizedDocument().motion?.enabled !== false\"\n (ngModelChange)=\"setMotionEnabled($event)\"\n [disabled]=\"isReadonly()\"\n >\n {{ t('praxis.charts.editor.field.motionEnabled', 'Enable animations') }}\n </mat-slide-toggle>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.motionPreset', 'Motion preset') }}</mat-label>\n <mat-select\n [ngModel]=\"normalizedDocument().motion?.preset || 'standard'\"\n (ngModelChange)=\"setMotionPreset($event)\"\n [disabled]=\"isReadonly() || normalizedDocument().motion?.enabled === false\"\n >\n @for (preset of motionPresets; track preset) {\n <mat-option [value]=\"preset\">\n {{ t('praxis.charts.editor.motionPreset.' + preset, preset) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n </div>\n }\n\n @case ('appearance') {\n <div class=\"editor-stack\">\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.appearance.featuresTitle', 'Display features') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-grid\">\n <mat-slide-toggle\n [ngModel]=\"featureEnabled('legend')\"\n (ngModelChange)=\"setFeatureEnabled('legend', $event)\"\n [disabled]=\"isReadonly()\"\n >\n {{ t('praxis.charts.editor.field.legendEnabled', 'Show legend') }}\n </mat-slide-toggle>\n\n <mat-slide-toggle\n [ngModel]=\"featureEnabled('labels')\"\n (ngModelChange)=\"setFeatureEnabled('labels', $event)\"\n [disabled]=\"isReadonly()\"\n >\n {{ t('praxis.charts.editor.field.labelsEnabled', 'Show labels') }}\n </mat-slide-toggle>\n\n <mat-slide-toggle\n [ngModel]=\"featureEnabled('tooltip')\"\n (ngModelChange)=\"setFeatureEnabled('tooltip', $event)\"\n [disabled]=\"isReadonly()\"\n >\n {{ t('praxis.charts.editor.field.tooltipEnabled', 'Show tooltip') }}\n </mat-slide-toggle>\n </mat-card-content>\n </mat-card>\n\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.appearance.paletteTitle', 'Palette') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content>\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.palette', 'Palette colors') }}</mat-label>\n <textarea\n matInput\n rows=\"3\"\n [ngModel]=\"paletteValue()\"\n (ngModelChange)=\"setPalette($event)\"\n [disabled]=\"isReadonly()\"\n ></textarea>\n </mat-form-field>\n <p class=\"editor-caption\">\n {{ t('praxis.charts.editor.appearance.paletteHint', 'Use comma or line separated colors to persist theme.palette as a canonical array.') }}\n </p>\n </mat-card-content>\n </mat-card>\n\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.appearance.statesTitle', 'State messages') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-stack\">\n <div class=\"editor-row-card\">\n <div class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.emptyTitle', 'Empty title') }}</mat-label>\n <input matInput [ngModel]=\"stateTitle('empty')\" (ngModelChange)=\"setStateTitle('empty', $event)\" [disabled]=\"isReadonly()\" />\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.emptyDescription', 'Empty description') }}</mat-label>\n <textarea matInput rows=\"2\" [ngModel]=\"stateDescription('empty')\" (ngModelChange)=\"setStateDescription('empty', $event)\" [disabled]=\"isReadonly()\"></textarea>\n </mat-form-field>\n </div>\n </div>\n\n <div class=\"editor-row-card\">\n <div class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.loadingTitle', 'Loading title') }}</mat-label>\n <input matInput [ngModel]=\"stateTitle('loading')\" (ngModelChange)=\"setStateTitle('loading', $event)\" [disabled]=\"isReadonly()\" />\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.loadingDescription', 'Loading description') }}</mat-label>\n <textarea matInput rows=\"2\" [ngModel]=\"stateDescription('loading')\" (ngModelChange)=\"setStateDescription('loading', $event)\" [disabled]=\"isReadonly()\"></textarea>\n </mat-form-field>\n </div>\n </div>\n\n <div class=\"editor-row-card\">\n <div class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.errorTitle', 'Error title') }}</mat-label>\n <input matInput [ngModel]=\"stateTitle('error')\" (ngModelChange)=\"setStateTitle('error', $event)\" [disabled]=\"isReadonly()\" />\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.errorDescription', 'Error description') }}</mat-label>\n <textarea matInput rows=\"2\" [ngModel]=\"stateDescription('error')\" (ngModelChange)=\"setStateDescription('error', $event)\" [disabled]=\"isReadonly()\"></textarea>\n </mat-form-field>\n </div>\n </div>\n </mat-card-content>\n </mat-card>\n </div>\n }\n\n @case ('analytics') {\n <div class=\"editor-stack\">\n @if (showComboPanel()) {\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.specialization.comboTitle', 'Combo guidance') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content>\n <p class=\"editor-caption\">\n {{ t('praxis.charts.editor.specialization.comboHint', 'Combo charts require at least two metrics and allow per-metric axis and series kind mapping.') }}\n </p>\n </mat-card-content>\n </mat-card>\n }\n\n @if (showPieDonutPanel()) {\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.specialization.pieDonutTitle', 'Composition guidance') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content>\n <p class=\"editor-caption\">\n {{ t('praxis.charts.editor.specialization.pieDonutHint', 'Pie and donut charts keep only the first metric and use the first dimension as the category segment.') }}\n </p>\n </mat-card-content>\n </mat-card>\n }\n\n @if (showScatterPanel()) {\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.specialization.scatterTitle', 'Scatter guidance') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content>\n <p class=\"editor-caption\">\n {{ t('praxis.charts.editor.specialization.scatterHint', 'Scatter charts use the first dimension as X and the first metric as Y, so keep both fields mapped.') }}\n </p>\n </mat-card-content>\n </mat-card>\n }\n\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.analytics.dimensionsTitle', 'Dimensions') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-stack\">\n @for (dimension of dimensions(); track $index) {\n <div class=\"editor-row-card\">\n <div class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.dimension', 'Dimension') }}</mat-label>\n @if (fieldOptions('dimension').length) {\n <mat-select [ngModel]=\"dimension.field || ''\" (ngModelChange)=\"setDimensionField($index, $event)\" [disabled]=\"isReadonly()\">\n @for (field of fieldOptions('dimension'); track field.field) {\n <mat-option [value]=\"field.field\">{{ field.label || field.field }}</mat-option>\n }\n </mat-select>\n } @else {\n <input matInput [ngModel]=\"dimension.field || ''\" (ngModelChange)=\"setDimensionField($index, $event)\" [disabled]=\"isReadonly()\" />\n }\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.dimensionRole', 'Dimension role') }}</mat-label>\n <mat-select [ngModel]=\"dimension.role || 'category'\" (ngModelChange)=\"setDimensionRole($index, $event)\" [disabled]=\"isReadonly()\">\n @for (role of dimensionRoles; track role) {\n <mat-option [value]=\"role\">\n {{ t('praxis.charts.editor.dimensionRole.' + role, role) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n </div>\n\n <div class=\"editor-row-actions\">\n <button mat-button type=\"button\" (click)=\"removeDimension($index)\" [disabled]=\"isReadonly() || dimensions().length <= 1\">\n {{ t('praxis.charts.editor.analytics.removeDimension', 'Remove dimension') }}\n </button>\n </div>\n </div>\n }\n\n <div>\n <button mat-stroked-button type=\"button\" (click)=\"addDimension()\" [disabled]=\"isReadonly()\">\n {{ t('praxis.charts.editor.analytics.addDimension', 'Add dimension') }}\n </button>\n </div>\n </mat-card-content>\n </mat-card>\n\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.analytics.metricsTitle', 'Metrics') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-stack\">\n @for (metric of metrics(); track $index) {\n <div class=\"editor-row-card\">\n <div class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.metric', 'Metric') }}</mat-label>\n @if (fieldOptions('metric').length) {\n <mat-select [ngModel]=\"metric.field || ''\" (ngModelChange)=\"setMetricField($index, $event)\" [disabled]=\"isReadonly()\">\n @for (field of fieldOptions('metric'); track field.field) {\n <mat-option [value]=\"field.field\">{{ field.label || field.field }}</mat-option>\n }\n </mat-select>\n } @else {\n <input matInput [ngModel]=\"metric.field || ''\" (ngModelChange)=\"setMetricField($index, $event)\" [disabled]=\"isReadonly()\" />\n }\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.metricLabel', 'Metric label') }}</mat-label>\n <input matInput [ngModel]=\"metric.label || ''\" (ngModelChange)=\"setMetricLabel($index, $event)\" [disabled]=\"isReadonly()\" />\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.metricAggregation', 'Aggregation') }}</mat-label>\n <mat-select [ngModel]=\"metric.aggregation || 'sum'\" (ngModelChange)=\"setMetricAggregation($index, $event)\" [disabled]=\"isReadonly()\">\n @for (aggregation of metricAggregations; track aggregation) {\n <mat-option [value]=\"aggregation\">\n {{ t('praxis.charts.editor.metricAggregation.' + aggregation, aggregation) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n @if (showMetricAxisControls()) {\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.metricAxis', 'Axis') }}</mat-label>\n <mat-select [ngModel]=\"metric.axis || 'primary'\" (ngModelChange)=\"setMetricAxis($index, $event)\" [disabled]=\"isReadonly()\">\n @for (axis of metricAxes; track axis) {\n <mat-option [value]=\"axis\">\n {{ t('praxis.charts.editor.metricAxis.' + axis, axis) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n }\n\n @if (showMetricSeriesKindControls()) {\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.metricSeriesKind', 'Series kind') }}</mat-label>\n <mat-select [ngModel]=\"metric.seriesKind || 'bar'\" (ngModelChange)=\"setMetricSeriesKind($index, $event)\" [disabled]=\"isReadonly()\">\n @for (seriesKind of metricSeriesKinds; track seriesKind) {\n <mat-option [value]=\"seriesKind\">\n {{ t('praxis.charts.editor.metricSeriesKind.' + seriesKind, seriesKind) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n }\n </div>\n\n <div class=\"editor-row-actions\">\n <button mat-button type=\"button\" (click)=\"removeMetric($index)\" [disabled]=\"isReadonly() || metrics().length <= 1\">\n {{ t('praxis.charts.editor.analytics.removeMetric', 'Remove metric') }}\n </button>\n </div>\n </div>\n }\n\n <div>\n <button mat-stroked-button type=\"button\" (click)=\"addMetric()\" [disabled]=\"isReadonly()\">\n {{ t('praxis.charts.editor.analytics.addMetric', 'Add metric') }}\n </button>\n </div>\n </mat-card-content>\n </mat-card>\n </div>\n }\n\n @case ('events') {\n <div class=\"editor-stack\">\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.events.pointClickTitle', 'Point click') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventAction', 'Action') }}</mat-label>\n <mat-select [ngModel]=\"eventAction('pointClick')\" (ngModelChange)=\"setEventAction('pointClick', $event)\" [disabled]=\"isReadonly()\">\n <mat-option value=\"\">{{ t('praxis.charts.editor.events.none', 'None') }}</mat-option>\n @for (action of eventActionOptions; track action) {\n <mat-option [value]=\"action\">\n {{ t('praxis.charts.editor.eventAction.' + action, action) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventTarget', 'Target') }}</mat-label>\n @if (targetOptions().length) {\n <mat-select [ngModel]=\"eventTarget('pointClick')\" (ngModelChange)=\"setEventTarget('pointClick', $event)\" [disabled]=\"isReadonly() || !eventAction('pointClick')\">\n @for (target of targetOptions(); track target.id) {\n <mat-option [value]=\"target.id\">{{ target.label }}</mat-option>\n }\n </mat-select>\n } @else {\n <input matInput [ngModel]=\"eventTarget('pointClick')\" (ngModelChange)=\"setEventTarget('pointClick', $event)\" [disabled]=\"isReadonly() || !eventAction('pointClick')\" />\n }\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventMapping', 'Mapping') }}</mat-label>\n <textarea\n matInput\n rows=\"4\"\n [ngModel]=\"eventMappingText('pointClick')\"\n (ngModelChange)=\"setEventMapping('pointClick', $event)\"\n [disabled]=\"isReadonly() || !eventAction('pointClick')\"\n ></textarea>\n </mat-form-field>\n </mat-card-content>\n </mat-card>\n\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.events.drillDownTitle', 'Drill down') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventAction', 'Action') }}</mat-label>\n <mat-select [ngModel]=\"eventAction('drillDown')\" (ngModelChange)=\"setEventAction('drillDown', $event)\" [disabled]=\"isReadonly()\">\n <mat-option value=\"\">{{ t('praxis.charts.editor.events.none', 'None') }}</mat-option>\n @for (action of eventActionOptions; track action) {\n <mat-option [value]=\"action\">\n {{ t('praxis.charts.editor.eventAction.' + action, action) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventTarget', 'Target') }}</mat-label>\n @if (targetOptions().length) {\n <mat-select [ngModel]=\"eventTarget('drillDown')\" (ngModelChange)=\"setEventTarget('drillDown', $event)\" [disabled]=\"isReadonly() || !eventAction('drillDown')\">\n @for (target of targetOptions(); track target.id) {\n <mat-option [value]=\"target.id\">{{ target.label }}</mat-option>\n }\n </mat-select>\n } @else {\n <input matInput [ngModel]=\"eventTarget('drillDown')\" (ngModelChange)=\"setEventTarget('drillDown', $event)\" [disabled]=\"isReadonly() || !eventAction('drillDown')\" />\n }\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventMapping', 'Mapping') }}</mat-label>\n <textarea\n matInput\n rows=\"4\"\n [ngModel]=\"eventMappingText('drillDown')\"\n (ngModelChange)=\"setEventMapping('drillDown', $event)\"\n [disabled]=\"isReadonly() || !eventAction('drillDown')\"\n ></textarea>\n </mat-form-field>\n </mat-card-content>\n </mat-card>\n </div>\n }\n\n @case ('preview') {\n <p class=\"editor-caption\">\n {{ t('praxis.charts.editor.preview.caption', 'Local preview derived from the canonical contract without remote calls.') }}\n </p>\n }\n }\n </mat-card-content>\n </mat-card>\n </div>\n\n <div class=\"editor-side\">\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.issues.title', 'Validation issues') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content>\n @if (issues().length) {\n <ul class=\"editor-issues\">\n @for (issue of issues(); track issueTrackBy($index, issue)) {\n <li class=\"editor-issue\">\n <strong>{{ issue.field }}</strong>\n <span>{{ issue.message }}</span>\n </li>\n }\n </ul>\n } @else {\n <div class=\"editor-empty\">\n {{ t('praxis.charts.editor.issues.empty', 'No issues were identified.') }}\n </div>\n }\n </mat-card-content>\n </mat-card>\n\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.preview.title', 'Chart preview') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content>\n @if (preview(); as chartPreview) {\n <p class=\"editor-caption\">\n {{ t('praxis.charts.editor.preview.caption', 'Local preview derived from the canonical contract without remote calls.') }}\n </p>\n <praxis-chart [config]=\"chartPreview.config\" [data]=\"chartPreview.data\"></praxis-chart>\n } @else {\n <div class=\"editor-empty\">\n {{ t('praxis.charts.editor.preview.invalid', 'Preview is unavailable while the contract has blocking errors.') }}\n </div>\n }\n </mat-card-content>\n </mat-card>\n </div>\n </div>\n</div>\n", styles: [":host{display:block;min-width:0;color:var(--md-sys-color-on-surface, #1a1b20)}.editor-shell{display:grid;gap:18px}.editor-nav{display:flex;gap:8px;flex-wrap:wrap}.editor-nav button.active{background:color-mix(in srgb,var(--md-sys-color-primary, #1263b4) 18%,transparent);color:var(--md-sys-color-primary, #1263b4)}.editor-layout{display:grid;gap:18px;grid-template-columns:minmax(0,1.35fr) minmax(320px,.9fr);align-items:start}.editor-form,.editor-side{display:grid;gap:16px}.editor-card{border-radius:20px;border:1px solid color-mix(in srgb,var(--md-sys-color-outline, #c5c7ce) 72%,transparent);background:linear-gradient(180deg,#1263b408,#1263b400)}.editor-grid{display:grid;gap:14px;grid-template-columns:repeat(auto-fit,minmax(220px,1fr))}.editor-stack{display:grid;gap:14px}.editor-row-card{padding:14px;border-radius:16px;border:1px solid color-mix(in srgb,var(--md-sys-color-outline, #c5c7ce) 54%,transparent);background:color-mix(in srgb,var(--md-sys-color-surface, #fff) 92%,rgba(18,99,180,.04))}.editor-row-actions{display:flex;justify-content:flex-end}.editor-field{width:100%}.editor-issues{display:grid;gap:10px;margin:0;padding:0;list-style:none}.editor-issue{padding:12px 14px;border-radius:14px;background:color-mix(in srgb,var(--md-sys-color-error, #b3261e) 8%,transparent);border:1px solid color-mix(in srgb,var(--md-sys-color-error, #b3261e) 18%,transparent)}.editor-issue strong{display:block;margin-bottom:4px}.editor-caption{margin:0 0 12px;color:var(--md-sys-color-on-surface-variant, #5a5d67);font-size:.92rem}.editor-empty{padding:18px;border-radius:14px;background:color-mix(in srgb,var(--md-sys-color-surface-variant, #eceff4) 78%,transparent)}@media(max-width:960px){.editor-layout{grid-template-columns:minmax(0,1fr)}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$3.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$3.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$3.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i1$1.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatCardModule }, { kind: "component", type: i3$1.MatCard, selector: "mat-card", inputs: ["appearance"], exportAs: ["matCard"] }, { kind: "directive", type: i3$1.MatCardContent, selector: "mat-card-content" }, { kind: "component", type: i3$1.MatCardHeader, selector: "mat-card-header" }, { kind: "directive", type: i3$1.MatCardTitle, selector: "mat-card-title, [mat-card-title], [matCardTitle]" }, { kind: "ngmodule", type: MatFormFieldModule }, { kind: "component", type: i4.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i4.MatLabel, selector: "mat-label" }, { kind: "ngmodule", type: MatInputModule }, { kind: "directive", type: i5.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly", "disabledInteractive"], exportAs: ["matInput"] }, { kind: "ngmodule", type: MatSelectModule }, { kind: "component", type: i6.MatSelect, selector: "mat-select", inputs: ["aria-describedby", "panelClass", "disabled", "disableRipple", "tabIndex", "hideSingleSelectionIndicator", "placeholder", "required", "multiple", "disableOptionCentering", "compareWith", "value", "aria-label", "aria-labelledby", "errorStateMatcher", "typeaheadDebounceInterval", "sortComparator", "id", "panelWidth", "canSelectNullableOptions"], outputs: ["openedChange", "opened", "closed", "selectionChange", "valueChange"], exportAs: ["matSelect"] }, { kind: "component", type: i6.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "ngmodule", type: MatSlideToggleModule }, { kind: "component", type: i7.MatSlideToggle, selector: "mat-slide-toggle", inputs: ["name", "id", "labelPosition", "aria-label", "aria-labelledby", "aria-describedby", "required", "color", "disabled", "disableRipple", "tabIndex", "checked", "hideIcon", "disabledInteractive"], outputs: ["change", "toggleChange"], exportAs: ["matSlideToggle"] }, { kind: "component", type: PraxisChartComponent, selector: "praxis-chart", inputs: ["config", "data", "chartDocument", "filterCriteria", "enableCustomization", "availableResources", "availableFields", "availableTargets"], outputs: ["pointClick", "queryRequest", "loadStateChange", "chartDocumentApplied", "chartDocumentSaved"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
4865
+ }
4866
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: PraxisChartConfigEditor, decorators: [{
4867
+ type: Component,
4868
+ args: [{ selector: 'praxis-chart-config-editor', standalone: true, imports: [
4869
+ CommonModule,
4870
+ FormsModule,
4871
+ MatButtonModule,
4872
+ MatCardModule,
4873
+ MatFormFieldModule,
4874
+ MatInputModule,
4875
+ MatSelectModule,
4876
+ MatSlideToggleModule,
4877
+ PraxisChartComponent,
4878
+ ], providers: [providePraxisChartsI18n()], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"editor-shell\">\n <div class=\"editor-nav\">\n @for (section of sections; track section.id) {\n <button\n mat-stroked-button\n type=\"button\"\n [class.active]=\"activeSection() === section.id\"\n (click)=\"setSection(section.id)\"\n >\n {{ t(section.labelKey, section.fallback) }}\n </button>\n }\n </div>\n\n <div class=\"editor-layout\">\n <div class=\"editor-form\">\n <mat-card class=\"editor-card\">\n <mat-card-content>\n @switch (activeSection()) {\n @case ('general') {\n <div class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.chartId', 'Chart ID') }}</mat-label>\n <input matInput [ngModel]=\"doc().chartId || ''\" (ngModelChange)=\"setChartId($event)\" [disabled]=\"isReadonly()\" />\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.kind', 'Kind') }}</mat-label>\n <mat-select [ngModel]=\"doc().kind\" (ngModelChange)=\"setKind($event)\" [disabled]=\"isReadonly()\">\n @for (kind of chartKinds; track kind) {\n <mat-option [value]=\"kind\">\n {{ t('praxis.charts.editor.kind.' + kind, kind) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.title', 'Title') }}</mat-label>\n <input matInput [ngModel]=\"titleValue()\" (ngModelChange)=\"setTitle($event)\" [disabled]=\"isReadonly()\" />\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.subtitle', 'Subtitle') }}</mat-label>\n <input matInput [ngModel]=\"subtitleValue()\" (ngModelChange)=\"setSubtitle($event)\" [disabled]=\"isReadonly()\" />\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.height', 'Height') }}</mat-label>\n <input matInput [ngModel]=\"heightValue()\" (ngModelChange)=\"setHeight($event)\" [disabled]=\"isReadonly()\" />\n </mat-form-field>\n </div>\n }\n\n @case ('data') {\n <div class=\"editor-stack\">\n <div class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.sourceKind', 'Source') }}</mat-label>\n <mat-select [ngModel]=\"doc().source.kind\" (ngModelChange)=\"setSourceKind($event)\" [disabled]=\"isReadonly()\">\n @for (sourceKind of sourceKinds; track sourceKind) {\n <mat-option [value]=\"sourceKind\">\n {{ t('praxis.charts.editor.sourceKind.' + (sourceKind === 'praxis.stats' ? 'praxisStats' : 'derived'), sourceKind) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n @if (doc().source.kind === 'praxis.stats') {\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.resource', 'Resource') }}</mat-label>\n @if (resourceOptions().length) {\n <mat-select [ngModel]=\"resourceValue()\" (ngModelChange)=\"setResource($event)\" [disabled]=\"isReadonly()\">\n @for (resource of resourceOptions(); track resource.id) {\n <mat-option [value]=\"resource.path\">{{ resource.label }}</mat-option>\n }\n </mat-select>\n } @else {\n <input matInput [ngModel]=\"resourceValue()\" (ngModelChange)=\"setResource($event)\" [disabled]=\"isReadonly()\" />\n }\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.operation', 'Operation') }}</mat-label>\n <mat-select\n [ngModel]=\"doc().source.operation || 'group-by'\"\n (ngModelChange)=\"setOperation($event)\"\n [disabled]=\"isReadonly()\"\n >\n @for (operation of operations; track operation) {\n <mat-option [value]=\"operation\">\n {{ t('praxis.charts.editor.operation.' + (operation === 'group-by' ? 'groupBy' : operation), operation) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n }\n </div>\n\n @if (showTimeseriesControls()) {\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.specialization.timeseriesTitle', 'Timeseries options') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.granularity', 'Granularity') }}</mat-label>\n <mat-select [ngModel]=\"granularityValue()\" (ngModelChange)=\"setGranularity($event)\" [disabled]=\"isReadonly()\">\n @for (granularity of timeGranularities; track granularity) {\n <mat-option [value]=\"granularity\">\n {{ t('praxis.charts.editor.granularity.' + granularity, granularity) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n <mat-slide-toggle\n [ngModel]=\"fillGapsValue()\"\n (ngModelChange)=\"setFillGaps($event)\"\n [disabled]=\"isReadonly()\"\n >\n {{ t('praxis.charts.editor.field.fillGaps', 'Fill missing intervals') }}\n </mat-slide-toggle>\n </mat-card-content>\n </mat-card>\n }\n\n @if (showDistributionControls()) {\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.specialization.distributionTitle', 'Distribution options') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.distributionMode', 'Distribution mode') }}</mat-label>\n <mat-select [ngModel]=\"distributionModeValue()\" (ngModelChange)=\"setDistributionMode($event)\" [disabled]=\"isReadonly()\">\n @for (mode of distributionModes; track mode) {\n <mat-option [value]=\"mode\">\n {{ t('praxis.charts.editor.distributionMode.' + mode, mode) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.bucketSize', 'Bucket size') }}</mat-label>\n <input matInput [ngModel]=\"bucketSizeValue()\" (ngModelChange)=\"setBucketSize($event)\" [disabled]=\"isReadonly()\" />\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.bucketCount', 'Bucket count') }}</mat-label>\n <input matInput [ngModel]=\"bucketCountValue()\" (ngModelChange)=\"setBucketCount($event)\" [disabled]=\"isReadonly()\" />\n </mat-form-field>\n </mat-card-content>\n </mat-card>\n }\n </div>\n }\n\n @case ('motion') {\n <div class=\"editor-grid\">\n <mat-slide-toggle\n [ngModel]=\"normalizedDocument().motion?.enabled !== false\"\n (ngModelChange)=\"setMotionEnabled($event)\"\n [disabled]=\"isReadonly()\"\n >\n {{ t('praxis.charts.editor.field.motionEnabled', 'Enable animations') }}\n </mat-slide-toggle>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.motionPreset', 'Motion preset') }}</mat-label>\n <mat-select\n [ngModel]=\"normalizedDocument().motion?.preset || 'standard'\"\n (ngModelChange)=\"setMotionPreset($event)\"\n [disabled]=\"isReadonly() || normalizedDocument().motion?.enabled === false\"\n >\n @for (preset of motionPresets; track preset) {\n <mat-option [value]=\"preset\">\n {{ t('praxis.charts.editor.motionPreset.' + preset, preset) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n </div>\n }\n\n @case ('appearance') {\n <div class=\"editor-stack\">\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.appearance.featuresTitle', 'Display features') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-grid\">\n <mat-slide-toggle\n [ngModel]=\"featureEnabled('legend')\"\n (ngModelChange)=\"setFeatureEnabled('legend', $event)\"\n [disabled]=\"isReadonly()\"\n >\n {{ t('praxis.charts.editor.field.legendEnabled', 'Show legend') }}\n </mat-slide-toggle>\n\n <mat-slide-toggle\n [ngModel]=\"featureEnabled('labels')\"\n (ngModelChange)=\"setFeatureEnabled('labels', $event)\"\n [disabled]=\"isReadonly()\"\n >\n {{ t('praxis.charts.editor.field.labelsEnabled', 'Show labels') }}\n </mat-slide-toggle>\n\n <mat-slide-toggle\n [ngModel]=\"featureEnabled('tooltip')\"\n (ngModelChange)=\"setFeatureEnabled('tooltip', $event)\"\n [disabled]=\"isReadonly()\"\n >\n {{ t('praxis.charts.editor.field.tooltipEnabled', 'Show tooltip') }}\n </mat-slide-toggle>\n </mat-card-content>\n </mat-card>\n\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.appearance.paletteTitle', 'Palette') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content>\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.palette', 'Palette colors') }}</mat-label>\n <textarea\n matInput\n rows=\"3\"\n [ngModel]=\"paletteValue()\"\n (ngModelChange)=\"setPalette($event)\"\n [disabled]=\"isReadonly()\"\n ></textarea>\n </mat-form-field>\n <p class=\"editor-caption\">\n {{ t('praxis.charts.editor.appearance.paletteHint', 'Use comma or line separated colors to persist theme.palette as a canonical array.') }}\n </p>\n </mat-card-content>\n </mat-card>\n\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.appearance.statesTitle', 'State messages') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-stack\">\n <div class=\"editor-row-card\">\n <div class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.emptyTitle', 'Empty title') }}</mat-label>\n <input matInput [ngModel]=\"stateTitle('empty')\" (ngModelChange)=\"setStateTitle('empty', $event)\" [disabled]=\"isReadonly()\" />\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.emptyDescription', 'Empty description') }}</mat-label>\n <textarea matInput rows=\"2\" [ngModel]=\"stateDescription('empty')\" (ngModelChange)=\"setStateDescription('empty', $event)\" [disabled]=\"isReadonly()\"></textarea>\n </mat-form-field>\n </div>\n </div>\n\n <div class=\"editor-row-card\">\n <div class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.loadingTitle', 'Loading title') }}</mat-label>\n <input matInput [ngModel]=\"stateTitle('loading')\" (ngModelChange)=\"setStateTitle('loading', $event)\" [disabled]=\"isReadonly()\" />\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.loadingDescription', 'Loading description') }}</mat-label>\n <textarea matInput rows=\"2\" [ngModel]=\"stateDescription('loading')\" (ngModelChange)=\"setStateDescription('loading', $event)\" [disabled]=\"isReadonly()\"></textarea>\n </mat-form-field>\n </div>\n </div>\n\n <div class=\"editor-row-card\">\n <div class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.errorTitle', 'Error title') }}</mat-label>\n <input matInput [ngModel]=\"stateTitle('error')\" (ngModelChange)=\"setStateTitle('error', $event)\" [disabled]=\"isReadonly()\" />\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.errorDescription', 'Error description') }}</mat-label>\n <textarea matInput rows=\"2\" [ngModel]=\"stateDescription('error')\" (ngModelChange)=\"setStateDescription('error', $event)\" [disabled]=\"isReadonly()\"></textarea>\n </mat-form-field>\n </div>\n </div>\n </mat-card-content>\n </mat-card>\n </div>\n }\n\n @case ('analytics') {\n <div class=\"editor-stack\">\n @if (showComboPanel()) {\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.specialization.comboTitle', 'Combo guidance') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content>\n <p class=\"editor-caption\">\n {{ t('praxis.charts.editor.specialization.comboHint', 'Combo charts require at least two metrics and allow per-metric axis and series kind mapping.') }}\n </p>\n </mat-card-content>\n </mat-card>\n }\n\n @if (showPieDonutPanel()) {\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.specialization.pieDonutTitle', 'Composition guidance') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content>\n <p class=\"editor-caption\">\n {{ t('praxis.charts.editor.specialization.pieDonutHint', 'Pie and donut charts keep only the first metric and use the first dimension as the category segment.') }}\n </p>\n </mat-card-content>\n </mat-card>\n }\n\n @if (showScatterPanel()) {\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.specialization.scatterTitle', 'Scatter guidance') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content>\n <p class=\"editor-caption\">\n {{ t('praxis.charts.editor.specialization.scatterHint', 'Scatter charts use the first dimension as X and the first metric as Y, so keep both fields mapped.') }}\n </p>\n </mat-card-content>\n </mat-card>\n }\n\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.analytics.dimensionsTitle', 'Dimensions') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-stack\">\n @for (dimension of dimensions(); track $index) {\n <div class=\"editor-row-card\">\n <div class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.dimension', 'Dimension') }}</mat-label>\n @if (fieldOptions('dimension').length) {\n <mat-select [ngModel]=\"dimension.field || ''\" (ngModelChange)=\"setDimensionField($index, $event)\" [disabled]=\"isReadonly()\">\n @for (field of fieldOptions('dimension'); track field.field) {\n <mat-option [value]=\"field.field\">{{ field.label || field.field }}</mat-option>\n }\n </mat-select>\n } @else {\n <input matInput [ngModel]=\"dimension.field || ''\" (ngModelChange)=\"setDimensionField($index, $event)\" [disabled]=\"isReadonly()\" />\n }\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.dimensionRole', 'Dimension role') }}</mat-label>\n <mat-select [ngModel]=\"dimension.role || 'category'\" (ngModelChange)=\"setDimensionRole($index, $event)\" [disabled]=\"isReadonly()\">\n @for (role of dimensionRoles; track role) {\n <mat-option [value]=\"role\">\n {{ t('praxis.charts.editor.dimensionRole.' + role, role) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n </div>\n\n <div class=\"editor-row-actions\">\n <button mat-button type=\"button\" (click)=\"removeDimension($index)\" [disabled]=\"isReadonly() || dimensions().length <= 1\">\n {{ t('praxis.charts.editor.analytics.removeDimension', 'Remove dimension') }}\n </button>\n </div>\n </div>\n }\n\n <div>\n <button mat-stroked-button type=\"button\" (click)=\"addDimension()\" [disabled]=\"isReadonly()\">\n {{ t('praxis.charts.editor.analytics.addDimension', 'Add dimension') }}\n </button>\n </div>\n </mat-card-content>\n </mat-card>\n\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.analytics.metricsTitle', 'Metrics') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-stack\">\n @for (metric of metrics(); track $index) {\n <div class=\"editor-row-card\">\n <div class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.metric', 'Metric') }}</mat-label>\n @if (fieldOptions('metric').length) {\n <mat-select [ngModel]=\"metric.field || ''\" (ngModelChange)=\"setMetricField($index, $event)\" [disabled]=\"isReadonly()\">\n @for (field of fieldOptions('metric'); track field.field) {\n <mat-option [value]=\"field.field\">{{ field.label || field.field }}</mat-option>\n }\n </mat-select>\n } @else {\n <input matInput [ngModel]=\"metric.field || ''\" (ngModelChange)=\"setMetricField($index, $event)\" [disabled]=\"isReadonly()\" />\n }\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.metricLabel', 'Metric label') }}</mat-label>\n <input matInput [ngModel]=\"metric.label || ''\" (ngModelChange)=\"setMetricLabel($index, $event)\" [disabled]=\"isReadonly()\" />\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.metricAggregation', 'Aggregation') }}</mat-label>\n <mat-select [ngModel]=\"metric.aggregation || 'sum'\" (ngModelChange)=\"setMetricAggregation($index, $event)\" [disabled]=\"isReadonly()\">\n @for (aggregation of metricAggregations; track aggregation) {\n <mat-option [value]=\"aggregation\">\n {{ t('praxis.charts.editor.metricAggregation.' + aggregation, aggregation) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n @if (showMetricAxisControls()) {\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.metricAxis', 'Axis') }}</mat-label>\n <mat-select [ngModel]=\"metric.axis || 'primary'\" (ngModelChange)=\"setMetricAxis($index, $event)\" [disabled]=\"isReadonly()\">\n @for (axis of metricAxes; track axis) {\n <mat-option [value]=\"axis\">\n {{ t('praxis.charts.editor.metricAxis.' + axis, axis) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n }\n\n @if (showMetricSeriesKindControls()) {\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.metricSeriesKind', 'Series kind') }}</mat-label>\n <mat-select [ngModel]=\"metric.seriesKind || 'bar'\" (ngModelChange)=\"setMetricSeriesKind($index, $event)\" [disabled]=\"isReadonly()\">\n @for (seriesKind of metricSeriesKinds; track seriesKind) {\n <mat-option [value]=\"seriesKind\">\n {{ t('praxis.charts.editor.metricSeriesKind.' + seriesKind, seriesKind) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n }\n </div>\n\n <div class=\"editor-row-actions\">\n <button mat-button type=\"button\" (click)=\"removeMetric($index)\" [disabled]=\"isReadonly() || metrics().length <= 1\">\n {{ t('praxis.charts.editor.analytics.removeMetric', 'Remove metric') }}\n </button>\n </div>\n </div>\n }\n\n <div>\n <button mat-stroked-button type=\"button\" (click)=\"addMetric()\" [disabled]=\"isReadonly()\">\n {{ t('praxis.charts.editor.analytics.addMetric', 'Add metric') }}\n </button>\n </div>\n </mat-card-content>\n </mat-card>\n </div>\n }\n\n @case ('events') {\n <div class=\"editor-stack\">\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.events.pointClickTitle', 'Point click') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventAction', 'Action') }}</mat-label>\n <mat-select [ngModel]=\"eventAction('pointClick')\" (ngModelChange)=\"setEventAction('pointClick', $event)\" [disabled]=\"isReadonly()\">\n <mat-option value=\"\">{{ t('praxis.charts.editor.events.none', 'None') }}</mat-option>\n @for (action of eventActionOptions; track action) {\n <mat-option [value]=\"action\">\n {{ t('praxis.charts.editor.eventAction.' + action, action) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventTarget', 'Target') }}</mat-label>\n @if (targetOptions().length) {\n <mat-select [ngModel]=\"eventTarget('pointClick')\" (ngModelChange)=\"setEventTarget('pointClick', $event)\" [disabled]=\"isReadonly() || !eventAction('pointClick')\">\n @for (target of targetOptions(); track target.id) {\n <mat-option [value]=\"target.id\">{{ target.label }}</mat-option>\n }\n </mat-select>\n } @else {\n <input matInput [ngModel]=\"eventTarget('pointClick')\" (ngModelChange)=\"setEventTarget('pointClick', $event)\" [disabled]=\"isReadonly() || !eventAction('pointClick')\" />\n }\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventMapping', 'Mapping') }}</mat-label>\n <textarea\n matInput\n rows=\"4\"\n [ngModel]=\"eventMappingText('pointClick')\"\n (ngModelChange)=\"setEventMapping('pointClick', $event)\"\n [disabled]=\"isReadonly() || !eventAction('pointClick')\"\n ></textarea>\n </mat-form-field>\n </mat-card-content>\n </mat-card>\n\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.events.drillDownTitle', 'Drill down') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventAction', 'Action') }}</mat-label>\n <mat-select [ngModel]=\"eventAction('drillDown')\" (ngModelChange)=\"setEventAction('drillDown', $event)\" [disabled]=\"isReadonly()\">\n <mat-option value=\"\">{{ t('praxis.charts.editor.events.none', 'None') }}</mat-option>\n @for (action of eventActionOptions; track action) {\n <mat-option [value]=\"action\">\n {{ t('praxis.charts.editor.eventAction.' + action, action) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventTarget', 'Target') }}</mat-label>\n @if (targetOptions().length) {\n <mat-select [ngModel]=\"eventTarget('drillDown')\" (ngModelChange)=\"setEventTarget('drillDown', $event)\" [disabled]=\"isReadonly() || !eventAction('drillDown')\">\n @for (target of targetOptions(); track target.id) {\n <mat-option [value]=\"target.id\">{{ target.label }}</mat-option>\n }\n </mat-select>\n } @else {\n <input matInput [ngModel]=\"eventTarget('drillDown')\" (ngModelChange)=\"setEventTarget('drillDown', $event)\" [disabled]=\"isReadonly() || !eventAction('drillDown')\" />\n }\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventMapping', 'Mapping') }}</mat-label>\n <textarea\n matInput\n rows=\"4\"\n [ngModel]=\"eventMappingText('drillDown')\"\n (ngModelChange)=\"setEventMapping('drillDown', $event)\"\n [disabled]=\"isReadonly() || !eventAction('drillDown')\"\n ></textarea>\n </mat-form-field>\n </mat-card-content>\n </mat-card>\n </div>\n }\n\n @case ('preview') {\n <p class=\"editor-caption\">\n {{ t('praxis.charts.editor.preview.caption', 'Local preview derived from the canonical contract without remote calls.') }}\n </p>\n }\n }\n </mat-card-content>\n </mat-card>\n </div>\n\n <div class=\"editor-side\">\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.issues.title', 'Validation issues') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content>\n @if (issues().length) {\n <ul class=\"editor-issues\">\n @for (issue of issues(); track issueTrackBy($index, issue)) {\n <li class=\"editor-issue\">\n <strong>{{ issue.field }}</strong>\n <span>{{ issue.message }}</span>\n </li>\n }\n </ul>\n } @else {\n <div class=\"editor-empty\">\n {{ t('praxis.charts.editor.issues.empty', 'No issues were identified.') }}\n </div>\n }\n </mat-card-content>\n </mat-card>\n\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.preview.title', 'Chart preview') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content>\n @if (preview(); as chartPreview) {\n <p class=\"editor-caption\">\n {{ t('praxis.charts.editor.preview.caption', 'Local preview derived from the canonical contract without remote calls.') }}\n </p>\n <praxis-chart [config]=\"chartPreview.config\" [data]=\"chartPreview.data\"></praxis-chart>\n } @else {\n <div class=\"editor-empty\">\n {{ t('praxis.charts.editor.preview.invalid', 'Preview is unavailable while the contract has blocking errors.') }}\n </div>\n }\n </mat-card-content>\n </mat-card>\n </div>\n </div>\n</div>\n", styles: [":host{display:block;min-width:0;color:var(--md-sys-color-on-surface, #1a1b20)}.editor-shell{display:grid;gap:18px}.editor-nav{display:flex;gap:8px;flex-wrap:wrap}.editor-nav button.active{background:color-mix(in srgb,var(--md-sys-color-primary, #1263b4) 18%,transparent);color:var(--md-sys-color-primary, #1263b4)}.editor-layout{display:grid;gap:18px;grid-template-columns:minmax(0,1.35fr) minmax(320px,.9fr);align-items:start}.editor-form,.editor-side{display:grid;gap:16px}.editor-card{border-radius:20px;border:1px solid color-mix(in srgb,var(--md-sys-color-outline, #c5c7ce) 72%,transparent);background:linear-gradient(180deg,#1263b408,#1263b400)}.editor-grid{display:grid;gap:14px;grid-template-columns:repeat(auto-fit,minmax(220px,1fr))}.editor-stack{display:grid;gap:14px}.editor-row-card{padding:14px;border-radius:16px;border:1px solid color-mix(in srgb,var(--md-sys-color-outline, #c5c7ce) 54%,transparent);background:color-mix(in srgb,var(--md-sys-color-surface, #fff) 92%,rgba(18,99,180,.04))}.editor-row-actions{display:flex;justify-content:flex-end}.editor-field{width:100%}.editor-issues{display:grid;gap:10px;margin:0;padding:0;list-style:none}.editor-issue{padding:12px 14px;border-radius:14px;background:color-mix(in srgb,var(--md-sys-color-error, #b3261e) 8%,transparent);border:1px solid color-mix(in srgb,var(--md-sys-color-error, #b3261e) 18%,transparent)}.editor-issue strong{display:block;margin-bottom:4px}.editor-caption{margin:0 0 12px;color:var(--md-sys-color-on-surface-variant, #5a5d67);font-size:.92rem}.editor-empty{padding:18px;border-radius:14px;background:color-mix(in srgb,var(--md-sys-color-surface-variant, #eceff4) 78%,transparent)}@media(max-width:960px){.editor-layout{grid-template-columns:minmax(0,1fr)}}\n"] }]
4879
+ }], ctorParameters: () => [], propDecorators: { documentInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "document", required: false }] }], modeInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "mode", required: false }] }], readonlyInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "readonly", required: false }] }], availableResourcesInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "availableResources", required: false }] }], availableFieldsInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "availableFields", required: false }] }], availableTargetsInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "availableTargets", required: false }] }], apply: [{ type: i0.Output, args: ["apply"] }], save: [{ type: i0.Output, args: ["save"] }], resetChange: [{ type: i0.Output, args: ["resetChange"] }], documentChange: [{ type: i0.Output, args: ["documentChange"] }] } });
4880
+
4881
+ var praxisChartConfigEditor = /*#__PURE__*/Object.freeze({
4882
+ __proto__: null,
4883
+ PraxisChartConfigEditor: PraxisChartConfigEditor
4884
+ });
4885
+
3359
4886
  /*
3360
4887
  * Public API Surface of praxis-charts
3361
4888
  */
@@ -3364,5 +4891,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
3364
4891
  * Generated bundle index. Do not edit.
3365
4892
  */
3366
4893
 
3367
- export { PRAXIS_CHART_BACKEND_MOCK_BAR, PRAXIS_CHART_BACKEND_MOCK_COMBO, PRAXIS_CHART_BACKEND_MOCK_DONUT, PRAXIS_CHART_BACKEND_MOCK_HORIZONTAL_BAR, PRAXIS_CHART_BACKEND_MOCK_MULTI_METRIC_BAR, PRAXIS_CHART_BACKEND_MOCK_SCATTER, PRAXIS_CHART_BACKEND_MOCK_STACKED_AREA, PRAXIS_CHART_BACKEND_MOCK_TIMESERIES, PRAXIS_CHART_COMPONENT_METADATA, PRAXIS_CHART_DRILLDOWN_DATA_BY_MONTH, PRAXIS_CHART_DRILLDOWN_PANEL_METADATA, PRAXIS_CHART_ENGINE, PRAXIS_CHART_STATE_PROBE_COMPONENT_METADATA, PraxisChartBackendPayloadAdapterService, PraxisChartCanonicalContractMapperService, PraxisChartComponent, PraxisChartCompositionShowcaseComponent, PraxisChartDataTransformerService, PraxisChartDrilldownPanelComponent, PraxisChartOptionBuilderService, PraxisChartSchemaMapperService, PraxisChartStateProbeComponent, PraxisChartStatsApiService, buildPraxisChartInteractiveGridPage, buildPraxisChartInteractiveWidgetPage, buildPraxisChartMockGridPage, buildPraxisChartMockWidgetPage, providePraxisChartDrilldownPanelMetadata, providePraxisChartStateProbeMetadata, providePraxisCharts, providePraxisChartsMetadata };
4894
+ export { ChartContractNormalizerService, ChartContractValidationService, ChartEditorDefaultsService, ChartEditorPreviewMapperService, PRAXIS_CHARTS_I18N, PRAXIS_CHART_BACKEND_MOCK_BAR, PRAXIS_CHART_BACKEND_MOCK_COMBO, PRAXIS_CHART_BACKEND_MOCK_DONUT, PRAXIS_CHART_BACKEND_MOCK_HORIZONTAL_BAR, PRAXIS_CHART_BACKEND_MOCK_MULTI_METRIC_BAR, PRAXIS_CHART_BACKEND_MOCK_SCATTER, PRAXIS_CHART_BACKEND_MOCK_STACKED_AREA, PRAXIS_CHART_BACKEND_MOCK_TIMESERIES, PRAXIS_CHART_COMPONENT_METADATA, PRAXIS_CHART_DRILLDOWN_DATA_BY_MONTH, PRAXIS_CHART_DRILLDOWN_PANEL_METADATA, PRAXIS_CHART_ENGINE, PRAXIS_CHART_STATE_PROBE_COMPONENT_METADATA, PraxisChartBackendPayloadAdapterService, PraxisChartCanonicalContractMapperService, PraxisChartComponent, PraxisChartCompositionShowcaseComponent, PraxisChartConfigEditor, PraxisChartDataTransformerService, PraxisChartDrilldownPanelComponent, PraxisChartOptionBuilderService, PraxisChartSchemaMapperService, PraxisChartStateProbeComponent, PraxisChartStatsApiService, buildPraxisChartInteractiveGridPage, buildPraxisChartInteractiveWidgetPage, buildPraxisChartMockGridPage, buildPraxisChartMockWidgetPage, createPraxisChartsI18nConfig, providePraxisChartDrilldownPanelMetadata, providePraxisChartStateProbeMetadata, providePraxisCharts, providePraxisChartsI18n, providePraxisChartsMetadata, resolvePraxisChartsText };
3368
4895
  //# sourceMappingURL=praxisui-charts.mjs.map