@praxisui/charts 9.0.25 → 9.0.26
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.
- package/README.md +29 -0
- package/ai/component-registry.json +418 -54
- package/fesm2022/praxisui-charts.mjs +1495 -1061
- package/package.json +3 -3
- package/types/praxisui-charts.d.ts +80 -9
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
2
|
import { Injectable, Optional, Inject, InjectionToken, input, booleanAttribute, output, viewChild, inject, ElementRef, NgZone, DestroyRef, signal, computed, afterNextRender, effect, ChangeDetectionStrategy, Component, ViewChild, Input, Injector, ENVIRONMENT_INITIALIZER } from '@angular/core';
|
|
3
3
|
import * as i1 from '@praxisui/core';
|
|
4
|
-
import { buildApiUrl, API_URL, PraxisI18nService, SETTINGS_PANEL_BRIDGE, normalizePraxisDataQueryContext, resolvePraxisFilterCriteria, BUILTIN_PAGE_THEME_PRESETS, PraxisIconButtonComponent, normalizePraxisPresentationVisualization, providePraxisI18n, ComponentMetadataRegistry, ResourceDiscoveryService, SETTINGS_PANEL_DATA, createDefaultTableConfig, PRAXIS_TABLE_DETAIL_INLINE_RENDERERS,
|
|
4
|
+
import { buildApiUrl, API_URL, PraxisI18nService, SETTINGS_PANEL_BRIDGE, normalizePraxisDataQueryContext, resolvePraxisFilterCriteria, BUILTIN_PAGE_THEME_PRESETS, PraxisIconButtonComponent, normalizePraxisPresentationVisualization, AnalyticsStatsRequestBuilderService, providePraxisI18n, AnalyticsPresentationResolver, ComponentMetadataRegistry, ResourceDiscoveryService, SETTINGS_PANEL_DATA, createDefaultTableConfig, PRAXIS_TABLE_DETAIL_INLINE_RENDERERS, DynamicWidgetPageComponent } from '@praxisui/core';
|
|
5
5
|
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
|
6
6
|
import { throwError, map, of, isObservable, from, firstValueFrom, timeout, BehaviorSubject, Subscription } from 'rxjs';
|
|
7
7
|
import * as i1$2 from '@angular/material/tooltip';
|
|
@@ -9,8 +9,9 @@ import { MatTooltipModule } from '@angular/material/tooltip';
|
|
|
9
9
|
import * as i1$1 from '@angular/common/http';
|
|
10
10
|
import { HttpErrorResponse } from '@angular/common/http';
|
|
11
11
|
import { catchError } from 'rxjs/operators';
|
|
12
|
+
import { AnalyticsTableConfigAdapterService, AnalyticsTableStatsApiService, PraxisTable } from '@praxisui/table';
|
|
12
13
|
import { use, init } from 'echarts/core';
|
|
13
|
-
import { BarChart, LineChart, PieChart, ScatterChart } from 'echarts/charts';
|
|
14
|
+
import { BarChart, FunnelChart, LineChart, PieChart, ScatterChart } from 'echarts/charts';
|
|
14
15
|
import { AriaComponent, DatasetComponent, GridComponent, LegendComponent, TitleComponent, TooltipComponent, TransformComponent } from 'echarts/components';
|
|
15
16
|
import { CanvasRenderer } from 'echarts/renderers';
|
|
16
17
|
import * as i1$3 from '@angular/forms';
|
|
@@ -27,7 +28,6 @@ import * as i6 from '@angular/material/select';
|
|
|
27
28
|
import { MatSelectModule } from '@angular/material/select';
|
|
28
29
|
import * as i7 from '@angular/material/slide-toggle';
|
|
29
30
|
import { MatSlideToggleModule } from '@angular/material/slide-toggle';
|
|
30
|
-
import { PraxisTable } from '@praxisui/table';
|
|
31
31
|
|
|
32
32
|
const PRAXIS_CHART_PALETTE_TOKENS = {
|
|
33
33
|
'brand-primary': ['#1263b4', '#0f766e', '#f08c00', '#c92a2a', '#7b61ff'],
|
|
@@ -79,7 +79,10 @@ const PRAXIS_CHART_THEME_VARIANTS = {
|
|
|
79
79
|
class PraxisChartDataTransformerService {
|
|
80
80
|
transform(config, rows) {
|
|
81
81
|
if (config.type === 'pie' || config.type === 'donut') {
|
|
82
|
-
return this.
|
|
82
|
+
return this.transformCategorySlices(config, rows, 'pie');
|
|
83
|
+
}
|
|
84
|
+
if (config.type === 'funnel' || config.type === 'pyramid') {
|
|
85
|
+
return this.transformCategorySlices(config, rows, 'funnel');
|
|
83
86
|
}
|
|
84
87
|
if (config.type === 'scatter') {
|
|
85
88
|
return this.transformScatter(config, rows);
|
|
@@ -109,12 +112,12 @@ class PraxisChartDataTransformerService {
|
|
|
109
112
|
hasData: categories.length > 0 && series.some((item) => item.points.some((point) => this.hasPointValue(point))),
|
|
110
113
|
};
|
|
111
114
|
}
|
|
112
|
-
|
|
115
|
+
transformCategorySlices(config, rows, mode) {
|
|
113
116
|
const seriesConfig = config.series[0];
|
|
114
117
|
const categoryField = seriesConfig?.categoryField ?? config.axes?.x?.field;
|
|
115
118
|
if (!seriesConfig || !categoryField || !rows.length) {
|
|
116
119
|
return {
|
|
117
|
-
mode
|
|
120
|
+
mode,
|
|
118
121
|
categories: [],
|
|
119
122
|
series: [],
|
|
120
123
|
slices: [],
|
|
@@ -122,18 +125,18 @@ class PraxisChartDataTransformerService {
|
|
|
122
125
|
};
|
|
123
126
|
}
|
|
124
127
|
const slices = this.buildCategoryBuckets(config, rows, categoryField)
|
|
125
|
-
.
|
|
128
|
+
.flatMap((bucket) => {
|
|
126
129
|
const value = bucket.rows.reduce((sum, row) => sum + this.extractMetricValue(row, seriesConfig), 0);
|
|
127
|
-
return {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
130
|
+
return value < 0 ? [] : [{
|
|
131
|
+
id: `${seriesConfig.id}:${bucket.identity}`,
|
|
132
|
+
name: bucket.label,
|
|
133
|
+
value,
|
|
134
|
+
color: seriesConfig.color,
|
|
135
|
+
data: this.buildPointData(bucket.rows[0], value),
|
|
136
|
+
}];
|
|
134
137
|
});
|
|
135
138
|
return {
|
|
136
|
-
mode
|
|
139
|
+
mode,
|
|
137
140
|
categories: slices.map((slice) => slice.name),
|
|
138
141
|
series: [],
|
|
139
142
|
slices,
|
|
@@ -491,7 +494,7 @@ class ChartContractNormalizerService {
|
|
|
491
494
|
})),
|
|
492
495
|
};
|
|
493
496
|
}
|
|
494
|
-
if ((
|
|
497
|
+
if ((this.isSingleMetricCategoryChart(document) || this.isDistribution(document)) && document.metrics?.length) {
|
|
495
498
|
nextDocument = {
|
|
496
499
|
...nextDocument,
|
|
497
500
|
metrics: document.metrics.slice(0, 1),
|
|
@@ -538,6 +541,12 @@ class ChartContractNormalizerService {
|
|
|
538
541
|
isDistribution(document) {
|
|
539
542
|
return document.source.kind === 'praxis.stats' && document.source.operation === 'distribution';
|
|
540
543
|
}
|
|
544
|
+
isSingleMetricCategoryChart(document) {
|
|
545
|
+
return document.kind === 'pie'
|
|
546
|
+
|| document.kind === 'donut'
|
|
547
|
+
|| document.kind === 'funnel'
|
|
548
|
+
|| document.kind === 'pyramid';
|
|
549
|
+
}
|
|
541
550
|
normalizeCssSizeValue(value) {
|
|
542
551
|
if (typeof value !== 'string') {
|
|
543
552
|
return value;
|
|
@@ -721,19 +730,20 @@ class ChartContractValidationService {
|
|
|
721
730
|
}
|
|
722
731
|
validateKinds(document, issues) {
|
|
723
732
|
const metricCount = document.metrics?.length ?? 0;
|
|
733
|
+
const categorySliceChart = this.isCategorySliceChart(document.kind);
|
|
724
734
|
document.dimensions?.forEach((dimension, index) => {
|
|
725
735
|
if (!dimension.field?.trim()) {
|
|
726
736
|
issues.push(this.error('dimension-field-required', `dimensions[${index}].field`, 'x-ui.chart dimension.field is required.'));
|
|
727
737
|
}
|
|
728
738
|
});
|
|
729
|
-
if (
|
|
739
|
+
if (!categorySliceChart && !document.dimensions?.length) {
|
|
730
740
|
issues.push(this.error('missing-dimension', 'dimensions', 'x-ui.chart cartesian charts require at least one dimension.'));
|
|
731
741
|
}
|
|
732
|
-
if (
|
|
733
|
-
issues.push(this.error('
|
|
742
|
+
if (categorySliceChart && !document.dimensions?.[0]?.field) {
|
|
743
|
+
issues.push(this.error('category-slice-missing-dimension', 'dimensions[0].field', 'x-ui.chart pie, donut, funnel and pyramid charts require a first dimension for category mapping.'));
|
|
734
744
|
}
|
|
735
|
-
if (
|
|
736
|
-
issues.push(this.error('
|
|
745
|
+
if (categorySliceChart && metricCount !== 1) {
|
|
746
|
+
issues.push(this.error('category-slice-single-metric', 'metrics', 'x-ui.chart pie, donut, funnel and pyramid charts require exactly one metric in @praxisui/charts.'));
|
|
737
747
|
}
|
|
738
748
|
if (document.kind === 'combo' && metricCount < 2) {
|
|
739
749
|
issues.push(this.error('combo-min-metrics', 'metrics', 'x-ui.chart combo charts require at least two metrics.'));
|
|
@@ -758,6 +768,9 @@ class ChartContractValidationService {
|
|
|
758
768
|
issues.push(this.error('combo-operation-unsupported', 'source.operation', 'x-ui.chart combo charts over praxis.stats support only group-by or timeseries operations in @praxisui/charts.'));
|
|
759
769
|
}
|
|
760
770
|
}
|
|
771
|
+
isCategorySliceChart(kind) {
|
|
772
|
+
return kind === 'pie' || kind === 'donut' || kind === 'funnel' || kind === 'pyramid';
|
|
773
|
+
}
|
|
761
774
|
validateDistribution(document, issues) {
|
|
762
775
|
if (document.source.kind !== 'praxis.stats'
|
|
763
776
|
|| document.source.operation !== 'distribution') {
|
|
@@ -928,7 +941,7 @@ class PraxisChartCanonicalContractMapperService {
|
|
|
928
941
|
const secondaryMetric = contract.metrics?.find((metric) => metric.axis === 'secondary');
|
|
929
942
|
const metricCount = (contract.metrics?.length ?? 0)
|
|
930
943
|
* (contract.source.kind === 'praxis.stats' && contract.source.operation === 'comparison' ? 2 : 1);
|
|
931
|
-
if (
|
|
944
|
+
if (this.isCategorySliceChart(contract.kind)) {
|
|
932
945
|
return {
|
|
933
946
|
x: {
|
|
934
947
|
field: firstDimension?.field,
|
|
@@ -984,7 +997,7 @@ class PraxisChartCanonicalContractMapperService {
|
|
|
984
997
|
name: period ? `${baseName} (${this.comparisonPeriodLabel(period)})` : baseName,
|
|
985
998
|
type: this.resolveSeriesType(contract.kind, metric.seriesKind, index),
|
|
986
999
|
axis: metric.axis ?? 'primary',
|
|
987
|
-
categoryField: comparison ||
|
|
1000
|
+
categoryField: comparison || this.isCategorySliceChart(contract.kind)
|
|
988
1001
|
? firstDimension?.field
|
|
989
1002
|
: undefined,
|
|
990
1003
|
metric: {
|
|
@@ -1078,7 +1091,7 @@ class PraxisChartCanonicalContractMapperService {
|
|
|
1078
1091
|
tooltip: {
|
|
1079
1092
|
...(variantTheme?.tooltip ?? {}),
|
|
1080
1093
|
enabled: this.resolveToggle(contract.tooltip, true),
|
|
1081
|
-
trigger:
|
|
1094
|
+
trigger: this.isCategorySliceChart(contract.kind) || contract.kind === 'scatter' ? 'item' : 'axis',
|
|
1082
1095
|
},
|
|
1083
1096
|
surface,
|
|
1084
1097
|
backgroundColor: surface?.background ?? variantTheme?.backgroundColor,
|
|
@@ -1148,6 +1161,9 @@ class PraxisChartCanonicalContractMapperService {
|
|
|
1148
1161
|
}
|
|
1149
1162
|
return contract.orientation;
|
|
1150
1163
|
}
|
|
1164
|
+
isCategorySliceChart(kind) {
|
|
1165
|
+
return kind === 'pie' || kind === 'donut' || kind === 'funnel' || kind === 'pyramid';
|
|
1166
|
+
}
|
|
1151
1167
|
resolveSeriesType(chartKind, seriesKind, index) {
|
|
1152
1168
|
if (chartKind === 'combo') {
|
|
1153
1169
|
return seriesKind ?? (index === 0 ? 'bar' : 'line');
|
|
@@ -1767,6 +1783,7 @@ class PraxisChartComponent {
|
|
|
1767
1783
|
surfaceBorderColor = computed(() => this.effectiveConfig().theme?.surface?.borderColor ?? null, ...(ngDevMode ? [{ debugName: "surfaceBorderColor" }] : /* istanbul ignore next */ []));
|
|
1768
1784
|
surfaceBorderWidth = computed(() => this.toCssSize(this.effectiveConfig().theme?.surface?.borderWidth) ?? null, ...(ngDevMode ? [{ debugName: "surfaceBorderWidth" }] : /* istanbul ignore next */ []));
|
|
1769
1785
|
surfaceBorderRadius = computed(() => this.toCssSize(this.effectiveConfig().theme?.surface?.borderRadius) ?? null, ...(ngDevMode ? [{ debugName: "surfaceBorderRadius" }] : /* istanbul ignore next */ []));
|
|
1786
|
+
surfaceTextColor = computed(() => this.effectiveConfig().theme?.textColor ?? null, ...(ngDevMode ? [{ debugName: "surfaceTextColor" }] : /* istanbul ignore next */ []));
|
|
1770
1787
|
renderConfig = computed(() => {
|
|
1771
1788
|
const config = this.effectiveConfig();
|
|
1772
1789
|
const explicitData = this.data();
|
|
@@ -2436,6 +2453,7 @@ class PraxisChartComponent {
|
|
|
2436
2453
|
[style.--praxis-chart-config-surface-border]="surfaceBorderColor()"
|
|
2437
2454
|
[style.--praxis-chart-config-surface-border-width]="surfaceBorderWidth()"
|
|
2438
2455
|
[style.--praxis-chart-config-surface-radius]="surfaceBorderRadius()"
|
|
2456
|
+
[style.--praxis-chart-config-text-color]="surfaceTextColor()"
|
|
2439
2457
|
>
|
|
2440
2458
|
@if (canOpenConfigEditor()) {
|
|
2441
2459
|
<button
|
|
@@ -2496,7 +2514,7 @@ class PraxisChartComponent {
|
|
|
2496
2514
|
<div #chartHost class="praxis-chart-host"></div>
|
|
2497
2515
|
}
|
|
2498
2516
|
</section>
|
|
2499
|
-
`, isInline: true, styles: [":host{display:block;min-height:var(--praxis-chart-runtime-height, 320px);min-width:0;color:var(--md-sys-color-on-surface, #1a1b20)}:host(.praxis-chart-host-fill-container){height:100%;min-height:0}:host-context(.pdx-shell.no-shell) .praxis-chart-shell,:host-context(.pdx-shell.body-fill) .praxis-chart-shell,: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:var(--praxis-chart-config-surface-radius, var(--praxis-chart-surface-radius, 8px));overflow:hidden;background:var( --praxis-chart-config-surface-bg, var(--praxis-chart-surface-bg, var(--md-sys-color-surface-container-lowest, #fff)) );border-color:var( --praxis-chart-config-surface-border, var( --praxis-chart-surface-border, color-mix(in srgb, var(--md-sys-color-outline, #c5c7ce) 44%, transparent) ) );border-style:solid;border-width:var(--praxis-chart-config-surface-border-width, 1px)}.praxis-chart-shell-fill-container{min-height:0}:host-context(.pdx-shell) .praxis-chart-shell:not(.praxis-chart-shell-contained),.praxis-chart-shell-embedded{border-width:var( --praxis-chart-config-surface-border-width, var(--praxis-chart-embedded-surface-border-width, 0) );border-radius:var( --praxis-chart-config-surface-radius, var(--praxis-chart-embedded-surface-radius, 0) );background:var( --praxis-chart-config-surface-bg, var(--praxis-chart-embedded-surface-bg, transparent) )}.praxis-chart-settings-trigger{position:absolute;top:10px;right:10px;z-index:3;--praxis-icon-button-filled-background: color-mix(in srgb, var(--md-sys-color-surface, #fff) 88%, rgba(18, 99, 180, .12));--praxis-icon-button-filled-foreground: var(--md-sys-color-on-surface-variant, #44474f);-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px)}.praxis-chart-host{width:100%;height:100%;min-height:inherit}.praxis-chart-state{height:100%;min-height:min(240px,100%);display:grid;align-content:center;justify-items:center;gap:14px;padding:24px;text-align:center}.praxis-chart-state-copy{display:grid;gap:6px;justify-items: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-loading-hero{width:min(100%,320px);display:grid;gap:14px}.praxis-chart-loading-summary{display:flex;gap:8px;justify-content:center}.praxis-chart-loading-chip,.praxis-chart-loading-bar{border-radius:999px;background:linear-gradient(90deg,#1263b414,#1263b438,#1263b414);background-size:200% 100%;animation:praxis-chart-loading-wave 1.2s ease-in-out infinite}.praxis-chart-loading-chip{display:block;width:104px;height:12px}.praxis-chart-loading-chip--short{width:64px}.praxis-chart-loading-plot{display:grid;grid-template-columns:repeat(5,minmax(0,1fr));align-items:end;gap:10px;height:108px;padding:12px 8px 4px;border-radius:8px;background:color-mix(in srgb,var(--md-sys-color-surface-container, #eef3f8) 72%,transparent);border:1px solid rgba(18,99,180,.08)}.praxis-chart-loading-bar{display:block;width:100%}.praxis-chart-loading-bar--1{height:32%}.praxis-chart-loading-bar--2{height:68%}.praxis-chart-loading-bar--3{height:48%}.praxis-chart-loading-bar--4{height:78%}.praxis-chart-loading-bar--5{height:56%}@keyframes praxis-chart-loading-wave{0%{background-position:100% 0}to{background-position:-100% 0}}\n"], dependencies: [{ kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i1$2.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "component", type: PraxisIconButtonComponent, selector: "button[praxisIconButton]", inputs: ["praxisIconButton", "size", "appearance", "presentation", "pressed", "busy"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
2517
|
+
`, isInline: true, styles: [":host{display:block;min-height:var(--praxis-chart-runtime-height, 320px);min-width:0;color:var(--md-sys-color-on-surface, #1a1b20)}:host(.praxis-chart-host-fill-container){height:100%;min-height:0}:host-context(.pdx-shell.no-shell) .praxis-chart-shell,:host-context(.pdx-shell.body-fill) .praxis-chart-shell,: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:var(--praxis-chart-config-surface-radius, var(--praxis-chart-surface-radius, 8px));overflow:hidden;background:var( --praxis-chart-config-surface-bg, var(--praxis-chart-surface-bg, var(--md-sys-color-surface-container-lowest, #fff)) );border-color:var( --praxis-chart-config-surface-border, var( --praxis-chart-surface-border, color-mix(in srgb, var(--md-sys-color-outline, #c5c7ce) 44%, transparent) ) );border-style:solid;border-width:var(--praxis-chart-config-surface-border-width, 1px)}.praxis-chart-shell-fill-container{min-height:0}:host-context(.pdx-shell) .praxis-chart-shell:not(.praxis-chart-shell-contained),.praxis-chart-shell-embedded{border-width:var( --praxis-chart-config-surface-border-width, var(--praxis-chart-embedded-surface-border-width, 0) );border-radius:var( --praxis-chart-config-surface-radius, var(--praxis-chart-embedded-surface-radius, 0) );background:var( --praxis-chart-config-surface-bg, var(--praxis-chart-embedded-surface-bg, transparent) )}.praxis-chart-settings-trigger{position:absolute;top:10px;right:10px;z-index:3;--praxis-icon-button-filled-background: color-mix(in srgb, var(--md-sys-color-surface, #fff) 88%, rgba(18, 99, 180, .12));--praxis-icon-button-filled-foreground: var(--md-sys-color-on-surface-variant, #44474f);-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px)}.praxis-chart-host{width:100%;height:100%;min-height:inherit}.praxis-chart-state{height:100%;min-height:min(240px,100%);display:grid;align-content:center;justify-items:center;gap:14px;padding:24px;text-align:center}.praxis-chart-state-copy{display:grid;gap:6px;justify-items:center}.praxis-chart-state-title{font-size:1rem;font-weight:600;color:var(--praxis-chart-config-text-color, var(--md-sys-color-on-surface, #1a1b20))}.praxis-chart-state-description{font-size:.925rem;color:color-mix(in srgb,var(--praxis-chart-config-text-color, var(--md-sys-color-on-surface-variant, #5a5d67)) 72%,transparent);max-width:36rem}.praxis-chart-loading-hero{width:min(100%,320px);display:grid;gap:14px}.praxis-chart-loading-summary{display:flex;gap:8px;justify-content:center}.praxis-chart-loading-chip,.praxis-chart-loading-bar{border-radius:999px;background:linear-gradient(90deg,#1263b414,#1263b438,#1263b414);background-size:200% 100%;animation:praxis-chart-loading-wave 1.2s ease-in-out infinite}.praxis-chart-loading-chip{display:block;width:104px;height:12px}.praxis-chart-loading-chip--short{width:64px}.praxis-chart-loading-plot{display:grid;grid-template-columns:repeat(5,minmax(0,1fr));align-items:end;gap:10px;height:108px;padding:12px 8px 4px;border-radius:8px;background:color-mix(in srgb,var(--md-sys-color-surface-container, #eef3f8) 72%,transparent);border:1px solid rgba(18,99,180,.08)}.praxis-chart-loading-bar{display:block;width:100%}.praxis-chart-loading-bar--1{height:32%}.praxis-chart-loading-bar--2{height:68%}.praxis-chart-loading-bar--3{height:48%}.praxis-chart-loading-bar--4{height:78%}.praxis-chart-loading-bar--5{height:56%}@keyframes praxis-chart-loading-wave{0%{background-position:100% 0}to{background-position:-100% 0}}\n"], dependencies: [{ kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i1$2.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "component", type: PraxisIconButtonComponent, selector: "button[praxisIconButton]", inputs: ["praxisIconButton", "size", "appearance", "presentation", "pressed", "busy"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
2500
2518
|
}
|
|
2501
2519
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: PraxisChartComponent, decorators: [{
|
|
2502
2520
|
type: Component,
|
|
@@ -2516,6 +2534,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
|
|
|
2516
2534
|
[style.--praxis-chart-config-surface-border]="surfaceBorderColor()"
|
|
2517
2535
|
[style.--praxis-chart-config-surface-border-width]="surfaceBorderWidth()"
|
|
2518
2536
|
[style.--praxis-chart-config-surface-radius]="surfaceBorderRadius()"
|
|
2537
|
+
[style.--praxis-chart-config-text-color]="surfaceTextColor()"
|
|
2519
2538
|
>
|
|
2520
2539
|
@if (canOpenConfigEditor()) {
|
|
2521
2540
|
<button
|
|
@@ -2576,7 +2595,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
|
|
|
2576
2595
|
<div #chartHost class="praxis-chart-host"></div>
|
|
2577
2596
|
}
|
|
2578
2597
|
</section>
|
|
2579
|
-
`, styles: [":host{display:block;min-height:var(--praxis-chart-runtime-height, 320px);min-width:0;color:var(--md-sys-color-on-surface, #1a1b20)}:host(.praxis-chart-host-fill-container){height:100%;min-height:0}:host-context(.pdx-shell.no-shell) .praxis-chart-shell,:host-context(.pdx-shell.body-fill) .praxis-chart-shell,: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:var(--praxis-chart-config-surface-radius, var(--praxis-chart-surface-radius, 8px));overflow:hidden;background:var( --praxis-chart-config-surface-bg, var(--praxis-chart-surface-bg, var(--md-sys-color-surface-container-lowest, #fff)) );border-color:var( --praxis-chart-config-surface-border, var( --praxis-chart-surface-border, color-mix(in srgb, var(--md-sys-color-outline, #c5c7ce) 44%, transparent) ) );border-style:solid;border-width:var(--praxis-chart-config-surface-border-width, 1px)}.praxis-chart-shell-fill-container{min-height:0}:host-context(.pdx-shell) .praxis-chart-shell:not(.praxis-chart-shell-contained),.praxis-chart-shell-embedded{border-width:var( --praxis-chart-config-surface-border-width, var(--praxis-chart-embedded-surface-border-width, 0) );border-radius:var( --praxis-chart-config-surface-radius, var(--praxis-chart-embedded-surface-radius, 0) );background:var( --praxis-chart-config-surface-bg, var(--praxis-chart-embedded-surface-bg, transparent) )}.praxis-chart-settings-trigger{position:absolute;top:10px;right:10px;z-index:3;--praxis-icon-button-filled-background: color-mix(in srgb, var(--md-sys-color-surface, #fff) 88%, rgba(18, 99, 180, .12));--praxis-icon-button-filled-foreground: var(--md-sys-color-on-surface-variant, #44474f);-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px)}.praxis-chart-host{width:100%;height:100%;min-height:inherit}.praxis-chart-state{height:100%;min-height:min(240px,100%);display:grid;align-content:center;justify-items:center;gap:14px;padding:24px;text-align:center}.praxis-chart-state-copy{display:grid;gap:6px;justify-items: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-loading-hero{width:min(100%,320px);display:grid;gap:14px}.praxis-chart-loading-summary{display:flex;gap:8px;justify-content:center}.praxis-chart-loading-chip,.praxis-chart-loading-bar{border-radius:999px;background:linear-gradient(90deg,#1263b414,#1263b438,#1263b414);background-size:200% 100%;animation:praxis-chart-loading-wave 1.2s ease-in-out infinite}.praxis-chart-loading-chip{display:block;width:104px;height:12px}.praxis-chart-loading-chip--short{width:64px}.praxis-chart-loading-plot{display:grid;grid-template-columns:repeat(5,minmax(0,1fr));align-items:end;gap:10px;height:108px;padding:12px 8px 4px;border-radius:8px;background:color-mix(in srgb,var(--md-sys-color-surface-container, #eef3f8) 72%,transparent);border:1px solid rgba(18,99,180,.08)}.praxis-chart-loading-bar{display:block;width:100%}.praxis-chart-loading-bar--1{height:32%}.praxis-chart-loading-bar--2{height:68%}.praxis-chart-loading-bar--3{height:48%}.praxis-chart-loading-bar--4{height:78%}.praxis-chart-loading-bar--5{height:56%}@keyframes praxis-chart-loading-wave{0%{background-position:100% 0}to{background-position:-100% 0}}\n"] }]
|
|
2598
|
+
`, styles: [":host{display:block;min-height:var(--praxis-chart-runtime-height, 320px);min-width:0;color:var(--md-sys-color-on-surface, #1a1b20)}:host(.praxis-chart-host-fill-container){height:100%;min-height:0}:host-context(.pdx-shell.no-shell) .praxis-chart-shell,:host-context(.pdx-shell.body-fill) .praxis-chart-shell,: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:var(--praxis-chart-config-surface-radius, var(--praxis-chart-surface-radius, 8px));overflow:hidden;background:var( --praxis-chart-config-surface-bg, var(--praxis-chart-surface-bg, var(--md-sys-color-surface-container-lowest, #fff)) );border-color:var( --praxis-chart-config-surface-border, var( --praxis-chart-surface-border, color-mix(in srgb, var(--md-sys-color-outline, #c5c7ce) 44%, transparent) ) );border-style:solid;border-width:var(--praxis-chart-config-surface-border-width, 1px)}.praxis-chart-shell-fill-container{min-height:0}:host-context(.pdx-shell) .praxis-chart-shell:not(.praxis-chart-shell-contained),.praxis-chart-shell-embedded{border-width:var( --praxis-chart-config-surface-border-width, var(--praxis-chart-embedded-surface-border-width, 0) );border-radius:var( --praxis-chart-config-surface-radius, var(--praxis-chart-embedded-surface-radius, 0) );background:var( --praxis-chart-config-surface-bg, var(--praxis-chart-embedded-surface-bg, transparent) )}.praxis-chart-settings-trigger{position:absolute;top:10px;right:10px;z-index:3;--praxis-icon-button-filled-background: color-mix(in srgb, var(--md-sys-color-surface, #fff) 88%, rgba(18, 99, 180, .12));--praxis-icon-button-filled-foreground: var(--md-sys-color-on-surface-variant, #44474f);-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px)}.praxis-chart-host{width:100%;height:100%;min-height:inherit}.praxis-chart-state{height:100%;min-height:min(240px,100%);display:grid;align-content:center;justify-items:center;gap:14px;padding:24px;text-align:center}.praxis-chart-state-copy{display:grid;gap:6px;justify-items:center}.praxis-chart-state-title{font-size:1rem;font-weight:600;color:var(--praxis-chart-config-text-color, var(--md-sys-color-on-surface, #1a1b20))}.praxis-chart-state-description{font-size:.925rem;color:color-mix(in srgb,var(--praxis-chart-config-text-color, var(--md-sys-color-on-surface-variant, #5a5d67)) 72%,transparent);max-width:36rem}.praxis-chart-loading-hero{width:min(100%,320px);display:grid;gap:14px}.praxis-chart-loading-summary{display:flex;gap:8px;justify-content:center}.praxis-chart-loading-chip,.praxis-chart-loading-bar{border-radius:999px;background:linear-gradient(90deg,#1263b414,#1263b438,#1263b414);background-size:200% 100%;animation:praxis-chart-loading-wave 1.2s ease-in-out infinite}.praxis-chart-loading-chip{display:block;width:104px;height:12px}.praxis-chart-loading-chip--short{width:64px}.praxis-chart-loading-plot{display:grid;grid-template-columns:repeat(5,minmax(0,1fr));align-items:end;gap:10px;height:108px;padding:12px 8px 4px;border-radius:8px;background:color-mix(in srgb,var(--md-sys-color-surface-container, #eef3f8) 72%,transparent);border:1px solid rgba(18,99,180,.08)}.praxis-chart-loading-bar{display:block;width:100%}.praxis-chart-loading-bar--1{height:32%}.praxis-chart-loading-bar--2{height:68%}.praxis-chart-loading-bar--3{height:48%}.praxis-chart-loading-bar--4{height:78%}.praxis-chart-loading-bar--5{height:56%}@keyframes praxis-chart-loading-wave{0%{background-position:100% 0}to{background-position:-100% 0}}\n"] }]
|
|
2580
2599
|
}], ctorParameters: () => [], propDecorators: { config: [{ type: i0.Input, args: [{ isSignal: true, alias: "config", required: false }] }], 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 }] }], queryContext: [{ type: i0.Input, args: [{ isSignal: true, alias: "queryContext", required: false }] }], remoteDataResolver: [{ type: i0.Input, args: [{ isSignal: true, alias: "remoteDataResolver", 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"] }], pointAction: [{ type: i0.Output, args: ["pointAction"] }], selectionChange: [{ type: i0.Output, args: ["selectionChange"] }], drillDown: [{ type: i0.Output, args: ["drillDown"] }], crossFilter: [{ type: i0.Output, args: ["crossFilter"] }], 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 }] }] } });
|
|
2581
2600
|
function mergeRemoteQueryContext(config, queryContext, runtimeFilters) {
|
|
2582
2601
|
const dataSource = config.dataSource;
|
|
@@ -3684,830 +3703,196 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
|
|
|
3684
3703
|
`, 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"] }]
|
|
3685
3704
|
}], propDecorators: { title: [{ type: i0.Input, args: [{ isSignal: true, alias: "title", required: false }] }], value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }] } });
|
|
3686
3705
|
|
|
3687
|
-
|
|
3688
|
-
|
|
3689
|
-
|
|
3690
|
-
|
|
3691
|
-
|
|
3692
|
-
const CARTESIAN_GRID_BOTTOM_WITH_LEGEND = 64;
|
|
3693
|
-
const CARTESIAN_GRID_BOTTOM_WITHOUT_LEGEND = 40;
|
|
3694
|
-
const DEFAULT_VALUE_LOCALE = 'pt-BR';
|
|
3695
|
-
class EChartsOptionBuilderService {
|
|
3696
|
-
transformer;
|
|
3697
|
-
constructor(transformer) {
|
|
3698
|
-
this.transformer = transformer;
|
|
3706
|
+
class AnalyticsChartConfigAdapterService {
|
|
3707
|
+
i18n;
|
|
3708
|
+
statsBuilder = new AnalyticsStatsRequestBuilderService();
|
|
3709
|
+
constructor(i18n) {
|
|
3710
|
+
this.i18n = i18n;
|
|
3699
3711
|
}
|
|
3700
|
-
|
|
3701
|
-
const
|
|
3702
|
-
const
|
|
3703
|
-
const
|
|
3704
|
-
const
|
|
3705
|
-
|
|
3706
|
-
|
|
3707
|
-
const hasChartTitle = this.hasText(config.title) || this.hasText(config.subtitle);
|
|
3708
|
-
const labelsVisible = config.series[0]?.labels?.visible ?? transformed.slices.length <= 4;
|
|
3709
|
-
const pieSeries = {
|
|
3710
|
-
type: 'pie',
|
|
3711
|
-
radius: config.type === 'donut'
|
|
3712
|
-
? labelsVisible ? ['32%', '52%'] : ['42%', '64%']
|
|
3713
|
-
: labelsVisible ? '54%' : '66%',
|
|
3714
|
-
center: ['50%', hasChartTitle ? '54%' : '50%'],
|
|
3715
|
-
avoidLabelOverlap: true,
|
|
3716
|
-
label: {
|
|
3717
|
-
show: labelsVisible,
|
|
3718
|
-
color: textColor,
|
|
3719
|
-
overflow: 'truncate',
|
|
3720
|
-
width: 120,
|
|
3721
|
-
formatter: labelsVisible
|
|
3722
|
-
? (params) => {
|
|
3723
|
-
const formatted = this.formatValue(params.value, config.series[0]?.labels?.format);
|
|
3724
|
-
return params.name ? `${params.name}: ${formatted}` : formatted;
|
|
3725
|
-
}
|
|
3726
|
-
: undefined,
|
|
3727
|
-
},
|
|
3728
|
-
labelLine: labelsVisible ? { length: 14, length2: 12, lineStyle: { color: textColor } } : undefined,
|
|
3729
|
-
data: transformed.slices.map((slice) => ({
|
|
3730
|
-
...(slice.data || {}),
|
|
3731
|
-
name: slice.name,
|
|
3732
|
-
value: slice.value,
|
|
3733
|
-
itemStyle: slice.color ? { color: slice.color } : undefined,
|
|
3734
|
-
})),
|
|
3735
|
-
};
|
|
3736
|
-
return {
|
|
3737
|
-
backgroundColor: config.theme?.backgroundColor,
|
|
3738
|
-
color: palette,
|
|
3739
|
-
title: this.buildTitle(config, 'center', textColor),
|
|
3740
|
-
tooltip: tooltipEnabled
|
|
3741
|
-
? {
|
|
3742
|
-
trigger: 'item',
|
|
3743
|
-
valueFormatter: (value) => this.formatValue(value, config.series[0]?.labels?.format),
|
|
3744
|
-
}
|
|
3745
|
-
: undefined,
|
|
3746
|
-
legend: this.buildLegend(config, legendVisible, 'bottom', textColor),
|
|
3747
|
-
series: [pieSeries],
|
|
3748
|
-
};
|
|
3712
|
+
toPraxisChartConfig(projection, options) {
|
|
3713
|
+
const chartType = this.resolveChartType(projection);
|
|
3714
|
+
const orientation = chartType === 'horizontal-bar' ? 'horizontal' : undefined;
|
|
3715
|
+
const dimension = projection.bindings.primaryDimension;
|
|
3716
|
+
const metrics = projection.bindings.primaryMetrics ?? [];
|
|
3717
|
+
if (!dimension?.field) {
|
|
3718
|
+
throw new Error(`AnalyticsChartConfigAdapterService requires primaryDimension for projection "${projection.id}".`);
|
|
3749
3719
|
}
|
|
3750
|
-
if (
|
|
3751
|
-
|
|
3752
|
-
|
|
3753
|
-
|
|
3754
|
-
|
|
3755
|
-
|
|
3756
|
-
|
|
3757
|
-
|
|
3758
|
-
|
|
3759
|
-
|
|
3760
|
-
|
|
3761
|
-
|
|
3762
|
-
|
|
3763
|
-
|
|
3764
|
-
|
|
3765
|
-
|
|
3766
|
-
|
|
3767
|
-
|
|
3768
|
-
|
|
3769
|
-
|
|
3720
|
+
if (!metrics.length) {
|
|
3721
|
+
throw new Error(`AnalyticsChartConfigAdapterService requires at least one metric for projection "${projection.id}".`);
|
|
3722
|
+
}
|
|
3723
|
+
const crossFilter = Boolean(projection.interactions?.crossFilter);
|
|
3724
|
+
const keyFilterField = dimension.keyFilterField?.trim();
|
|
3725
|
+
if (crossFilter && !keyFilterField) {
|
|
3726
|
+
throw new Error(`AnalyticsChartConfigAdapterService requires primaryDimension.keyFilterField when crossFilter is enabled for projection "${projection.id}".`);
|
|
3727
|
+
}
|
|
3728
|
+
return {
|
|
3729
|
+
id: projection.id,
|
|
3730
|
+
type: chartType,
|
|
3731
|
+
orientation,
|
|
3732
|
+
title: options?.title,
|
|
3733
|
+
subtitle: options?.subtitle,
|
|
3734
|
+
height: options?.height,
|
|
3735
|
+
axes: this.buildAxes(projection, chartType),
|
|
3736
|
+
series: this.buildSeries(projection, chartType),
|
|
3737
|
+
dataSource: this.buildDataSource(projection),
|
|
3738
|
+
interactions: {
|
|
3739
|
+
pointClick: Boolean(projection.interactions?.pointSelection || projection.interactions?.drillDown),
|
|
3740
|
+
drillDown: Boolean(projection.interactions?.drillDown),
|
|
3741
|
+
selection: Boolean(projection.interactions?.pointSelection),
|
|
3742
|
+
crossFilter,
|
|
3743
|
+
...(crossFilter
|
|
3770
3744
|
? {
|
|
3771
|
-
|
|
3772
|
-
|
|
3745
|
+
eventActions: {
|
|
3746
|
+
crossFilter: {
|
|
3747
|
+
action: 'emit',
|
|
3748
|
+
mapping: { key: keyFilterField },
|
|
3749
|
+
},
|
|
3750
|
+
},
|
|
3773
3751
|
}
|
|
3774
|
-
:
|
|
3775
|
-
|
|
3776
|
-
|
|
3777
|
-
|
|
3778
|
-
|
|
3779
|
-
|
|
3780
|
-
|
|
3781
|
-
|
|
3782
|
-
|
|
3783
|
-
|
|
3784
|
-
|
|
3785
|
-
|
|
3786
|
-
|
|
3787
|
-
formatter: this.axisLabelFormatter(config.axes?.x?.labels?.format),
|
|
3788
|
-
},
|
|
3789
|
-
},
|
|
3790
|
-
yAxis: {
|
|
3791
|
-
type: config.axes?.y?.type ?? 'value',
|
|
3792
|
-
name: config.axes?.y?.label,
|
|
3793
|
-
nameLocation: 'middle',
|
|
3794
|
-
nameGap: 40,
|
|
3795
|
-
min: config.axes?.y?.min,
|
|
3796
|
-
max: config.axes?.y?.max,
|
|
3797
|
-
axisLabel: {
|
|
3798
|
-
show: config.axes?.y?.labels?.visible ?? true,
|
|
3799
|
-
color: textColor,
|
|
3800
|
-
hideOverlap: true,
|
|
3801
|
-
formatter: this.axisLabelFormatter(config.axes?.y?.labels?.format),
|
|
3802
|
-
},
|
|
3752
|
+
: {}),
|
|
3753
|
+
},
|
|
3754
|
+
};
|
|
3755
|
+
}
|
|
3756
|
+
buildAxes(projection, chartType) {
|
|
3757
|
+
const dimension = projection.bindings.primaryDimension;
|
|
3758
|
+
const metrics = this.getDisplayMetrics(projection);
|
|
3759
|
+
const firstMetric = metrics[0];
|
|
3760
|
+
if (this.isCategorySliceChart(chartType)) {
|
|
3761
|
+
return {
|
|
3762
|
+
x: {
|
|
3763
|
+
field: dimension.field,
|
|
3764
|
+
label: dimension.label ?? undefined,
|
|
3803
3765
|
},
|
|
3804
|
-
series: scatterSeries,
|
|
3805
3766
|
};
|
|
3806
3767
|
}
|
|
3807
|
-
const horizontal = config.orientation === 'horizontal' || config.type === 'horizontal-bar';
|
|
3808
|
-
const xAxisType = this.usesCanonicalStatsTime(config)
|
|
3809
|
-
? 'time'
|
|
3810
|
-
: config.axes?.x?.type ?? 'category';
|
|
3811
3768
|
return {
|
|
3812
|
-
|
|
3813
|
-
|
|
3814
|
-
|
|
3815
|
-
|
|
3816
|
-
|
|
3817
|
-
|
|
3818
|
-
|
|
3819
|
-
|
|
3820
|
-
|
|
3821
|
-
|
|
3822
|
-
|
|
3823
|
-
|
|
3824
|
-
|
|
3825
|
-
|
|
3826
|
-
|
|
3827
|
-
|
|
3828
|
-
|
|
3829
|
-
|
|
3830
|
-
|
|
3831
|
-
|
|
3832
|
-
|
|
3833
|
-
:
|
|
3834
|
-
|
|
3835
|
-
|
|
3836
|
-
|
|
3837
|
-
|
|
3838
|
-
|
|
3839
|
-
|
|
3840
|
-
|
|
3841
|
-
|
|
3842
|
-
|
|
3843
|
-
|
|
3844
|
-
|
|
3769
|
+
x: {
|
|
3770
|
+
field: dimension.field,
|
|
3771
|
+
label: dimension.label ?? undefined,
|
|
3772
|
+
type: dimension.role === 'time' ? 'time' : 'category',
|
|
3773
|
+
},
|
|
3774
|
+
y: {
|
|
3775
|
+
label: metrics.length === 1
|
|
3776
|
+
? firstMetric?.label ?? firstMetric?.field
|
|
3777
|
+
: undefined,
|
|
3778
|
+
type: 'value',
|
|
3779
|
+
},
|
|
3780
|
+
};
|
|
3781
|
+
}
|
|
3782
|
+
buildSeries(projection, chartType) {
|
|
3783
|
+
const dimension = projection.bindings.primaryDimension;
|
|
3784
|
+
const comparison = projection.source.operation === 'comparison';
|
|
3785
|
+
return this.getDisplayMetrics(projection).flatMap((metric, index) => comparison
|
|
3786
|
+
? ['current', 'previous'].map((period) => ({
|
|
3787
|
+
id: `${projection.id}.${metric.field}.${period}`,
|
|
3788
|
+
name: `${metric.label ?? metric.field} (${this.comparisonPeriodLabel(period)})`,
|
|
3789
|
+
type: chartType,
|
|
3790
|
+
categoryField: dimension.field,
|
|
3791
|
+
metric: { field: this.comparisonMetricField(metric.field, period), aggregation: this.mapAggregation(metric.aggregation) },
|
|
3792
|
+
}))
|
|
3793
|
+
: [{
|
|
3794
|
+
id: `${projection.id}.${metric.field}.${index + 1}`,
|
|
3795
|
+
name: metric.label ?? metric.field,
|
|
3796
|
+
type: chartType,
|
|
3797
|
+
categoryField: this.isCategorySliceChart(chartType) ? dimension.field : undefined,
|
|
3798
|
+
metric: {
|
|
3799
|
+
field: metric.field,
|
|
3800
|
+
aggregation: this.mapAggregation(metric.aggregation),
|
|
3801
|
+
label: metric.label ?? undefined,
|
|
3845
3802
|
},
|
|
3846
|
-
|
|
3847
|
-
|
|
3848
|
-
? {
|
|
3849
|
-
type: config.axes?.x?.type ?? 'category',
|
|
3850
|
-
name: config.axes?.x?.label,
|
|
3851
|
-
data: transformed.categories,
|
|
3852
|
-
axisLabel: {
|
|
3853
|
-
show: config.axes?.x?.labels?.visible ?? true,
|
|
3854
|
-
color: textColor,
|
|
3855
|
-
rotate: config.axes?.x?.labels?.rotate ?? 0,
|
|
3856
|
-
hideOverlap: true,
|
|
3857
|
-
overflow: 'truncate',
|
|
3858
|
-
width: 160,
|
|
3859
|
-
formatter: this.axisLabelFormatter(config.axes?.x?.labels?.format),
|
|
3860
|
-
},
|
|
3861
|
-
}
|
|
3862
|
-
: this.buildCartesianYAxis(config),
|
|
3863
|
-
series: transformed.series.map((series) => ({
|
|
3864
|
-
id: series.id,
|
|
3865
|
-
name: series.name,
|
|
3866
|
-
type: series.type,
|
|
3867
|
-
yAxisIndex: series.axis === 'secondary' ? 1 : 0,
|
|
3868
|
-
stack: series.stack,
|
|
3869
|
-
smooth: series.smooth,
|
|
3870
|
-
areaStyle: series.area ? {} : undefined,
|
|
3871
|
-
label: {
|
|
3872
|
-
show: series.labelsVisible,
|
|
3873
|
-
color: textColor,
|
|
3874
|
-
formatter: series.labelsVisible
|
|
3875
|
-
? (params) => this.formatValue(params.value, this.seriesLabelFormat(config, series.id))
|
|
3876
|
-
: undefined,
|
|
3877
|
-
},
|
|
3878
|
-
itemStyle: series.color ? { color: series.color } : undefined,
|
|
3879
|
-
data: series.points,
|
|
3880
|
-
})),
|
|
3881
|
-
};
|
|
3882
|
-
}
|
|
3883
|
-
resolveText(value) {
|
|
3884
|
-
if (!value)
|
|
3885
|
-
return undefined;
|
|
3886
|
-
if (typeof value === 'string')
|
|
3887
|
-
return value;
|
|
3888
|
-
if (typeof value === 'object' && value && 'text' in value) {
|
|
3889
|
-
const text = value.text;
|
|
3890
|
-
return text || undefined;
|
|
3891
|
-
}
|
|
3892
|
-
if (typeof value === 'object' && value && 'fallback' in value) {
|
|
3893
|
-
const fallback = value.fallback;
|
|
3894
|
-
return fallback || undefined;
|
|
3895
|
-
}
|
|
3896
|
-
return undefined;
|
|
3897
|
-
}
|
|
3898
|
-
resolveTextColor(config) {
|
|
3899
|
-
return config.theme?.textColor ?? '#4b5563';
|
|
3900
|
-
}
|
|
3901
|
-
hasText(value) {
|
|
3902
|
-
const text = this.resolveText(value);
|
|
3903
|
-
return !!text && text.trim().length > 0;
|
|
3803
|
+
smooth: chartType === 'line' || chartType === 'area',
|
|
3804
|
+
}]);
|
|
3904
3805
|
}
|
|
3905
|
-
|
|
3906
|
-
const
|
|
3907
|
-
const subtitle = this.resolveText(config.subtitle);
|
|
3908
|
-
const hasTitle = !!title || !!subtitle;
|
|
3806
|
+
buildDataSource(projection) {
|
|
3807
|
+
const executionPlan = this.statsBuilder.buildExecutionPlan(projection);
|
|
3909
3808
|
return {
|
|
3910
|
-
|
|
3911
|
-
|
|
3912
|
-
|
|
3913
|
-
|
|
3914
|
-
|
|
3915
|
-
|
|
3916
|
-
|
|
3917
|
-
|
|
3918
|
-
|
|
3919
|
-
|
|
3920
|
-
|
|
3921
|
-
|
|
3922
|
-
|
|
3923
|
-
|
|
3924
|
-
|
|
3925
|
-
|
|
3926
|
-
|
|
3927
|
-
|
|
3809
|
+
kind: 'remote',
|
|
3810
|
+
resourcePath: executionPlan.resourcePath,
|
|
3811
|
+
schemaId: executionPlan.resourcePath,
|
|
3812
|
+
query: {
|
|
3813
|
+
sourceKind: 'praxis.stats',
|
|
3814
|
+
statsOperation: executionPlan.operation,
|
|
3815
|
+
statsPath: executionPlan.statsPath,
|
|
3816
|
+
statsRequest: executionPlan.statsRequest,
|
|
3817
|
+
dimensions: executionPlan.dimensions,
|
|
3818
|
+
metrics: projection.source.operation === 'comparison'
|
|
3819
|
+
? this.getDisplayMetrics(projection).flatMap((metric) => ['current', 'previous'].map((period) => ({
|
|
3820
|
+
field: this.comparisonMetricField(metric.field, period),
|
|
3821
|
+
aggregation: this.mapAggregation(metric.aggregation),
|
|
3822
|
+
alias: this.comparisonMetricField(metric.field, period),
|
|
3823
|
+
})))
|
|
3824
|
+
: this.mapExecutionMetrics(executionPlan),
|
|
3825
|
+
sort: executionPlan.sort,
|
|
3826
|
+
limit: executionPlan.limit,
|
|
3928
3827
|
},
|
|
3929
3828
|
};
|
|
3930
3829
|
}
|
|
3931
|
-
|
|
3932
|
-
if (
|
|
3933
|
-
return
|
|
3830
|
+
resolveChartType(projection) {
|
|
3831
|
+
if (projection.intent === 'trend' && projection.source.operation === 'timeseries') {
|
|
3832
|
+
return 'line';
|
|
3934
3833
|
}
|
|
3935
|
-
|
|
3936
|
-
|
|
3937
|
-
show: true,
|
|
3938
|
-
type: 'scroll',
|
|
3939
|
-
pageIconSize: 10,
|
|
3940
|
-
itemWidth: 18,
|
|
3941
|
-
itemHeight: 10,
|
|
3942
|
-
textStyle: {
|
|
3943
|
-
color: textColor,
|
|
3944
|
-
},
|
|
3945
|
-
formatter: (name) => truncateLegendText(name),
|
|
3946
|
-
};
|
|
3947
|
-
if (position === 'top') {
|
|
3948
|
-
return {
|
|
3949
|
-
...base,
|
|
3950
|
-
orient: 'horizontal',
|
|
3951
|
-
top: this.hasText(config.title) || this.hasText(config.subtitle) ? 70 : 12,
|
|
3952
|
-
left: 'center',
|
|
3953
|
-
right: TITLE_LEFT,
|
|
3954
|
-
};
|
|
3834
|
+
if (projection.intent === 'ranking' && projection.source.operation === 'group-by') {
|
|
3835
|
+
return 'horizontal-bar';
|
|
3955
3836
|
}
|
|
3956
|
-
if (
|
|
3957
|
-
return
|
|
3958
|
-
...base,
|
|
3959
|
-
orient: 'vertical',
|
|
3960
|
-
top: 88,
|
|
3961
|
-
bottom: 28,
|
|
3962
|
-
[position]: 10,
|
|
3963
|
-
};
|
|
3837
|
+
if (projection.intent === 'composition') {
|
|
3838
|
+
return 'pie';
|
|
3964
3839
|
}
|
|
3965
|
-
|
|
3966
|
-
|
|
3967
|
-
|
|
3968
|
-
|
|
3969
|
-
left: TITLE_LEFT,
|
|
3970
|
-
right: TITLE_LEFT,
|
|
3971
|
-
};
|
|
3840
|
+
if (projection.intent === 'distribution') {
|
|
3841
|
+
return 'bar';
|
|
3842
|
+
}
|
|
3843
|
+
return 'bar';
|
|
3972
3844
|
}
|
|
3973
|
-
|
|
3974
|
-
|
|
3975
|
-
const legendPosition = config.theme?.legend?.position ?? 'bottom';
|
|
3976
|
-
const sideLegend = options.legendVisible && (legendPosition === 'left' || legendPosition === 'right');
|
|
3977
|
-
return {
|
|
3978
|
-
top: hasChartTitle ? CARTESIAN_GRID_TOP_WITH_TITLE : CARTESIAN_GRID_TOP_WITHOUT_TITLE,
|
|
3979
|
-
right: sideLegend && legendPosition === 'right' ? 160 : 32,
|
|
3980
|
-
bottom: options.legendVisible && legendPosition === 'bottom'
|
|
3981
|
-
? CARTESIAN_GRID_BOTTOM_WITH_LEGEND
|
|
3982
|
-
: CARTESIAN_GRID_BOTTOM_WITHOUT_LEGEND,
|
|
3983
|
-
left: sideLegend && legendPosition === 'left'
|
|
3984
|
-
? 168
|
|
3985
|
-
: options.horizontal ? 136 : 56,
|
|
3986
|
-
// ECharts deprecou containLabel; esta combinacao preserva o mesmo comportamento
|
|
3987
|
-
// sem depender da feature legacy de grid.
|
|
3988
|
-
outerBoundsMode: 'same',
|
|
3989
|
-
outerBoundsContain: 'axisLabel',
|
|
3990
|
-
};
|
|
3845
|
+
isCategorySliceChart(chartType) {
|
|
3846
|
+
return chartType === 'pie' || chartType === 'donut' || chartType === 'funnel' || chartType === 'pyramid';
|
|
3991
3847
|
}
|
|
3992
|
-
|
|
3993
|
-
const
|
|
3994
|
-
|
|
3995
|
-
|
|
3996
|
-
|
|
3997
|
-
|
|
3998
|
-
|
|
3999
|
-
|
|
4000
|
-
|
|
4001
|
-
|
|
4002
|
-
|
|
4003
|
-
|
|
4004
|
-
|
|
4005
|
-
|
|
4006
|
-
},
|
|
4007
|
-
};
|
|
4008
|
-
if (!config.axes?.ySecondary) {
|
|
4009
|
-
return primaryAxis;
|
|
3848
|
+
mapAggregation(aggregation) {
|
|
3849
|
+
const normalizedAggregation = (aggregation ?? '').toLowerCase();
|
|
3850
|
+
switch (normalizedAggregation) {
|
|
3851
|
+
case 'avg':
|
|
3852
|
+
case 'min':
|
|
3853
|
+
case 'max':
|
|
3854
|
+
case 'count':
|
|
3855
|
+
case 'distinct-count':
|
|
3856
|
+
case 'sum':
|
|
3857
|
+
return normalizedAggregation;
|
|
3858
|
+
case '':
|
|
3859
|
+
return undefined;
|
|
3860
|
+
default:
|
|
3861
|
+
throw new Error(`Analytics aggregation "${aggregation}" is not supported in @praxisui/charts.`);
|
|
4010
3862
|
}
|
|
3863
|
+
}
|
|
3864
|
+
mapExecutionMetrics(executionPlan) {
|
|
3865
|
+
return executionPlan.metrics.map((metric) => ({
|
|
3866
|
+
field: metric.field,
|
|
3867
|
+
aggregation: this.mapAggregation(metric.aggregation),
|
|
3868
|
+
alias: metric.alias,
|
|
3869
|
+
}));
|
|
3870
|
+
}
|
|
3871
|
+
getDisplayMetrics(projection) {
|
|
4011
3872
|
return [
|
|
4012
|
-
|
|
4013
|
-
|
|
4014
|
-
type: config.axes.ySecondary.type ?? 'value',
|
|
4015
|
-
name: config.axes.ySecondary.label,
|
|
4016
|
-
nameLocation: 'middle',
|
|
4017
|
-
nameGap: 40,
|
|
4018
|
-
min: config.axes.ySecondary.min,
|
|
4019
|
-
max: config.axes.ySecondary.max,
|
|
4020
|
-
position: config.axes.ySecondary.position ?? 'right',
|
|
4021
|
-
axisLabel: {
|
|
4022
|
-
show: config.axes.ySecondary.labels?.visible ?? true,
|
|
4023
|
-
color: textColor,
|
|
4024
|
-
hideOverlap: true,
|
|
4025
|
-
formatter: this.axisLabelFormatter(config.axes.ySecondary.labels?.format),
|
|
4026
|
-
},
|
|
4027
|
-
},
|
|
3873
|
+
...(projection.bindings.primaryMetrics ?? []),
|
|
3874
|
+
...(projection.bindings.secondaryMetrics ?? []),
|
|
4028
3875
|
];
|
|
4029
3876
|
}
|
|
4030
|
-
|
|
4031
|
-
return
|
|
4032
|
-
?? config.axes?.y?.labels?.format
|
|
4033
|
-
?? config.series[0]?.labels?.format;
|
|
4034
|
-
}
|
|
4035
|
-
buildCartesianTooltip(config) {
|
|
4036
|
-
const tooltip = {
|
|
4037
|
-
trigger: config.theme?.tooltip?.trigger ?? 'axis',
|
|
4038
|
-
confine: true,
|
|
4039
|
-
appendToBody: false,
|
|
4040
|
-
valueFormatter: (value) => this.formatValue(value, this.primaryValueFormat(config)),
|
|
4041
|
-
};
|
|
4042
|
-
return this.usesCanonicalStatsTime(config)
|
|
4043
|
-
? {
|
|
4044
|
-
...tooltip,
|
|
4045
|
-
formatter: (params) => this.formatCanonicalTimeTooltip(params, config),
|
|
4046
|
-
}
|
|
4047
|
-
: tooltip;
|
|
3877
|
+
comparisonMetricField(metricField, period) {
|
|
3878
|
+
return `__praxisComparison_${metricField}_${period}`;
|
|
4048
3879
|
}
|
|
4049
|
-
|
|
4050
|
-
|
|
4051
|
-
|
|
4052
|
-
|
|
4053
|
-
|
|
4054
|
-
return
|
|
4055
|
-
|| config.dataSource.query.statsOperation === 'timeseries';
|
|
3880
|
+
comparisonPeriodLabel(period) {
|
|
3881
|
+
const key = period === 'current'
|
|
3882
|
+
? 'praxis.charts.runtime.comparisonCurrent'
|
|
3883
|
+
: 'praxis.charts.runtime.comparisonPrevious';
|
|
3884
|
+
const fallback = period === 'current' ? 'Current' : 'Previous';
|
|
3885
|
+
return this.i18n?.t(key, undefined, fallback, 'charts') ?? fallback;
|
|
4056
3886
|
}
|
|
4057
|
-
|
|
4058
|
-
|
|
4059
|
-
.filter((item) => !!item && typeof item === 'object');
|
|
4060
|
-
if (!items.length) {
|
|
4061
|
-
return '';
|
|
4062
|
-
}
|
|
4063
|
-
const first = items[0];
|
|
4064
|
-
const data = this.tooltipPointData(first['data']);
|
|
4065
|
-
const header = this.formatCanonicalTimeTooltipHeader(first, data, config);
|
|
4066
|
-
const lines = items.map((item) => {
|
|
4067
|
-
const itemData = this.tooltipPointData(item['data']);
|
|
4068
|
-
const seriesId = item['seriesId'] === null || item['seriesId'] === undefined
|
|
4069
|
-
? undefined
|
|
4070
|
-
: String(item['seriesId']);
|
|
4071
|
-
const format = seriesId
|
|
4072
|
-
? this.seriesLabelFormat(config, seriesId) ?? this.primaryValueFormat(config)
|
|
4073
|
-
: this.primaryValueFormat(config);
|
|
4074
|
-
const value = this.formatValue(item['value'] ?? itemData['value'], format);
|
|
4075
|
-
const seriesName = item['seriesName'] === null || item['seriesName'] === undefined
|
|
4076
|
-
? ''
|
|
4077
|
-
: String(item['seriesName']);
|
|
4078
|
-
return seriesName
|
|
4079
|
-
? `${escapeTooltipHtml(seriesName)}: ${escapeTooltipHtml(value)}`
|
|
4080
|
-
: escapeTooltipHtml(value);
|
|
4081
|
-
});
|
|
4082
|
-
return [escapeTooltipHtml(header), ...lines]
|
|
4083
|
-
.filter((part) => part.length > 0)
|
|
4084
|
-
.join('<br/>');
|
|
4085
|
-
}
|
|
4086
|
-
formatCanonicalTimeTooltipHeader(params, data, config) {
|
|
4087
|
-
const dateFormat = config.axes?.x?.labels?.format;
|
|
4088
|
-
const label = this.nonEmptyText(data['label']);
|
|
4089
|
-
const start = this.nonEmptyText(data['start']);
|
|
4090
|
-
const end = this.nonEmptyText(data['end']);
|
|
4091
|
-
const formattedStart = start ? this.formatValue(start, dateFormat) : '';
|
|
4092
|
-
const formattedEnd = end ? this.formatValue(end, dateFormat) : '';
|
|
4093
|
-
const interval = formattedStart && formattedEnd && formattedStart !== formattedEnd
|
|
4094
|
-
? `${formattedStart} – ${formattedEnd}`
|
|
4095
|
-
: formattedStart || formattedEnd;
|
|
4096
|
-
const fallback = this.nonEmptyText(params['axisValueLabel'])
|
|
4097
|
-
?? this.nonEmptyText(params['name'])
|
|
4098
|
-
?? '';
|
|
4099
|
-
if (label && interval && label !== formattedStart && label !== interval) {
|
|
4100
|
-
return `${label} · ${interval}`;
|
|
4101
|
-
}
|
|
4102
|
-
return interval || label || fallback;
|
|
4103
|
-
}
|
|
4104
|
-
tooltipPointData(value) {
|
|
4105
|
-
return value && typeof value === 'object' && !Array.isArray(value)
|
|
4106
|
-
? value
|
|
4107
|
-
: {};
|
|
4108
|
-
}
|
|
4109
|
-
nonEmptyText(value) {
|
|
4110
|
-
if (value === null || value === undefined) {
|
|
4111
|
-
return undefined;
|
|
4112
|
-
}
|
|
4113
|
-
const text = String(value).trim();
|
|
4114
|
-
return text || undefined;
|
|
4115
|
-
}
|
|
4116
|
-
seriesLabelFormat(config, seriesId) {
|
|
4117
|
-
return config.series.find((series) => series.id === seriesId)?.labels?.format;
|
|
4118
|
-
}
|
|
4119
|
-
axisLabelFormatter(format) {
|
|
4120
|
-
return format ? (value) => this.formatValue(value, format) : undefined;
|
|
4121
|
-
}
|
|
4122
|
-
formatValue(value, format) {
|
|
4123
|
-
const normalizedValue = Array.isArray(value) ? value[value.length - 1] : value;
|
|
4124
|
-
if (normalizedValue === null || normalizedValue === undefined || normalizedValue === '') {
|
|
4125
|
-
return '';
|
|
4126
|
-
}
|
|
4127
|
-
if (!format) {
|
|
4128
|
-
return String(normalizedValue);
|
|
4129
|
-
}
|
|
4130
|
-
const formattedDate = this.formatDateValue(normalizedValue, format);
|
|
4131
|
-
if (formattedDate !== null) {
|
|
4132
|
-
return formattedDate;
|
|
4133
|
-
}
|
|
4134
|
-
const numeric = typeof normalizedValue === 'number'
|
|
4135
|
-
? normalizedValue
|
|
4136
|
-
: Number(String(normalizedValue).replace(',', '.'));
|
|
4137
|
-
if (!Number.isFinite(numeric)) {
|
|
4138
|
-
return String(normalizedValue);
|
|
4139
|
-
}
|
|
4140
|
-
const currency = this.parseCurrencyFormat(format);
|
|
4141
|
-
if (currency) {
|
|
4142
|
-
return new Intl.NumberFormat(DEFAULT_VALUE_LOCALE, {
|
|
4143
|
-
style: 'currency',
|
|
4144
|
-
currency: currency.code,
|
|
4145
|
-
currencyDisplay: currency.display,
|
|
4146
|
-
minimumFractionDigits: currency.decimals,
|
|
4147
|
-
maximumFractionDigits: currency.decimals,
|
|
4148
|
-
useGrouping: currency.useGrouping,
|
|
4149
|
-
}).format(numeric);
|
|
4150
|
-
}
|
|
4151
|
-
const number = this.parseNumberFormat(format);
|
|
4152
|
-
if (number) {
|
|
4153
|
-
return new Intl.NumberFormat(DEFAULT_VALUE_LOCALE, {
|
|
4154
|
-
minimumFractionDigits: number.minimumFractionDigits,
|
|
4155
|
-
maximumFractionDigits: number.maximumFractionDigits,
|
|
4156
|
-
useGrouping: number.useGrouping,
|
|
4157
|
-
}).format(numeric);
|
|
4158
|
-
}
|
|
4159
|
-
return String(normalizedValue);
|
|
4160
|
-
}
|
|
4161
|
-
formatDateValue(value, format) {
|
|
4162
|
-
if (!this.isDateFormat(format)) {
|
|
4163
|
-
return null;
|
|
4164
|
-
}
|
|
4165
|
-
const parts = this.readDateParts(value);
|
|
4166
|
-
if (!parts) {
|
|
4167
|
-
return null;
|
|
4168
|
-
}
|
|
4169
|
-
const day = String(parts.day);
|
|
4170
|
-
const day2 = day.padStart(2, '0');
|
|
4171
|
-
const month = String(parts.month);
|
|
4172
|
-
const month2 = month.padStart(2, '0');
|
|
4173
|
-
const year = String(parts.year);
|
|
4174
|
-
const year2 = year.slice(-2);
|
|
4175
|
-
const date = new Date(parts.year, parts.month - 1, parts.day);
|
|
4176
|
-
const monthShort = new Intl.DateTimeFormat(DEFAULT_VALUE_LOCALE, { month: 'short' }).format(date);
|
|
4177
|
-
const monthLong = new Intl.DateTimeFormat(DEFAULT_VALUE_LOCALE, { month: 'long' }).format(date);
|
|
4178
|
-
return format
|
|
4179
|
-
.replace(/yyyy/g, year)
|
|
4180
|
-
.replace(/yy/g, year2)
|
|
4181
|
-
.replace(/MMMM/g, monthLong)
|
|
4182
|
-
.replace(/MMM/g, monthShort)
|
|
4183
|
-
.replace(/MM/g, month2)
|
|
4184
|
-
.replace(/M/g, month)
|
|
4185
|
-
.replace(/dd/g, day2)
|
|
4186
|
-
.replace(/d/g, day);
|
|
4187
|
-
}
|
|
4188
|
-
isDateFormat(format) {
|
|
4189
|
-
return /(^|[^A-Za-z])(d{1,2}|M{1,4}|y{2,4})(?=$|[^A-Za-z])/.test(format);
|
|
4190
|
-
}
|
|
4191
|
-
readDateParts(value) {
|
|
4192
|
-
if (value instanceof Date) {
|
|
4193
|
-
return Number.isNaN(value.getTime())
|
|
4194
|
-
? null
|
|
4195
|
-
: { year: value.getFullYear(), month: value.getMonth() + 1, day: value.getDate() };
|
|
4196
|
-
}
|
|
4197
|
-
if (typeof value === 'number') {
|
|
4198
|
-
const date = new Date(value);
|
|
4199
|
-
return Number.isNaN(date.getTime())
|
|
4200
|
-
? null
|
|
4201
|
-
: { year: date.getFullYear(), month: date.getMonth() + 1, day: date.getDate() };
|
|
4202
|
-
}
|
|
4203
|
-
const raw = String(value).trim();
|
|
4204
|
-
const isoDate = /^(\d{4})-(\d{2})-(\d{2})(?:[T\s].*)?$/.exec(raw);
|
|
4205
|
-
if (isoDate) {
|
|
4206
|
-
return {
|
|
4207
|
-
year: Number(isoDate[1]),
|
|
4208
|
-
month: Number(isoDate[2]),
|
|
4209
|
-
day: Number(isoDate[3]),
|
|
4210
|
-
};
|
|
4211
|
-
}
|
|
4212
|
-
const brazilianDate = /^(\d{2})\/(\d{2})\/(\d{4})$/.exec(raw);
|
|
4213
|
-
if (brazilianDate) {
|
|
4214
|
-
return {
|
|
4215
|
-
year: Number(brazilianDate[3]),
|
|
4216
|
-
month: Number(brazilianDate[2]),
|
|
4217
|
-
day: Number(brazilianDate[1]),
|
|
4218
|
-
};
|
|
4219
|
-
}
|
|
4220
|
-
const parsed = new Date(raw);
|
|
4221
|
-
return Number.isNaN(parsed.getTime())
|
|
4222
|
-
? null
|
|
4223
|
-
: { year: parsed.getFullYear(), month: parsed.getMonth() + 1, day: parsed.getDate() };
|
|
4224
|
-
}
|
|
4225
|
-
parseCurrencyFormat(format) {
|
|
4226
|
-
const parts = format.split('|').map((part) => part.trim()).filter(Boolean);
|
|
4227
|
-
const code = parts[0]?.toUpperCase();
|
|
4228
|
-
if (!code || !/^[A-Z]{3}$/.test(code)) {
|
|
4229
|
-
return null;
|
|
4230
|
-
}
|
|
4231
|
-
const display = this.normalizeCurrencyDisplay(parts[1]);
|
|
4232
|
-
const decimals = this.clampDecimals(Number.parseInt(parts[2] ?? '2', 10), 2);
|
|
4233
|
-
return {
|
|
4234
|
-
code,
|
|
4235
|
-
display,
|
|
4236
|
-
decimals,
|
|
4237
|
-
useGrouping: !parts.some((part) => part.toLowerCase() === 'nosep'),
|
|
4238
|
-
};
|
|
4239
|
-
}
|
|
4240
|
-
normalizeCurrencyDisplay(value) {
|
|
4241
|
-
if (value === 'code' || value === 'name' || value === 'narrowSymbol') {
|
|
4242
|
-
return value;
|
|
4243
|
-
}
|
|
4244
|
-
return 'symbol';
|
|
4245
|
-
}
|
|
4246
|
-
parseNumberFormat(format) {
|
|
4247
|
-
const [pattern, ...modifiers] = format.split('|').map((part) => part.trim());
|
|
4248
|
-
const match = /^(\d+)\.(\d+)-(\d+)$/.exec(pattern);
|
|
4249
|
-
if (!match) {
|
|
4250
|
-
return null;
|
|
4251
|
-
}
|
|
4252
|
-
return {
|
|
4253
|
-
minimumFractionDigits: this.clampDecimals(Number.parseInt(match[2], 10), 0),
|
|
4254
|
-
maximumFractionDigits: this.clampDecimals(Number.parseInt(match[3], 10), 0),
|
|
4255
|
-
useGrouping: !modifiers.some((part) => part.toLowerCase() === 'nosep'),
|
|
4256
|
-
};
|
|
4257
|
-
}
|
|
4258
|
-
clampDecimals(value, fallback) {
|
|
4259
|
-
if (!Number.isFinite(value)) {
|
|
4260
|
-
return fallback;
|
|
4261
|
-
}
|
|
4262
|
-
return Math.min(Math.max(value, 0), 20);
|
|
4263
|
-
}
|
|
4264
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: EChartsOptionBuilderService, deps: [{ token: PraxisChartDataTransformerService }], target: i0.ɵɵFactoryTarget.Injectable });
|
|
4265
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: EChartsOptionBuilderService, providedIn: 'root' });
|
|
3887
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: AnalyticsChartConfigAdapterService, deps: [{ token: i1.PraxisI18nService, optional: true }], target: i0.ɵɵFactoryTarget.Injectable });
|
|
3888
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: AnalyticsChartConfigAdapterService, providedIn: 'root' });
|
|
4266
3889
|
}
|
|
4267
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type:
|
|
3890
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: AnalyticsChartConfigAdapterService, decorators: [{
|
|
4268
3891
|
type: Injectable,
|
|
4269
3892
|
args: [{ providedIn: 'root' }]
|
|
4270
|
-
}], ctorParameters: () => [{ type:
|
|
4271
|
-
|
|
4272
|
-
|
|
4273
|
-
if (value.length <= 28) {
|
|
4274
|
-
return value;
|
|
4275
|
-
}
|
|
4276
|
-
return `${value.slice(0, 25)}...`;
|
|
4277
|
-
}
|
|
4278
|
-
function escapeTooltipHtml(value) {
|
|
4279
|
-
return String(value ?? '')
|
|
4280
|
-
.replace(/&/g, '&')
|
|
4281
|
-
.replace(/</g, '<')
|
|
4282
|
-
.replace(/>/g, '>')
|
|
4283
|
-
.replace(/"/g, '"')
|
|
4284
|
-
.replace(/'/g, ''');
|
|
4285
|
-
}
|
|
4286
|
-
|
|
4287
|
-
use([
|
|
4288
|
-
AriaComponent,
|
|
4289
|
-
BarChart,
|
|
4290
|
-
CanvasRenderer,
|
|
4291
|
-
DatasetComponent,
|
|
4292
|
-
GridComponent,
|
|
4293
|
-
LegendComponent,
|
|
4294
|
-
LineChart,
|
|
4295
|
-
PieChart,
|
|
4296
|
-
ScatterChart,
|
|
4297
|
-
TitleComponent,
|
|
4298
|
-
TooltipComponent,
|
|
4299
|
-
TransformComponent,
|
|
4300
|
-
]);
|
|
4301
|
-
class EChartsEngineAdapter {
|
|
4302
|
-
optionBuilder;
|
|
4303
|
-
chart;
|
|
4304
|
-
currentHost;
|
|
4305
|
-
clickHost;
|
|
4306
|
-
domClickHandler;
|
|
4307
|
-
zrenderClickHandler;
|
|
4308
|
-
lastSeriesClickAt = 0;
|
|
4309
|
-
lastGridClick;
|
|
4310
|
-
constructor(optionBuilder) {
|
|
4311
|
-
this.optionBuilder = optionBuilder;
|
|
4312
|
-
}
|
|
4313
|
-
render(host, payload) {
|
|
4314
|
-
if (this.chart && this.currentHost !== host) {
|
|
4315
|
-
this.disposeChart();
|
|
4316
|
-
}
|
|
4317
|
-
if (!this.chart) {
|
|
4318
|
-
this.chart = this.createChart(host);
|
|
4319
|
-
this.currentHost = host;
|
|
4320
|
-
}
|
|
4321
|
-
const option = this.optionBuilder.build(payload.config, payload.data);
|
|
4322
|
-
const chart = this.chart;
|
|
4323
|
-
chart.setOption(option, true);
|
|
4324
|
-
this.detachHandlers();
|
|
4325
|
-
chart.off('click');
|
|
4326
|
-
chart.on('click', (params) => {
|
|
4327
|
-
this.lastSeriesClickAt = Date.now();
|
|
4328
|
-
this.lastGridClick = undefined;
|
|
4329
|
-
const data = pointData(params?.name, params?.data ?? params?.value);
|
|
4330
|
-
payload.onPointClick?.({
|
|
4331
|
-
chartId: payload.config.id,
|
|
4332
|
-
seriesId: params?.seriesId,
|
|
4333
|
-
seriesName: params?.seriesName,
|
|
4334
|
-
category: this.resolvePointCategory(payload, data, params?.name),
|
|
4335
|
-
value: pointValue(params?.value ?? params?.data),
|
|
4336
|
-
data,
|
|
4337
|
-
});
|
|
4338
|
-
});
|
|
4339
|
-
const zrender = chart?.getZr?.();
|
|
4340
|
-
this.zrenderClickHandler = (event) => {
|
|
4341
|
-
window.setTimeout(() => {
|
|
4342
|
-
if (Date.now() - this.lastSeriesClickAt < 80) {
|
|
4343
|
-
return;
|
|
4344
|
-
}
|
|
4345
|
-
this.emitCategoryClickFromGrid(event, payload);
|
|
4346
|
-
}, 0);
|
|
4347
|
-
};
|
|
4348
|
-
zrender?.on?.('click', this.zrenderClickHandler);
|
|
4349
|
-
this.domClickHandler = (event) => {
|
|
4350
|
-
window.setTimeout(() => {
|
|
4351
|
-
if (Date.now() - this.lastSeriesClickAt < 80) {
|
|
4352
|
-
return;
|
|
4353
|
-
}
|
|
4354
|
-
const rect = host.getBoundingClientRect();
|
|
4355
|
-
const offsetX = Number.isFinite(event.offsetX) ? event.offsetX : event.clientX - rect.left;
|
|
4356
|
-
const offsetY = Number.isFinite(event.offsetY) ? event.offsetY : event.clientY - rect.top;
|
|
4357
|
-
this.emitCategoryClickFromGrid({
|
|
4358
|
-
offsetX,
|
|
4359
|
-
offsetY,
|
|
4360
|
-
}, payload);
|
|
4361
|
-
}, 0);
|
|
4362
|
-
};
|
|
4363
|
-
host.addEventListener('click', this.domClickHandler, true);
|
|
4364
|
-
this.clickHost = host;
|
|
4365
|
-
}
|
|
4366
|
-
resize() {
|
|
4367
|
-
this.chart?.resize();
|
|
4368
|
-
}
|
|
4369
|
-
destroy() {
|
|
4370
|
-
this.disposeChart();
|
|
4371
|
-
}
|
|
4372
|
-
createChart(host) {
|
|
4373
|
-
return init(host);
|
|
4374
|
-
}
|
|
4375
|
-
disposeChart() {
|
|
4376
|
-
this.detachHandlers();
|
|
4377
|
-
this.chart?.off('click');
|
|
4378
|
-
this.chart?.dispose();
|
|
4379
|
-
this.chart = undefined;
|
|
4380
|
-
this.currentHost = undefined;
|
|
4381
|
-
this.lastSeriesClickAt = 0;
|
|
4382
|
-
this.lastGridClick = undefined;
|
|
4383
|
-
}
|
|
4384
|
-
detachHandlers() {
|
|
4385
|
-
const zrender = this.chart?.getZr?.();
|
|
4386
|
-
if (zrender && this.zrenderClickHandler) {
|
|
4387
|
-
zrender.off?.('click', this.zrenderClickHandler);
|
|
4388
|
-
}
|
|
4389
|
-
if (this.clickHost && this.domClickHandler) {
|
|
4390
|
-
this.clickHost.removeEventListener('click', this.domClickHandler, true);
|
|
4391
|
-
}
|
|
4392
|
-
this.clickHost = undefined;
|
|
4393
|
-
this.domClickHandler = undefined;
|
|
4394
|
-
this.zrenderClickHandler = undefined;
|
|
4395
|
-
}
|
|
4396
|
-
resolvePointCategory(payload, data, fallback) {
|
|
4397
|
-
const categoryField = payload.config.axes?.x?.field
|
|
4398
|
-
?? payload.config.series.find((series) => series.categoryField)?.categoryField;
|
|
4399
|
-
const dataSource = payload.config.dataSource;
|
|
4400
|
-
const usesCanonicalStatsTime = dataSource?.kind === 'remote'
|
|
4401
|
-
&& dataSource.query?.sourceKind === 'praxis.stats';
|
|
4402
|
-
const usesCanonicalStatsTimePoint = usesCanonicalStatsTime
|
|
4403
|
-
&& (payload.config.axes?.x?.type === 'time'
|
|
4404
|
-
|| (dataSource?.kind === 'remote' && dataSource.query?.statsOperation === 'timeseries'));
|
|
4405
|
-
const candidate = usesCanonicalStatsTimePoint && data['label'] !== null
|
|
4406
|
-
&& data['label'] !== undefined
|
|
4407
|
-
&& data['label'] !== ''
|
|
4408
|
-
? data['label']
|
|
4409
|
-
: categoryField && data[categoryField] !== null && data[categoryField] !== undefined
|
|
4410
|
-
? data[categoryField]
|
|
4411
|
-
: fallback;
|
|
4412
|
-
return candidate === null || candidate === undefined
|
|
4413
|
-
? undefined
|
|
4414
|
-
: String(candidate);
|
|
4415
|
-
}
|
|
4416
|
-
emitCategoryClickFromGrid(event, payload) {
|
|
4417
|
-
const chart = this.chart;
|
|
4418
|
-
if (!chart || typeof chart.containPixel !== 'function' || typeof chart.convertFromPixel !== 'function') {
|
|
4419
|
-
return;
|
|
4420
|
-
}
|
|
4421
|
-
const point = [event?.offsetX, event?.offsetY];
|
|
4422
|
-
if (!Number.isFinite(point[0]) || !Number.isFinite(point[1])) {
|
|
4423
|
-
return;
|
|
4424
|
-
}
|
|
4425
|
-
if (!chart.containPixel({ gridIndex: 0 }, point)) {
|
|
4426
|
-
return;
|
|
4427
|
-
}
|
|
4428
|
-
const option = chart.getOption?.();
|
|
4429
|
-
const xAxis = firstOptionEntry(option?.xAxis);
|
|
4430
|
-
const yAxis = firstOptionEntry(option?.yAxis);
|
|
4431
|
-
const horizontal = yAxis?.type === 'category';
|
|
4432
|
-
const categories = (horizontal ? yAxis?.data : xAxis?.data) ?? [];
|
|
4433
|
-
if (!Array.isArray(categories) || categories.length === 0) {
|
|
4434
|
-
return;
|
|
4435
|
-
}
|
|
4436
|
-
const converted = chart.convertFromPixel({ gridIndex: 0 }, point);
|
|
4437
|
-
const categoryIndex = Math.round(Number(horizontal ? converted?.[1] : converted?.[0]));
|
|
4438
|
-
if (!Number.isInteger(categoryIndex) || categoryIndex < 0 || categoryIndex >= categories.length) {
|
|
4439
|
-
return;
|
|
4440
|
-
}
|
|
4441
|
-
const category = categories[categoryIndex];
|
|
4442
|
-
const categoryValue = category == null ? undefined : String(category);
|
|
4443
|
-
const categoryField = payload.config.axes?.x?.field
|
|
4444
|
-
?? payload.config.series.find((series) => series.categoryField)?.categoryField;
|
|
4445
|
-
const signature = JSON.stringify({
|
|
4446
|
-
chartId: payload.config.id,
|
|
4447
|
-
category: categoryValue,
|
|
4448
|
-
horizontal,
|
|
4449
|
-
categoryIndex,
|
|
4450
|
-
});
|
|
4451
|
-
if (this.isDuplicateGridClick(signature)) {
|
|
4452
|
-
return;
|
|
4453
|
-
}
|
|
4454
|
-
const usesCanonicalStatsRows = payload.config.dataSource?.kind === 'remote'
|
|
4455
|
-
&& payload.config.dataSource.query?.sourceKind === 'praxis.stats';
|
|
4456
|
-
const fallbackSourceRow = !usesCanonicalStatsRows
|
|
4457
|
-
&& categoryField
|
|
4458
|
-
&& categoryValue !== undefined
|
|
4459
|
-
? payload.data.find((row) => String(row[categoryField] ?? '') === categoryValue)
|
|
4460
|
-
: undefined;
|
|
4461
|
-
const sourceRow = usesCanonicalStatsRows
|
|
4462
|
-
? payload.data[categoryIndex]
|
|
4463
|
-
: fallbackSourceRow;
|
|
4464
|
-
const data = {
|
|
4465
|
-
...(sourceRow ?? {}),
|
|
4466
|
-
category: categoryValue,
|
|
4467
|
-
};
|
|
4468
|
-
if (categoryField && categoryValue !== undefined) {
|
|
4469
|
-
data[categoryField] = categoryValue;
|
|
4470
|
-
}
|
|
4471
|
-
payload.onPointClick?.({
|
|
4472
|
-
chartId: payload.config.id,
|
|
4473
|
-
category: categoryValue,
|
|
4474
|
-
data,
|
|
4475
|
-
});
|
|
4476
|
-
}
|
|
4477
|
-
isDuplicateGridClick(signature) {
|
|
4478
|
-
const now = Date.now();
|
|
4479
|
-
const duplicate = this.lastGridClick?.signature === signature
|
|
4480
|
-
&& now - this.lastGridClick.at < 80;
|
|
4481
|
-
this.lastGridClick = { signature, at: now };
|
|
4482
|
-
return duplicate;
|
|
4483
|
-
}
|
|
4484
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: EChartsEngineAdapter, deps: [{ token: EChartsOptionBuilderService }], target: i0.ɵɵFactoryTarget.Injectable });
|
|
4485
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: EChartsEngineAdapter });
|
|
4486
|
-
}
|
|
4487
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: EChartsEngineAdapter, decorators: [{
|
|
4488
|
-
type: Injectable
|
|
4489
|
-
}], ctorParameters: () => [{ type: EChartsOptionBuilderService }] });
|
|
4490
|
-
function firstOptionEntry(value) {
|
|
4491
|
-
return Array.isArray(value) ? value[0] : value;
|
|
4492
|
-
}
|
|
4493
|
-
function pointValue(value) {
|
|
4494
|
-
if (Array.isArray(value)) {
|
|
4495
|
-
return value.length > 1 ? value[1] : value[0];
|
|
4496
|
-
}
|
|
4497
|
-
if (value && typeof value === 'object' && 'value' in value) {
|
|
4498
|
-
return pointValue(value.value);
|
|
4499
|
-
}
|
|
4500
|
-
return value;
|
|
4501
|
-
}
|
|
4502
|
-
function pointData(category, value) {
|
|
4503
|
-
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
4504
|
-
return value;
|
|
4505
|
-
}
|
|
4506
|
-
return {
|
|
4507
|
-
category,
|
|
4508
|
-
value: pointValue(value),
|
|
4509
|
-
};
|
|
4510
|
-
}
|
|
3893
|
+
}], ctorParameters: () => [{ type: i1.PraxisI18nService, decorators: [{
|
|
3894
|
+
type: Optional
|
|
3895
|
+
}] }] });
|
|
4511
3896
|
|
|
4512
3897
|
const PRAXIS_CHARTS_EN_US = {
|
|
4513
3898
|
'praxis.charts.runtime.editChart': 'Edit chart settings',
|
|
@@ -4645,7 +4030,7 @@ const PRAXIS_CHARTS_EN_US = {
|
|
|
4645
4030
|
'praxis.charts.editor.paletteToken.brand-balanced': 'Brand balanced',
|
|
4646
4031
|
'praxis.charts.editor.paletteToken.status': 'Status',
|
|
4647
4032
|
'praxis.charts.editor.paletteToken.executive': 'Executive',
|
|
4648
|
-
'praxis.charts.editor.specialization.pieDonutHint': 'Pie and
|
|
4033
|
+
'praxis.charts.editor.specialization.pieDonutHint': 'Pie, donut, funnel and pyramid charts keep one metric and use the first dimension as the category segment.',
|
|
4649
4034
|
'praxis.charts.editor.specialization.scatterTitle': 'Scatter guidance',
|
|
4650
4035
|
'praxis.charts.editor.specialization.scatterHint': 'Scatter charts use the first dimension as X and the first metric as Y.',
|
|
4651
4036
|
'praxis.charts.editor.events.pointClickTitle': 'Point click',
|
|
@@ -4714,6 +4099,17 @@ const PRAXIS_CHARTS_EN_US = {
|
|
|
4714
4099
|
'praxis.charts.editor.kind.pie': 'Pie',
|
|
4715
4100
|
'praxis.charts.editor.kind.donut': 'Donut',
|
|
4716
4101
|
'praxis.charts.editor.kind.scatter': 'Scatter',
|
|
4102
|
+
'praxis.charts.editor.kind.funnel': 'Funnel',
|
|
4103
|
+
'praxis.charts.editor.kind.pyramid': 'Pyramid',
|
|
4104
|
+
'praxis.charts.analyticsPresentation.selectorLabel': 'Analytics presentation',
|
|
4105
|
+
'praxis.charts.analyticsPresentation.chart': 'Chart',
|
|
4106
|
+
'praxis.charts.analyticsPresentation.table': 'Table',
|
|
4107
|
+
'praxis.charts.analyticsPresentation.unavailable': 'Presentation unavailable',
|
|
4108
|
+
'praxis.charts.analyticsPresentation.loading': 'Loading analytics…',
|
|
4109
|
+
'praxis.charts.analyticsPresentation.empty': 'No data available.',
|
|
4110
|
+
'praxis.charts.analyticsPresentation.projectionMissing': 'Analytics projection "{projectionId}" was not found.',
|
|
4111
|
+
'praxis.charts.analyticsPresentation.noRenderer': 'Analytics projection "{projectionId}" has no eligible installed renderer.',
|
|
4112
|
+
'praxis.charts.analyticsPresentation.familyIneligible': 'Presentation family "{family}" is not eligible for projection "{projectionId}".',
|
|
4717
4113
|
};
|
|
4718
4114
|
|
|
4719
4115
|
const PRAXIS_CHARTS_PT_BR = {
|
|
@@ -4852,7 +4248,7 @@ const PRAXIS_CHARTS_PT_BR = {
|
|
|
4852
4248
|
'praxis.charts.editor.paletteToken.brand-balanced': 'Marca balanceada',
|
|
4853
4249
|
'praxis.charts.editor.paletteToken.status': 'Status',
|
|
4854
4250
|
'praxis.charts.editor.paletteToken.executive': 'Executiva',
|
|
4855
|
-
'praxis.charts.editor.specialization.pieDonutHint': 'Gráficos de pizza
|
|
4251
|
+
'praxis.charts.editor.specialization.pieDonutHint': 'Gráficos de pizza, rosca, funil e pirâmide mantêm uma métrica e usam a primeira dimensão como segmento categórico.',
|
|
4856
4252
|
'praxis.charts.editor.specialization.scatterTitle': 'Guia de dispersão (scatter)',
|
|
4857
4253
|
'praxis.charts.editor.specialization.scatterHint': 'Gráficos de dispersão usam a primeira dimensão como eixo X e a primeira métrica como eixo Y.',
|
|
4858
4254
|
'praxis.charts.editor.events.pointClickTitle': 'Clique no ponto',
|
|
@@ -4921,56 +4317,1121 @@ const PRAXIS_CHARTS_PT_BR = {
|
|
|
4921
4317
|
'praxis.charts.editor.kind.pie': 'Pizza (Pie)',
|
|
4922
4318
|
'praxis.charts.editor.kind.donut': 'Rosca (Donut)',
|
|
4923
4319
|
'praxis.charts.editor.kind.scatter': 'Dispersão (Scatter)',
|
|
4320
|
+
'praxis.charts.editor.kind.funnel': 'Funil',
|
|
4321
|
+
'praxis.charts.editor.kind.pyramid': 'Pirâmide',
|
|
4322
|
+
'praxis.charts.analyticsPresentation.selectorLabel': 'Apresentação analítica',
|
|
4323
|
+
'praxis.charts.analyticsPresentation.chart': 'Gráfico',
|
|
4324
|
+
'praxis.charts.analyticsPresentation.table': 'Tabela',
|
|
4325
|
+
'praxis.charts.analyticsPresentation.unavailable': 'Apresentação indisponível',
|
|
4326
|
+
'praxis.charts.analyticsPresentation.loading': 'Carregando dados analíticos…',
|
|
4327
|
+
'praxis.charts.analyticsPresentation.empty': 'Nenhum dado disponível.',
|
|
4328
|
+
'praxis.charts.analyticsPresentation.projectionMissing': 'A projection analítica "{projectionId}" não foi encontrada.',
|
|
4329
|
+
'praxis.charts.analyticsPresentation.noRenderer': 'A projection analítica "{projectionId}" não possui renderer instalado elegível.',
|
|
4330
|
+
'praxis.charts.analyticsPresentation.familyIneligible': 'A família de apresentação "{family}" não é elegível para a projection "{projectionId}".',
|
|
4924
4331
|
};
|
|
4925
4332
|
|
|
4926
|
-
const PRAXIS_CHARTS_I18N = new InjectionToken('PRAXIS_CHARTS_I18N', {
|
|
4927
|
-
factory: () => ({}),
|
|
4928
|
-
});
|
|
4929
|
-
function createPraxisChartsI18nConfig(options = {}) {
|
|
4930
|
-
const localeDictionaries = {
|
|
4931
|
-
'pt-BR': {
|
|
4932
|
-
...PRAXIS_CHARTS_PT_BR,
|
|
4933
|
-
...(options.dictionaries?.['pt-BR'] ?? {}),
|
|
4934
|
-
},
|
|
4935
|
-
'en-US': {
|
|
4936
|
-
...PRAXIS_CHARTS_EN_US,
|
|
4937
|
-
...(options.dictionaries?.['en-US'] ?? {}),
|
|
4938
|
-
},
|
|
4939
|
-
};
|
|
4940
|
-
for (const [locale, dictionary] of Object.entries(options.dictionaries ?? {})) {
|
|
4941
|
-
if (locale === 'pt-BR' || locale === 'en-US') {
|
|
4942
|
-
continue;
|
|
4333
|
+
const PRAXIS_CHARTS_I18N = new InjectionToken('PRAXIS_CHARTS_I18N', {
|
|
4334
|
+
factory: () => ({}),
|
|
4335
|
+
});
|
|
4336
|
+
function createPraxisChartsI18nConfig(options = {}) {
|
|
4337
|
+
const localeDictionaries = {
|
|
4338
|
+
'pt-BR': {
|
|
4339
|
+
...PRAXIS_CHARTS_PT_BR,
|
|
4340
|
+
...(options.dictionaries?.['pt-BR'] ?? {}),
|
|
4341
|
+
},
|
|
4342
|
+
'en-US': {
|
|
4343
|
+
...PRAXIS_CHARTS_EN_US,
|
|
4344
|
+
...(options.dictionaries?.['en-US'] ?? {}),
|
|
4345
|
+
},
|
|
4346
|
+
};
|
|
4347
|
+
for (const [locale, dictionary] of Object.entries(options.dictionaries ?? {})) {
|
|
4348
|
+
if (locale === 'pt-BR' || locale === 'en-US') {
|
|
4349
|
+
continue;
|
|
4350
|
+
}
|
|
4351
|
+
localeDictionaries[locale] = {
|
|
4352
|
+
...(localeDictionaries[locale] ?? {}),
|
|
4353
|
+
...dictionary,
|
|
4354
|
+
};
|
|
4355
|
+
}
|
|
4356
|
+
return {
|
|
4357
|
+
locale: options.locale,
|
|
4358
|
+
fallbackLocale: options.fallbackLocale ?? 'pt-BR',
|
|
4359
|
+
namespaces: {
|
|
4360
|
+
charts: localeDictionaries,
|
|
4361
|
+
},
|
|
4362
|
+
};
|
|
4363
|
+
}
|
|
4364
|
+
function providePraxisChartsI18n(options = {}) {
|
|
4365
|
+
return [
|
|
4366
|
+
{
|
|
4367
|
+
provide: PRAXIS_CHARTS_I18N,
|
|
4368
|
+
useValue: {},
|
|
4369
|
+
},
|
|
4370
|
+
providePraxisI18n(createPraxisChartsI18nConfig(options)),
|
|
4371
|
+
];
|
|
4372
|
+
}
|
|
4373
|
+
function resolvePraxisChartsText(value, fallback) {
|
|
4374
|
+
if (typeof value === 'string') {
|
|
4375
|
+
return { text: value };
|
|
4376
|
+
}
|
|
4377
|
+
if (value?.key || value?.text) {
|
|
4378
|
+
return value;
|
|
4379
|
+
}
|
|
4380
|
+
return { text: fallback ?? '' };
|
|
4381
|
+
}
|
|
4382
|
+
|
|
4383
|
+
const INSTALLED_FAMILIES = ['chart', 'analytic-table'];
|
|
4384
|
+
class PraxisAnalyticsPresentationComponent {
|
|
4385
|
+
analytics = input.required(...(ngDevMode ? [{ debugName: "analytics" }] : /* istanbul ignore next */ []));
|
|
4386
|
+
projectionId = input.required(...(ngDevMode ? [{ debugName: "projectionId" }] : /* istanbul ignore next */ []));
|
|
4387
|
+
availableFamilies = input(INSTALLED_FAMILIES, ...(ngDevMode ? [{ debugName: "availableFamilies" }] : /* istanbul ignore next */ []));
|
|
4388
|
+
preferredFamily = input(null, ...(ngDevMode ? [{ debugName: "preferredFamily" }] : /* istanbul ignore next */ []));
|
|
4389
|
+
queryContext = input(null, ...(ngDevMode ? [{ debugName: "queryContext" }] : /* istanbul ignore next */ []));
|
|
4390
|
+
title = input(undefined, ...(ngDevMode ? [{ debugName: "title" }] : /* istanbul ignore next */ []));
|
|
4391
|
+
subtitle = input(undefined, ...(ngDevMode ? [{ debugName: "subtitle" }] : /* istanbul ignore next */ []));
|
|
4392
|
+
enableCustomization = input(false, ...(ngDevMode ? [{ debugName: "enableCustomization" }] : /* istanbul ignore next */ []));
|
|
4393
|
+
presentationChange = output();
|
|
4394
|
+
pointClick = output();
|
|
4395
|
+
pointAction = output();
|
|
4396
|
+
selectionChange = output();
|
|
4397
|
+
drillDown = output();
|
|
4398
|
+
crossFilter = output();
|
|
4399
|
+
queryRequest = output();
|
|
4400
|
+
loadStateChange = output();
|
|
4401
|
+
resolver = inject(AnalyticsPresentationResolver);
|
|
4402
|
+
i18n = inject(PraxisI18nService);
|
|
4403
|
+
chartAdapter = inject(AnalyticsChartConfigAdapterService);
|
|
4404
|
+
tableAdapter = inject(AnalyticsTableConfigAdapterService);
|
|
4405
|
+
tableStats = inject(AnalyticsTableStatsApiService);
|
|
4406
|
+
temporaryFamily = signal(null, ...(ngDevMode ? [{ debugName: "temporaryFamily" }] : /* istanbul ignore next */ []));
|
|
4407
|
+
tableRows = signal([], ...(ngDevMode ? [{ debugName: "tableRows" }] : /* istanbul ignore next */ []));
|
|
4408
|
+
tableState = signal('idle', ...(ngDevMode ? [{ debugName: "tableState" }] : /* istanbul ignore next */ []));
|
|
4409
|
+
tableDiagnostic = signal(null, ...(ngDevMode ? [{ debugName: "tableDiagnostic" }] : /* istanbul ignore next */ []));
|
|
4410
|
+
tableRequestGeneration = 0;
|
|
4411
|
+
previousProjectionId = null;
|
|
4412
|
+
resolved = computed(() => this.resolvePresentation(), ...(ngDevMode ? [{ debugName: "resolved" }] : /* istanbul ignore next */ []));
|
|
4413
|
+
activeFamily = computed(() => this.resolved().family, ...(ngDevMode ? [{ debugName: "activeFamily" }] : /* istanbul ignore next */ []));
|
|
4414
|
+
eligibleFamilies = computed(() => this.resolved().eligibleFamilies, ...(ngDevMode ? [{ debugName: "eligibleFamilies" }] : /* istanbul ignore next */ []));
|
|
4415
|
+
diagnostic = computed(() => this.resolved().diagnostic ?? this.tableDiagnostic(), ...(ngDevMode ? [{ debugName: "diagnostic" }] : /* istanbul ignore next */ []));
|
|
4416
|
+
chartConfig = computed(() => {
|
|
4417
|
+
const projection = this.resolved().projection;
|
|
4418
|
+
return projection
|
|
4419
|
+
? this.chartAdapter.toPraxisChartConfig(projection, { title: this.title(), subtitle: this.subtitle() })
|
|
4420
|
+
: null;
|
|
4421
|
+
}, ...(ngDevMode ? [{ debugName: "chartConfig" }] : /* istanbul ignore next */ []));
|
|
4422
|
+
tableConfig = computed(() => {
|
|
4423
|
+
const projection = this.resolved().projection;
|
|
4424
|
+
return projection
|
|
4425
|
+
? this.tableAdapter.toTableConfig(projection, { title: this.title(), subtitle: this.subtitle() })
|
|
4426
|
+
: null;
|
|
4427
|
+
}, ...(ngDevMode ? [{ debugName: "tableConfig" }] : /* istanbul ignore next */ []));
|
|
4428
|
+
isTableLoading = computed(() => this.tableState() === 'loading', ...(ngDevMode ? [{ debugName: "isTableLoading" }] : /* istanbul ignore next */ []));
|
|
4429
|
+
isTableEmpty = computed(() => this.tableState() === 'ready' && this.tableRows().length === 0, ...(ngDevMode ? [{ debugName: "isTableEmpty" }] : /* istanbul ignore next */ []));
|
|
4430
|
+
constructor() {
|
|
4431
|
+
effect(() => {
|
|
4432
|
+
const projectionId = this.projectionId();
|
|
4433
|
+
if (this.previousProjectionId !== projectionId) {
|
|
4434
|
+
this.previousProjectionId = projectionId;
|
|
4435
|
+
this.temporaryFamily.set(null);
|
|
4436
|
+
}
|
|
4437
|
+
});
|
|
4438
|
+
effect((onCleanup) => {
|
|
4439
|
+
const resolved = this.resolved();
|
|
4440
|
+
const queryContext = this.queryContext();
|
|
4441
|
+
const generation = ++this.tableRequestGeneration;
|
|
4442
|
+
onCleanup(() => {
|
|
4443
|
+
if (this.tableRequestGeneration === generation) {
|
|
4444
|
+
this.tableRequestGeneration += 1;
|
|
4445
|
+
}
|
|
4446
|
+
});
|
|
4447
|
+
this.tableRows.set([]);
|
|
4448
|
+
this.tableDiagnostic.set(null);
|
|
4449
|
+
this.tableState.set('idle');
|
|
4450
|
+
if (resolved.family !== 'analytic-table' || !resolved.projection) {
|
|
4451
|
+
return;
|
|
4452
|
+
}
|
|
4453
|
+
this.tableState.set('loading');
|
|
4454
|
+
void this.tableStats.execute(resolved.projection, queryContext).then((rows) => {
|
|
4455
|
+
if (this.tableRequestGeneration !== generation)
|
|
4456
|
+
return;
|
|
4457
|
+
this.tableRows.set(rows);
|
|
4458
|
+
this.tableState.set('ready');
|
|
4459
|
+
}, (error) => {
|
|
4460
|
+
if (this.tableRequestGeneration !== generation)
|
|
4461
|
+
return;
|
|
4462
|
+
this.tableRows.set([]);
|
|
4463
|
+
this.tableState.set('error');
|
|
4464
|
+
this.tableDiagnostic.set(normalizeError$1(error));
|
|
4465
|
+
});
|
|
4466
|
+
});
|
|
4467
|
+
}
|
|
4468
|
+
selectFamily(family) {
|
|
4469
|
+
if (!this.eligibleFamilies().includes(family)) {
|
|
4470
|
+
this.tableDiagnostic.set(this.message('praxis.charts.analyticsPresentation.familyIneligible', 'Presentation family "{family}" is not eligible for projection "{projectionId}".', { family, projectionId: this.projectionId() }));
|
|
4471
|
+
return;
|
|
4472
|
+
}
|
|
4473
|
+
this.tableDiagnostic.set(null);
|
|
4474
|
+
this.temporaryFamily.set(family);
|
|
4475
|
+
this.presentationChange.emit({
|
|
4476
|
+
projectionId: this.projectionId(),
|
|
4477
|
+
family,
|
|
4478
|
+
source: 'temporary-user-choice',
|
|
4479
|
+
});
|
|
4480
|
+
}
|
|
4481
|
+
handleTableRowClick(event) {
|
|
4482
|
+
const row = event?.row;
|
|
4483
|
+
const projection = this.resolved().projection;
|
|
4484
|
+
if (!row || !projection)
|
|
4485
|
+
return;
|
|
4486
|
+
const dimensionField = projection.bindings.primaryDimension?.field;
|
|
4487
|
+
const metricField = projection.bindings.primaryMetrics[0]?.field;
|
|
4488
|
+
this.pointClick.emit({
|
|
4489
|
+
chartId: projection.id,
|
|
4490
|
+
category: dimensionField ? stringifyValue(row[dimensionField]) : undefined,
|
|
4491
|
+
value: metricField ? row[metricField] : undefined,
|
|
4492
|
+
data: row,
|
|
4493
|
+
});
|
|
4494
|
+
}
|
|
4495
|
+
resolvePresentation() {
|
|
4496
|
+
const projectionId = this.projectionId();
|
|
4497
|
+
const projection = this.analytics().projections.find((candidate) => candidate.id === projectionId) ?? null;
|
|
4498
|
+
if (!projection) {
|
|
4499
|
+
return {
|
|
4500
|
+
projection: null,
|
|
4501
|
+
family: null,
|
|
4502
|
+
eligibleFamilies: [],
|
|
4503
|
+
diagnostic: this.message('praxis.charts.analyticsPresentation.projectionMissing', 'Analytics projection "{projectionId}" was not found.', { projectionId }),
|
|
4504
|
+
};
|
|
4505
|
+
}
|
|
4506
|
+
const authorized = this.availableFamilies().filter((family) => INSTALLED_FAMILIES.includes(family));
|
|
4507
|
+
const eligibleFamilies = authorized.filter((family) => this.isEligible(projection, family));
|
|
4508
|
+
if (!eligibleFamilies.length) {
|
|
4509
|
+
return {
|
|
4510
|
+
projection,
|
|
4511
|
+
family: null,
|
|
4512
|
+
eligibleFamilies,
|
|
4513
|
+
diagnostic: this.message('praxis.charts.analyticsPresentation.noRenderer', 'Analytics projection "{projectionId}" has no eligible installed renderer.', { projectionId }),
|
|
4514
|
+
};
|
|
4515
|
+
}
|
|
4516
|
+
const requestedFamily = this.temporaryFamily() ?? this.preferredFamily();
|
|
4517
|
+
if (requestedFamily && !eligibleFamilies.includes(requestedFamily)) {
|
|
4518
|
+
return {
|
|
4519
|
+
projection,
|
|
4520
|
+
family: null,
|
|
4521
|
+
eligibleFamilies,
|
|
4522
|
+
diagnostic: this.message('praxis.charts.analyticsPresentation.familyIneligible', 'Presentation family "{family}" is not eligible for projection "{projectionId}".', { family: requestedFamily, projectionId }),
|
|
4523
|
+
};
|
|
4524
|
+
}
|
|
4525
|
+
try {
|
|
4526
|
+
const decision = this.resolver.resolve({ projections: [projection] }, { availableFamilies: eligibleFamilies, preferFamily: requestedFamily });
|
|
4527
|
+
return { projection, family: decision.family, eligibleFamilies, diagnostic: null };
|
|
4528
|
+
}
|
|
4529
|
+
catch (error) {
|
|
4530
|
+
return { projection, family: null, eligibleFamilies, diagnostic: normalizeError$1(error) };
|
|
4531
|
+
}
|
|
4532
|
+
}
|
|
4533
|
+
isEligible(projection, family) {
|
|
4534
|
+
try {
|
|
4535
|
+
return this.resolver.resolve({ projections: [projection] }, { availableFamilies: [family], preferFamily: family }).family === family;
|
|
4536
|
+
}
|
|
4537
|
+
catch {
|
|
4538
|
+
return false;
|
|
4539
|
+
}
|
|
4540
|
+
}
|
|
4541
|
+
t(key, fallback) {
|
|
4542
|
+
return this.i18n.resolve(resolvePraxisChartsText({ key, text: fallback }, fallback));
|
|
4543
|
+
}
|
|
4544
|
+
message(key, fallback, values) {
|
|
4545
|
+
return Object.entries(values).reduce((message, [name, value]) => message.replaceAll(`{${name}}`, value), this.t(key, fallback));
|
|
4546
|
+
}
|
|
4547
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: PraxisAnalyticsPresentationComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
4548
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.14", type: PraxisAnalyticsPresentationComponent, isStandalone: true, selector: "praxis-analytics-presentation", inputs: { analytics: { classPropertyName: "analytics", publicName: "analytics", isSignal: true, isRequired: true, transformFunction: null }, projectionId: { classPropertyName: "projectionId", publicName: "projectionId", isSignal: true, isRequired: true, transformFunction: null }, availableFamilies: { classPropertyName: "availableFamilies", publicName: "availableFamilies", isSignal: true, isRequired: false, transformFunction: null }, preferredFamily: { classPropertyName: "preferredFamily", publicName: "preferredFamily", isSignal: true, isRequired: false, transformFunction: null }, queryContext: { classPropertyName: "queryContext", publicName: "queryContext", isSignal: true, isRequired: false, transformFunction: null }, title: { classPropertyName: "title", publicName: "title", isSignal: true, isRequired: false, transformFunction: null }, subtitle: { classPropertyName: "subtitle", publicName: "subtitle", isSignal: true, isRequired: false, transformFunction: null }, enableCustomization: { classPropertyName: "enableCustomization", publicName: "enableCustomization", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { presentationChange: "presentationChange", pointClick: "pointClick", pointAction: "pointAction", selectionChange: "selectionChange", drillDown: "drillDown", crossFilter: "crossFilter", queryRequest: "queryRequest", loadStateChange: "loadStateChange" }, ngImport: i0, template: "<section class=\"analytics-presentation\" [attr.aria-label]=\"projectionId()\">\n <div class=\"analytics-presentation__toolbar\" role=\"group\" [attr.aria-label]=\"t('praxis.charts.analyticsPresentation.selectorLabel', 'Analytics presentation')\">\n @if (eligibleFamilies().includes('chart')) {\n <button type=\"button\" [attr.aria-pressed]=\"activeFamily() === 'chart'\" (click)=\"selectFamily('chart')\">\n {{ t('praxis.charts.analyticsPresentation.chart', 'Chart') }}\n </button>\n }\n @if (eligibleFamilies().includes('analytic-table')) {\n <button type=\"button\" [attr.aria-pressed]=\"activeFamily() === 'analytic-table'\" (click)=\"selectFamily('analytic-table')\">\n {{ t('praxis.charts.analyticsPresentation.table', 'Table') }}\n </button>\n }\n </div>\n\n @if (diagnostic(); as message) {\n <section class=\"analytics-presentation__state analytics-presentation__state--error\" role=\"alert\">\n <strong>{{ t('praxis.charts.analyticsPresentation.unavailable', 'Presentation unavailable') }}</strong>\n <span>{{ message }}</span>\n </section>\n } @else if (activeFamily() === 'chart' && chartConfig(); as config) {\n <praxis-chart\n [config]=\"config\"\n [queryContext]=\"queryContext()\"\n [enableCustomization]=\"enableCustomization()\"\n (pointClick)=\"pointClick.emit($event)\"\n (pointAction)=\"pointAction.emit($event)\"\n (selectionChange)=\"selectionChange.emit($event)\"\n (drillDown)=\"drillDown.emit($event)\"\n (crossFilter)=\"crossFilter.emit($event)\"\n (queryRequest)=\"queryRequest.emit($event)\"\n (loadStateChange)=\"loadStateChange.emit($event)\"\n />\n } @else if (activeFamily() === 'analytic-table' && tableConfig(); as config) {\n @if (isTableLoading()) {\n <section class=\"analytics-presentation__state\" role=\"status\">{{ t('praxis.charts.analyticsPresentation.loading', 'Loading analytics\u2026') }}</section>\n } @else if (isTableEmpty()) {\n <section class=\"analytics-presentation__state\" role=\"status\">{{ t('praxis.charts.analyticsPresentation.empty', 'No data available.') }}</section>\n } @else {\n <praxis-table\n [config]=\"config\"\n [data]=\"tableRows()\"\n [tableId]=\"projectionId() + '-analytic-table'\"\n (rowClick)=\"handleTableRowClick($event)\"\n />\n }\n }\n</section>\n", styles: [":host{display:block;min-width:0}.analytics-presentation{display:grid;gap:12px;min-width:0}.analytics-presentation__toolbar{display:flex;flex-wrap:wrap;justify-content:flex-end;gap:4px}.analytics-presentation__toolbar button{min-height:40px;padding:0 14px;border:1px solid var(--md-sys-color-outline-variant, #c4c7c5);border-radius:999px;background:var(--md-sys-color-surface, #fff);color:var(--md-sys-color-on-surface, #1a1c1e);font:inherit;cursor:pointer}.analytics-presentation__toolbar button[aria-pressed=true]{border-color:var(--md-sys-color-primary, #1263b4);background:var(--md-sys-color-primary-container, #d7e3ff);color:var(--md-sys-color-on-primary-container, #001b3e)}.analytics-presentation__toolbar button:focus-visible{outline:3px solid color-mix(in srgb,var(--md-sys-color-primary, #1263b4) 45%,transparent);outline-offset:2px}.analytics-presentation__state{min-height:220px;display:grid;place-content:center;gap:6px;padding:24px;border:1px solid var(--md-sys-color-outline-variant, #c4c7c5);border-radius:16px;background:var(--md-sys-color-surface-container-low, #f7f8fa);color:var(--md-sys-color-on-surface, #1a1c1e);text-align:center}.analytics-presentation__state--error{border-color:var(--md-sys-color-error, #ba1a1a);color:var(--md-sys-color-error, #ba1a1a)}@media(max-width:600px){.analytics-presentation__toolbar{justify-content:stretch}.analytics-presentation__toolbar button{flex:1 1 0}}\n"], dependencies: [{ kind: "component", type: PraxisChartComponent, selector: "praxis-chart", inputs: ["config", "data", "chartDocument", "filterCriteria", "queryContext", "remoteDataResolver", "enableCustomization", "availableResources", "availableFields", "availableTargets"], outputs: ["pointClick", "pointAction", "selectionChange", "drillDown", "crossFilter", "queryRequest", "loadStateChange", "chartDocumentApplied", "chartDocumentSaved"] }, { kind: "component", type: PraxisTable, selector: "praxis-table", inputs: ["config", "resourcePath", "data", "tableId", "componentInstanceId", "configPersistenceStrategy", "title", "subtitle", "icon", "autoDelete", "notifyIfOutdated", "snoozeMs", "autoOpenSettingsOnOutdated", "crudContext", "filterCriteria", "queryContext", "aiContext", "aiAssistantVoiceInputMode", "aiAssistantVoiceLanguage", "horizontalScroll", "enableCustomization", "authoringCapability", "dense"], outputs: ["rowClick", "widgetEvent", "resourceEvent", "rowDoubleClick", "rowExpansionChange", "rowAction", "toolbarAction", "bulkAction", "exportAction", "columnReorder", "columnReorderAttempt", "columnResize", "beforeDelete", "afterDelete", "deleteError", "beforeBulkDelete", "afterBulkDelete", "bulkDeleteError", "schemaStatusChange", "configChange", "metadataChange", "loadingStateChange", "collectionLinksChange", "selectionChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
4549
|
+
}
|
|
4550
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: PraxisAnalyticsPresentationComponent, decorators: [{
|
|
4551
|
+
type: Component,
|
|
4552
|
+
args: [{ selector: 'praxis-analytics-presentation', standalone: true, imports: [PraxisChartComponent, PraxisTable], changeDetection: ChangeDetectionStrategy.OnPush, template: "<section class=\"analytics-presentation\" [attr.aria-label]=\"projectionId()\">\n <div class=\"analytics-presentation__toolbar\" role=\"group\" [attr.aria-label]=\"t('praxis.charts.analyticsPresentation.selectorLabel', 'Analytics presentation')\">\n @if (eligibleFamilies().includes('chart')) {\n <button type=\"button\" [attr.aria-pressed]=\"activeFamily() === 'chart'\" (click)=\"selectFamily('chart')\">\n {{ t('praxis.charts.analyticsPresentation.chart', 'Chart') }}\n </button>\n }\n @if (eligibleFamilies().includes('analytic-table')) {\n <button type=\"button\" [attr.aria-pressed]=\"activeFamily() === 'analytic-table'\" (click)=\"selectFamily('analytic-table')\">\n {{ t('praxis.charts.analyticsPresentation.table', 'Table') }}\n </button>\n }\n </div>\n\n @if (diagnostic(); as message) {\n <section class=\"analytics-presentation__state analytics-presentation__state--error\" role=\"alert\">\n <strong>{{ t('praxis.charts.analyticsPresentation.unavailable', 'Presentation unavailable') }}</strong>\n <span>{{ message }}</span>\n </section>\n } @else if (activeFamily() === 'chart' && chartConfig(); as config) {\n <praxis-chart\n [config]=\"config\"\n [queryContext]=\"queryContext()\"\n [enableCustomization]=\"enableCustomization()\"\n (pointClick)=\"pointClick.emit($event)\"\n (pointAction)=\"pointAction.emit($event)\"\n (selectionChange)=\"selectionChange.emit($event)\"\n (drillDown)=\"drillDown.emit($event)\"\n (crossFilter)=\"crossFilter.emit($event)\"\n (queryRequest)=\"queryRequest.emit($event)\"\n (loadStateChange)=\"loadStateChange.emit($event)\"\n />\n } @else if (activeFamily() === 'analytic-table' && tableConfig(); as config) {\n @if (isTableLoading()) {\n <section class=\"analytics-presentation__state\" role=\"status\">{{ t('praxis.charts.analyticsPresentation.loading', 'Loading analytics\u2026') }}</section>\n } @else if (isTableEmpty()) {\n <section class=\"analytics-presentation__state\" role=\"status\">{{ t('praxis.charts.analyticsPresentation.empty', 'No data available.') }}</section>\n } @else {\n <praxis-table\n [config]=\"config\"\n [data]=\"tableRows()\"\n [tableId]=\"projectionId() + '-analytic-table'\"\n (rowClick)=\"handleTableRowClick($event)\"\n />\n }\n }\n</section>\n", styles: [":host{display:block;min-width:0}.analytics-presentation{display:grid;gap:12px;min-width:0}.analytics-presentation__toolbar{display:flex;flex-wrap:wrap;justify-content:flex-end;gap:4px}.analytics-presentation__toolbar button{min-height:40px;padding:0 14px;border:1px solid var(--md-sys-color-outline-variant, #c4c7c5);border-radius:999px;background:var(--md-sys-color-surface, #fff);color:var(--md-sys-color-on-surface, #1a1c1e);font:inherit;cursor:pointer}.analytics-presentation__toolbar button[aria-pressed=true]{border-color:var(--md-sys-color-primary, #1263b4);background:var(--md-sys-color-primary-container, #d7e3ff);color:var(--md-sys-color-on-primary-container, #001b3e)}.analytics-presentation__toolbar button:focus-visible{outline:3px solid color-mix(in srgb,var(--md-sys-color-primary, #1263b4) 45%,transparent);outline-offset:2px}.analytics-presentation__state{min-height:220px;display:grid;place-content:center;gap:6px;padding:24px;border:1px solid var(--md-sys-color-outline-variant, #c4c7c5);border-radius:16px;background:var(--md-sys-color-surface-container-low, #f7f8fa);color:var(--md-sys-color-on-surface, #1a1c1e);text-align:center}.analytics-presentation__state--error{border-color:var(--md-sys-color-error, #ba1a1a);color:var(--md-sys-color-error, #ba1a1a)}@media(max-width:600px){.analytics-presentation__toolbar{justify-content:stretch}.analytics-presentation__toolbar button{flex:1 1 0}}\n"] }]
|
|
4553
|
+
}], ctorParameters: () => [], propDecorators: { analytics: [{ type: i0.Input, args: [{ isSignal: true, alias: "analytics", required: true }] }], projectionId: [{ type: i0.Input, args: [{ isSignal: true, alias: "projectionId", required: true }] }], availableFamilies: [{ type: i0.Input, args: [{ isSignal: true, alias: "availableFamilies", required: false }] }], preferredFamily: [{ type: i0.Input, args: [{ isSignal: true, alias: "preferredFamily", required: false }] }], queryContext: [{ type: i0.Input, args: [{ isSignal: true, alias: "queryContext", required: false }] }], title: [{ type: i0.Input, args: [{ isSignal: true, alias: "title", required: false }] }], subtitle: [{ type: i0.Input, args: [{ isSignal: true, alias: "subtitle", required: false }] }], enableCustomization: [{ type: i0.Input, args: [{ isSignal: true, alias: "enableCustomization", required: false }] }], presentationChange: [{ type: i0.Output, args: ["presentationChange"] }], pointClick: [{ type: i0.Output, args: ["pointClick"] }], pointAction: [{ type: i0.Output, args: ["pointAction"] }], selectionChange: [{ type: i0.Output, args: ["selectionChange"] }], drillDown: [{ type: i0.Output, args: ["drillDown"] }], crossFilter: [{ type: i0.Output, args: ["crossFilter"] }], queryRequest: [{ type: i0.Output, args: ["queryRequest"] }], loadStateChange: [{ type: i0.Output, args: ["loadStateChange"] }] } });
|
|
4554
|
+
function normalizeError$1(error) {
|
|
4555
|
+
return error instanceof Error && error.message.trim() ? error.message.trim() : 'Analytics presentation failed.';
|
|
4556
|
+
}
|
|
4557
|
+
function stringifyValue(value) {
|
|
4558
|
+
return value === null || value === undefined ? undefined : String(value);
|
|
4559
|
+
}
|
|
4560
|
+
|
|
4561
|
+
const PRAXIS_CHART_DEFAULT_PALETTE = ['#1263b4', '#0f766e', '#f08c00', '#c92a2a', '#7b61ff'];
|
|
4562
|
+
const TITLE_TOP = 18;
|
|
4563
|
+
const TITLE_LEFT = 24;
|
|
4564
|
+
const CARTESIAN_GRID_TOP_WITH_TITLE = 96;
|
|
4565
|
+
const CARTESIAN_GRID_TOP_WITHOUT_TITLE = 48;
|
|
4566
|
+
const CARTESIAN_GRID_BOTTOM_WITH_LEGEND = 64;
|
|
4567
|
+
const CARTESIAN_GRID_BOTTOM_WITHOUT_LEGEND = 40;
|
|
4568
|
+
const DEFAULT_VALUE_LOCALE = 'pt-BR';
|
|
4569
|
+
class EChartsOptionBuilderService {
|
|
4570
|
+
transformer;
|
|
4571
|
+
constructor(transformer) {
|
|
4572
|
+
this.transformer = transformer;
|
|
4573
|
+
}
|
|
4574
|
+
build(config, rows) {
|
|
4575
|
+
const transformed = this.transformer.transform(config, rows);
|
|
4576
|
+
const palette = config.theme?.palette?.length ? config.theme.palette : PRAXIS_CHART_DEFAULT_PALETTE;
|
|
4577
|
+
const tooltipEnabled = config.theme?.tooltip?.enabled ?? true;
|
|
4578
|
+
const legendVisible = config.theme?.legend?.visible ?? true;
|
|
4579
|
+
const textColor = this.resolveTextColor(config);
|
|
4580
|
+
if (transformed.mode === 'pie') {
|
|
4581
|
+
const hasChartTitle = this.hasText(config.title) || this.hasText(config.subtitle);
|
|
4582
|
+
const labelsVisible = config.series[0]?.labels?.visible ?? transformed.slices.length <= 4;
|
|
4583
|
+
const pieSeries = {
|
|
4584
|
+
type: 'pie',
|
|
4585
|
+
radius: config.type === 'donut'
|
|
4586
|
+
? labelsVisible ? ['32%', '52%'] : ['42%', '64%']
|
|
4587
|
+
: labelsVisible ? '54%' : '66%',
|
|
4588
|
+
center: ['50%', hasChartTitle ? '54%' : '50%'],
|
|
4589
|
+
avoidLabelOverlap: true,
|
|
4590
|
+
label: {
|
|
4591
|
+
show: labelsVisible,
|
|
4592
|
+
color: textColor,
|
|
4593
|
+
overflow: 'truncate',
|
|
4594
|
+
width: 120,
|
|
4595
|
+
formatter: labelsVisible
|
|
4596
|
+
? (params) => {
|
|
4597
|
+
const formatted = this.formatValue(params.value, config.series[0]?.labels?.format);
|
|
4598
|
+
return params.name ? `${params.name}: ${formatted}` : formatted;
|
|
4599
|
+
}
|
|
4600
|
+
: undefined,
|
|
4601
|
+
},
|
|
4602
|
+
labelLine: labelsVisible ? { length: 14, length2: 12, lineStyle: { color: textColor } } : undefined,
|
|
4603
|
+
data: transformed.slices.map((slice) => ({
|
|
4604
|
+
...(slice.data || {}),
|
|
4605
|
+
name: slice.name,
|
|
4606
|
+
value: slice.value,
|
|
4607
|
+
itemStyle: slice.color ? { color: slice.color } : undefined,
|
|
4608
|
+
})),
|
|
4609
|
+
};
|
|
4610
|
+
return {
|
|
4611
|
+
backgroundColor: config.theme?.backgroundColor,
|
|
4612
|
+
color: palette,
|
|
4613
|
+
title: this.buildTitle(config, 'center', textColor),
|
|
4614
|
+
tooltip: tooltipEnabled
|
|
4615
|
+
? {
|
|
4616
|
+
trigger: 'item',
|
|
4617
|
+
valueFormatter: (value) => this.formatValue(value, config.series[0]?.labels?.format),
|
|
4618
|
+
}
|
|
4619
|
+
: undefined,
|
|
4620
|
+
legend: this.buildLegend(config, legendVisible, 'bottom', textColor),
|
|
4621
|
+
series: [pieSeries],
|
|
4622
|
+
};
|
|
4623
|
+
}
|
|
4624
|
+
if (transformed.mode === 'funnel') {
|
|
4625
|
+
const labelsVisible = config.series[0]?.labels?.visible ?? true;
|
|
4626
|
+
const slices = config.type === 'pyramid'
|
|
4627
|
+
? [...transformed.slices].reverse()
|
|
4628
|
+
: transformed.slices;
|
|
4629
|
+
const funnelSeries = {
|
|
4630
|
+
type: 'funnel',
|
|
4631
|
+
sort: 'none',
|
|
4632
|
+
funnelAlign: 'center',
|
|
4633
|
+
left: '10%',
|
|
4634
|
+
top: this.hasText(config.title) || this.hasText(config.subtitle) ? 88 : 32,
|
|
4635
|
+
bottom: legendVisible ? 56 : 24,
|
|
4636
|
+
width: '80%',
|
|
4637
|
+
minSize: '0%',
|
|
4638
|
+
maxSize: '100%',
|
|
4639
|
+
gap: 2,
|
|
4640
|
+
label: {
|
|
4641
|
+
show: labelsVisible,
|
|
4642
|
+
color: textColor,
|
|
4643
|
+
overflow: 'truncate',
|
|
4644
|
+
width: 160,
|
|
4645
|
+
formatter: labelsVisible
|
|
4646
|
+
? (params) => {
|
|
4647
|
+
const formatted = this.formatValue(params.value, config.series[0]?.labels?.format);
|
|
4648
|
+
return params.name ? `${params.name}: ${formatted}` : formatted;
|
|
4649
|
+
}
|
|
4650
|
+
: undefined,
|
|
4651
|
+
},
|
|
4652
|
+
labelLine: labelsVisible ? { length: 12, lineStyle: { color: textColor } } : undefined,
|
|
4653
|
+
data: slices.map((slice) => ({
|
|
4654
|
+
...(slice.data || {}),
|
|
4655
|
+
name: slice.name,
|
|
4656
|
+
value: slice.value,
|
|
4657
|
+
itemStyle: slice.color ? { color: slice.color } : undefined,
|
|
4658
|
+
})),
|
|
4659
|
+
};
|
|
4660
|
+
return {
|
|
4661
|
+
backgroundColor: config.theme?.backgroundColor,
|
|
4662
|
+
color: palette,
|
|
4663
|
+
title: this.buildTitle(config, 'center', textColor),
|
|
4664
|
+
tooltip: tooltipEnabled
|
|
4665
|
+
? {
|
|
4666
|
+
trigger: 'item',
|
|
4667
|
+
valueFormatter: (value) => this.formatValue(value, config.series[0]?.labels?.format),
|
|
4668
|
+
}
|
|
4669
|
+
: undefined,
|
|
4670
|
+
legend: this.buildLegend(config, legendVisible, 'bottom', textColor),
|
|
4671
|
+
series: [funnelSeries],
|
|
4672
|
+
};
|
|
4673
|
+
}
|
|
4674
|
+
if (transformed.mode === 'scatter') {
|
|
4675
|
+
const scatterSeries = transformed.series.map((series) => ({
|
|
4676
|
+
id: series.id,
|
|
4677
|
+
name: series.name,
|
|
4678
|
+
type: 'scatter',
|
|
4679
|
+
label: {
|
|
4680
|
+
show: series.labelsVisible,
|
|
4681
|
+
color: textColor,
|
|
4682
|
+
formatter: series.labelsVisible
|
|
4683
|
+
? (params) => this.formatValue(params.value, this.seriesLabelFormat(config, series.id))
|
|
4684
|
+
: undefined,
|
|
4685
|
+
},
|
|
4686
|
+
itemStyle: series.color ? { color: series.color } : undefined,
|
|
4687
|
+
data: series.points,
|
|
4688
|
+
}));
|
|
4689
|
+
return {
|
|
4690
|
+
backgroundColor: config.theme?.backgroundColor,
|
|
4691
|
+
color: palette,
|
|
4692
|
+
title: this.buildTitle(config, 'left', textColor),
|
|
4693
|
+
tooltip: tooltipEnabled
|
|
4694
|
+
? {
|
|
4695
|
+
trigger: 'item',
|
|
4696
|
+
valueFormatter: (value) => this.formatValue(value, config.axes?.y?.labels?.format),
|
|
4697
|
+
}
|
|
4698
|
+
: undefined,
|
|
4699
|
+
legend: this.buildLegend(config, legendVisible, 'bottom', textColor),
|
|
4700
|
+
grid: this.buildGrid(config, { legendVisible }),
|
|
4701
|
+
xAxis: {
|
|
4702
|
+
type: config.axes?.x?.type ?? 'value',
|
|
4703
|
+
name: config.axes?.x?.label,
|
|
4704
|
+
nameLocation: 'middle',
|
|
4705
|
+
nameGap: 32,
|
|
4706
|
+
axisLabel: {
|
|
4707
|
+
show: config.axes?.x?.labels?.visible ?? true,
|
|
4708
|
+
color: textColor,
|
|
4709
|
+
rotate: config.axes?.x?.labels?.rotate ?? 0,
|
|
4710
|
+
hideOverlap: true,
|
|
4711
|
+
formatter: this.axisLabelFormatter(config.axes?.x?.labels?.format),
|
|
4712
|
+
},
|
|
4713
|
+
},
|
|
4714
|
+
yAxis: {
|
|
4715
|
+
type: config.axes?.y?.type ?? 'value',
|
|
4716
|
+
name: config.axes?.y?.label,
|
|
4717
|
+
nameLocation: 'middle',
|
|
4718
|
+
nameGap: 40,
|
|
4719
|
+
min: config.axes?.y?.min,
|
|
4720
|
+
max: config.axes?.y?.max,
|
|
4721
|
+
axisLabel: {
|
|
4722
|
+
show: config.axes?.y?.labels?.visible ?? true,
|
|
4723
|
+
color: textColor,
|
|
4724
|
+
hideOverlap: true,
|
|
4725
|
+
formatter: this.axisLabelFormatter(config.axes?.y?.labels?.format),
|
|
4726
|
+
},
|
|
4727
|
+
},
|
|
4728
|
+
series: scatterSeries,
|
|
4729
|
+
};
|
|
4730
|
+
}
|
|
4731
|
+
const horizontal = config.orientation === 'horizontal' || config.type === 'horizontal-bar';
|
|
4732
|
+
const xAxisType = this.usesCanonicalStatsTime(config)
|
|
4733
|
+
? 'time'
|
|
4734
|
+
: config.axes?.x?.type ?? 'category';
|
|
4735
|
+
return {
|
|
4736
|
+
backgroundColor: config.theme?.backgroundColor,
|
|
4737
|
+
color: palette,
|
|
4738
|
+
title: this.buildTitle(config, 'left', textColor),
|
|
4739
|
+
tooltip: tooltipEnabled ? this.buildCartesianTooltip(config) : undefined,
|
|
4740
|
+
legend: this.buildLegend(config, legendVisible, 'bottom', textColor),
|
|
4741
|
+
grid: this.buildGrid(config, { horizontal, legendVisible }),
|
|
4742
|
+
xAxis: horizontal
|
|
4743
|
+
? {
|
|
4744
|
+
type: config.axes?.y?.type ?? 'value',
|
|
4745
|
+
name: config.axes?.y?.label,
|
|
4746
|
+
nameLocation: 'middle',
|
|
4747
|
+
nameGap: 32,
|
|
4748
|
+
min: config.axes?.y?.min,
|
|
4749
|
+
max: config.axes?.y?.max,
|
|
4750
|
+
axisLabel: {
|
|
4751
|
+
show: config.axes?.y?.labels?.visible ?? true,
|
|
4752
|
+
color: textColor,
|
|
4753
|
+
hideOverlap: true,
|
|
4754
|
+
formatter: this.axisLabelFormatter(config.axes?.y?.labels?.format),
|
|
4755
|
+
},
|
|
4756
|
+
}
|
|
4757
|
+
: {
|
|
4758
|
+
type: xAxisType,
|
|
4759
|
+
name: config.axes?.x?.label,
|
|
4760
|
+
data: xAxisType === 'time' ? undefined : transformed.categories,
|
|
4761
|
+
nameLocation: 'middle',
|
|
4762
|
+
nameGap: 18,
|
|
4763
|
+
axisLabel: {
|
|
4764
|
+
show: config.axes?.x?.labels?.visible ?? true,
|
|
4765
|
+
color: textColor,
|
|
4766
|
+
rotate: config.axes?.x?.labels?.rotate ?? 0,
|
|
4767
|
+
hideOverlap: true,
|
|
4768
|
+
formatter: this.axisLabelFormatter(config.axes?.x?.labels?.format),
|
|
4769
|
+
},
|
|
4770
|
+
},
|
|
4771
|
+
yAxis: horizontal
|
|
4772
|
+
? {
|
|
4773
|
+
type: config.axes?.x?.type ?? 'category',
|
|
4774
|
+
name: config.axes?.x?.label,
|
|
4775
|
+
data: transformed.categories,
|
|
4776
|
+
axisLabel: {
|
|
4777
|
+
show: config.axes?.x?.labels?.visible ?? true,
|
|
4778
|
+
color: textColor,
|
|
4779
|
+
rotate: config.axes?.x?.labels?.rotate ?? 0,
|
|
4780
|
+
hideOverlap: true,
|
|
4781
|
+
overflow: 'truncate',
|
|
4782
|
+
width: 160,
|
|
4783
|
+
formatter: this.axisLabelFormatter(config.axes?.x?.labels?.format),
|
|
4784
|
+
},
|
|
4785
|
+
}
|
|
4786
|
+
: this.buildCartesianYAxis(config),
|
|
4787
|
+
series: transformed.series.map((series) => ({
|
|
4788
|
+
id: series.id,
|
|
4789
|
+
name: series.name,
|
|
4790
|
+
type: series.type,
|
|
4791
|
+
yAxisIndex: series.axis === 'secondary' ? 1 : 0,
|
|
4792
|
+
stack: series.stack,
|
|
4793
|
+
smooth: series.smooth,
|
|
4794
|
+
areaStyle: series.area ? {} : undefined,
|
|
4795
|
+
label: {
|
|
4796
|
+
show: series.labelsVisible,
|
|
4797
|
+
color: textColor,
|
|
4798
|
+
formatter: series.labelsVisible
|
|
4799
|
+
? (params) => this.formatValue(params.value, this.seriesLabelFormat(config, series.id))
|
|
4800
|
+
: undefined,
|
|
4801
|
+
},
|
|
4802
|
+
itemStyle: series.color ? { color: series.color } : undefined,
|
|
4803
|
+
data: series.points,
|
|
4804
|
+
})),
|
|
4805
|
+
};
|
|
4806
|
+
}
|
|
4807
|
+
resolveText(value) {
|
|
4808
|
+
if (!value)
|
|
4809
|
+
return undefined;
|
|
4810
|
+
if (typeof value === 'string')
|
|
4811
|
+
return value;
|
|
4812
|
+
if (typeof value === 'object' && value && 'text' in value) {
|
|
4813
|
+
const text = value.text;
|
|
4814
|
+
return text || undefined;
|
|
4815
|
+
}
|
|
4816
|
+
if (typeof value === 'object' && value && 'fallback' in value) {
|
|
4817
|
+
const fallback = value.fallback;
|
|
4818
|
+
return fallback || undefined;
|
|
4819
|
+
}
|
|
4820
|
+
return undefined;
|
|
4821
|
+
}
|
|
4822
|
+
resolveTextColor(config) {
|
|
4823
|
+
return config.theme?.textColor ?? '#4b5563';
|
|
4824
|
+
}
|
|
4825
|
+
hasText(value) {
|
|
4826
|
+
const text = this.resolveText(value);
|
|
4827
|
+
return !!text && text.trim().length > 0;
|
|
4828
|
+
}
|
|
4829
|
+
buildTitle(config, align, textColor = this.resolveTextColor(config)) {
|
|
4830
|
+
const title = this.resolveText(config.title);
|
|
4831
|
+
const subtitle = this.resolveText(config.subtitle);
|
|
4832
|
+
const hasTitle = !!title || !!subtitle;
|
|
4833
|
+
return {
|
|
4834
|
+
show: hasTitle,
|
|
4835
|
+
text: title,
|
|
4836
|
+
subtext: subtitle,
|
|
4837
|
+
top: TITLE_TOP,
|
|
4838
|
+
left: align === 'center' ? 'center' : TITLE_LEFT,
|
|
4839
|
+
right: TITLE_LEFT,
|
|
4840
|
+
textStyle: {
|
|
4841
|
+
fontSize: 18,
|
|
4842
|
+
fontWeight: 600,
|
|
4843
|
+
lineHeight: 24,
|
|
4844
|
+
color: textColor,
|
|
4845
|
+
overflow: 'truncate',
|
|
4846
|
+
},
|
|
4847
|
+
subtextStyle: {
|
|
4848
|
+
fontSize: 12,
|
|
4849
|
+
lineHeight: 18,
|
|
4850
|
+
color: textColor,
|
|
4851
|
+
overflow: 'truncate',
|
|
4852
|
+
},
|
|
4853
|
+
};
|
|
4854
|
+
}
|
|
4855
|
+
buildLegend(config, visible, fallbackPosition, textColor = this.resolveTextColor(config)) {
|
|
4856
|
+
if (!visible) {
|
|
4857
|
+
return { show: false };
|
|
4858
|
+
}
|
|
4859
|
+
const position = config.theme?.legend?.position ?? fallbackPosition;
|
|
4860
|
+
const base = {
|
|
4861
|
+
show: true,
|
|
4862
|
+
type: 'scroll',
|
|
4863
|
+
pageIconSize: 10,
|
|
4864
|
+
itemWidth: 18,
|
|
4865
|
+
itemHeight: 10,
|
|
4866
|
+
textStyle: {
|
|
4867
|
+
color: textColor,
|
|
4868
|
+
},
|
|
4869
|
+
formatter: (name) => truncateLegendText(name),
|
|
4870
|
+
};
|
|
4871
|
+
if (position === 'top') {
|
|
4872
|
+
return {
|
|
4873
|
+
...base,
|
|
4874
|
+
orient: 'horizontal',
|
|
4875
|
+
top: this.hasText(config.title) || this.hasText(config.subtitle) ? 70 : 12,
|
|
4876
|
+
left: 'center',
|
|
4877
|
+
right: TITLE_LEFT,
|
|
4878
|
+
};
|
|
4879
|
+
}
|
|
4880
|
+
if (position === 'left' || position === 'right') {
|
|
4881
|
+
return {
|
|
4882
|
+
...base,
|
|
4883
|
+
orient: 'vertical',
|
|
4884
|
+
top: 88,
|
|
4885
|
+
bottom: 28,
|
|
4886
|
+
[position]: 10,
|
|
4887
|
+
};
|
|
4888
|
+
}
|
|
4889
|
+
return {
|
|
4890
|
+
...base,
|
|
4891
|
+
orient: 'horizontal',
|
|
4892
|
+
bottom: 16,
|
|
4893
|
+
left: TITLE_LEFT,
|
|
4894
|
+
right: TITLE_LEFT,
|
|
4895
|
+
};
|
|
4896
|
+
}
|
|
4897
|
+
buildGrid(config, options = {}) {
|
|
4898
|
+
const hasChartTitle = this.hasText(config.title) || this.hasText(config.subtitle);
|
|
4899
|
+
const legendPosition = config.theme?.legend?.position ?? 'bottom';
|
|
4900
|
+
const sideLegend = options.legendVisible && (legendPosition === 'left' || legendPosition === 'right');
|
|
4901
|
+
return {
|
|
4902
|
+
top: hasChartTitle ? CARTESIAN_GRID_TOP_WITH_TITLE : CARTESIAN_GRID_TOP_WITHOUT_TITLE,
|
|
4903
|
+
right: sideLegend && legendPosition === 'right' ? 160 : 32,
|
|
4904
|
+
bottom: options.legendVisible && legendPosition === 'bottom'
|
|
4905
|
+
? CARTESIAN_GRID_BOTTOM_WITH_LEGEND
|
|
4906
|
+
: CARTESIAN_GRID_BOTTOM_WITHOUT_LEGEND,
|
|
4907
|
+
left: sideLegend && legendPosition === 'left'
|
|
4908
|
+
? 168
|
|
4909
|
+
: options.horizontal ? 136 : 56,
|
|
4910
|
+
// ECharts deprecou containLabel; esta combinacao preserva o mesmo comportamento
|
|
4911
|
+
// sem depender da feature legacy de grid.
|
|
4912
|
+
outerBoundsMode: 'same',
|
|
4913
|
+
outerBoundsContain: 'axisLabel',
|
|
4914
|
+
};
|
|
4915
|
+
}
|
|
4916
|
+
buildCartesianYAxis(config) {
|
|
4917
|
+
const textColor = this.resolveTextColor(config);
|
|
4918
|
+
const primaryAxis = {
|
|
4919
|
+
type: config.axes?.y?.type ?? 'value',
|
|
4920
|
+
name: config.axes?.y?.label,
|
|
4921
|
+
nameLocation: 'middle',
|
|
4922
|
+
nameGap: 40,
|
|
4923
|
+
min: config.axes?.y?.min,
|
|
4924
|
+
max: config.axes?.y?.max,
|
|
4925
|
+
axisLabel: {
|
|
4926
|
+
show: config.axes?.y?.labels?.visible ?? true,
|
|
4927
|
+
color: textColor,
|
|
4928
|
+
hideOverlap: true,
|
|
4929
|
+
formatter: this.axisLabelFormatter(config.axes?.y?.labels?.format),
|
|
4930
|
+
},
|
|
4931
|
+
};
|
|
4932
|
+
if (!config.axes?.ySecondary) {
|
|
4933
|
+
return primaryAxis;
|
|
4934
|
+
}
|
|
4935
|
+
return [
|
|
4936
|
+
primaryAxis,
|
|
4937
|
+
{
|
|
4938
|
+
type: config.axes.ySecondary.type ?? 'value',
|
|
4939
|
+
name: config.axes.ySecondary.label,
|
|
4940
|
+
nameLocation: 'middle',
|
|
4941
|
+
nameGap: 40,
|
|
4942
|
+
min: config.axes.ySecondary.min,
|
|
4943
|
+
max: config.axes.ySecondary.max,
|
|
4944
|
+
position: config.axes.ySecondary.position ?? 'right',
|
|
4945
|
+
axisLabel: {
|
|
4946
|
+
show: config.axes.ySecondary.labels?.visible ?? true,
|
|
4947
|
+
color: textColor,
|
|
4948
|
+
hideOverlap: true,
|
|
4949
|
+
formatter: this.axisLabelFormatter(config.axes.ySecondary.labels?.format),
|
|
4950
|
+
},
|
|
4951
|
+
},
|
|
4952
|
+
];
|
|
4953
|
+
}
|
|
4954
|
+
primaryValueFormat(config) {
|
|
4955
|
+
return config.series.find((series) => series.axis !== 'secondary')?.labels?.format
|
|
4956
|
+
?? config.axes?.y?.labels?.format
|
|
4957
|
+
?? config.series[0]?.labels?.format;
|
|
4958
|
+
}
|
|
4959
|
+
buildCartesianTooltip(config) {
|
|
4960
|
+
const tooltip = {
|
|
4961
|
+
trigger: config.theme?.tooltip?.trigger ?? 'axis',
|
|
4962
|
+
confine: true,
|
|
4963
|
+
appendToBody: false,
|
|
4964
|
+
valueFormatter: (value) => this.formatValue(value, this.primaryValueFormat(config)),
|
|
4965
|
+
};
|
|
4966
|
+
return this.usesCanonicalStatsTime(config)
|
|
4967
|
+
? {
|
|
4968
|
+
...tooltip,
|
|
4969
|
+
formatter: (params) => this.formatCanonicalTimeTooltip(params, config),
|
|
4970
|
+
}
|
|
4971
|
+
: tooltip;
|
|
4972
|
+
}
|
|
4973
|
+
usesCanonicalStatsTime(config) {
|
|
4974
|
+
if (config.dataSource?.kind !== 'remote'
|
|
4975
|
+
|| config.dataSource.query?.sourceKind !== 'praxis.stats') {
|
|
4976
|
+
return false;
|
|
4977
|
+
}
|
|
4978
|
+
return config.axes?.x?.type === 'time'
|
|
4979
|
+
|| config.dataSource.query.statsOperation === 'timeseries';
|
|
4980
|
+
}
|
|
4981
|
+
formatCanonicalTimeTooltip(params, config) {
|
|
4982
|
+
const items = (Array.isArray(params) ? params : [params])
|
|
4983
|
+
.filter((item) => !!item && typeof item === 'object');
|
|
4984
|
+
if (!items.length) {
|
|
4985
|
+
return '';
|
|
4986
|
+
}
|
|
4987
|
+
const first = items[0];
|
|
4988
|
+
const data = this.tooltipPointData(first['data']);
|
|
4989
|
+
const header = this.formatCanonicalTimeTooltipHeader(first, data, config);
|
|
4990
|
+
const lines = items.map((item) => {
|
|
4991
|
+
const itemData = this.tooltipPointData(item['data']);
|
|
4992
|
+
const seriesId = item['seriesId'] === null || item['seriesId'] === undefined
|
|
4993
|
+
? undefined
|
|
4994
|
+
: String(item['seriesId']);
|
|
4995
|
+
const format = seriesId
|
|
4996
|
+
? this.seriesLabelFormat(config, seriesId) ?? this.primaryValueFormat(config)
|
|
4997
|
+
: this.primaryValueFormat(config);
|
|
4998
|
+
const value = this.formatValue(item['value'] ?? itemData['value'], format);
|
|
4999
|
+
const seriesName = item['seriesName'] === null || item['seriesName'] === undefined
|
|
5000
|
+
? ''
|
|
5001
|
+
: String(item['seriesName']);
|
|
5002
|
+
return seriesName
|
|
5003
|
+
? `${escapeTooltipHtml(seriesName)}: ${escapeTooltipHtml(value)}`
|
|
5004
|
+
: escapeTooltipHtml(value);
|
|
5005
|
+
});
|
|
5006
|
+
return [escapeTooltipHtml(header), ...lines]
|
|
5007
|
+
.filter((part) => part.length > 0)
|
|
5008
|
+
.join('<br/>');
|
|
5009
|
+
}
|
|
5010
|
+
formatCanonicalTimeTooltipHeader(params, data, config) {
|
|
5011
|
+
const dateFormat = config.axes?.x?.labels?.format;
|
|
5012
|
+
const label = this.nonEmptyText(data['label']);
|
|
5013
|
+
const start = this.nonEmptyText(data['start']);
|
|
5014
|
+
const end = this.nonEmptyText(data['end']);
|
|
5015
|
+
const formattedStart = start ? this.formatValue(start, dateFormat) : '';
|
|
5016
|
+
const formattedEnd = end ? this.formatValue(end, dateFormat) : '';
|
|
5017
|
+
const interval = formattedStart && formattedEnd && formattedStart !== formattedEnd
|
|
5018
|
+
? `${formattedStart} – ${formattedEnd}`
|
|
5019
|
+
: formattedStart || formattedEnd;
|
|
5020
|
+
const fallback = this.nonEmptyText(params['axisValueLabel'])
|
|
5021
|
+
?? this.nonEmptyText(params['name'])
|
|
5022
|
+
?? '';
|
|
5023
|
+
if (label && interval && label !== formattedStart && label !== interval) {
|
|
5024
|
+
return `${label} · ${interval}`;
|
|
5025
|
+
}
|
|
5026
|
+
return interval || label || fallback;
|
|
5027
|
+
}
|
|
5028
|
+
tooltipPointData(value) {
|
|
5029
|
+
return value && typeof value === 'object' && !Array.isArray(value)
|
|
5030
|
+
? value
|
|
5031
|
+
: {};
|
|
5032
|
+
}
|
|
5033
|
+
nonEmptyText(value) {
|
|
5034
|
+
if (value === null || value === undefined) {
|
|
5035
|
+
return undefined;
|
|
5036
|
+
}
|
|
5037
|
+
const text = String(value).trim();
|
|
5038
|
+
return text || undefined;
|
|
5039
|
+
}
|
|
5040
|
+
seriesLabelFormat(config, seriesId) {
|
|
5041
|
+
return config.series.find((series) => series.id === seriesId)?.labels?.format;
|
|
5042
|
+
}
|
|
5043
|
+
axisLabelFormatter(format) {
|
|
5044
|
+
return format ? (value) => this.formatValue(value, format) : undefined;
|
|
5045
|
+
}
|
|
5046
|
+
formatValue(value, format) {
|
|
5047
|
+
const normalizedValue = Array.isArray(value) ? value[value.length - 1] : value;
|
|
5048
|
+
if (normalizedValue === null || normalizedValue === undefined || normalizedValue === '') {
|
|
5049
|
+
return '';
|
|
5050
|
+
}
|
|
5051
|
+
if (!format) {
|
|
5052
|
+
return String(normalizedValue);
|
|
5053
|
+
}
|
|
5054
|
+
const formattedDate = this.formatDateValue(normalizedValue, format);
|
|
5055
|
+
if (formattedDate !== null) {
|
|
5056
|
+
return formattedDate;
|
|
5057
|
+
}
|
|
5058
|
+
const numeric = typeof normalizedValue === 'number'
|
|
5059
|
+
? normalizedValue
|
|
5060
|
+
: Number(String(normalizedValue).replace(',', '.'));
|
|
5061
|
+
if (!Number.isFinite(numeric)) {
|
|
5062
|
+
return String(normalizedValue);
|
|
5063
|
+
}
|
|
5064
|
+
const currency = this.parseCurrencyFormat(format);
|
|
5065
|
+
if (currency) {
|
|
5066
|
+
return new Intl.NumberFormat(DEFAULT_VALUE_LOCALE, {
|
|
5067
|
+
style: 'currency',
|
|
5068
|
+
currency: currency.code,
|
|
5069
|
+
currencyDisplay: currency.display,
|
|
5070
|
+
minimumFractionDigits: currency.decimals,
|
|
5071
|
+
maximumFractionDigits: currency.decimals,
|
|
5072
|
+
useGrouping: currency.useGrouping,
|
|
5073
|
+
}).format(numeric);
|
|
5074
|
+
}
|
|
5075
|
+
const number = this.parseNumberFormat(format);
|
|
5076
|
+
if (number) {
|
|
5077
|
+
return new Intl.NumberFormat(DEFAULT_VALUE_LOCALE, {
|
|
5078
|
+
minimumFractionDigits: number.minimumFractionDigits,
|
|
5079
|
+
maximumFractionDigits: number.maximumFractionDigits,
|
|
5080
|
+
useGrouping: number.useGrouping,
|
|
5081
|
+
}).format(numeric);
|
|
5082
|
+
}
|
|
5083
|
+
return String(normalizedValue);
|
|
5084
|
+
}
|
|
5085
|
+
formatDateValue(value, format) {
|
|
5086
|
+
if (!this.isDateFormat(format)) {
|
|
5087
|
+
return null;
|
|
5088
|
+
}
|
|
5089
|
+
const parts = this.readDateParts(value);
|
|
5090
|
+
if (!parts) {
|
|
5091
|
+
return null;
|
|
5092
|
+
}
|
|
5093
|
+
const day = String(parts.day);
|
|
5094
|
+
const day2 = day.padStart(2, '0');
|
|
5095
|
+
const month = String(parts.month);
|
|
5096
|
+
const month2 = month.padStart(2, '0');
|
|
5097
|
+
const year = String(parts.year);
|
|
5098
|
+
const year2 = year.slice(-2);
|
|
5099
|
+
const date = new Date(parts.year, parts.month - 1, parts.day);
|
|
5100
|
+
const monthShort = new Intl.DateTimeFormat(DEFAULT_VALUE_LOCALE, { month: 'short' }).format(date);
|
|
5101
|
+
const monthLong = new Intl.DateTimeFormat(DEFAULT_VALUE_LOCALE, { month: 'long' }).format(date);
|
|
5102
|
+
return format
|
|
5103
|
+
.replace(/yyyy/g, year)
|
|
5104
|
+
.replace(/yy/g, year2)
|
|
5105
|
+
.replace(/MMMM/g, monthLong)
|
|
5106
|
+
.replace(/MMM/g, monthShort)
|
|
5107
|
+
.replace(/MM/g, month2)
|
|
5108
|
+
.replace(/M/g, month)
|
|
5109
|
+
.replace(/dd/g, day2)
|
|
5110
|
+
.replace(/d/g, day);
|
|
5111
|
+
}
|
|
5112
|
+
isDateFormat(format) {
|
|
5113
|
+
return /(^|[^A-Za-z])(d{1,2}|M{1,4}|y{2,4})(?=$|[^A-Za-z])/.test(format);
|
|
5114
|
+
}
|
|
5115
|
+
readDateParts(value) {
|
|
5116
|
+
if (value instanceof Date) {
|
|
5117
|
+
return Number.isNaN(value.getTime())
|
|
5118
|
+
? null
|
|
5119
|
+
: { year: value.getFullYear(), month: value.getMonth() + 1, day: value.getDate() };
|
|
5120
|
+
}
|
|
5121
|
+
if (typeof value === 'number') {
|
|
5122
|
+
const date = new Date(value);
|
|
5123
|
+
return Number.isNaN(date.getTime())
|
|
5124
|
+
? null
|
|
5125
|
+
: { year: date.getFullYear(), month: date.getMonth() + 1, day: date.getDate() };
|
|
5126
|
+
}
|
|
5127
|
+
const raw = String(value).trim();
|
|
5128
|
+
const isoDate = /^(\d{4})-(\d{2})-(\d{2})(?:[T\s].*)?$/.exec(raw);
|
|
5129
|
+
if (isoDate) {
|
|
5130
|
+
return {
|
|
5131
|
+
year: Number(isoDate[1]),
|
|
5132
|
+
month: Number(isoDate[2]),
|
|
5133
|
+
day: Number(isoDate[3]),
|
|
5134
|
+
};
|
|
5135
|
+
}
|
|
5136
|
+
const brazilianDate = /^(\d{2})\/(\d{2})\/(\d{4})$/.exec(raw);
|
|
5137
|
+
if (brazilianDate) {
|
|
5138
|
+
return {
|
|
5139
|
+
year: Number(brazilianDate[3]),
|
|
5140
|
+
month: Number(brazilianDate[2]),
|
|
5141
|
+
day: Number(brazilianDate[1]),
|
|
5142
|
+
};
|
|
5143
|
+
}
|
|
5144
|
+
const parsed = new Date(raw);
|
|
5145
|
+
return Number.isNaN(parsed.getTime())
|
|
5146
|
+
? null
|
|
5147
|
+
: { year: parsed.getFullYear(), month: parsed.getMonth() + 1, day: parsed.getDate() };
|
|
5148
|
+
}
|
|
5149
|
+
parseCurrencyFormat(format) {
|
|
5150
|
+
const parts = format.split('|').map((part) => part.trim()).filter(Boolean);
|
|
5151
|
+
const code = parts[0]?.toUpperCase();
|
|
5152
|
+
if (!code || !/^[A-Z]{3}$/.test(code)) {
|
|
5153
|
+
return null;
|
|
5154
|
+
}
|
|
5155
|
+
const display = this.normalizeCurrencyDisplay(parts[1]);
|
|
5156
|
+
const decimals = this.clampDecimals(Number.parseInt(parts[2] ?? '2', 10), 2);
|
|
5157
|
+
return {
|
|
5158
|
+
code,
|
|
5159
|
+
display,
|
|
5160
|
+
decimals,
|
|
5161
|
+
useGrouping: !parts.some((part) => part.toLowerCase() === 'nosep'),
|
|
5162
|
+
};
|
|
5163
|
+
}
|
|
5164
|
+
normalizeCurrencyDisplay(value) {
|
|
5165
|
+
if (value === 'code' || value === 'name' || value === 'narrowSymbol') {
|
|
5166
|
+
return value;
|
|
5167
|
+
}
|
|
5168
|
+
return 'symbol';
|
|
5169
|
+
}
|
|
5170
|
+
parseNumberFormat(format) {
|
|
5171
|
+
const [pattern, ...modifiers] = format.split('|').map((part) => part.trim());
|
|
5172
|
+
const match = /^(\d+)\.(\d+)-(\d+)$/.exec(pattern);
|
|
5173
|
+
if (!match) {
|
|
5174
|
+
return null;
|
|
5175
|
+
}
|
|
5176
|
+
return {
|
|
5177
|
+
minimumFractionDigits: this.clampDecimals(Number.parseInt(match[2], 10), 0),
|
|
5178
|
+
maximumFractionDigits: this.clampDecimals(Number.parseInt(match[3], 10), 0),
|
|
5179
|
+
useGrouping: !modifiers.some((part) => part.toLowerCase() === 'nosep'),
|
|
5180
|
+
};
|
|
5181
|
+
}
|
|
5182
|
+
clampDecimals(value, fallback) {
|
|
5183
|
+
if (!Number.isFinite(value)) {
|
|
5184
|
+
return fallback;
|
|
5185
|
+
}
|
|
5186
|
+
return Math.min(Math.max(value, 0), 20);
|
|
5187
|
+
}
|
|
5188
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: EChartsOptionBuilderService, deps: [{ token: PraxisChartDataTransformerService }], target: i0.ɵɵFactoryTarget.Injectable });
|
|
5189
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: EChartsOptionBuilderService, providedIn: 'root' });
|
|
5190
|
+
}
|
|
5191
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: EChartsOptionBuilderService, decorators: [{
|
|
5192
|
+
type: Injectable,
|
|
5193
|
+
args: [{ providedIn: 'root' }]
|
|
5194
|
+
}], ctorParameters: () => [{ type: PraxisChartDataTransformerService }] });
|
|
5195
|
+
function truncateLegendText(name) {
|
|
5196
|
+
const value = String(name ?? '').trim();
|
|
5197
|
+
if (value.length <= 28) {
|
|
5198
|
+
return value;
|
|
5199
|
+
}
|
|
5200
|
+
return `${value.slice(0, 25)}...`;
|
|
5201
|
+
}
|
|
5202
|
+
function escapeTooltipHtml(value) {
|
|
5203
|
+
return String(value ?? '')
|
|
5204
|
+
.replace(/&/g, '&')
|
|
5205
|
+
.replace(/</g, '<')
|
|
5206
|
+
.replace(/>/g, '>')
|
|
5207
|
+
.replace(/"/g, '"')
|
|
5208
|
+
.replace(/'/g, ''');
|
|
5209
|
+
}
|
|
5210
|
+
|
|
5211
|
+
use([
|
|
5212
|
+
AriaComponent,
|
|
5213
|
+
BarChart,
|
|
5214
|
+
CanvasRenderer,
|
|
5215
|
+
DatasetComponent,
|
|
5216
|
+
FunnelChart,
|
|
5217
|
+
GridComponent,
|
|
5218
|
+
LegendComponent,
|
|
5219
|
+
LineChart,
|
|
5220
|
+
PieChart,
|
|
5221
|
+
ScatterChart,
|
|
5222
|
+
TitleComponent,
|
|
5223
|
+
TooltipComponent,
|
|
5224
|
+
TransformComponent,
|
|
5225
|
+
]);
|
|
5226
|
+
class EChartsEngineAdapter {
|
|
5227
|
+
optionBuilder;
|
|
5228
|
+
chart;
|
|
5229
|
+
currentHost;
|
|
5230
|
+
clickHost;
|
|
5231
|
+
domClickHandler;
|
|
5232
|
+
zrenderClickHandler;
|
|
5233
|
+
lastSeriesClickAt = 0;
|
|
5234
|
+
lastGridClick;
|
|
5235
|
+
constructor(optionBuilder) {
|
|
5236
|
+
this.optionBuilder = optionBuilder;
|
|
5237
|
+
}
|
|
5238
|
+
render(host, payload) {
|
|
5239
|
+
if (this.chart && this.currentHost !== host) {
|
|
5240
|
+
this.disposeChart();
|
|
4943
5241
|
}
|
|
4944
|
-
|
|
4945
|
-
|
|
4946
|
-
|
|
5242
|
+
if (!this.chart) {
|
|
5243
|
+
this.chart = this.createChart(host);
|
|
5244
|
+
this.currentHost = host;
|
|
5245
|
+
}
|
|
5246
|
+
const option = this.optionBuilder.build(payload.config, payload.data);
|
|
5247
|
+
const chart = this.chart;
|
|
5248
|
+
chart.setOption(option, true);
|
|
5249
|
+
this.detachHandlers();
|
|
5250
|
+
chart.off('click');
|
|
5251
|
+
chart.on('click', (params) => {
|
|
5252
|
+
this.lastSeriesClickAt = Date.now();
|
|
5253
|
+
this.lastGridClick = undefined;
|
|
5254
|
+
const data = pointData(params?.name, params?.data ?? params?.value);
|
|
5255
|
+
payload.onPointClick?.({
|
|
5256
|
+
chartId: payload.config.id,
|
|
5257
|
+
seriesId: params?.seriesId,
|
|
5258
|
+
seriesName: params?.seriesName,
|
|
5259
|
+
category: this.resolvePointCategory(payload, data, params?.name),
|
|
5260
|
+
value: pointValue(params?.value ?? params?.data),
|
|
5261
|
+
data,
|
|
5262
|
+
});
|
|
5263
|
+
});
|
|
5264
|
+
const zrender = chart?.getZr?.();
|
|
5265
|
+
this.zrenderClickHandler = (event) => {
|
|
5266
|
+
window.setTimeout(() => {
|
|
5267
|
+
if (Date.now() - this.lastSeriesClickAt < 80) {
|
|
5268
|
+
return;
|
|
5269
|
+
}
|
|
5270
|
+
this.emitCategoryClickFromGrid(event, payload);
|
|
5271
|
+
}, 0);
|
|
5272
|
+
};
|
|
5273
|
+
zrender?.on?.('click', this.zrenderClickHandler);
|
|
5274
|
+
this.domClickHandler = (event) => {
|
|
5275
|
+
window.setTimeout(() => {
|
|
5276
|
+
if (Date.now() - this.lastSeriesClickAt < 80) {
|
|
5277
|
+
return;
|
|
5278
|
+
}
|
|
5279
|
+
const rect = host.getBoundingClientRect();
|
|
5280
|
+
const offsetX = Number.isFinite(event.offsetX) ? event.offsetX : event.clientX - rect.left;
|
|
5281
|
+
const offsetY = Number.isFinite(event.offsetY) ? event.offsetY : event.clientY - rect.top;
|
|
5282
|
+
this.emitCategoryClickFromGrid({
|
|
5283
|
+
offsetX,
|
|
5284
|
+
offsetY,
|
|
5285
|
+
}, payload);
|
|
5286
|
+
}, 0);
|
|
4947
5287
|
};
|
|
5288
|
+
host.addEventListener('click', this.domClickHandler, true);
|
|
5289
|
+
this.clickHost = host;
|
|
4948
5290
|
}
|
|
4949
|
-
|
|
4950
|
-
|
|
4951
|
-
|
|
4952
|
-
|
|
4953
|
-
|
|
4954
|
-
|
|
4955
|
-
|
|
5291
|
+
resize() {
|
|
5292
|
+
this.chart?.resize();
|
|
5293
|
+
}
|
|
5294
|
+
destroy() {
|
|
5295
|
+
this.disposeChart();
|
|
5296
|
+
}
|
|
5297
|
+
createChart(host) {
|
|
5298
|
+
return init(host);
|
|
5299
|
+
}
|
|
5300
|
+
disposeChart() {
|
|
5301
|
+
this.detachHandlers();
|
|
5302
|
+
this.chart?.off('click');
|
|
5303
|
+
this.chart?.dispose();
|
|
5304
|
+
this.chart = undefined;
|
|
5305
|
+
this.currentHost = undefined;
|
|
5306
|
+
this.lastSeriesClickAt = 0;
|
|
5307
|
+
this.lastGridClick = undefined;
|
|
5308
|
+
}
|
|
5309
|
+
detachHandlers() {
|
|
5310
|
+
const zrender = this.chart?.getZr?.();
|
|
5311
|
+
if (zrender && this.zrenderClickHandler) {
|
|
5312
|
+
zrender.off?.('click', this.zrenderClickHandler);
|
|
5313
|
+
}
|
|
5314
|
+
if (this.clickHost && this.domClickHandler) {
|
|
5315
|
+
this.clickHost.removeEventListener('click', this.domClickHandler, true);
|
|
5316
|
+
}
|
|
5317
|
+
this.clickHost = undefined;
|
|
5318
|
+
this.domClickHandler = undefined;
|
|
5319
|
+
this.zrenderClickHandler = undefined;
|
|
5320
|
+
}
|
|
5321
|
+
resolvePointCategory(payload, data, fallback) {
|
|
5322
|
+
const categoryField = payload.config.axes?.x?.field
|
|
5323
|
+
?? payload.config.series.find((series) => series.categoryField)?.categoryField;
|
|
5324
|
+
const dataSource = payload.config.dataSource;
|
|
5325
|
+
const usesCanonicalStatsTime = dataSource?.kind === 'remote'
|
|
5326
|
+
&& dataSource.query?.sourceKind === 'praxis.stats';
|
|
5327
|
+
const usesCanonicalStatsTimePoint = usesCanonicalStatsTime
|
|
5328
|
+
&& (payload.config.axes?.x?.type === 'time'
|
|
5329
|
+
|| (dataSource?.kind === 'remote' && dataSource.query?.statsOperation === 'timeseries'));
|
|
5330
|
+
const candidate = usesCanonicalStatsTimePoint && data['label'] !== null
|
|
5331
|
+
&& data['label'] !== undefined
|
|
5332
|
+
&& data['label'] !== ''
|
|
5333
|
+
? data['label']
|
|
5334
|
+
: categoryField && data[categoryField] !== null && data[categoryField] !== undefined
|
|
5335
|
+
? data[categoryField]
|
|
5336
|
+
: fallback;
|
|
5337
|
+
return candidate === null || candidate === undefined
|
|
5338
|
+
? undefined
|
|
5339
|
+
: String(candidate);
|
|
5340
|
+
}
|
|
5341
|
+
emitCategoryClickFromGrid(event, payload) {
|
|
5342
|
+
const chart = this.chart;
|
|
5343
|
+
if (!chart || typeof chart.containPixel !== 'function' || typeof chart.convertFromPixel !== 'function') {
|
|
5344
|
+
return;
|
|
5345
|
+
}
|
|
5346
|
+
const point = [event?.offsetX, event?.offsetY];
|
|
5347
|
+
if (!Number.isFinite(point[0]) || !Number.isFinite(point[1])) {
|
|
5348
|
+
return;
|
|
5349
|
+
}
|
|
5350
|
+
if (!chart.containPixel({ gridIndex: 0 }, point)) {
|
|
5351
|
+
return;
|
|
5352
|
+
}
|
|
5353
|
+
const option = chart.getOption?.();
|
|
5354
|
+
const xAxis = firstOptionEntry(option?.xAxis);
|
|
5355
|
+
const yAxis = firstOptionEntry(option?.yAxis);
|
|
5356
|
+
const horizontal = yAxis?.type === 'category';
|
|
5357
|
+
const categories = (horizontal ? yAxis?.data : xAxis?.data) ?? [];
|
|
5358
|
+
if (!Array.isArray(categories) || categories.length === 0) {
|
|
5359
|
+
return;
|
|
5360
|
+
}
|
|
5361
|
+
const converted = chart.convertFromPixel({ gridIndex: 0 }, point);
|
|
5362
|
+
const categoryIndex = Math.round(Number(horizontal ? converted?.[1] : converted?.[0]));
|
|
5363
|
+
if (!Number.isInteger(categoryIndex) || categoryIndex < 0 || categoryIndex >= categories.length) {
|
|
5364
|
+
return;
|
|
5365
|
+
}
|
|
5366
|
+
const category = categories[categoryIndex];
|
|
5367
|
+
const categoryValue = category == null ? undefined : String(category);
|
|
5368
|
+
const categoryField = payload.config.axes?.x?.field
|
|
5369
|
+
?? payload.config.series.find((series) => series.categoryField)?.categoryField;
|
|
5370
|
+
const signature = JSON.stringify({
|
|
5371
|
+
chartId: payload.config.id,
|
|
5372
|
+
category: categoryValue,
|
|
5373
|
+
horizontal,
|
|
5374
|
+
categoryIndex,
|
|
5375
|
+
});
|
|
5376
|
+
if (this.isDuplicateGridClick(signature)) {
|
|
5377
|
+
return;
|
|
5378
|
+
}
|
|
5379
|
+
const usesCanonicalStatsRows = payload.config.dataSource?.kind === 'remote'
|
|
5380
|
+
&& payload.config.dataSource.query?.sourceKind === 'praxis.stats';
|
|
5381
|
+
const fallbackSourceRow = !usesCanonicalStatsRows
|
|
5382
|
+
&& categoryField
|
|
5383
|
+
&& categoryValue !== undefined
|
|
5384
|
+
? payload.data.find((row) => String(row[categoryField] ?? '') === categoryValue)
|
|
5385
|
+
: undefined;
|
|
5386
|
+
const sourceRow = usesCanonicalStatsRows
|
|
5387
|
+
? payload.data[categoryIndex]
|
|
5388
|
+
: fallbackSourceRow;
|
|
5389
|
+
const data = {
|
|
5390
|
+
...(sourceRow ?? {}),
|
|
5391
|
+
category: categoryValue,
|
|
5392
|
+
};
|
|
5393
|
+
if (categoryField && categoryValue !== undefined) {
|
|
5394
|
+
data[categoryField] = categoryValue;
|
|
5395
|
+
}
|
|
5396
|
+
payload.onPointClick?.({
|
|
5397
|
+
chartId: payload.config.id,
|
|
5398
|
+
category: categoryValue,
|
|
5399
|
+
data,
|
|
5400
|
+
});
|
|
5401
|
+
}
|
|
5402
|
+
isDuplicateGridClick(signature) {
|
|
5403
|
+
const now = Date.now();
|
|
5404
|
+
const duplicate = this.lastGridClick?.signature === signature
|
|
5405
|
+
&& now - this.lastGridClick.at < 80;
|
|
5406
|
+
this.lastGridClick = { signature, at: now };
|
|
5407
|
+
return duplicate;
|
|
5408
|
+
}
|
|
5409
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: EChartsEngineAdapter, deps: [{ token: EChartsOptionBuilderService }], target: i0.ɵɵFactoryTarget.Injectable });
|
|
5410
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: EChartsEngineAdapter });
|
|
4956
5411
|
}
|
|
4957
|
-
|
|
4958
|
-
|
|
4959
|
-
{
|
|
4960
|
-
|
|
4961
|
-
|
|
4962
|
-
},
|
|
4963
|
-
providePraxisI18n(createPraxisChartsI18nConfig(options)),
|
|
4964
|
-
];
|
|
5412
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: EChartsEngineAdapter, decorators: [{
|
|
5413
|
+
type: Injectable
|
|
5414
|
+
}], ctorParameters: () => [{ type: EChartsOptionBuilderService }] });
|
|
5415
|
+
function firstOptionEntry(value) {
|
|
5416
|
+
return Array.isArray(value) ? value[0] : value;
|
|
4965
5417
|
}
|
|
4966
|
-
function
|
|
4967
|
-
if (
|
|
4968
|
-
return
|
|
5418
|
+
function pointValue(value) {
|
|
5419
|
+
if (Array.isArray(value)) {
|
|
5420
|
+
return value.length > 1 ? value[1] : value[0];
|
|
4969
5421
|
}
|
|
4970
|
-
if (value
|
|
5422
|
+
if (value && typeof value === 'object' && 'value' in value) {
|
|
5423
|
+
return pointValue(value.value);
|
|
5424
|
+
}
|
|
5425
|
+
return value;
|
|
5426
|
+
}
|
|
5427
|
+
function pointData(category, value) {
|
|
5428
|
+
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
4971
5429
|
return value;
|
|
4972
5430
|
}
|
|
4973
|
-
return {
|
|
5431
|
+
return {
|
|
5432
|
+
category,
|
|
5433
|
+
value: pointValue(value),
|
|
5434
|
+
};
|
|
4974
5435
|
}
|
|
4975
5436
|
|
|
4976
5437
|
class ChartEditorDefaultsService {
|
|
@@ -5977,6 +6438,8 @@ class PraxisChartConfigEditor {
|
|
|
5977
6438
|
'pie',
|
|
5978
6439
|
'donut',
|
|
5979
6440
|
'scatter',
|
|
6441
|
+
'funnel',
|
|
6442
|
+
'pyramid',
|
|
5980
6443
|
];
|
|
5981
6444
|
sourceKinds = ['praxis.stats', 'derived'];
|
|
5982
6445
|
operations = ['group-by', 'timeseries', 'distribution', 'comparison'];
|
|
@@ -6821,7 +7284,10 @@ class PraxisChartConfigEditor {
|
|
|
6821
7284
|
return this.doc().kind === 'combo';
|
|
6822
7285
|
}
|
|
6823
7286
|
showPieDonutPanel() {
|
|
6824
|
-
return this.doc().kind === 'pie'
|
|
7287
|
+
return this.doc().kind === 'pie'
|
|
7288
|
+
|| this.doc().kind === 'donut'
|
|
7289
|
+
|| this.doc().kind === 'funnel'
|
|
7290
|
+
|| this.doc().kind === 'pyramid';
|
|
6825
7291
|
}
|
|
6826
7292
|
showScatterPanel() {
|
|
6827
7293
|
return this.doc().kind === 'scatter';
|
|
@@ -7137,7 +7603,7 @@ class PraxisChartConfigEditor {
|
|
|
7137
7603
|
this.documentChange.emit(structuredClone(this.normalizedDocument()));
|
|
7138
7604
|
}
|
|
7139
7605
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: PraxisChartConfigEditor, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
7140
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.14", 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 [attr.data-testid]=\"'chart-editor-section-' + section.id\"\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 data-testid=\"chart-editor-title-input\" [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.sizingMode', 'Sizing') }}</mat-label>\n <mat-select [ngModel]=\"sizingModeValue()\" (ngModelChange)=\"setSizingMode($event)\" [disabled]=\"isReadonly()\">\n @for (mode of sizingModes; track mode) {\n <mat-option [value]=\"mode\">\n {{ t('praxis.charts.editor.sizing.' + mode, mode) }}\n </mat-option>\n }\n </mat-select>\n <mat-hint>{{ t('praxis.charts.editor.hint.sizingMode', 'Use fill-container only when the host widget provides a defined body height.') }}</mat-hint>\n </mat-form-field>\n\n @if (sizingModeValue() === 'fixed') {\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)=\"setSizingHeight($event)\" [disabled]=\"isReadonly()\" />\n <mat-hint>{{ t('praxis.charts.editor.hint.height', 'Numbers are saved as pixels; CSS lengths such as 20rem are also accepted.') }}</mat-hint>\n </mat-form-field>\n }\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.minHeight', 'Minimum height') }}</mat-label>\n <input matInput [ngModel]=\"sizingMinHeightValue()\" (ngModelChange)=\"setSizingMinHeight($event)\" [disabled]=\"isReadonly()\" />\n <mat-hint>{{ t('praxis.charts.editor.hint.minHeight', 'Set a readable minimum for compact dashboard widgets.') }}</mat-hint>\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.maxHeight', 'Maximum height') }}</mat-label>\n <input matInput [ngModel]=\"sizingMaxHeightValue()\" (ngModelChange)=\"setSizingMaxHeight($event)\" [disabled]=\"isReadonly()\" />\n <mat-hint>{{ t('praxis.charts.editor.hint.maxHeight', 'Leave empty unless the chart must stop growing inside a flexible layout.') }}</mat-hint>\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.aspectRatio', 'Aspect ratio') }}</mat-label>\n <input matInput [ngModel]=\"sizingAspectRatioValue()\" (ngModelChange)=\"setSizingAspectRatio($event)\" [disabled]=\"isReadonly()\" />\n <mat-hint>{{ t('praxis.charts.editor.hint.aspectRatio', 'Optional. Use values such as 1.777 or 16 / 9.') }}</mat-hint>\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 data-testid=\"chart-editor-source-kind\" [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 <mat-select data-testid=\"chart-editor-resource\" [ngModel]=\"resourceValue()\" (ngModelChange)=\"setResource($event)\" [disabled]=\"isReadonly() || resourceCatalogUnavailable()\">\n @for (resource of resourceOptions(); track resource.id) {\n <mat-option [value]=\"resource.path\">{{ resource.label }}</mat-option>\n }\n </mat-select>\n @if (resourceCatalogUnavailable()) {\n <mat-hint>{{ t('praxis.charts.editor.catalog.resourceMissing', 'Resource catalog is required for governed praxis.stats authoring.') }}</mat-hint>\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 data-testid=\"chart-editor-operation\"\n [ngModel]=\"doc().source.operation || 'group-by'\"\n (ngModelChange)=\"setOperation($event)\"\n [disabled]=\"isReadonly() || operationCatalogUnavailable()\"\n >\n @for (operation of operationOptions(); 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 @if (operationCatalogUnavailable()) {\n <mat-hint>{{ t('praxis.charts.editor.catalog.operationMissing', 'Selected resource does not publish authorable stats operations.') }}</mat-hint>\n }\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 data-testid=\"chart-editor-distribution-mode\" [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 @if (distributionModeValue() === 'histogram') {\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 data-testid=\"chart-editor-bucket-size\" [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 data-testid=\"chart-editor-bucket-count\" [ngModel]=\"bucketCountValue()\" (ngModelChange)=\"setBucketCount($event)\" [disabled]=\"isReadonly()\" />\n </mat-form-field>\n }\n </mat-card-content>\n </mat-card>\n }\n\n @if (showComparisonControls()) {\n <mat-card class=\"editor-card\" data-testid=\"chart-editor-comparison-options\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.specialization.comparisonTitle', 'Comparison period') }}</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.comparisonPeriodField', 'Period field') }}</mat-label>\n @if (fieldOptions('time').length) {\n <mat-select\n [ngModel]=\"comparisonPeriodFieldValue()\"\n (ngModelChange)=\"setComparisonPeriodField($event)\"\n [disabled]=\"isReadonly()\"\n >\n @for (field of fieldOptions('time'); track field.field) {\n <mat-option [value]=\"field.field\">{{ field.label || field.field }}</mat-option>\n }\n </mat-select>\n } @else {\n <mat-select [ngModel]=\"comparisonPeriodFieldValue()\" [disabled]=\"true\"></mat-select>\n <mat-hint>{{ t('praxis.charts.editor.catalog.comparisonPeriodFieldsMissing', 'No governed time-series field is available for comparison periods.') }}</mat-hint>\n }\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.comparisonTimezone', 'Timezone') }}</mat-label>\n <input\n matInput\n [ngModel]=\"comparisonPeriodTimezoneValue()\"\n (ngModelChange)=\"setComparisonPeriodTimezone($event)\"\n [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.comparisonPreset', 'Period preset') }}</mat-label>\n <mat-select\n [ngModel]=\"comparisonPeriodPresetValue()\"\n (ngModelChange)=\"setComparisonPeriodPreset($event)\"\n [disabled]=\"isReadonly()\"\n >\n @for (preset of comparisonPeriodPresets; track preset) {\n <mat-option [value]=\"preset\">\n {{ t('praxis.charts.editor.comparisonPreset.' + preset, preset) }}\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.comparisonMode', 'Comparison mode') }}</mat-label>\n <mat-select\n [ngModel]=\"comparisonPeriodModeValue()\"\n (ngModelChange)=\"setComparisonPeriodMode($event)\"\n [disabled]=\"isReadonly()\"\n >\n @for (mode of comparisonPeriodModes; track mode) {\n <mat-option [value]=\"mode\">\n {{ t('praxis.charts.editor.comparisonMode.' + mode, mode) }}\n </mat-option>\n }\n </mat-select>\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 class=\"editor-stack\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.themeVariant', 'Theme variant') }}</mat-label>\n <mat-select [ngModel]=\"themeVariantValue()\" (ngModelChange)=\"setThemeVariant($event)\" [disabled]=\"isReadonly()\">\n <mat-option [value]=\"''\">\n {{ t('praxis.charts.editor.themeVariant.none', 'No variant') }}\n </mat-option>\n @for (variant of themeVariants; track variant) {\n <mat-option [value]=\"variant\">\n {{ t('praxis.charts.editor.themeVariant.' + variant, variant) }}\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.paletteMode', 'Palette mode') }}</mat-label>\n <mat-select [ngModel]=\"paletteModeValue()\" (ngModelChange)=\"setPaletteMode($event)\" [disabled]=\"isReadonly()\">\n @for (mode of paletteModes; track mode) {\n <mat-option [value]=\"mode\">\n {{ t('praxis.charts.editor.paletteMode.' + mode, mode) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n @if (paletteModeValue() === 'token') {\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.paletteToken', 'Palette token') }}</mat-label>\n <mat-select [ngModel]=\"paletteTokenValue()\" (ngModelChange)=\"setPaletteToken($event)\" [disabled]=\"isReadonly()\">\n @for (token of paletteTokens; track token) {\n <mat-option [value]=\"token\">\n {{ t('praxis.charts.editor.paletteToken.' + token, token) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n } @else {\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 }\n\n <p class=\"editor-caption\">\n {{ t('praxis.charts.editor.appearance.paletteHint', 'Use a registered token or comma-separated colors to persist theme.palette in the canonical contract.') }}\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.surfaceTitle', 'Surface') }}</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.surfaceMode', 'Surface mode') }}</mat-label>\n <mat-select [ngModel]=\"surfaceModeValue()\" (ngModelChange)=\"setSurfaceMode($event)\" [disabled]=\"isReadonly()\">\n @for (mode of surfaceModes; track mode) {\n <mat-option [value]=\"mode\">\n {{ t('praxis.charts.editor.surface.' + mode, mode) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n <p class=\"editor-caption\">\n {{ t('praxis.charts.editor.appearance.surfaceHint', 'Use embedded for dashboard widgets and contained only when the chart must own its visual surface.') }}\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\" [attr.data-testid]=\"'chart-editor-dimension-row-' + $index\">\n <div class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\" [attr.data-testid]=\"'chart-editor-dimension-field-' + $index\">\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\">{{ fieldOptionLabel(field) }}</mat-option>\n }\n </mat-select>\n } @else {\n <mat-select [ngModel]=\"dimension.field || ''\" (ngModelChange)=\"setDimensionField($index, $event)\" [disabled]=\"true\"></mat-select>\n <mat-hint>{{ t('praxis.charts.editor.catalog.dimensionFieldsMissing', 'No governed dimensions are available for the current resource and operation.') }}</mat-hint>\n }\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\" [attr.data-testid]=\"'chart-editor-dimension-role-' + $index\">\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\" data-testid=\"chart-editor-add-dimension\" (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\" [attr.data-testid]=\"'chart-editor-metric-row-' + $index\">\n <div class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\" [attr.data-testid]=\"'chart-editor-metric-field-' + $index\">\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\">{{ fieldOptionLabel(field) }}</mat-option>\n }\n </mat-select>\n } @else {\n <mat-select [ngModel]=\"metric.field || ''\" (ngModelChange)=\"setMetricField($index, $event)\" [disabled]=\"true\"></mat-select>\n <mat-hint>{{ t('praxis.charts.editor.catalog.metricFieldsMissing', 'No governed metrics are available for the current resource and operation.') }}</mat-hint>\n }\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\" [attr.data-testid]=\"'chart-editor-metric-label-' + $index\">\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\" [attr.data-testid]=\"'chart-editor-metric-aggregation-' + $index\">\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 metricAggregationOptions(metric.field); 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\" [attr.data-testid]=\"'chart-editor-metric-axis-' + $index\">\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\" [attr.data-testid]=\"'chart-editor-metric-series-kind-' + $index\">\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\" [attr.data-testid]=\"'chart-editor-remove-metric-' + $index\" (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\" data-testid=\"chart-editor-add-metric\" (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\" data-testid=\"chart-editor-event-pointClick\">\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 data-testid=\"chart-editor-event-pointClick-action\" [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 <mat-select data-testid=\"chart-editor-event-pointClick-target\" [ngModel]=\"eventTarget('pointClick')\" (ngModelChange)=\"setEventTarget('pointClick', $event)\" [disabled]=\"isReadonly() || !eventAction('pointClick') || targetCatalogUnavailable(eventAction('pointClick'), 'pointClick')\">\n @for (target of targetOptions(eventAction('pointClick'), 'pointClick'); track target.id) {\n <mat-option [value]=\"target.id\">{{ target.label }}</mat-option>\n }\n </mat-select>\n @if (targetCatalogUnavailable(eventAction('pointClick'), 'pointClick')) {\n <mat-hint>{{ t('praxis.charts.editor.catalog.targetMissing', 'Target catalog is required for this governed event action.') }}</mat-hint>\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 data-testid=\"chart-editor-event-pointClick-mapping\"\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\" data-testid=\"chart-editor-event-selectionChange\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.events.selectionChangeTitle', 'Selection change') }}</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 data-testid=\"chart-editor-event-selectionChange-action\" [ngModel]=\"eventAction('selectionChange')\" (ngModelChange)=\"setEventAction('selectionChange', $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 <mat-select data-testid=\"chart-editor-event-selectionChange-target\" [ngModel]=\"eventTarget('selectionChange')\" (ngModelChange)=\"setEventTarget('selectionChange', $event)\" [disabled]=\"isReadonly() || !eventAction('selectionChange') || targetCatalogUnavailable(eventAction('selectionChange'), 'selectionChange')\">\n @for (target of targetOptions(eventAction('selectionChange'), 'selectionChange'); track target.id) {\n <mat-option [value]=\"target.id\">{{ target.label }}</mat-option>\n }\n </mat-select>\n @if (targetCatalogUnavailable(eventAction('selectionChange'), 'selectionChange')) {\n <mat-hint>{{ t('praxis.charts.editor.catalog.targetMissing', 'Target catalog is required for this governed event action.') }}</mat-hint>\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 data-testid=\"chart-editor-event-selectionChange-mapping\"\n rows=\"4\"\n [ngModel]=\"eventMappingText('selectionChange')\"\n (ngModelChange)=\"setEventMapping('selectionChange', $event)\"\n [disabled]=\"isReadonly() || !eventAction('selectionChange')\"\n ></textarea>\n </mat-form-field>\n </mat-card-content>\n </mat-card>\n\n <mat-card class=\"editor-card\" data-testid=\"chart-editor-event-drillDown\">\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 data-testid=\"chart-editor-event-drillDown-action\" [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 <mat-select data-testid=\"chart-editor-event-drillDown-target\" [ngModel]=\"eventTarget('drillDown')\" (ngModelChange)=\"setEventTarget('drillDown', $event)\" [disabled]=\"isReadonly() || !eventAction('drillDown') || targetCatalogUnavailable(eventAction('drillDown'), 'drillDown')\">\n @for (target of targetOptions(eventAction('drillDown'), 'drillDown'); track target.id) {\n <mat-option [value]=\"target.id\">{{ target.label }}</mat-option>\n }\n </mat-select>\n @if (targetCatalogUnavailable(eventAction('drillDown'), 'drillDown')) {\n <mat-hint>{{ t('praxis.charts.editor.catalog.targetMissing', 'Target catalog is required for this governed event action.') }}</mat-hint>\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 data-testid=\"chart-editor-event-drillDown-mapping\"\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\n <mat-card class=\"editor-card\" data-testid=\"chart-editor-event-crossFilter\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.events.crossFilterTitle', 'Cross filter') }}</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 data-testid=\"chart-editor-event-crossFilter-action\" [ngModel]=\"eventAction('crossFilter')\" (ngModelChange)=\"setEventAction('crossFilter', $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 <mat-select data-testid=\"chart-editor-event-crossFilter-target\" [ngModel]=\"eventTarget('crossFilter')\" (ngModelChange)=\"setEventTarget('crossFilter', $event)\" [disabled]=\"isReadonly() || !eventAction('crossFilter') || targetCatalogUnavailable(eventAction('crossFilter'), 'crossFilter')\">\n @for (target of targetOptions(eventAction('crossFilter'), 'crossFilter'); track target.id) {\n <mat-option [value]=\"target.id\">{{ target.label }}</mat-option>\n }\n </mat-select>\n @if (targetCatalogUnavailable(eventAction('crossFilter'), 'crossFilter')) {\n <mat-hint>{{ t('praxis.charts.editor.catalog.targetMissing', 'Target catalog is required for this governed event action.') }}</mat-hint>\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 data-testid=\"chart-editor-event-crossFilter-mapping\"\n rows=\"4\"\n [ngModel]=\"eventMappingText('crossFilter')\"\n (ngModelChange)=\"setEventMapping('crossFilter', $event)\"\n [disabled]=\"isReadonly() || !eventAction('crossFilter')\"\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;container:praxis-chart-editor / inline-size;color:var(--md-sys-color-on-surface, #1a1b20)}.editor-shell{display:grid;gap:18px;min-width:0}.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;min-width:0}.editor-card{box-sizing:border-box;width:100%;min-width:0;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)}.editor-side praxis-chart{display:block;width:100%;min-width:0;max-width:100%}@container praxis-chart-editor (max-width: 840px){.editor-layout{grid-template-columns:minmax(0,1fr)}}@media(max-width:960px){.editor-layout{grid-template-columns:minmax(0,1fr)}}\n"], dependencies: [{ 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: i2.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.MatCard, selector: "mat-card", inputs: ["appearance"], exportAs: ["matCard"] }, { kind: "directive", type: i3.MatCardContent, selector: "mat-card-content" }, { kind: "component", type: i3.MatCardHeader, selector: "mat-card-header" }, { kind: "directive", type: i3.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: "directive", type: i4.MatHint, selector: "mat-hint", inputs: ["align", "id"] }, { 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", "queryContext", "remoteDataResolver", "enableCustomization", "availableResources", "availableFields", "availableTargets"], outputs: ["pointClick", "pointAction", "selectionChange", "drillDown", "crossFilter", "queryRequest", "loadStateChange", "chartDocumentApplied", "chartDocumentSaved"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
7606
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.14", 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 [attr.data-testid]=\"'chart-editor-section-' + section.id\"\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 data-testid=\"chart-editor-title-input\" [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.sizingMode', 'Sizing') }}</mat-label>\n <mat-select [ngModel]=\"sizingModeValue()\" (ngModelChange)=\"setSizingMode($event)\" [disabled]=\"isReadonly()\">\n @for (mode of sizingModes; track mode) {\n <mat-option [value]=\"mode\">\n {{ t('praxis.charts.editor.sizing.' + mode, mode) }}\n </mat-option>\n }\n </mat-select>\n <mat-hint>{{ t('praxis.charts.editor.hint.sizingMode', 'Use fill-container only when the host widget provides a defined body height.') }}</mat-hint>\n </mat-form-field>\n\n @if (sizingModeValue() === 'fixed') {\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)=\"setSizingHeight($event)\" [disabled]=\"isReadonly()\" />\n <mat-hint>{{ t('praxis.charts.editor.hint.height', 'Numbers are saved as pixels; CSS lengths such as 20rem are also accepted.') }}</mat-hint>\n </mat-form-field>\n }\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.minHeight', 'Minimum height') }}</mat-label>\n <input matInput [ngModel]=\"sizingMinHeightValue()\" (ngModelChange)=\"setSizingMinHeight($event)\" [disabled]=\"isReadonly()\" />\n <mat-hint>{{ t('praxis.charts.editor.hint.minHeight', 'Set a readable minimum for compact dashboard widgets.') }}</mat-hint>\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.maxHeight', 'Maximum height') }}</mat-label>\n <input matInput [ngModel]=\"sizingMaxHeightValue()\" (ngModelChange)=\"setSizingMaxHeight($event)\" [disabled]=\"isReadonly()\" />\n <mat-hint>{{ t('praxis.charts.editor.hint.maxHeight', 'Leave empty unless the chart must stop growing inside a flexible layout.') }}</mat-hint>\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.aspectRatio', 'Aspect ratio') }}</mat-label>\n <input matInput [ngModel]=\"sizingAspectRatioValue()\" (ngModelChange)=\"setSizingAspectRatio($event)\" [disabled]=\"isReadonly()\" />\n <mat-hint>{{ t('praxis.charts.editor.hint.aspectRatio', 'Optional. Use values such as 1.777 or 16 / 9.') }}</mat-hint>\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 data-testid=\"chart-editor-source-kind\" [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 <mat-select data-testid=\"chart-editor-resource\" [ngModel]=\"resourceValue()\" (ngModelChange)=\"setResource($event)\" [disabled]=\"isReadonly() || resourceCatalogUnavailable()\">\n @for (resource of resourceOptions(); track resource.id) {\n <mat-option [value]=\"resource.path\">{{ resource.label }}</mat-option>\n }\n </mat-select>\n @if (resourceCatalogUnavailable()) {\n <mat-hint>{{ t('praxis.charts.editor.catalog.resourceMissing', 'Resource catalog is required for governed praxis.stats authoring.') }}</mat-hint>\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 data-testid=\"chart-editor-operation\"\n [ngModel]=\"doc().source.operation || 'group-by'\"\n (ngModelChange)=\"setOperation($event)\"\n [disabled]=\"isReadonly() || operationCatalogUnavailable()\"\n >\n @for (operation of operationOptions(); 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 @if (operationCatalogUnavailable()) {\n <mat-hint>{{ t('praxis.charts.editor.catalog.operationMissing', 'Selected resource does not publish authorable stats operations.') }}</mat-hint>\n }\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 data-testid=\"chart-editor-distribution-mode\" [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 @if (distributionModeValue() === 'histogram') {\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 data-testid=\"chart-editor-bucket-size\" [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 data-testid=\"chart-editor-bucket-count\" [ngModel]=\"bucketCountValue()\" (ngModelChange)=\"setBucketCount($event)\" [disabled]=\"isReadonly()\" />\n </mat-form-field>\n }\n </mat-card-content>\n </mat-card>\n }\n\n @if (showComparisonControls()) {\n <mat-card class=\"editor-card\" data-testid=\"chart-editor-comparison-options\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.specialization.comparisonTitle', 'Comparison period') }}</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.comparisonPeriodField', 'Period field') }}</mat-label>\n @if (fieldOptions('time').length) {\n <mat-select\n [ngModel]=\"comparisonPeriodFieldValue()\"\n (ngModelChange)=\"setComparisonPeriodField($event)\"\n [disabled]=\"isReadonly()\"\n >\n @for (field of fieldOptions('time'); track field.field) {\n <mat-option [value]=\"field.field\">{{ field.label || field.field }}</mat-option>\n }\n </mat-select>\n } @else {\n <mat-select [ngModel]=\"comparisonPeriodFieldValue()\" [disabled]=\"true\"></mat-select>\n <mat-hint>{{ t('praxis.charts.editor.catalog.comparisonPeriodFieldsMissing', 'No governed time-series field is available for comparison periods.') }}</mat-hint>\n }\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.comparisonTimezone', 'Timezone') }}</mat-label>\n <input\n matInput\n [ngModel]=\"comparisonPeriodTimezoneValue()\"\n (ngModelChange)=\"setComparisonPeriodTimezone($event)\"\n [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.comparisonPreset', 'Period preset') }}</mat-label>\n <mat-select\n [ngModel]=\"comparisonPeriodPresetValue()\"\n (ngModelChange)=\"setComparisonPeriodPreset($event)\"\n [disabled]=\"isReadonly()\"\n >\n @for (preset of comparisonPeriodPresets; track preset) {\n <mat-option [value]=\"preset\">\n {{ t('praxis.charts.editor.comparisonPreset.' + preset, preset) }}\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.comparisonMode', 'Comparison mode') }}</mat-label>\n <mat-select\n [ngModel]=\"comparisonPeriodModeValue()\"\n (ngModelChange)=\"setComparisonPeriodMode($event)\"\n [disabled]=\"isReadonly()\"\n >\n @for (mode of comparisonPeriodModes; track mode) {\n <mat-option [value]=\"mode\">\n {{ t('praxis.charts.editor.comparisonMode.' + mode, mode) }}\n </mat-option>\n }\n </mat-select>\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 class=\"editor-stack\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.themeVariant', 'Theme variant') }}</mat-label>\n <mat-select [ngModel]=\"themeVariantValue()\" (ngModelChange)=\"setThemeVariant($event)\" [disabled]=\"isReadonly()\">\n <mat-option [value]=\"''\">\n {{ t('praxis.charts.editor.themeVariant.none', 'No variant') }}\n </mat-option>\n @for (variant of themeVariants; track variant) {\n <mat-option [value]=\"variant\">\n {{ t('praxis.charts.editor.themeVariant.' + variant, variant) }}\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.paletteMode', 'Palette mode') }}</mat-label>\n <mat-select [ngModel]=\"paletteModeValue()\" (ngModelChange)=\"setPaletteMode($event)\" [disabled]=\"isReadonly()\">\n @for (mode of paletteModes; track mode) {\n <mat-option [value]=\"mode\">\n {{ t('praxis.charts.editor.paletteMode.' + mode, mode) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n @if (paletteModeValue() === 'token') {\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.paletteToken', 'Palette token') }}</mat-label>\n <mat-select [ngModel]=\"paletteTokenValue()\" (ngModelChange)=\"setPaletteToken($event)\" [disabled]=\"isReadonly()\">\n @for (token of paletteTokens; track token) {\n <mat-option [value]=\"token\">\n {{ t('praxis.charts.editor.paletteToken.' + token, token) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n } @else {\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 }\n\n <p class=\"editor-caption\">\n {{ t('praxis.charts.editor.appearance.paletteHint', 'Use a registered token or comma-separated colors to persist theme.palette in the canonical contract.') }}\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.surfaceTitle', 'Surface') }}</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.surfaceMode', 'Surface mode') }}</mat-label>\n <mat-select [ngModel]=\"surfaceModeValue()\" (ngModelChange)=\"setSurfaceMode($event)\" [disabled]=\"isReadonly()\">\n @for (mode of surfaceModes; track mode) {\n <mat-option [value]=\"mode\">\n {{ t('praxis.charts.editor.surface.' + mode, mode) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n <p class=\"editor-caption\">\n {{ t('praxis.charts.editor.appearance.surfaceHint', 'Use embedded for dashboard widgets and contained only when the chart must own its visual surface.') }}\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, donut, funnel and pyramid charts keep one 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\" [attr.data-testid]=\"'chart-editor-dimension-row-' + $index\">\n <div class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\" [attr.data-testid]=\"'chart-editor-dimension-field-' + $index\">\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\">{{ fieldOptionLabel(field) }}</mat-option>\n }\n </mat-select>\n } @else {\n <mat-select [ngModel]=\"dimension.field || ''\" (ngModelChange)=\"setDimensionField($index, $event)\" [disabled]=\"true\"></mat-select>\n <mat-hint>{{ t('praxis.charts.editor.catalog.dimensionFieldsMissing', 'No governed dimensions are available for the current resource and operation.') }}</mat-hint>\n }\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\" [attr.data-testid]=\"'chart-editor-dimension-role-' + $index\">\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\" data-testid=\"chart-editor-add-dimension\" (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\" [attr.data-testid]=\"'chart-editor-metric-row-' + $index\">\n <div class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\" [attr.data-testid]=\"'chart-editor-metric-field-' + $index\">\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\">{{ fieldOptionLabel(field) }}</mat-option>\n }\n </mat-select>\n } @else {\n <mat-select [ngModel]=\"metric.field || ''\" (ngModelChange)=\"setMetricField($index, $event)\" [disabled]=\"true\"></mat-select>\n <mat-hint>{{ t('praxis.charts.editor.catalog.metricFieldsMissing', 'No governed metrics are available for the current resource and operation.') }}</mat-hint>\n }\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\" [attr.data-testid]=\"'chart-editor-metric-label-' + $index\">\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\" [attr.data-testid]=\"'chart-editor-metric-aggregation-' + $index\">\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 metricAggregationOptions(metric.field); 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\" [attr.data-testid]=\"'chart-editor-metric-axis-' + $index\">\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\" [attr.data-testid]=\"'chart-editor-metric-series-kind-' + $index\">\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\" [attr.data-testid]=\"'chart-editor-remove-metric-' + $index\" (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\" data-testid=\"chart-editor-add-metric\" (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\" data-testid=\"chart-editor-event-pointClick\">\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 data-testid=\"chart-editor-event-pointClick-action\" [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 <mat-select data-testid=\"chart-editor-event-pointClick-target\" [ngModel]=\"eventTarget('pointClick')\" (ngModelChange)=\"setEventTarget('pointClick', $event)\" [disabled]=\"isReadonly() || !eventAction('pointClick') || targetCatalogUnavailable(eventAction('pointClick'), 'pointClick')\">\n @for (target of targetOptions(eventAction('pointClick'), 'pointClick'); track target.id) {\n <mat-option [value]=\"target.id\">{{ target.label }}</mat-option>\n }\n </mat-select>\n @if (targetCatalogUnavailable(eventAction('pointClick'), 'pointClick')) {\n <mat-hint>{{ t('praxis.charts.editor.catalog.targetMissing', 'Target catalog is required for this governed event action.') }}</mat-hint>\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 data-testid=\"chart-editor-event-pointClick-mapping\"\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\" data-testid=\"chart-editor-event-selectionChange\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.events.selectionChangeTitle', 'Selection change') }}</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 data-testid=\"chart-editor-event-selectionChange-action\" [ngModel]=\"eventAction('selectionChange')\" (ngModelChange)=\"setEventAction('selectionChange', $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 <mat-select data-testid=\"chart-editor-event-selectionChange-target\" [ngModel]=\"eventTarget('selectionChange')\" (ngModelChange)=\"setEventTarget('selectionChange', $event)\" [disabled]=\"isReadonly() || !eventAction('selectionChange') || targetCatalogUnavailable(eventAction('selectionChange'), 'selectionChange')\">\n @for (target of targetOptions(eventAction('selectionChange'), 'selectionChange'); track target.id) {\n <mat-option [value]=\"target.id\">{{ target.label }}</mat-option>\n }\n </mat-select>\n @if (targetCatalogUnavailable(eventAction('selectionChange'), 'selectionChange')) {\n <mat-hint>{{ t('praxis.charts.editor.catalog.targetMissing', 'Target catalog is required for this governed event action.') }}</mat-hint>\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 data-testid=\"chart-editor-event-selectionChange-mapping\"\n rows=\"4\"\n [ngModel]=\"eventMappingText('selectionChange')\"\n (ngModelChange)=\"setEventMapping('selectionChange', $event)\"\n [disabled]=\"isReadonly() || !eventAction('selectionChange')\"\n ></textarea>\n </mat-form-field>\n </mat-card-content>\n </mat-card>\n\n <mat-card class=\"editor-card\" data-testid=\"chart-editor-event-drillDown\">\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 data-testid=\"chart-editor-event-drillDown-action\" [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 <mat-select data-testid=\"chart-editor-event-drillDown-target\" [ngModel]=\"eventTarget('drillDown')\" (ngModelChange)=\"setEventTarget('drillDown', $event)\" [disabled]=\"isReadonly() || !eventAction('drillDown') || targetCatalogUnavailable(eventAction('drillDown'), 'drillDown')\">\n @for (target of targetOptions(eventAction('drillDown'), 'drillDown'); track target.id) {\n <mat-option [value]=\"target.id\">{{ target.label }}</mat-option>\n }\n </mat-select>\n @if (targetCatalogUnavailable(eventAction('drillDown'), 'drillDown')) {\n <mat-hint>{{ t('praxis.charts.editor.catalog.targetMissing', 'Target catalog is required for this governed event action.') }}</mat-hint>\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 data-testid=\"chart-editor-event-drillDown-mapping\"\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\n <mat-card class=\"editor-card\" data-testid=\"chart-editor-event-crossFilter\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.events.crossFilterTitle', 'Cross filter') }}</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 data-testid=\"chart-editor-event-crossFilter-action\" [ngModel]=\"eventAction('crossFilter')\" (ngModelChange)=\"setEventAction('crossFilter', $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 <mat-select data-testid=\"chart-editor-event-crossFilter-target\" [ngModel]=\"eventTarget('crossFilter')\" (ngModelChange)=\"setEventTarget('crossFilter', $event)\" [disabled]=\"isReadonly() || !eventAction('crossFilter') || targetCatalogUnavailable(eventAction('crossFilter'), 'crossFilter')\">\n @for (target of targetOptions(eventAction('crossFilter'), 'crossFilter'); track target.id) {\n <mat-option [value]=\"target.id\">{{ target.label }}</mat-option>\n }\n </mat-select>\n @if (targetCatalogUnavailable(eventAction('crossFilter'), 'crossFilter')) {\n <mat-hint>{{ t('praxis.charts.editor.catalog.targetMissing', 'Target catalog is required for this governed event action.') }}</mat-hint>\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 data-testid=\"chart-editor-event-crossFilter-mapping\"\n rows=\"4\"\n [ngModel]=\"eventMappingText('crossFilter')\"\n (ngModelChange)=\"setEventMapping('crossFilter', $event)\"\n [disabled]=\"isReadonly() || !eventAction('crossFilter')\"\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;container:praxis-chart-editor / inline-size;color:var(--md-sys-color-on-surface, #1a1b20)}.editor-shell{display:grid;gap:18px;min-width:0}.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;min-width:0}.editor-card{box-sizing:border-box;width:100%;min-width:0;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)}.editor-side praxis-chart{display:block;width:100%;min-width:0;max-width:100%}@container praxis-chart-editor (max-width: 840px){.editor-layout{grid-template-columns:minmax(0,1fr)}}@media(max-width:960px){.editor-layout{grid-template-columns:minmax(0,1fr)}}\n"], dependencies: [{ 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: i2.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.MatCard, selector: "mat-card", inputs: ["appearance"], exportAs: ["matCard"] }, { kind: "directive", type: i3.MatCardContent, selector: "mat-card-content" }, { kind: "component", type: i3.MatCardHeader, selector: "mat-card-header" }, { kind: "directive", type: i3.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: "directive", type: i4.MatHint, selector: "mat-hint", inputs: ["align", "id"] }, { 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", "queryContext", "remoteDataResolver", "enableCustomization", "availableResources", "availableFields", "availableTargets"], outputs: ["pointClick", "pointAction", "selectionChange", "drillDown", "crossFilter", "queryRequest", "loadStateChange", "chartDocumentApplied", "chartDocumentSaved"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
7141
7607
|
}
|
|
7142
7608
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: PraxisChartConfigEditor, decorators: [{
|
|
7143
7609
|
type: Component,
|
|
@@ -7150,7 +7616,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
|
|
|
7150
7616
|
MatSelectModule,
|
|
7151
7617
|
MatSlideToggleModule,
|
|
7152
7618
|
PraxisChartComponent
|
|
7153
|
-
], 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 [attr.data-testid]=\"'chart-editor-section-' + section.id\"\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 data-testid=\"chart-editor-title-input\" [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.sizingMode', 'Sizing') }}</mat-label>\n <mat-select [ngModel]=\"sizingModeValue()\" (ngModelChange)=\"setSizingMode($event)\" [disabled]=\"isReadonly()\">\n @for (mode of sizingModes; track mode) {\n <mat-option [value]=\"mode\">\n {{ t('praxis.charts.editor.sizing.' + mode, mode) }}\n </mat-option>\n }\n </mat-select>\n <mat-hint>{{ t('praxis.charts.editor.hint.sizingMode', 'Use fill-container only when the host widget provides a defined body height.') }}</mat-hint>\n </mat-form-field>\n\n @if (sizingModeValue() === 'fixed') {\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)=\"setSizingHeight($event)\" [disabled]=\"isReadonly()\" />\n <mat-hint>{{ t('praxis.charts.editor.hint.height', 'Numbers are saved as pixels; CSS lengths such as 20rem are also accepted.') }}</mat-hint>\n </mat-form-field>\n }\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.minHeight', 'Minimum height') }}</mat-label>\n <input matInput [ngModel]=\"sizingMinHeightValue()\" (ngModelChange)=\"setSizingMinHeight($event)\" [disabled]=\"isReadonly()\" />\n <mat-hint>{{ t('praxis.charts.editor.hint.minHeight', 'Set a readable minimum for compact dashboard widgets.') }}</mat-hint>\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.maxHeight', 'Maximum height') }}</mat-label>\n <input matInput [ngModel]=\"sizingMaxHeightValue()\" (ngModelChange)=\"setSizingMaxHeight($event)\" [disabled]=\"isReadonly()\" />\n <mat-hint>{{ t('praxis.charts.editor.hint.maxHeight', 'Leave empty unless the chart must stop growing inside a flexible layout.') }}</mat-hint>\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.aspectRatio', 'Aspect ratio') }}</mat-label>\n <input matInput [ngModel]=\"sizingAspectRatioValue()\" (ngModelChange)=\"setSizingAspectRatio($event)\" [disabled]=\"isReadonly()\" />\n <mat-hint>{{ t('praxis.charts.editor.hint.aspectRatio', 'Optional. Use values such as 1.777 or 16 / 9.') }}</mat-hint>\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 data-testid=\"chart-editor-source-kind\" [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 <mat-select data-testid=\"chart-editor-resource\" [ngModel]=\"resourceValue()\" (ngModelChange)=\"setResource($event)\" [disabled]=\"isReadonly() || resourceCatalogUnavailable()\">\n @for (resource of resourceOptions(); track resource.id) {\n <mat-option [value]=\"resource.path\">{{ resource.label }}</mat-option>\n }\n </mat-select>\n @if (resourceCatalogUnavailable()) {\n <mat-hint>{{ t('praxis.charts.editor.catalog.resourceMissing', 'Resource catalog is required for governed praxis.stats authoring.') }}</mat-hint>\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 data-testid=\"chart-editor-operation\"\n [ngModel]=\"doc().source.operation || 'group-by'\"\n (ngModelChange)=\"setOperation($event)\"\n [disabled]=\"isReadonly() || operationCatalogUnavailable()\"\n >\n @for (operation of operationOptions(); 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 @if (operationCatalogUnavailable()) {\n <mat-hint>{{ t('praxis.charts.editor.catalog.operationMissing', 'Selected resource does not publish authorable stats operations.') }}</mat-hint>\n }\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 data-testid=\"chart-editor-distribution-mode\" [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 @if (distributionModeValue() === 'histogram') {\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 data-testid=\"chart-editor-bucket-size\" [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 data-testid=\"chart-editor-bucket-count\" [ngModel]=\"bucketCountValue()\" (ngModelChange)=\"setBucketCount($event)\" [disabled]=\"isReadonly()\" />\n </mat-form-field>\n }\n </mat-card-content>\n </mat-card>\n }\n\n @if (showComparisonControls()) {\n <mat-card class=\"editor-card\" data-testid=\"chart-editor-comparison-options\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.specialization.comparisonTitle', 'Comparison period') }}</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.comparisonPeriodField', 'Period field') }}</mat-label>\n @if (fieldOptions('time').length) {\n <mat-select\n [ngModel]=\"comparisonPeriodFieldValue()\"\n (ngModelChange)=\"setComparisonPeriodField($event)\"\n [disabled]=\"isReadonly()\"\n >\n @for (field of fieldOptions('time'); track field.field) {\n <mat-option [value]=\"field.field\">{{ field.label || field.field }}</mat-option>\n }\n </mat-select>\n } @else {\n <mat-select [ngModel]=\"comparisonPeriodFieldValue()\" [disabled]=\"true\"></mat-select>\n <mat-hint>{{ t('praxis.charts.editor.catalog.comparisonPeriodFieldsMissing', 'No governed time-series field is available for comparison periods.') }}</mat-hint>\n }\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.comparisonTimezone', 'Timezone') }}</mat-label>\n <input\n matInput\n [ngModel]=\"comparisonPeriodTimezoneValue()\"\n (ngModelChange)=\"setComparisonPeriodTimezone($event)\"\n [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.comparisonPreset', 'Period preset') }}</mat-label>\n <mat-select\n [ngModel]=\"comparisonPeriodPresetValue()\"\n (ngModelChange)=\"setComparisonPeriodPreset($event)\"\n [disabled]=\"isReadonly()\"\n >\n @for (preset of comparisonPeriodPresets; track preset) {\n <mat-option [value]=\"preset\">\n {{ t('praxis.charts.editor.comparisonPreset.' + preset, preset) }}\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.comparisonMode', 'Comparison mode') }}</mat-label>\n <mat-select\n [ngModel]=\"comparisonPeriodModeValue()\"\n (ngModelChange)=\"setComparisonPeriodMode($event)\"\n [disabled]=\"isReadonly()\"\n >\n @for (mode of comparisonPeriodModes; track mode) {\n <mat-option [value]=\"mode\">\n {{ t('praxis.charts.editor.comparisonMode.' + mode, mode) }}\n </mat-option>\n }\n </mat-select>\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 class=\"editor-stack\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.themeVariant', 'Theme variant') }}</mat-label>\n <mat-select [ngModel]=\"themeVariantValue()\" (ngModelChange)=\"setThemeVariant($event)\" [disabled]=\"isReadonly()\">\n <mat-option [value]=\"''\">\n {{ t('praxis.charts.editor.themeVariant.none', 'No variant') }}\n </mat-option>\n @for (variant of themeVariants; track variant) {\n <mat-option [value]=\"variant\">\n {{ t('praxis.charts.editor.themeVariant.' + variant, variant) }}\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.paletteMode', 'Palette mode') }}</mat-label>\n <mat-select [ngModel]=\"paletteModeValue()\" (ngModelChange)=\"setPaletteMode($event)\" [disabled]=\"isReadonly()\">\n @for (mode of paletteModes; track mode) {\n <mat-option [value]=\"mode\">\n {{ t('praxis.charts.editor.paletteMode.' + mode, mode) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n @if (paletteModeValue() === 'token') {\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.paletteToken', 'Palette token') }}</mat-label>\n <mat-select [ngModel]=\"paletteTokenValue()\" (ngModelChange)=\"setPaletteToken($event)\" [disabled]=\"isReadonly()\">\n @for (token of paletteTokens; track token) {\n <mat-option [value]=\"token\">\n {{ t('praxis.charts.editor.paletteToken.' + token, token) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n } @else {\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 }\n\n <p class=\"editor-caption\">\n {{ t('praxis.charts.editor.appearance.paletteHint', 'Use a registered token or comma-separated colors to persist theme.palette in the canonical contract.') }}\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.surfaceTitle', 'Surface') }}</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.surfaceMode', 'Surface mode') }}</mat-label>\n <mat-select [ngModel]=\"surfaceModeValue()\" (ngModelChange)=\"setSurfaceMode($event)\" [disabled]=\"isReadonly()\">\n @for (mode of surfaceModes; track mode) {\n <mat-option [value]=\"mode\">\n {{ t('praxis.charts.editor.surface.' + mode, mode) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n <p class=\"editor-caption\">\n {{ t('praxis.charts.editor.appearance.surfaceHint', 'Use embedded for dashboard widgets and contained only when the chart must own its visual surface.') }}\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\" [attr.data-testid]=\"'chart-editor-dimension-row-' + $index\">\n <div class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\" [attr.data-testid]=\"'chart-editor-dimension-field-' + $index\">\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\">{{ fieldOptionLabel(field) }}</mat-option>\n }\n </mat-select>\n } @else {\n <mat-select [ngModel]=\"dimension.field || ''\" (ngModelChange)=\"setDimensionField($index, $event)\" [disabled]=\"true\"></mat-select>\n <mat-hint>{{ t('praxis.charts.editor.catalog.dimensionFieldsMissing', 'No governed dimensions are available for the current resource and operation.') }}</mat-hint>\n }\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\" [attr.data-testid]=\"'chart-editor-dimension-role-' + $index\">\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\" data-testid=\"chart-editor-add-dimension\" (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\" [attr.data-testid]=\"'chart-editor-metric-row-' + $index\">\n <div class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\" [attr.data-testid]=\"'chart-editor-metric-field-' + $index\">\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\">{{ fieldOptionLabel(field) }}</mat-option>\n }\n </mat-select>\n } @else {\n <mat-select [ngModel]=\"metric.field || ''\" (ngModelChange)=\"setMetricField($index, $event)\" [disabled]=\"true\"></mat-select>\n <mat-hint>{{ t('praxis.charts.editor.catalog.metricFieldsMissing', 'No governed metrics are available for the current resource and operation.') }}</mat-hint>\n }\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\" [attr.data-testid]=\"'chart-editor-metric-label-' + $index\">\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\" [attr.data-testid]=\"'chart-editor-metric-aggregation-' + $index\">\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 metricAggregationOptions(metric.field); 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\" [attr.data-testid]=\"'chart-editor-metric-axis-' + $index\">\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\" [attr.data-testid]=\"'chart-editor-metric-series-kind-' + $index\">\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\" [attr.data-testid]=\"'chart-editor-remove-metric-' + $index\" (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\" data-testid=\"chart-editor-add-metric\" (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\" data-testid=\"chart-editor-event-pointClick\">\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 data-testid=\"chart-editor-event-pointClick-action\" [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 <mat-select data-testid=\"chart-editor-event-pointClick-target\" [ngModel]=\"eventTarget('pointClick')\" (ngModelChange)=\"setEventTarget('pointClick', $event)\" [disabled]=\"isReadonly() || !eventAction('pointClick') || targetCatalogUnavailable(eventAction('pointClick'), 'pointClick')\">\n @for (target of targetOptions(eventAction('pointClick'), 'pointClick'); track target.id) {\n <mat-option [value]=\"target.id\">{{ target.label }}</mat-option>\n }\n </mat-select>\n @if (targetCatalogUnavailable(eventAction('pointClick'), 'pointClick')) {\n <mat-hint>{{ t('praxis.charts.editor.catalog.targetMissing', 'Target catalog is required for this governed event action.') }}</mat-hint>\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 data-testid=\"chart-editor-event-pointClick-mapping\"\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\" data-testid=\"chart-editor-event-selectionChange\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.events.selectionChangeTitle', 'Selection change') }}</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 data-testid=\"chart-editor-event-selectionChange-action\" [ngModel]=\"eventAction('selectionChange')\" (ngModelChange)=\"setEventAction('selectionChange', $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 <mat-select data-testid=\"chart-editor-event-selectionChange-target\" [ngModel]=\"eventTarget('selectionChange')\" (ngModelChange)=\"setEventTarget('selectionChange', $event)\" [disabled]=\"isReadonly() || !eventAction('selectionChange') || targetCatalogUnavailable(eventAction('selectionChange'), 'selectionChange')\">\n @for (target of targetOptions(eventAction('selectionChange'), 'selectionChange'); track target.id) {\n <mat-option [value]=\"target.id\">{{ target.label }}</mat-option>\n }\n </mat-select>\n @if (targetCatalogUnavailable(eventAction('selectionChange'), 'selectionChange')) {\n <mat-hint>{{ t('praxis.charts.editor.catalog.targetMissing', 'Target catalog is required for this governed event action.') }}</mat-hint>\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 data-testid=\"chart-editor-event-selectionChange-mapping\"\n rows=\"4\"\n [ngModel]=\"eventMappingText('selectionChange')\"\n (ngModelChange)=\"setEventMapping('selectionChange', $event)\"\n [disabled]=\"isReadonly() || !eventAction('selectionChange')\"\n ></textarea>\n </mat-form-field>\n </mat-card-content>\n </mat-card>\n\n <mat-card class=\"editor-card\" data-testid=\"chart-editor-event-drillDown\">\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 data-testid=\"chart-editor-event-drillDown-action\" [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 <mat-select data-testid=\"chart-editor-event-drillDown-target\" [ngModel]=\"eventTarget('drillDown')\" (ngModelChange)=\"setEventTarget('drillDown', $event)\" [disabled]=\"isReadonly() || !eventAction('drillDown') || targetCatalogUnavailable(eventAction('drillDown'), 'drillDown')\">\n @for (target of targetOptions(eventAction('drillDown'), 'drillDown'); track target.id) {\n <mat-option [value]=\"target.id\">{{ target.label }}</mat-option>\n }\n </mat-select>\n @if (targetCatalogUnavailable(eventAction('drillDown'), 'drillDown')) {\n <mat-hint>{{ t('praxis.charts.editor.catalog.targetMissing', 'Target catalog is required for this governed event action.') }}</mat-hint>\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 data-testid=\"chart-editor-event-drillDown-mapping\"\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\n <mat-card class=\"editor-card\" data-testid=\"chart-editor-event-crossFilter\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.events.crossFilterTitle', 'Cross filter') }}</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 data-testid=\"chart-editor-event-crossFilter-action\" [ngModel]=\"eventAction('crossFilter')\" (ngModelChange)=\"setEventAction('crossFilter', $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 <mat-select data-testid=\"chart-editor-event-crossFilter-target\" [ngModel]=\"eventTarget('crossFilter')\" (ngModelChange)=\"setEventTarget('crossFilter', $event)\" [disabled]=\"isReadonly() || !eventAction('crossFilter') || targetCatalogUnavailable(eventAction('crossFilter'), 'crossFilter')\">\n @for (target of targetOptions(eventAction('crossFilter'), 'crossFilter'); track target.id) {\n <mat-option [value]=\"target.id\">{{ target.label }}</mat-option>\n }\n </mat-select>\n @if (targetCatalogUnavailable(eventAction('crossFilter'), 'crossFilter')) {\n <mat-hint>{{ t('praxis.charts.editor.catalog.targetMissing', 'Target catalog is required for this governed event action.') }}</mat-hint>\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 data-testid=\"chart-editor-event-crossFilter-mapping\"\n rows=\"4\"\n [ngModel]=\"eventMappingText('crossFilter')\"\n (ngModelChange)=\"setEventMapping('crossFilter', $event)\"\n [disabled]=\"isReadonly() || !eventAction('crossFilter')\"\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;container:praxis-chart-editor / inline-size;color:var(--md-sys-color-on-surface, #1a1b20)}.editor-shell{display:grid;gap:18px;min-width:0}.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;min-width:0}.editor-card{box-sizing:border-box;width:100%;min-width:0;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)}.editor-side praxis-chart{display:block;width:100%;min-width:0;max-width:100%}@container praxis-chart-editor (max-width: 840px){.editor-layout{grid-template-columns:minmax(0,1fr)}}@media(max-width:960px){.editor-layout{grid-template-columns:minmax(0,1fr)}}\n"] }]
|
|
7619
|
+
], 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 [attr.data-testid]=\"'chart-editor-section-' + section.id\"\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 data-testid=\"chart-editor-title-input\" [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.sizingMode', 'Sizing') }}</mat-label>\n <mat-select [ngModel]=\"sizingModeValue()\" (ngModelChange)=\"setSizingMode($event)\" [disabled]=\"isReadonly()\">\n @for (mode of sizingModes; track mode) {\n <mat-option [value]=\"mode\">\n {{ t('praxis.charts.editor.sizing.' + mode, mode) }}\n </mat-option>\n }\n </mat-select>\n <mat-hint>{{ t('praxis.charts.editor.hint.sizingMode', 'Use fill-container only when the host widget provides a defined body height.') }}</mat-hint>\n </mat-form-field>\n\n @if (sizingModeValue() === 'fixed') {\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)=\"setSizingHeight($event)\" [disabled]=\"isReadonly()\" />\n <mat-hint>{{ t('praxis.charts.editor.hint.height', 'Numbers are saved as pixels; CSS lengths such as 20rem are also accepted.') }}</mat-hint>\n </mat-form-field>\n }\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.minHeight', 'Minimum height') }}</mat-label>\n <input matInput [ngModel]=\"sizingMinHeightValue()\" (ngModelChange)=\"setSizingMinHeight($event)\" [disabled]=\"isReadonly()\" />\n <mat-hint>{{ t('praxis.charts.editor.hint.minHeight', 'Set a readable minimum for compact dashboard widgets.') }}</mat-hint>\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.maxHeight', 'Maximum height') }}</mat-label>\n <input matInput [ngModel]=\"sizingMaxHeightValue()\" (ngModelChange)=\"setSizingMaxHeight($event)\" [disabled]=\"isReadonly()\" />\n <mat-hint>{{ t('praxis.charts.editor.hint.maxHeight', 'Leave empty unless the chart must stop growing inside a flexible layout.') }}</mat-hint>\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.aspectRatio', 'Aspect ratio') }}</mat-label>\n <input matInput [ngModel]=\"sizingAspectRatioValue()\" (ngModelChange)=\"setSizingAspectRatio($event)\" [disabled]=\"isReadonly()\" />\n <mat-hint>{{ t('praxis.charts.editor.hint.aspectRatio', 'Optional. Use values such as 1.777 or 16 / 9.') }}</mat-hint>\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 data-testid=\"chart-editor-source-kind\" [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 <mat-select data-testid=\"chart-editor-resource\" [ngModel]=\"resourceValue()\" (ngModelChange)=\"setResource($event)\" [disabled]=\"isReadonly() || resourceCatalogUnavailable()\">\n @for (resource of resourceOptions(); track resource.id) {\n <mat-option [value]=\"resource.path\">{{ resource.label }}</mat-option>\n }\n </mat-select>\n @if (resourceCatalogUnavailable()) {\n <mat-hint>{{ t('praxis.charts.editor.catalog.resourceMissing', 'Resource catalog is required for governed praxis.stats authoring.') }}</mat-hint>\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 data-testid=\"chart-editor-operation\"\n [ngModel]=\"doc().source.operation || 'group-by'\"\n (ngModelChange)=\"setOperation($event)\"\n [disabled]=\"isReadonly() || operationCatalogUnavailable()\"\n >\n @for (operation of operationOptions(); 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 @if (operationCatalogUnavailable()) {\n <mat-hint>{{ t('praxis.charts.editor.catalog.operationMissing', 'Selected resource does not publish authorable stats operations.') }}</mat-hint>\n }\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 data-testid=\"chart-editor-distribution-mode\" [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 @if (distributionModeValue() === 'histogram') {\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 data-testid=\"chart-editor-bucket-size\" [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 data-testid=\"chart-editor-bucket-count\" [ngModel]=\"bucketCountValue()\" (ngModelChange)=\"setBucketCount($event)\" [disabled]=\"isReadonly()\" />\n </mat-form-field>\n }\n </mat-card-content>\n </mat-card>\n }\n\n @if (showComparisonControls()) {\n <mat-card class=\"editor-card\" data-testid=\"chart-editor-comparison-options\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.specialization.comparisonTitle', 'Comparison period') }}</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.comparisonPeriodField', 'Period field') }}</mat-label>\n @if (fieldOptions('time').length) {\n <mat-select\n [ngModel]=\"comparisonPeriodFieldValue()\"\n (ngModelChange)=\"setComparisonPeriodField($event)\"\n [disabled]=\"isReadonly()\"\n >\n @for (field of fieldOptions('time'); track field.field) {\n <mat-option [value]=\"field.field\">{{ field.label || field.field }}</mat-option>\n }\n </mat-select>\n } @else {\n <mat-select [ngModel]=\"comparisonPeriodFieldValue()\" [disabled]=\"true\"></mat-select>\n <mat-hint>{{ t('praxis.charts.editor.catalog.comparisonPeriodFieldsMissing', 'No governed time-series field is available for comparison periods.') }}</mat-hint>\n }\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.comparisonTimezone', 'Timezone') }}</mat-label>\n <input\n matInput\n [ngModel]=\"comparisonPeriodTimezoneValue()\"\n (ngModelChange)=\"setComparisonPeriodTimezone($event)\"\n [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.comparisonPreset', 'Period preset') }}</mat-label>\n <mat-select\n [ngModel]=\"comparisonPeriodPresetValue()\"\n (ngModelChange)=\"setComparisonPeriodPreset($event)\"\n [disabled]=\"isReadonly()\"\n >\n @for (preset of comparisonPeriodPresets; track preset) {\n <mat-option [value]=\"preset\">\n {{ t('praxis.charts.editor.comparisonPreset.' + preset, preset) }}\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.comparisonMode', 'Comparison mode') }}</mat-label>\n <mat-select\n [ngModel]=\"comparisonPeriodModeValue()\"\n (ngModelChange)=\"setComparisonPeriodMode($event)\"\n [disabled]=\"isReadonly()\"\n >\n @for (mode of comparisonPeriodModes; track mode) {\n <mat-option [value]=\"mode\">\n {{ t('praxis.charts.editor.comparisonMode.' + mode, mode) }}\n </mat-option>\n }\n </mat-select>\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 class=\"editor-stack\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.themeVariant', 'Theme variant') }}</mat-label>\n <mat-select [ngModel]=\"themeVariantValue()\" (ngModelChange)=\"setThemeVariant($event)\" [disabled]=\"isReadonly()\">\n <mat-option [value]=\"''\">\n {{ t('praxis.charts.editor.themeVariant.none', 'No variant') }}\n </mat-option>\n @for (variant of themeVariants; track variant) {\n <mat-option [value]=\"variant\">\n {{ t('praxis.charts.editor.themeVariant.' + variant, variant) }}\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.paletteMode', 'Palette mode') }}</mat-label>\n <mat-select [ngModel]=\"paletteModeValue()\" (ngModelChange)=\"setPaletteMode($event)\" [disabled]=\"isReadonly()\">\n @for (mode of paletteModes; track mode) {\n <mat-option [value]=\"mode\">\n {{ t('praxis.charts.editor.paletteMode.' + mode, mode) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n @if (paletteModeValue() === 'token') {\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.paletteToken', 'Palette token') }}</mat-label>\n <mat-select [ngModel]=\"paletteTokenValue()\" (ngModelChange)=\"setPaletteToken($event)\" [disabled]=\"isReadonly()\">\n @for (token of paletteTokens; track token) {\n <mat-option [value]=\"token\">\n {{ t('praxis.charts.editor.paletteToken.' + token, token) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n } @else {\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 }\n\n <p class=\"editor-caption\">\n {{ t('praxis.charts.editor.appearance.paletteHint', 'Use a registered token or comma-separated colors to persist theme.palette in the canonical contract.') }}\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.surfaceTitle', 'Surface') }}</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.surfaceMode', 'Surface mode') }}</mat-label>\n <mat-select [ngModel]=\"surfaceModeValue()\" (ngModelChange)=\"setSurfaceMode($event)\" [disabled]=\"isReadonly()\">\n @for (mode of surfaceModes; track mode) {\n <mat-option [value]=\"mode\">\n {{ t('praxis.charts.editor.surface.' + mode, mode) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n <p class=\"editor-caption\">\n {{ t('praxis.charts.editor.appearance.surfaceHint', 'Use embedded for dashboard widgets and contained only when the chart must own its visual surface.') }}\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, donut, funnel and pyramid charts keep one 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\" [attr.data-testid]=\"'chart-editor-dimension-row-' + $index\">\n <div class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\" [attr.data-testid]=\"'chart-editor-dimension-field-' + $index\">\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\">{{ fieldOptionLabel(field) }}</mat-option>\n }\n </mat-select>\n } @else {\n <mat-select [ngModel]=\"dimension.field || ''\" (ngModelChange)=\"setDimensionField($index, $event)\" [disabled]=\"true\"></mat-select>\n <mat-hint>{{ t('praxis.charts.editor.catalog.dimensionFieldsMissing', 'No governed dimensions are available for the current resource and operation.') }}</mat-hint>\n }\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\" [attr.data-testid]=\"'chart-editor-dimension-role-' + $index\">\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\" data-testid=\"chart-editor-add-dimension\" (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\" [attr.data-testid]=\"'chart-editor-metric-row-' + $index\">\n <div class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\" [attr.data-testid]=\"'chart-editor-metric-field-' + $index\">\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\">{{ fieldOptionLabel(field) }}</mat-option>\n }\n </mat-select>\n } @else {\n <mat-select [ngModel]=\"metric.field || ''\" (ngModelChange)=\"setMetricField($index, $event)\" [disabled]=\"true\"></mat-select>\n <mat-hint>{{ t('praxis.charts.editor.catalog.metricFieldsMissing', 'No governed metrics are available for the current resource and operation.') }}</mat-hint>\n }\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\" [attr.data-testid]=\"'chart-editor-metric-label-' + $index\">\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\" [attr.data-testid]=\"'chart-editor-metric-aggregation-' + $index\">\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 metricAggregationOptions(metric.field); 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\" [attr.data-testid]=\"'chart-editor-metric-axis-' + $index\">\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\" [attr.data-testid]=\"'chart-editor-metric-series-kind-' + $index\">\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\" [attr.data-testid]=\"'chart-editor-remove-metric-' + $index\" (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\" data-testid=\"chart-editor-add-metric\" (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\" data-testid=\"chart-editor-event-pointClick\">\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 data-testid=\"chart-editor-event-pointClick-action\" [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 <mat-select data-testid=\"chart-editor-event-pointClick-target\" [ngModel]=\"eventTarget('pointClick')\" (ngModelChange)=\"setEventTarget('pointClick', $event)\" [disabled]=\"isReadonly() || !eventAction('pointClick') || targetCatalogUnavailable(eventAction('pointClick'), 'pointClick')\">\n @for (target of targetOptions(eventAction('pointClick'), 'pointClick'); track target.id) {\n <mat-option [value]=\"target.id\">{{ target.label }}</mat-option>\n }\n </mat-select>\n @if (targetCatalogUnavailable(eventAction('pointClick'), 'pointClick')) {\n <mat-hint>{{ t('praxis.charts.editor.catalog.targetMissing', 'Target catalog is required for this governed event action.') }}</mat-hint>\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 data-testid=\"chart-editor-event-pointClick-mapping\"\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\" data-testid=\"chart-editor-event-selectionChange\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.events.selectionChangeTitle', 'Selection change') }}</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 data-testid=\"chart-editor-event-selectionChange-action\" [ngModel]=\"eventAction('selectionChange')\" (ngModelChange)=\"setEventAction('selectionChange', $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 <mat-select data-testid=\"chart-editor-event-selectionChange-target\" [ngModel]=\"eventTarget('selectionChange')\" (ngModelChange)=\"setEventTarget('selectionChange', $event)\" [disabled]=\"isReadonly() || !eventAction('selectionChange') || targetCatalogUnavailable(eventAction('selectionChange'), 'selectionChange')\">\n @for (target of targetOptions(eventAction('selectionChange'), 'selectionChange'); track target.id) {\n <mat-option [value]=\"target.id\">{{ target.label }}</mat-option>\n }\n </mat-select>\n @if (targetCatalogUnavailable(eventAction('selectionChange'), 'selectionChange')) {\n <mat-hint>{{ t('praxis.charts.editor.catalog.targetMissing', 'Target catalog is required for this governed event action.') }}</mat-hint>\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 data-testid=\"chart-editor-event-selectionChange-mapping\"\n rows=\"4\"\n [ngModel]=\"eventMappingText('selectionChange')\"\n (ngModelChange)=\"setEventMapping('selectionChange', $event)\"\n [disabled]=\"isReadonly() || !eventAction('selectionChange')\"\n ></textarea>\n </mat-form-field>\n </mat-card-content>\n </mat-card>\n\n <mat-card class=\"editor-card\" data-testid=\"chart-editor-event-drillDown\">\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 data-testid=\"chart-editor-event-drillDown-action\" [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 <mat-select data-testid=\"chart-editor-event-drillDown-target\" [ngModel]=\"eventTarget('drillDown')\" (ngModelChange)=\"setEventTarget('drillDown', $event)\" [disabled]=\"isReadonly() || !eventAction('drillDown') || targetCatalogUnavailable(eventAction('drillDown'), 'drillDown')\">\n @for (target of targetOptions(eventAction('drillDown'), 'drillDown'); track target.id) {\n <mat-option [value]=\"target.id\">{{ target.label }}</mat-option>\n }\n </mat-select>\n @if (targetCatalogUnavailable(eventAction('drillDown'), 'drillDown')) {\n <mat-hint>{{ t('praxis.charts.editor.catalog.targetMissing', 'Target catalog is required for this governed event action.') }}</mat-hint>\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 data-testid=\"chart-editor-event-drillDown-mapping\"\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\n <mat-card class=\"editor-card\" data-testid=\"chart-editor-event-crossFilter\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.events.crossFilterTitle', 'Cross filter') }}</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 data-testid=\"chart-editor-event-crossFilter-action\" [ngModel]=\"eventAction('crossFilter')\" (ngModelChange)=\"setEventAction('crossFilter', $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 <mat-select data-testid=\"chart-editor-event-crossFilter-target\" [ngModel]=\"eventTarget('crossFilter')\" (ngModelChange)=\"setEventTarget('crossFilter', $event)\" [disabled]=\"isReadonly() || !eventAction('crossFilter') || targetCatalogUnavailable(eventAction('crossFilter'), 'crossFilter')\">\n @for (target of targetOptions(eventAction('crossFilter'), 'crossFilter'); track target.id) {\n <mat-option [value]=\"target.id\">{{ target.label }}</mat-option>\n }\n </mat-select>\n @if (targetCatalogUnavailable(eventAction('crossFilter'), 'crossFilter')) {\n <mat-hint>{{ t('praxis.charts.editor.catalog.targetMissing', 'Target catalog is required for this governed event action.') }}</mat-hint>\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 data-testid=\"chart-editor-event-crossFilter-mapping\"\n rows=\"4\"\n [ngModel]=\"eventMappingText('crossFilter')\"\n (ngModelChange)=\"setEventMapping('crossFilter', $event)\"\n [disabled]=\"isReadonly() || !eventAction('crossFilter')\"\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;container:praxis-chart-editor / inline-size;color:var(--md-sys-color-on-surface, #1a1b20)}.editor-shell{display:grid;gap:18px;min-width:0}.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;min-width:0}.editor-card{box-sizing:border-box;width:100%;min-width:0;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)}.editor-side praxis-chart{display:block;width:100%;min-width:0;max-width:100%}@container praxis-chart-editor (max-width: 840px){.editor-layout{grid-template-columns:minmax(0,1fr)}}@media(max-width:960px){.editor-layout{grid-template-columns:minmax(0,1fr)}}\n"] }]
|
|
7154
7620
|
}], 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"] }] } });
|
|
7155
7621
|
|
|
7156
7622
|
var praxisChartConfigEditor = /*#__PURE__*/Object.freeze({
|
|
@@ -8361,6 +8827,68 @@ function providePraxisChartShowcaseWidgetMetadata() {
|
|
|
8361
8827
|
};
|
|
8362
8828
|
}
|
|
8363
8829
|
|
|
8830
|
+
const PRAXIS_ANALYTICS_PRESENTATION_COMPONENT_METADATA = {
|
|
8831
|
+
id: 'praxis-analytics-presentation',
|
|
8832
|
+
componentType: 'praxis-analytics-presentation',
|
|
8833
|
+
displayName: 'Praxis Analytics Presentation',
|
|
8834
|
+
selector: 'praxis-analytics-presentation',
|
|
8835
|
+
component: PraxisAnalyticsPresentationComponent,
|
|
8836
|
+
friendlyName: 'Apresentação analítica governada',
|
|
8837
|
+
description: 'Alterna renderers elegíveis para uma projection x-ui.analytics selecionada por id, preservando o query context.',
|
|
8838
|
+
icon: 'monitoring',
|
|
8839
|
+
tags: ['analytics', 'chart', 'table', 'projection', 'dashboard'],
|
|
8840
|
+
lib: '@praxisui/charts',
|
|
8841
|
+
authoringManifestRef: {
|
|
8842
|
+
componentId: 'praxis-analytics-presentation',
|
|
8843
|
+
version: '1.0.0',
|
|
8844
|
+
source: 'PRAXIS_ANALYTICS_PRESENTATION_AUTHORING_MANIFEST',
|
|
8845
|
+
},
|
|
8846
|
+
inputs: [
|
|
8847
|
+
{
|
|
8848
|
+
name: 'analytics',
|
|
8849
|
+
type: 'PraxisXUiAnalytics',
|
|
8850
|
+
description: 'Contrato x-ui.analytics que contém a projection canônica.',
|
|
8851
|
+
default: {
|
|
8852
|
+
projections: [{
|
|
8853
|
+
id: 'analytics-presentation-sample',
|
|
8854
|
+
intent: 'ranking',
|
|
8855
|
+
source: { kind: 'praxis.stats', resource: '/api/sample', operation: 'group-by' },
|
|
8856
|
+
bindings: {
|
|
8857
|
+
primaryDimension: { field: 'category', role: 'category', label: 'Categoria' },
|
|
8858
|
+
primaryMetrics: [{ field: 'value', aggregation: 'sum', label: 'Valor' }],
|
|
8859
|
+
},
|
|
8860
|
+
presentationHints: { preferredFamilies: ['chart', 'analytic-table'] },
|
|
8861
|
+
}],
|
|
8862
|
+
},
|
|
8863
|
+
},
|
|
8864
|
+
{ name: 'projectionId', type: 'string', description: 'Identidade exata da projection; labels não são usados para seleção.', default: 'analytics-presentation-sample' },
|
|
8865
|
+
{ name: 'availableFamilies', type: 'AnalyticsPresentationFamily[]', description: 'Famílias autorizadas após capabilities e renderers instalados.', default: ['chart', 'analytic-table'] },
|
|
8866
|
+
{ name: 'preferredFamily', type: 'AnalyticsPresentationFamily | null', description: 'Preferência persistida pelo host; a escolha temporária do usuário tem precedência.', default: null },
|
|
8867
|
+
{ name: 'queryContext', type: 'PraxisDataQueryContext | null', description: 'Contexto governado compartilhado por chart e analytic-table.', default: null },
|
|
8868
|
+
{ name: 'title', type: 'PraxisTextValue', description: 'Título compartilhado pelos renderers.' },
|
|
8869
|
+
{ name: 'subtitle', type: 'PraxisTextValue', description: 'Subtítulo compartilhado pelos renderers.' },
|
|
8870
|
+
{ name: 'enableCustomization', type: 'boolean', description: 'Habilita authoring interno do chart quando aplicável.', default: false },
|
|
8871
|
+
],
|
|
8872
|
+
outputs: [
|
|
8873
|
+
{ name: 'presentationChange', type: 'PraxisAnalyticsPresentationChange', description: 'Escolha temporária do usuário; o host decide se persiste.' },
|
|
8874
|
+
{ name: 'pointClick', type: 'PraxisChartPointEvent', description: 'Evidência de ponto/linha preservada entre renderers.' },
|
|
8875
|
+
{ name: 'pointAction', type: 'PraxisChartPointActionEvent', description: 'Ação configurada emitida pelo chart.' },
|
|
8876
|
+
{ name: 'selectionChange', type: 'PraxisChartSelectionEvent', description: 'Seleção semântica emitida pelo chart.' },
|
|
8877
|
+
{ name: 'drillDown', type: 'PraxisChartDrillDownEvent', description: 'Drill-down emitido pelo chart.' },
|
|
8878
|
+
{ name: 'crossFilter', type: 'PraxisChartCrossFilterEvent', description: 'Cross-filter emitido pelo chart.' },
|
|
8879
|
+
{ name: 'queryRequest', type: 'PraxisChartQueryRequestEvent', description: 'Observação da consulta remota do chart.' },
|
|
8880
|
+
{ name: 'loadStateChange', type: 'PraxisChartLoadState', description: 'Estado de carga do chart.' },
|
|
8881
|
+
],
|
|
8882
|
+
};
|
|
8883
|
+
function providePraxisAnalyticsPresentationMetadata() {
|
|
8884
|
+
return {
|
|
8885
|
+
provide: ENVIRONMENT_INITIALIZER,
|
|
8886
|
+
multi: true,
|
|
8887
|
+
useFactory: (registry) => () => registry.register(PRAXIS_ANALYTICS_PRESENTATION_COMPONENT_METADATA),
|
|
8888
|
+
deps: [ComponentMetadataRegistry],
|
|
8889
|
+
};
|
|
8890
|
+
}
|
|
8891
|
+
|
|
8364
8892
|
const PRAXIS_TABLE_CHART_DETAIL_RENDERER = {
|
|
8365
8893
|
nodeType: 'chartRef',
|
|
8366
8894
|
renderMode: 'inline',
|
|
@@ -8395,6 +8923,7 @@ function providePraxisCharts(options = {}) {
|
|
|
8395
8923
|
providePraxisChartDrilldownPanelMetadata(),
|
|
8396
8924
|
providePraxisChartStateProbeMetadata(),
|
|
8397
8925
|
providePraxisChartShowcaseWidgetMetadata(),
|
|
8926
|
+
providePraxisAnalyticsPresentationMetadata(),
|
|
8398
8927
|
{
|
|
8399
8928
|
provide: PRAXIS_TABLE_DETAIL_INLINE_RENDERERS,
|
|
8400
8929
|
multi: true,
|
|
@@ -8569,194 +9098,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
|
|
|
8569
9098
|
args: [{ providedIn: 'root' }]
|
|
8570
9099
|
}], ctorParameters: () => [{ type: PraxisChartSchemaMapperService }, { type: PraxisChartCanonicalContractMapperService }] });
|
|
8571
9100
|
|
|
8572
|
-
class AnalyticsChartConfigAdapterService {
|
|
8573
|
-
i18n;
|
|
8574
|
-
statsBuilder = new AnalyticsStatsRequestBuilderService();
|
|
8575
|
-
constructor(i18n) {
|
|
8576
|
-
this.i18n = i18n;
|
|
8577
|
-
}
|
|
8578
|
-
toPraxisChartConfig(projection, options) {
|
|
8579
|
-
const chartType = this.resolveChartType(projection);
|
|
8580
|
-
const orientation = chartType === 'horizontal-bar' ? 'horizontal' : undefined;
|
|
8581
|
-
const dimension = projection.bindings.primaryDimension;
|
|
8582
|
-
const metrics = projection.bindings.primaryMetrics ?? [];
|
|
8583
|
-
if (!dimension?.field) {
|
|
8584
|
-
throw new Error(`AnalyticsChartConfigAdapterService requires primaryDimension for projection "${projection.id}".`);
|
|
8585
|
-
}
|
|
8586
|
-
if (!metrics.length) {
|
|
8587
|
-
throw new Error(`AnalyticsChartConfigAdapterService requires at least one metric for projection "${projection.id}".`);
|
|
8588
|
-
}
|
|
8589
|
-
const crossFilter = Boolean(projection.interactions?.crossFilter);
|
|
8590
|
-
const keyFilterField = dimension.keyFilterField?.trim();
|
|
8591
|
-
if (crossFilter && !keyFilterField) {
|
|
8592
|
-
throw new Error(`AnalyticsChartConfigAdapterService requires primaryDimension.keyFilterField when crossFilter is enabled for projection "${projection.id}".`);
|
|
8593
|
-
}
|
|
8594
|
-
return {
|
|
8595
|
-
id: projection.id,
|
|
8596
|
-
type: chartType,
|
|
8597
|
-
orientation,
|
|
8598
|
-
title: options?.title,
|
|
8599
|
-
subtitle: options?.subtitle,
|
|
8600
|
-
height: options?.height,
|
|
8601
|
-
axes: this.buildAxes(projection, chartType),
|
|
8602
|
-
series: this.buildSeries(projection, chartType),
|
|
8603
|
-
dataSource: this.buildDataSource(projection),
|
|
8604
|
-
interactions: {
|
|
8605
|
-
pointClick: Boolean(projection.interactions?.pointSelection || projection.interactions?.drillDown),
|
|
8606
|
-
drillDown: Boolean(projection.interactions?.drillDown),
|
|
8607
|
-
selection: Boolean(projection.interactions?.pointSelection),
|
|
8608
|
-
crossFilter,
|
|
8609
|
-
...(crossFilter
|
|
8610
|
-
? {
|
|
8611
|
-
eventActions: {
|
|
8612
|
-
crossFilter: {
|
|
8613
|
-
action: 'emit',
|
|
8614
|
-
mapping: { key: keyFilterField },
|
|
8615
|
-
},
|
|
8616
|
-
},
|
|
8617
|
-
}
|
|
8618
|
-
: {}),
|
|
8619
|
-
},
|
|
8620
|
-
};
|
|
8621
|
-
}
|
|
8622
|
-
buildAxes(projection, chartType) {
|
|
8623
|
-
const dimension = projection.bindings.primaryDimension;
|
|
8624
|
-
const metrics = this.getDisplayMetrics(projection);
|
|
8625
|
-
const firstMetric = metrics[0];
|
|
8626
|
-
if (chartType === 'pie' || chartType === 'donut') {
|
|
8627
|
-
return {
|
|
8628
|
-
x: {
|
|
8629
|
-
field: dimension.field,
|
|
8630
|
-
label: dimension.label ?? undefined,
|
|
8631
|
-
},
|
|
8632
|
-
};
|
|
8633
|
-
}
|
|
8634
|
-
return {
|
|
8635
|
-
x: {
|
|
8636
|
-
field: dimension.field,
|
|
8637
|
-
label: dimension.label ?? undefined,
|
|
8638
|
-
type: dimension.role === 'time' ? 'time' : 'category',
|
|
8639
|
-
},
|
|
8640
|
-
y: {
|
|
8641
|
-
label: metrics.length === 1
|
|
8642
|
-
? firstMetric?.label ?? firstMetric?.field
|
|
8643
|
-
: undefined,
|
|
8644
|
-
type: 'value',
|
|
8645
|
-
},
|
|
8646
|
-
};
|
|
8647
|
-
}
|
|
8648
|
-
buildSeries(projection, chartType) {
|
|
8649
|
-
const dimension = projection.bindings.primaryDimension;
|
|
8650
|
-
const comparison = projection.source.operation === 'comparison';
|
|
8651
|
-
return this.getDisplayMetrics(projection).flatMap((metric, index) => comparison
|
|
8652
|
-
? ['current', 'previous'].map((period) => ({
|
|
8653
|
-
id: `${projection.id}.${metric.field}.${period}`,
|
|
8654
|
-
name: `${metric.label ?? metric.field} (${this.comparisonPeriodLabel(period)})`,
|
|
8655
|
-
type: chartType,
|
|
8656
|
-
categoryField: dimension.field,
|
|
8657
|
-
metric: { field: this.comparisonMetricField(metric.field, period), aggregation: this.mapAggregation(metric.aggregation) },
|
|
8658
|
-
}))
|
|
8659
|
-
: [{
|
|
8660
|
-
id: `${projection.id}.${metric.field}.${index + 1}`,
|
|
8661
|
-
name: metric.label ?? metric.field,
|
|
8662
|
-
type: chartType,
|
|
8663
|
-
categoryField: chartType === 'pie' || chartType === 'donut' ? dimension.field : undefined,
|
|
8664
|
-
metric: {
|
|
8665
|
-
field: metric.field,
|
|
8666
|
-
aggregation: this.mapAggregation(metric.aggregation),
|
|
8667
|
-
label: metric.label ?? undefined,
|
|
8668
|
-
},
|
|
8669
|
-
smooth: chartType === 'line' || chartType === 'area',
|
|
8670
|
-
}]);
|
|
8671
|
-
}
|
|
8672
|
-
buildDataSource(projection) {
|
|
8673
|
-
const executionPlan = this.statsBuilder.buildExecutionPlan(projection);
|
|
8674
|
-
return {
|
|
8675
|
-
kind: 'remote',
|
|
8676
|
-
resourcePath: executionPlan.resourcePath,
|
|
8677
|
-
schemaId: executionPlan.resourcePath,
|
|
8678
|
-
query: {
|
|
8679
|
-
sourceKind: 'praxis.stats',
|
|
8680
|
-
statsOperation: executionPlan.operation,
|
|
8681
|
-
statsPath: executionPlan.statsPath,
|
|
8682
|
-
statsRequest: executionPlan.statsRequest,
|
|
8683
|
-
dimensions: executionPlan.dimensions,
|
|
8684
|
-
metrics: projection.source.operation === 'comparison'
|
|
8685
|
-
? this.getDisplayMetrics(projection).flatMap((metric) => ['current', 'previous'].map((period) => ({
|
|
8686
|
-
field: this.comparisonMetricField(metric.field, period),
|
|
8687
|
-
aggregation: this.mapAggregation(metric.aggregation),
|
|
8688
|
-
alias: this.comparisonMetricField(metric.field, period),
|
|
8689
|
-
})))
|
|
8690
|
-
: this.mapExecutionMetrics(executionPlan),
|
|
8691
|
-
sort: executionPlan.sort,
|
|
8692
|
-
limit: executionPlan.limit,
|
|
8693
|
-
},
|
|
8694
|
-
};
|
|
8695
|
-
}
|
|
8696
|
-
resolveChartType(projection) {
|
|
8697
|
-
if (projection.intent === 'trend' && projection.source.operation === 'timeseries') {
|
|
8698
|
-
return 'line';
|
|
8699
|
-
}
|
|
8700
|
-
if (projection.intent === 'ranking' && projection.source.operation === 'group-by') {
|
|
8701
|
-
return 'horizontal-bar';
|
|
8702
|
-
}
|
|
8703
|
-
if (projection.intent === 'composition') {
|
|
8704
|
-
return 'pie';
|
|
8705
|
-
}
|
|
8706
|
-
if (projection.intent === 'distribution') {
|
|
8707
|
-
return 'bar';
|
|
8708
|
-
}
|
|
8709
|
-
return 'bar';
|
|
8710
|
-
}
|
|
8711
|
-
mapAggregation(aggregation) {
|
|
8712
|
-
const normalizedAggregation = (aggregation ?? '').toLowerCase();
|
|
8713
|
-
switch (normalizedAggregation) {
|
|
8714
|
-
case 'avg':
|
|
8715
|
-
case 'min':
|
|
8716
|
-
case 'max':
|
|
8717
|
-
case 'count':
|
|
8718
|
-
case 'distinct-count':
|
|
8719
|
-
case 'sum':
|
|
8720
|
-
return normalizedAggregation;
|
|
8721
|
-
case '':
|
|
8722
|
-
return undefined;
|
|
8723
|
-
default:
|
|
8724
|
-
throw new Error(`Analytics aggregation "${aggregation}" is not supported in @praxisui/charts.`);
|
|
8725
|
-
}
|
|
8726
|
-
}
|
|
8727
|
-
mapExecutionMetrics(executionPlan) {
|
|
8728
|
-
return executionPlan.metrics.map((metric) => ({
|
|
8729
|
-
field: metric.field,
|
|
8730
|
-
aggregation: this.mapAggregation(metric.aggregation),
|
|
8731
|
-
alias: metric.alias,
|
|
8732
|
-
}));
|
|
8733
|
-
}
|
|
8734
|
-
getDisplayMetrics(projection) {
|
|
8735
|
-
return [
|
|
8736
|
-
...(projection.bindings.primaryMetrics ?? []),
|
|
8737
|
-
...(projection.bindings.secondaryMetrics ?? []),
|
|
8738
|
-
];
|
|
8739
|
-
}
|
|
8740
|
-
comparisonMetricField(metricField, period) {
|
|
8741
|
-
return `__praxisComparison_${metricField}_${period}`;
|
|
8742
|
-
}
|
|
8743
|
-
comparisonPeriodLabel(period) {
|
|
8744
|
-
const key = period === 'current'
|
|
8745
|
-
? 'praxis.charts.runtime.comparisonCurrent'
|
|
8746
|
-
: 'praxis.charts.runtime.comparisonPrevious';
|
|
8747
|
-
const fallback = period === 'current' ? 'Current' : 'Previous';
|
|
8748
|
-
return this.i18n?.t(key, undefined, fallback, 'charts') ?? fallback;
|
|
8749
|
-
}
|
|
8750
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: AnalyticsChartConfigAdapterService, deps: [{ token: i1.PraxisI18nService, optional: true }], target: i0.ɵɵFactoryTarget.Injectable });
|
|
8751
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: AnalyticsChartConfigAdapterService, providedIn: 'root' });
|
|
8752
|
-
}
|
|
8753
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: AnalyticsChartConfigAdapterService, decorators: [{
|
|
8754
|
-
type: Injectable,
|
|
8755
|
-
args: [{ providedIn: 'root' }]
|
|
8756
|
-
}], ctorParameters: () => [{ type: i1.PraxisI18nService, decorators: [{
|
|
8757
|
-
type: Optional
|
|
8758
|
-
}] }] });
|
|
8759
|
-
|
|
8760
9101
|
class AnalyticsChartContractService {
|
|
8761
9102
|
analyticsSchema;
|
|
8762
9103
|
resolver;
|
|
@@ -10106,7 +10447,7 @@ const chartDocumentSchema = {
|
|
|
10106
10447
|
required: ['version', 'kind', 'source'],
|
|
10107
10448
|
properties: {
|
|
10108
10449
|
version: { const: PRAXIS_X_UI_CHART_AUTHORABLE_VERSION },
|
|
10109
|
-
kind: { enum: ['bar', 'combo', 'horizontal-bar', 'line', 'pie', 'donut', 'area', 'stacked-bar', 'stacked-area', 'scatter'] },
|
|
10450
|
+
kind: { enum: ['bar', 'combo', 'horizontal-bar', 'line', 'pie', 'donut', 'area', 'stacked-bar', 'stacked-area', 'scatter', 'funnel', 'pyramid'] },
|
|
10110
10451
|
chartId: { type: 'string' },
|
|
10111
10452
|
title: { oneOf: [{ type: 'string' }, { type: 'object' }] },
|
|
10112
10453
|
subtitle: { oneOf: [{ type: 'string' }, { type: 'object' }] },
|
|
@@ -10135,7 +10476,7 @@ const PRAXIS_CHARTS_AUTHORING_MANIFEST = {
|
|
|
10135
10476
|
componentId: 'praxis-chart',
|
|
10136
10477
|
ownerPackage: '@praxisui/charts',
|
|
10137
10478
|
configSchemaId: 'PraxisXUiChartContract',
|
|
10138
|
-
manifestVersion: '1.
|
|
10479
|
+
manifestVersion: '1.2.0',
|
|
10139
10480
|
runtimeInputs: [
|
|
10140
10481
|
{ name: 'chartDocument', type: 'PraxisXUiChartContract | null', description: 'Canonical x-ui.chart document used as the authoring source of truth.' },
|
|
10141
10482
|
{ name: 'config', type: 'PraxisChartConfig', description: 'Runtime chart configuration mapped from or supplied beside the canonical chart document.' },
|
|
@@ -10180,11 +10521,11 @@ const PRAXIS_CHARTS_AUTHORING_MANIFEST = {
|
|
|
10180
10521
|
scope: 'global',
|
|
10181
10522
|
targetKind: 'chartType',
|
|
10182
10523
|
target: { kind: 'chartType', resolver: 'x-ui-chart-kind', ambiguityPolicy: 'fail', required: false },
|
|
10183
|
-
inputSchema: { type: 'object', required: ['kind'], properties: { kind: { enum: ['bar', 'combo', 'horizontal-bar', 'line', 'pie', 'donut', 'area', 'stacked-bar', 'stacked-area', 'scatter'] }, orientation: { enum: ['vertical', 'horizontal'] } } },
|
|
10524
|
+
inputSchema: { type: 'object', required: ['kind'], properties: { kind: { enum: ['bar', 'combo', 'horizontal-bar', 'line', 'pie', 'donut', 'area', 'stacked-bar', 'stacked-area', 'scatter', 'funnel', 'pyramid'] }, orientation: { enum: ['vertical', 'horizontal'] } } },
|
|
10184
10525
|
effects: [{ kind: 'merge-object', path: 'chartDocument' }],
|
|
10185
10526
|
destructive: false,
|
|
10186
10527
|
requiresConfirmation: false,
|
|
10187
|
-
validators: ['chart-type-supported', 'chart-type-series-axis-compatible', '
|
|
10528
|
+
validators: ['chart-type-supported', 'chart-type-series-axis-compatible', 'category-slice-single-metric', 'combo-minimum-series', 'editor-runtime-round-trip'],
|
|
10188
10529
|
affectedPaths: ['chartDocument.kind', 'chartDocument.orientation', 'chartDocument.metrics[]', 'chartDocument.dimensions[]'],
|
|
10189
10530
|
submissionImpact: 'config-only',
|
|
10190
10531
|
preconditions: ['config-initialized'],
|
|
@@ -10455,7 +10796,7 @@ const PRAXIS_CHARTS_AUTHORING_MANIFEST = {
|
|
|
10455
10796
|
{ validatorId: 'chart-version-supported', level: 'error', code: 'CHART_VERSION_SUPPORTED', description: `Only x-ui.chart version ${PRAXIS_X_UI_CHART_AUTHORABLE_VERSION} is authorable.` },
|
|
10456
10797
|
{ validatorId: 'chart-type-supported', level: 'error', code: 'CHART_TYPE_SUPPORTED', description: 'Chart kind must be supported by @praxisui/charts.' },
|
|
10457
10798
|
{ validatorId: 'chart-type-series-axis-compatible', level: 'error', code: 'CHART_TYPE_SERIES_AXIS_COMPATIBLE', description: 'Chart kind, dimensions, metrics and axes must be compatible.' },
|
|
10458
|
-
{ validatorId: '
|
|
10799
|
+
{ validatorId: 'category-slice-single-metric', level: 'error', code: 'CATEGORY_SLICE_SINGLE_METRIC', description: 'Pie, donut, funnel and pyramid charts require exactly one metric.' },
|
|
10459
10800
|
{ validatorId: 'combo-minimum-series', level: 'error', code: 'COMBO_MINIMUM_SERIES', description: 'Combo charts require at least two metrics.' },
|
|
10460
10801
|
{ validatorId: 'series-field-exists', level: 'error', code: 'SERIES_FIELD_EXISTS', description: 'Series field must exist in availableFields or schema context.' },
|
|
10461
10802
|
{ validatorId: 'series-field-aggregable', level: 'error', code: 'SERIES_FIELD_AGGREGABLE', description: 'Metric fields must support the requested aggregation.' },
|
|
@@ -10516,6 +10857,99 @@ const PRAXIS_CHARTS_AUTHORING_MANIFEST = {
|
|
|
10516
10857
|
],
|
|
10517
10858
|
};
|
|
10518
10859
|
|
|
10860
|
+
const PRAXIS_ANALYTICS_PRESENTATION_AUTHORING_MANIFEST = {
|
|
10861
|
+
schemaVersion: '1.0.0',
|
|
10862
|
+
componentId: 'praxis-analytics-presentation',
|
|
10863
|
+
ownerPackage: '@praxisui/charts',
|
|
10864
|
+
configSchemaId: 'PraxisAnalyticsPresentationInputs',
|
|
10865
|
+
manifestVersion: '1.0.0',
|
|
10866
|
+
runtimeInputs: [
|
|
10867
|
+
{ name: 'analytics', type: 'PraxisXUiAnalytics', description: 'Canonical x-ui.analytics projections.' },
|
|
10868
|
+
{ name: 'projectionId', type: 'string', description: 'Stable projection identity; labels never select projections.' },
|
|
10869
|
+
{ name: 'availableFamilies', type: 'AnalyticsPresentationFamily[]', description: 'Capability- and host-authorized renderer families.' },
|
|
10870
|
+
{ name: 'preferredFamily', type: 'AnalyticsPresentationFamily | null', allowedValues: ['chart', 'analytic-table'], description: 'Persisted host preference. Temporary user choice remains runtime-only.' },
|
|
10871
|
+
{ name: 'queryContext', type: 'PraxisDataQueryContext | null', description: 'Shared governed query context forwarded to the selected renderer.' },
|
|
10872
|
+
],
|
|
10873
|
+
editableTargets: [
|
|
10874
|
+
{
|
|
10875
|
+
kind: 'analyticsPresentationPreference',
|
|
10876
|
+
resolver: 'component-config',
|
|
10877
|
+
description: 'Persisted preferredFamily for the projection selected by exact projectionId.',
|
|
10878
|
+
},
|
|
10879
|
+
],
|
|
10880
|
+
operations: [
|
|
10881
|
+
{
|
|
10882
|
+
operationId: 'analytics.presentation.prefer-family',
|
|
10883
|
+
title: 'Prefer analytics presentation family',
|
|
10884
|
+
description: 'Persists a renderer-family preference without changing the canonical projection, bindings or query context.',
|
|
10885
|
+
scope: 'global',
|
|
10886
|
+
targetKind: 'analyticsPresentationPreference',
|
|
10887
|
+
target: {
|
|
10888
|
+
kind: 'analyticsPresentationPreference',
|
|
10889
|
+
resolver: 'component-config',
|
|
10890
|
+
ambiguityPolicy: 'fail',
|
|
10891
|
+
required: false,
|
|
10892
|
+
},
|
|
10893
|
+
inputSchema: {
|
|
10894
|
+
type: 'object',
|
|
10895
|
+
additionalProperties: false,
|
|
10896
|
+
required: ['family'],
|
|
10897
|
+
properties: { family: { enum: ['chart', 'analytic-table'] } },
|
|
10898
|
+
},
|
|
10899
|
+
effects: [{ kind: 'set-value', path: 'preferredFamily', inputPath: 'family' }],
|
|
10900
|
+
destructive: false,
|
|
10901
|
+
requiresConfirmation: false,
|
|
10902
|
+
validators: ['diagnostics-before-patch', 'renderer-supported', 'runtime-component-resolves'],
|
|
10903
|
+
affectedPaths: ['preferredFamily'],
|
|
10904
|
+
submissionImpact: 'visual-only',
|
|
10905
|
+
preconditions: ['analytics-projection-resolved'],
|
|
10906
|
+
},
|
|
10907
|
+
],
|
|
10908
|
+
validators: [
|
|
10909
|
+
{ validatorId: 'diagnostics-before-patch', level: 'error', code: 'ANALYTICS_DIAGNOSTICS_BEFORE_PATCH', description: 'Projection resolution and family eligibility diagnostics must be clear before changing the preference.' },
|
|
10910
|
+
{ validatorId: 'renderer-supported', level: 'error', code: 'ANALYTICS_RENDERER_SUPPORTED', description: 'The requested family must be a renderer supported by the component manifest.' },
|
|
10911
|
+
{ validatorId: 'runtime-component-resolves', level: 'error', code: 'ANALYTICS_RUNTIME_COMPONENT_RESOLVES', description: 'The analytics presentation runtime must be registered before persisting its preference.' },
|
|
10912
|
+
],
|
|
10913
|
+
roundTripRequirements: [
|
|
10914
|
+
'Save and reopen must preserve preferredFamily without copying remote analytics results into the page document.',
|
|
10915
|
+
'Changing preferredFamily must preserve analytics, projectionId and queryContext byte-for-byte.',
|
|
10916
|
+
'Temporary user choice emitted by presentationChange must not mutate preferredFamily unless the host explicitly persists it.',
|
|
10917
|
+
],
|
|
10918
|
+
presentationAffordances: {
|
|
10919
|
+
version: '1.0.0',
|
|
10920
|
+
componentId: 'praxis-analytics-presentation',
|
|
10921
|
+
defaultTargetKind: 'analyticsPresentationPreference',
|
|
10922
|
+
sourceRef: 'AnalyticsPresentationResolver',
|
|
10923
|
+
affordances: [
|
|
10924
|
+
{
|
|
10925
|
+
id: 'analytics-presentation-family',
|
|
10926
|
+
targetKind: 'analyticsPresentationPreference',
|
|
10927
|
+
category: 'analytics-renderer',
|
|
10928
|
+
description: 'Compatible presentation families resolved from the canonical projection and installed renderers.',
|
|
10929
|
+
options: ['chart', 'analytic-table'],
|
|
10930
|
+
appliesToTypes: ['PraxisXUiAnalytics'],
|
|
10931
|
+
unknownCompatible: false,
|
|
10932
|
+
},
|
|
10933
|
+
],
|
|
10934
|
+
},
|
|
10935
|
+
examples: [
|
|
10936
|
+
{
|
|
10937
|
+
id: 'prefer-analytic-table',
|
|
10938
|
+
request: 'Use the table presentation for projection supplier-procurement-funnel.',
|
|
10939
|
+
operationId: 'analytics.presentation.prefer-family',
|
|
10940
|
+
params: { family: 'analytic-table' },
|
|
10941
|
+
isPositive: true,
|
|
10942
|
+
},
|
|
10943
|
+
{
|
|
10944
|
+
id: 'reject-kpi-without-renderer',
|
|
10945
|
+
request: 'Use KPI even though no KPI renderer is installed.',
|
|
10946
|
+
operationId: 'analytics.presentation.prefer-family',
|
|
10947
|
+
params: { family: 'kpi' },
|
|
10948
|
+
isPositive: false,
|
|
10949
|
+
},
|
|
10950
|
+
],
|
|
10951
|
+
};
|
|
10952
|
+
|
|
10519
10953
|
/*
|
|
10520
10954
|
* Public API Surface of praxis-charts
|
|
10521
10955
|
*/
|
|
@@ -10524,4 +10958,4 @@ const PRAXIS_CHARTS_AUTHORING_MANIFEST = {
|
|
|
10524
10958
|
* Generated bundle index. Do not edit.
|
|
10525
10959
|
*/
|
|
10526
10960
|
|
|
10527
|
-
export { AnalyticsChartConfigAdapterService, AnalyticsChartContractService, ChartContractNormalizerService, ChartContractValidationService, ChartEditorDefaultsService, ChartEditorPreviewMapperService, ChartResourceCapabilityCatalogAdapter, PRAXIS_CHARTS_AUTHORING_MANIFEST, PRAXIS_CHARTS_I18N, PRAXIS_CHART_AUTHORING_COVERAGE, 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_ENGINE_FACTORY, PRAXIS_CHART_PALETTE_TOKENS, PRAXIS_CHART_STATE_PROBE_COMPONENT_METADATA, PRAXIS_CHART_THEME_VARIANTS, PRAXIS_X_UI_CHART_AUTHORABLE_VERSION, PraxisChartBackendPayloadAdapterService, PraxisChartCanonicalContractMapperService, PraxisChartComponent, PraxisChartCompositionShowcaseComponent, PraxisChartConfigEditor, PraxisChartDataTransformerService, PraxisChartDrilldownPanelComponent, PraxisChartSchemaMapperService, PraxisChartStateProbeComponent, PraxisChartStatsApiService, PraxisChartWidgetConfigEditor, PraxisMicroVisualizationComponent, buildPraxisChartInteractiveCanvasPage, buildPraxisChartInteractiveWidgetPage, buildPraxisChartMockCanvasPage, buildPraxisChartMockWidgetPage, createPraxisChartsI18nConfig, isPraxisChartPaletteToken, providePraxisChartDrilldownPanelMetadata, providePraxisChartStateProbeMetadata, providePraxisCharts, providePraxisChartsI18n, providePraxisChartsMetadata, resolvePraxisChartPaletteToken, resolvePraxisChartsText };
|
|
10961
|
+
export { AnalyticsChartConfigAdapterService, AnalyticsChartContractService, ChartContractNormalizerService, ChartContractValidationService, ChartEditorDefaultsService, ChartEditorPreviewMapperService, ChartResourceCapabilityCatalogAdapter, PRAXIS_ANALYTICS_PRESENTATION_AUTHORING_MANIFEST, PRAXIS_ANALYTICS_PRESENTATION_COMPONENT_METADATA, PRAXIS_CHARTS_AUTHORING_MANIFEST, PRAXIS_CHARTS_I18N, PRAXIS_CHART_AUTHORING_COVERAGE, 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_ENGINE_FACTORY, PRAXIS_CHART_PALETTE_TOKENS, PRAXIS_CHART_STATE_PROBE_COMPONENT_METADATA, PRAXIS_CHART_THEME_VARIANTS, PRAXIS_X_UI_CHART_AUTHORABLE_VERSION, PraxisAnalyticsPresentationComponent, PraxisChartBackendPayloadAdapterService, PraxisChartCanonicalContractMapperService, PraxisChartComponent, PraxisChartCompositionShowcaseComponent, PraxisChartConfigEditor, PraxisChartDataTransformerService, PraxisChartDrilldownPanelComponent, PraxisChartSchemaMapperService, PraxisChartStateProbeComponent, PraxisChartStatsApiService, PraxisChartWidgetConfigEditor, PraxisMicroVisualizationComponent, buildPraxisChartInteractiveCanvasPage, buildPraxisChartInteractiveWidgetPage, buildPraxisChartMockCanvasPage, buildPraxisChartMockWidgetPage, createPraxisChartsI18nConfig, isPraxisChartPaletteToken, providePraxisAnalyticsPresentationMetadata, providePraxisChartDrilldownPanelMetadata, providePraxisChartStateProbeMetadata, providePraxisCharts, providePraxisChartsI18n, providePraxisChartsMetadata, resolvePraxisChartPaletteToken, resolvePraxisChartsText };
|