@praxisui/charts 9.0.32 → 9.0.34

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.
@@ -11,7 +11,7 @@ import { HttpErrorResponse } from '@angular/common/http';
11
11
  import { catchError } from 'rxjs/operators';
12
12
  import { AnalyticsTableConfigAdapterService, AnalyticsTableStatsApiService, ANALYTICS_TABLE_ROW_KEY_FIELD, PraxisTable } from '@praxisui/table';
13
13
  import { use, init } from 'echarts/core';
14
- import { BarChart, FunnelChart, LineChart, PieChart, ScatterChart } from 'echarts/charts';
14
+ import { BarChart, FunnelChart, GaugeChart, LineChart, PieChart, ScatterChart, TreemapChart } from 'echarts/charts';
15
15
  import { AriaComponent, DatasetComponent, GridComponent, LegendComponent, TitleComponent, TooltipComponent, TransformComponent } from 'echarts/components';
16
16
  import { CanvasRenderer } from 'echarts/renderers';
17
17
  import * as i1$3 from '@angular/forms';
@@ -84,11 +84,70 @@ class PraxisChartDataTransformerService {
84
84
  if (config.type === 'funnel' || config.type === 'pyramid') {
85
85
  return this.transformCategorySlices(config, rows, 'funnel');
86
86
  }
87
+ if (config.type === 'treemap') {
88
+ return this.transformCategorySlices(config, rows, 'treemap');
89
+ }
90
+ if (config.type === 'gauge') {
91
+ return this.transformGauge(config, rows);
92
+ }
87
93
  if (config.type === 'scatter') {
88
94
  return this.transformScatter(config, rows);
89
95
  }
90
96
  return this.transformCartesian(config, rows);
91
97
  }
98
+ transformGauge(config, rows) {
99
+ const scale = config.gauge?.scale;
100
+ if (!scale
101
+ || !Number.isFinite(scale.min)
102
+ || !Number.isFinite(scale.max)
103
+ || scale.max <= scale.min) {
104
+ return this.gaugeDiagnostic('gauge-scale-invalid', 'Gauge rendering requires a finite scale with max greater than min.');
105
+ }
106
+ if (rows.length !== 1) {
107
+ return this.gaugeDiagnostic('gauge-bucket-cardinality', `Gauge rendering requires exactly one bucket; received ${rows.length}.`);
108
+ }
109
+ const seriesConfig = config.series[0];
110
+ const row = rows[0];
111
+ const value = seriesConfig ? this.extractStrictMetricValue(row, seriesConfig) : null;
112
+ if (!seriesConfig || value === null) {
113
+ return this.gaugeDiagnostic('gauge-value-invalid', 'Gauge rendering requires one finite metric value.');
114
+ }
115
+ if (value < scale.min || value > scale.max) {
116
+ return this.gaugeDiagnostic('gauge-value-out-of-range', `Gauge value ${value} is outside the configured scale [${scale.min}, ${scale.max}].`);
117
+ }
118
+ const categoryField = seriesConfig.categoryField ?? config.axes?.x?.field;
119
+ const category = categoryField ? row[categoryField] : row['label'];
120
+ const name = row['label'] !== null && row['label'] !== undefined && row['label'] !== ''
121
+ ? this.normalizeCategory(row['label'])
122
+ : category !== null && category !== undefined && category !== ''
123
+ ? this.normalizeCategory(category)
124
+ : seriesConfig.name ?? seriesConfig.metric?.label ?? seriesConfig.id;
125
+ const gauge = {
126
+ id: `${seriesConfig.id}:${this.normalizeCategory(row['key'] ?? name)}`,
127
+ name,
128
+ value,
129
+ color: seriesConfig.color,
130
+ data: this.buildPointData(row, value),
131
+ };
132
+ return {
133
+ mode: 'gauge',
134
+ categories: [name],
135
+ series: [],
136
+ slices: [],
137
+ gauge,
138
+ hasData: true,
139
+ };
140
+ }
141
+ gaugeDiagnostic(code, message) {
142
+ return {
143
+ mode: 'gauge',
144
+ categories: [],
145
+ series: [],
146
+ slices: [],
147
+ hasData: false,
148
+ diagnostic: { code, message },
149
+ };
150
+ }
92
151
  transformCartesian(config, rows) {
93
152
  const categoryField = config.axes?.x?.field;
94
153
  if (!categoryField || !rows.length || !config.series.length) {
@@ -372,6 +431,21 @@ class PraxisChartDataTransformerService {
372
431
  }
373
432
  return 0;
374
433
  }
434
+ extractStrictMetricValue(row, seriesConfig) {
435
+ const field = seriesConfig.metric?.field;
436
+ if (!field) {
437
+ return null;
438
+ }
439
+ const value = row[field];
440
+ if (typeof value === 'number') {
441
+ return Number.isFinite(value) ? value : null;
442
+ }
443
+ if (typeof value === 'string' && value.trim()) {
444
+ const parsed = Number(value);
445
+ return Number.isFinite(parsed) ? parsed : null;
446
+ }
447
+ return null;
448
+ }
375
449
  normalizeCategory(value) {
376
450
  if (value instanceof Date) {
377
451
  return value.toISOString();
@@ -478,6 +552,11 @@ class ChartContractNormalizerService {
478
552
  }
479
553
  normalizeKindSpecificFields(document) {
480
554
  let nextDocument = document;
555
+ if (document.kind !== 'gauge' && document.gauge !== undefined) {
556
+ const withoutGauge = { ...nextDocument };
557
+ delete withoutGauge.gauge;
558
+ nextDocument = withoutGauge;
559
+ }
481
560
  if (document.kind === 'horizontal-bar') {
482
561
  nextDocument = {
483
562
  ...nextDocument,
@@ -545,7 +624,8 @@ class ChartContractNormalizerService {
545
624
  return document.kind === 'pie'
546
625
  || document.kind === 'donut'
547
626
  || document.kind === 'funnel'
548
- || document.kind === 'pyramid';
627
+ || document.kind === 'pyramid'
628
+ || document.kind === 'treemap';
549
629
  }
550
630
  normalizeCssSizeValue(value) {
551
631
  if (typeof value !== 'string') {
@@ -729,6 +809,25 @@ class ChartContractValidationService {
729
809
  }
730
810
  }
731
811
  validateKinds(document, issues) {
812
+ const supportedKinds = [
813
+ 'bar',
814
+ 'combo',
815
+ 'horizontal-bar',
816
+ 'line',
817
+ 'pie',
818
+ 'donut',
819
+ 'area',
820
+ 'stacked-bar',
821
+ 'stacked-area',
822
+ 'scatter',
823
+ 'funnel',
824
+ 'pyramid',
825
+ 'treemap',
826
+ 'gauge',
827
+ ];
828
+ if (!supportedKinds.includes(document.kind)) {
829
+ issues.push(this.error('unsupported-kind', 'kind', `x-ui.chart kind="${document.kind}" is not supported in @praxisui/charts.`));
830
+ }
732
831
  const metricCount = document.metrics?.length ?? 0;
733
832
  const categorySliceChart = this.isCategorySliceChart(document.kind);
734
833
  document.dimensions?.forEach((dimension, index) => {
@@ -736,18 +835,19 @@ class ChartContractValidationService {
736
835
  issues.push(this.error('dimension-field-required', `dimensions[${index}].field`, 'x-ui.chart dimension.field is required.'));
737
836
  }
738
837
  });
739
- if (!categorySliceChart && !document.dimensions?.length) {
838
+ if (!categorySliceChart && document.kind !== 'gauge' && !document.dimensions?.length) {
740
839
  issues.push(this.error('missing-dimension', 'dimensions', 'x-ui.chart cartesian charts require at least one dimension.'));
741
840
  }
742
841
  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.'));
842
+ issues.push(this.error('category-slice-missing-dimension', 'dimensions[0].field', 'x-ui.chart pie, donut, funnel, pyramid and treemap charts require a first dimension for category mapping.'));
744
843
  }
745
844
  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.'));
845
+ issues.push(this.error('category-slice-single-metric', 'metrics', 'x-ui.chart pie, donut, funnel, pyramid and treemap charts require exactly one metric in @praxisui/charts.'));
747
846
  }
748
847
  if (document.kind === 'combo' && metricCount < 2) {
749
848
  issues.push(this.error('combo-min-metrics', 'metrics', 'x-ui.chart combo charts require at least two metrics.'));
750
849
  }
850
+ this.validateGauge(document, issues);
751
851
  if (document.kind !== 'combo' && document.metrics?.some((metric) => metric.axis === 'secondary')) {
752
852
  issues.push(this.error('secondary-axis-non-combo', 'metrics', 'x-ui.chart axis="secondary" is supported only for combo charts in @praxisui/charts.'));
753
853
  }
@@ -771,8 +871,51 @@ class ChartContractValidationService {
771
871
  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.'));
772
872
  }
773
873
  }
874
+ validateGauge(document, issues) {
875
+ if (document.kind !== 'gauge') {
876
+ return;
877
+ }
878
+ if (!document.dimensions?.length) {
879
+ issues.push(this.error('gauge-missing-dimension', 'dimensions', 'x-ui.chart gauge charts require at least one dimension.'));
880
+ }
881
+ if ((document.metrics?.length ?? 0) !== 1) {
882
+ issues.push(this.error('gauge-single-metric', 'metrics', 'x-ui.chart gauge charts require exactly one metric.'));
883
+ }
884
+ const scale = document.gauge?.scale;
885
+ if (!scale) {
886
+ issues.push(this.error('gauge-scale-required', 'gauge.scale', 'x-ui.chart gauge.scale is required for gauge charts.'));
887
+ }
888
+ else {
889
+ if (typeof scale.min !== 'number' || !Number.isFinite(scale.min)) {
890
+ issues.push(this.error('gauge-scale-min-invalid', 'gauge.scale.min', 'x-ui.chart gauge.scale.min must be a finite number.'));
891
+ }
892
+ if (typeof scale.max !== 'number' || !Number.isFinite(scale.max)) {
893
+ issues.push(this.error('gauge-scale-max-invalid', 'gauge.scale.max', 'x-ui.chart gauge.scale.max must be a finite number.'));
894
+ }
895
+ if (typeof scale.min === 'number'
896
+ && Number.isFinite(scale.min)
897
+ && typeof scale.max === 'number'
898
+ && Number.isFinite(scale.max)
899
+ && scale.max <= scale.min) {
900
+ issues.push(this.error('gauge-scale-order-invalid', 'gauge.scale.max', 'x-ui.chart gauge.scale.max must be greater than gauge.scale.min.'));
901
+ }
902
+ }
903
+ if (document.source.kind !== 'praxis.stats') {
904
+ return;
905
+ }
906
+ if (document.source.operation !== 'group-by') {
907
+ issues.push(this.error('gauge-operation-unsupported', 'source.operation', 'x-ui.chart gauge charts over praxis.stats require operation="group-by".'));
908
+ }
909
+ if (document.source.options?.limit !== 1) {
910
+ issues.push(this.error('gauge-limit-required', 'source.options.limit', 'x-ui.chart gauge charts over praxis.stats require source.options.limit=1.'));
911
+ }
912
+ }
774
913
  isCategorySliceChart(kind) {
775
- return kind === 'pie' || kind === 'donut' || kind === 'funnel' || kind === 'pyramid';
914
+ return kind === 'pie'
915
+ || kind === 'donut'
916
+ || kind === 'funnel'
917
+ || kind === 'pyramid'
918
+ || kind === 'treemap';
776
919
  }
777
920
  validateDistribution(document, issues) {
778
921
  if (document.source.kind !== 'praxis.stats'
@@ -919,6 +1062,14 @@ class PraxisChartCanonicalContractMapperService {
919
1062
  series: this.buildSeries(normalizedContract),
920
1063
  dataSource: this.buildDataSource(normalizedContract),
921
1064
  interactions: this.buildInteractions(normalizedContract),
1065
+ gauge: normalizedContract.gauge
1066
+ ? {
1067
+ scale: {
1068
+ min: normalizedContract.gauge.scale.min,
1069
+ max: normalizedContract.gauge.scale.max,
1070
+ },
1071
+ }
1072
+ : undefined,
922
1073
  theme: this.buildTheme(normalizedContract),
923
1074
  motion: this.buildMotion(normalizedContract.motion),
924
1075
  emptyState: normalizedContract.state?.empty
@@ -945,7 +1096,7 @@ class PraxisChartCanonicalContractMapperService {
945
1096
  const secondaryMetric = contract.metrics?.find((metric) => metric.axis === 'secondary');
946
1097
  const metricCount = (contract.metrics?.length ?? 0)
947
1098
  * (contract.source.kind === 'praxis.stats' && contract.source.operation === 'comparison' ? 2 : 1);
948
- if (this.isCategorySliceChart(contract.kind)) {
1099
+ if (this.isCategorySliceChart(contract.kind) || contract.kind === 'gauge') {
949
1100
  return {
950
1101
  x: {
951
1102
  field: firstDimension?.field,
@@ -1005,7 +1156,7 @@ class PraxisChartCanonicalContractMapperService {
1005
1156
  name: period ? `${baseName} (${this.comparisonPeriodLabel(period)})` : baseName,
1006
1157
  type: this.resolveSeriesType(contract.kind, metric.seriesKind, index),
1007
1158
  axis: metric.axis ?? 'primary',
1008
- categoryField: comparison || this.isCategorySliceChart(contract.kind)
1159
+ categoryField: comparison || this.isCategorySliceChart(contract.kind) || contract.kind === 'gauge'
1009
1160
  ? firstDimension?.field
1010
1161
  : undefined,
1011
1162
  metric: {
@@ -1099,7 +1250,7 @@ class PraxisChartCanonicalContractMapperService {
1099
1250
  tooltip: {
1100
1251
  ...(variantTheme?.tooltip ?? {}),
1101
1252
  enabled: this.resolveToggle(contract.tooltip, true),
1102
- trigger: this.isCategorySliceChart(contract.kind) || contract.kind === 'scatter' ? 'item' : 'axis',
1253
+ trigger: this.isCategorySliceChart(contract.kind) || contract.kind === 'scatter' || contract.kind === 'gauge' ? 'item' : 'axis',
1103
1254
  },
1104
1255
  surface,
1105
1256
  backgroundColor: surface?.background ?? variantTheme?.backgroundColor,
@@ -1170,7 +1321,11 @@ class PraxisChartCanonicalContractMapperService {
1170
1321
  return contract.orientation;
1171
1322
  }
1172
1323
  isCategorySliceChart(kind) {
1173
- return kind === 'pie' || kind === 'donut' || kind === 'funnel' || kind === 'pyramid';
1324
+ return kind === 'pie'
1325
+ || kind === 'donut'
1326
+ || kind === 'funnel'
1327
+ || kind === 'pyramid'
1328
+ || kind === 'treemap';
1174
1329
  }
1175
1330
  resolveSeriesType(chartKind, seriesKind, index) {
1176
1331
  if (chartKind === 'combo') {
@@ -1841,6 +1996,17 @@ class PraxisChartComponent {
1841
1996
  return config;
1842
1997
  }, ...(ngDevMode ? [{ debugName: "renderConfig" }] : /* istanbul ignore next */ []));
1843
1998
  loadingLabel = computed(() => this.i18n.resolve(this.renderConfig().state?.loadingLabel), ...(ngDevMode ? [{ debugName: "loadingLabel" }] : /* istanbul ignore next */ []));
1999
+ transformedData = computed(() => this.transformer.transform(this.renderConfig(), this.resolvedData()), ...(ngDevMode ? [{ debugName: "transformedData" }] : /* istanbul ignore next */ []));
2000
+ dataDiagnostic = computed(() => {
2001
+ const config = this.renderConfig();
2002
+ const explicitData = this.data();
2003
+ if (config.dataSource?.kind === 'remote'
2004
+ && (explicitData === null || explicitData === undefined)
2005
+ && this.remoteRuntimeState() !== 'ready') {
2006
+ return null;
2007
+ }
2008
+ return this.transformedData().diagnostic ?? null;
2009
+ }, ...(ngDevMode ? [{ debugName: "dataDiagnostic" }] : /* istanbul ignore next */ []));
1844
2010
  emptyTitle = computed(() => this.i18n.resolve(this.renderConfig().emptyState?.title), ...(ngDevMode ? [{ debugName: "emptyTitle" }] : /* istanbul ignore next */ []));
1845
2011
  emptyDescription = computed(() => this.i18n.resolve(this.renderConfig().emptyState?.description), ...(ngDevMode ? [{ debugName: "emptyDescription" }] : /* istanbul ignore next */ []));
1846
2012
  errorTitle = computed(() => this.chartDocumentMappingError()
@@ -1848,21 +2014,28 @@ class PraxisChartComponent {
1848
2014
  key: 'praxis.charts.runtime.invalidDocumentTitle',
1849
2015
  text: 'Chart configuration is invalid',
1850
2016
  })
1851
- : this.i18n.resolve(this.renderConfig().state?.error?.title)
1852
- || this.i18n.resolve({
1853
- key: 'praxis.charts.runtime.remoteErrorTitle',
1854
- text: 'Não foi possível carregar os dados do gráfico',
1855
- }), ...(ngDevMode ? [{ debugName: "errorTitle" }] : /* istanbul ignore next */ []));
2017
+ : this.dataDiagnostic()
2018
+ ? this.i18n.resolve({
2019
+ key: 'praxis.charts.runtime.invalidDataTitle',
2020
+ text: 'Chart data is incompatible with its canonical configuration',
2021
+ })
2022
+ : this.i18n.resolve(this.renderConfig().state?.error?.title)
2023
+ || this.i18n.resolve({
2024
+ key: 'praxis.charts.runtime.remoteErrorTitle',
2025
+ text: 'Não foi possível carregar os dados do gráfico',
2026
+ }), ...(ngDevMode ? [{ debugName: "errorTitle" }] : /* istanbul ignore next */ []));
1856
2027
  errorDescription = computed(() => this.chartDocumentMappingError()
1857
2028
  ? this.i18n.resolve({
1858
2029
  key: 'praxis.charts.runtime.invalidDocumentDescription',
1859
2030
  text: 'The canonical chart document could not be mapped to the Praxis chart runtime. Review the chart contract before continuing.',
1860
2031
  })
1861
- : this.i18n.resolve(this.renderConfig().state?.error?.description)
1862
- || this.i18n.resolve({
1863
- key: 'praxis.charts.runtime.remoteErrorDescription',
1864
- text: 'Revise a fonte de dados e os filtros antes de continuar.',
1865
- }), ...(ngDevMode ? [{ debugName: "errorDescription" }] : /* istanbul ignore next */ []));
2032
+ : this.dataDiagnostic()
2033
+ ? this.resolveDataDiagnosticDescription(this.dataDiagnostic())
2034
+ : this.i18n.resolve(this.renderConfig().state?.error?.description)
2035
+ || this.i18n.resolve({
2036
+ key: 'praxis.charts.runtime.remoteErrorDescription',
2037
+ text: 'Revise a fonte de dados e os filtros antes de continuar.',
2038
+ }), ...(ngDevMode ? [{ debugName: "errorDescription" }] : /* istanbul ignore next */ []));
1866
2039
  editChartLabel = computed(() => this.i18n.resolve({ key: 'praxis.charts.runtime.editChart', text: 'Edit chart settings' }), ...(ngDevMode ? [{ debugName: "editChartLabel" }] : /* istanbul ignore next */ []));
1867
2040
  loadState = computed(() => {
1868
2041
  if (this.chartDocumentMappingError()) {
@@ -1884,7 +2057,10 @@ class PraxisChartComponent {
1884
2057
  return 'loading';
1885
2058
  }
1886
2059
  }
1887
- const transformed = this.transformer.transform(config, this.resolvedData());
2060
+ if (this.dataDiagnostic()) {
2061
+ return 'error';
2062
+ }
2063
+ const transformed = this.transformedData();
1888
2064
  return transformed.hasData ? 'ready' : 'empty';
1889
2065
  }, ...(ngDevMode ? [{ debugName: "loadState" }] : /* istanbul ignore next */ []));
1890
2066
  constructor() {
@@ -2037,6 +2213,27 @@ class PraxisChartComponent {
2037
2213
  this.engine.destroy();
2038
2214
  });
2039
2215
  }
2216
+ resolveDataDiagnosticDescription(diagnostic) {
2217
+ const descriptions = {
2218
+ 'gauge-scale-invalid': {
2219
+ key: 'praxis.charts.runtime.gaugeScaleInvalid',
2220
+ text: 'Configure a finite gauge scale whose maximum is greater than its minimum.',
2221
+ },
2222
+ 'gauge-bucket-cardinality': {
2223
+ key: 'praxis.charts.runtime.gaugeBucketCardinality',
2224
+ text: 'The gauge requires exactly one data bucket. Review the filters and the source limit.',
2225
+ },
2226
+ 'gauge-value-invalid': {
2227
+ key: 'praxis.charts.runtime.gaugeValueInvalid',
2228
+ text: 'The gauge requires one finite metric value.',
2229
+ },
2230
+ 'gauge-value-out-of-range': {
2231
+ key: 'praxis.charts.runtime.gaugeValueOutOfRange',
2232
+ text: 'The gauge value is outside the configured scale. Review the metric and scale instead of clamping the value.',
2233
+ },
2234
+ };
2235
+ return this.i18n.resolve(descriptions[diagnostic.code]);
2236
+ }
2040
2237
  canOpenConfigEditor() {
2041
2238
  return this.enableCustomization() && !!this.settingsPanel && !!this.runtimeChartDocument();
2042
2239
  }
@@ -3893,7 +4090,11 @@ class AnalyticsChartConfigAdapterService {
3893
4090
  return 'bar';
3894
4091
  }
3895
4092
  isCategorySliceChart(chartType) {
3896
- return chartType === 'pie' || chartType === 'donut' || chartType === 'funnel' || chartType === 'pyramid';
4093
+ return chartType === 'pie'
4094
+ || chartType === 'donut'
4095
+ || chartType === 'funnel'
4096
+ || chartType === 'pyramid'
4097
+ || chartType === 'treemap';
3897
4098
  }
3898
4099
  mapAggregation(aggregation) {
3899
4100
  const normalizedAggregation = (aggregation ?? '').toLowerCase();
@@ -4199,6 +4400,84 @@ class EChartsOptionBuilderService {
4199
4400
  const tooltipEnabled = config.theme?.tooltip?.enabled ?? true;
4200
4401
  const legendVisible = config.theme?.legend?.visible ?? true;
4201
4402
  const textColor = this.resolveTextColor(config);
4403
+ if (transformed.mode === 'gauge' && transformed.gauge && config.gauge) {
4404
+ const gauge = transformed.gauge;
4405
+ const format = config.series[0]?.labels?.format;
4406
+ const gaugeColor = gauge.color ?? palette[0];
4407
+ const gaugeSeries = {
4408
+ id: config.series[0]?.id,
4409
+ name: config.series[0]?.name,
4410
+ type: 'gauge',
4411
+ min: config.gauge.scale.min,
4412
+ max: config.gauge.scale.max,
4413
+ center: ['50%', this.hasText(config.title) || this.hasText(config.subtitle) ? '58%' : '52%'],
4414
+ radius: '72%',
4415
+ progress: {
4416
+ show: true,
4417
+ width: 14,
4418
+ itemStyle: gaugeColor ? { color: gaugeColor } : undefined,
4419
+ },
4420
+ axisLine: {
4421
+ lineStyle: {
4422
+ width: 14,
4423
+ },
4424
+ },
4425
+ axisTick: {
4426
+ distance: -18,
4427
+ length: 5,
4428
+ lineStyle: { color: textColor },
4429
+ },
4430
+ splitLine: {
4431
+ distance: -20,
4432
+ length: 10,
4433
+ lineStyle: { color: textColor },
4434
+ },
4435
+ axisLabel: {
4436
+ distance: 22,
4437
+ color: textColor,
4438
+ formatter: (value) => this.formatValue(value, format),
4439
+ },
4440
+ pointer: {
4441
+ show: true,
4442
+ itemStyle: gaugeColor ? { color: gaugeColor } : undefined,
4443
+ },
4444
+ anchor: {
4445
+ show: true,
4446
+ size: 10,
4447
+ itemStyle: gaugeColor ? { color: gaugeColor } : undefined,
4448
+ },
4449
+ title: {
4450
+ show: true,
4451
+ color: textColor,
4452
+ fontSize: 12,
4453
+ offsetCenter: [0, '75%'],
4454
+ },
4455
+ detail: {
4456
+ color: textColor,
4457
+ offsetCenter: [0, '30%'],
4458
+ formatter: (value) => this.formatValue(value, format),
4459
+ },
4460
+ data: [{
4461
+ ...gauge.data,
4462
+ name: gauge.name,
4463
+ value: gauge.value,
4464
+ itemStyle: gaugeColor ? { color: gaugeColor } : undefined,
4465
+ }],
4466
+ };
4467
+ return {
4468
+ backgroundColor: config.theme?.backgroundColor,
4469
+ color: palette,
4470
+ title: this.buildTitle(config, 'center', textColor),
4471
+ tooltip: tooltipEnabled
4472
+ ? {
4473
+ trigger: 'item',
4474
+ valueFormatter: (value) => this.formatValue(value, format),
4475
+ }
4476
+ : undefined,
4477
+ legend: { show: false },
4478
+ series: [gaugeSeries],
4479
+ };
4480
+ }
4202
4481
  if (transformed.mode === 'pie') {
4203
4482
  const hasChartTitle = this.hasText(config.title) || this.hasText(config.subtitle);
4204
4483
  const labelsVisible = config.series[0]?.labels?.visible ?? transformed.slices.length <= 4;
@@ -4293,6 +4572,49 @@ class EChartsOptionBuilderService {
4293
4572
  series: [funnelSeries],
4294
4573
  };
4295
4574
  }
4575
+ if (transformed.mode === 'treemap') {
4576
+ const labelsVisible = config.series[0]?.labels?.visible ?? true;
4577
+ const treemapSeries = {
4578
+ type: 'treemap',
4579
+ roam: false,
4580
+ nodeClick: false,
4581
+ breadcrumb: { show: false },
4582
+ left: '4%',
4583
+ right: '4%',
4584
+ top: this.hasText(config.title) || this.hasText(config.subtitle) ? 88 : 24,
4585
+ bottom: legendVisible ? 56 : 24,
4586
+ label: {
4587
+ show: labelsVisible,
4588
+ color: textColor,
4589
+ overflow: 'truncate',
4590
+ formatter: labelsVisible
4591
+ ? (params) => {
4592
+ const formatted = this.formatValue(params.value, config.series[0]?.labels?.format);
4593
+ return params.name ? `${params.name}\n${formatted}` : formatted;
4594
+ }
4595
+ : undefined,
4596
+ },
4597
+ data: transformed.slices.map((slice) => ({
4598
+ ...(slice.data || {}),
4599
+ name: slice.name,
4600
+ value: slice.value,
4601
+ itemStyle: slice.color ? { color: slice.color } : undefined,
4602
+ })),
4603
+ };
4604
+ return {
4605
+ backgroundColor: config.theme?.backgroundColor,
4606
+ color: palette,
4607
+ title: this.buildTitle(config, 'center', textColor),
4608
+ tooltip: tooltipEnabled
4609
+ ? {
4610
+ trigger: 'item',
4611
+ valueFormatter: (value) => this.formatValue(value, config.series[0]?.labels?.format),
4612
+ }
4613
+ : undefined,
4614
+ legend: this.buildLegend(config, legendVisible, 'bottom', textColor),
4615
+ series: [treemapSeries],
4616
+ };
4617
+ }
4296
4618
  if (transformed.mode === 'scatter') {
4297
4619
  const scatterSeries = transformed.series.map((series) => ({
4298
4620
  id: series.id,
@@ -4683,6 +5005,14 @@ class EChartsOptionBuilderService {
4683
5005
  if (!Number.isFinite(numeric)) {
4684
5006
  return String(normalizedValue);
4685
5007
  }
5008
+ const percent = this.parsePercentFormat(format);
5009
+ if (percent) {
5010
+ return new Intl.NumberFormat(DEFAULT_VALUE_LOCALE, {
5011
+ style: 'percent',
5012
+ minimumFractionDigits: percent.minimumFractionDigits,
5013
+ maximumFractionDigits: percent.maximumFractionDigits,
5014
+ }).format(numeric);
5015
+ }
4686
5016
  const currency = this.parseCurrencyFormat(format);
4687
5017
  if (currency) {
4688
5018
  return new Intl.NumberFormat(DEFAULT_VALUE_LOCALE, {
@@ -4783,6 +5113,23 @@ class EChartsOptionBuilderService {
4783
5113
  useGrouping: !parts.some((part) => part.toLowerCase() === 'nosep'),
4784
5114
  };
4785
5115
  }
5116
+ parsePercentFormat(format) {
5117
+ const [kind, precision] = format.split('|').map((part) => part.trim().toLowerCase());
5118
+ if (kind !== 'percent') {
5119
+ return null;
5120
+ }
5121
+ const match = precision ? /^(\d+)-(\d+)$/.exec(precision) : null;
5122
+ const minimumFractionDigits = match
5123
+ ? this.clampDecimals(Number.parseInt(match[1], 10), 0)
5124
+ : 0;
5125
+ const maximumFractionDigits = match
5126
+ ? this.clampDecimals(Number.parseInt(match[2], 10), 1)
5127
+ : 1;
5128
+ return {
5129
+ minimumFractionDigits: Math.min(minimumFractionDigits, maximumFractionDigits),
5130
+ maximumFractionDigits,
5131
+ };
5132
+ }
4786
5133
  normalizeCurrencyDisplay(value) {
4787
5134
  if (value === 'code' || value === 'name' || value === 'narrowSymbol') {
4788
5135
  return value;
@@ -4836,6 +5183,7 @@ use([
4836
5183
  CanvasRenderer,
4837
5184
  DatasetComponent,
4838
5185
  FunnelChart,
5186
+ GaugeChart,
4839
5187
  GridComponent,
4840
5188
  LegendComponent,
4841
5189
  LineChart,
@@ -4844,6 +5192,7 @@ use([
4844
5192
  TitleComponent,
4845
5193
  TooltipComponent,
4846
5194
  TransformComponent,
5195
+ TreemapChart,
4847
5196
  ]);
4848
5197
  class EChartsEngineAdapter {
4849
5198
  optionBuilder;
@@ -5062,6 +5411,11 @@ const PRAXIS_CHARTS_EN_US = {
5062
5411
  'praxis.charts.runtime.invalidDocumentDescription': 'The canonical chart document could not be mapped by Praxis Charts. Review the chart contract before continuing.',
5063
5412
  'praxis.charts.runtime.remoteErrorTitle': 'Chart data could not be loaded',
5064
5413
  'praxis.charts.runtime.remoteErrorDescription': 'Review the chart data source and analytics request before continuing.',
5414
+ 'praxis.charts.runtime.invalidDataTitle': 'Chart data is incompatible with its canonical configuration',
5415
+ 'praxis.charts.runtime.gaugeScaleInvalid': 'Configure a finite gauge scale whose maximum is greater than its minimum.',
5416
+ 'praxis.charts.runtime.gaugeBucketCardinality': 'The gauge requires exactly one data bucket. Review the filters and the source limit.',
5417
+ 'praxis.charts.runtime.gaugeValueInvalid': 'The gauge requires one finite metric value.',
5418
+ 'praxis.charts.runtime.gaugeValueOutOfRange': 'The gauge value is outside the configured scale. Review the metric and scale instead of clamping the value.',
5065
5419
  'praxis.charts.runtime.booleanFalseLabel': 'Not {{ label }}',
5066
5420
  'praxis.charts.runtime.comparisonCurrent': 'Current',
5067
5421
  'praxis.charts.runtime.comparisonPrevious': 'Previous',
@@ -5105,6 +5459,8 @@ const PRAXIS_CHARTS_EN_US = {
5105
5459
  'praxis.charts.editor.field.metricAggregation': 'Aggregation',
5106
5460
  'praxis.charts.editor.field.metricAxis': 'Axis',
5107
5461
  'praxis.charts.editor.field.metricSeriesKind': 'Series kind',
5462
+ 'praxis.charts.editor.field.gaugeMin': 'Scale minimum',
5463
+ 'praxis.charts.editor.field.gaugeMax': 'Scale maximum',
5108
5464
  'praxis.charts.editor.field.legendEnabled': 'Show legend',
5109
5465
  'praxis.charts.editor.field.labelsEnabled': 'Show labels',
5110
5466
  'praxis.charts.editor.field.tooltipEnabled': 'Show tooltip',
@@ -5192,9 +5548,11 @@ const PRAXIS_CHARTS_EN_US = {
5192
5548
  'praxis.charts.editor.paletteToken.brand-balanced': 'Brand balanced',
5193
5549
  'praxis.charts.editor.paletteToken.status': 'Status',
5194
5550
  'praxis.charts.editor.paletteToken.executive': 'Executive',
5195
- 'praxis.charts.editor.specialization.pieDonutHint': 'Pie, donut, funnel and pyramid charts keep one metric and use the first dimension as the category segment.',
5551
+ 'praxis.charts.editor.specialization.pieDonutHint': 'Pie, donut, funnel, pyramid and treemap charts keep one metric and use the first dimension as the category segment.',
5196
5552
  'praxis.charts.editor.specialization.scatterTitle': 'Scatter guidance',
5197
5553
  'praxis.charts.editor.specialization.scatterHint': 'With one metric, scatter uses the first dimension as X and the metric as Y. With two metrics, the dimension identifies the grouped bucket and the metrics map to X and Y.',
5554
+ 'praxis.charts.editor.specialization.gaugeTitle': 'Gauge scale',
5555
+ 'praxis.charts.editor.specialization.gaugeHint': 'Gauge charts render exactly one bucket and one metric. Remote stats use group-by with limit 1; values outside the scale fail instead of being clamped.',
5198
5556
  'praxis.charts.editor.events.pointClickTitle': 'Point click',
5199
5557
  'praxis.charts.editor.events.selectionChangeTitle': 'Selection change',
5200
5558
  'praxis.charts.editor.events.drillDownTitle': 'Drill down',
@@ -5263,6 +5621,8 @@ const PRAXIS_CHARTS_EN_US = {
5263
5621
  'praxis.charts.editor.kind.scatter': 'Scatter',
5264
5622
  'praxis.charts.editor.kind.funnel': 'Funnel',
5265
5623
  'praxis.charts.editor.kind.pyramid': 'Pyramid',
5624
+ 'praxis.charts.editor.kind.treemap': 'Treemap',
5625
+ 'praxis.charts.editor.kind.gauge': 'Gauge',
5266
5626
  'praxis.charts.analyticsPresentation.selectorLabel': 'Analytics presentation',
5267
5627
  'praxis.charts.analyticsPresentation.chart': 'Chart',
5268
5628
  'praxis.charts.analyticsPresentation.table': 'Table',
@@ -5286,6 +5646,11 @@ const PRAXIS_CHARTS_PT_BR = {
5286
5646
  'praxis.charts.runtime.invalidDocumentDescription': 'O documento canônico do gráfico não pode ser mapeado pelo Praxis Charts. Revise o contrato do gráfico antes de continuar.',
5287
5647
  'praxis.charts.runtime.remoteErrorTitle': 'Não foi possível carregar os dados do gráfico',
5288
5648
  'praxis.charts.runtime.remoteErrorDescription': 'Revise a fonte de dados e os filtros antes de continuar.',
5649
+ 'praxis.charts.runtime.invalidDataTitle': 'Os dados do gráfico são incompatíveis com a configuração canônica',
5650
+ 'praxis.charts.runtime.gaugeScaleInvalid': 'Configure uma escala finita para o velocímetro, com o valor máximo maior que o mínimo.',
5651
+ 'praxis.charts.runtime.gaugeBucketCardinality': 'O velocímetro exige exatamente um bucket de dados. Revise os filtros e o limite da fonte.',
5652
+ 'praxis.charts.runtime.gaugeValueInvalid': 'O velocímetro exige um único valor numérico finito.',
5653
+ 'praxis.charts.runtime.gaugeValueOutOfRange': 'O valor do velocímetro está fora da escala configurada. Revise a métrica e a escala em vez de limitar o valor silenciosamente.',
5289
5654
  'praxis.charts.runtime.booleanFalseLabel': 'Não {{ label }}',
5290
5655
  'praxis.charts.runtime.comparisonCurrent': 'Atual',
5291
5656
  'praxis.charts.runtime.comparisonPrevious': 'Anterior',
@@ -5329,6 +5694,8 @@ const PRAXIS_CHARTS_PT_BR = {
5329
5694
  'praxis.charts.editor.field.metricAggregation': 'Agregação',
5330
5695
  'praxis.charts.editor.field.metricAxis': 'Eixo',
5331
5696
  'praxis.charts.editor.field.metricSeriesKind': 'Tipo da série',
5697
+ 'praxis.charts.editor.field.gaugeMin': 'Mínimo da escala',
5698
+ 'praxis.charts.editor.field.gaugeMax': 'Máximo da escala',
5332
5699
  'praxis.charts.editor.field.legendEnabled': 'Exibir legenda',
5333
5700
  'praxis.charts.editor.field.labelsEnabled': 'Exibir rótulos',
5334
5701
  'praxis.charts.editor.field.tooltipEnabled': 'Exibir dica de ferramenta (tooltip)',
@@ -5416,9 +5783,11 @@ const PRAXIS_CHARTS_PT_BR = {
5416
5783
  'praxis.charts.editor.paletteToken.brand-balanced': 'Marca balanceada',
5417
5784
  'praxis.charts.editor.paletteToken.status': 'Status',
5418
5785
  'praxis.charts.editor.paletteToken.executive': 'Executiva',
5419
- '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.',
5786
+ 'praxis.charts.editor.specialization.pieDonutHint': 'Gráficos de pizza, rosca, funil, pirâmide e mapa de árvore mantêm uma métrica e usam a primeira dimensão como segmento categórico.',
5420
5787
  'praxis.charts.editor.specialization.scatterTitle': 'Guia de dispersão (scatter)',
5421
5788
  'praxis.charts.editor.specialization.scatterHint': 'Com uma métrica, o scatter usa a primeira dimensão em X e a métrica em Y. Com duas métricas, a dimensão identifica o bucket agrupado e as métricas ocupam X e Y.',
5789
+ 'praxis.charts.editor.specialization.gaugeTitle': 'Escala do velocímetro',
5790
+ 'praxis.charts.editor.specialization.gaugeHint': 'O velocímetro renderiza exatamente um bucket e uma métrica. Stats remotos usam agrupar por com limite 1; valores fora da escala falham em vez de serem limitados silenciosamente.',
5422
5791
  'praxis.charts.editor.events.pointClickTitle': 'Clique no ponto',
5423
5792
  'praxis.charts.editor.events.selectionChangeTitle': 'Mudança de seleção',
5424
5793
  'praxis.charts.editor.events.drillDownTitle': 'Drill down',
@@ -5487,6 +5856,8 @@ const PRAXIS_CHARTS_PT_BR = {
5487
5856
  'praxis.charts.editor.kind.scatter': 'Dispersão (Scatter)',
5488
5857
  'praxis.charts.editor.kind.funnel': 'Funil',
5489
5858
  'praxis.charts.editor.kind.pyramid': 'Pirâmide',
5859
+ 'praxis.charts.editor.kind.treemap': 'Mapa de árvore',
5860
+ 'praxis.charts.editor.kind.gauge': 'Velocímetro',
5490
5861
  'praxis.charts.analyticsPresentation.selectorLabel': 'Apresentação analítica',
5491
5862
  'praxis.charts.analyticsPresentation.chart': 'Gráfico',
5492
5863
  'praxis.charts.analyticsPresentation.table': 'Tabela',
@@ -5600,6 +5971,18 @@ class ChartEditorPreviewMapperService {
5600
5971
  if (!dimensionField || !metrics.length) {
5601
5972
  return [];
5602
5973
  }
5974
+ if (document.kind === 'gauge') {
5975
+ const scale = document.gauge?.scale;
5976
+ const metricField = metrics[0]?.field;
5977
+ if (!scale || !metricField || !Number.isFinite(scale.min) || !Number.isFinite(scale.max) || scale.max <= scale.min) {
5978
+ return [];
5979
+ }
5980
+ return [{
5981
+ [dimensionField]: 'Preview',
5982
+ [metricField]: scale.min + (scale.max - scale.min) * 0.62,
5983
+ label: 'Preview',
5984
+ }];
5985
+ }
5603
5986
  if (document.kind === 'scatter') {
5604
5987
  if (!metrics[0]?.field) {
5605
5988
  return [];
@@ -6568,6 +6951,8 @@ class PraxisChartConfigEditor {
6568
6951
  'scatter',
6569
6952
  'funnel',
6570
6953
  'pyramid',
6954
+ 'treemap',
6955
+ 'gauge',
6571
6956
  ];
6572
6957
  sourceKinds = ['praxis.stats', 'derived'];
6573
6958
  operations = ['group-by', 'timeseries', 'distribution', 'comparison'];
@@ -6739,10 +7124,35 @@ class PraxisChartConfigEditor {
6739
7124
  }));
6740
7125
  }
6741
7126
  setKind(value) {
6742
- this.patchDocument((document) => ({
6743
- ...document,
6744
- kind: value,
6745
- }));
7127
+ this.patchDocument((document) => {
7128
+ if (value !== 'gauge') {
7129
+ return {
7130
+ ...document,
7131
+ kind: value,
7132
+ gauge: undefined,
7133
+ };
7134
+ }
7135
+ const firstMetric = document.metrics?.[0];
7136
+ const defaultScale = firstMetric?.format === 'percent'
7137
+ ? { min: 0, max: 1 }
7138
+ : { min: 0, max: 100 };
7139
+ return {
7140
+ ...document,
7141
+ kind: value,
7142
+ metrics: document.metrics?.slice(0, 1),
7143
+ gauge: document.gauge ?? { scale: defaultScale },
7144
+ source: document.source.kind === 'praxis.stats'
7145
+ ? {
7146
+ ...document.source,
7147
+ operation: 'group-by',
7148
+ options: {
7149
+ orderBy: document.source.options?.orderBy,
7150
+ limit: 1,
7151
+ },
7152
+ }
7153
+ : document.source,
7154
+ };
7155
+ });
6746
7156
  }
6747
7157
  setTitle(value) {
6748
7158
  this.patchDocument((document) => ({
@@ -6823,8 +7233,15 @@ class PraxisChartConfigEditor {
6823
7233
  : {
6824
7234
  kind: 'praxis.stats',
6825
7235
  resource: document.source.kind === 'praxis.stats' ? document.source.resource : '',
6826
- operation: document.source.kind === 'praxis.stats' ? document.source.operation ?? 'group-by' : 'group-by',
6827
- options: document.source.kind === 'praxis.stats' ? document.source.options : undefined,
7236
+ operation: document.kind === 'gauge'
7237
+ ? 'group-by'
7238
+ : document.source.kind === 'praxis.stats' ? document.source.operation ?? 'group-by' : 'group-by',
7239
+ options: document.kind === 'gauge'
7240
+ ? {
7241
+ orderBy: document.source.kind === 'praxis.stats' ? document.source.options?.orderBy : undefined,
7242
+ limit: 1,
7243
+ }
7244
+ : document.source.kind === 'praxis.stats' ? document.source.options : undefined,
6828
7245
  },
6829
7246
  }));
6830
7247
  }
@@ -6854,6 +7271,9 @@ class PraxisChartConfigEditor {
6854
7271
  void this.resolveSelectedResource(resource);
6855
7272
  }
6856
7273
  setOperation(value) {
7274
+ if (this.doc().kind === 'gauge' && value !== 'group-by') {
7275
+ return;
7276
+ }
6857
7277
  this.patchDocument((document) => ({
6858
7278
  ...document,
6859
7279
  source: document.source.kind === 'praxis.stats'
@@ -6967,6 +7387,12 @@ class PraxisChartConfigEditor {
6967
7387
  setComparisonPeriodMode(mode) {
6968
7388
  this.updateComparisonPeriod({ mode });
6969
7389
  }
7390
+ setGaugeMin(value) {
7391
+ this.setGaugeScaleValue('min', value);
7392
+ }
7393
+ setGaugeMax(value) {
7394
+ this.setGaugeScaleValue('max', value);
7395
+ }
6970
7396
  addDimension() {
6971
7397
  this.patchDocument((document) => ({
6972
7398
  ...document,
@@ -7219,10 +7645,13 @@ class PraxisChartConfigEditor {
7219
7645
  }
7220
7646
  operationOptions() {
7221
7647
  const selectedResource = this.selectedResourceOption();
7222
- if (!selectedResource) {
7223
- return this.resourceOptions().length ? this.operations : [];
7648
+ const availableOperations = !selectedResource
7649
+ ? this.resourceOptions().length ? this.operations : []
7650
+ : selectedResource.operations?.length ? selectedResource.operations : [];
7651
+ if (this.doc().kind === 'gauge') {
7652
+ return availableOperations.filter((operation) => operation === 'group-by');
7224
7653
  }
7225
- return selectedResource.operations?.length ? selectedResource.operations : [];
7654
+ return availableOperations;
7226
7655
  }
7227
7656
  resourceCatalogUnavailable() {
7228
7657
  return this.doc().source.kind === 'praxis.stats' && !this.resourceOptions().length;
@@ -7372,6 +7801,9 @@ class PraxisChartConfigEditor {
7372
7801
  return this.doc().metrics ?? [];
7373
7802
  }
7374
7803
  canAddMetric() {
7804
+ if (this.doc().kind === 'gauge') {
7805
+ return false;
7806
+ }
7375
7807
  return this.doc().kind !== 'scatter' || this.metrics().length < 2;
7376
7808
  }
7377
7809
  fieldOptions(role) {
@@ -7421,11 +7853,23 @@ class PraxisChartConfigEditor {
7421
7853
  return this.doc().kind === 'pie'
7422
7854
  || this.doc().kind === 'donut'
7423
7855
  || this.doc().kind === 'funnel'
7424
- || this.doc().kind === 'pyramid';
7856
+ || this.doc().kind === 'pyramid'
7857
+ || this.doc().kind === 'treemap';
7425
7858
  }
7426
7859
  showScatterPanel() {
7427
7860
  return this.doc().kind === 'scatter';
7428
7861
  }
7862
+ showGaugePanel() {
7863
+ return this.doc().kind === 'gauge';
7864
+ }
7865
+ gaugeMinValue() {
7866
+ const min = this.doc().gauge?.scale.min;
7867
+ return min === undefined ? '' : String(min);
7868
+ }
7869
+ gaugeMaxValue() {
7870
+ const max = this.doc().gauge?.scale.max;
7871
+ return max === undefined ? '' : String(max);
7872
+ }
7429
7873
  issueTrackBy(_, issue) {
7430
7874
  return `${issue.code}:${issue.field}`;
7431
7875
  }
@@ -7444,6 +7888,25 @@ class PraxisChartConfigEditor {
7444
7888
  },
7445
7889
  }));
7446
7890
  }
7891
+ setGaugeScaleValue(key, value) {
7892
+ if (typeof value === 'string' && !value.trim()) {
7893
+ return;
7894
+ }
7895
+ const parsed = typeof value === 'number' ? value : Number(String(value).trim());
7896
+ if (!Number.isFinite(parsed)) {
7897
+ return;
7898
+ }
7899
+ this.patchDocument((document) => ({
7900
+ ...document,
7901
+ gauge: {
7902
+ scale: {
7903
+ min: document.gauge?.scale.min ?? 0,
7904
+ max: document.gauge?.scale.max ?? 100,
7905
+ [key]: parsed,
7906
+ },
7907
+ },
7908
+ }));
7909
+ }
7447
7910
  patchEvent(eventKey, updater) {
7448
7911
  this.patchDocument((document) => {
7449
7912
  const currentEvent = document.events?.[eventKey];
@@ -7737,7 +8200,7 @@ class PraxisChartConfigEditor {
7737
8200
  this.documentChange.emit(structuredClone(this.normalizedDocument()));
7738
8201
  }
7739
8202
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: PraxisChartConfigEditor, deps: [], target: i0.ɵɵFactoryTarget.Component });
7740
- 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', 'With one metric, scatter uses the first dimension as X and the metric as Y. With two metrics, the dimension identifies the grouped bucket and the metrics map to X and Y.') }}\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() || !canAddMetric()\">\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 });
8203
+ 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 (showGaugePanel()) {\n <mat-card class=\"editor-card\" data-testid=\"chart-editor-gauge-scale\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.specialization.gaugeTitle', 'Gauge scale') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-stack\">\n <p class=\"editor-caption\">\n {{ t('praxis.charts.editor.specialization.gaugeHint', 'Gauge charts render exactly one bucket and one metric. Remote stats use group-by with limit 1; values outside the scale fail instead of being clamped.') }}\n </p>\n <div class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.gaugeMin', 'Scale minimum') }}</mat-label>\n <input\n matInput\n type=\"number\"\n data-testid=\"chart-editor-gauge-min\"\n [ngModel]=\"gaugeMinValue()\"\n (ngModelChange)=\"setGaugeMin($event)\"\n [disabled]=\"isReadonly()\"\n />\n </mat-form-field>\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.gaugeMax', 'Scale maximum') }}</mat-label>\n <input\n matInput\n type=\"number\"\n data-testid=\"chart-editor-gauge-max\"\n [ngModel]=\"gaugeMaxValue()\"\n (ngModelChange)=\"setGaugeMax($event)\"\n [disabled]=\"isReadonly()\"\n />\n </mat-form-field>\n </div>\n </mat-card-content>\n </mat-card>\n }\n\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, pyramid and treemap 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', 'With one metric, scatter uses the first dimension as X and the metric as Y. With two metrics, the dimension identifies the grouped bucket and the metrics map to X and Y.') }}\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() || !canAddMetric()\">\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.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { 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 });
7741
8204
  }
7742
8205
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: PraxisChartConfigEditor, decorators: [{
7743
8206
  type: Component,
@@ -7750,7 +8213,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
7750
8213
  MatSelectModule,
7751
8214
  MatSlideToggleModule,
7752
8215
  PraxisChartComponent
7753
- ], 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', 'With one metric, scatter uses the first dimension as X and the metric as Y. With two metrics, the dimension identifies the grouped bucket and the metrics map to X and Y.') }}\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() || !canAddMetric()\">\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"] }]
8216
+ ], 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 (showGaugePanel()) {\n <mat-card class=\"editor-card\" data-testid=\"chart-editor-gauge-scale\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.specialization.gaugeTitle', 'Gauge scale') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-stack\">\n <p class=\"editor-caption\">\n {{ t('praxis.charts.editor.specialization.gaugeHint', 'Gauge charts render exactly one bucket and one metric. Remote stats use group-by with limit 1; values outside the scale fail instead of being clamped.') }}\n </p>\n <div class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.gaugeMin', 'Scale minimum') }}</mat-label>\n <input\n matInput\n type=\"number\"\n data-testid=\"chart-editor-gauge-min\"\n [ngModel]=\"gaugeMinValue()\"\n (ngModelChange)=\"setGaugeMin($event)\"\n [disabled]=\"isReadonly()\"\n />\n </mat-form-field>\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.gaugeMax', 'Scale maximum') }}</mat-label>\n <input\n matInput\n type=\"number\"\n data-testid=\"chart-editor-gauge-max\"\n [ngModel]=\"gaugeMaxValue()\"\n (ngModelChange)=\"setGaugeMax($event)\"\n [disabled]=\"isReadonly()\"\n />\n </mat-form-field>\n </div>\n </mat-card-content>\n </mat-card>\n }\n\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, pyramid and treemap 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', 'With one metric, scatter uses the first dimension as X and the metric as Y. With two metrics, the dimension identifies the grouped bucket and the metrics map to X and Y.') }}\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() || !canAddMetric()\">\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"] }]
7754
8217
  }], 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"] }] } });
7755
8218
 
7756
8219
  var praxisChartConfigEditor = /*#__PURE__*/Object.freeze({
@@ -10531,7 +10994,7 @@ const PRAXIS_CHART_AUTHORING_COVERAGE = [
10531
10994
  { path: 'chartDocument.source.options.comparisonPeriod.preset', mode: 'visual-editable', owner: 'chart-config-editor', rationale: 'The period preset is constrained to the canonical backend vocabulary and persisted without local date arithmetic.', proof: ['praxis-chart-config-editor.spec.ts', 'chart-contract-validation.service.spec.ts'] },
10532
10995
  { path: 'chartDocument.source.options.comparisonPeriod.mode', mode: 'visual-editable', owner: 'chart-config-editor', rationale: 'Comparison mode is selected from the canonical aligned or calendar-period semantics resolved by the backend.', proof: ['praxis-chart-config-editor.spec.ts', 'chart-contract-validation.service.spec.ts'] },
10533
10996
  { path: 'chartDocument.source.options.orderBy', mode: 'preserved-only', owner: 'chart-contract-normalizer', rationale: 'Ordering is runtime-supported but not yet exposed as a primary visual control; preserving avoids silent loss.', proof: ['praxis-chart-config-editor.spec.ts'] },
10534
- { path: 'chartDocument.source.options.limit', mode: 'preserved-only', owner: 'chart-contract-normalizer', rationale: 'Source-level result limit remains governed by document/runtime semantics until a ranked visual control exists.', proof: ['praxis-chart-config-editor.spec.ts'] },
10997
+ { path: 'chartDocument.source.options.limit', mode: 'visual-editable', owner: 'chart-config-editor', rationale: 'Source result limit is normally preserved, while scalar gauge authoring governs it to exactly one bucket and the AI data-binding contract transports that invariant.', proof: ['praxis-chart-config-editor.spec.ts', 'praxis-charts-authoring-manifest.spec.ts'] },
10535
10998
  { path: 'chartDocument.source.refresh', mode: 'preserved-only', owner: 'chart-contract-normalizer', rationale: 'Refresh policy belongs to runtime/backend cadence governance; current editor preserves it.', proof: ['praxis-chart-config-editor.spec.ts'] },
10536
10999
  { path: 'chartDocument.dimensions[]', mode: 'visual-editable', owner: 'chart-config-editor', rationale: 'Dimension field and role are visually editable with catalog compatibility checks.', proof: ['praxis-chart-config-editor.spec.ts', 'praxis-charts-authoring-manifest.spec.ts'] },
10537
11000
  { path: 'chartDocument.dimensions[].label', mode: 'preserved-only', owner: 'chart-contract-normalizer', rationale: 'Dimension labels can be authored by AI/imported contracts and must survive visual edits until a governed label control exists.', proof: ['praxis-chart-config-editor.spec.ts'] },
@@ -10539,6 +11002,7 @@ const PRAXIS_CHART_AUTHORING_COVERAGE = [
10539
11002
  { path: 'chartDocument.metrics[]', mode: 'visual-editable', owner: 'chart-config-editor', rationale: 'Metric field, label, aggregation, axis and series kind are core visual analytics controls.', proof: ['praxis-chart-config-editor.spec.ts', 'praxis-charts-authoring-manifest.spec.ts'] },
10540
11003
  { path: 'chartDocument.metrics[].color', mode: 'preserved-only', owner: 'chart-contract-normalizer', rationale: 'Series color is renderer-neutral config but not yet a governed swatch control in the editor.', proof: ['praxis-chart-config-editor.spec.ts'] },
10541
11004
  { path: 'chartDocument.metrics[].format', mode: 'preserved-only', owner: 'chart-contract-normalizer', rationale: 'Metric formatting is preserved as runtime presentation metadata until visual format presets are introduced.', proof: ['praxis-chart-config-editor.spec.ts'] },
11005
+ { path: 'chartDocument.gauge.scale', mode: 'visual-editable', owner: 'chart-config-editor', rationale: 'Gauge scale is a paired finite min/max decision edited visually and validated before runtime materialization.', proof: ['praxis-chart-config-editor.spec.ts', 'praxis-charts-authoring-manifest.spec.ts', 'chart-contract-validation.service.spec.ts'] },
10542
11006
  { path: 'chartDocument.aggregations[]', mode: 'ai-authorable', owner: 'chart-ai-manifest', rationale: 'Aggregation config is a structured analytics contract; visual authoring currently edits metric aggregation instead.', proof: ['chart-authoring-coverage.matrix.spec.ts', 'praxis-chart-config-editor.spec.ts'] },
10543
11007
  { path: 'chartDocument.groupBy[]', mode: 'ai-authorable', owner: 'chart-ai-manifest', rationale: 'Grouping is derivable from dimensions for current visual flows and remains preserved for structured authoring.', proof: ['chart-authoring-coverage.matrix.spec.ts', 'praxis-chart-config-editor.spec.ts'] },
10544
11008
  { path: 'chartDocument.sort[]', mode: 'preserved-only', owner: 'chart-contract-normalizer', rationale: 'Document-level sort is runtime-supported but lacks a governed visual ordering editor.', proof: ['praxis-chart-config-editor.spec.ts'] },
@@ -10566,6 +11030,7 @@ const PRAXIS_CHART_AUTHORING_COVERAGE = [
10566
11030
  ];
10567
11031
 
10568
11032
  const chartStatsOperations = ['group-by', 'timeseries', 'distribution', 'comparison'];
11033
+ const chartKinds = ['bar', 'combo', 'horizontal-bar', 'line', 'pie', 'donut', 'area', 'stacked-bar', 'stacked-area', 'scatter', 'funnel', 'pyramid', 'treemap', 'gauge'];
10569
11034
  const chartComparisonPeriodSchema = {
10570
11035
  type: 'object',
10571
11036
  required: ['field', 'timezone', 'preset', 'mode'],
@@ -10579,7 +11044,8 @@ const chartComparisonPeriodSchema = {
10579
11044
  const chartDocumentSchema = {
10580
11045
  type: 'object',
10581
11046
  required: ['version', 'kind', 'source'],
10582
- allOf: [{
11047
+ allOf: [
11048
+ {
10583
11049
  if: { properties: { kind: { const: 'scatter' } }, required: ['kind'] },
10584
11050
  then: {
10585
11051
  required: ['dimensions', 'metrics'],
@@ -10588,10 +11054,62 @@ const chartDocumentSchema = {
10588
11054
  metrics: { type: 'array', minItems: 1, maxItems: 2 },
10589
11055
  },
10590
11056
  },
10591
- }],
11057
+ },
11058
+ {
11059
+ if: { properties: { kind: { const: 'gauge' } }, required: ['kind'] },
11060
+ then: {
11061
+ required: ['dimensions', 'metrics', 'gauge'],
11062
+ properties: {
11063
+ dimensions: { type: 'array', minItems: 1 },
11064
+ metrics: { type: 'array', minItems: 1, maxItems: 1 },
11065
+ gauge: {
11066
+ type: 'object',
11067
+ required: ['scale'],
11068
+ properties: {
11069
+ scale: {
11070
+ type: 'object',
11071
+ required: ['min', 'max'],
11072
+ properties: {
11073
+ min: { type: 'number' },
11074
+ max: { type: 'number' },
11075
+ },
11076
+ },
11077
+ },
11078
+ },
11079
+ },
11080
+ },
11081
+ },
11082
+ {
11083
+ if: {
11084
+ properties: {
11085
+ kind: { const: 'gauge' },
11086
+ source: {
11087
+ properties: { kind: { const: 'praxis.stats' } },
11088
+ required: ['kind'],
11089
+ },
11090
+ },
11091
+ required: ['kind', 'source'],
11092
+ },
11093
+ then: {
11094
+ properties: {
11095
+ source: {
11096
+ required: ['operation', 'options'],
11097
+ properties: {
11098
+ operation: { const: 'group-by' },
11099
+ options: {
11100
+ type: 'object',
11101
+ required: ['limit'],
11102
+ properties: { limit: { const: 1 } },
11103
+ },
11104
+ },
11105
+ },
11106
+ },
11107
+ },
11108
+ },
11109
+ ],
10592
11110
  properties: {
10593
11111
  version: { const: PRAXIS_X_UI_CHART_AUTHORABLE_VERSION },
10594
- kind: { enum: ['bar', 'combo', 'horizontal-bar', 'line', 'pie', 'donut', 'area', 'stacked-bar', 'stacked-area', 'scatter', 'funnel', 'pyramid'] },
11112
+ kind: { enum: chartKinds },
10595
11113
  chartId: { type: 'string' },
10596
11114
  title: { oneOf: [{ type: 'string' }, { type: 'object' }] },
10597
11115
  subtitle: { oneOf: [{ type: 'string' }, { type: 'object' }] },
@@ -10612,6 +11130,17 @@ const chartDocumentSchema = {
10612
11130
  },
10613
11131
  dimensions: { type: 'array', items: { type: 'object' } },
10614
11132
  metrics: { type: 'array', items: { type: 'object' } },
11133
+ gauge: {
11134
+ type: 'object',
11135
+ required: ['scale'],
11136
+ properties: {
11137
+ scale: {
11138
+ type: 'object',
11139
+ required: ['min', 'max'],
11140
+ properties: { min: { type: 'number' }, max: { type: 'number' } },
11141
+ },
11142
+ },
11143
+ },
10615
11144
  events: { type: 'object' },
10616
11145
  },
10617
11146
  };
@@ -10620,7 +11149,7 @@ const PRAXIS_CHARTS_AUTHORING_MANIFEST = {
10620
11149
  componentId: 'praxis-chart',
10621
11150
  ownerPackage: '@praxisui/charts',
10622
11151
  configSchemaId: 'PraxisXUiChartContract',
10623
- manifestVersion: '1.3.0',
11152
+ manifestVersion: '1.4.0',
10624
11153
  runtimeInputs: [
10625
11154
  { name: 'chartDocument', type: 'PraxisXUiChartContract | null', description: 'Canonical x-ui.chart document used as the authoring source of truth.' },
10626
11155
  { name: 'config', type: 'PraxisChartConfig', description: 'Runtime chart configuration mapped from or supplied beside the canonical chart document.' },
@@ -10642,6 +11171,7 @@ const PRAXIS_CHARTS_AUTHORING_MANIFEST = {
10642
11171
  { kind: 'selection', resolver: 'x-ui-chart-events-selection-change', description: 'Single-point selection event enablement and mapping semantics; toggle and multi-select are intentionally not advertised.' },
10643
11172
  { kind: 'legend', resolver: 'x-ui-chart-legend-feature', description: 'Legend visibility through boolean or toggleable feature object.' },
10644
11173
  { kind: 'tooltip', resolver: 'x-ui-chart-tooltip-feature', description: 'Tooltip visibility through boolean or toggleable feature object.' },
11174
+ { kind: 'gaugeScale', resolver: 'x-ui-chart-gauge-scale', description: 'Canonical scalar gauge scale at `chartDocument.gauge.scale`, with finite `min` and `max` bounds.' },
10645
11175
  ],
10646
11176
  operations: [
10647
11177
  {
@@ -10654,7 +11184,7 @@ const PRAXIS_CHARTS_AUTHORING_MANIFEST = {
10654
11184
  effects: [{ kind: 'set-value', path: 'chartDocument' }],
10655
11185
  destructive: false,
10656
11186
  requiresConfirmation: false,
10657
- validators: ['chart-document-shape', 'chart-version-supported', 'chart-type-series-axis-compatible', 'editor-runtime-round-trip'],
11187
+ validators: ['chart-document-shape', 'chart-version-supported', 'chart-type-series-axis-compatible', 'gauge-scalar-contract', 'gauge-scale-valid', 'editor-runtime-round-trip'],
10658
11188
  affectedPaths: ['chartDocument'],
10659
11189
  submissionImpact: 'config-only',
10660
11190
  preconditions: ['config-initialized'],
@@ -10665,12 +11195,34 @@ const PRAXIS_CHARTS_AUTHORING_MANIFEST = {
10665
11195
  scope: 'global',
10666
11196
  targetKind: 'chartType',
10667
11197
  target: { kind: 'chartType', resolver: 'x-ui-chart-kind', ambiguityPolicy: 'fail', required: false },
10668
- 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'] } } },
11198
+ inputSchema: {
11199
+ type: 'object',
11200
+ required: ['kind'],
11201
+ allOf: [{
11202
+ if: { properties: { kind: { const: 'gauge' } }, required: ['kind'] },
11203
+ then: { required: ['gauge'] },
11204
+ }],
11205
+ properties: {
11206
+ kind: { enum: chartKinds },
11207
+ orientation: { enum: ['vertical', 'horizontal'] },
11208
+ gauge: {
11209
+ type: 'object',
11210
+ required: ['scale'],
11211
+ properties: {
11212
+ scale: {
11213
+ type: 'object',
11214
+ required: ['min', 'max'],
11215
+ properties: { min: { type: 'number' }, max: { type: 'number' } },
11216
+ },
11217
+ },
11218
+ },
11219
+ },
11220
+ },
10669
11221
  effects: [{ kind: 'merge-object', path: 'chartDocument' }],
10670
11222
  destructive: false,
10671
11223
  requiresConfirmation: false,
10672
- validators: ['chart-type-supported', 'chart-type-series-axis-compatible', 'category-slice-single-metric', 'combo-minimum-series', 'editor-runtime-round-trip'],
10673
- affectedPaths: ['chartDocument.kind', 'chartDocument.orientation', 'chartDocument.metrics[]', 'chartDocument.dimensions[]'],
11224
+ validators: ['chart-type-supported', 'chart-type-series-axis-compatible', 'category-slice-single-metric', 'combo-minimum-series', 'gauge-scalar-contract', 'gauge-scale-valid', 'editor-runtime-round-trip'],
11225
+ affectedPaths: ['chartDocument.kind', 'chartDocument.orientation', 'chartDocument.metrics[]', 'chartDocument.dimensions[]', 'chartDocument.gauge.scale'],
10674
11226
  submissionImpact: 'config-only',
10675
11227
  preconditions: ['config-initialized'],
10676
11228
  },
@@ -10717,7 +11269,7 @@ const PRAXIS_CHARTS_AUTHORING_MANIFEST = {
10717
11269
  } }],
10718
11270
  destructive: false,
10719
11271
  requiresConfirmation: false,
10720
- validators: ['series-field-exists', 'series-field-aggregable', 'series-id-unique', 'chart-type-series-axis-compatible', 'editor-runtime-round-trip'],
11272
+ validators: ['series-field-exists', 'series-field-aggregable', 'series-id-unique', 'chart-type-series-axis-compatible', 'gauge-scalar-contract', 'editor-runtime-round-trip'],
10721
11273
  affectedPaths: ['chartDocument.metrics[]'],
10722
11274
  submissionImpact: 'config-only',
10723
11275
  preconditions: ['config-initialized'],
@@ -10781,23 +11333,24 @@ const PRAXIS_CHARTS_AUTHORING_MANIFEST = {
10781
11333
  sourceKind: { enum: ['praxis.stats', 'derived'] },
10782
11334
  resource: { type: 'string', pattern: '^/(?!/)' },
10783
11335
  operation: { enum: chartStatsOperations },
11336
+ limit: { type: 'integer', minimum: 1 },
10784
11337
  comparisonPeriod: chartComparisonPeriodSchema,
10785
11338
  dimensions: { type: 'array', items: { type: 'object', required: ['field'], properties: { field: { type: 'string' }, role: { type: 'string' } } } },
10786
11339
  metrics: { type: 'array', items: { type: 'object', required: ['field'], properties: { field: { type: 'string' }, label: { oneOf: [{ type: 'string' }, { type: 'object' }] }, aggregation: { enum: ['sum', 'avg', 'min', 'max', 'count', 'distinct-count'] }, seriesKind: { enum: ['bar', 'line', 'area'] }, axis: { enum: ['primary', 'secondary'] }, color: { type: 'string' }, format: { type: 'string' }, afterField: { type: 'string' } } } },
10787
11340
  },
10788
11341
  },
10789
11342
  effects: [{ kind: 'compile-domain-patch', handler: 'chart-data-resource-bind', handlerContract: {
10790
- reads: ['availableResources[]', 'availableFields[]', 'chartDocument.source', 'chartDocument.dimensions[]', 'chartDocument.metrics[]'],
10791
- writes: ['chartDocument.source', 'chartDocument.source.options.comparisonPeriod', 'chartDocument.dimensions[]', 'chartDocument.metrics[]'],
11343
+ reads: ['availableResources[]', 'availableFields[]', 'chartDocument.source', 'chartDocument.source.options.limit', 'chartDocument.dimensions[]', 'chartDocument.metrics[]'],
11344
+ writes: ['chartDocument.source', 'chartDocument.source.options.limit', 'chartDocument.source.options.comparisonPeriod', 'chartDocument.dimensions[]', 'chartDocument.metrics[]'],
10792
11345
  identityKeys: ['availableResources[].path', 'availableFields[].field'],
10793
- inputSchema: { type: 'object', required: ['sourceKind'], properties: { sourceKind: { enum: ['praxis.stats', 'derived'] }, resource: { type: 'string', pattern: '^/(?!/)' }, operation: { enum: chartStatsOperations }, comparisonPeriod: chartComparisonPeriodSchema, dimensions: { type: 'array' }, metrics: { type: 'array' } } },
10794
- failureModes: ['resource-not-in-api-metadata', 'field-not-in-schema', 'stats-operation-not-supported', 'comparison-period-incomplete', 'comparison-metric-unsupported', 'remote-binding-incomplete'],
10795
- description: 'Binds a derived or praxis.stats source using governed resource, capability and field catalogs. For praxis.stats, availableResources[].id is discovery-only: validation requires the matching availableResources[].path and compilation persists that path. Comparison requires one canonical operation, metrics[] and an eligible period field.',
11346
+ inputSchema: { type: 'object', required: ['sourceKind'], properties: { sourceKind: { enum: ['praxis.stats', 'derived'] }, resource: { type: 'string', pattern: '^/(?!/)' }, operation: { enum: chartStatsOperations }, limit: { type: 'integer', minimum: 1 }, comparisonPeriod: chartComparisonPeriodSchema, dimensions: { type: 'array' }, metrics: { type: 'array' } } },
11347
+ failureModes: ['resource-not-in-api-metadata', 'field-not-in-schema', 'stats-operation-not-supported', 'gauge-limit-required', 'comparison-period-incomplete', 'comparison-metric-unsupported', 'remote-binding-incomplete'],
11348
+ description: 'Binds a derived or praxis.stats source using governed resource, capability and field catalogs. For praxis.stats, availableResources[].id is discovery-only: validation requires the matching availableResources[].path and compilation persists that path. Gauge bindings preserve the canonical scalar limit; comparison requires one canonical operation, metrics[] and an eligible period field.',
10796
11349
  } }],
10797
11350
  destructive: false,
10798
11351
  requiresConfirmation: false,
10799
- validators: ['remote-resource-in-api-metadata', 'bound-fields-exist', 'stats-operation-supported', 'chart-type-series-axis-compatible', 'comparison-period-complete', 'comparison-metrics-supported', 'editor-runtime-round-trip'],
10800
- affectedPaths: ['chartDocument.source', 'chartDocument.source.options.comparisonPeriod', 'chartDocument.dimensions[]', 'chartDocument.metrics[]'],
11352
+ validators: ['remote-resource-in-api-metadata', 'bound-fields-exist', 'stats-operation-supported', 'chart-type-series-axis-compatible', 'comparison-period-complete', 'comparison-metrics-supported', 'gauge-scalar-contract', 'editor-runtime-round-trip'],
11353
+ affectedPaths: ['chartDocument.source', 'chartDocument.source.options.limit', 'chartDocument.source.options.comparisonPeriod', 'chartDocument.dimensions[]', 'chartDocument.metrics[]'],
10801
11354
  submissionImpact: 'affects-remote-binding',
10802
11355
  preconditions: ['config-initialized'],
10803
11356
  },
@@ -10816,6 +11369,25 @@ const PRAXIS_CHARTS_AUTHORING_MANIFEST = {
10816
11369
  submissionImpact: 'affects-schema-backed-data',
10817
11370
  preconditions: ['config-initialized'],
10818
11371
  },
11372
+ {
11373
+ operationId: 'gauge.scale.configure',
11374
+ title: 'Configure gauge scale',
11375
+ scope: 'layout',
11376
+ targetKind: 'gaugeScale',
11377
+ target: { kind: 'gaugeScale', resolver: 'x-ui-chart-gauge-scale', ambiguityPolicy: 'fail', required: false },
11378
+ inputSchema: {
11379
+ type: 'object',
11380
+ required: ['min', 'max'],
11381
+ properties: { min: { type: 'number' }, max: { type: 'number' } },
11382
+ },
11383
+ effects: [{ kind: 'set-value', path: 'chartDocument.gauge.scale' }],
11384
+ destructive: false,
11385
+ requiresConfirmation: false,
11386
+ validators: ['gauge-scale-valid', 'gauge-scalar-contract', 'editor-runtime-round-trip'],
11387
+ affectedPaths: ['chartDocument.gauge.scale'],
11388
+ submissionImpact: 'config-only',
11389
+ preconditions: ['config-initialized', 'chart-kind-gauge'],
11390
+ },
10819
11391
  {
10820
11392
  operationId: 'pointClick.configure',
10821
11393
  title: 'Configure chart point click',
@@ -10940,8 +11512,10 @@ const PRAXIS_CHARTS_AUTHORING_MANIFEST = {
10940
11512
  { validatorId: 'chart-version-supported', level: 'error', code: 'CHART_VERSION_SUPPORTED', description: `Only x-ui.chart version ${PRAXIS_X_UI_CHART_AUTHORABLE_VERSION} is authorable.` },
10941
11513
  { validatorId: 'chart-type-supported', level: 'error', code: 'CHART_TYPE_SUPPORTED', description: 'Chart kind must be supported by @praxisui/charts.' },
10942
11514
  { validatorId: 'chart-type-series-axis-compatible', level: 'error', code: 'CHART_TYPE_SERIES_AXIS_COMPATIBLE', description: 'Chart kind, dimensions, metrics and axes must be compatible.' },
10943
- { validatorId: 'category-slice-single-metric', level: 'error', code: 'CATEGORY_SLICE_SINGLE_METRIC', description: 'Pie, donut, funnel and pyramid charts require exactly one metric.' },
11515
+ { validatorId: 'category-slice-single-metric', level: 'error', code: 'CATEGORY_SLICE_SINGLE_METRIC', description: 'Pie, donut, funnel, pyramid and treemap charts require exactly one metric.' },
10944
11516
  { validatorId: 'combo-minimum-series', level: 'error', code: 'COMBO_MINIMUM_SERIES', description: 'Combo charts require at least two metrics.' },
11517
+ { validatorId: 'gauge-scalar-contract', level: 'error', code: 'GAUGE_SCALAR_CONTRACT', description: 'Gauge charts require one dimension, exactly one metric and, for praxis.stats, one group-by bucket selected with source.options.limit=1.' },
11518
+ { validatorId: 'gauge-scale-valid', level: 'error', code: 'GAUGE_SCALE_VALID', description: 'Gauge scale requires finite min and max values with max greater than min; runtime rejects values outside the declared scale without clamping.' },
10945
11519
  { validatorId: 'series-field-exists', level: 'error', code: 'SERIES_FIELD_EXISTS', description: 'Series field must exist in availableFields or schema context.' },
10946
11520
  { validatorId: 'series-field-aggregable', level: 'error', code: 'SERIES_FIELD_AGGREGABLE', description: 'Metric fields must support the requested aggregation.' },
10947
11521
  { validatorId: 'series-id-unique', level: 'error', code: 'SERIES_ID_UNIQUE', description: 'Metric/series identity must be stable and unique by field.' },
@@ -10976,6 +11550,7 @@ const PRAXIS_CHARTS_AUTHORING_MANIFEST = {
10976
11550
  'Remote bindings persist availableResources[].path; availableResources[].id remains discovery identity, and transient resource/field/target catalogs and diagnostics are never persisted in widget inputs.',
10977
11551
  'Comparison must preserve one canonical /stats/comparison request with metrics[] and a complete comparisonPeriod; it must not downgrade into independent group-by calls.',
10978
11552
  'Scatter with one metric preserves dimension-X/metric-Y semantics; scatter with two metrics preserves the dimension as grouped bucket identity and ordered metrics as X/Y.',
11553
+ 'Gauge preserves one dimension, exactly one metric, finite gauge.scale bounds and, for praxis.stats, the canonical group-by request with source.options.limit=1.',
10979
11554
  'Cross-filter, drilldown and selection authoring must persist structured event actions, not prompt examples or command strings.',
10980
11555
  '[P1] Backend handler, resolver and validators now execute directly; end-to-end Page Builder parity remains open until assistant requests project the transient availableTargets[].events catalog through validationContext and expose its fail-closed diagnostics.',
10981
11556
  '[P2] Transform-output semantics and target input-schema/port fields must be materialized and validated by Core; local transform.output inspection and backend-enriched inputFields are not substitutes for those canonical projections.',
@@ -10995,6 +11570,7 @@ const PRAXIS_CHARTS_AUTHORING_MANIFEST = {
10995
11570
  { id: 'configure-selection', request: 'Emit selected department as a structured selection event.', operationId: 'selection.configure', params: { action: 'emit', mapping: { department: 'department' } }, isPositive: true },
10996
11571
  { id: 'toggle-legend', request: 'Show the legend.', operationId: 'legend.configure', params: { enabled: true }, isPositive: true },
10997
11572
  { id: 'toggle-tooltip', request: 'Disable tooltips.', operationId: 'tooltip.configure', params: { enabled: false }, isPositive: true },
11573
+ { id: 'configure-gauge-scale', request: 'Set the gauge scale from zero to one.', operationId: 'gauge.scale.configure', params: { min: 0, max: 1 }, isPositive: true },
10998
11574
  { id: 'round-trip-editor-runtime', request: 'Save this chart in the editor and reopen it without changing the runtime chart document.', operationId: 'chart.document.set', params: { version: PRAXIS_X_UI_CHART_AUTHORABLE_VERSION, kind: 'bar', source: { kind: 'derived' }, dimensions: [{ field: 'department' }], metrics: [{ field: 'total', aggregation: 'count' }] }, isPositive: true },
10999
11575
  { id: 'reject-ambiguous-series-target', request: 'Remove total when more than one chart series resolves to total.', operationId: 'series.remove', target: 'total', params: { field: 'total' }, isPositive: false },
11000
11576
  { id: 'reject-prompt-routing', request: 'Use the prompt example text to decide which backend endpoint to call.', operationId: 'data.resource.bind', params: { sourceKind: 'praxis.stats', resource: 'from prompt example' }, isPositive: false },