@praxisui/charts 9.0.0-beta.72 → 9.0.0-beta.74

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,9 +1,9 @@
1
1
  import * as i0 from '@angular/core';
2
- import { Injectable, Inject, InjectionToken, input, booleanAttribute, output, viewChild, inject, ElementRef, NgZone, DestroyRef, signal, computed, afterNextRender, effect, ChangeDetectionStrategy, Component, ViewChild, Input, ENVIRONMENT_INITIALIZER } from '@angular/core';
2
+ import { Injectable, Inject, InjectionToken, input, booleanAttribute, output, viewChild, inject, ElementRef, NgZone, DestroyRef, signal, computed, afterNextRender, effect, ChangeDetectionStrategy, Component, ViewChild, Input, Injector, ENVIRONMENT_INITIALIZER, Optional } from '@angular/core';
3
3
  import * as i1$1 from '@praxisui/core';
4
- import { buildApiUrl, API_URL, PraxisI18nService, SETTINGS_PANEL_BRIDGE, normalizePraxisPresentationVisualization, providePraxisI18n, SETTINGS_PANEL_DATA, ComponentMetadataRegistry, createDefaultTableConfig, PRAXIS_TABLE_DETAIL_INLINE_RENDERERS, AnalyticsStatsRequestBuilderService, DynamicWidgetPageComponent } from '@praxisui/core';
4
+ import { buildApiUrl, API_URL, PraxisI18nService, SETTINGS_PANEL_BRIDGE, normalizePraxisDataQueryContext, resolvePraxisFilterCriteria, BUILTIN_PAGE_THEME_PRESETS, normalizePraxisPresentationVisualization, providePraxisI18n, ComponentMetadataRegistry, ResourceDiscoveryService, SETTINGS_PANEL_DATA, createDefaultTableConfig, PRAXIS_TABLE_DETAIL_INLINE_RENDERERS, AnalyticsStatsRequestBuilderService, DynamicWidgetPageComponent } from '@praxisui/core';
5
5
  import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
6
- import { throwError, map, of, isObservable, from, BehaviorSubject, Subscription } from 'rxjs';
6
+ import { throwError, map, of, isObservable, from, firstValueFrom, timeout, BehaviorSubject, Subscription } from 'rxjs';
7
7
  import * as i1$2 from '@angular/material/button';
8
8
  import { MatButtonModule } from '@angular/material/button';
9
9
  import * as i2 from '@angular/material/icon';
@@ -31,6 +31,53 @@ import * as i7 from '@angular/material/slide-toggle';
31
31
  import { MatSlideToggleModule } from '@angular/material/slide-toggle';
32
32
  import { PraxisTable } from '@praxisui/table';
33
33
 
34
+ const PRAXIS_CHART_PALETTE_TOKENS = {
35
+ 'brand-primary': ['#1263b4', '#0f766e', '#f08c00', '#c92a2a', '#7b61ff'],
36
+ 'brand-balanced': ['#1263b4', '#15803d', '#7c3aed', '#c2410c', '#be123c'],
37
+ status: ['#15803d', '#ca8a04', '#dc2626', '#2563eb', '#7c3aed'],
38
+ executive: ['#14b8a6', '#f59e0b', '#e11d48', '#8b5cf6', '#38bdf8'],
39
+ };
40
+ function isPraxisChartPaletteToken(value) {
41
+ return Object.prototype.hasOwnProperty.call(PRAXIS_CHART_PALETTE_TOKENS, value);
42
+ }
43
+ function resolvePraxisChartPaletteToken(value) {
44
+ return isPraxisChartPaletteToken(value)
45
+ ? [...PRAXIS_CHART_PALETTE_TOKENS[value]]
46
+ : undefined;
47
+ }
48
+ const PRAXIS_CHART_THEME_VARIANTS = {
49
+ default: {
50
+ palette: [...PRAXIS_CHART_PALETTE_TOKENS['brand-primary']],
51
+ legend: { position: 'bottom' },
52
+ surface: { mode: 'auto' },
53
+ },
54
+ compact: {
55
+ palette: [...PRAXIS_CHART_PALETTE_TOKENS['brand-balanced']],
56
+ borderRadius: 6,
57
+ legend: { position: 'bottom' },
58
+ surface: {
59
+ mode: 'embedded',
60
+ background: 'transparent',
61
+ borderWidth: 0,
62
+ borderRadius: 0,
63
+ },
64
+ },
65
+ executive: {
66
+ palette: [...PRAXIS_CHART_PALETTE_TOKENS.executive],
67
+ backgroundColor: '#111827',
68
+ textColor: '#f9fafb',
69
+ borderRadius: 8,
70
+ legend: { position: 'bottom' },
71
+ surface: {
72
+ mode: 'contained',
73
+ background: '#111827',
74
+ borderColor: '#374151',
75
+ borderWidth: 1,
76
+ borderRadius: 8,
77
+ },
78
+ },
79
+ };
80
+
34
81
  class PraxisChartDataTransformerService {
35
82
  transform(config, rows) {
36
83
  if (config.type === 'pie' || config.type === 'donut') {
@@ -52,9 +99,10 @@ class PraxisChartDataTransformerService {
52
99
  hasData: false,
53
100
  };
54
101
  }
55
- const categories = Array.from(new Set(rows.map((row) => this.normalizeCategory(row[categoryField]))));
56
- const timeAxis = config.axes?.x?.type === 'time';
57
- const series = config.series.map((seriesConfig) => this.buildCartesianSeries(config, seriesConfig, rows, categories, timeAxis));
102
+ const categoryBuckets = this.buildCategoryBuckets(config, rows, categoryField);
103
+ const categories = categoryBuckets.map((bucket) => bucket.label);
104
+ const timeAxis = this.usesCanonicalTimeCoordinates(config);
105
+ const series = config.series.map((seriesConfig) => this.buildCartesianSeries(config, seriesConfig, categoryBuckets, timeAxis));
58
106
  return {
59
107
  mode: 'cartesian',
60
108
  categories,
@@ -75,23 +123,17 @@ class PraxisChartDataTransformerService {
75
123
  hasData: false,
76
124
  };
77
125
  }
78
- const bucket = new Map();
79
- for (const row of rows) {
80
- const key = this.normalizeCategory(row[categoryField]);
81
- const nextValue = this.extractMetricValue(row, seriesConfig);
82
- const current = bucket.get(key);
83
- bucket.set(key, {
84
- value: (current?.value ?? 0) + nextValue,
85
- source: current?.source ?? row,
86
- });
87
- }
88
- const slices = Array.from(bucket.entries()).map(([name, item]) => ({
89
- id: `${seriesConfig.id}:${name}`,
90
- name,
91
- value: item.value,
92
- color: seriesConfig.color,
93
- data: this.buildPointData(item.source, item.value),
94
- }));
126
+ const slices = this.buildCategoryBuckets(config, rows, categoryField)
127
+ .map((bucket) => {
128
+ const value = bucket.rows.reduce((sum, row) => sum + this.extractMetricValue(row, seriesConfig), 0);
129
+ return {
130
+ id: `${seriesConfig.id}:${bucket.identity}`,
131
+ name: bucket.label,
132
+ value,
133
+ color: seriesConfig.color,
134
+ data: this.buildPointData(bucket.rows[0], value),
135
+ };
136
+ });
95
137
  return {
96
138
  mode: 'pie',
97
139
  categories: slices.map((slice) => slice.name),
@@ -100,15 +142,7 @@ class PraxisChartDataTransformerService {
100
142
  hasData: slices.some((slice) => slice.value > 0),
101
143
  };
102
144
  }
103
- buildCartesianSeries(config, seriesConfig, rows, categories, timeAxis) {
104
- const categoryField = config.axes?.x?.field;
105
- const byCategory = new Map();
106
- for (const row of rows) {
107
- const key = this.normalizeCategory(row[categoryField]);
108
- const bucket = byCategory.get(key) ?? [];
109
- bucket.push(row);
110
- byCategory.set(key, bucket);
111
- }
145
+ buildCartesianSeries(config, seriesConfig, categoryBuckets, timeAxis) {
112
146
  return {
113
147
  id: seriesConfig.id,
114
148
  name: seriesConfig.name ?? seriesConfig.metric?.label ?? seriesConfig.id,
@@ -119,16 +153,94 @@ class PraxisChartDataTransformerService {
119
153
  area: this.isAreaLikeSeries(config, seriesConfig),
120
154
  color: seriesConfig.color,
121
155
  labelsVisible: seriesConfig.labels?.visible ?? false,
122
- points: categories.map((category) => {
123
- const bucket = byCategory.get(category) ?? [];
124
- const value = bucket.length ? this.aggregate(bucket, seriesConfig) : null;
125
- const source = bucket[0];
156
+ points: categoryBuckets.map((bucket) => {
157
+ const value = this.aggregate(bucket.rows, seriesConfig);
158
+ const source = bucket.rows[0];
126
159
  return source
127
- ? this.buildPointData(source, timeAxis ? [category, value] : value)
128
- : (timeAxis ? [category, value] : value);
160
+ ? this.buildPointData(source, timeAxis ? [bucket.coordinate, value] : value)
161
+ : (timeAxis ? [bucket.coordinate, value] : value);
129
162
  }),
130
163
  };
131
164
  }
165
+ buildCategoryBuckets(config, rows, categoryField) {
166
+ const buckets = new Map();
167
+ const usesCanonicalStatsIdentity = config.dataSource?.kind === 'remote'
168
+ && config.dataSource.query?.sourceKind === 'praxis.stats';
169
+ const usesCanonicalTimeCoordinate = usesCanonicalStatsIdentity
170
+ && this.usesCanonicalTimeCoordinates(config);
171
+ for (const row of rows) {
172
+ const category = this.normalizeCategory(row[categoryField]);
173
+ const coordinate = usesCanonicalTimeCoordinate
174
+ ? this.resolveCanonicalTimeCoordinate(row, categoryField, category)
175
+ : category;
176
+ const label = usesCanonicalTimeCoordinate
177
+ ? this.resolveTimePresentationLabel(row, category)
178
+ : category;
179
+ const identity = this.resolveCategoryIdentity(row, label, coordinate, usesCanonicalStatsIdentity, usesCanonicalTimeCoordinate);
180
+ const bucket = buckets.get(identity);
181
+ if (bucket) {
182
+ bucket.rows.push(row);
183
+ continue;
184
+ }
185
+ buckets.set(identity, {
186
+ identity,
187
+ label,
188
+ coordinate,
189
+ rows: [row],
190
+ });
191
+ }
192
+ return Array.from(buckets.values());
193
+ }
194
+ resolveCategoryIdentity(row, label, coordinate, usesCanonicalStatsIdentity, usesCanonicalTimeCoordinate) {
195
+ if (!usesCanonicalStatsIdentity) {
196
+ return label;
197
+ }
198
+ if (usesCanonicalTimeCoordinate) {
199
+ return `time:${this.typedIdentity(coordinate)}`;
200
+ }
201
+ if (row['key'] !== null && row['key'] !== undefined) {
202
+ return `key:${this.typedIdentity(row['key'])}`;
203
+ }
204
+ return `label:${label}`;
205
+ }
206
+ resolveCanonicalTimeCoordinate(row, categoryField, fallback) {
207
+ return this.extractTimeCoordinate(row['start'])
208
+ ?? this.extractTimeCoordinate(row['key'])
209
+ ?? this.extractTimeCoordinate(row[categoryField])
210
+ ?? fallback;
211
+ }
212
+ usesCanonicalTimeCoordinates(config) {
213
+ if (config.axes?.x?.type === 'time') {
214
+ return true;
215
+ }
216
+ return config.dataSource?.kind === 'remote'
217
+ && config.dataSource.query?.sourceKind === 'praxis.stats'
218
+ && config.dataSource.query.statsOperation === 'timeseries';
219
+ }
220
+ extractTimeCoordinate(value) {
221
+ const candidate = this.extractScatterXValue(value, 'time');
222
+ if (candidate === null) {
223
+ return null;
224
+ }
225
+ if (typeof candidate === 'string'
226
+ && !/^\d{4}-\d{2}-\d{2}(?:[T\s]\d{2}:\d{2}.*)?$/.test(candidate)) {
227
+ return null;
228
+ }
229
+ const timestamp = typeof candidate === 'number' ? new Date(candidate).getTime() : Date.parse(candidate);
230
+ return Number.isFinite(timestamp) ? candidate : null;
231
+ }
232
+ resolveTimePresentationLabel(row, fallback) {
233
+ const label = row['label'];
234
+ return label === null || label === undefined || label === ''
235
+ ? fallback
236
+ : this.normalizeCategory(label);
237
+ }
238
+ typedIdentity(value) {
239
+ if (value instanceof Date) {
240
+ return `date:${value.toISOString()}`;
241
+ }
242
+ return `${typeof value}:${String(value)}`;
243
+ }
132
244
  buildPointData(row, value) {
133
245
  return {
134
246
  ...row,
@@ -295,54 +407,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
295
407
  args: [{ providedIn: 'root' }]
296
408
  }] });
297
409
 
298
- const PRAXIS_CHART_PALETTE_TOKENS = {
299
- 'brand-primary': ['#1263b4', '#0f766e', '#f08c00', '#c92a2a', '#7b61ff'],
300
- 'brand-balanced': ['#1263b4', '#15803d', '#7c3aed', '#c2410c', '#be123c'],
301
- status: ['#15803d', '#ca8a04', '#dc2626', '#2563eb', '#7c3aed'],
302
- executive: ['#14b8a6', '#f59e0b', '#e11d48', '#8b5cf6', '#38bdf8'],
303
- };
304
- function isPraxisChartPaletteToken(value) {
305
- return Object.prototype.hasOwnProperty.call(PRAXIS_CHART_PALETTE_TOKENS, value);
306
- }
307
- function resolvePraxisChartPaletteToken(value) {
308
- return isPraxisChartPaletteToken(value)
309
- ? [...PRAXIS_CHART_PALETTE_TOKENS[value]]
310
- : undefined;
311
- }
312
- const PRAXIS_CHART_THEME_VARIANTS = {
313
- default: {
314
- palette: [...PRAXIS_CHART_PALETTE_TOKENS['brand-primary']],
315
- legend: { position: 'bottom' },
316
- surface: { mode: 'auto' },
317
- },
318
- compact: {
319
- palette: [...PRAXIS_CHART_PALETTE_TOKENS['brand-balanced']],
320
- textColor: '#374151',
321
- borderRadius: 6,
322
- legend: { position: 'bottom' },
323
- surface: {
324
- mode: 'embedded',
325
- background: 'transparent',
326
- borderWidth: 0,
327
- borderRadius: 0,
328
- },
329
- },
330
- executive: {
331
- palette: [...PRAXIS_CHART_PALETTE_TOKENS.executive],
332
- backgroundColor: '#111827',
333
- textColor: '#f9fafb',
334
- borderRadius: 8,
335
- legend: { position: 'bottom' },
336
- surface: {
337
- mode: 'contained',
338
- background: '#111827',
339
- borderColor: '#374151',
340
- borderWidth: 1,
341
- borderRadius: 8,
342
- },
343
- },
344
- };
345
-
346
410
  class ChartContractNormalizerService {
347
411
  normalize(input) {
348
412
  let document = structuredClone(input);
@@ -504,9 +568,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
504
568
  args: [{ providedIn: 'root' }]
505
569
  }] });
506
570
 
571
+ const PRAXIS_X_UI_CHART_AUTHORABLE_VERSION = '0.1.0';
572
+
507
573
  class ChartContractValidationService {
508
574
  validate(document) {
509
575
  const issues = [];
576
+ this.validateVersion(document, issues);
510
577
  this.validateSource(document, issues);
511
578
  this.validateTheme(document, issues);
512
579
  this.validateSizing(document, issues);
@@ -518,6 +585,11 @@ class ChartContractValidationService {
518
585
  issues,
519
586
  };
520
587
  }
588
+ validateVersion(document, issues) {
589
+ if (document.version !== PRAXIS_X_UI_CHART_AUTHORABLE_VERSION) {
590
+ issues.push(this.error('unsupported-version', 'version', `x-ui.chart version="${document.version}" is not authorable in @praxisui/charts; expected ${PRAXIS_X_UI_CHART_AUTHORABLE_VERSION}.`));
591
+ }
592
+ }
521
593
  validateSource(document, issues) {
522
594
  if (document.source.kind !== 'praxis.stats' && document.source.kind !== 'derived') {
523
595
  issues.push(this.error('unsupported-source-kind', 'source.kind', `x-ui.chart source.kind="${document.source.kind}" is not supported in @praxisui/charts.`));
@@ -525,6 +597,17 @@ class ChartContractValidationService {
525
597
  if (document.source.kind === 'praxis.stats' && !document.source.resource?.trim()) {
526
598
  issues.push(this.error('missing-resource', 'source.resource', 'x-ui.chart source.resource is required for source.kind="praxis.stats".'));
527
599
  }
600
+ const rawStatsResource = document.source.kind === 'praxis.stats'
601
+ ? document.source.resource
602
+ : undefined;
603
+ const statsResource = rawStatsResource?.trim();
604
+ if (statsResource
605
+ && (rawStatsResource !== statsResource
606
+ || !statsResource.startsWith('/')
607
+ || statsResource.startsWith('//')
608
+ || /^[a-z][a-z\d+.-]*:/i.test(statsResource))) {
609
+ issues.push(this.error('invalid-resource-path', 'source.resource', 'x-ui.chart source.resource must be a governed root-relative path starting with exactly one "/".'));
610
+ }
528
611
  if (document.source.kind === 'praxis.stats' && !document.source.operation) {
529
612
  issues.push(this.error('missing-operation', 'source.operation', 'x-ui.chart source.operation is required for source.kind="praxis.stats".'));
530
613
  }
@@ -546,6 +629,11 @@ class ChartContractValidationService {
546
629
  if (!document.metrics?.length) {
547
630
  issues.push(this.error('missing-metric', 'metrics', 'x-ui.chart requires at least one metric.'));
548
631
  }
632
+ document.metrics?.forEach((metric, index) => {
633
+ if (!metric.field?.trim()) {
634
+ issues.push(this.error('metric-field-required', `metrics[${index}].field`, 'x-ui.chart metric.field is required.'));
635
+ }
636
+ });
549
637
  document.metrics
550
638
  ?.filter((metric) => metric.aggregation === 'distinct-count' && !metric.field?.trim())
551
639
  .forEach((_metric, index) => {
@@ -580,6 +668,11 @@ class ChartContractValidationService {
580
668
  }
581
669
  validateKinds(document, issues) {
582
670
  const metricCount = document.metrics?.length ?? 0;
671
+ document.dimensions?.forEach((dimension, index) => {
672
+ if (!dimension.field?.trim()) {
673
+ issues.push(this.error('dimension-field-required', `dimensions[${index}].field`, 'x-ui.chart dimension.field is required.'));
674
+ }
675
+ });
583
676
  if (document.kind !== 'pie' && document.kind !== 'donut' && !document.dimensions?.length) {
584
677
  issues.push(this.error('missing-dimension', 'dimensions', 'x-ui.chart cartesian charts require at least one dimension.'));
585
678
  }
@@ -595,17 +688,7 @@ class ChartContractValidationService {
595
688
  if (document.kind !== 'combo' && document.metrics?.some((metric) => metric.axis === 'secondary')) {
596
689
  issues.push(this.error('secondary-axis-non-combo', 'metrics', 'x-ui.chart axis="secondary" is supported only for combo charts in @praxisui/charts.'));
597
690
  }
598
- if (document.source.kind === 'praxis.stats'
599
- && document.source.operation === 'distribution'
600
- && metricCount > 1) {
601
- issues.push(this.error('distribution-single-metric', 'metrics', 'x-ui.chart praxis.stats distribution supports only a single metric in @praxisui/charts.'));
602
- }
603
- if (document.source.kind === 'praxis.stats'
604
- && document.source.operation === 'distribution'
605
- && document.source.options?.mode === 'histogram'
606
- && !(Number(document.source.options.bucketSize) > 0)) {
607
- issues.push(this.error('distribution-histogram-missing-bucket-size', 'source.options.bucketSize', 'x-ui.chart praxis.stats histogram distribution requires source.options.bucketSize to match the backend contract.'));
608
- }
691
+ this.validateDistribution(document, issues);
609
692
  if (document.kind === 'horizontal-bar' && document.orientation && document.orientation !== 'horizontal') {
610
693
  issues.push(this.error('horizontal-bar-orientation', 'orientation', 'x-ui.chart kind="horizontal-bar" requires orientation="horizontal" when orientation is provided.'));
611
694
  }
@@ -622,6 +705,48 @@ class ChartContractValidationService {
622
705
  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.'));
623
706
  }
624
707
  }
708
+ validateDistribution(document, issues) {
709
+ if (document.source.kind !== 'praxis.stats'
710
+ || document.source.operation !== 'distribution') {
711
+ return;
712
+ }
713
+ const dimensions = document.dimensions ?? [];
714
+ const metrics = document.metrics ?? [];
715
+ if (dimensions.length !== 1) {
716
+ issues.push(this.error('distribution-single-dimension', 'dimensions', 'x-ui.chart praxis.stats distribution requires exactly one dimension.'));
717
+ }
718
+ if (metrics.length !== 1) {
719
+ issues.push(this.error('distribution-single-metric', 'metrics', 'x-ui.chart praxis.stats distribution requires exactly one metric.'));
720
+ }
721
+ const options = document.source.options;
722
+ const mode = options?.mode ?? 'terms';
723
+ if (mode !== 'terms' && mode !== 'histogram') {
724
+ issues.push(this.error('distribution-mode-unsupported', 'source.options.mode', `x-ui.chart praxis.stats distribution mode="${String(mode)}" is not supported; expected terms or histogram.`));
725
+ return;
726
+ }
727
+ if (mode === 'terms') {
728
+ if (options?.bucketSize !== undefined || options?.bucketCount !== undefined) {
729
+ issues.push(this.error('distribution-terms-histogram-options', 'source.options', 'x-ui.chart praxis.stats terms distribution does not support source.options.bucketSize or source.options.bucketCount.'));
730
+ }
731
+ return;
732
+ }
733
+ const bucketSize = options?.bucketSize;
734
+ if (typeof bucketSize !== 'number' || !Number.isFinite(bucketSize) || bucketSize <= 0) {
735
+ issues.push(this.error('distribution-histogram-missing-bucket-size', 'source.options.bucketSize', 'x-ui.chart praxis.stats histogram distribution requires a finite source.options.bucketSize greater than zero.'));
736
+ }
737
+ const bucketCount = options?.bucketCount;
738
+ if (bucketCount !== undefined
739
+ && (typeof bucketCount !== 'number'
740
+ || !Number.isFinite(bucketCount)
741
+ || !Number.isInteger(bucketCount)
742
+ || bucketCount <= 0)) {
743
+ issues.push(this.error('distribution-histogram-bucket-count-invalid', 'source.options.bucketCount', 'x-ui.chart praxis.stats histogram source.options.bucketCount must be a positive integer when provided.'));
744
+ }
745
+ const metric = metrics[0];
746
+ if (metrics.length === 1 && (metric.aggregation ?? 'count') !== 'count') {
747
+ issues.push(this.error('distribution-histogram-count-only', 'metrics[0].aggregation', 'x-ui.chart praxis.stats histogram distribution requires COUNT; the canonical mapper materializes this backend metric without a field.'));
748
+ }
749
+ }
625
750
  validateEvents(document, issues) {
626
751
  this.validateEventAction('pointClick', document.events?.pointClick, issues);
627
752
  this.validateEventAction('drillDown', document.events?.drillDown, issues);
@@ -856,7 +981,7 @@ class PraxisChartCanonicalContractMapperService {
856
981
  }
857
982
  buildInteractions(contract) {
858
983
  return {
859
- pointClick: Boolean(contract.events?.pointClick || contract.events?.drillDown),
984
+ pointClick: Boolean(contract.events?.pointClick),
860
985
  selection: Boolean(contract.events?.selectionChange),
861
986
  drillDown: Boolean(contract.events?.drillDown),
862
987
  crossFilter: Boolean(contract.events?.crossFilter),
@@ -1201,13 +1326,24 @@ class PraxisChartStatsApiService {
1201
1326
  return [];
1202
1327
  }
1203
1328
  const metricBindings = this.resolveMetricBindings(config, request, response);
1329
+ if ('periodField' in response) {
1330
+ return response.buckets.map((bucket) => {
1331
+ const category = this.resolveBucketCategory(bucket, categoryField, config);
1332
+ return {
1333
+ [categoryField]: category,
1334
+ ...this.projectComparisonMetricValues(metricBindings, bucket.values),
1335
+ key: bucket.key ?? null,
1336
+ label: bucket.label ?? category,
1337
+ };
1338
+ });
1339
+ }
1204
1340
  if ('points' in response) {
1205
1341
  return response.points.map((point) => {
1206
- const category = point.label ?? point.start ?? point.end ?? '';
1342
+ const category = point.start ?? point.end ?? point.label ?? '';
1207
1343
  return {
1208
1344
  [categoryField]: category,
1209
1345
  ...this.projectMetricValues(metricBindings, point.values, point.value, point.count),
1210
- key: point.start ?? point.label ?? point.end ?? category,
1346
+ key: point.start ?? point.end ?? point.label ?? category,
1211
1347
  label: point.label ?? category,
1212
1348
  value: point.value ?? null,
1213
1349
  count: point.count ?? null,
@@ -1358,6 +1494,16 @@ class PraxisChartStatsApiService {
1358
1494
  return acc;
1359
1495
  }, {});
1360
1496
  }
1497
+ projectComparisonMetricValues(bindings, values) {
1498
+ return bindings.reduce((acc, binding) => {
1499
+ const match = /^__praxisComparison_(.+)_(current|previous)$/.exec(binding.field);
1500
+ const value = match
1501
+ ? values?.[match[1]]?.[match[2]]
1502
+ : undefined;
1503
+ acc[binding.field] = this.resolveMetricValue(value, null);
1504
+ return acc;
1505
+ }, {});
1506
+ }
1361
1507
  buildStatsUrl(statsPath) {
1362
1508
  const base = this.buildDefaultApiBase();
1363
1509
  const normalizedStatsPath = this.normalizePath(statsPath);
@@ -1422,7 +1568,9 @@ class PraxisChartComponent {
1422
1568
  availableFields = input([], ...(ngDevMode ? [{ debugName: "availableFields" }] : /* istanbul ignore next */ []));
1423
1569
  availableTargets = input([], ...(ngDevMode ? [{ debugName: "availableTargets" }] : /* istanbul ignore next */ []));
1424
1570
  pointClick = output();
1571
+ pointAction = output();
1425
1572
  selectionChange = output();
1573
+ drillDown = output();
1426
1574
  crossFilter = output();
1427
1575
  queryRequest = output();
1428
1576
  loadStateChange = output();
@@ -1439,7 +1587,10 @@ class PraxisChartComponent {
1439
1587
  settingsPanel = inject(SETTINGS_PANEL_BRIDGE, { optional: true });
1440
1588
  destroyRef = inject(DestroyRef);
1441
1589
  resizeObserver = signal(null, ...(ngDevMode ? [{ debugName: "resizeObserver" }] : /* istanbul ignore next */ []));
1590
+ resizeObserverHost = null;
1442
1591
  shellObserver = signal(null, ...(ngDevMode ? [{ debugName: "shellObserver" }] : /* istanbul ignore next */ []));
1592
+ themeObserver = signal(null, ...(ngDevMode ? [{ debugName: "themeObserver" }] : /* istanbul ignore next */ []));
1593
+ inheritedTheme = signal(null, ...(ngDevMode ? [{ debugName: "inheritedTheme" }] : /* istanbul ignore next */ []));
1443
1594
  currentLoadState = signal('idle', ...(ngDevMode ? [{ debugName: "currentLoadState" }] : /* istanbul ignore next */ []));
1444
1595
  remoteResolvedData = signal(null, ...(ngDevMode ? [{ debugName: "remoteResolvedData" }] : /* istanbul ignore next */ []));
1445
1596
  remoteRuntimeState = signal('idle', ...(ngDevMode ? [{ debugName: "remoteRuntimeState" }] : /* istanbul ignore next */ []));
@@ -1455,11 +1606,14 @@ class PraxisChartComponent {
1455
1606
  previousRemoteSignature = null;
1456
1607
  previousRemoteDataResolver = undefined;
1457
1608
  previousDocumentSignature = null;
1609
+ inheritedThemeSignature = null;
1458
1610
  editorSessionSubscriptions = [];
1611
+ remoteRequestSubscription = null;
1612
+ remoteRequestSequence = 0;
1459
1613
  effectiveConfig = computed(() => {
1460
- const base = this.mappedRuntimeConfig() ?? this.config();
1461
- const runtimeQueryContext = normalizePraxisDataQueryContextBridge(this.queryContext());
1462
- const runtimeFilters = resolvePraxisFilterCriteriaBridge(this.filterCriteria(), runtimeQueryContext);
1614
+ const base = this.applyInheritedTheme(this.mappedRuntimeConfig() ?? this.config(), this.inheritedTheme());
1615
+ const runtimeQueryContext = normalizePraxisDataQueryContext(this.queryContext());
1616
+ const runtimeFilters = resolvePraxisFilterCriteria(this.filterCriteria(), runtimeQueryContext);
1463
1617
  if (!runtimeQueryContext
1464
1618
  && !Object.keys(runtimeFilters).length) {
1465
1619
  return base;
@@ -1534,18 +1688,17 @@ class PraxisChartComponent {
1534
1688
  };
1535
1689
  }
1536
1690
  if (remoteState === 'error') {
1691
+ const technicalDetails = this.remoteTechnicalError() ?? config.state?.error?.technicalDetails;
1537
1692
  return {
1538
1693
  ...config,
1539
1694
  preferredLoadState: 'error',
1540
- state: config.state?.error
1541
- ? {
1542
- ...config.state,
1543
- error: {
1544
- ...config.state.error,
1545
- technicalDetails: this.remoteTechnicalError() ?? config.state.error.technicalDetails,
1546
- },
1547
- }
1548
- : config.state,
1695
+ state: {
1696
+ ...config.state,
1697
+ error: {
1698
+ ...config.state?.error,
1699
+ technicalDetails,
1700
+ },
1701
+ },
1549
1702
  };
1550
1703
  }
1551
1704
  if (remoteData !== null) {
@@ -1614,6 +1767,7 @@ class PraxisChartComponent {
1614
1767
  constructor() {
1615
1768
  afterNextRender(() => {
1616
1769
  this.observeShellSizingContext();
1770
+ this.observeThemeContext();
1617
1771
  });
1618
1772
  effect(() => {
1619
1773
  const document = this.chartDocument();
@@ -1646,8 +1800,9 @@ class PraxisChartComponent {
1646
1800
  const config = this.effectiveConfig();
1647
1801
  const explicitData = this.data();
1648
1802
  const remoteDataResolver = this.remoteDataResolver();
1803
+ const queryContext = normalizePraxisDataQueryContext(this.queryContext());
1649
1804
  const nextSignature = explicitData === null || explicitData === undefined
1650
- ? this.buildRemoteSignature(config)
1805
+ ? this.buildRemoteSignature(config, queryContext)
1651
1806
  : null;
1652
1807
  if (nextSignature === this.previousRemoteSignature
1653
1808
  && remoteDataResolver === this.previousRemoteDataResolver) {
@@ -1655,6 +1810,7 @@ class PraxisChartComponent {
1655
1810
  }
1656
1811
  this.previousRemoteSignature = nextSignature;
1657
1812
  this.previousRemoteDataResolver = remoteDataResolver;
1813
+ this.cancelRemoteDataRequest();
1658
1814
  this.remoteResolvedData.set(null);
1659
1815
  this.remoteRuntimeState.set('idle');
1660
1816
  this.remoteTechnicalError.set(null);
@@ -1675,25 +1831,42 @@ class PraxisChartComponent {
1675
1831
  if (this.remoteRuntimeState() === 'loading' || this.remoteResolvedData() !== null) {
1676
1832
  return;
1677
1833
  }
1834
+ const queryContext = normalizePraxisDataQueryContext(this.queryContext());
1678
1835
  const event = {
1679
1836
  chartId: effectiveConfig.id,
1680
1837
  dataSource: effectiveConfig.dataSource,
1681
1838
  query: effectiveConfig.dataSource.query,
1682
- queryContext: normalizePraxisDataQueryContextBridge(this.queryContext()),
1839
+ queryContext,
1683
1840
  };
1684
1841
  this.queryRequest.emit(event);
1842
+ const unsupportedExpression = resolveUnsupportedStatsFilterExpressionDiagnostic(queryContext);
1843
+ if (unsupportedExpression) {
1844
+ this.cancelRemoteDataRequest();
1845
+ this.remoteRuntimeState.set('error');
1846
+ this.remoteResolvedData.set([]);
1847
+ this.remoteTechnicalError.set(unsupportedExpression);
1848
+ return;
1849
+ }
1685
1850
  this.remoteRuntimeState.set('loading');
1686
1851
  this.remoteResolvedData.set([]);
1687
1852
  this.remoteTechnicalError.set(null);
1688
- this.executeRemoteDataRequest(event, effectiveConfig)
1853
+ this.cancelRemoteDataRequest();
1854
+ const requestId = ++this.remoteRequestSequence;
1855
+ this.remoteRequestSubscription = this.executeRemoteDataRequest(event, effectiveConfig)
1689
1856
  .pipe(takeUntilDestroyed(this.destroyRef))
1690
1857
  .subscribe({
1691
1858
  next: (rows) => {
1859
+ if (requestId !== this.remoteRequestSequence) {
1860
+ return;
1861
+ }
1692
1862
  this.remoteResolvedData.set(rows);
1693
1863
  this.remoteRuntimeState.set('ready');
1694
1864
  this.remoteTechnicalError.set(null);
1695
1865
  },
1696
1866
  error: (error) => {
1867
+ if (requestId !== this.remoteRequestSequence) {
1868
+ return;
1869
+ }
1697
1870
  this.remoteResolvedData.set([]);
1698
1871
  this.remoteRuntimeState.set('error');
1699
1872
  this.remoteTechnicalError.set(error instanceof Error && error.message.trim()
@@ -1706,7 +1879,9 @@ class PraxisChartComponent {
1706
1879
  this.renderAttempt();
1707
1880
  if (this.loadState() !== 'ready') {
1708
1881
  this.cancelScheduledRender();
1882
+ this.cancelScheduledResize();
1709
1883
  this.deferredRenderAttempts = 0;
1884
+ this.destroyResizeObserver();
1710
1885
  this.engine.destroy();
1711
1886
  return;
1712
1887
  }
@@ -1730,8 +1905,10 @@ class PraxisChartComponent {
1730
1905
  });
1731
1906
  this.destroyRef.onDestroy(() => {
1732
1907
  this.clearEditorSessionSubscriptions();
1733
- this.resizeObserver()?.disconnect();
1908
+ this.cancelRemoteDataRequest();
1909
+ this.destroyResizeObserver();
1734
1910
  this.shellObserver()?.disconnect();
1911
+ this.themeObserver()?.disconnect();
1735
1912
  this.cancelScheduledResize();
1736
1913
  this.cancelScheduledRender();
1737
1914
  this.engine.destroy();
@@ -1790,13 +1967,45 @@ class PraxisChartComponent {
1790
1967
  this.pointClick.emit(event);
1791
1968
  const config = this.renderConfig();
1792
1969
  const interactions = config.interactions;
1793
- if (!interactions?.selection && !interactions?.crossFilter) {
1970
+ if (!interactions?.pointClick && !interactions?.selection && !interactions?.drillDown && !interactions?.crossFilter) {
1794
1971
  return;
1795
1972
  }
1796
1973
  const selectionEvent = this.buildSelectionEvent(config, event);
1974
+ if (interactions.pointClick) {
1975
+ const pointAction = interactions.eventActions?.pointClick;
1976
+ if (pointAction) {
1977
+ this.pointAction.emit({
1978
+ chartId: config.id ?? event.chartId,
1979
+ event: 'pointClick',
1980
+ point: {
1981
+ ...event,
1982
+ chartId: config.id ?? event.chartId,
1983
+ },
1984
+ filters: this.buildEventFilters(config, event, pointAction.mapping),
1985
+ target: pointAction.target,
1986
+ action: pointAction,
1987
+ });
1988
+ }
1989
+ }
1797
1990
  if (interactions.selection) {
1798
1991
  this.selectionChange.emit(selectionEvent);
1799
1992
  }
1993
+ if (interactions.drillDown) {
1994
+ const drillDownAction = interactions.eventActions?.drillDown;
1995
+ if (drillDownAction) {
1996
+ this.drillDown.emit({
1997
+ chartId: config.id ?? event.chartId,
1998
+ filters: this.buildEventFilters(config, event, drillDownAction.mapping),
1999
+ target: drillDownAction.target,
2000
+ action: drillDownAction,
2001
+ source: selectionEvent,
2002
+ point: {
2003
+ ...event,
2004
+ chartId: config.id ?? event.chartId,
2005
+ },
2006
+ });
2007
+ }
2008
+ }
1800
2009
  if (interactions.crossFilter) {
1801
2010
  const crossFilterAction = interactions.eventActions?.crossFilter;
1802
2011
  this.crossFilter.emit({
@@ -1815,6 +2024,7 @@ class PraxisChartComponent {
1815
2024
  ...event,
1816
2025
  chartId: config.id ?? event.chartId,
1817
2026
  selected: true,
2027
+ mode: 'single',
1818
2028
  filters,
1819
2029
  action,
1820
2030
  };
@@ -1870,16 +2080,23 @@ class PraxisChartComponent {
1870
2080
  }
1871
2081
  }
1872
2082
  ensureResizeObserver(host) {
1873
- if (this.resizeObserver()) {
2083
+ if (this.resizeObserver() && this.resizeObserverHost === host) {
1874
2084
  return;
1875
2085
  }
2086
+ this.destroyResizeObserver();
1876
2087
  const observer = new ResizeObserver(() => {
1877
2088
  this.scheduleChartRender();
1878
2089
  this.scheduleEngineResize();
1879
2090
  });
1880
2091
  observer.observe(host);
2092
+ this.resizeObserverHost = host;
1881
2093
  this.resizeObserver.set(observer);
1882
2094
  }
2095
+ destroyResizeObserver() {
2096
+ this.resizeObserver()?.disconnect();
2097
+ this.resizeObserver.set(null);
2098
+ this.resizeObserverHost = null;
2099
+ }
1883
2100
  hasRenderableSize(host) {
1884
2101
  const rect = host.getBoundingClientRect();
1885
2102
  const width = rect.width || host.clientWidth || host.offsetWidth;
@@ -1903,7 +2120,7 @@ class PraxisChartComponent {
1903
2120
  const trimmed = value?.trim();
1904
2121
  return trimmed || null;
1905
2122
  }
1906
- buildRemoteSignature(config) {
2123
+ buildRemoteSignature(config, queryContext) {
1907
2124
  if (config.dataSource?.kind !== 'remote') {
1908
2125
  return null;
1909
2126
  }
@@ -1911,8 +2128,14 @@ class PraxisChartComponent {
1911
2128
  id: config.id,
1912
2129
  resourcePath: config.dataSource.resourcePath,
1913
2130
  query: config.dataSource.query,
2131
+ queryContext,
1914
2132
  });
1915
2133
  }
2134
+ cancelRemoteDataRequest() {
2135
+ this.remoteRequestSequence += 1;
2136
+ this.remoteRequestSubscription?.unsubscribe();
2137
+ this.remoteRequestSubscription = null;
2138
+ }
1916
2139
  clearEditorSessionSubscriptions() {
1917
2140
  for (const subscription of this.editorSessionSubscriptions) {
1918
2141
  subscription.unsubscribe();
@@ -1940,6 +2163,100 @@ class PraxisChartComponent {
1940
2163
  });
1941
2164
  this.shellObserver.set(observer);
1942
2165
  }
2166
+ observeThemeContext() {
2167
+ const host = this.hostElement.nativeElement;
2168
+ this.syncInheritedTheme(host);
2169
+ if (typeof MutationObserver === 'undefined') {
2170
+ return;
2171
+ }
2172
+ this.themeObserver()?.disconnect();
2173
+ const observer = new MutationObserver(() => {
2174
+ this.syncInheritedTheme(host);
2175
+ });
2176
+ let current = host;
2177
+ while (current) {
2178
+ observer.observe(current, {
2179
+ attributes: true,
2180
+ attributeFilter: [
2181
+ 'class',
2182
+ 'style',
2183
+ 'data-theme',
2184
+ 'data-theme-preset',
2185
+ 'data-color-scheme',
2186
+ ],
2187
+ });
2188
+ current = current.parentElement;
2189
+ }
2190
+ this.themeObserver.set(observer);
2191
+ }
2192
+ syncInheritedTheme(host) {
2193
+ if (typeof getComputedStyle !== 'function') {
2194
+ return;
2195
+ }
2196
+ const pageThemePresetId = host
2197
+ .closest('.pdx-page-wrapper')
2198
+ ?.getAttribute('data-theme-preset')
2199
+ ?.trim();
2200
+ const chartThemePresetId = pageThemePresetId
2201
+ ? BUILTIN_PAGE_THEME_PRESETS[pageThemePresetId]?.chartThemePreset?.trim()
2202
+ : undefined;
2203
+ const preset = chartThemePresetId && this.isThemeVariant(chartThemePresetId)
2204
+ ? this.cloneTheme(PRAXIS_CHART_THEME_VARIANTS[chartThemePresetId])
2205
+ : {};
2206
+ const hostTextColor = getComputedStyle(host).color.trim();
2207
+ const theme = {
2208
+ ...preset,
2209
+ textColor: preset.textColor || hostTextColor || undefined,
2210
+ };
2211
+ const normalizedTheme = Object.values(theme).some((value) => value !== undefined)
2212
+ ? theme
2213
+ : null;
2214
+ const signature = normalizedTheme ? JSON.stringify(normalizedTheme) : null;
2215
+ if (signature === this.inheritedThemeSignature) {
2216
+ return;
2217
+ }
2218
+ this.inheritedThemeSignature = signature;
2219
+ this.inheritedTheme.set(normalizedTheme);
2220
+ }
2221
+ isThemeVariant(value) {
2222
+ return Object.prototype.hasOwnProperty.call(PRAXIS_CHART_THEME_VARIANTS, value);
2223
+ }
2224
+ applyInheritedTheme(config, inherited) {
2225
+ if (!inherited) {
2226
+ return config;
2227
+ }
2228
+ const explicit = config.theme;
2229
+ return {
2230
+ ...config,
2231
+ theme: {
2232
+ palette: explicit?.palette ?? inherited.palette,
2233
+ backgroundColor: explicit?.backgroundColor ?? inherited.backgroundColor,
2234
+ textColor: explicit?.textColor ?? inherited.textColor,
2235
+ borderRadius: explicit?.borderRadius ?? inherited.borderRadius,
2236
+ legend: this.mergeOptionalThemeObject(inherited.legend, explicit?.legend),
2237
+ tooltip: this.mergeOptionalThemeObject(inherited.tooltip, explicit?.tooltip),
2238
+ surface: this.mergeOptionalThemeObject(inherited.surface, explicit?.surface),
2239
+ },
2240
+ };
2241
+ }
2242
+ cloneTheme(theme) {
2243
+ return {
2244
+ ...theme,
2245
+ palette: theme.palette ? [...theme.palette] : undefined,
2246
+ legend: this.mergeOptionalThemeObject(undefined, theme.legend),
2247
+ tooltip: this.mergeOptionalThemeObject(undefined, theme.tooltip),
2248
+ surface: this.mergeOptionalThemeObject(undefined, theme.surface),
2249
+ };
2250
+ }
2251
+ mergeOptionalThemeObject(defaults, explicit) {
2252
+ if (!defaults && !explicit) {
2253
+ return undefined;
2254
+ }
2255
+ return {
2256
+ ...(defaults ?? {}),
2257
+ ...(explicit ?? {}),
2258
+ };
2259
+ }
1943
2260
  shouldFillShellContainer(shell) {
1944
2261
  const rect = shell.getBoundingClientRect();
1945
2262
  const hasControlledHeight = (rect.height || shell.clientHeight || shell.offsetHeight) > 0;
@@ -1953,6 +2270,9 @@ class PraxisChartComponent {
1953
2270
  this.scheduleEngineResize();
1954
2271
  }
1955
2272
  scheduleEngineResize() {
2273
+ if (this.loadState() !== 'ready') {
2274
+ return;
2275
+ }
1956
2276
  if (typeof requestAnimationFrame !== 'function') {
1957
2277
  this.engine.resize();
1958
2278
  return;
@@ -1962,6 +2282,9 @@ class PraxisChartComponent {
1962
2282
  }
1963
2283
  this.resizeFrameId = requestAnimationFrame(() => {
1964
2284
  this.resizeFrameId = null;
2285
+ if (this.loadState() !== 'ready') {
2286
+ return;
2287
+ }
1965
2288
  this.engine.resize();
1966
2289
  });
1967
2290
  }
@@ -2002,7 +2325,7 @@ class PraxisChartComponent {
2002
2325
  this.resizeFrameId = null;
2003
2326
  }
2004
2327
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: PraxisChartComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2005
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.14", type: PraxisChartComponent, isStandalone: true, selector: "praxis-chart", inputs: { config: { classPropertyName: "config", publicName: "config", isSignal: true, isRequired: false, transformFunction: null }, data: { classPropertyName: "data", publicName: "data", isSignal: true, isRequired: false, transformFunction: null }, chartDocument: { classPropertyName: "chartDocument", publicName: "chartDocument", isSignal: true, isRequired: false, transformFunction: null }, filterCriteria: { classPropertyName: "filterCriteria", publicName: "filterCriteria", isSignal: true, isRequired: false, transformFunction: null }, queryContext: { classPropertyName: "queryContext", publicName: "queryContext", isSignal: true, isRequired: false, transformFunction: null }, remoteDataResolver: { classPropertyName: "remoteDataResolver", publicName: "remoteDataResolver", isSignal: true, isRequired: false, transformFunction: null }, enableCustomization: { classPropertyName: "enableCustomization", publicName: "enableCustomization", isSignal: true, isRequired: false, transformFunction: null }, availableResources: { classPropertyName: "availableResources", publicName: "availableResources", isSignal: true, isRequired: false, transformFunction: null }, availableFields: { classPropertyName: "availableFields", publicName: "availableFields", isSignal: true, isRequired: false, transformFunction: null }, availableTargets: { classPropertyName: "availableTargets", publicName: "availableTargets", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { pointClick: "pointClick", selectionChange: "selectionChange", crossFilter: "crossFilter", queryRequest: "queryRequest", loadStateChange: "loadStateChange", chartDocumentApplied: "chartDocumentApplied", chartDocumentSaved: "chartDocumentSaved" }, host: { properties: { "class.praxis-chart-host-fill-container": "isFillContainerMode()" } }, viewQueries: [{ propertyName: "chartHost", first: true, predicate: ["chartHost"], descendants: true, isSignal: true }], ngImport: i0, template: `
2328
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.14", type: PraxisChartComponent, isStandalone: true, selector: "praxis-chart", inputs: { config: { classPropertyName: "config", publicName: "config", isSignal: true, isRequired: false, transformFunction: null }, data: { classPropertyName: "data", publicName: "data", isSignal: true, isRequired: false, transformFunction: null }, chartDocument: { classPropertyName: "chartDocument", publicName: "chartDocument", isSignal: true, isRequired: false, transformFunction: null }, filterCriteria: { classPropertyName: "filterCriteria", publicName: "filterCriteria", isSignal: true, isRequired: false, transformFunction: null }, queryContext: { classPropertyName: "queryContext", publicName: "queryContext", isSignal: true, isRequired: false, transformFunction: null }, remoteDataResolver: { classPropertyName: "remoteDataResolver", publicName: "remoteDataResolver", isSignal: true, isRequired: false, transformFunction: null }, enableCustomization: { classPropertyName: "enableCustomization", publicName: "enableCustomization", isSignal: true, isRequired: false, transformFunction: null }, availableResources: { classPropertyName: "availableResources", publicName: "availableResources", isSignal: true, isRequired: false, transformFunction: null }, availableFields: { classPropertyName: "availableFields", publicName: "availableFields", isSignal: true, isRequired: false, transformFunction: null }, availableTargets: { classPropertyName: "availableTargets", publicName: "availableTargets", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { pointClick: "pointClick", pointAction: "pointAction", selectionChange: "selectionChange", drillDown: "drillDown", crossFilter: "crossFilter", queryRequest: "queryRequest", loadStateChange: "loadStateChange", chartDocumentApplied: "chartDocumentApplied", chartDocumentSaved: "chartDocumentSaved" }, host: { properties: { "class.praxis-chart-host-fill-container": "isFillContainerMode()" } }, viewQueries: [{ propertyName: "chartHost", first: true, predicate: ["chartHost"], descendants: true, isSignal: true }], ngImport: i0, template: `
2006
2329
  <section
2007
2330
  class="praxis-chart-shell"
2008
2331
  [class.praxis-chart-shell-fill-container]="isFillContainerMode()"
@@ -2076,7 +2399,7 @@ class PraxisChartComponent {
2076
2399
  <div #chartHost class="praxis-chart-host"></div>
2077
2400
  }
2078
2401
  </section>
2079
- `, isInline: true, styles: [":host{display:block;min-height:var(--praxis-chart-runtime-height, 320px);min-width:0}:host(.praxis-chart-host-fill-container){height:100%;min-height:0}:host-context(.pdx-shell.no-shell) .praxis-chart-shell,:host-context(.pdx-shell.body-fill) .praxis-chart-shell,:host-context(.pdx-shell.expanded) .praxis-chart-shell,:host-context(.pdx-shell.fullscreen) .praxis-chart-shell{height:100%!important}.praxis-chart-shell{position:relative;width:100%;height:100%;min-height:240px;border-radius:var(--praxis-chart-config-surface-radius, var(--praxis-chart-surface-radius, 8px));overflow:hidden;background:var( --praxis-chart-config-surface-bg, var(--praxis-chart-surface-bg, var(--md-sys-color-surface-container-lowest, #fff)) );border-color:var( --praxis-chart-config-surface-border, var( --praxis-chart-surface-border, color-mix(in srgb, var(--md-sys-color-outline, #c5c7ce) 44%, transparent) ) );border-style:solid;border-width:var(--praxis-chart-config-surface-border-width, 1px)}.praxis-chart-shell-fill-container{min-height:0}:host-context(.pdx-shell) .praxis-chart-shell:not(.praxis-chart-shell-contained),.praxis-chart-shell-embedded{border-width:var( --praxis-chart-config-surface-border-width, var(--praxis-chart-embedded-surface-border-width, 0) );border-radius:var( --praxis-chart-config-surface-radius, var(--praxis-chart-embedded-surface-radius, 0) );background:var( --praxis-chart-config-surface-bg, var(--praxis-chart-embedded-surface-bg, transparent) )}.praxis-chart-settings-trigger{position:absolute;top:10px;right:10px;z-index:3;background:color-mix(in srgb,var(--md-sys-color-surface, #fff) 88%,rgba(18,99,180,.12));-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px)}.praxis-chart-host{width:100%;height:100%;min-height:inherit}.praxis-chart-state{height:100%;min-height:min(240px,100%);display:grid;align-content:center;justify-items:center;gap:14px;padding:24px;text-align:center}.praxis-chart-state-copy{display:grid;gap:6px;justify-items:center}.praxis-chart-state-title{font-size:1rem;font-weight:600;color:var(--md-sys-color-on-surface, #1a1b20)}.praxis-chart-state-description{font-size:.925rem;color:var(--md-sys-color-on-surface-variant, #5a5d67);max-width:36rem}.praxis-chart-loading-hero{width:min(100%,320px);display:grid;gap:14px}.praxis-chart-loading-summary{display:flex;gap:8px;justify-content:center}.praxis-chart-loading-chip,.praxis-chart-loading-bar{border-radius:999px;background:linear-gradient(90deg,#1263b414,#1263b438,#1263b414);background-size:200% 100%;animation:praxis-chart-loading-wave 1.2s ease-in-out infinite}.praxis-chart-loading-chip{display:block;width:104px;height:12px}.praxis-chart-loading-chip--short{width:64px}.praxis-chart-loading-plot{display:grid;grid-template-columns:repeat(5,minmax(0,1fr));align-items:end;gap:10px;height:108px;padding:12px 8px 4px;border-radius:8px;background:color-mix(in srgb,var(--md-sys-color-surface-container, #eef3f8) 72%,transparent);border:1px solid rgba(18,99,180,.08)}.praxis-chart-loading-bar{display:block;width:100%}.praxis-chart-loading-bar--1{height:32%}.praxis-chart-loading-bar--2{height:68%}.praxis-chart-loading-bar--3{height:48%}.praxis-chart-loading-bar--4{height:78%}.praxis-chart-loading-bar--5{height:56%}@keyframes praxis-chart-loading-wave{0%{background-position:100% 0}to{background-position:-100% 0}}\n"], dependencies: [{ kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i1$2.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i2.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i3.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
2402
+ `, isInline: true, styles: [":host{display:block;min-height:var(--praxis-chart-runtime-height, 320px);min-width:0;color:var(--md-sys-color-on-surface, #1a1b20)}:host(.praxis-chart-host-fill-container){height:100%;min-height:0}:host-context(.pdx-shell.no-shell) .praxis-chart-shell,:host-context(.pdx-shell.body-fill) .praxis-chart-shell,:host-context(.pdx-shell.expanded) .praxis-chart-shell,:host-context(.pdx-shell.fullscreen) .praxis-chart-shell{height:100%!important}.praxis-chart-shell{position:relative;width:100%;height:100%;min-height:240px;border-radius:var(--praxis-chart-config-surface-radius, var(--praxis-chart-surface-radius, 8px));overflow:hidden;background:var( --praxis-chart-config-surface-bg, var(--praxis-chart-surface-bg, var(--md-sys-color-surface-container-lowest, #fff)) );border-color:var( --praxis-chart-config-surface-border, var( --praxis-chart-surface-border, color-mix(in srgb, var(--md-sys-color-outline, #c5c7ce) 44%, transparent) ) );border-style:solid;border-width:var(--praxis-chart-config-surface-border-width, 1px)}.praxis-chart-shell-fill-container{min-height:0}:host-context(.pdx-shell) .praxis-chart-shell:not(.praxis-chart-shell-contained),.praxis-chart-shell-embedded{border-width:var( --praxis-chart-config-surface-border-width, var(--praxis-chart-embedded-surface-border-width, 0) );border-radius:var( --praxis-chart-config-surface-radius, var(--praxis-chart-embedded-surface-radius, 0) );background:var( --praxis-chart-config-surface-bg, var(--praxis-chart-embedded-surface-bg, transparent) )}.praxis-chart-settings-trigger{position:absolute;top:10px;right:10px;z-index:3;background:color-mix(in srgb,var(--md-sys-color-surface, #fff) 88%,rgba(18,99,180,.12));-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px)}.praxis-chart-host{width:100%;height:100%;min-height:inherit}.praxis-chart-state{height:100%;min-height:min(240px,100%);display:grid;align-content:center;justify-items:center;gap:14px;padding:24px;text-align:center}.praxis-chart-state-copy{display:grid;gap:6px;justify-items:center}.praxis-chart-state-title{font-size:1rem;font-weight:600;color:var(--md-sys-color-on-surface, #1a1b20)}.praxis-chart-state-description{font-size:.925rem;color:var(--md-sys-color-on-surface-variant, #5a5d67);max-width:36rem}.praxis-chart-loading-hero{width:min(100%,320px);display:grid;gap:14px}.praxis-chart-loading-summary{display:flex;gap:8px;justify-content:center}.praxis-chart-loading-chip,.praxis-chart-loading-bar{border-radius:999px;background:linear-gradient(90deg,#1263b414,#1263b438,#1263b414);background-size:200% 100%;animation:praxis-chart-loading-wave 1.2s ease-in-out infinite}.praxis-chart-loading-chip{display:block;width:104px;height:12px}.praxis-chart-loading-chip--short{width:64px}.praxis-chart-loading-plot{display:grid;grid-template-columns:repeat(5,minmax(0,1fr));align-items:end;gap:10px;height:108px;padding:12px 8px 4px;border-radius:8px;background:color-mix(in srgb,var(--md-sys-color-surface-container, #eef3f8) 72%,transparent);border:1px solid rgba(18,99,180,.08)}.praxis-chart-loading-bar{display:block;width:100%}.praxis-chart-loading-bar--1{height:32%}.praxis-chart-loading-bar--2{height:68%}.praxis-chart-loading-bar--3{height:48%}.praxis-chart-loading-bar--4{height:78%}.praxis-chart-loading-bar--5{height:56%}@keyframes praxis-chart-loading-wave{0%{background-position:100% 0}to{background-position:-100% 0}}\n"], dependencies: [{ kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i1$2.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i2.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i3.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
2080
2403
  }
2081
2404
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: PraxisChartComponent, decorators: [{
2082
2405
  type: Component,
@@ -2156,65 +2479,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
2156
2479
  <div #chartHost class="praxis-chart-host"></div>
2157
2480
  }
2158
2481
  </section>
2159
- `, styles: [":host{display:block;min-height:var(--praxis-chart-runtime-height, 320px);min-width:0}:host(.praxis-chart-host-fill-container){height:100%;min-height:0}:host-context(.pdx-shell.no-shell) .praxis-chart-shell,:host-context(.pdx-shell.body-fill) .praxis-chart-shell,:host-context(.pdx-shell.expanded) .praxis-chart-shell,:host-context(.pdx-shell.fullscreen) .praxis-chart-shell{height:100%!important}.praxis-chart-shell{position:relative;width:100%;height:100%;min-height:240px;border-radius:var(--praxis-chart-config-surface-radius, var(--praxis-chart-surface-radius, 8px));overflow:hidden;background:var( --praxis-chart-config-surface-bg, var(--praxis-chart-surface-bg, var(--md-sys-color-surface-container-lowest, #fff)) );border-color:var( --praxis-chart-config-surface-border, var( --praxis-chart-surface-border, color-mix(in srgb, var(--md-sys-color-outline, #c5c7ce) 44%, transparent) ) );border-style:solid;border-width:var(--praxis-chart-config-surface-border-width, 1px)}.praxis-chart-shell-fill-container{min-height:0}:host-context(.pdx-shell) .praxis-chart-shell:not(.praxis-chart-shell-contained),.praxis-chart-shell-embedded{border-width:var( --praxis-chart-config-surface-border-width, var(--praxis-chart-embedded-surface-border-width, 0) );border-radius:var( --praxis-chart-config-surface-radius, var(--praxis-chart-embedded-surface-radius, 0) );background:var( --praxis-chart-config-surface-bg, var(--praxis-chart-embedded-surface-bg, transparent) )}.praxis-chart-settings-trigger{position:absolute;top:10px;right:10px;z-index:3;background:color-mix(in srgb,var(--md-sys-color-surface, #fff) 88%,rgba(18,99,180,.12));-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px)}.praxis-chart-host{width:100%;height:100%;min-height:inherit}.praxis-chart-state{height:100%;min-height:min(240px,100%);display:grid;align-content:center;justify-items:center;gap:14px;padding:24px;text-align:center}.praxis-chart-state-copy{display:grid;gap:6px;justify-items:center}.praxis-chart-state-title{font-size:1rem;font-weight:600;color:var(--md-sys-color-on-surface, #1a1b20)}.praxis-chart-state-description{font-size:.925rem;color:var(--md-sys-color-on-surface-variant, #5a5d67);max-width:36rem}.praxis-chart-loading-hero{width:min(100%,320px);display:grid;gap:14px}.praxis-chart-loading-summary{display:flex;gap:8px;justify-content:center}.praxis-chart-loading-chip,.praxis-chart-loading-bar{border-radius:999px;background:linear-gradient(90deg,#1263b414,#1263b438,#1263b414);background-size:200% 100%;animation:praxis-chart-loading-wave 1.2s ease-in-out infinite}.praxis-chart-loading-chip{display:block;width:104px;height:12px}.praxis-chart-loading-chip--short{width:64px}.praxis-chart-loading-plot{display:grid;grid-template-columns:repeat(5,minmax(0,1fr));align-items:end;gap:10px;height:108px;padding:12px 8px 4px;border-radius:8px;background:color-mix(in srgb,var(--md-sys-color-surface-container, #eef3f8) 72%,transparent);border:1px solid rgba(18,99,180,.08)}.praxis-chart-loading-bar{display:block;width:100%}.praxis-chart-loading-bar--1{height:32%}.praxis-chart-loading-bar--2{height:68%}.praxis-chart-loading-bar--3{height:48%}.praxis-chart-loading-bar--4{height:78%}.praxis-chart-loading-bar--5{height:56%}@keyframes praxis-chart-loading-wave{0%{background-position:100% 0}to{background-position:-100% 0}}\n"] }]
2160
- }], ctorParameters: () => [], propDecorators: { config: [{ type: i0.Input, args: [{ isSignal: true, alias: "config", required: false }] }], data: [{ type: i0.Input, args: [{ isSignal: true, alias: "data", required: false }] }], chartDocument: [{ type: i0.Input, args: [{ isSignal: true, alias: "chartDocument", required: false }] }], filterCriteria: [{ type: i0.Input, args: [{ isSignal: true, alias: "filterCriteria", required: false }] }], queryContext: [{ type: i0.Input, args: [{ isSignal: true, alias: "queryContext", required: false }] }], remoteDataResolver: [{ type: i0.Input, args: [{ isSignal: true, alias: "remoteDataResolver", required: false }] }], enableCustomization: [{ type: i0.Input, args: [{ isSignal: true, alias: "enableCustomization", required: false }] }], availableResources: [{ type: i0.Input, args: [{ isSignal: true, alias: "availableResources", required: false }] }], availableFields: [{ type: i0.Input, args: [{ isSignal: true, alias: "availableFields", required: false }] }], availableTargets: [{ type: i0.Input, args: [{ isSignal: true, alias: "availableTargets", required: false }] }], pointClick: [{ type: i0.Output, args: ["pointClick"] }], selectionChange: [{ type: i0.Output, args: ["selectionChange"] }], crossFilter: [{ type: i0.Output, args: ["crossFilter"] }], queryRequest: [{ type: i0.Output, args: ["queryRequest"] }], loadStateChange: [{ type: i0.Output, args: ["loadStateChange"] }], chartDocumentApplied: [{ type: i0.Output, args: ["chartDocumentApplied"] }], chartDocumentSaved: [{ type: i0.Output, args: ["chartDocumentSaved"] }], chartHost: [{ type: i0.ViewChild, args: ['chartHost', { isSignal: true }] }] } });
2161
- function normalizeFilterCriteria(criteria) {
2162
- if (!criteria)
2163
- return null;
2164
- const next = Object.entries(criteria).reduce((acc, [key, value]) => {
2165
- if (value === null || value === undefined)
2166
- return acc;
2167
- if (Array.isArray(value) && value.length === 0)
2168
- return acc;
2169
- if (typeof value === 'string' && value.trim() === '')
2170
- return acc;
2171
- acc[key] = value;
2172
- return acc;
2173
- }, {});
2174
- return Object.keys(next).length ? next : null;
2175
- }
2176
- function normalizePraxisDataQueryContextBridge(context) {
2177
- if (!context || typeof context !== 'object')
2178
- return null;
2179
- const filters = normalizeFilterCriteria(context.filters || null);
2180
- const meta = normalizeFilterCriteria(context.meta || null);
2181
- const sort = Array.isArray(context.sort)
2182
- ? context.sort
2183
- .map((item) => (typeof item === 'string' ? item.trim() : ''))
2184
- .filter((item) => item.length > 0)
2185
- : [];
2186
- const limit = typeof context.limit === 'number' && Number.isFinite(context.limit)
2187
- ? Math.max(0, Math.trunc(context.limit))
2188
- : null;
2189
- const page = context.page && typeof context.page === 'object'
2190
- ? {
2191
- index: typeof context.page.index === 'number' && Number.isFinite(context.page.index)
2192
- ? Math.max(0, Math.trunc(context.page.index))
2193
- : null,
2194
- size: typeof context.page.size === 'number' && Number.isFinite(context.page.size)
2195
- ? Math.max(0, Math.trunc(context.page.size))
2196
- : null,
2197
- }
2198
- : null;
2199
- const next = {};
2200
- if (filters)
2201
- next.filters = filters;
2202
- if (sort.length)
2203
- next.sort = sort;
2204
- if (limit !== null)
2205
- next.limit = limit;
2206
- if (page && (page.index !== null || page.size !== null))
2207
- next.page = page;
2208
- if (meta)
2209
- next.meta = meta;
2210
- return Object.keys(next).length ? next : null;
2211
- }
2212
- function resolvePraxisFilterCriteriaBridge(filterCriteria, queryContext) {
2213
- return {
2214
- ...(normalizeFilterCriteria(filterCriteria) || {}),
2215
- ...(normalizePraxisDataQueryContextBridge(queryContext)?.filters || {}),
2216
- };
2217
- }
2482
+ `, styles: [":host{display:block;min-height:var(--praxis-chart-runtime-height, 320px);min-width:0;color:var(--md-sys-color-on-surface, #1a1b20)}:host(.praxis-chart-host-fill-container){height:100%;min-height:0}:host-context(.pdx-shell.no-shell) .praxis-chart-shell,:host-context(.pdx-shell.body-fill) .praxis-chart-shell,:host-context(.pdx-shell.expanded) .praxis-chart-shell,:host-context(.pdx-shell.fullscreen) .praxis-chart-shell{height:100%!important}.praxis-chart-shell{position:relative;width:100%;height:100%;min-height:240px;border-radius:var(--praxis-chart-config-surface-radius, var(--praxis-chart-surface-radius, 8px));overflow:hidden;background:var( --praxis-chart-config-surface-bg, var(--praxis-chart-surface-bg, var(--md-sys-color-surface-container-lowest, #fff)) );border-color:var( --praxis-chart-config-surface-border, var( --praxis-chart-surface-border, color-mix(in srgb, var(--md-sys-color-outline, #c5c7ce) 44%, transparent) ) );border-style:solid;border-width:var(--praxis-chart-config-surface-border-width, 1px)}.praxis-chart-shell-fill-container{min-height:0}:host-context(.pdx-shell) .praxis-chart-shell:not(.praxis-chart-shell-contained),.praxis-chart-shell-embedded{border-width:var( --praxis-chart-config-surface-border-width, var(--praxis-chart-embedded-surface-border-width, 0) );border-radius:var( --praxis-chart-config-surface-radius, var(--praxis-chart-embedded-surface-radius, 0) );background:var( --praxis-chart-config-surface-bg, var(--praxis-chart-embedded-surface-bg, transparent) )}.praxis-chart-settings-trigger{position:absolute;top:10px;right:10px;z-index:3;background:color-mix(in srgb,var(--md-sys-color-surface, #fff) 88%,rgba(18,99,180,.12));-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px)}.praxis-chart-host{width:100%;height:100%;min-height:inherit}.praxis-chart-state{height:100%;min-height:min(240px,100%);display:grid;align-content:center;justify-items:center;gap:14px;padding:24px;text-align:center}.praxis-chart-state-copy{display:grid;gap:6px;justify-items:center}.praxis-chart-state-title{font-size:1rem;font-weight:600;color:var(--md-sys-color-on-surface, #1a1b20)}.praxis-chart-state-description{font-size:.925rem;color:var(--md-sys-color-on-surface-variant, #5a5d67);max-width:36rem}.praxis-chart-loading-hero{width:min(100%,320px);display:grid;gap:14px}.praxis-chart-loading-summary{display:flex;gap:8px;justify-content:center}.praxis-chart-loading-chip,.praxis-chart-loading-bar{border-radius:999px;background:linear-gradient(90deg,#1263b414,#1263b438,#1263b414);background-size:200% 100%;animation:praxis-chart-loading-wave 1.2s ease-in-out infinite}.praxis-chart-loading-chip{display:block;width:104px;height:12px}.praxis-chart-loading-chip--short{width:64px}.praxis-chart-loading-plot{display:grid;grid-template-columns:repeat(5,minmax(0,1fr));align-items:end;gap:10px;height:108px;padding:12px 8px 4px;border-radius:8px;background:color-mix(in srgb,var(--md-sys-color-surface-container, #eef3f8) 72%,transparent);border:1px solid rgba(18,99,180,.08)}.praxis-chart-loading-bar{display:block;width:100%}.praxis-chart-loading-bar--1{height:32%}.praxis-chart-loading-bar--2{height:68%}.praxis-chart-loading-bar--3{height:48%}.praxis-chart-loading-bar--4{height:78%}.praxis-chart-loading-bar--5{height:56%}@keyframes praxis-chart-loading-wave{0%{background-position:100% 0}to{background-position:-100% 0}}\n"] }]
2483
+ }], ctorParameters: () => [], propDecorators: { config: [{ type: i0.Input, args: [{ isSignal: true, alias: "config", required: false }] }], data: [{ type: i0.Input, args: [{ isSignal: true, alias: "data", required: false }] }], chartDocument: [{ type: i0.Input, args: [{ isSignal: true, alias: "chartDocument", required: false }] }], filterCriteria: [{ type: i0.Input, args: [{ isSignal: true, alias: "filterCriteria", required: false }] }], queryContext: [{ type: i0.Input, args: [{ isSignal: true, alias: "queryContext", required: false }] }], remoteDataResolver: [{ type: i0.Input, args: [{ isSignal: true, alias: "remoteDataResolver", required: false }] }], enableCustomization: [{ type: i0.Input, args: [{ isSignal: true, alias: "enableCustomization", required: false }] }], availableResources: [{ type: i0.Input, args: [{ isSignal: true, alias: "availableResources", required: false }] }], availableFields: [{ type: i0.Input, args: [{ isSignal: true, alias: "availableFields", required: false }] }], availableTargets: [{ type: i0.Input, args: [{ isSignal: true, alias: "availableTargets", required: false }] }], pointClick: [{ type: i0.Output, args: ["pointClick"] }], pointAction: [{ type: i0.Output, args: ["pointAction"] }], selectionChange: [{ type: i0.Output, args: ["selectionChange"] }], drillDown: [{ type: i0.Output, args: ["drillDown"] }], crossFilter: [{ type: i0.Output, args: ["crossFilter"] }], queryRequest: [{ type: i0.Output, args: ["queryRequest"] }], loadStateChange: [{ type: i0.Output, args: ["loadStateChange"] }], chartDocumentApplied: [{ type: i0.Output, args: ["chartDocumentApplied"] }], chartDocumentSaved: [{ type: i0.Output, args: ["chartDocumentSaved"] }], chartHost: [{ type: i0.ViewChild, args: ['chartHost', { isSignal: true }] }] } });
2218
2484
  function mergeRemoteQueryContext(config, queryContext, runtimeFilters) {
2219
2485
  const dataSource = config.dataSource;
2220
2486
  if (dataSource?.kind !== 'remote' || !dataSource.query)
@@ -2247,6 +2513,17 @@ function mergeRemoteQueryContext(config, queryContext, runtimeFilters) {
2247
2513
  },
2248
2514
  };
2249
2515
  }
2516
+ function resolveUnsupportedStatsFilterExpressionDiagnostic(queryContext) {
2517
+ if (!queryContext?.filterExpression) {
2518
+ return null;
2519
+ }
2520
+ const governance = queryContext.filterExpression.governance;
2521
+ const decision = governance?.decisionId ? ` Decision: ${governance.decisionId}.` : '';
2522
+ const reason = queryContext.filterExpression.projection?.reason
2523
+ ? ` Projection reason: ${queryContext.filterExpression.projection.reason}.`
2524
+ : '';
2525
+ return `Praxis Charts received a governed queryContext.filterExpression, but praxis.stats execution currently accepts only flat statsRequest.filter criteria. The expression was rejected instead of being flattened into an unsafe AND filter.${decision}${reason}`;
2526
+ }
2250
2527
  function normalizeRemoteDataResolverResult$1(result) {
2251
2528
  if (Array.isArray(result)) {
2252
2529
  return of(result);
@@ -3248,7 +3525,7 @@ class PraxisChartDrilldownPanelComponent {
3248
3525
  [data]="detailData()"
3249
3526
  ></praxis-chart>
3250
3527
  </section>
3251
- `, isInline: true, styles: [":host{display:block}.drilldown-shell{display:grid;gap:14px}.drilldown-header h3,.drilldown-eyebrow,.drilldown-description{margin:0}.drilldown-eyebrow{font-size:.75rem;letter-spacing:.14em;text-transform:uppercase;color:var(--md-sys-color-on-surface-variant, #5a5d67)}.drilldown-header h3{font-size:1.2rem;color:var(--md-sys-color-on-surface, #1a1b20)}.drilldown-description{color:var(--md-sys-color-on-surface-variant, #5a5d67)}\n"], dependencies: [{ kind: "component", type: PraxisChartComponent, selector: "praxis-chart", inputs: ["config", "data", "chartDocument", "filterCriteria", "queryContext", "remoteDataResolver", "enableCustomization", "availableResources", "availableFields", "availableTargets"], outputs: ["pointClick", "selectionChange", "crossFilter", "queryRequest", "loadStateChange", "chartDocumentApplied", "chartDocumentSaved"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
3528
+ `, isInline: true, styles: [":host{display:block}.drilldown-shell{display:grid;gap:14px}.drilldown-header h3,.drilldown-eyebrow,.drilldown-description{margin:0}.drilldown-eyebrow{font-size:.75rem;letter-spacing:.14em;text-transform:uppercase;color:var(--md-sys-color-on-surface-variant, #5a5d67)}.drilldown-header h3{font-size:1.2rem;color:var(--md-sys-color-on-surface, #1a1b20)}.drilldown-description{color:var(--md-sys-color-on-surface-variant, #5a5d67)}\n"], dependencies: [{ kind: "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 });
3252
3529
  }
3253
3530
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: PraxisChartDrilldownPanelComponent, decorators: [{
3254
3531
  type: Component,
@@ -3318,7 +3595,7 @@ const CARTESIAN_GRID_TOP_WITHOUT_TITLE = 48;
3318
3595
  const CARTESIAN_GRID_BOTTOM_WITH_LEGEND = 64;
3319
3596
  const CARTESIAN_GRID_BOTTOM_WITHOUT_LEGEND = 40;
3320
3597
  const DEFAULT_VALUE_LOCALE = 'pt-BR';
3321
- class PraxisChartOptionBuilderService {
3598
+ class EChartsOptionBuilderService {
3322
3599
  transformer;
3323
3600
  constructor(transformer) {
3324
3601
  this.transformer = transformer;
@@ -3431,19 +3708,14 @@ class PraxisChartOptionBuilderService {
3431
3708
  };
3432
3709
  }
3433
3710
  const horizontal = config.orientation === 'horizontal' || config.type === 'horizontal-bar';
3434
- const xAxisType = config.axes?.x?.type ?? 'category';
3711
+ const xAxisType = this.usesCanonicalStatsTime(config)
3712
+ ? 'time'
3713
+ : config.axes?.x?.type ?? 'category';
3435
3714
  return {
3436
3715
  backgroundColor: config.theme?.backgroundColor,
3437
3716
  color: palette,
3438
3717
  title: this.buildTitle(config, 'left', textColor),
3439
- tooltip: tooltipEnabled
3440
- ? {
3441
- trigger: config.theme?.tooltip?.trigger ?? 'axis',
3442
- confine: true,
3443
- appendToBody: false,
3444
- valueFormatter: (value) => this.formatValue(value, this.primaryValueFormat(config)),
3445
- }
3446
- : undefined,
3718
+ tooltip: tooltipEnabled ? this.buildCartesianTooltip(config) : undefined,
3447
3719
  legend: this.buildLegend(config, legendVisible, 'bottom', textColor),
3448
3720
  grid: this.buildGrid(config, { horizontal, legendVisible }),
3449
3721
  xAxis: horizontal
@@ -3663,6 +3935,87 @@ class PraxisChartOptionBuilderService {
3663
3935
  ?? config.axes?.y?.labels?.format
3664
3936
  ?? config.series[0]?.labels?.format;
3665
3937
  }
3938
+ buildCartesianTooltip(config) {
3939
+ const tooltip = {
3940
+ trigger: config.theme?.tooltip?.trigger ?? 'axis',
3941
+ confine: true,
3942
+ appendToBody: false,
3943
+ valueFormatter: (value) => this.formatValue(value, this.primaryValueFormat(config)),
3944
+ };
3945
+ return this.usesCanonicalStatsTime(config)
3946
+ ? {
3947
+ ...tooltip,
3948
+ formatter: (params) => this.formatCanonicalTimeTooltip(params, config),
3949
+ }
3950
+ : tooltip;
3951
+ }
3952
+ usesCanonicalStatsTime(config) {
3953
+ if (config.dataSource?.kind !== 'remote'
3954
+ || config.dataSource.query?.sourceKind !== 'praxis.stats') {
3955
+ return false;
3956
+ }
3957
+ return config.axes?.x?.type === 'time'
3958
+ || config.dataSource.query.statsOperation === 'timeseries';
3959
+ }
3960
+ formatCanonicalTimeTooltip(params, config) {
3961
+ const items = (Array.isArray(params) ? params : [params])
3962
+ .filter((item) => !!item && typeof item === 'object');
3963
+ if (!items.length) {
3964
+ return '';
3965
+ }
3966
+ const first = items[0];
3967
+ const data = this.tooltipPointData(first['data']);
3968
+ const header = this.formatCanonicalTimeTooltipHeader(first, data, config);
3969
+ const lines = items.map((item) => {
3970
+ const itemData = this.tooltipPointData(item['data']);
3971
+ const seriesId = item['seriesId'] === null || item['seriesId'] === undefined
3972
+ ? undefined
3973
+ : String(item['seriesId']);
3974
+ const format = seriesId
3975
+ ? this.seriesLabelFormat(config, seriesId) ?? this.primaryValueFormat(config)
3976
+ : this.primaryValueFormat(config);
3977
+ const value = this.formatValue(item['value'] ?? itemData['value'], format);
3978
+ const seriesName = item['seriesName'] === null || item['seriesName'] === undefined
3979
+ ? ''
3980
+ : String(item['seriesName']);
3981
+ return seriesName
3982
+ ? `${escapeTooltipHtml(seriesName)}: ${escapeTooltipHtml(value)}`
3983
+ : escapeTooltipHtml(value);
3984
+ });
3985
+ return [escapeTooltipHtml(header), ...lines]
3986
+ .filter((part) => part.length > 0)
3987
+ .join('<br/>');
3988
+ }
3989
+ formatCanonicalTimeTooltipHeader(params, data, config) {
3990
+ const dateFormat = config.axes?.x?.labels?.format;
3991
+ const label = this.nonEmptyText(data['label']);
3992
+ const start = this.nonEmptyText(data['start']);
3993
+ const end = this.nonEmptyText(data['end']);
3994
+ const formattedStart = start ? this.formatValue(start, dateFormat) : '';
3995
+ const formattedEnd = end ? this.formatValue(end, dateFormat) : '';
3996
+ const interval = formattedStart && formattedEnd && formattedStart !== formattedEnd
3997
+ ? `${formattedStart} – ${formattedEnd}`
3998
+ : formattedStart || formattedEnd;
3999
+ const fallback = this.nonEmptyText(params['axisValueLabel'])
4000
+ ?? this.nonEmptyText(params['name'])
4001
+ ?? '';
4002
+ if (label && interval && label !== formattedStart && label !== interval) {
4003
+ return `${label} · ${interval}`;
4004
+ }
4005
+ return interval || label || fallback;
4006
+ }
4007
+ tooltipPointData(value) {
4008
+ return value && typeof value === 'object' && !Array.isArray(value)
4009
+ ? value
4010
+ : {};
4011
+ }
4012
+ nonEmptyText(value) {
4013
+ if (value === null || value === undefined) {
4014
+ return undefined;
4015
+ }
4016
+ const text = String(value).trim();
4017
+ return text || undefined;
4018
+ }
3666
4019
  seriesLabelFormat(config, seriesId) {
3667
4020
  return config.series.find((series) => series.id === seriesId)?.labels?.format;
3668
4021
  }
@@ -3811,10 +4164,10 @@ class PraxisChartOptionBuilderService {
3811
4164
  }
3812
4165
  return Math.min(Math.max(value, 0), 20);
3813
4166
  }
3814
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: PraxisChartOptionBuilderService, deps: [{ token: PraxisChartDataTransformerService }], target: i0.ɵɵFactoryTarget.Injectable });
3815
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: PraxisChartOptionBuilderService, providedIn: 'root' });
4167
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: EChartsOptionBuilderService, deps: [{ token: PraxisChartDataTransformerService }], target: i0.ɵɵFactoryTarget.Injectable });
4168
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: EChartsOptionBuilderService, providedIn: 'root' });
3816
4169
  }
3817
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: PraxisChartOptionBuilderService, decorators: [{
4170
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: EChartsOptionBuilderService, decorators: [{
3818
4171
  type: Injectable,
3819
4172
  args: [{ providedIn: 'root' }]
3820
4173
  }], ctorParameters: () => [{ type: PraxisChartDataTransformerService }] });
@@ -3825,6 +4178,14 @@ function truncateLegendText(name) {
3825
4178
  }
3826
4179
  return `${value.slice(0, 25)}...`;
3827
4180
  }
4181
+ function escapeTooltipHtml(value) {
4182
+ return String(value ?? '')
4183
+ .replace(/&/g, '&amp;')
4184
+ .replace(/</g, '&lt;')
4185
+ .replace(/>/g, '&gt;')
4186
+ .replace(/"/g, '&quot;')
4187
+ .replace(/'/g, '&#39;');
4188
+ }
3828
4189
 
3829
4190
  use([
3830
4191
  AriaComponent,
@@ -3843,38 +4204,42 @@ use([
3843
4204
  class EChartsEngineAdapter {
3844
4205
  optionBuilder;
3845
4206
  chart;
4207
+ currentHost;
3846
4208
  clickHost;
3847
4209
  domClickHandler;
3848
4210
  zrenderClickHandler;
3849
4211
  lastSeriesClickAt = 0;
4212
+ lastGridClick;
3850
4213
  constructor(optionBuilder) {
3851
4214
  this.optionBuilder = optionBuilder;
3852
4215
  }
3853
4216
  render(host, payload) {
4217
+ if (this.chart && this.currentHost !== host) {
4218
+ this.disposeChart();
4219
+ }
3854
4220
  if (!this.chart) {
3855
- this.chart = init(host);
4221
+ this.chart = this.createChart(host);
4222
+ this.currentHost = host;
3856
4223
  }
3857
4224
  const option = this.optionBuilder.build(payload.config, payload.data);
3858
- this.chart?.setOption(option, true);
3859
- this.chart?.off('click');
3860
- this.chart?.on('click', (params) => {
4225
+ const chart = this.chart;
4226
+ chart.setOption(option, true);
4227
+ this.detachHandlers();
4228
+ chart.off('click');
4229
+ chart.on('click', (params) => {
3861
4230
  this.lastSeriesClickAt = Date.now();
4231
+ this.lastGridClick = undefined;
4232
+ const data = pointData(params?.name, params?.data ?? params?.value);
3862
4233
  payload.onPointClick?.({
3863
4234
  chartId: payload.config.id,
3864
4235
  seriesId: params?.seriesId,
3865
4236
  seriesName: params?.seriesName,
3866
- category: params?.name,
4237
+ category: this.resolvePointCategory(payload, data, params?.name),
3867
4238
  value: pointValue(params?.value ?? params?.data),
3868
- data: pointData(params?.name, params?.data ?? params?.value),
4239
+ data,
3869
4240
  });
3870
4241
  });
3871
- const zrender = this.chart?.getZr?.();
3872
- if (zrender && this.zrenderClickHandler) {
3873
- zrender.off?.('click', this.zrenderClickHandler);
3874
- }
3875
- if (this.clickHost && this.domClickHandler) {
3876
- this.clickHost.removeEventListener('click', this.domClickHandler, true);
3877
- }
4242
+ const zrender = chart?.getZr?.();
3878
4243
  this.zrenderClickHandler = (event) => {
3879
4244
  window.setTimeout(() => {
3880
4245
  if (Date.now() - this.lastSeriesClickAt < 80) {
@@ -3905,6 +4270,21 @@ class EChartsEngineAdapter {
3905
4270
  this.chart?.resize();
3906
4271
  }
3907
4272
  destroy() {
4273
+ this.disposeChart();
4274
+ }
4275
+ createChart(host) {
4276
+ return init(host);
4277
+ }
4278
+ disposeChart() {
4279
+ this.detachHandlers();
4280
+ this.chart?.off('click');
4281
+ this.chart?.dispose();
4282
+ this.chart = undefined;
4283
+ this.currentHost = undefined;
4284
+ this.lastSeriesClickAt = 0;
4285
+ this.lastGridClick = undefined;
4286
+ }
4287
+ detachHandlers() {
3908
4288
  const zrender = this.chart?.getZr?.();
3909
4289
  if (zrender && this.zrenderClickHandler) {
3910
4290
  zrender.off?.('click', this.zrenderClickHandler);
@@ -3915,8 +4295,26 @@ class EChartsEngineAdapter {
3915
4295
  this.clickHost = undefined;
3916
4296
  this.domClickHandler = undefined;
3917
4297
  this.zrenderClickHandler = undefined;
3918
- this.chart?.dispose();
3919
- this.chart = undefined;
4298
+ }
4299
+ resolvePointCategory(payload, data, fallback) {
4300
+ const categoryField = payload.config.axes?.x?.field
4301
+ ?? payload.config.series.find((series) => series.categoryField)?.categoryField;
4302
+ const dataSource = payload.config.dataSource;
4303
+ const usesCanonicalStatsTime = dataSource?.kind === 'remote'
4304
+ && dataSource.query?.sourceKind === 'praxis.stats';
4305
+ const usesCanonicalStatsTimePoint = usesCanonicalStatsTime
4306
+ && (payload.config.axes?.x?.type === 'time'
4307
+ || (dataSource?.kind === 'remote' && dataSource.query?.statsOperation === 'timeseries'));
4308
+ const candidate = usesCanonicalStatsTimePoint && data['label'] !== null
4309
+ && data['label'] !== undefined
4310
+ && data['label'] !== ''
4311
+ ? data['label']
4312
+ : categoryField && data[categoryField] !== null && data[categoryField] !== undefined
4313
+ ? data[categoryField]
4314
+ : fallback;
4315
+ return candidate === null || candidate === undefined
4316
+ ? undefined
4317
+ : String(candidate);
3920
4318
  }
3921
4319
  emitCategoryClickFromGrid(event, payload) {
3922
4320
  const chart = this.chart;
@@ -3944,23 +4342,54 @@ class EChartsEngineAdapter {
3944
4342
  return;
3945
4343
  }
3946
4344
  const category = categories[categoryIndex];
3947
- const series = firstOptionEntry(option?.series);
3948
- const value = Array.isArray(series?.data) ? series.data[categoryIndex] : undefined;
4345
+ const categoryValue = category == null ? undefined : String(category);
4346
+ const categoryField = payload.config.axes?.x?.field
4347
+ ?? payload.config.series.find((series) => series.categoryField)?.categoryField;
4348
+ const signature = JSON.stringify({
4349
+ chartId: payload.config.id,
4350
+ category: categoryValue,
4351
+ horizontal,
4352
+ categoryIndex,
4353
+ });
4354
+ if (this.isDuplicateGridClick(signature)) {
4355
+ return;
4356
+ }
4357
+ const usesCanonicalStatsRows = payload.config.dataSource?.kind === 'remote'
4358
+ && payload.config.dataSource.query?.sourceKind === 'praxis.stats';
4359
+ const fallbackSourceRow = !usesCanonicalStatsRows
4360
+ && categoryField
4361
+ && categoryValue !== undefined
4362
+ ? payload.data.find((row) => String(row[categoryField] ?? '') === categoryValue)
4363
+ : undefined;
4364
+ const sourceRow = usesCanonicalStatsRows
4365
+ ? payload.data[categoryIndex]
4366
+ : fallbackSourceRow;
4367
+ const data = {
4368
+ ...(sourceRow ?? {}),
4369
+ category: categoryValue,
4370
+ };
4371
+ if (categoryField && categoryValue !== undefined) {
4372
+ data[categoryField] = categoryValue;
4373
+ }
3949
4374
  payload.onPointClick?.({
3950
4375
  chartId: payload.config.id,
3951
- seriesId: series?.id,
3952
- seriesName: series?.name,
3953
- category: category == null ? undefined : String(category),
3954
- value: pointValue(value),
3955
- data: pointData(category, value),
4376
+ category: categoryValue,
4377
+ data,
3956
4378
  });
3957
4379
  }
3958
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: EChartsEngineAdapter, deps: [{ token: PraxisChartOptionBuilderService }], target: i0.ɵɵFactoryTarget.Injectable });
4380
+ isDuplicateGridClick(signature) {
4381
+ const now = Date.now();
4382
+ const duplicate = this.lastGridClick?.signature === signature
4383
+ && now - this.lastGridClick.at < 80;
4384
+ this.lastGridClick = { signature, at: now };
4385
+ return duplicate;
4386
+ }
4387
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: EChartsEngineAdapter, deps: [{ token: EChartsOptionBuilderService }], target: i0.ɵɵFactoryTarget.Injectable });
3959
4388
  static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: EChartsEngineAdapter });
3960
4389
  }
3961
4390
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: EChartsEngineAdapter, decorators: [{
3962
4391
  type: Injectable
3963
- }], ctorParameters: () => [{ type: PraxisChartOptionBuilderService }] });
4392
+ }], ctorParameters: () => [{ type: EChartsOptionBuilderService }] });
3964
4393
  function firstOptionEntry(value) {
3965
4394
  return Array.isArray(value) ? value[0] : value;
3966
4395
  }
@@ -3990,6 +4419,13 @@ const PRAXIS_CHARTS_EN_US = {
3990
4419
  'praxis.charts.runtime.remoteErrorTitle': 'Chart data could not be loaded',
3991
4420
  'praxis.charts.runtime.remoteErrorDescription': 'Review the chart data source and analytics request before continuing.',
3992
4421
  'praxis.charts.runtime.booleanFalseLabel': 'Not {{ label }}',
4422
+ 'praxis.charts.runtime.comparisonCurrent': 'Current',
4423
+ 'praxis.charts.runtime.comparisonPrevious': 'Previous',
4424
+ 'praxis.charts.widget.missingDocument': 'This widget does not have a canonical chart document yet. Runtime inputs are preserved until a canonical chartDocument is provided by the host.',
4425
+ 'praxis.charts.widget.queryContext.label': 'Query context',
4426
+ 'praxis.charts.widget.queryContext.ariaLabel': 'Chart query context',
4427
+ 'praxis.charts.widget.queryContext.invalidJson': 'Query context must be valid JSON.',
4428
+ 'praxis.charts.widget.queryContext.mustBeObject': 'Query context must be a JSON object.',
3993
4429
  'praxis.charts.editor.section.general': 'General',
3994
4430
  'praxis.charts.editor.section.data': 'Data',
3995
4431
  'praxis.charts.editor.section.analytics': 'Analytics',
@@ -4051,6 +4487,34 @@ const PRAXIS_CHARTS_EN_US = {
4051
4487
  'praxis.charts.editor.appearance.surfaceTitle': 'Surface',
4052
4488
  'praxis.charts.editor.appearance.surfaceHint': 'Use embedded for dashboard widgets and contained only when the chart must own its visual surface.',
4053
4489
  'praxis.charts.editor.appearance.statesTitle': 'State messages',
4490
+ 'praxis.charts.editor.catalog.resourceMissing': 'Resource catalog is required for governed praxis.stats authoring.',
4491
+ 'praxis.charts.editor.catalog.resourceStale': 'Selected resource is not available in the governed resource catalog.',
4492
+ 'praxis.charts.editor.catalog.operationMissing': 'Selected resource does not publish authorable stats operations.',
4493
+ 'praxis.charts.editor.catalog.operationStale': 'Selected operation is not supported by the governed resource catalog.',
4494
+ 'praxis.charts.editor.catalog.fieldCatalogMissing': 'The selected resource does not publish governed fields for chart authoring.',
4495
+ 'praxis.charts.editor.catalog.dimensionFieldsMissing': 'No governed dimensions are available for the current resource and operation.',
4496
+ 'praxis.charts.editor.catalog.metricFieldsMissing': 'No governed metrics are available for the current resource and operation.',
4497
+ 'praxis.charts.editor.catalog.dimensionFieldStale': 'Selected dimension is not eligible for the current resource and operation.',
4498
+ 'praxis.charts.editor.catalog.metricFieldStale': 'Selected metric is not eligible for the current resource and operation.',
4499
+ 'praxis.charts.editor.catalog.metricAggregationStale': 'Selected aggregation is not allowed for the selected metric field.',
4500
+ 'praxis.charts.editor.catalog.targetMissing': 'Target catalog is required for this governed event action.',
4501
+ 'praxis.charts.editor.catalog.targetStale': 'Selected target is not available for this event action.',
4502
+ 'praxis.charts.editor.catalog.capabilitiesDenied': 'Access to the selected resource capabilities was denied.',
4503
+ 'praxis.charts.editor.catalog.capabilitiesUnavailable': 'The selected resource capabilities are temporarily unavailable.',
4504
+ 'praxis.charts.editor.catalog.contextUnavailable': 'Governed chart authoring context could not be resolved.',
4505
+ 'praxis.charts.widget.contextDiagnostics.title': 'Authoring context',
4506
+ 'praxis.charts.widget.contextDiagnostics.catalogDenied': 'Access to the governed resource catalog was denied.',
4507
+ 'praxis.charts.widget.contextDiagnostics.catalogUnavailable': 'The governed resource catalog is temporarily unavailable.',
4508
+ 'praxis.charts.widget.contextDiagnostics.catalogEmpty': 'No authorable stats resources were published by the governed catalog.',
4509
+ 'praxis.charts.widget.contextDiagnostics.catalogPathInvalid': 'The resource catalog published an unsafe or non-relative operational path.',
4510
+ 'praxis.charts.widget.contextDiagnostics.capabilitiesDenied': 'Access to the selected resource capabilities was denied.',
4511
+ 'praxis.charts.widget.contextDiagnostics.capabilitiesUnavailable': 'The selected resource capabilities are temporarily unavailable.',
4512
+ 'praxis.charts.widget.contextDiagnostics.operationsEmpty': 'The selected resource does not publish authorable stats operations.',
4513
+ 'praxis.charts.widget.contextDiagnostics.fieldsEmpty': 'The selected resource does not publish governed fields for chart authoring.',
4514
+ 'praxis.charts.widget.contextDiagnostics.identityMismatch': 'The selected resource identity does not match its governed capability snapshot.',
4515
+ 'praxis.charts.widget.contextDiagnostics.capabilitiesPathInvalid': 'The selected capability snapshot did not publish a safe governed resource path.',
4516
+ 'praxis.charts.widget.contextDiagnostics.capabilitiesPathMismatch': 'The selected capability snapshot path does not match the operational path published by the catalog.',
4517
+ 'praxis.charts.widget.contextDiagnostics.partial': 'Some governed authoring choices could not be materialized from the current page context.',
4054
4518
  'praxis.charts.editor.hint.sizingMode': 'Use fill-container only when the host widget provides a defined body height.',
4055
4519
  'praxis.charts.editor.hint.height': 'Numbers are saved as pixels; CSS lengths such as 20rem are also accepted.',
4056
4520
  'praxis.charts.editor.hint.minHeight': 'Set a readable minimum for compact dashboard widgets.',
@@ -4143,6 +4607,13 @@ const PRAXIS_CHARTS_PT_BR = {
4143
4607
  'praxis.charts.runtime.remoteErrorTitle': 'Não foi possível carregar os dados do gráfico',
4144
4608
  'praxis.charts.runtime.remoteErrorDescription': 'Revise a fonte de dados e os filtros antes de continuar.',
4145
4609
  'praxis.charts.runtime.booleanFalseLabel': 'Não {{ label }}',
4610
+ 'praxis.charts.runtime.comparisonCurrent': 'Atual',
4611
+ 'praxis.charts.runtime.comparisonPrevious': 'Anterior',
4612
+ 'praxis.charts.widget.missingDocument': 'Este widget ainda não possui um documento canônico de gráfico. Os inputs runtime são preservados até o host fornecer um chartDocument canônico.',
4613
+ 'praxis.charts.widget.queryContext.label': 'Contexto da consulta',
4614
+ 'praxis.charts.widget.queryContext.ariaLabel': 'Contexto da consulta do gráfico',
4615
+ 'praxis.charts.widget.queryContext.invalidJson': 'O contexto da consulta deve ser um JSON válido.',
4616
+ 'praxis.charts.widget.queryContext.mustBeObject': 'O contexto da consulta deve ser um objeto JSON.',
4146
4617
  'praxis.charts.editor.section.general': 'Geral',
4147
4618
  'praxis.charts.editor.section.data': 'Dados',
4148
4619
  'praxis.charts.editor.section.analytics': 'Estrutura',
@@ -4204,6 +4675,34 @@ const PRAXIS_CHARTS_PT_BR = {
4204
4675
  'praxis.charts.editor.appearance.surfaceTitle': 'Superfície',
4205
4676
  'praxis.charts.editor.appearance.surfaceHint': 'Use embutido para widgets de dashboard e contido apenas quando o gráfico precisar ser dono da própria superfície visual.',
4206
4677
  'praxis.charts.editor.appearance.statesTitle': 'Mensagens de estado',
4678
+ 'praxis.charts.editor.catalog.resourceMissing': 'O catálogo de recursos é obrigatório para authoring governado de praxis.stats.',
4679
+ 'praxis.charts.editor.catalog.resourceStale': 'O recurso selecionado não está disponível no catálogo governado de recursos.',
4680
+ 'praxis.charts.editor.catalog.operationMissing': 'O recurso selecionado não publica operações de stats authoráveis.',
4681
+ 'praxis.charts.editor.catalog.operationStale': 'A operação selecionada não é suportada pelo catálogo governado do recurso.',
4682
+ 'praxis.charts.editor.catalog.fieldCatalogMissing': 'O recurso selecionado não publica campos governados para a autoria do gráfico.',
4683
+ 'praxis.charts.editor.catalog.dimensionFieldsMissing': 'Nenhuma dimensão governada está disponível para o recurso e a operação atuais.',
4684
+ 'praxis.charts.editor.catalog.metricFieldsMissing': 'Nenhuma métrica governada está disponível para o recurso e a operação atuais.',
4685
+ 'praxis.charts.editor.catalog.dimensionFieldStale': 'A dimensão selecionada não é elegível para o recurso e a operação atuais.',
4686
+ 'praxis.charts.editor.catalog.metricFieldStale': 'A métrica selecionada não é elegível para o recurso e a operação atuais.',
4687
+ 'praxis.charts.editor.catalog.metricAggregationStale': 'A agregação selecionada não é permitida para a métrica escolhida.',
4688
+ 'praxis.charts.editor.catalog.targetMissing': 'O catálogo de destinos é obrigatório para esta ação de evento governada.',
4689
+ 'praxis.charts.editor.catalog.targetStale': 'O destino selecionado não está disponível para esta ação de evento.',
4690
+ 'praxis.charts.editor.catalog.capabilitiesDenied': 'O acesso às capabilities do recurso selecionado foi negado.',
4691
+ 'praxis.charts.editor.catalog.capabilitiesUnavailable': 'As capabilities do recurso selecionado estão temporariamente indisponíveis.',
4692
+ 'praxis.charts.editor.catalog.contextUnavailable': 'Não foi possível resolver o contexto governado de authoring do gráfico.',
4693
+ 'praxis.charts.widget.contextDiagnostics.title': 'Contexto de authoring',
4694
+ 'praxis.charts.widget.contextDiagnostics.catalogDenied': 'O acesso ao catálogo governado de recursos foi negado.',
4695
+ 'praxis.charts.widget.contextDiagnostics.catalogUnavailable': 'O catálogo governado de recursos está temporariamente indisponível.',
4696
+ 'praxis.charts.widget.contextDiagnostics.catalogEmpty': 'Nenhum recurso de stats authorável foi publicado pelo catálogo governado.',
4697
+ 'praxis.charts.widget.contextDiagnostics.catalogPathInvalid': 'O catálogo de recursos publicou um path operacional inseguro ou não relativo.',
4698
+ 'praxis.charts.widget.contextDiagnostics.capabilitiesDenied': 'O acesso às capabilities do recurso selecionado foi negado.',
4699
+ 'praxis.charts.widget.contextDiagnostics.capabilitiesUnavailable': 'As capabilities do recurso selecionado estão temporariamente indisponíveis.',
4700
+ 'praxis.charts.widget.contextDiagnostics.operationsEmpty': 'O recurso selecionado não publica operações de stats authoráveis.',
4701
+ 'praxis.charts.widget.contextDiagnostics.fieldsEmpty': 'O recurso selecionado não publica campos governados para a autoria do gráfico.',
4702
+ 'praxis.charts.widget.contextDiagnostics.identityMismatch': 'A identidade do recurso selecionado não corresponde ao snapshot governado de capabilities.',
4703
+ 'praxis.charts.widget.contextDiagnostics.capabilitiesPathInvalid': 'O snapshot de capabilities selecionado não publicou um path de recurso governado e seguro.',
4704
+ 'praxis.charts.widget.contextDiagnostics.capabilitiesPathMismatch': 'O path do snapshot de capabilities não corresponde ao path operacional publicado pelo catálogo.',
4705
+ 'praxis.charts.widget.contextDiagnostics.partial': 'Algumas opções governadas de authoring não puderam ser materializadas a partir do contexto atual da página.',
4207
4706
  'praxis.charts.editor.hint.sizingMode': 'Use preencher container apenas quando o widget host fornecer altura definida para o corpo.',
4208
4707
  'praxis.charts.editor.hint.height': 'Números são salvos como pixels; medidas CSS como 20rem também são aceitas.',
4209
4708
  'praxis.charts.editor.hint.minHeight': 'Defina um mínimo legível para widgets compactos de dashboard.',
@@ -4342,7 +4841,7 @@ function resolvePraxisChartsText(value, fallback) {
4342
4841
  class ChartEditorDefaultsService {
4343
4842
  create() {
4344
4843
  return {
4345
- version: '1.0.0',
4844
+ version: PRAXIS_X_UI_CHART_AUTHORABLE_VERSION,
4346
4845
  kind: 'bar',
4347
4846
  title: 'Untitled chart',
4348
4847
  source: {
@@ -4418,13 +4917,844 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
4418
4917
  args: [{ providedIn: 'root' }]
4419
4918
  }], ctorParameters: () => [{ type: PraxisChartCanonicalContractMapperService }] });
4420
4919
 
4920
+ /**
4921
+ * Internal projection of already-governed Page Builder composition evidence.
4922
+ *
4923
+ * It deliberately does not discover unconnected widgets, nested widgets,
4924
+ * routes, surfaces, or global actions. A target enters this catalog only when
4925
+ * an existing top-level link from the current chart proves the destination.
4926
+ */
4927
+ class ChartCompositionTargetCatalogAdapter {
4928
+ registry = inject(ComponentMetadataRegistry);
4929
+ resolve(request) {
4930
+ const targets = new Map();
4931
+ const diagnostics = [];
4932
+ const diagnosticKeys = new Set();
4933
+ const addDiagnostic = (code, severity, path) => {
4934
+ const key = `${code}:${path}`;
4935
+ if (diagnosticKeys.has(key)) {
4936
+ return;
4937
+ }
4938
+ diagnosticKeys.add(key);
4939
+ diagnostics.push({ code, severity, path });
4940
+ };
4941
+ if (!this.isPageEvidence(request.page)) {
4942
+ addDiagnostic('chart-target-page-invalid', 'error', 'page');
4943
+ return { targets: [], diagnostics };
4944
+ }
4945
+ const page = request.page;
4946
+ const currentWidget = page.widgets.find((candidate) => this.isWidgetEvidence(candidate) && candidate.key === request.widgetKey);
4947
+ if (!currentWidget) {
4948
+ addDiagnostic('chart-target-source-widget-missing', 'error', 'page.widgets');
4949
+ return { targets: [], diagnostics };
4950
+ }
4951
+ for (const [index, candidate] of (page.composition?.links ?? []).entries()) {
4952
+ if (this.sourceWidgetKey(candidate) !== request.widgetKey) {
4953
+ continue;
4954
+ }
4955
+ const linkPath = `page.composition.links[${index}]`;
4956
+ if (!this.isRecord(candidate)) {
4957
+ addDiagnostic('chart-target-link-invalid', 'warning', linkPath);
4958
+ continue;
4959
+ }
4960
+ if (this.isDeprecatedLink(candidate)) {
4961
+ addDiagnostic('chart-target-link-deprecated', 'warning', linkPath);
4962
+ continue;
4963
+ }
4964
+ const source = this.readComponentPortEndpoint(candidate['from']);
4965
+ if (!source || source.ref.direction !== 'output') {
4966
+ addDiagnostic('chart-target-source-endpoint-invalid', 'warning', `${linkPath}.from`);
4967
+ continue;
4968
+ }
4969
+ if (source.ref.nestedPath?.length) {
4970
+ addDiagnostic('chart-target-source-nested-unsupported', 'info', `${linkPath}.from`);
4971
+ continue;
4972
+ }
4973
+ if (!this.endpointComponentTypeMatches(source.ref.componentType, currentWidget.definition.id, request.widgetType, request.componentId, request.metadata.id, request.metadata.componentType)) {
4974
+ addDiagnostic('chart-target-source-component-type-mismatch', 'warning', `${linkPath}.from`);
4975
+ continue;
4976
+ }
4977
+ const sourcePort = request.metadata.ports?.find((port) => port.id === source.ref.port);
4978
+ if (!this.isAuthorablePort(sourcePort, 'output')) {
4979
+ addDiagnostic('chart-target-source-port-not-authorable', 'warning', `${linkPath}.from`);
4980
+ continue;
4981
+ }
4982
+ const sourceEvent = this.sourceEvent(sourcePort);
4983
+ if (!sourceEvent) {
4984
+ addDiagnostic('chart-target-source-event-unsupported', 'info', `${linkPath}.from.ref.port`);
4985
+ continue;
4986
+ }
4987
+ const target = candidate['to'];
4988
+ if (this.hasEndpointKind(target, 'component-port')) {
4989
+ this.resolveWidgetTarget(target, sourceEvent, sourcePort, candidate['transform'], page, request.widgetKey, linkPath, targets, addDiagnostic);
4990
+ continue;
4991
+ }
4992
+ if (this.hasEndpointKind(target, 'state')) {
4993
+ this.resolveStateTarget(target, sourceEvent, candidate['intent'], page, linkPath, targets, addDiagnostic);
4994
+ continue;
4995
+ }
4996
+ if (this.hasEndpointKind(target, 'global-action')) {
4997
+ addDiagnostic('chart-target-global-action-unsupported', 'info', `${linkPath}.to`);
4998
+ continue;
4999
+ }
5000
+ addDiagnostic('chart-target-endpoint-unsupported', 'warning', `${linkPath}.to`);
5001
+ }
5002
+ return {
5003
+ targets: Array.from(targets.values()),
5004
+ diagnostics,
5005
+ };
5006
+ }
5007
+ resolveWidgetTarget(value, sourceEvent, sourcePort, transform, page, sourceWidgetKey, linkPath, targets, addDiagnostic) {
5008
+ const endpoint = this.readComponentPortEndpoint(value);
5009
+ if (!endpoint || endpoint.ref.direction !== 'input') {
5010
+ addDiagnostic('chart-target-widget-endpoint-invalid', 'warning', `${linkPath}.to`);
5011
+ return;
5012
+ }
5013
+ if (endpoint.ref.nestedPath?.length) {
5014
+ addDiagnostic('chart-target-widget-nested-unsupported', 'info', `${linkPath}.to`);
5015
+ return;
5016
+ }
5017
+ const widget = page.widgets.find((candidate) => this.isWidgetEvidence(candidate) && candidate.key === endpoint.ref.widget);
5018
+ if (!widget) {
5019
+ addDiagnostic('chart-target-widget-missing', 'warning', `${linkPath}.to`);
5020
+ return;
5021
+ }
5022
+ if (widget.key === sourceWidgetKey) {
5023
+ addDiagnostic('chart-target-widget-self-unsupported', 'warning', `${linkPath}.to`);
5024
+ return;
5025
+ }
5026
+ const metadata = this.registry.get(widget.definition.id);
5027
+ if (!metadata) {
5028
+ addDiagnostic('chart-target-widget-metadata-missing', 'warning', `${linkPath}.to`);
5029
+ return;
5030
+ }
5031
+ if (!this.endpointComponentTypeMatches(endpoint.ref.componentType, widget.definition.id, metadata.id, metadata.componentType)) {
5032
+ addDiagnostic('chart-target-widget-component-type-mismatch', 'warning', `${linkPath}.to`);
5033
+ return;
5034
+ }
5035
+ const port = metadata.ports?.find((candidate) => candidate.id === endpoint.ref.port);
5036
+ if (!this.isAuthorablePort(port, 'input')
5037
+ || port?.semanticKind !== 'query-context') {
5038
+ addDiagnostic('chart-target-widget-port-not-authorable', 'warning', `${linkPath}.to`);
5039
+ return;
5040
+ }
5041
+ if (!this.isWidgetLinkSemanticallyCompatible(sourcePort, port, transform)) {
5042
+ addDiagnostic('chart-target-widget-link-incompatible', 'warning', linkPath);
5043
+ return;
5044
+ }
5045
+ const id = widget.key;
5046
+ this.upsertTarget(targets, `filter-widget:${id}`, {
5047
+ id,
5048
+ label: this.widgetLabel(metadata.friendlyName, id),
5049
+ kind: 'widget',
5050
+ actions: ['filter-widget'],
5051
+ events: [sourceEvent],
5052
+ }, sourceEvent);
5053
+ }
5054
+ resolveStateTarget(value, sourceEvent, intent, page, linkPath, targets, addDiagnostic) {
5055
+ const endpoint = this.readStateEndpoint(value);
5056
+ if (!endpoint || !endpoint.ref.path || endpoint.ref.path.trim() !== endpoint.ref.path) {
5057
+ addDiagnostic('chart-target-state-endpoint-invalid', 'warning', `${linkPath}.to`);
5058
+ return;
5059
+ }
5060
+ if (endpoint.ref.layer !== undefined && endpoint.ref.layer !== 'values') {
5061
+ addDiagnostic('chart-target-state-layer-unsupported', 'warning', `${linkPath}.to`);
5062
+ return;
5063
+ }
5064
+ if (endpoint.ref.writable !== true) {
5065
+ addDiagnostic('chart-target-state-not-explicitly-writable', 'warning', `${linkPath}.to`);
5066
+ return;
5067
+ }
5068
+ if (intent !== 'state-write') {
5069
+ addDiagnostic('chart-target-state-intent-invalid', 'warning', `${linkPath}.intent`);
5070
+ return;
5071
+ }
5072
+ const schema = this.readDeclaredStateSchema(page.state);
5073
+ if (!schema || !Object.prototype.hasOwnProperty.call(schema, endpoint.ref.path)) {
5074
+ addDiagnostic('chart-target-state-path-undeclared', 'warning', `${linkPath}.to.ref.path`);
5075
+ return;
5076
+ }
5077
+ const id = endpoint.ref.path;
5078
+ this.upsertTarget(targets, `update-context:${id}`, {
5079
+ id,
5080
+ label: this.stateLabel(schema[id], id),
5081
+ actions: ['update-context'],
5082
+ events: [sourceEvent],
5083
+ }, sourceEvent);
5084
+ }
5085
+ sourceEvent(port) {
5086
+ switch (port.id) {
5087
+ case 'pointAction':
5088
+ return 'pointClick';
5089
+ case 'selectionChange':
5090
+ case 'drillDown':
5091
+ case 'crossFilter':
5092
+ return port.id;
5093
+ default:
5094
+ return null;
5095
+ }
5096
+ }
5097
+ isWidgetLinkSemanticallyCompatible(sourcePort, targetPort, transform) {
5098
+ let effectiveSourceKind = sourcePort.semanticKind;
5099
+ if (transform !== undefined) {
5100
+ if (!this.isRecord(transform) || !Array.isArray(transform['steps'])) {
5101
+ return false;
5102
+ }
5103
+ if (transform['steps'].some((step) => (!this.isRecord(step)
5104
+ || typeof step['kind'] !== 'string'
5105
+ || typeof step['phase'] !== 'string'))) {
5106
+ return false;
5107
+ }
5108
+ const output = transform['output'];
5109
+ if (output !== undefined) {
5110
+ if (!this.isRecord(output)) {
5111
+ return false;
5112
+ }
5113
+ if (output['semanticKind'] !== undefined
5114
+ && typeof output['semanticKind'] !== 'string') {
5115
+ return false;
5116
+ }
5117
+ effectiveSourceKind = typeof output['semanticKind'] === 'string'
5118
+ ? output['semanticKind']
5119
+ : effectiveSourceKind;
5120
+ }
5121
+ }
5122
+ return targetPort.semanticKind === 'query-context'
5123
+ && (effectiveSourceKind === 'query-context' || effectiveSourceKind === 'value');
5124
+ }
5125
+ upsertTarget(targets, key, target, sourceEvent) {
5126
+ const current = targets.get(key);
5127
+ if (!current) {
5128
+ targets.set(key, target);
5129
+ return;
5130
+ }
5131
+ targets.set(key, {
5132
+ ...current,
5133
+ events: current.events?.includes(sourceEvent)
5134
+ ? current.events
5135
+ : [...(current.events ?? []), sourceEvent],
5136
+ });
5137
+ }
5138
+ isPageEvidence(value) {
5139
+ if (!this.isRecord(value) || !Array.isArray(value['widgets'])) {
5140
+ return false;
5141
+ }
5142
+ const composition = value['composition'];
5143
+ if (composition === undefined) {
5144
+ return true;
5145
+ }
5146
+ if (!this.isRecord(composition)) {
5147
+ return false;
5148
+ }
5149
+ return composition['links'] === undefined || Array.isArray(composition['links']);
5150
+ }
5151
+ isWidgetEvidence(value) {
5152
+ if (!this.isRecord(value) || typeof value['key'] !== 'string') {
5153
+ return false;
5154
+ }
5155
+ const definition = value['definition'];
5156
+ return this.isRecord(definition) && typeof definition['id'] === 'string';
5157
+ }
5158
+ sourceWidgetKey(value) {
5159
+ if (!this.isRecord(value)) {
5160
+ return undefined;
5161
+ }
5162
+ const source = value['from'];
5163
+ if (!this.hasEndpointKind(source, 'component-port') || !this.isRecord(source['ref'])) {
5164
+ return undefined;
5165
+ }
5166
+ return typeof source['ref']['widget'] === 'string'
5167
+ ? source['ref']['widget']
5168
+ : undefined;
5169
+ }
5170
+ readComponentPortEndpoint(value) {
5171
+ if (!this.hasEndpointKind(value, 'component-port')) {
5172
+ return null;
5173
+ }
5174
+ const ref = value['ref'];
5175
+ if (!this.isRecord(ref)
5176
+ || typeof ref['widget'] !== 'string'
5177
+ || typeof ref['port'] !== 'string'
5178
+ || typeof ref['direction'] !== 'string'
5179
+ || (ref['componentType'] !== undefined && typeof ref['componentType'] !== 'string')
5180
+ || (ref['nestedPath'] !== undefined && !Array.isArray(ref['nestedPath']))) {
5181
+ return null;
5182
+ }
5183
+ return {
5184
+ kind: 'component-port',
5185
+ ref: {
5186
+ widget: ref['widget'],
5187
+ port: ref['port'],
5188
+ direction: ref['direction'],
5189
+ ...(typeof ref['componentType'] === 'string'
5190
+ ? { componentType: ref['componentType'] }
5191
+ : {}),
5192
+ ...(Array.isArray(ref['nestedPath'])
5193
+ ? { nestedPath: ref['nestedPath'] }
5194
+ : {}),
5195
+ },
5196
+ };
5197
+ }
5198
+ readStateEndpoint(value) {
5199
+ if (!this.hasEndpointKind(value, 'state')) {
5200
+ return null;
5201
+ }
5202
+ const ref = value['ref'];
5203
+ if (!this.isRecord(ref)
5204
+ || typeof ref['path'] !== 'string'
5205
+ || (ref['layer'] !== undefined && typeof ref['layer'] !== 'string')
5206
+ || (ref['writable'] !== undefined && typeof ref['writable'] !== 'boolean')) {
5207
+ return null;
5208
+ }
5209
+ return {
5210
+ kind: 'state',
5211
+ ref: {
5212
+ path: ref['path'],
5213
+ ...(typeof ref['layer'] === 'string' ? { layer: ref['layer'] } : {}),
5214
+ ...(typeof ref['writable'] === 'boolean' ? { writable: ref['writable'] } : {}),
5215
+ },
5216
+ };
5217
+ }
5218
+ readDeclaredStateSchema(value) {
5219
+ if (!this.isRecord(value) || !this.isRecord(value['schema'])) {
5220
+ return null;
5221
+ }
5222
+ return value['schema'];
5223
+ }
5224
+ isAuthorablePort(port, direction) {
5225
+ return !!port
5226
+ && port.direction === direction
5227
+ && port.exposure?.public === true
5228
+ && port.exposure.deprecated !== true;
5229
+ }
5230
+ endpointComponentTypeMatches(endpointComponentType, ...canonicalIds) {
5231
+ if (endpointComponentType === undefined) {
5232
+ return true;
5233
+ }
5234
+ return canonicalIds.some((candidate) => candidate === endpointComponentType);
5235
+ }
5236
+ widgetLabel(friendlyName, widgetKey) {
5237
+ const label = friendlyName.trim();
5238
+ return label && label !== widgetKey
5239
+ ? `${label} (${widgetKey})`
5240
+ : widgetKey;
5241
+ }
5242
+ stateLabel(descriptor, path) {
5243
+ const description = this.isRecord(descriptor) && typeof descriptor['description'] === 'string'
5244
+ ? descriptor['description'].trim()
5245
+ : '';
5246
+ return description && description !== path
5247
+ ? `${description} (${path})`
5248
+ : path;
5249
+ }
5250
+ isDeprecatedLink(value) {
5251
+ const metadata = value['metadata'];
5252
+ return this.isRecord(metadata) && metadata['deprecated'] === true;
5253
+ }
5254
+ hasEndpointKind(value, kind) {
5255
+ return this.isRecord(value) && value['kind'] === kind;
5256
+ }
5257
+ isRecord(value) {
5258
+ return !!value && typeof value === 'object' && !Array.isArray(value);
5259
+ }
5260
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: ChartCompositionTargetCatalogAdapter, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
5261
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: ChartCompositionTargetCatalogAdapter, providedIn: 'root' });
5262
+ }
5263
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: ChartCompositionTargetCatalogAdapter, decorators: [{
5264
+ type: Injectable,
5265
+ args: [{ providedIn: 'root' }]
5266
+ }] });
5267
+
5268
+ class ChartResourceCapabilityCatalogAdapter {
5269
+ toResourceOperations(capabilities) {
5270
+ if (!capabilities) {
5271
+ return [];
5272
+ }
5273
+ const operations = [];
5274
+ if (capabilities.canonicalOperations?.['statsGroupBy'] === true) {
5275
+ operations.push('group-by');
5276
+ }
5277
+ if (capabilities.canonicalOperations?.['statsTimeSeries'] === true) {
5278
+ operations.push('timeseries');
5279
+ }
5280
+ if (capabilities.canonicalOperations?.['statsDistribution'] === true) {
5281
+ operations.push('distribution');
5282
+ }
5283
+ return operations;
5284
+ }
5285
+ toFieldOptions(capabilities, operation) {
5286
+ if (!capabilities) {
5287
+ return [];
5288
+ }
5289
+ const fields = capabilities?.stats?.fields ?? [];
5290
+ const resourceOperations = this.toResourceOperations(capabilities);
5291
+ const fieldOptions = fields
5292
+ .filter((field) => !!field?.field)
5293
+ .flatMap((field) => this.toFieldOptionsForRoles(field, capabilities, resourceOperations));
5294
+ const countOption = this.toCountOption(fields, capabilities, resourceOperations);
5295
+ return [...fieldOptions, ...(countOption ? [countOption] : [])]
5296
+ .filter((option) => !operation || option.operations?.includes(operation));
5297
+ }
5298
+ toFieldOptionsForRoles(field, capabilities, resourceOperations) {
5299
+ const aggregations = this.resolveAggregations(field);
5300
+ const shared = {
5301
+ field: field.field,
5302
+ ...(field.label ? { label: field.label } : {}),
5303
+ ...(capabilities.resourceKey ? { resourceIds: [capabilities.resourceKey] } : {}),
5304
+ ...(capabilities.resourcePath ? { resourcePaths: [capabilities.resourcePath] } : {}),
5305
+ ...(aggregations.length ? { aggregations } : {}),
5306
+ };
5307
+ const options = [];
5308
+ const dimensionOperations = this.resolveDimensionOperations(field)
5309
+ .filter((candidate) => resourceOperations.includes(candidate));
5310
+ const distributionModes = this.resolveDistributionModes(field);
5311
+ if (dimensionOperations.length) {
5312
+ options.push({
5313
+ ...shared,
5314
+ roles: field.timeSeriesEligible === true
5315
+ ? ['dimension', 'time']
5316
+ : ['dimension'],
5317
+ operations: dimensionOperations,
5318
+ ...(dimensionOperations.includes('distribution')
5319
+ ? { distributionModes }
5320
+ : {}),
5321
+ });
5322
+ }
5323
+ if (field.metricFieldEligible === true && aggregations.length) {
5324
+ options.push({
5325
+ ...shared,
5326
+ aggregable: true,
5327
+ roles: ['metric'],
5328
+ operations: resourceOperations,
5329
+ ...(resourceOperations.includes('distribution')
5330
+ ? { distributionModes: ['terms'] }
5331
+ : {}),
5332
+ });
5333
+ }
5334
+ return options;
5335
+ }
5336
+ resolveDimensionOperations(field) {
5337
+ const operations = new Set();
5338
+ const modes = field.modes ?? [];
5339
+ if (field.groupByEligible === true || modes.includes('GROUP_BY')) {
5340
+ operations.add('group-by');
5341
+ }
5342
+ if (field.timeSeriesEligible === true || modes.includes('TIME_SERIES')) {
5343
+ operations.add('timeseries');
5344
+ }
5345
+ if (field.distributionTermsEligible === true
5346
+ || field.distributionHistogramEligible === true
5347
+ || modes.includes('DISTRIBUTION_TERMS')
5348
+ || modes.includes('DISTRIBUTION_HISTOGRAM')) {
5349
+ operations.add('distribution');
5350
+ }
5351
+ return Array.from(operations);
5352
+ }
5353
+ resolveAggregations(field) {
5354
+ return (field.metrics ?? [])
5355
+ .map((metric) => this.mapMetric(metric))
5356
+ .filter((metric) => !!metric && metric !== 'count');
5357
+ }
5358
+ resolveDistributionModes(field) {
5359
+ const modes = [];
5360
+ if (field.distributionTermsEligible === true || field.modes?.includes('DISTRIBUTION_TERMS')) {
5361
+ modes.push('terms');
5362
+ }
5363
+ if (field.distributionHistogramEligible === true
5364
+ || field.modes?.includes('DISTRIBUTION_HISTOGRAM')) {
5365
+ modes.push('histogram');
5366
+ }
5367
+ return modes;
5368
+ }
5369
+ mapMetric(metric) {
5370
+ switch (String(metric || '').trim().toUpperCase()) {
5371
+ case 'COUNT':
5372
+ return 'count';
5373
+ case 'DISTINCT_COUNT':
5374
+ return 'distinct-count';
5375
+ case 'SUM':
5376
+ return 'sum';
5377
+ case 'AVG':
5378
+ case 'AVERAGE':
5379
+ return 'avg';
5380
+ case 'MIN':
5381
+ return 'min';
5382
+ case 'MAX':
5383
+ return 'max';
5384
+ default:
5385
+ return null;
5386
+ }
5387
+ }
5388
+ toCountOption(fields, capabilities, resourceOperations) {
5389
+ if (!resourceOperations.length
5390
+ || !fields.some((field) => (field.metrics ?? []).some((metric) => String(metric).toUpperCase() === 'COUNT'))) {
5391
+ return null;
5392
+ }
5393
+ return {
5394
+ field: 'count',
5395
+ label: 'Count',
5396
+ aggregable: true,
5397
+ roles: ['metric'],
5398
+ ...(capabilities.resourceKey ? { resourceIds: [capabilities.resourceKey] } : {}),
5399
+ ...(capabilities.resourcePath ? { resourcePaths: [capabilities.resourcePath] } : {}),
5400
+ operations: resourceOperations,
5401
+ ...(resourceOperations.includes('distribution')
5402
+ ? { distributionModes: ['terms', 'histogram'] }
5403
+ : {}),
5404
+ aggregations: ['count'],
5405
+ };
5406
+ }
5407
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: ChartResourceCapabilityCatalogAdapter, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
5408
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: ChartResourceCapabilityCatalogAdapter, providedIn: 'root' });
5409
+ }
5410
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: ChartResourceCapabilityCatalogAdapter, decorators: [{
5411
+ type: Injectable,
5412
+ args: [{ providedIn: 'root' }]
5413
+ }] });
5414
+
5415
+ class PraxisChartConfigEditorContextResolverService {
5416
+ static REQUEST_TIMEOUT_MS = 10_000;
5417
+ discovery = inject(ResourceDiscoveryService);
5418
+ capabilityAdapter = inject(ChartResourceCapabilityCatalogAdapter);
5419
+ targetAdapter = inject(ChartCompositionTargetCatalogAdapter);
5420
+ /**
5421
+ * ComponentConfigEditorContextRequest currently exposes no principal, tenant, or
5422
+ * profile identity. Do not infer an authorization scope from widget/page data.
5423
+ * Successful reads are not cached across requests because capabilities may be
5424
+ * permission-sensitive. Only concurrent reads are deduplicated, and every pending
5425
+ * read has a finite timeout; server-side authorization remains canonical.
5426
+ */
5427
+ catalogProjectionRequest;
5428
+ capabilityRequests = new Map();
5429
+ async resolve(request) {
5430
+ const targetProjection = this.targetAdapter.resolve(request);
5431
+ let projection;
5432
+ try {
5433
+ projection = await this.getCatalogProjection();
5434
+ }
5435
+ catch (error) {
5436
+ return {
5437
+ context: {
5438
+ availableResources: [],
5439
+ availableFields: [],
5440
+ availableTargets: targetProjection.targets,
5441
+ },
5442
+ diagnostics: [
5443
+ ...(targetProjection.diagnostics ?? []),
5444
+ this.httpDiagnostic(error, 'catalog'),
5445
+ ],
5446
+ };
5447
+ }
5448
+ const resources = projection.candidates.map((candidate) => candidate.option);
5449
+ const selectedResource = this.selectedResource(request.persistedInputs);
5450
+ if (!selectedResource) {
5451
+ return {
5452
+ context: {
5453
+ availableResources: resources,
5454
+ availableFields: [],
5455
+ availableTargets: targetProjection.targets,
5456
+ },
5457
+ diagnostics: [
5458
+ ...projection.diagnostics,
5459
+ ...(targetProjection.diagnostics ?? []),
5460
+ ],
5461
+ };
5462
+ }
5463
+ const selected = await this.resolveResourceFromProjection(selectedResource, projection);
5464
+ const resolvedResources = selected.resource
5465
+ ? resources.map((resource) => resource.id === selected.resource?.id ? selected.resource : resource)
5466
+ : resources;
5467
+ return {
5468
+ context: {
5469
+ availableResources: resolvedResources,
5470
+ availableFields: selected.fields,
5471
+ availableTargets: targetProjection.targets,
5472
+ },
5473
+ diagnostics: [
5474
+ ...projection.diagnostics,
5475
+ ...(targetProjection.diagnostics ?? []),
5476
+ ...selected.diagnostics,
5477
+ ],
5478
+ };
5479
+ }
5480
+ async resolveResource(resourceIdentity) {
5481
+ let projection;
5482
+ try {
5483
+ projection = await this.getCatalogProjection();
5484
+ }
5485
+ catch (error) {
5486
+ return {
5487
+ fields: [],
5488
+ diagnostics: [this.httpDiagnostic(error, 'catalog')],
5489
+ };
5490
+ }
5491
+ const selected = await this.resolveResourceFromProjection(resourceIdentity, projection);
5492
+ return {
5493
+ ...selected,
5494
+ diagnostics: [...projection.diagnostics, ...selected.diagnostics],
5495
+ };
5496
+ }
5497
+ async resolveResourceFromProjection(resourceIdentity, projection) {
5498
+ const candidate = this.findCandidate(projection.candidates, resourceIdentity);
5499
+ if (!candidate) {
5500
+ return {
5501
+ fields: [],
5502
+ diagnostics: [{
5503
+ code: 'chart-resource-catalog-selection-stale',
5504
+ severity: 'error',
5505
+ path: 'chartDocument.source.resource',
5506
+ }],
5507
+ };
5508
+ }
5509
+ let capabilities;
5510
+ try {
5511
+ capabilities = await this.getCapabilities(candidate);
5512
+ }
5513
+ catch (error) {
5514
+ return {
5515
+ fields: [],
5516
+ diagnostics: [this.httpDiagnostic(error, 'capabilities')],
5517
+ };
5518
+ }
5519
+ if (String(capabilities.resourceKey || '').trim() !== candidate.option.id) {
5520
+ return {
5521
+ fields: [],
5522
+ diagnostics: [{
5523
+ code: 'chart-resource-capabilities-identity-mismatch',
5524
+ severity: 'error',
5525
+ path: 'chartDocument.source.resource',
5526
+ }],
5527
+ };
5528
+ }
5529
+ const capabilityResourcePath = String(capabilities.resourcePath || '').trim();
5530
+ if (!this.isGovernedRelativePath(capabilityResourcePath)) {
5531
+ return {
5532
+ fields: [],
5533
+ diagnostics: [{
5534
+ code: 'chart-resource-capabilities-path-invalid',
5535
+ severity: 'error',
5536
+ path: 'chartDocument.source.resource',
5537
+ }],
5538
+ };
5539
+ }
5540
+ const catalogResourcePath = this.normalizeOperationalPath(candidate.option.path);
5541
+ if (this.normalizeOperationalPath(capabilityResourcePath) !== catalogResourcePath) {
5542
+ return {
5543
+ fields: [],
5544
+ diagnostics: [{
5545
+ code: 'chart-resource-capabilities-path-mismatch',
5546
+ severity: 'error',
5547
+ path: 'chartDocument.source.resource',
5548
+ }],
5549
+ };
5550
+ }
5551
+ const canonicalCapabilities = {
5552
+ ...capabilities,
5553
+ resourcePath: catalogResourcePath,
5554
+ };
5555
+ const operations = this.capabilityAdapter.toResourceOperations(canonicalCapabilities);
5556
+ const fields = this.capabilityAdapter.toFieldOptions(canonicalCapabilities);
5557
+ const diagnostics = [];
5558
+ if (!operations.length) {
5559
+ diagnostics.push({
5560
+ code: 'chart-resource-stats-operations-empty',
5561
+ severity: 'warning',
5562
+ path: 'chartDocument.source.operation',
5563
+ });
5564
+ }
5565
+ if (!fields.length) {
5566
+ diagnostics.push({
5567
+ code: 'chart-resource-stats-fields-empty',
5568
+ severity: 'error',
5569
+ path: 'chartDocument.dimensions',
5570
+ });
5571
+ }
5572
+ return {
5573
+ resource: {
5574
+ ...candidate.option,
5575
+ id: canonicalCapabilities.resourceKey,
5576
+ path: catalogResourcePath,
5577
+ operations,
5578
+ },
5579
+ fields,
5580
+ diagnostics,
5581
+ };
5582
+ }
5583
+ async getCatalogProjection() {
5584
+ const cached = this.catalogProjectionRequest;
5585
+ if (cached) {
5586
+ return cached.promise;
5587
+ }
5588
+ let request;
5589
+ const promise = firstValueFrom(this.discovery.getSchemaCatalog().pipe(timeout(PraxisChartConfigEditorContextResolverService.REQUEST_TIMEOUT_MS)))
5590
+ .then((catalog) => this.projectCatalog(catalog))
5591
+ .finally(() => {
5592
+ if (this.catalogProjectionRequest === request) {
5593
+ this.catalogProjectionRequest = undefined;
5594
+ }
5595
+ });
5596
+ request = { promise };
5597
+ this.catalogProjectionRequest = request;
5598
+ return request.promise;
5599
+ }
5600
+ projectCatalog(catalog) {
5601
+ const byResourceKey = new Map();
5602
+ for (const endpoint of Array.isArray(catalog?.endpoints) ? catalog.endpoints : []) {
5603
+ const resourceKey = String(endpoint?.resourceKey || '').trim();
5604
+ if (!resourceKey) {
5605
+ continue;
5606
+ }
5607
+ byResourceKey.set(resourceKey, [...(byResourceKey.get(resourceKey) ?? []), endpoint]);
5608
+ }
5609
+ const candidates = [];
5610
+ const diagnostics = [];
5611
+ for (const [resourceKey, endpoints] of byResourceKey.entries()) {
5612
+ // The semantic resourceKey scopes the resource first. Path suffixes below only
5613
+ // identify canonical HTTP projections inside that governed scope; they never
5614
+ // route user intent or choose a resource from text.
5615
+ const declaredCapabilityEndpoints = endpoints.filter((endpoint) => (endpoint.method === 'GET'
5616
+ && !endpoint.path.includes('{')
5617
+ && endpoint.path.endsWith('/capabilities')));
5618
+ if (declaredCapabilityEndpoints.some((endpoint) => !this.isGovernedRelativePath(endpoint.path))) {
5619
+ diagnostics.push({
5620
+ code: 'chart-resource-catalog-path-invalid',
5621
+ severity: 'error',
5622
+ path: resourceKey,
5623
+ });
5624
+ continue;
5625
+ }
5626
+ const capabilityEndpoints = declaredCapabilityEndpoints.filter((endpoint) => (this.isGovernedRelativePath(endpoint.path.slice(0, -'/capabilities'.length))));
5627
+ if (declaredCapabilityEndpoints.length && !capabilityEndpoints.length) {
5628
+ diagnostics.push({
5629
+ code: 'chart-resource-catalog-path-invalid',
5630
+ severity: 'error',
5631
+ path: resourceKey,
5632
+ });
5633
+ continue;
5634
+ }
5635
+ if (capabilityEndpoints.length !== 1) {
5636
+ if (capabilityEndpoints.length > 1) {
5637
+ diagnostics.push({
5638
+ code: 'chart-resource-capabilities-endpoint-ambiguous',
5639
+ severity: 'warning',
5640
+ path: resourceKey,
5641
+ });
5642
+ }
5643
+ continue;
5644
+ }
5645
+ const capabilitiesEndpoint = capabilityEndpoints[0];
5646
+ const resourcePath = this.normalizeOperationalPath(capabilitiesEndpoint.path.slice(0, -'/capabilities'.length));
5647
+ const statsPaths = new Set([
5648
+ `${resourcePath}/stats/group-by`,
5649
+ `${resourcePath}/stats/timeseries`,
5650
+ `${resourcePath}/stats/distribution`,
5651
+ ]);
5652
+ const publishesStats = endpoints.some((endpoint) => (endpoint.method === 'POST'
5653
+ && statsPaths.has(endpoint.path)));
5654
+ if (!publishesStats) {
5655
+ continue;
5656
+ }
5657
+ const visual = capabilitiesEndpoint.resourceVisual
5658
+ ?? endpoints.find((endpoint) => endpoint.resourceVisual)?.resourceVisual
5659
+ ?? null;
5660
+ candidates.push({
5661
+ capabilitiesHref: capabilitiesEndpoint.path,
5662
+ option: {
5663
+ id: resourceKey,
5664
+ label: String(visual?.title || resourceKey).trim(),
5665
+ path: resourcePath,
5666
+ ...(visual?.description ? { description: visual.description } : {}),
5667
+ },
5668
+ });
5669
+ }
5670
+ candidates.sort((left, right) => (left.option.label.localeCompare(right.option.label)
5671
+ || left.option.id.localeCompare(right.option.id)));
5672
+ if (!candidates.length) {
5673
+ diagnostics.push({
5674
+ code: 'chart-resource-catalog-empty',
5675
+ severity: 'warning',
5676
+ path: 'availableResources',
5677
+ });
5678
+ }
5679
+ return { candidates, diagnostics };
5680
+ }
5681
+ async getCapabilities(candidate) {
5682
+ const key = `${candidate.option.id}\u0000${candidate.capabilitiesHref}`;
5683
+ const cached = this.capabilityRequests.get(key);
5684
+ if (cached) {
5685
+ return cached.promise;
5686
+ }
5687
+ let request;
5688
+ const promise = firstValueFrom(this.discovery.getCapabilitiesByUrl(candidate.capabilitiesHref).pipe(timeout(PraxisChartConfigEditorContextResolverService.REQUEST_TIMEOUT_MS)))
5689
+ .finally(() => {
5690
+ if (this.capabilityRequests.get(key) === request) {
5691
+ this.capabilityRequests.delete(key);
5692
+ }
5693
+ });
5694
+ request = { promise };
5695
+ this.capabilityRequests.set(key, request);
5696
+ return request.promise;
5697
+ }
5698
+ findCandidate(candidates, resourceIdentity) {
5699
+ const identity = String(resourceIdentity || '').trim();
5700
+ const normalizedPath = this.normalizeOperationalPath(identity);
5701
+ return candidates.find((candidate) => (candidate.option.id === identity
5702
+ || this.normalizeOperationalPath(candidate.option.path) === normalizedPath));
5703
+ }
5704
+ selectedResource(inputs) {
5705
+ const chartDocument = this.asRecord(inputs?.['chartDocument']);
5706
+ const source = this.asRecord(chartDocument?.source);
5707
+ if (source?.['kind'] !== 'praxis.stats') {
5708
+ return null;
5709
+ }
5710
+ const resource = String(source?.['resource'] || '').trim();
5711
+ return resource || null;
5712
+ }
5713
+ normalizeOperationalPath(value) {
5714
+ const path = String(value || '').trim();
5715
+ let end = path.length;
5716
+ while (end > 1 && path.charAt(end - 1) === '/') {
5717
+ end -= 1;
5718
+ }
5719
+ return path.slice(0, end);
5720
+ }
5721
+ isGovernedRelativePath(value) {
5722
+ const path = String(value || '').trim();
5723
+ return path.startsWith('/') && !path.startsWith('//');
5724
+ }
5725
+ asRecord(value) {
5726
+ return value && typeof value === 'object' && !Array.isArray(value)
5727
+ ? value
5728
+ : null;
5729
+ }
5730
+ httpDiagnostic(error, phase) {
5731
+ const denied = error instanceof HttpErrorResponse
5732
+ && (error.status === 401 || error.status === 403);
5733
+ return {
5734
+ code: denied
5735
+ ? `chart-resource-${phase}-denied`
5736
+ : `chart-resource-${phase}-unavailable`,
5737
+ severity: 'error',
5738
+ path: phase === 'catalog'
5739
+ ? 'availableResources'
5740
+ : 'chartDocument.source.resource',
5741
+ };
5742
+ }
5743
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: PraxisChartConfigEditorContextResolverService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
5744
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: PraxisChartConfigEditorContextResolverService, providedIn: 'root' });
5745
+ }
5746
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: PraxisChartConfigEditorContextResolverService, decorators: [{
5747
+ type: Injectable,
5748
+ args: [{ providedIn: 'root' }]
5749
+ }] });
5750
+
4421
5751
  class PraxisChartConfigEditor {
4422
5752
  documentInput = input(null, { ...(ngDevMode ? { debugName: "documentInput" } : /* istanbul ignore next */ {}), alias: 'document' });
4423
5753
  modeInput = input('edit', { ...(ngDevMode ? { debugName: "modeInput" } : /* istanbul ignore next */ {}), alias: 'mode' });
4424
5754
  readonlyInput = input(false, { ...(ngDevMode ? { debugName: "readonlyInput" } : /* istanbul ignore next */ {}), alias: 'readonly' });
4425
- availableResourcesInput = input([], { ...(ngDevMode ? { debugName: "availableResourcesInput" } : /* istanbul ignore next */ {}), alias: 'availableResources' });
4426
- availableFieldsInput = input([], { ...(ngDevMode ? { debugName: "availableFieldsInput" } : /* istanbul ignore next */ {}), alias: 'availableFields' });
4427
- availableTargetsInput = input([], { ...(ngDevMode ? { debugName: "availableTargetsInput" } : /* istanbul ignore next */ {}), alias: 'availableTargets' });
5755
+ availableResourcesInput = input(null, { ...(ngDevMode ? { debugName: "availableResourcesInput" } : /* istanbul ignore next */ {}), alias: 'availableResources' });
5756
+ availableFieldsInput = input(null, { ...(ngDevMode ? { debugName: "availableFieldsInput" } : /* istanbul ignore next */ {}), alias: 'availableFields' });
5757
+ availableTargetsInput = input(null, { ...(ngDevMode ? { debugName: "availableTargetsInput" } : /* istanbul ignore next */ {}), alias: 'availableTargets' });
4428
5758
  apply = output();
4429
5759
  save = output();
4430
5760
  resetChange = output();
@@ -4474,22 +5804,38 @@ class PraxisChartConfigEditor {
4474
5804
  normalizer = inject(ChartContractNormalizerService);
4475
5805
  validator = inject(ChartContractValidationService);
4476
5806
  previewMapper = inject(ChartEditorPreviewMapperService);
5807
+ contextResolver = inject(PraxisChartConfigEditorContextResolverService);
4477
5808
  i18n = inject(PraxisI18nService);
4478
5809
  currentDocument = signal(this.defaults.create(), ...(ngDevMode ? [{ debugName: "currentDocument" }] : /* istanbul ignore next */ []));
4479
5810
  initialDocument = signal(this.defaults.create(), ...(ngDevMode ? [{ debugName: "initialDocument" }] : /* istanbul ignore next */ []));
5811
+ resolvedResources = signal(null, ...(ngDevMode ? [{ debugName: "resolvedResources" }] : /* istanbul ignore next */ []));
5812
+ resolvedFields = signal(null, ...(ngDevMode ? [{ debugName: "resolvedFields" }] : /* istanbul ignore next */ []));
5813
+ catalogResolutionDiagnostics = signal([], ...(ngDevMode ? [{ debugName: "catalogResolutionDiagnostics" }] : /* istanbul ignore next */ []));
4480
5814
  lastExternalSignature = null;
5815
+ lastCatalogInputSignature = null;
5816
+ catalogResolutionRequestId = 0;
5817
+ baseAvailableResources = computed(() => (this.availableResourcesInput() ?? this.injectedData?.availableResources ?? []), ...(ngDevMode ? [{ debugName: "baseAvailableResources" }] : /* istanbul ignore next */ []));
5818
+ baseAvailableFields = computed(() => (this.availableFieldsInput() ?? this.injectedData?.availableFields ?? []), ...(ngDevMode ? [{ debugName: "baseAvailableFields" }] : /* istanbul ignore next */ []));
4481
5819
  normalizedDocument = computed(() => this.normalizer.normalize(this.currentDocument()), ...(ngDevMode ? [{ debugName: "normalizedDocument" }] : /* istanbul ignore next */ []));
4482
- validation = computed(() => this.validator.validate(this.normalizedDocument()), ...(ngDevMode ? [{ debugName: "validation" }] : /* istanbul ignore next */ []));
5820
+ validation = computed(() => {
5821
+ const validation = this.validator.validate(this.normalizedDocument());
5822
+ const catalogIssues = this.catalogIssues();
5823
+ return {
5824
+ valid: validation.valid && !catalogIssues.some((issue) => issue.severity === 'error'),
5825
+ issues: [
5826
+ ...validation.issues,
5827
+ ...catalogIssues,
5828
+ ],
5829
+ };
5830
+ }, ...(ngDevMode ? [{ debugName: "validation" }] : /* istanbul ignore next */ []));
4483
5831
  issues = computed(() => this.validation().issues, ...(ngDevMode ? [{ debugName: "issues" }] : /* istanbul ignore next */ []));
4484
- availableResources = computed(() => this.availableResourcesInput().length
4485
- ? this.availableResourcesInput()
4486
- : (this.injectedData?.availableResources ?? []), ...(ngDevMode ? [{ debugName: "availableResources" }] : /* istanbul ignore next */ []));
4487
- availableFields = computed(() => this.availableFieldsInput().length
4488
- ? this.availableFieldsInput()
4489
- : (this.injectedData?.availableFields ?? []), ...(ngDevMode ? [{ debugName: "availableFields" }] : /* istanbul ignore next */ []));
4490
- availableTargets = computed(() => this.availableTargetsInput().length
4491
- ? this.availableTargetsInput()
4492
- : (this.injectedData?.availableTargets ?? []), ...(ngDevMode ? [{ debugName: "availableTargets" }] : /* istanbul ignore next */ []));
5832
+ catalogIssues = computed(() => [
5833
+ ...this.validateCatalogContext(),
5834
+ ...this.catalogResolutionDiagnostics().map((diagnostic) => this.toCatalogIssue(diagnostic)),
5835
+ ], ...(ngDevMode ? [{ debugName: "catalogIssues" }] : /* istanbul ignore next */ []));
5836
+ availableResources = computed(() => this.resolvedResources() ?? this.baseAvailableResources(), ...(ngDevMode ? [{ debugName: "availableResources" }] : /* istanbul ignore next */ []));
5837
+ availableFields = computed(() => this.resolvedFields() ?? this.baseAvailableFields(), ...(ngDevMode ? [{ debugName: "availableFields" }] : /* istanbul ignore next */ []));
5838
+ availableTargets = computed(() => (this.availableTargetsInput() ?? this.injectedData?.availableTargets ?? []), ...(ngDevMode ? [{ debugName: "availableTargets" }] : /* istanbul ignore next */ []));
4493
5839
  preview = computed(() => {
4494
5840
  if (!this.validation().valid) {
4495
5841
  return null;
@@ -4497,6 +5843,21 @@ class PraxisChartConfigEditor {
4497
5843
  return this.previewMapper.build(this.normalizedDocument());
4498
5844
  }, ...(ngDevMode ? [{ debugName: "preview" }] : /* istanbul ignore next */ []));
4499
5845
  constructor() {
5846
+ effect(() => {
5847
+ const signature = JSON.stringify({
5848
+ resources: this.baseAvailableResources(),
5849
+ fields: this.baseAvailableFields(),
5850
+ });
5851
+ if (signature === this.lastCatalogInputSignature) {
5852
+ return;
5853
+ }
5854
+ this.lastCatalogInputSignature = signature;
5855
+ this.catalogResolutionRequestId += 1;
5856
+ this.resolvedResources.set(null);
5857
+ this.resolvedFields.set(null);
5858
+ this.catalogResolutionDiagnostics.set([]);
5859
+ this.isBusy$.next(false);
5860
+ });
4500
5861
  effect(() => {
4501
5862
  const externalDocument = this.documentInput()
4502
5863
  ?? this.injectedData?.chartDocument
@@ -4515,6 +5876,29 @@ class PraxisChartConfigEditor {
4515
5876
  this.lastExternalSignature = signature;
4516
5877
  this.refreshState(false);
4517
5878
  });
5879
+ effect(() => {
5880
+ const document = this.currentDocument();
5881
+ if (this.isReadonly() || document.source.kind !== 'praxis.stats') {
5882
+ return;
5883
+ }
5884
+ const resource = String(document.source.resource || '').trim();
5885
+ const option = resource
5886
+ ? this.availableResources().find((candidate) => this.resourceIdentityMatches(candidate, resource))
5887
+ : undefined;
5888
+ const canonicalPath = String(option?.path || '').trim();
5889
+ if (!canonicalPath || canonicalPath === resource) {
5890
+ return;
5891
+ }
5892
+ this.patchDocument((current) => current.source.kind === 'praxis.stats'
5893
+ ? {
5894
+ ...current,
5895
+ source: {
5896
+ ...current.source,
5897
+ resource: canonicalPath,
5898
+ },
5899
+ }
5900
+ : current);
5901
+ });
4518
5902
  }
4519
5903
  getSettingsValue() {
4520
5904
  return structuredClone(this.normalizedDocument());
@@ -4523,6 +5907,11 @@ class PraxisChartConfigEditor {
4523
5907
  return this.saveChanges().document;
4524
5908
  }
4525
5909
  reset() {
5910
+ this.catalogResolutionRequestId += 1;
5911
+ this.resolvedResources.set(null);
5912
+ this.resolvedFields.set(null);
5913
+ this.catalogResolutionDiagnostics.set([]);
5914
+ this.isBusy$.next(false);
4526
5915
  const snapshot = structuredClone(this.initialDocument());
4527
5916
  this.currentDocument.set(snapshot);
4528
5917
  this.refreshState(false);
@@ -4640,15 +6029,29 @@ class PraxisChartConfigEditor {
4640
6029
  }));
4641
6030
  }
4642
6031
  setResource(value) {
6032
+ const identity = value.trim();
6033
+ const option = this.resourceOptions().find((candidate) => (this.resourceIdentityMatches(candidate, identity)));
6034
+ const resource = String(option?.path || identity).trim();
4643
6035
  this.patchDocument((document) => ({
4644
6036
  ...document,
4645
6037
  source: document.source.kind === 'praxis.stats'
4646
6038
  ? {
4647
6039
  ...document.source,
4648
- resource: value.trim() || undefined,
6040
+ resource: resource || undefined,
4649
6041
  }
4650
6042
  : document.source,
4651
6043
  }));
6044
+ const baseCatalogAlreadyCoversSelection = !!option?.operations?.length
6045
+ && this.baseAvailableFields().some((field) => this.fieldMatchesResourceOption(field, option));
6046
+ if (!resource || !option || baseCatalogAlreadyCoversSelection) {
6047
+ this.catalogResolutionRequestId += 1;
6048
+ this.isBusy$.next(false);
6049
+ this.catalogResolutionDiagnostics.set([]);
6050
+ this.resolvedFields.set(null);
6051
+ this.refreshState(this.isDirty$.value);
6052
+ return;
6053
+ }
6054
+ void this.resolveSelectedResource(resource);
4652
6055
  }
4653
6056
  setOperation(value) {
4654
6057
  this.patchDocument((document) => ({
@@ -4695,10 +6098,18 @@ class PraxisChartConfigEditor {
4695
6098
  source: document.source.kind === 'praxis.stats'
4696
6099
  ? {
4697
6100
  ...document.source,
4698
- options: {
4699
- ...(document.source.options ?? {}),
4700
- mode: value,
4701
- },
6101
+ options: value === 'terms'
6102
+ ? {
6103
+ granularity: document.source.options?.granularity,
6104
+ fillGaps: document.source.options?.fillGaps,
6105
+ mode: value,
6106
+ orderBy: document.source.options?.orderBy,
6107
+ limit: document.source.options?.limit,
6108
+ }
6109
+ : {
6110
+ ...(document.source.options ?? {}),
6111
+ mode: value,
6112
+ },
4702
6113
  }
4703
6114
  : document.source,
4704
6115
  }));
@@ -4967,6 +6378,29 @@ class PraxisChartConfigEditor {
4967
6378
  resourceOptions() {
4968
6379
  return this.availableResources();
4969
6380
  }
6381
+ selectedResourceOption() {
6382
+ const resource = this.resourceValue();
6383
+ if (!resource) {
6384
+ return undefined;
6385
+ }
6386
+ return this.resourceOptions().find((option) => this.resourceIdentityMatches(option, resource));
6387
+ }
6388
+ operationOptions() {
6389
+ const selectedResource = this.selectedResourceOption();
6390
+ if (!selectedResource) {
6391
+ return this.resourceOptions().length ? this.operations : [];
6392
+ }
6393
+ return selectedResource.operations?.length ? selectedResource.operations : [];
6394
+ }
6395
+ resourceCatalogUnavailable() {
6396
+ return this.doc().source.kind === 'praxis.stats' && !this.resourceOptions().length;
6397
+ }
6398
+ operationCatalogUnavailable() {
6399
+ return this.doc().source.kind === 'praxis.stats'
6400
+ && !!this.resourceValue()
6401
+ && !!this.selectedResourceOption()
6402
+ && !this.operationOptions().length;
6403
+ }
4970
6404
  granularityValue() {
4971
6405
  return this.doc().source.kind === 'praxis.stats'
4972
6406
  ? (this.doc().source.options?.granularity ?? 'day')
@@ -5060,8 +6494,15 @@ class PraxisChartConfigEditor {
5060
6494
  eventTarget(eventKey) {
5061
6495
  return this.doc().events?.[eventKey]?.target ?? '';
5062
6496
  }
5063
- targetOptions() {
5064
- return this.availableTargets();
6497
+ targetOptions(action, eventKey) {
6498
+ const targets = this.availableTargets();
6499
+ return targets.filter((target) => ((!action || !target.actions?.length || target.actions.includes(action))
6500
+ && (!eventKey
6501
+ || target.events === undefined
6502
+ || target.events.includes(eventKey))));
6503
+ }
6504
+ targetCatalogUnavailable(action, eventKey) {
6505
+ return !!action && action !== 'emit' && !this.targetOptions(action, eventKey).length;
5065
6506
  }
5066
6507
  eventMappingText(eventKey) {
5067
6508
  const mapping = this.doc().events?.[eventKey]?.mapping;
@@ -5083,12 +6524,23 @@ class PraxisChartConfigEditor {
5083
6524
  if (!fields.length) {
5084
6525
  return [];
5085
6526
  }
5086
- return fields.filter((field) => {
5087
- if (!field.roles?.length) {
5088
- return role === 'dimension' ? field.aggregable !== true : true;
5089
- }
5090
- return field.roles.includes(role);
5091
- });
6527
+ return fields.filter((field) => this.fieldMatchesSelectedResource(field)
6528
+ && this.fieldMatchesOperation(field)
6529
+ && this.fieldMatchesDistributionMode(field)
6530
+ && this.fieldMatchesRole(field, role));
6531
+ }
6532
+ metricAggregationOptions(fieldName) {
6533
+ const field = this.fieldOptions('metric').find((option) => option.field === fieldName);
6534
+ if (!field?.aggregations?.length) {
6535
+ return this.metricAggregations;
6536
+ }
6537
+ return this.metricAggregations.filter((aggregation) => field.aggregations?.includes(aggregation));
6538
+ }
6539
+ fieldOptionLabel(field) {
6540
+ if (field.field === 'count') {
6541
+ return this.t('praxis.charts.editor.metricAggregation.count', 'count');
6542
+ }
6543
+ return field.label || field.field;
5092
6544
  }
5093
6545
  showMetricAxisControls() {
5094
6546
  return this.doc().kind === 'combo';
@@ -5163,6 +6615,184 @@ class PraxisChartConfigEditor {
5163
6615
  return accumulator;
5164
6616
  }, {});
5165
6617
  }
6618
+ validateCatalogContext() {
6619
+ const document = this.normalizedDocument();
6620
+ const issues = [];
6621
+ if (document.source.kind === 'praxis.stats') {
6622
+ if (!this.resourceOptions().length) {
6623
+ issues.push(this.catalogError('resource-catalog-missing', 'source.resource', this.t('praxis.charts.editor.catalog.resourceMissing', 'Resource catalog is required for governed praxis.stats authoring.')));
6624
+ }
6625
+ else if (document.source.resource && !this.selectedResourceOption()) {
6626
+ issues.push(this.catalogError('resource-stale', 'source.resource', this.t('praxis.charts.editor.catalog.resourceStale', 'Selected resource is not available in the governed resource catalog.')));
6627
+ }
6628
+ if (document.source.resource && this.selectedResourceOption() && !this.operationOptions().length) {
6629
+ issues.push(this.catalogError('operation-catalog-missing', 'source.operation', this.t('praxis.charts.editor.catalog.operationMissing', 'Selected resource does not publish authorable stats operations.')));
6630
+ }
6631
+ else if (document.source.operation
6632
+ && this.operationOptions().length
6633
+ && !this.operationOptions().includes(document.source.operation)) {
6634
+ issues.push(this.catalogError('operation-stale', 'source.operation', this.t('praxis.charts.editor.catalog.operationStale', 'Selected operation is not supported by the governed resource catalog.')));
6635
+ }
6636
+ if (document.source.resource
6637
+ && this.selectedResourceOption()
6638
+ && this.operationOptions().length
6639
+ && !this.availableFields().length) {
6640
+ issues.push(this.catalogError('field-catalog-missing', 'dimensions', this.t('praxis.charts.editor.catalog.fieldCatalogMissing', 'The selected resource does not publish governed fields for chart authoring.')));
6641
+ }
6642
+ }
6643
+ if (this.availableFields().length) {
6644
+ document.dimensions?.forEach((dimension, index) => {
6645
+ if (!dimension.field || this.fieldOptions('dimension').some((field) => field.field === dimension.field)) {
6646
+ return;
6647
+ }
6648
+ issues.push(this.catalogError('dimension-field-stale', `dimensions[${index}].field`, this.t('praxis.charts.editor.catalog.dimensionFieldStale', 'Selected dimension is not eligible for the current resource and operation.')));
6649
+ });
6650
+ document.metrics?.forEach((metric, index) => {
6651
+ const metricField = metric.field;
6652
+ if (metricField && !this.fieldOptions('metric').some((field) => field.field === metricField)) {
6653
+ issues.push(this.catalogError('metric-field-stale', `metrics[${index}].field`, this.t('praxis.charts.editor.catalog.metricFieldStale', 'Selected metric is not eligible for the current resource and operation.')));
6654
+ return;
6655
+ }
6656
+ const aggregationOptions = this.metricAggregationOptions(metricField);
6657
+ if (metric.aggregation && !aggregationOptions.includes(metric.aggregation)) {
6658
+ issues.push(this.catalogError('metric-aggregation-stale', `metrics[${index}].aggregation`, this.t('praxis.charts.editor.catalog.metricAggregationStale', 'Selected aggregation is not allowed for the selected metric field.')));
6659
+ }
6660
+ });
6661
+ }
6662
+ for (const eventKey of ['pointClick', 'selectionChange', 'drillDown', 'crossFilter']) {
6663
+ const eventAction = document.events?.[eventKey];
6664
+ if (!eventAction?.action || eventAction.action === 'emit') {
6665
+ continue;
6666
+ }
6667
+ const targetOptions = this.targetOptions(eventAction.action, eventKey);
6668
+ if (!targetOptions.length) {
6669
+ issues.push(this.catalogError('target-catalog-missing', `events.${eventKey}.target`, this.t('praxis.charts.editor.catalog.targetMissing', 'Target catalog is required for this governed event action.')));
6670
+ }
6671
+ else if (eventAction.target && !targetOptions.some((target) => target.id === eventAction.target)) {
6672
+ issues.push(this.catalogError('target-stale', `events.${eventKey}.target`, this.t('praxis.charts.editor.catalog.targetStale', 'Selected target is not available for this event action.')));
6673
+ }
6674
+ }
6675
+ return issues;
6676
+ }
6677
+ fieldMatchesSelectedResource(field) {
6678
+ const resource = this.selectedResourceOption();
6679
+ if (!resource) {
6680
+ return true;
6681
+ }
6682
+ return this.fieldMatchesResourceOption(field, resource);
6683
+ }
6684
+ fieldMatchesResourceOption(field, resource) {
6685
+ const resourceIds = field.resourceIds ?? [];
6686
+ const resourcePaths = field.resourcePaths ?? [];
6687
+ if (!resourceIds.length && !resourcePaths.length) {
6688
+ return true;
6689
+ }
6690
+ return resourceIds.includes(resource.id)
6691
+ || resourcePaths.some((path) => this.normalizeResourcePath(path) === this.normalizeResourcePath(resource.path));
6692
+ }
6693
+ async resolveSelectedResource(resource) {
6694
+ const requestId = ++this.catalogResolutionRequestId;
6695
+ this.resolvedFields.set([]);
6696
+ this.catalogResolutionDiagnostics.set([]);
6697
+ this.isBusy$.next(true);
6698
+ this.refreshState(this.isDirty$.value);
6699
+ try {
6700
+ const resolution = await this.contextResolver.resolveResource(resource);
6701
+ if (requestId !== this.catalogResolutionRequestId) {
6702
+ return;
6703
+ }
6704
+ if (resolution.resource) {
6705
+ const currentResources = this.resolvedResources() ?? this.baseAvailableResources();
6706
+ const matchIndex = currentResources.findIndex((candidate) => (candidate.id === resolution.resource?.id
6707
+ || this.resourceIdentityMatches(candidate, resource)));
6708
+ this.resolvedResources.set(matchIndex >= 0
6709
+ ? currentResources.map((candidate, index) => index === matchIndex ? resolution.resource : candidate)
6710
+ : [...currentResources, resolution.resource]);
6711
+ }
6712
+ this.resolvedFields.set(resolution.fields);
6713
+ this.catalogResolutionDiagnostics.set(resolution.diagnostics);
6714
+ }
6715
+ catch {
6716
+ if (requestId !== this.catalogResolutionRequestId) {
6717
+ return;
6718
+ }
6719
+ this.resolvedFields.set([]);
6720
+ this.catalogResolutionDiagnostics.set([{
6721
+ code: 'chart-resource-capabilities-unavailable',
6722
+ severity: 'error',
6723
+ path: 'chartDocument.source.resource',
6724
+ }]);
6725
+ }
6726
+ finally {
6727
+ if (requestId === this.catalogResolutionRequestId) {
6728
+ this.isBusy$.next(false);
6729
+ this.refreshState(this.isDirty$.value);
6730
+ }
6731
+ }
6732
+ }
6733
+ resourceIdentityMatches(option, identity) {
6734
+ const normalizedIdentity = this.normalizeResourcePath(identity);
6735
+ return option.id === identity
6736
+ || this.normalizeResourcePath(option.path) === normalizedIdentity;
6737
+ }
6738
+ normalizeResourcePath(value) {
6739
+ return String(value || '').trim().replace(/^\/+|\/+$/g, '');
6740
+ }
6741
+ toCatalogIssue(diagnostic) {
6742
+ return {
6743
+ severity: diagnostic.severity === 'error' ? 'error' : 'warning',
6744
+ code: diagnostic.code,
6745
+ field: diagnostic.path || 'catalog',
6746
+ message: this.catalogDiagnosticMessage(diagnostic.code),
6747
+ };
6748
+ }
6749
+ catalogDiagnosticMessage(code) {
6750
+ switch (code) {
6751
+ case 'chart-resource-capabilities-denied':
6752
+ return this.t('praxis.charts.editor.catalog.capabilitiesDenied', 'Access to the selected resource capabilities was denied.');
6753
+ case 'chart-resource-capabilities-unavailable':
6754
+ return this.t('praxis.charts.editor.catalog.capabilitiesUnavailable', 'The selected resource capabilities are temporarily unavailable.');
6755
+ case 'chart-resource-catalog-selection-stale':
6756
+ return this.t('praxis.charts.editor.catalog.resourceStale', 'Selected resource is not available in the governed resource catalog.');
6757
+ case 'chart-resource-stats-fields-empty':
6758
+ return this.t('praxis.charts.editor.catalog.fieldCatalogMissing', 'The selected resource does not publish governed fields for chart authoring.');
6759
+ case 'chart-resource-stats-operations-empty':
6760
+ return this.t('praxis.charts.editor.catalog.operationMissing', 'Selected resource does not publish authorable stats operations.');
6761
+ default:
6762
+ return this.t('praxis.charts.editor.catalog.contextUnavailable', 'Governed chart authoring context could not be resolved.');
6763
+ }
6764
+ }
6765
+ fieldMatchesOperation(field) {
6766
+ const operation = this.doc().source.kind === 'praxis.stats' ? this.doc().source.operation : undefined;
6767
+ if (!operation || !field.operations?.length) {
6768
+ return true;
6769
+ }
6770
+ return field.operations.includes(operation);
6771
+ }
6772
+ fieldMatchesDistributionMode(field) {
6773
+ const source = this.doc().source;
6774
+ if (source.kind !== 'praxis.stats' || source.operation !== 'distribution') {
6775
+ return true;
6776
+ }
6777
+ if (!field.distributionModes?.length) {
6778
+ return true;
6779
+ }
6780
+ return field.distributionModes.includes(source.options?.mode ?? 'terms');
6781
+ }
6782
+ fieldMatchesRole(field, role) {
6783
+ if (!field.roles?.length) {
6784
+ return role === 'dimension' ? field.aggregable !== true : true;
6785
+ }
6786
+ return field.roles.includes(role);
6787
+ }
6788
+ catalogError(code, field, message) {
6789
+ return {
6790
+ severity: 'error',
6791
+ code,
6792
+ field,
6793
+ message,
6794
+ };
6795
+ }
5166
6796
  normalizeSizingSizeInput(value) {
5167
6797
  const trimmed = value.trim();
5168
6798
  if (!trimmed) {
@@ -5206,7 +6836,7 @@ class PraxisChartConfigEditor {
5206
6836
  this.documentChange.emit(structuredClone(this.normalizedDocument()));
5207
6837
  }
5208
6838
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: PraxisChartConfigEditor, deps: [], target: i0.ɵɵFactoryTarget.Component });
5209
- 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 [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 [ngModel]=\"doc().source.kind\" (ngModelChange)=\"setSourceKind($event)\" [disabled]=\"isReadonly()\">\n @for (sourceKind of sourceKinds; track sourceKind) {\n <mat-option [value]=\"sourceKind\">\n {{ t('praxis.charts.editor.sourceKind.' + (sourceKind === 'praxis.stats' ? 'praxisStats' : 'derived'), sourceKind) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n @if (doc().source.kind === 'praxis.stats') {\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.resource', 'Resource') }}</mat-label>\n @if (resourceOptions().length) {\n <mat-select [ngModel]=\"resourceValue()\" (ngModelChange)=\"setResource($event)\" [disabled]=\"isReadonly()\">\n @for (resource of resourceOptions(); track resource.id) {\n <mat-option [value]=\"resource.path\">{{ resource.label }}</mat-option>\n }\n </mat-select>\n } @else {\n <input matInput [ngModel]=\"resourceValue()\" (ngModelChange)=\"setResource($event)\" [disabled]=\"isReadonly()\" />\n }\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.operation', 'Operation') }}</mat-label>\n <mat-select\n [ngModel]=\"doc().source.operation || 'group-by'\"\n (ngModelChange)=\"setOperation($event)\"\n [disabled]=\"isReadonly()\"\n >\n @for (operation of operations; track operation) {\n <mat-option [value]=\"operation\">\n {{ t('praxis.charts.editor.operation.' + (operation === 'group-by' ? 'groupBy' : operation), operation) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n }\n </div>\n\n @if (showTimeseriesControls()) {\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.specialization.timeseriesTitle', 'Timeseries options') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.granularity', 'Granularity') }}</mat-label>\n <mat-select [ngModel]=\"granularityValue()\" (ngModelChange)=\"setGranularity($event)\" [disabled]=\"isReadonly()\">\n @for (granularity of timeGranularities; track granularity) {\n <mat-option [value]=\"granularity\">\n {{ t('praxis.charts.editor.granularity.' + granularity, granularity) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n <mat-slide-toggle\n [ngModel]=\"fillGapsValue()\"\n (ngModelChange)=\"setFillGaps($event)\"\n [disabled]=\"isReadonly()\"\n >\n {{ t('praxis.charts.editor.field.fillGaps', 'Fill missing intervals') }}\n </mat-slide-toggle>\n </mat-card-content>\n </mat-card>\n }\n\n @if (showDistributionControls()) {\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.specialization.distributionTitle', 'Distribution options') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.distributionMode', 'Distribution mode') }}</mat-label>\n <mat-select [ngModel]=\"distributionModeValue()\" (ngModelChange)=\"setDistributionMode($event)\" [disabled]=\"isReadonly()\">\n @for (mode of distributionModes; track mode) {\n <mat-option [value]=\"mode\">\n {{ t('praxis.charts.editor.distributionMode.' + mode, mode) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.bucketSize', 'Bucket size') }}</mat-label>\n <input matInput [ngModel]=\"bucketSizeValue()\" (ngModelChange)=\"setBucketSize($event)\" [disabled]=\"isReadonly()\" />\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.bucketCount', 'Bucket count') }}</mat-label>\n <input matInput [ngModel]=\"bucketCountValue()\" (ngModelChange)=\"setBucketCount($event)\" [disabled]=\"isReadonly()\" />\n </mat-form-field>\n </mat-card-content>\n </mat-card>\n }\n </div>\n }\n\n @case ('motion') {\n <div class=\"editor-grid\">\n <mat-slide-toggle\n [ngModel]=\"normalizedDocument().motion?.enabled !== false\"\n (ngModelChange)=\"setMotionEnabled($event)\"\n [disabled]=\"isReadonly()\"\n >\n {{ t('praxis.charts.editor.field.motionEnabled', 'Enable animations') }}\n </mat-slide-toggle>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.motionPreset', 'Motion preset') }}</mat-label>\n <mat-select\n [ngModel]=\"normalizedDocument().motion?.preset || 'standard'\"\n (ngModelChange)=\"setMotionPreset($event)\"\n [disabled]=\"isReadonly() || normalizedDocument().motion?.enabled === false\"\n >\n @for (preset of motionPresets; track preset) {\n <mat-option [value]=\"preset\">\n {{ t('praxis.charts.editor.motionPreset.' + preset, preset) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n </div>\n }\n\n @case ('appearance') {\n <div class=\"editor-stack\">\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.appearance.featuresTitle', 'Display features') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-grid\">\n <mat-slide-toggle\n [ngModel]=\"featureEnabled('legend')\"\n (ngModelChange)=\"setFeatureEnabled('legend', $event)\"\n [disabled]=\"isReadonly()\"\n >\n {{ t('praxis.charts.editor.field.legendEnabled', 'Show legend') }}\n </mat-slide-toggle>\n\n <mat-slide-toggle\n [ngModel]=\"featureEnabled('labels')\"\n (ngModelChange)=\"setFeatureEnabled('labels', $event)\"\n [disabled]=\"isReadonly()\"\n >\n {{ t('praxis.charts.editor.field.labelsEnabled', 'Show labels') }}\n </mat-slide-toggle>\n\n <mat-slide-toggle\n [ngModel]=\"featureEnabled('tooltip')\"\n (ngModelChange)=\"setFeatureEnabled('tooltip', $event)\"\n [disabled]=\"isReadonly()\"\n >\n {{ t('praxis.charts.editor.field.tooltipEnabled', 'Show tooltip') }}\n </mat-slide-toggle>\n </mat-card-content>\n </mat-card>\n\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.appearance.paletteTitle', 'Palette') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-stack\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.themeVariant', 'Theme variant') }}</mat-label>\n <mat-select [ngModel]=\"themeVariantValue()\" (ngModelChange)=\"setThemeVariant($event)\" [disabled]=\"isReadonly()\">\n <mat-option [value]=\"''\">\n {{ t('praxis.charts.editor.themeVariant.none', 'No variant') }}\n </mat-option>\n @for (variant of themeVariants; track variant) {\n <mat-option [value]=\"variant\">\n {{ t('praxis.charts.editor.themeVariant.' + variant, variant) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.paletteMode', 'Palette mode') }}</mat-label>\n <mat-select [ngModel]=\"paletteModeValue()\" (ngModelChange)=\"setPaletteMode($event)\" [disabled]=\"isReadonly()\">\n @for (mode of paletteModes; track mode) {\n <mat-option [value]=\"mode\">\n {{ t('praxis.charts.editor.paletteMode.' + mode, mode) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n @if (paletteModeValue() === 'token') {\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.paletteToken', 'Palette token') }}</mat-label>\n <mat-select [ngModel]=\"paletteTokenValue()\" (ngModelChange)=\"setPaletteToken($event)\" [disabled]=\"isReadonly()\">\n @for (token of paletteTokens; track token) {\n <mat-option [value]=\"token\">\n {{ t('praxis.charts.editor.paletteToken.' + token, token) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n } @else {\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.palette', 'Palette colors') }}</mat-label>\n <textarea\n matInput\n rows=\"3\"\n [ngModel]=\"paletteValue()\"\n (ngModelChange)=\"setPalette($event)\"\n [disabled]=\"isReadonly()\"\n ></textarea>\n </mat-form-field>\n }\n\n <p class=\"editor-caption\">\n {{ t('praxis.charts.editor.appearance.paletteHint', 'Use a registered token or comma-separated colors to persist theme.palette in the canonical contract.') }}\n </p>\n </mat-card-content>\n </mat-card>\n\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.appearance.surfaceTitle', 'Surface') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content>\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.surfaceMode', 'Surface mode') }}</mat-label>\n <mat-select [ngModel]=\"surfaceModeValue()\" (ngModelChange)=\"setSurfaceMode($event)\" [disabled]=\"isReadonly()\">\n @for (mode of surfaceModes; track mode) {\n <mat-option [value]=\"mode\">\n {{ t('praxis.charts.editor.surface.' + mode, mode) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n <p class=\"editor-caption\">\n {{ t('praxis.charts.editor.appearance.surfaceHint', 'Use embedded for dashboard widgets and contained only when the chart must own its visual surface.') }}\n </p>\n </mat-card-content>\n </mat-card>\n\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.appearance.statesTitle', 'State messages') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-stack\">\n <div class=\"editor-row-card\">\n <div class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.emptyTitle', 'Empty title') }}</mat-label>\n <input matInput [ngModel]=\"stateTitle('empty')\" (ngModelChange)=\"setStateTitle('empty', $event)\" [disabled]=\"isReadonly()\" />\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.emptyDescription', 'Empty description') }}</mat-label>\n <textarea matInput rows=\"2\" [ngModel]=\"stateDescription('empty')\" (ngModelChange)=\"setStateDescription('empty', $event)\" [disabled]=\"isReadonly()\"></textarea>\n </mat-form-field>\n </div>\n </div>\n\n <div class=\"editor-row-card\">\n <div class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.loadingTitle', 'Loading title') }}</mat-label>\n <input matInput [ngModel]=\"stateTitle('loading')\" (ngModelChange)=\"setStateTitle('loading', $event)\" [disabled]=\"isReadonly()\" />\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.loadingDescription', 'Loading description') }}</mat-label>\n <textarea matInput rows=\"2\" [ngModel]=\"stateDescription('loading')\" (ngModelChange)=\"setStateDescription('loading', $event)\" [disabled]=\"isReadonly()\"></textarea>\n </mat-form-field>\n </div>\n </div>\n\n <div class=\"editor-row-card\">\n <div class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.errorTitle', 'Error title') }}</mat-label>\n <input matInput [ngModel]=\"stateTitle('error')\" (ngModelChange)=\"setStateTitle('error', $event)\" [disabled]=\"isReadonly()\" />\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.errorDescription', 'Error description') }}</mat-label>\n <textarea matInput rows=\"2\" [ngModel]=\"stateDescription('error')\" (ngModelChange)=\"setStateDescription('error', $event)\" [disabled]=\"isReadonly()\"></textarea>\n </mat-form-field>\n </div>\n </div>\n </mat-card-content>\n </mat-card>\n </div>\n }\n\n @case ('analytics') {\n <div class=\"editor-stack\">\n @if (showComboPanel()) {\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.specialization.comboTitle', 'Combo guidance') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content>\n <p class=\"editor-caption\">\n {{ t('praxis.charts.editor.specialization.comboHint', 'Combo charts require at least two metrics and allow per-metric axis and series kind mapping.') }}\n </p>\n </mat-card-content>\n </mat-card>\n }\n\n @if (showPieDonutPanel()) {\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.specialization.pieDonutTitle', 'Composition guidance') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content>\n <p class=\"editor-caption\">\n {{ t('praxis.charts.editor.specialization.pieDonutHint', 'Pie and donut charts keep only the first metric and use the first dimension as the category segment.') }}\n </p>\n </mat-card-content>\n </mat-card>\n }\n\n @if (showScatterPanel()) {\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.specialization.scatterTitle', 'Scatter guidance') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content>\n <p class=\"editor-caption\">\n {{ t('praxis.charts.editor.specialization.scatterHint', 'Scatter charts use the first dimension as X and the first metric as Y, so keep both fields mapped.') }}\n </p>\n </mat-card-content>\n </mat-card>\n }\n\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.analytics.dimensionsTitle', 'Dimensions') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-stack\">\n @for (dimension of dimensions(); track $index) {\n <div class=\"editor-row-card\" [attr.data-testid]=\"'chart-editor-dimension-row-' + $index\">\n <div class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\" [attr.data-testid]=\"'chart-editor-dimension-field-' + $index\">\n <mat-label>{{ t('praxis.charts.editor.field.dimension', 'Dimension') }}</mat-label>\n @if (fieldOptions('dimension').length) {\n <mat-select [ngModel]=\"dimension.field || ''\" (ngModelChange)=\"setDimensionField($index, $event)\" [disabled]=\"isReadonly()\">\n @for (field of fieldOptions('dimension'); track field.field) {\n <mat-option [value]=\"field.field\">{{ field.label || field.field }}</mat-option>\n }\n </mat-select>\n } @else {\n <input matInput [ngModel]=\"dimension.field || ''\" (ngModelChange)=\"setDimensionField($index, $event)\" [disabled]=\"isReadonly()\" />\n }\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\" [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\">{{ field.label || field.field }}</mat-option>\n }\n </mat-select>\n } @else {\n <input matInput [ngModel]=\"metric.field || ''\" (ngModelChange)=\"setMetricField($index, $event)\" [disabled]=\"isReadonly()\" />\n }\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\" [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 metricAggregations; track aggregation) {\n <mat-option [value]=\"aggregation\">\n {{ t('praxis.charts.editor.metricAggregation.' + aggregation, aggregation) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n @if (showMetricAxisControls()) {\n <mat-form-field class=\"editor-field\" appearance=\"outline\" [attr.data-testid]=\"'chart-editor-metric-axis-' + $index\">\n <mat-label>{{ t('praxis.charts.editor.field.metricAxis', 'Axis') }}</mat-label>\n <mat-select [ngModel]=\"metric.axis || 'primary'\" (ngModelChange)=\"setMetricAxis($index, $event)\" [disabled]=\"isReadonly()\">\n @for (axis of metricAxes; track axis) {\n <mat-option [value]=\"axis\">\n {{ t('praxis.charts.editor.metricAxis.' + axis, axis) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n }\n\n @if (showMetricSeriesKindControls()) {\n <mat-form-field class=\"editor-field\" appearance=\"outline\" [attr.data-testid]=\"'chart-editor-metric-series-kind-' + $index\">\n <mat-label>{{ t('praxis.charts.editor.field.metricSeriesKind', 'Series kind') }}</mat-label>\n <mat-select [ngModel]=\"metric.seriesKind || 'bar'\" (ngModelChange)=\"setMetricSeriesKind($index, $event)\" [disabled]=\"isReadonly()\">\n @for (seriesKind of metricSeriesKinds; track seriesKind) {\n <mat-option [value]=\"seriesKind\">\n {{ t('praxis.charts.editor.metricSeriesKind.' + seriesKind, seriesKind) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n }\n </div>\n\n <div class=\"editor-row-actions\">\n <button mat-button type=\"button\" [attr.data-testid]=\"'chart-editor-remove-metric-' + $index\" (click)=\"removeMetric($index)\" [disabled]=\"isReadonly() || metrics().length <= 1\">\n {{ t('praxis.charts.editor.analytics.removeMetric', 'Remove metric') }}\n </button>\n </div>\n </div>\n }\n\n <div>\n <button mat-stroked-button type=\"button\" data-testid=\"chart-editor-add-metric\" (click)=\"addMetric()\" [disabled]=\"isReadonly()\">\n {{ t('praxis.charts.editor.analytics.addMetric', 'Add metric') }}\n </button>\n </div>\n </mat-card-content>\n </mat-card>\n </div>\n }\n\n @case ('events') {\n <div class=\"editor-stack\">\n <mat-card class=\"editor-card\" data-testid=\"chart-editor-event-pointClick\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.events.pointClickTitle', 'Point click') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventAction', 'Action') }}</mat-label>\n <mat-select data-testid=\"chart-editor-event-pointClick-action\" [ngModel]=\"eventAction('pointClick')\" (ngModelChange)=\"setEventAction('pointClick', $event)\" [disabled]=\"isReadonly()\">\n <mat-option value=\"\">{{ t('praxis.charts.editor.events.none', 'None') }}</mat-option>\n @for (action of eventActionOptions; track action) {\n <mat-option [value]=\"action\">\n {{ t('praxis.charts.editor.eventAction.' + action, action) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventTarget', 'Target') }}</mat-label>\n @if (targetOptions().length) {\n <mat-select data-testid=\"chart-editor-event-pointClick-target\" [ngModel]=\"eventTarget('pointClick')\" (ngModelChange)=\"setEventTarget('pointClick', $event)\" [disabled]=\"isReadonly() || !eventAction('pointClick')\">\n @for (target of targetOptions(); track target.id) {\n <mat-option [value]=\"target.id\">{{ target.label }}</mat-option>\n }\n </mat-select>\n } @else {\n <input matInput data-testid=\"chart-editor-event-pointClick-target\" [ngModel]=\"eventTarget('pointClick')\" (ngModelChange)=\"setEventTarget('pointClick', $event)\" [disabled]=\"isReadonly() || !eventAction('pointClick')\" />\n }\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventMapping', 'Mapping') }}</mat-label>\n <textarea\n matInput\n 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 @if (targetOptions().length) {\n <mat-select data-testid=\"chart-editor-event-selectionChange-target\" [ngModel]=\"eventTarget('selectionChange')\" (ngModelChange)=\"setEventTarget('selectionChange', $event)\" [disabled]=\"isReadonly() || !eventAction('selectionChange')\">\n @for (target of targetOptions(); track target.id) {\n <mat-option [value]=\"target.id\">{{ target.label }}</mat-option>\n }\n </mat-select>\n } @else {\n <input matInput data-testid=\"chart-editor-event-selectionChange-target\" [ngModel]=\"eventTarget('selectionChange')\" (ngModelChange)=\"setEventTarget('selectionChange', $event)\" [disabled]=\"isReadonly() || !eventAction('selectionChange')\" />\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 @if (targetOptions().length) {\n <mat-select data-testid=\"chart-editor-event-drillDown-target\" [ngModel]=\"eventTarget('drillDown')\" (ngModelChange)=\"setEventTarget('drillDown', $event)\" [disabled]=\"isReadonly() || !eventAction('drillDown')\">\n @for (target of targetOptions(); track target.id) {\n <mat-option [value]=\"target.id\">{{ target.label }}</mat-option>\n }\n </mat-select>\n } @else {\n <input matInput data-testid=\"chart-editor-event-drillDown-target\" [ngModel]=\"eventTarget('drillDown')\" (ngModelChange)=\"setEventTarget('drillDown', $event)\" [disabled]=\"isReadonly() || !eventAction('drillDown')\" />\n }\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventMapping', 'Mapping') }}</mat-label>\n <textarea\n matInput\n 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 @if (targetOptions().length) {\n <mat-select data-testid=\"chart-editor-event-crossFilter-target\" [ngModel]=\"eventTarget('crossFilter')\" (ngModelChange)=\"setEventTarget('crossFilter', $event)\" [disabled]=\"isReadonly() || !eventAction('crossFilter')\">\n @for (target of targetOptions(); track target.id) {\n <mat-option [value]=\"target.id\">{{ target.label }}</mat-option>\n }\n </mat-select>\n } @else {\n <input matInput data-testid=\"chart-editor-event-crossFilter-target\" [ngModel]=\"eventTarget('crossFilter')\" (ngModelChange)=\"setEventTarget('crossFilter', $event)\" [disabled]=\"isReadonly() || !eventAction('crossFilter')\" />\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;color:var(--md-sys-color-on-surface, #1a1b20)}.editor-shell{display:grid;gap:18px}.editor-nav{display:flex;gap:8px;flex-wrap:wrap}.editor-nav button.active{background:color-mix(in srgb,var(--md-sys-color-primary, #1263b4) 18%,transparent);color:var(--md-sys-color-primary, #1263b4)}.editor-layout{display:grid;gap:18px;grid-template-columns:minmax(0,1.35fr) minmax(320px,.9fr);align-items:start}.editor-form,.editor-side{display:grid;gap:16px}.editor-card{border-radius:20px;border:1px solid color-mix(in srgb,var(--md-sys-color-outline, #c5c7ce) 72%,transparent);background:linear-gradient(180deg,#1263b408,#1263b400)}.editor-grid{display:grid;gap:14px;grid-template-columns:repeat(auto-fit,minmax(220px,1fr))}.editor-stack{display:grid;gap:14px}.editor-row-card{padding:14px;border-radius:16px;border:1px solid color-mix(in srgb,var(--md-sys-color-outline, #c5c7ce) 54%,transparent);background:color-mix(in srgb,var(--md-sys-color-surface, #fff) 92%,rgba(18,99,180,.04))}.editor-row-actions{display:flex;justify-content:flex-end}.editor-field{width:100%}.editor-issues{display:grid;gap:10px;margin:0;padding:0;list-style:none}.editor-issue{padding:12px 14px;border-radius:14px;background:color-mix(in srgb,var(--md-sys-color-error, #b3261e) 8%,transparent);border:1px solid color-mix(in srgb,var(--md-sys-color-error, #b3261e) 18%,transparent)}.editor-issue strong{display:block;margin-bottom:4px}.editor-caption{margin:0 0 12px;color:var(--md-sys-color-on-surface-variant, #5a5d67);font-size:.92rem}.editor-empty{padding:18px;border-radius:14px;background:color-mix(in srgb,var(--md-sys-color-surface-variant, #eceff4) 78%,transparent)}@media(max-width:960px){.editor-layout{grid-template-columns:minmax(0,1fr)}}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$3.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$3.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$3.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i1$2.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatCardModule }, { kind: "component", type: i3$1.MatCard, selector: "mat-card", inputs: ["appearance"], exportAs: ["matCard"] }, { kind: "directive", type: i3$1.MatCardContent, selector: "mat-card-content" }, { kind: "component", type: i3$1.MatCardHeader, selector: "mat-card-header" }, { kind: "directive", type: i3$1.MatCardTitle, selector: "mat-card-title, [mat-card-title], [matCardTitle]" }, { kind: "ngmodule", type: MatFormFieldModule }, { kind: "component", type: i4.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i4.MatLabel, selector: "mat-label" }, { kind: "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", "selectionChange", "crossFilter", "queryRequest", "loadStateChange", "chartDocumentApplied", "chartDocumentSaved"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
6839
+ 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 </div>\n }\n\n @case ('motion') {\n <div class=\"editor-grid\">\n <mat-slide-toggle\n [ngModel]=\"normalizedDocument().motion?.enabled !== false\"\n (ngModelChange)=\"setMotionEnabled($event)\"\n [disabled]=\"isReadonly()\"\n >\n {{ t('praxis.charts.editor.field.motionEnabled', 'Enable animations') }}\n </mat-slide-toggle>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.motionPreset', 'Motion preset') }}</mat-label>\n <mat-select\n [ngModel]=\"normalizedDocument().motion?.preset || 'standard'\"\n (ngModelChange)=\"setMotionPreset($event)\"\n [disabled]=\"isReadonly() || normalizedDocument().motion?.enabled === false\"\n >\n @for (preset of motionPresets; track preset) {\n <mat-option [value]=\"preset\">\n {{ t('praxis.charts.editor.motionPreset.' + preset, preset) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n </div>\n }\n\n @case ('appearance') {\n <div class=\"editor-stack\">\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.appearance.featuresTitle', 'Display features') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-grid\">\n <mat-slide-toggle\n [ngModel]=\"featureEnabled('legend')\"\n (ngModelChange)=\"setFeatureEnabled('legend', $event)\"\n [disabled]=\"isReadonly()\"\n >\n {{ t('praxis.charts.editor.field.legendEnabled', 'Show legend') }}\n </mat-slide-toggle>\n\n <mat-slide-toggle\n [ngModel]=\"featureEnabled('labels')\"\n (ngModelChange)=\"setFeatureEnabled('labels', $event)\"\n [disabled]=\"isReadonly()\"\n >\n {{ t('praxis.charts.editor.field.labelsEnabled', 'Show labels') }}\n </mat-slide-toggle>\n\n <mat-slide-toggle\n [ngModel]=\"featureEnabled('tooltip')\"\n (ngModelChange)=\"setFeatureEnabled('tooltip', $event)\"\n [disabled]=\"isReadonly()\"\n >\n {{ t('praxis.charts.editor.field.tooltipEnabled', 'Show tooltip') }}\n </mat-slide-toggle>\n </mat-card-content>\n </mat-card>\n\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.appearance.paletteTitle', 'Palette') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-stack\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.themeVariant', 'Theme variant') }}</mat-label>\n <mat-select [ngModel]=\"themeVariantValue()\" (ngModelChange)=\"setThemeVariant($event)\" [disabled]=\"isReadonly()\">\n <mat-option [value]=\"''\">\n {{ t('praxis.charts.editor.themeVariant.none', 'No variant') }}\n </mat-option>\n @for (variant of themeVariants; track variant) {\n <mat-option [value]=\"variant\">\n {{ t('praxis.charts.editor.themeVariant.' + variant, variant) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.paletteMode', 'Palette mode') }}</mat-label>\n <mat-select [ngModel]=\"paletteModeValue()\" (ngModelChange)=\"setPaletteMode($event)\" [disabled]=\"isReadonly()\">\n @for (mode of paletteModes; track mode) {\n <mat-option [value]=\"mode\">\n {{ t('praxis.charts.editor.paletteMode.' + mode, mode) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n @if (paletteModeValue() === 'token') {\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.paletteToken', 'Palette token') }}</mat-label>\n <mat-select [ngModel]=\"paletteTokenValue()\" (ngModelChange)=\"setPaletteToken($event)\" [disabled]=\"isReadonly()\">\n @for (token of paletteTokens; track token) {\n <mat-option [value]=\"token\">\n {{ t('praxis.charts.editor.paletteToken.' + token, token) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n } @else {\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.palette', 'Palette colors') }}</mat-label>\n <textarea\n matInput\n rows=\"3\"\n [ngModel]=\"paletteValue()\"\n (ngModelChange)=\"setPalette($event)\"\n [disabled]=\"isReadonly()\"\n ></textarea>\n </mat-form-field>\n }\n\n <p class=\"editor-caption\">\n {{ t('praxis.charts.editor.appearance.paletteHint', 'Use a registered token or comma-separated colors to persist theme.palette in the canonical contract.') }}\n </p>\n </mat-card-content>\n </mat-card>\n\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.appearance.surfaceTitle', 'Surface') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content>\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.surfaceMode', 'Surface mode') }}</mat-label>\n <mat-select [ngModel]=\"surfaceModeValue()\" (ngModelChange)=\"setSurfaceMode($event)\" [disabled]=\"isReadonly()\">\n @for (mode of surfaceModes; track mode) {\n <mat-option [value]=\"mode\">\n {{ t('praxis.charts.editor.surface.' + mode, mode) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n <p class=\"editor-caption\">\n {{ t('praxis.charts.editor.appearance.surfaceHint', 'Use embedded for dashboard widgets and contained only when the chart must own its visual surface.') }}\n </p>\n </mat-card-content>\n </mat-card>\n\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.appearance.statesTitle', 'State messages') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-stack\">\n <div class=\"editor-row-card\">\n <div class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.emptyTitle', 'Empty title') }}</mat-label>\n <input matInput [ngModel]=\"stateTitle('empty')\" (ngModelChange)=\"setStateTitle('empty', $event)\" [disabled]=\"isReadonly()\" />\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.emptyDescription', 'Empty description') }}</mat-label>\n <textarea matInput rows=\"2\" [ngModel]=\"stateDescription('empty')\" (ngModelChange)=\"setStateDescription('empty', $event)\" [disabled]=\"isReadonly()\"></textarea>\n </mat-form-field>\n </div>\n </div>\n\n <div class=\"editor-row-card\">\n <div class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.loadingTitle', 'Loading title') }}</mat-label>\n <input matInput [ngModel]=\"stateTitle('loading')\" (ngModelChange)=\"setStateTitle('loading', $event)\" [disabled]=\"isReadonly()\" />\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.loadingDescription', 'Loading description') }}</mat-label>\n <textarea matInput rows=\"2\" [ngModel]=\"stateDescription('loading')\" (ngModelChange)=\"setStateDescription('loading', $event)\" [disabled]=\"isReadonly()\"></textarea>\n </mat-form-field>\n </div>\n </div>\n\n <div class=\"editor-row-card\">\n <div class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.errorTitle', 'Error title') }}</mat-label>\n <input matInput [ngModel]=\"stateTitle('error')\" (ngModelChange)=\"setStateTitle('error', $event)\" [disabled]=\"isReadonly()\" />\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.errorDescription', 'Error description') }}</mat-label>\n <textarea matInput rows=\"2\" [ngModel]=\"stateDescription('error')\" (ngModelChange)=\"setStateDescription('error', $event)\" [disabled]=\"isReadonly()\"></textarea>\n </mat-form-field>\n </div>\n </div>\n </mat-card-content>\n </mat-card>\n </div>\n }\n\n @case ('analytics') {\n <div class=\"editor-stack\">\n @if (showComboPanel()) {\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.specialization.comboTitle', 'Combo guidance') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content>\n <p class=\"editor-caption\">\n {{ t('praxis.charts.editor.specialization.comboHint', 'Combo charts require at least two metrics and allow per-metric axis and series kind mapping.') }}\n </p>\n </mat-card-content>\n </mat-card>\n }\n\n @if (showPieDonutPanel()) {\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.specialization.pieDonutTitle', 'Composition guidance') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content>\n <p class=\"editor-caption\">\n {{ t('praxis.charts.editor.specialization.pieDonutHint', 'Pie and donut charts keep only the first metric and use the first dimension as the category segment.') }}\n </p>\n </mat-card-content>\n </mat-card>\n }\n\n @if (showScatterPanel()) {\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.specialization.scatterTitle', 'Scatter guidance') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content>\n <p class=\"editor-caption\">\n {{ t('praxis.charts.editor.specialization.scatterHint', 'Scatter charts use the first dimension as X and the first metric as Y, so keep both fields mapped.') }}\n </p>\n </mat-card-content>\n </mat-card>\n }\n\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.analytics.dimensionsTitle', 'Dimensions') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-stack\">\n @for (dimension of dimensions(); track $index) {\n <div class=\"editor-row-card\" [attr.data-testid]=\"'chart-editor-dimension-row-' + $index\">\n <div class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\" [attr.data-testid]=\"'chart-editor-dimension-field-' + $index\">\n <mat-label>{{ t('praxis.charts.editor.field.dimension', 'Dimension') }}</mat-label>\n @if (fieldOptions('dimension').length) {\n <mat-select [ngModel]=\"dimension.field || ''\" (ngModelChange)=\"setDimensionField($index, $event)\" [disabled]=\"isReadonly()\">\n @for (field of fieldOptions('dimension'); track field.field) {\n <mat-option [value]=\"field.field\">{{ fieldOptionLabel(field) }}</mat-option>\n }\n </mat-select>\n } @else {\n <mat-select [ngModel]=\"dimension.field || ''\" (ngModelChange)=\"setDimensionField($index, $event)\" [disabled]=\"true\"></mat-select>\n <mat-hint>{{ t('praxis.charts.editor.catalog.dimensionFieldsMissing', 'No governed dimensions are available for the current resource and operation.') }}</mat-hint>\n }\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\" [attr.data-testid]=\"'chart-editor-dimension-role-' + $index\">\n <mat-label>{{ t('praxis.charts.editor.field.dimensionRole', 'Dimension role') }}</mat-label>\n <mat-select [ngModel]=\"dimension.role || 'category'\" (ngModelChange)=\"setDimensionRole($index, $event)\" [disabled]=\"isReadonly()\">\n @for (role of dimensionRoles; track role) {\n <mat-option [value]=\"role\">\n {{ t('praxis.charts.editor.dimensionRole.' + role, role) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n </div>\n\n <div class=\"editor-row-actions\">\n <button mat-button type=\"button\" (click)=\"removeDimension($index)\" [disabled]=\"isReadonly() || dimensions().length <= 1\">\n {{ t('praxis.charts.editor.analytics.removeDimension', 'Remove dimension') }}\n </button>\n </div>\n </div>\n }\n\n <div>\n <button mat-stroked-button type=\"button\" data-testid=\"chart-editor-add-dimension\" (click)=\"addDimension()\" [disabled]=\"isReadonly()\">\n {{ t('praxis.charts.editor.analytics.addDimension', 'Add dimension') }}\n </button>\n </div>\n </mat-card-content>\n </mat-card>\n\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.analytics.metricsTitle', 'Metrics') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-stack\">\n @for (metric of metrics(); track $index) {\n <div class=\"editor-row-card\" [attr.data-testid]=\"'chart-editor-metric-row-' + $index\">\n <div class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\" [attr.data-testid]=\"'chart-editor-metric-field-' + $index\">\n <mat-label>{{ t('praxis.charts.editor.field.metric', 'Metric') }}</mat-label>\n @if (fieldOptions('metric').length) {\n <mat-select [ngModel]=\"metric.field || ''\" (ngModelChange)=\"setMetricField($index, $event)\" [disabled]=\"isReadonly()\">\n @for (field of fieldOptions('metric'); track field.field) {\n <mat-option [value]=\"field.field\">{{ fieldOptionLabel(field) }}</mat-option>\n }\n </mat-select>\n } @else {\n <mat-select [ngModel]=\"metric.field || ''\" (ngModelChange)=\"setMetricField($index, $event)\" [disabled]=\"true\"></mat-select>\n <mat-hint>{{ t('praxis.charts.editor.catalog.metricFieldsMissing', 'No governed metrics are available for the current resource and operation.') }}</mat-hint>\n }\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\" [attr.data-testid]=\"'chart-editor-metric-label-' + $index\">\n <mat-label>{{ t('praxis.charts.editor.field.metricLabel', 'Metric label') }}</mat-label>\n <input matInput [ngModel]=\"metric.label || ''\" (ngModelChange)=\"setMetricLabel($index, $event)\" [disabled]=\"isReadonly()\" />\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\" [attr.data-testid]=\"'chart-editor-metric-aggregation-' + $index\">\n <mat-label>{{ t('praxis.charts.editor.field.metricAggregation', 'Aggregation') }}</mat-label>\n <mat-select [ngModel]=\"metric.aggregation || 'sum'\" (ngModelChange)=\"setMetricAggregation($index, $event)\" [disabled]=\"isReadonly()\">\n @for (aggregation of metricAggregationOptions(metric.field); track aggregation) {\n <mat-option [value]=\"aggregation\">\n {{ t('praxis.charts.editor.metricAggregation.' + aggregation, aggregation) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n @if (showMetricAxisControls()) {\n <mat-form-field class=\"editor-field\" appearance=\"outline\" [attr.data-testid]=\"'chart-editor-metric-axis-' + $index\">\n <mat-label>{{ t('praxis.charts.editor.field.metricAxis', 'Axis') }}</mat-label>\n <mat-select [ngModel]=\"metric.axis || 'primary'\" (ngModelChange)=\"setMetricAxis($index, $event)\" [disabled]=\"isReadonly()\">\n @for (axis of metricAxes; track axis) {\n <mat-option [value]=\"axis\">\n {{ t('praxis.charts.editor.metricAxis.' + axis, axis) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n }\n\n @if (showMetricSeriesKindControls()) {\n <mat-form-field class=\"editor-field\" appearance=\"outline\" [attr.data-testid]=\"'chart-editor-metric-series-kind-' + $index\">\n <mat-label>{{ t('praxis.charts.editor.field.metricSeriesKind', 'Series kind') }}</mat-label>\n <mat-select [ngModel]=\"metric.seriesKind || 'bar'\" (ngModelChange)=\"setMetricSeriesKind($index, $event)\" [disabled]=\"isReadonly()\">\n @for (seriesKind of metricSeriesKinds; track seriesKind) {\n <mat-option [value]=\"seriesKind\">\n {{ t('praxis.charts.editor.metricSeriesKind.' + seriesKind, seriesKind) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n }\n </div>\n\n <div class=\"editor-row-actions\">\n <button mat-button type=\"button\" [attr.data-testid]=\"'chart-editor-remove-metric-' + $index\" (click)=\"removeMetric($index)\" [disabled]=\"isReadonly() || metrics().length <= 1\">\n {{ t('praxis.charts.editor.analytics.removeMetric', 'Remove metric') }}\n </button>\n </div>\n </div>\n }\n\n <div>\n <button mat-stroked-button type=\"button\" data-testid=\"chart-editor-add-metric\" (click)=\"addMetric()\" [disabled]=\"isReadonly()\">\n {{ t('praxis.charts.editor.analytics.addMetric', 'Add metric') }}\n </button>\n </div>\n </mat-card-content>\n </mat-card>\n </div>\n }\n\n @case ('events') {\n <div class=\"editor-stack\">\n <mat-card class=\"editor-card\" data-testid=\"chart-editor-event-pointClick\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.events.pointClickTitle', 'Point click') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventAction', 'Action') }}</mat-label>\n <mat-select data-testid=\"chart-editor-event-pointClick-action\" [ngModel]=\"eventAction('pointClick')\" (ngModelChange)=\"setEventAction('pointClick', $event)\" [disabled]=\"isReadonly()\">\n <mat-option value=\"\">{{ t('praxis.charts.editor.events.none', 'None') }}</mat-option>\n @for (action of eventActionOptions; track action) {\n <mat-option [value]=\"action\">\n {{ t('praxis.charts.editor.eventAction.' + action, action) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventTarget', 'Target') }}</mat-label>\n <mat-select data-testid=\"chart-editor-event-pointClick-target\" [ngModel]=\"eventTarget('pointClick')\" (ngModelChange)=\"setEventTarget('pointClick', $event)\" [disabled]=\"isReadonly() || !eventAction('pointClick') || targetCatalogUnavailable(eventAction('pointClick'), 'pointClick')\">\n @for (target of targetOptions(eventAction('pointClick'), 'pointClick'); track target.id) {\n <mat-option [value]=\"target.id\">{{ target.label }}</mat-option>\n }\n </mat-select>\n @if (targetCatalogUnavailable(eventAction('pointClick'), 'pointClick')) {\n <mat-hint>{{ t('praxis.charts.editor.catalog.targetMissing', 'Target catalog is required for this governed event action.') }}</mat-hint>\n }\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventMapping', 'Mapping') }}</mat-label>\n <textarea\n matInput\n data-testid=\"chart-editor-event-pointClick-mapping\"\n rows=\"4\"\n [ngModel]=\"eventMappingText('pointClick')\"\n (ngModelChange)=\"setEventMapping('pointClick', $event)\"\n [disabled]=\"isReadonly() || !eventAction('pointClick')\"\n ></textarea>\n </mat-form-field>\n </mat-card-content>\n </mat-card>\n\n <mat-card class=\"editor-card\" data-testid=\"chart-editor-event-selectionChange\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.events.selectionChangeTitle', 'Selection change') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventAction', 'Action') }}</mat-label>\n <mat-select data-testid=\"chart-editor-event-selectionChange-action\" [ngModel]=\"eventAction('selectionChange')\" (ngModelChange)=\"setEventAction('selectionChange', $event)\" [disabled]=\"isReadonly()\">\n <mat-option value=\"\">{{ t('praxis.charts.editor.events.none', 'None') }}</mat-option>\n @for (action of eventActionOptions; track action) {\n <mat-option [value]=\"action\">\n {{ t('praxis.charts.editor.eventAction.' + action, action) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventTarget', 'Target') }}</mat-label>\n <mat-select data-testid=\"chart-editor-event-selectionChange-target\" [ngModel]=\"eventTarget('selectionChange')\" (ngModelChange)=\"setEventTarget('selectionChange', $event)\" [disabled]=\"isReadonly() || !eventAction('selectionChange') || targetCatalogUnavailable(eventAction('selectionChange'), 'selectionChange')\">\n @for (target of targetOptions(eventAction('selectionChange'), 'selectionChange'); track target.id) {\n <mat-option [value]=\"target.id\">{{ target.label }}</mat-option>\n }\n </mat-select>\n @if (targetCatalogUnavailable(eventAction('selectionChange'), 'selectionChange')) {\n <mat-hint>{{ t('praxis.charts.editor.catalog.targetMissing', 'Target catalog is required for this governed event action.') }}</mat-hint>\n }\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventMapping', 'Mapping') }}</mat-label>\n <textarea\n matInput\n data-testid=\"chart-editor-event-selectionChange-mapping\"\n rows=\"4\"\n [ngModel]=\"eventMappingText('selectionChange')\"\n (ngModelChange)=\"setEventMapping('selectionChange', $event)\"\n [disabled]=\"isReadonly() || !eventAction('selectionChange')\"\n ></textarea>\n </mat-form-field>\n </mat-card-content>\n </mat-card>\n\n <mat-card class=\"editor-card\" data-testid=\"chart-editor-event-drillDown\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.events.drillDownTitle', 'Drill down') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventAction', 'Action') }}</mat-label>\n <mat-select data-testid=\"chart-editor-event-drillDown-action\" [ngModel]=\"eventAction('drillDown')\" (ngModelChange)=\"setEventAction('drillDown', $event)\" [disabled]=\"isReadonly()\">\n <mat-option value=\"\">{{ t('praxis.charts.editor.events.none', 'None') }}</mat-option>\n @for (action of eventActionOptions; track action) {\n <mat-option [value]=\"action\">\n {{ t('praxis.charts.editor.eventAction.' + action, action) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventTarget', 'Target') }}</mat-label>\n <mat-select data-testid=\"chart-editor-event-drillDown-target\" [ngModel]=\"eventTarget('drillDown')\" (ngModelChange)=\"setEventTarget('drillDown', $event)\" [disabled]=\"isReadonly() || !eventAction('drillDown') || targetCatalogUnavailable(eventAction('drillDown'), 'drillDown')\">\n @for (target of targetOptions(eventAction('drillDown'), 'drillDown'); track target.id) {\n <mat-option [value]=\"target.id\">{{ target.label }}</mat-option>\n }\n </mat-select>\n @if (targetCatalogUnavailable(eventAction('drillDown'), 'drillDown')) {\n <mat-hint>{{ t('praxis.charts.editor.catalog.targetMissing', 'Target catalog is required for this governed event action.') }}</mat-hint>\n }\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventMapping', 'Mapping') }}</mat-label>\n <textarea\n matInput\n data-testid=\"chart-editor-event-drillDown-mapping\"\n rows=\"4\"\n [ngModel]=\"eventMappingText('drillDown')\"\n (ngModelChange)=\"setEventMapping('drillDown', $event)\"\n [disabled]=\"isReadonly() || !eventAction('drillDown')\"\n ></textarea>\n </mat-form-field>\n </mat-card-content>\n </mat-card>\n\n <mat-card class=\"editor-card\" data-testid=\"chart-editor-event-crossFilter\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.events.crossFilterTitle', 'Cross filter') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventAction', 'Action') }}</mat-label>\n <mat-select data-testid=\"chart-editor-event-crossFilter-action\" [ngModel]=\"eventAction('crossFilter')\" (ngModelChange)=\"setEventAction('crossFilter', $event)\" [disabled]=\"isReadonly()\">\n <mat-option value=\"\">{{ t('praxis.charts.editor.events.none', 'None') }}</mat-option>\n @for (action of eventActionOptions; track action) {\n <mat-option [value]=\"action\">\n {{ t('praxis.charts.editor.eventAction.' + action, action) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventTarget', 'Target') }}</mat-label>\n <mat-select data-testid=\"chart-editor-event-crossFilter-target\" [ngModel]=\"eventTarget('crossFilter')\" (ngModelChange)=\"setEventTarget('crossFilter', $event)\" [disabled]=\"isReadonly() || !eventAction('crossFilter') || targetCatalogUnavailable(eventAction('crossFilter'), 'crossFilter')\">\n @for (target of targetOptions(eventAction('crossFilter'), 'crossFilter'); track target.id) {\n <mat-option [value]=\"target.id\">{{ target.label }}</mat-option>\n }\n </mat-select>\n @if (targetCatalogUnavailable(eventAction('crossFilter'), 'crossFilter')) {\n <mat-hint>{{ t('praxis.charts.editor.catalog.targetMissing', 'Target catalog is required for this governed event action.') }}</mat-hint>\n }\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventMapping', 'Mapping') }}</mat-label>\n <textarea\n matInput\n data-testid=\"chart-editor-event-crossFilter-mapping\"\n rows=\"4\"\n [ngModel]=\"eventMappingText('crossFilter')\"\n (ngModelChange)=\"setEventMapping('crossFilter', $event)\"\n [disabled]=\"isReadonly() || !eventAction('crossFilter')\"\n ></textarea>\n </mat-form-field>\n </mat-card-content>\n </mat-card>\n </div>\n }\n\n @case ('preview') {\n <p class=\"editor-caption\">\n {{ t('praxis.charts.editor.preview.caption', 'Local preview derived from the canonical contract without remote calls.') }}\n </p>\n }\n }\n </mat-card-content>\n </mat-card>\n </div>\n\n <div class=\"editor-side\">\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.issues.title', 'Validation issues') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content>\n @if (issues().length) {\n <ul class=\"editor-issues\">\n @for (issue of issues(); track issueTrackBy($index, issue)) {\n <li class=\"editor-issue\">\n <strong>{{ issue.field }}</strong>\n <span>{{ issue.message }}</span>\n </li>\n }\n </ul>\n } @else {\n <div class=\"editor-empty\">\n {{ t('praxis.charts.editor.issues.empty', 'No issues were identified.') }}\n </div>\n }\n </mat-card-content>\n </mat-card>\n\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.preview.title', 'Chart preview') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content>\n @if (preview(); as chartPreview) {\n <p class=\"editor-caption\">\n {{ t('praxis.charts.editor.preview.caption', 'Local preview derived from the canonical contract without remote calls.') }}\n </p>\n <praxis-chart [config]=\"chartPreview.config\" [data]=\"chartPreview.data\"></praxis-chart>\n } @else {\n <div class=\"editor-empty\">\n {{ t('praxis.charts.editor.preview.invalid', 'Preview is unavailable while the contract has blocking errors.') }}\n </div>\n }\n </mat-card-content>\n </mat-card>\n </div>\n </div>\n</div>\n", styles: [":host{display:block;min-width:0;container:praxis-chart-editor / inline-size;color:var(--md-sys-color-on-surface, #1a1b20)}.editor-shell{display:grid;gap:18px;min-width:0}.editor-nav{display:flex;gap:8px;flex-wrap:wrap}.editor-nav button.active{background:color-mix(in srgb,var(--md-sys-color-primary, #1263b4) 18%,transparent);color:var(--md-sys-color-primary, #1263b4)}.editor-layout{display:grid;gap:18px;grid-template-columns:minmax(0,1.35fr) minmax(320px,.9fr);align-items:start}.editor-form,.editor-side{display:grid;gap:16px;min-width:0}.editor-card{box-sizing:border-box;width:100%;min-width:0;border-radius:20px;border:1px solid color-mix(in srgb,var(--md-sys-color-outline, #c5c7ce) 72%,transparent);background:linear-gradient(180deg,#1263b408,#1263b400)}.editor-grid{display:grid;gap:14px;grid-template-columns:repeat(auto-fit,minmax(220px,1fr))}.editor-stack{display:grid;gap:14px}.editor-row-card{padding:14px;border-radius:16px;border:1px solid color-mix(in srgb,var(--md-sys-color-outline, #c5c7ce) 54%,transparent);background:color-mix(in srgb,var(--md-sys-color-surface, #fff) 92%,rgba(18,99,180,.04))}.editor-row-actions{display:flex;justify-content:flex-end}.editor-field{width:100%}.editor-issues{display:grid;gap:10px;margin:0;padding:0;list-style:none}.editor-issue{padding:12px 14px;border-radius:14px;background:color-mix(in srgb,var(--md-sys-color-error, #b3261e) 8%,transparent);border:1px solid color-mix(in srgb,var(--md-sys-color-error, #b3261e) 18%,transparent)}.editor-issue strong{display:block;margin-bottom:4px}.editor-caption{margin:0 0 12px;color:var(--md-sys-color-on-surface-variant, #5a5d67);font-size:.92rem}.editor-empty{padding:18px;border-radius:14px;background:color-mix(in srgb,var(--md-sys-color-surface-variant, #eceff4) 78%,transparent)}.editor-side praxis-chart{display:block;width:100%;min-width:0;max-width:100%}@container praxis-chart-editor (max-width: 840px){.editor-layout{grid-template-columns:minmax(0,1fr)}}@media(max-width:960px){.editor-layout{grid-template-columns:minmax(0,1fr)}}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$3.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$3.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$3.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i1$2.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatCardModule }, { kind: "component", type: i3$1.MatCard, selector: "mat-card", inputs: ["appearance"], exportAs: ["matCard"] }, { kind: "directive", type: i3$1.MatCardContent, selector: "mat-card-content" }, { kind: "component", type: i3$1.MatCardHeader, selector: "mat-card-header" }, { kind: "directive", type: i3$1.MatCardTitle, selector: "mat-card-title, [mat-card-title], [matCardTitle]" }, { kind: "ngmodule", type: MatFormFieldModule }, { kind: "component", type: i4.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i4.MatLabel, selector: "mat-label" }, { kind: "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 });
5210
6840
  }
5211
6841
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: PraxisChartConfigEditor, decorators: [{
5212
6842
  type: Component,
@@ -5219,7 +6849,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
5219
6849
  MatSelectModule,
5220
6850
  MatSlideToggleModule,
5221
6851
  PraxisChartComponent
5222
- ], providers: [providePraxisChartsI18n()], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"editor-shell\">\n <div class=\"editor-nav\">\n @for (section of sections; track section.id) {\n <button\n mat-stroked-button\n type=\"button\"\n [class.active]=\"activeSection() === section.id\"\n (click)=\"setSection(section.id)\"\n >\n {{ t(section.labelKey, section.fallback) }}\n </button>\n }\n </div>\n\n <div class=\"editor-layout\">\n <div class=\"editor-form\">\n <mat-card class=\"editor-card\">\n <mat-card-content>\n @switch (activeSection()) {\n @case ('general') {\n <div class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.chartId', 'Chart ID') }}</mat-label>\n <input matInput [ngModel]=\"doc().chartId || ''\" (ngModelChange)=\"setChartId($event)\" [disabled]=\"isReadonly()\" />\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.kind', 'Kind') }}</mat-label>\n <mat-select [ngModel]=\"doc().kind\" (ngModelChange)=\"setKind($event)\" [disabled]=\"isReadonly()\">\n @for (kind of chartKinds; track kind) {\n <mat-option [value]=\"kind\">\n {{ t('praxis.charts.editor.kind.' + kind, kind) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.title', 'Title') }}</mat-label>\n <input matInput 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 [ngModel]=\"doc().source.kind\" (ngModelChange)=\"setSourceKind($event)\" [disabled]=\"isReadonly()\">\n @for (sourceKind of sourceKinds; track sourceKind) {\n <mat-option [value]=\"sourceKind\">\n {{ t('praxis.charts.editor.sourceKind.' + (sourceKind === 'praxis.stats' ? 'praxisStats' : 'derived'), sourceKind) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n @if (doc().source.kind === 'praxis.stats') {\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.resource', 'Resource') }}</mat-label>\n @if (resourceOptions().length) {\n <mat-select [ngModel]=\"resourceValue()\" (ngModelChange)=\"setResource($event)\" [disabled]=\"isReadonly()\">\n @for (resource of resourceOptions(); track resource.id) {\n <mat-option [value]=\"resource.path\">{{ resource.label }}</mat-option>\n }\n </mat-select>\n } @else {\n <input matInput [ngModel]=\"resourceValue()\" (ngModelChange)=\"setResource($event)\" [disabled]=\"isReadonly()\" />\n }\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.operation', 'Operation') }}</mat-label>\n <mat-select\n [ngModel]=\"doc().source.operation || 'group-by'\"\n (ngModelChange)=\"setOperation($event)\"\n [disabled]=\"isReadonly()\"\n >\n @for (operation of operations; track operation) {\n <mat-option [value]=\"operation\">\n {{ t('praxis.charts.editor.operation.' + (operation === 'group-by' ? 'groupBy' : operation), operation) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n }\n </div>\n\n @if (showTimeseriesControls()) {\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.specialization.timeseriesTitle', 'Timeseries options') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.granularity', 'Granularity') }}</mat-label>\n <mat-select [ngModel]=\"granularityValue()\" (ngModelChange)=\"setGranularity($event)\" [disabled]=\"isReadonly()\">\n @for (granularity of timeGranularities; track granularity) {\n <mat-option [value]=\"granularity\">\n {{ t('praxis.charts.editor.granularity.' + granularity, granularity) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n <mat-slide-toggle\n [ngModel]=\"fillGapsValue()\"\n (ngModelChange)=\"setFillGaps($event)\"\n [disabled]=\"isReadonly()\"\n >\n {{ t('praxis.charts.editor.field.fillGaps', 'Fill missing intervals') }}\n </mat-slide-toggle>\n </mat-card-content>\n </mat-card>\n }\n\n @if (showDistributionControls()) {\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.specialization.distributionTitle', 'Distribution options') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.distributionMode', 'Distribution mode') }}</mat-label>\n <mat-select [ngModel]=\"distributionModeValue()\" (ngModelChange)=\"setDistributionMode($event)\" [disabled]=\"isReadonly()\">\n @for (mode of distributionModes; track mode) {\n <mat-option [value]=\"mode\">\n {{ t('praxis.charts.editor.distributionMode.' + mode, mode) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.bucketSize', 'Bucket size') }}</mat-label>\n <input matInput [ngModel]=\"bucketSizeValue()\" (ngModelChange)=\"setBucketSize($event)\" [disabled]=\"isReadonly()\" />\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.bucketCount', 'Bucket count') }}</mat-label>\n <input matInput [ngModel]=\"bucketCountValue()\" (ngModelChange)=\"setBucketCount($event)\" [disabled]=\"isReadonly()\" />\n </mat-form-field>\n </mat-card-content>\n </mat-card>\n }\n </div>\n }\n\n @case ('motion') {\n <div class=\"editor-grid\">\n <mat-slide-toggle\n [ngModel]=\"normalizedDocument().motion?.enabled !== false\"\n (ngModelChange)=\"setMotionEnabled($event)\"\n [disabled]=\"isReadonly()\"\n >\n {{ t('praxis.charts.editor.field.motionEnabled', 'Enable animations') }}\n </mat-slide-toggle>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.motionPreset', 'Motion preset') }}</mat-label>\n <mat-select\n [ngModel]=\"normalizedDocument().motion?.preset || 'standard'\"\n (ngModelChange)=\"setMotionPreset($event)\"\n [disabled]=\"isReadonly() || normalizedDocument().motion?.enabled === false\"\n >\n @for (preset of motionPresets; track preset) {\n <mat-option [value]=\"preset\">\n {{ t('praxis.charts.editor.motionPreset.' + preset, preset) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n </div>\n }\n\n @case ('appearance') {\n <div class=\"editor-stack\">\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.appearance.featuresTitle', 'Display features') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-grid\">\n <mat-slide-toggle\n [ngModel]=\"featureEnabled('legend')\"\n (ngModelChange)=\"setFeatureEnabled('legend', $event)\"\n [disabled]=\"isReadonly()\"\n >\n {{ t('praxis.charts.editor.field.legendEnabled', 'Show legend') }}\n </mat-slide-toggle>\n\n <mat-slide-toggle\n [ngModel]=\"featureEnabled('labels')\"\n (ngModelChange)=\"setFeatureEnabled('labels', $event)\"\n [disabled]=\"isReadonly()\"\n >\n {{ t('praxis.charts.editor.field.labelsEnabled', 'Show labels') }}\n </mat-slide-toggle>\n\n <mat-slide-toggle\n [ngModel]=\"featureEnabled('tooltip')\"\n (ngModelChange)=\"setFeatureEnabled('tooltip', $event)\"\n [disabled]=\"isReadonly()\"\n >\n {{ t('praxis.charts.editor.field.tooltipEnabled', 'Show tooltip') }}\n </mat-slide-toggle>\n </mat-card-content>\n </mat-card>\n\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.appearance.paletteTitle', 'Palette') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-stack\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.themeVariant', 'Theme variant') }}</mat-label>\n <mat-select [ngModel]=\"themeVariantValue()\" (ngModelChange)=\"setThemeVariant($event)\" [disabled]=\"isReadonly()\">\n <mat-option [value]=\"''\">\n {{ t('praxis.charts.editor.themeVariant.none', 'No variant') }}\n </mat-option>\n @for (variant of themeVariants; track variant) {\n <mat-option [value]=\"variant\">\n {{ t('praxis.charts.editor.themeVariant.' + variant, variant) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.paletteMode', 'Palette mode') }}</mat-label>\n <mat-select [ngModel]=\"paletteModeValue()\" (ngModelChange)=\"setPaletteMode($event)\" [disabled]=\"isReadonly()\">\n @for (mode of paletteModes; track mode) {\n <mat-option [value]=\"mode\">\n {{ t('praxis.charts.editor.paletteMode.' + mode, mode) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n @if (paletteModeValue() === 'token') {\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.paletteToken', 'Palette token') }}</mat-label>\n <mat-select [ngModel]=\"paletteTokenValue()\" (ngModelChange)=\"setPaletteToken($event)\" [disabled]=\"isReadonly()\">\n @for (token of paletteTokens; track token) {\n <mat-option [value]=\"token\">\n {{ t('praxis.charts.editor.paletteToken.' + token, token) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n } @else {\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.palette', 'Palette colors') }}</mat-label>\n <textarea\n matInput\n rows=\"3\"\n [ngModel]=\"paletteValue()\"\n (ngModelChange)=\"setPalette($event)\"\n [disabled]=\"isReadonly()\"\n ></textarea>\n </mat-form-field>\n }\n\n <p class=\"editor-caption\">\n {{ t('praxis.charts.editor.appearance.paletteHint', 'Use a registered token or comma-separated colors to persist theme.palette in the canonical contract.') }}\n </p>\n </mat-card-content>\n </mat-card>\n\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.appearance.surfaceTitle', 'Surface') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content>\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.surfaceMode', 'Surface mode') }}</mat-label>\n <mat-select [ngModel]=\"surfaceModeValue()\" (ngModelChange)=\"setSurfaceMode($event)\" [disabled]=\"isReadonly()\">\n @for (mode of surfaceModes; track mode) {\n <mat-option [value]=\"mode\">\n {{ t('praxis.charts.editor.surface.' + mode, mode) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n <p class=\"editor-caption\">\n {{ t('praxis.charts.editor.appearance.surfaceHint', 'Use embedded for dashboard widgets and contained only when the chart must own its visual surface.') }}\n </p>\n </mat-card-content>\n </mat-card>\n\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.appearance.statesTitle', 'State messages') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-stack\">\n <div class=\"editor-row-card\">\n <div class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.emptyTitle', 'Empty title') }}</mat-label>\n <input matInput [ngModel]=\"stateTitle('empty')\" (ngModelChange)=\"setStateTitle('empty', $event)\" [disabled]=\"isReadonly()\" />\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.emptyDescription', 'Empty description') }}</mat-label>\n <textarea matInput rows=\"2\" [ngModel]=\"stateDescription('empty')\" (ngModelChange)=\"setStateDescription('empty', $event)\" [disabled]=\"isReadonly()\"></textarea>\n </mat-form-field>\n </div>\n </div>\n\n <div class=\"editor-row-card\">\n <div class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.loadingTitle', 'Loading title') }}</mat-label>\n <input matInput [ngModel]=\"stateTitle('loading')\" (ngModelChange)=\"setStateTitle('loading', $event)\" [disabled]=\"isReadonly()\" />\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.loadingDescription', 'Loading description') }}</mat-label>\n <textarea matInput rows=\"2\" [ngModel]=\"stateDescription('loading')\" (ngModelChange)=\"setStateDescription('loading', $event)\" [disabled]=\"isReadonly()\"></textarea>\n </mat-form-field>\n </div>\n </div>\n\n <div class=\"editor-row-card\">\n <div class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.errorTitle', 'Error title') }}</mat-label>\n <input matInput [ngModel]=\"stateTitle('error')\" (ngModelChange)=\"setStateTitle('error', $event)\" [disabled]=\"isReadonly()\" />\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.errorDescription', 'Error description') }}</mat-label>\n <textarea matInput rows=\"2\" [ngModel]=\"stateDescription('error')\" (ngModelChange)=\"setStateDescription('error', $event)\" [disabled]=\"isReadonly()\"></textarea>\n </mat-form-field>\n </div>\n </div>\n </mat-card-content>\n </mat-card>\n </div>\n }\n\n @case ('analytics') {\n <div class=\"editor-stack\">\n @if (showComboPanel()) {\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.specialization.comboTitle', 'Combo guidance') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content>\n <p class=\"editor-caption\">\n {{ t('praxis.charts.editor.specialization.comboHint', 'Combo charts require at least two metrics and allow per-metric axis and series kind mapping.') }}\n </p>\n </mat-card-content>\n </mat-card>\n }\n\n @if (showPieDonutPanel()) {\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.specialization.pieDonutTitle', 'Composition guidance') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content>\n <p class=\"editor-caption\">\n {{ t('praxis.charts.editor.specialization.pieDonutHint', 'Pie and donut charts keep only the first metric and use the first dimension as the category segment.') }}\n </p>\n </mat-card-content>\n </mat-card>\n }\n\n @if (showScatterPanel()) {\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.specialization.scatterTitle', 'Scatter guidance') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content>\n <p class=\"editor-caption\">\n {{ t('praxis.charts.editor.specialization.scatterHint', 'Scatter charts use the first dimension as X and the first metric as Y, so keep both fields mapped.') }}\n </p>\n </mat-card-content>\n </mat-card>\n }\n\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.analytics.dimensionsTitle', 'Dimensions') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-stack\">\n @for (dimension of dimensions(); track $index) {\n <div class=\"editor-row-card\" [attr.data-testid]=\"'chart-editor-dimension-row-' + $index\">\n <div class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\" [attr.data-testid]=\"'chart-editor-dimension-field-' + $index\">\n <mat-label>{{ t('praxis.charts.editor.field.dimension', 'Dimension') }}</mat-label>\n @if (fieldOptions('dimension').length) {\n <mat-select [ngModel]=\"dimension.field || ''\" (ngModelChange)=\"setDimensionField($index, $event)\" [disabled]=\"isReadonly()\">\n @for (field of fieldOptions('dimension'); track field.field) {\n <mat-option [value]=\"field.field\">{{ field.label || field.field }}</mat-option>\n }\n </mat-select>\n } @else {\n <input matInput [ngModel]=\"dimension.field || ''\" (ngModelChange)=\"setDimensionField($index, $event)\" [disabled]=\"isReadonly()\" />\n }\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\" [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\">{{ field.label || field.field }}</mat-option>\n }\n </mat-select>\n } @else {\n <input matInput [ngModel]=\"metric.field || ''\" (ngModelChange)=\"setMetricField($index, $event)\" [disabled]=\"isReadonly()\" />\n }\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\" [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 metricAggregations; track aggregation) {\n <mat-option [value]=\"aggregation\">\n {{ t('praxis.charts.editor.metricAggregation.' + aggregation, aggregation) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n @if (showMetricAxisControls()) {\n <mat-form-field class=\"editor-field\" appearance=\"outline\" [attr.data-testid]=\"'chart-editor-metric-axis-' + $index\">\n <mat-label>{{ t('praxis.charts.editor.field.metricAxis', 'Axis') }}</mat-label>\n <mat-select [ngModel]=\"metric.axis || 'primary'\" (ngModelChange)=\"setMetricAxis($index, $event)\" [disabled]=\"isReadonly()\">\n @for (axis of metricAxes; track axis) {\n <mat-option [value]=\"axis\">\n {{ t('praxis.charts.editor.metricAxis.' + axis, axis) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n }\n\n @if (showMetricSeriesKindControls()) {\n <mat-form-field class=\"editor-field\" appearance=\"outline\" [attr.data-testid]=\"'chart-editor-metric-series-kind-' + $index\">\n <mat-label>{{ t('praxis.charts.editor.field.metricSeriesKind', 'Series kind') }}</mat-label>\n <mat-select [ngModel]=\"metric.seriesKind || 'bar'\" (ngModelChange)=\"setMetricSeriesKind($index, $event)\" [disabled]=\"isReadonly()\">\n @for (seriesKind of metricSeriesKinds; track seriesKind) {\n <mat-option [value]=\"seriesKind\">\n {{ t('praxis.charts.editor.metricSeriesKind.' + seriesKind, seriesKind) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n }\n </div>\n\n <div class=\"editor-row-actions\">\n <button mat-button type=\"button\" [attr.data-testid]=\"'chart-editor-remove-metric-' + $index\" (click)=\"removeMetric($index)\" [disabled]=\"isReadonly() || metrics().length <= 1\">\n {{ t('praxis.charts.editor.analytics.removeMetric', 'Remove metric') }}\n </button>\n </div>\n </div>\n }\n\n <div>\n <button mat-stroked-button type=\"button\" data-testid=\"chart-editor-add-metric\" (click)=\"addMetric()\" [disabled]=\"isReadonly()\">\n {{ t('praxis.charts.editor.analytics.addMetric', 'Add metric') }}\n </button>\n </div>\n </mat-card-content>\n </mat-card>\n </div>\n }\n\n @case ('events') {\n <div class=\"editor-stack\">\n <mat-card class=\"editor-card\" data-testid=\"chart-editor-event-pointClick\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.events.pointClickTitle', 'Point click') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventAction', 'Action') }}</mat-label>\n <mat-select data-testid=\"chart-editor-event-pointClick-action\" [ngModel]=\"eventAction('pointClick')\" (ngModelChange)=\"setEventAction('pointClick', $event)\" [disabled]=\"isReadonly()\">\n <mat-option value=\"\">{{ t('praxis.charts.editor.events.none', 'None') }}</mat-option>\n @for (action of eventActionOptions; track action) {\n <mat-option [value]=\"action\">\n {{ t('praxis.charts.editor.eventAction.' + action, action) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventTarget', 'Target') }}</mat-label>\n @if (targetOptions().length) {\n <mat-select data-testid=\"chart-editor-event-pointClick-target\" [ngModel]=\"eventTarget('pointClick')\" (ngModelChange)=\"setEventTarget('pointClick', $event)\" [disabled]=\"isReadonly() || !eventAction('pointClick')\">\n @for (target of targetOptions(); track target.id) {\n <mat-option [value]=\"target.id\">{{ target.label }}</mat-option>\n }\n </mat-select>\n } @else {\n <input matInput data-testid=\"chart-editor-event-pointClick-target\" [ngModel]=\"eventTarget('pointClick')\" (ngModelChange)=\"setEventTarget('pointClick', $event)\" [disabled]=\"isReadonly() || !eventAction('pointClick')\" />\n }\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventMapping', 'Mapping') }}</mat-label>\n <textarea\n matInput\n 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 @if (targetOptions().length) {\n <mat-select data-testid=\"chart-editor-event-selectionChange-target\" [ngModel]=\"eventTarget('selectionChange')\" (ngModelChange)=\"setEventTarget('selectionChange', $event)\" [disabled]=\"isReadonly() || !eventAction('selectionChange')\">\n @for (target of targetOptions(); track target.id) {\n <mat-option [value]=\"target.id\">{{ target.label }}</mat-option>\n }\n </mat-select>\n } @else {\n <input matInput data-testid=\"chart-editor-event-selectionChange-target\" [ngModel]=\"eventTarget('selectionChange')\" (ngModelChange)=\"setEventTarget('selectionChange', $event)\" [disabled]=\"isReadonly() || !eventAction('selectionChange')\" />\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 @if (targetOptions().length) {\n <mat-select data-testid=\"chart-editor-event-drillDown-target\" [ngModel]=\"eventTarget('drillDown')\" (ngModelChange)=\"setEventTarget('drillDown', $event)\" [disabled]=\"isReadonly() || !eventAction('drillDown')\">\n @for (target of targetOptions(); track target.id) {\n <mat-option [value]=\"target.id\">{{ target.label }}</mat-option>\n }\n </mat-select>\n } @else {\n <input matInput data-testid=\"chart-editor-event-drillDown-target\" [ngModel]=\"eventTarget('drillDown')\" (ngModelChange)=\"setEventTarget('drillDown', $event)\" [disabled]=\"isReadonly() || !eventAction('drillDown')\" />\n }\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventMapping', 'Mapping') }}</mat-label>\n <textarea\n matInput\n 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 @if (targetOptions().length) {\n <mat-select data-testid=\"chart-editor-event-crossFilter-target\" [ngModel]=\"eventTarget('crossFilter')\" (ngModelChange)=\"setEventTarget('crossFilter', $event)\" [disabled]=\"isReadonly() || !eventAction('crossFilter')\">\n @for (target of targetOptions(); track target.id) {\n <mat-option [value]=\"target.id\">{{ target.label }}</mat-option>\n }\n </mat-select>\n } @else {\n <input matInput data-testid=\"chart-editor-event-crossFilter-target\" [ngModel]=\"eventTarget('crossFilter')\" (ngModelChange)=\"setEventTarget('crossFilter', $event)\" [disabled]=\"isReadonly() || !eventAction('crossFilter')\" />\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;color:var(--md-sys-color-on-surface, #1a1b20)}.editor-shell{display:grid;gap:18px}.editor-nav{display:flex;gap:8px;flex-wrap:wrap}.editor-nav button.active{background:color-mix(in srgb,var(--md-sys-color-primary, #1263b4) 18%,transparent);color:var(--md-sys-color-primary, #1263b4)}.editor-layout{display:grid;gap:18px;grid-template-columns:minmax(0,1.35fr) minmax(320px,.9fr);align-items:start}.editor-form,.editor-side{display:grid;gap:16px}.editor-card{border-radius:20px;border:1px solid color-mix(in srgb,var(--md-sys-color-outline, #c5c7ce) 72%,transparent);background:linear-gradient(180deg,#1263b408,#1263b400)}.editor-grid{display:grid;gap:14px;grid-template-columns:repeat(auto-fit,minmax(220px,1fr))}.editor-stack{display:grid;gap:14px}.editor-row-card{padding:14px;border-radius:16px;border:1px solid color-mix(in srgb,var(--md-sys-color-outline, #c5c7ce) 54%,transparent);background:color-mix(in srgb,var(--md-sys-color-surface, #fff) 92%,rgba(18,99,180,.04))}.editor-row-actions{display:flex;justify-content:flex-end}.editor-field{width:100%}.editor-issues{display:grid;gap:10px;margin:0;padding:0;list-style:none}.editor-issue{padding:12px 14px;border-radius:14px;background:color-mix(in srgb,var(--md-sys-color-error, #b3261e) 8%,transparent);border:1px solid color-mix(in srgb,var(--md-sys-color-error, #b3261e) 18%,transparent)}.editor-issue strong{display:block;margin-bottom:4px}.editor-caption{margin:0 0 12px;color:var(--md-sys-color-on-surface-variant, #5a5d67);font-size:.92rem}.editor-empty{padding:18px;border-radius:14px;background:color-mix(in srgb,var(--md-sys-color-surface-variant, #eceff4) 78%,transparent)}@media(max-width:960px){.editor-layout{grid-template-columns:minmax(0,1fr)}}\n"] }]
6852
+ ], 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 </div>\n }\n\n @case ('motion') {\n <div class=\"editor-grid\">\n <mat-slide-toggle\n [ngModel]=\"normalizedDocument().motion?.enabled !== false\"\n (ngModelChange)=\"setMotionEnabled($event)\"\n [disabled]=\"isReadonly()\"\n >\n {{ t('praxis.charts.editor.field.motionEnabled', 'Enable animations') }}\n </mat-slide-toggle>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.motionPreset', 'Motion preset') }}</mat-label>\n <mat-select\n [ngModel]=\"normalizedDocument().motion?.preset || 'standard'\"\n (ngModelChange)=\"setMotionPreset($event)\"\n [disabled]=\"isReadonly() || normalizedDocument().motion?.enabled === false\"\n >\n @for (preset of motionPresets; track preset) {\n <mat-option [value]=\"preset\">\n {{ t('praxis.charts.editor.motionPreset.' + preset, preset) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n </div>\n }\n\n @case ('appearance') {\n <div class=\"editor-stack\">\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.appearance.featuresTitle', 'Display features') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-grid\">\n <mat-slide-toggle\n [ngModel]=\"featureEnabled('legend')\"\n (ngModelChange)=\"setFeatureEnabled('legend', $event)\"\n [disabled]=\"isReadonly()\"\n >\n {{ t('praxis.charts.editor.field.legendEnabled', 'Show legend') }}\n </mat-slide-toggle>\n\n <mat-slide-toggle\n [ngModel]=\"featureEnabled('labels')\"\n (ngModelChange)=\"setFeatureEnabled('labels', $event)\"\n [disabled]=\"isReadonly()\"\n >\n {{ t('praxis.charts.editor.field.labelsEnabled', 'Show labels') }}\n </mat-slide-toggle>\n\n <mat-slide-toggle\n [ngModel]=\"featureEnabled('tooltip')\"\n (ngModelChange)=\"setFeatureEnabled('tooltip', $event)\"\n [disabled]=\"isReadonly()\"\n >\n {{ t('praxis.charts.editor.field.tooltipEnabled', 'Show tooltip') }}\n </mat-slide-toggle>\n </mat-card-content>\n </mat-card>\n\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.appearance.paletteTitle', 'Palette') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-stack\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.themeVariant', 'Theme variant') }}</mat-label>\n <mat-select [ngModel]=\"themeVariantValue()\" (ngModelChange)=\"setThemeVariant($event)\" [disabled]=\"isReadonly()\">\n <mat-option [value]=\"''\">\n {{ t('praxis.charts.editor.themeVariant.none', 'No variant') }}\n </mat-option>\n @for (variant of themeVariants; track variant) {\n <mat-option [value]=\"variant\">\n {{ t('praxis.charts.editor.themeVariant.' + variant, variant) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.paletteMode', 'Palette mode') }}</mat-label>\n <mat-select [ngModel]=\"paletteModeValue()\" (ngModelChange)=\"setPaletteMode($event)\" [disabled]=\"isReadonly()\">\n @for (mode of paletteModes; track mode) {\n <mat-option [value]=\"mode\">\n {{ t('praxis.charts.editor.paletteMode.' + mode, mode) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n @if (paletteModeValue() === 'token') {\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.paletteToken', 'Palette token') }}</mat-label>\n <mat-select [ngModel]=\"paletteTokenValue()\" (ngModelChange)=\"setPaletteToken($event)\" [disabled]=\"isReadonly()\">\n @for (token of paletteTokens; track token) {\n <mat-option [value]=\"token\">\n {{ t('praxis.charts.editor.paletteToken.' + token, token) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n } @else {\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.palette', 'Palette colors') }}</mat-label>\n <textarea\n matInput\n rows=\"3\"\n [ngModel]=\"paletteValue()\"\n (ngModelChange)=\"setPalette($event)\"\n [disabled]=\"isReadonly()\"\n ></textarea>\n </mat-form-field>\n }\n\n <p class=\"editor-caption\">\n {{ t('praxis.charts.editor.appearance.paletteHint', 'Use a registered token or comma-separated colors to persist theme.palette in the canonical contract.') }}\n </p>\n </mat-card-content>\n </mat-card>\n\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.appearance.surfaceTitle', 'Surface') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content>\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.surfaceMode', 'Surface mode') }}</mat-label>\n <mat-select [ngModel]=\"surfaceModeValue()\" (ngModelChange)=\"setSurfaceMode($event)\" [disabled]=\"isReadonly()\">\n @for (mode of surfaceModes; track mode) {\n <mat-option [value]=\"mode\">\n {{ t('praxis.charts.editor.surface.' + mode, mode) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n <p class=\"editor-caption\">\n {{ t('praxis.charts.editor.appearance.surfaceHint', 'Use embedded for dashboard widgets and contained only when the chart must own its visual surface.') }}\n </p>\n </mat-card-content>\n </mat-card>\n\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.appearance.statesTitle', 'State messages') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-stack\">\n <div class=\"editor-row-card\">\n <div class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.emptyTitle', 'Empty title') }}</mat-label>\n <input matInput [ngModel]=\"stateTitle('empty')\" (ngModelChange)=\"setStateTitle('empty', $event)\" [disabled]=\"isReadonly()\" />\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.emptyDescription', 'Empty description') }}</mat-label>\n <textarea matInput rows=\"2\" [ngModel]=\"stateDescription('empty')\" (ngModelChange)=\"setStateDescription('empty', $event)\" [disabled]=\"isReadonly()\"></textarea>\n </mat-form-field>\n </div>\n </div>\n\n <div class=\"editor-row-card\">\n <div class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.loadingTitle', 'Loading title') }}</mat-label>\n <input matInput [ngModel]=\"stateTitle('loading')\" (ngModelChange)=\"setStateTitle('loading', $event)\" [disabled]=\"isReadonly()\" />\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.loadingDescription', 'Loading description') }}</mat-label>\n <textarea matInput rows=\"2\" [ngModel]=\"stateDescription('loading')\" (ngModelChange)=\"setStateDescription('loading', $event)\" [disabled]=\"isReadonly()\"></textarea>\n </mat-form-field>\n </div>\n </div>\n\n <div class=\"editor-row-card\">\n <div class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.errorTitle', 'Error title') }}</mat-label>\n <input matInput [ngModel]=\"stateTitle('error')\" (ngModelChange)=\"setStateTitle('error', $event)\" [disabled]=\"isReadonly()\" />\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.errorDescription', 'Error description') }}</mat-label>\n <textarea matInput rows=\"2\" [ngModel]=\"stateDescription('error')\" (ngModelChange)=\"setStateDescription('error', $event)\" [disabled]=\"isReadonly()\"></textarea>\n </mat-form-field>\n </div>\n </div>\n </mat-card-content>\n </mat-card>\n </div>\n }\n\n @case ('analytics') {\n <div class=\"editor-stack\">\n @if (showComboPanel()) {\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.specialization.comboTitle', 'Combo guidance') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content>\n <p class=\"editor-caption\">\n {{ t('praxis.charts.editor.specialization.comboHint', 'Combo charts require at least two metrics and allow per-metric axis and series kind mapping.') }}\n </p>\n </mat-card-content>\n </mat-card>\n }\n\n @if (showPieDonutPanel()) {\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.specialization.pieDonutTitle', 'Composition guidance') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content>\n <p class=\"editor-caption\">\n {{ t('praxis.charts.editor.specialization.pieDonutHint', 'Pie and donut charts keep only the first metric and use the first dimension as the category segment.') }}\n </p>\n </mat-card-content>\n </mat-card>\n }\n\n @if (showScatterPanel()) {\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.specialization.scatterTitle', 'Scatter guidance') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content>\n <p class=\"editor-caption\">\n {{ t('praxis.charts.editor.specialization.scatterHint', 'Scatter charts use the first dimension as X and the first metric as Y, so keep both fields mapped.') }}\n </p>\n </mat-card-content>\n </mat-card>\n }\n\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.analytics.dimensionsTitle', 'Dimensions') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-stack\">\n @for (dimension of dimensions(); track $index) {\n <div class=\"editor-row-card\" [attr.data-testid]=\"'chart-editor-dimension-row-' + $index\">\n <div class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\" [attr.data-testid]=\"'chart-editor-dimension-field-' + $index\">\n <mat-label>{{ t('praxis.charts.editor.field.dimension', 'Dimension') }}</mat-label>\n @if (fieldOptions('dimension').length) {\n <mat-select [ngModel]=\"dimension.field || ''\" (ngModelChange)=\"setDimensionField($index, $event)\" [disabled]=\"isReadonly()\">\n @for (field of fieldOptions('dimension'); track field.field) {\n <mat-option [value]=\"field.field\">{{ fieldOptionLabel(field) }}</mat-option>\n }\n </mat-select>\n } @else {\n <mat-select [ngModel]=\"dimension.field || ''\" (ngModelChange)=\"setDimensionField($index, $event)\" [disabled]=\"true\"></mat-select>\n <mat-hint>{{ t('praxis.charts.editor.catalog.dimensionFieldsMissing', 'No governed dimensions are available for the current resource and operation.') }}</mat-hint>\n }\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\" [attr.data-testid]=\"'chart-editor-dimension-role-' + $index\">\n <mat-label>{{ t('praxis.charts.editor.field.dimensionRole', 'Dimension role') }}</mat-label>\n <mat-select [ngModel]=\"dimension.role || 'category'\" (ngModelChange)=\"setDimensionRole($index, $event)\" [disabled]=\"isReadonly()\">\n @for (role of dimensionRoles; track role) {\n <mat-option [value]=\"role\">\n {{ t('praxis.charts.editor.dimensionRole.' + role, role) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n </div>\n\n <div class=\"editor-row-actions\">\n <button mat-button type=\"button\" (click)=\"removeDimension($index)\" [disabled]=\"isReadonly() || dimensions().length <= 1\">\n {{ t('praxis.charts.editor.analytics.removeDimension', 'Remove dimension') }}\n </button>\n </div>\n </div>\n }\n\n <div>\n <button mat-stroked-button type=\"button\" data-testid=\"chart-editor-add-dimension\" (click)=\"addDimension()\" [disabled]=\"isReadonly()\">\n {{ t('praxis.charts.editor.analytics.addDimension', 'Add dimension') }}\n </button>\n </div>\n </mat-card-content>\n </mat-card>\n\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.analytics.metricsTitle', 'Metrics') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-stack\">\n @for (metric of metrics(); track $index) {\n <div class=\"editor-row-card\" [attr.data-testid]=\"'chart-editor-metric-row-' + $index\">\n <div class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\" [attr.data-testid]=\"'chart-editor-metric-field-' + $index\">\n <mat-label>{{ t('praxis.charts.editor.field.metric', 'Metric') }}</mat-label>\n @if (fieldOptions('metric').length) {\n <mat-select [ngModel]=\"metric.field || ''\" (ngModelChange)=\"setMetricField($index, $event)\" [disabled]=\"isReadonly()\">\n @for (field of fieldOptions('metric'); track field.field) {\n <mat-option [value]=\"field.field\">{{ fieldOptionLabel(field) }}</mat-option>\n }\n </mat-select>\n } @else {\n <mat-select [ngModel]=\"metric.field || ''\" (ngModelChange)=\"setMetricField($index, $event)\" [disabled]=\"true\"></mat-select>\n <mat-hint>{{ t('praxis.charts.editor.catalog.metricFieldsMissing', 'No governed metrics are available for the current resource and operation.') }}</mat-hint>\n }\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\" [attr.data-testid]=\"'chart-editor-metric-label-' + $index\">\n <mat-label>{{ t('praxis.charts.editor.field.metricLabel', 'Metric label') }}</mat-label>\n <input matInput [ngModel]=\"metric.label || ''\" (ngModelChange)=\"setMetricLabel($index, $event)\" [disabled]=\"isReadonly()\" />\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\" [attr.data-testid]=\"'chart-editor-metric-aggregation-' + $index\">\n <mat-label>{{ t('praxis.charts.editor.field.metricAggregation', 'Aggregation') }}</mat-label>\n <mat-select [ngModel]=\"metric.aggregation || 'sum'\" (ngModelChange)=\"setMetricAggregation($index, $event)\" [disabled]=\"isReadonly()\">\n @for (aggregation of metricAggregationOptions(metric.field); track aggregation) {\n <mat-option [value]=\"aggregation\">\n {{ t('praxis.charts.editor.metricAggregation.' + aggregation, aggregation) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n @if (showMetricAxisControls()) {\n <mat-form-field class=\"editor-field\" appearance=\"outline\" [attr.data-testid]=\"'chart-editor-metric-axis-' + $index\">\n <mat-label>{{ t('praxis.charts.editor.field.metricAxis', 'Axis') }}</mat-label>\n <mat-select [ngModel]=\"metric.axis || 'primary'\" (ngModelChange)=\"setMetricAxis($index, $event)\" [disabled]=\"isReadonly()\">\n @for (axis of metricAxes; track axis) {\n <mat-option [value]=\"axis\">\n {{ t('praxis.charts.editor.metricAxis.' + axis, axis) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n }\n\n @if (showMetricSeriesKindControls()) {\n <mat-form-field class=\"editor-field\" appearance=\"outline\" [attr.data-testid]=\"'chart-editor-metric-series-kind-' + $index\">\n <mat-label>{{ t('praxis.charts.editor.field.metricSeriesKind', 'Series kind') }}</mat-label>\n <mat-select [ngModel]=\"metric.seriesKind || 'bar'\" (ngModelChange)=\"setMetricSeriesKind($index, $event)\" [disabled]=\"isReadonly()\">\n @for (seriesKind of metricSeriesKinds; track seriesKind) {\n <mat-option [value]=\"seriesKind\">\n {{ t('praxis.charts.editor.metricSeriesKind.' + seriesKind, seriesKind) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n }\n </div>\n\n <div class=\"editor-row-actions\">\n <button mat-button type=\"button\" [attr.data-testid]=\"'chart-editor-remove-metric-' + $index\" (click)=\"removeMetric($index)\" [disabled]=\"isReadonly() || metrics().length <= 1\">\n {{ t('praxis.charts.editor.analytics.removeMetric', 'Remove metric') }}\n </button>\n </div>\n </div>\n }\n\n <div>\n <button mat-stroked-button type=\"button\" data-testid=\"chart-editor-add-metric\" (click)=\"addMetric()\" [disabled]=\"isReadonly()\">\n {{ t('praxis.charts.editor.analytics.addMetric', 'Add metric') }}\n </button>\n </div>\n </mat-card-content>\n </mat-card>\n </div>\n }\n\n @case ('events') {\n <div class=\"editor-stack\">\n <mat-card class=\"editor-card\" data-testid=\"chart-editor-event-pointClick\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.events.pointClickTitle', 'Point click') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventAction', 'Action') }}</mat-label>\n <mat-select data-testid=\"chart-editor-event-pointClick-action\" [ngModel]=\"eventAction('pointClick')\" (ngModelChange)=\"setEventAction('pointClick', $event)\" [disabled]=\"isReadonly()\">\n <mat-option value=\"\">{{ t('praxis.charts.editor.events.none', 'None') }}</mat-option>\n @for (action of eventActionOptions; track action) {\n <mat-option [value]=\"action\">\n {{ t('praxis.charts.editor.eventAction.' + action, action) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventTarget', 'Target') }}</mat-label>\n <mat-select data-testid=\"chart-editor-event-pointClick-target\" [ngModel]=\"eventTarget('pointClick')\" (ngModelChange)=\"setEventTarget('pointClick', $event)\" [disabled]=\"isReadonly() || !eventAction('pointClick') || targetCatalogUnavailable(eventAction('pointClick'), 'pointClick')\">\n @for (target of targetOptions(eventAction('pointClick'), 'pointClick'); track target.id) {\n <mat-option [value]=\"target.id\">{{ target.label }}</mat-option>\n }\n </mat-select>\n @if (targetCatalogUnavailable(eventAction('pointClick'), 'pointClick')) {\n <mat-hint>{{ t('praxis.charts.editor.catalog.targetMissing', 'Target catalog is required for this governed event action.') }}</mat-hint>\n }\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventMapping', 'Mapping') }}</mat-label>\n <textarea\n matInput\n data-testid=\"chart-editor-event-pointClick-mapping\"\n rows=\"4\"\n [ngModel]=\"eventMappingText('pointClick')\"\n (ngModelChange)=\"setEventMapping('pointClick', $event)\"\n [disabled]=\"isReadonly() || !eventAction('pointClick')\"\n ></textarea>\n </mat-form-field>\n </mat-card-content>\n </mat-card>\n\n <mat-card class=\"editor-card\" data-testid=\"chart-editor-event-selectionChange\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.events.selectionChangeTitle', 'Selection change') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventAction', 'Action') }}</mat-label>\n <mat-select data-testid=\"chart-editor-event-selectionChange-action\" [ngModel]=\"eventAction('selectionChange')\" (ngModelChange)=\"setEventAction('selectionChange', $event)\" [disabled]=\"isReadonly()\">\n <mat-option value=\"\">{{ t('praxis.charts.editor.events.none', 'None') }}</mat-option>\n @for (action of eventActionOptions; track action) {\n <mat-option [value]=\"action\">\n {{ t('praxis.charts.editor.eventAction.' + action, action) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventTarget', 'Target') }}</mat-label>\n <mat-select data-testid=\"chart-editor-event-selectionChange-target\" [ngModel]=\"eventTarget('selectionChange')\" (ngModelChange)=\"setEventTarget('selectionChange', $event)\" [disabled]=\"isReadonly() || !eventAction('selectionChange') || targetCatalogUnavailable(eventAction('selectionChange'), 'selectionChange')\">\n @for (target of targetOptions(eventAction('selectionChange'), 'selectionChange'); track target.id) {\n <mat-option [value]=\"target.id\">{{ target.label }}</mat-option>\n }\n </mat-select>\n @if (targetCatalogUnavailable(eventAction('selectionChange'), 'selectionChange')) {\n <mat-hint>{{ t('praxis.charts.editor.catalog.targetMissing', 'Target catalog is required for this governed event action.') }}</mat-hint>\n }\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventMapping', 'Mapping') }}</mat-label>\n <textarea\n matInput\n data-testid=\"chart-editor-event-selectionChange-mapping\"\n rows=\"4\"\n [ngModel]=\"eventMappingText('selectionChange')\"\n (ngModelChange)=\"setEventMapping('selectionChange', $event)\"\n [disabled]=\"isReadonly() || !eventAction('selectionChange')\"\n ></textarea>\n </mat-form-field>\n </mat-card-content>\n </mat-card>\n\n <mat-card class=\"editor-card\" data-testid=\"chart-editor-event-drillDown\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.events.drillDownTitle', 'Drill down') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventAction', 'Action') }}</mat-label>\n <mat-select data-testid=\"chart-editor-event-drillDown-action\" [ngModel]=\"eventAction('drillDown')\" (ngModelChange)=\"setEventAction('drillDown', $event)\" [disabled]=\"isReadonly()\">\n <mat-option value=\"\">{{ t('praxis.charts.editor.events.none', 'None') }}</mat-option>\n @for (action of eventActionOptions; track action) {\n <mat-option [value]=\"action\">\n {{ t('praxis.charts.editor.eventAction.' + action, action) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventTarget', 'Target') }}</mat-label>\n <mat-select data-testid=\"chart-editor-event-drillDown-target\" [ngModel]=\"eventTarget('drillDown')\" (ngModelChange)=\"setEventTarget('drillDown', $event)\" [disabled]=\"isReadonly() || !eventAction('drillDown') || targetCatalogUnavailable(eventAction('drillDown'), 'drillDown')\">\n @for (target of targetOptions(eventAction('drillDown'), 'drillDown'); track target.id) {\n <mat-option [value]=\"target.id\">{{ target.label }}</mat-option>\n }\n </mat-select>\n @if (targetCatalogUnavailable(eventAction('drillDown'), 'drillDown')) {\n <mat-hint>{{ t('praxis.charts.editor.catalog.targetMissing', 'Target catalog is required for this governed event action.') }}</mat-hint>\n }\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventMapping', 'Mapping') }}</mat-label>\n <textarea\n matInput\n data-testid=\"chart-editor-event-drillDown-mapping\"\n rows=\"4\"\n [ngModel]=\"eventMappingText('drillDown')\"\n (ngModelChange)=\"setEventMapping('drillDown', $event)\"\n [disabled]=\"isReadonly() || !eventAction('drillDown')\"\n ></textarea>\n </mat-form-field>\n </mat-card-content>\n </mat-card>\n\n <mat-card class=\"editor-card\" data-testid=\"chart-editor-event-crossFilter\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.events.crossFilterTitle', 'Cross filter') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content class=\"editor-grid\">\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventAction', 'Action') }}</mat-label>\n <mat-select data-testid=\"chart-editor-event-crossFilter-action\" [ngModel]=\"eventAction('crossFilter')\" (ngModelChange)=\"setEventAction('crossFilter', $event)\" [disabled]=\"isReadonly()\">\n <mat-option value=\"\">{{ t('praxis.charts.editor.events.none', 'None') }}</mat-option>\n @for (action of eventActionOptions; track action) {\n <mat-option [value]=\"action\">\n {{ t('praxis.charts.editor.eventAction.' + action, action) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventTarget', 'Target') }}</mat-label>\n <mat-select data-testid=\"chart-editor-event-crossFilter-target\" [ngModel]=\"eventTarget('crossFilter')\" (ngModelChange)=\"setEventTarget('crossFilter', $event)\" [disabled]=\"isReadonly() || !eventAction('crossFilter') || targetCatalogUnavailable(eventAction('crossFilter'), 'crossFilter')\">\n @for (target of targetOptions(eventAction('crossFilter'), 'crossFilter'); track target.id) {\n <mat-option [value]=\"target.id\">{{ target.label }}</mat-option>\n }\n </mat-select>\n @if (targetCatalogUnavailable(eventAction('crossFilter'), 'crossFilter')) {\n <mat-hint>{{ t('praxis.charts.editor.catalog.targetMissing', 'Target catalog is required for this governed event action.') }}</mat-hint>\n }\n </mat-form-field>\n\n <mat-form-field class=\"editor-field\" appearance=\"outline\">\n <mat-label>{{ t('praxis.charts.editor.field.eventMapping', 'Mapping') }}</mat-label>\n <textarea\n matInput\n data-testid=\"chart-editor-event-crossFilter-mapping\"\n rows=\"4\"\n [ngModel]=\"eventMappingText('crossFilter')\"\n (ngModelChange)=\"setEventMapping('crossFilter', $event)\"\n [disabled]=\"isReadonly() || !eventAction('crossFilter')\"\n ></textarea>\n </mat-form-field>\n </mat-card-content>\n </mat-card>\n </div>\n }\n\n @case ('preview') {\n <p class=\"editor-caption\">\n {{ t('praxis.charts.editor.preview.caption', 'Local preview derived from the canonical contract without remote calls.') }}\n </p>\n }\n }\n </mat-card-content>\n </mat-card>\n </div>\n\n <div class=\"editor-side\">\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.issues.title', 'Validation issues') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content>\n @if (issues().length) {\n <ul class=\"editor-issues\">\n @for (issue of issues(); track issueTrackBy($index, issue)) {\n <li class=\"editor-issue\">\n <strong>{{ issue.field }}</strong>\n <span>{{ issue.message }}</span>\n </li>\n }\n </ul>\n } @else {\n <div class=\"editor-empty\">\n {{ t('praxis.charts.editor.issues.empty', 'No issues were identified.') }}\n </div>\n }\n </mat-card-content>\n </mat-card>\n\n <mat-card class=\"editor-card\">\n <mat-card-header>\n <mat-card-title>{{ t('praxis.charts.editor.preview.title', 'Chart preview') }}</mat-card-title>\n </mat-card-header>\n <mat-card-content>\n @if (preview(); as chartPreview) {\n <p class=\"editor-caption\">\n {{ t('praxis.charts.editor.preview.caption', 'Local preview derived from the canonical contract without remote calls.') }}\n </p>\n <praxis-chart [config]=\"chartPreview.config\" [data]=\"chartPreview.data\"></praxis-chart>\n } @else {\n <div class=\"editor-empty\">\n {{ t('praxis.charts.editor.preview.invalid', 'Preview is unavailable while the contract has blocking errors.') }}\n </div>\n }\n </mat-card-content>\n </mat-card>\n </div>\n </div>\n</div>\n", styles: [":host{display:block;min-width:0;container:praxis-chart-editor / inline-size;color:var(--md-sys-color-on-surface, #1a1b20)}.editor-shell{display:grid;gap:18px;min-width:0}.editor-nav{display:flex;gap:8px;flex-wrap:wrap}.editor-nav button.active{background:color-mix(in srgb,var(--md-sys-color-primary, #1263b4) 18%,transparent);color:var(--md-sys-color-primary, #1263b4)}.editor-layout{display:grid;gap:18px;grid-template-columns:minmax(0,1.35fr) minmax(320px,.9fr);align-items:start}.editor-form,.editor-side{display:grid;gap:16px;min-width:0}.editor-card{box-sizing:border-box;width:100%;min-width:0;border-radius:20px;border:1px solid color-mix(in srgb,var(--md-sys-color-outline, #c5c7ce) 72%,transparent);background:linear-gradient(180deg,#1263b408,#1263b400)}.editor-grid{display:grid;gap:14px;grid-template-columns:repeat(auto-fit,minmax(220px,1fr))}.editor-stack{display:grid;gap:14px}.editor-row-card{padding:14px;border-radius:16px;border:1px solid color-mix(in srgb,var(--md-sys-color-outline, #c5c7ce) 54%,transparent);background:color-mix(in srgb,var(--md-sys-color-surface, #fff) 92%,rgba(18,99,180,.04))}.editor-row-actions{display:flex;justify-content:flex-end}.editor-field{width:100%}.editor-issues{display:grid;gap:10px;margin:0;padding:0;list-style:none}.editor-issue{padding:12px 14px;border-radius:14px;background:color-mix(in srgb,var(--md-sys-color-error, #b3261e) 8%,transparent);border:1px solid color-mix(in srgb,var(--md-sys-color-error, #b3261e) 18%,transparent)}.editor-issue strong{display:block;margin-bottom:4px}.editor-caption{margin:0 0 12px;color:var(--md-sys-color-on-surface-variant, #5a5d67);font-size:.92rem}.editor-empty{padding:18px;border-radius:14px;background:color-mix(in srgb,var(--md-sys-color-surface-variant, #eceff4) 78%,transparent)}.editor-side praxis-chart{display:block;width:100%;min-width:0;max-width:100%}@container praxis-chart-editor (max-width: 840px){.editor-layout{grid-template-columns:minmax(0,1fr)}}@media(max-width:960px){.editor-layout{grid-template-columns:minmax(0,1fr)}}\n"] }]
5223
6853
  }], 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"] }] } });
5224
6854
 
5225
6855
  var praxisChartConfigEditor = /*#__PURE__*/Object.freeze({
@@ -5228,66 +6858,118 @@ var praxisChartConfigEditor = /*#__PURE__*/Object.freeze({
5228
6858
  });
5229
6859
 
5230
6860
  class PraxisChartWidgetConfigEditor {
6861
+ i18n = inject(PraxisI18nService);
5231
6862
  inputs = null;
5232
- chartEditor;
6863
+ context = null;
6864
+ contextDiagnostics = [];
6865
+ activeChartEditor;
6866
+ chartEditorSubscription = new Subscription();
6867
+ set chartEditor(value) {
6868
+ if (this.activeChartEditor === value) {
6869
+ return;
6870
+ }
6871
+ this.chartEditorSubscription.unsubscribe();
6872
+ this.chartEditorSubscription = new Subscription();
6873
+ this.activeChartEditor = value;
6874
+ this.chartEditorDirty = false;
6875
+ this.chartEditorValid = true;
6876
+ if (value) {
6877
+ this.bindChartEditor(value);
6878
+ }
6879
+ this.updateDirty();
6880
+ this.updateValidity();
6881
+ }
6882
+ get chartEditor() {
6883
+ return this.activeChartEditor;
6884
+ }
5233
6885
  isDirty$ = new BehaviorSubject(false);
5234
6886
  isValid$ = new BehaviorSubject(true);
5235
6887
  isBusy$ = new BehaviorSubject(false);
5236
- subscription = new Subscription();
6888
+ chartEditorDirty = false;
5237
6889
  chartEditorValid = true;
5238
6890
  queryContextDirty = false;
6891
+ baselineQueryContextText = '';
5239
6892
  queryContextText = '';
5240
6893
  queryContextError = '';
5241
6894
  get chartDocument() {
5242
6895
  return this.inputs?.chartDocument ?? null;
5243
6896
  }
5244
6897
  get availableResources() {
5245
- return this.inputs?.availableResources ?? [];
6898
+ return this.context?.availableResources ?? this.inputs?.availableResources ?? [];
5246
6899
  }
5247
6900
  get availableFields() {
5248
- return this.inputs?.availableFields ?? [];
6901
+ return this.context?.availableFields ?? this.inputs?.availableFields ?? [];
5249
6902
  }
5250
6903
  get availableTargets() {
5251
- return this.inputs?.availableTargets ?? [];
5252
- }
5253
- ngAfterViewInit() {
5254
- if (!this.chartEditor) {
5255
- return;
5256
- }
5257
- this.subscription.add(this.chartEditor.isDirty$.subscribe((value) => this.isDirty$.next(value || this.queryContextDirty)));
5258
- this.subscription.add(this.chartEditor.isValid$.subscribe((value) => {
5259
- this.chartEditorValid = value;
5260
- this.updateValidity();
5261
- }));
5262
- this.subscription.add(this.chartEditor.isBusy$.subscribe((value) => this.isBusy$.next(value)));
6904
+ return this.context?.availableTargets ?? this.inputs?.availableTargets ?? [];
5263
6905
  }
5264
6906
  ngOnChanges(changes) {
5265
- if (changes['inputs']) {
5266
- this.queryContextText = this.formatQueryContext(this.inputs?.queryContext);
5267
- this.queryContextError = '';
5268
- this.queryContextDirty = false;
5269
- this.isDirty$.next(this.chartEditor?.isDirty$.value ?? false);
5270
- this.updateValidity();
6907
+ if (changes['inputs'] && (changes['inputs'].firstChange || !this.isDirty$.value)) {
6908
+ this.rebaseQueryContext();
5271
6909
  }
5272
6910
  }
5273
6911
  ngOnDestroy() {
5274
- this.subscription.unsubscribe();
6912
+ this.chartEditorSubscription.unsubscribe();
5275
6913
  }
5276
6914
  setQueryContextText(value) {
5277
6915
  this.queryContextText = value;
5278
- this.queryContextDirty = true;
6916
+ this.queryContextDirty = value !== this.baselineQueryContextText;
5279
6917
  this.queryContextError = this.validateQueryContextText(value);
5280
- this.isDirty$.next(true);
6918
+ this.updateDirty();
5281
6919
  this.updateValidity();
5282
6920
  }
5283
6921
  getSettingsValue() {
5284
- return this.buildValue(this.chartEditor?.getSettingsValue());
6922
+ return this.buildValue(this.getChartDocumentForApply());
5285
6923
  }
5286
6924
  onSave() {
5287
- return this.buildValue(this.chartEditor?.onSave?.() ?? this.chartEditor?.getSettingsValue());
6925
+ const value = this.buildValue(this.isValid$.value
6926
+ ? this.getChartDocumentForSave()
6927
+ : this.getChartDocumentForApply());
6928
+ if (this.isValid$.value) {
6929
+ this.baselineQueryContextText = this.queryContextText;
6930
+ this.queryContextDirty = false;
6931
+ this.updateDirty();
6932
+ }
6933
+ return value;
5288
6934
  }
5289
6935
  reset() {
5290
6936
  this.chartEditor?.reset();
6937
+ this.queryContextText = this.baselineQueryContextText;
6938
+ this.queryContextError = '';
6939
+ this.queryContextDirty = false;
6940
+ this.updateDirty();
6941
+ this.updateValidity();
6942
+ }
6943
+ t(key, fallback) {
6944
+ return this.i18n.resolve(resolvePraxisChartsText({ key, text: fallback }, fallback));
6945
+ }
6946
+ contextDiagnosticText(diagnostic) {
6947
+ switch (diagnostic.code) {
6948
+ case 'chart-resource-catalog-denied':
6949
+ return this.t('praxis.charts.widget.contextDiagnostics.catalogDenied', 'Access to the governed resource catalog was denied.');
6950
+ case 'chart-resource-catalog-unavailable':
6951
+ return this.t('praxis.charts.widget.contextDiagnostics.catalogUnavailable', 'The governed resource catalog is temporarily unavailable.');
6952
+ case 'chart-resource-catalog-empty':
6953
+ return this.t('praxis.charts.widget.contextDiagnostics.catalogEmpty', 'No authorable stats resources were published by the governed catalog.');
6954
+ case 'chart-resource-catalog-path-invalid':
6955
+ return this.t('praxis.charts.widget.contextDiagnostics.catalogPathInvalid', 'The resource catalog published an unsafe or non-relative operational path.');
6956
+ case 'chart-resource-capabilities-denied':
6957
+ return this.t('praxis.charts.widget.contextDiagnostics.capabilitiesDenied', 'Access to the selected resource capabilities was denied.');
6958
+ case 'chart-resource-capabilities-unavailable':
6959
+ return this.t('praxis.charts.widget.contextDiagnostics.capabilitiesUnavailable', 'The selected resource capabilities are temporarily unavailable.');
6960
+ case 'chart-resource-stats-operations-empty':
6961
+ return this.t('praxis.charts.widget.contextDiagnostics.operationsEmpty', 'The selected resource does not publish authorable stats operations.');
6962
+ case 'chart-resource-stats-fields-empty':
6963
+ return this.t('praxis.charts.widget.contextDiagnostics.fieldsEmpty', 'The selected resource does not publish governed fields for chart authoring.');
6964
+ case 'chart-resource-capabilities-identity-mismatch':
6965
+ return this.t('praxis.charts.widget.contextDiagnostics.identityMismatch', 'The selected resource identity does not match its governed capability snapshot.');
6966
+ case 'chart-resource-capabilities-path-invalid':
6967
+ return this.t('praxis.charts.widget.contextDiagnostics.capabilitiesPathInvalid', 'The selected capability snapshot did not publish a safe governed resource path.');
6968
+ case 'chart-resource-capabilities-path-mismatch':
6969
+ return this.t('praxis.charts.widget.contextDiagnostics.capabilitiesPathMismatch', 'The selected capability snapshot path does not match the operational path published by the catalog.');
6970
+ default:
6971
+ return this.t('praxis.charts.widget.contextDiagnostics.partial', 'Some governed authoring choices could not be materialized from the current page context.');
6972
+ }
5291
6973
  }
5292
6974
  buildValue(document) {
5293
6975
  const inputs = {
@@ -5295,19 +6977,57 @@ class PraxisChartWidgetConfigEditor {
5295
6977
  ...(document ? { chartDocument: document } : {}),
5296
6978
  };
5297
6979
  const queryContext = this.parseQueryContextText(this.queryContextText);
5298
- if (queryContext === undefined) {
6980
+ if (queryContext.valid && queryContext.value === undefined) {
5299
6981
  delete inputs.queryContext;
5300
6982
  }
5301
- else {
5302
- inputs.queryContext = queryContext;
6983
+ else if (queryContext.valid) {
6984
+ inputs.queryContext = queryContext.value;
5303
6985
  }
5304
6986
  if (document?.source?.kind === 'praxis.stats') {
5305
6987
  delete inputs.data;
5306
6988
  }
6989
+ delete inputs.availableResources;
6990
+ delete inputs.availableFields;
6991
+ delete inputs.availableTargets;
6992
+ delete inputs.contextDiagnostics;
5307
6993
  return {
5308
6994
  inputs,
5309
6995
  };
5310
6996
  }
6997
+ bindChartEditor(editor) {
6998
+ this.chartEditorDirty = editor.isDirty$.value;
6999
+ this.chartEditorValid = editor.isValid$.value;
7000
+ this.isBusy$.next(editor.isBusy$.value);
7001
+ this.chartEditorSubscription.add(editor.isDirty$.subscribe((value) => {
7002
+ this.chartEditorDirty = value;
7003
+ this.updateDirty();
7004
+ }));
7005
+ this.chartEditorSubscription.add(editor.isValid$.subscribe((value) => {
7006
+ this.chartEditorValid = value;
7007
+ this.updateValidity();
7008
+ }));
7009
+ this.chartEditorSubscription.add(editor.isBusy$.subscribe((value) => this.isBusy$.next(value)));
7010
+ }
7011
+ getChartDocumentForApply() {
7012
+ return this.chartDocument ? this.chartEditor?.getSettingsValue() : undefined;
7013
+ }
7014
+ getChartDocumentForSave() {
7015
+ if (!this.chartDocument) {
7016
+ return undefined;
7017
+ }
7018
+ return this.chartEditor?.onSave?.() ?? this.chartEditor?.getSettingsValue();
7019
+ }
7020
+ rebaseQueryContext() {
7021
+ this.baselineQueryContextText = this.formatQueryContext(this.inputs?.queryContext);
7022
+ this.queryContextText = this.baselineQueryContextText;
7023
+ this.queryContextError = '';
7024
+ this.queryContextDirty = false;
7025
+ this.updateDirty();
7026
+ this.updateValidity();
7027
+ }
7028
+ updateDirty() {
7029
+ this.isDirty$.next(this.chartEditorDirty || this.queryContextDirty);
7030
+ }
5311
7031
  updateValidity() {
5312
7032
  this.isValid$.next(this.chartEditorValid && !this.queryContextError);
5313
7033
  }
@@ -5326,42 +7046,70 @@ class PraxisChartWidgetConfigEditor {
5326
7046
  const parsed = JSON.parse(trimmed);
5327
7047
  return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
5328
7048
  ? ''
5329
- : 'Query context must be a JSON object.';
7049
+ : this.t('praxis.charts.widget.queryContext.mustBeObject', 'Query context must be a JSON object.');
5330
7050
  }
5331
- catch (error) {
5332
- return error instanceof Error ? error.message : 'Invalid JSON.';
7051
+ catch {
7052
+ return this.t('praxis.charts.widget.queryContext.invalidJson', 'Query context must be valid JSON.');
5333
7053
  }
5334
7054
  }
5335
7055
  parseQueryContextText(value) {
5336
7056
  const trimmed = value.trim();
5337
7057
  if (!trimmed) {
5338
- return undefined;
7058
+ return { valid: true, value: undefined };
7059
+ }
7060
+ try {
7061
+ const parsed = JSON.parse(trimmed);
7062
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
7063
+ return { valid: false };
7064
+ }
7065
+ return { valid: true, value: parsed };
7066
+ }
7067
+ catch {
7068
+ return { valid: false };
5339
7069
  }
5340
- const parsed = JSON.parse(trimmed);
5341
- return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
5342
- ? parsed
5343
- : null;
5344
7070
  }
5345
7071
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: PraxisChartWidgetConfigEditor, deps: [], target: i0.ɵɵFactoryTarget.Component });
5346
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.14", type: PraxisChartWidgetConfigEditor, isStandalone: true, selector: "praxis-chart-widget-config-editor", inputs: { inputs: "inputs" }, viewQueries: [{ propertyName: "chartEditor", first: true, predicate: ["chartEditor"], descendants: true }], usesOnChanges: true, ngImport: i0, template: `
7072
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.14", type: PraxisChartWidgetConfigEditor, isStandalone: true, selector: "praxis-chart-widget-config-editor", inputs: { inputs: "inputs", context: "context", contextDiagnostics: "contextDiagnostics" }, providers: [providePraxisChartsI18n()], viewQueries: [{ propertyName: "chartEditor", first: true, predicate: ["chartEditor"], descendants: true }], usesOnChanges: true, ngImport: i0, template: `
5347
7073
  <section data-testid="chart-widget-config-editor">
5348
- <praxis-chart-config-editor
5349
- #chartEditor
5350
- [document]="chartDocument"
5351
- [availableResources]="availableResources"
5352
- [availableFields]="availableFields"
5353
- [availableTargets]="availableTargets"
5354
- />
7074
+ @if (contextDiagnostics.length) {
7075
+ <section
7076
+ class="chart-widget-config-editor__diagnostics"
7077
+ data-testid="chart-widget-context-diagnostics"
7078
+ role="status"
7079
+ >
7080
+ <strong>{{ t('praxis.charts.widget.contextDiagnostics.title', 'Authoring context') }}</strong>
7081
+ <ul>
7082
+ @for (diagnostic of contextDiagnostics; track diagnostic.code + ':' + (diagnostic.path || '')) {
7083
+ <li [class]="'chart-widget-config-editor__diagnostic--' + (diagnostic.severity || 'info')">
7084
+ {{ contextDiagnosticText(diagnostic) }}
7085
+ </li>
7086
+ }
7087
+ </ul>
7088
+ </section>
7089
+ }
7090
+ @if (chartDocument) {
7091
+ <praxis-chart-config-editor
7092
+ #chartEditor
7093
+ [document]="chartDocument"
7094
+ [availableResources]="availableResources"
7095
+ [availableFields]="availableFields"
7096
+ [availableTargets]="availableTargets"
7097
+ />
7098
+ } @else {
7099
+ <section class="chart-widget-config-editor__notice" data-testid="chart-widget-missing-document">
7100
+ {{ t('praxis.charts.widget.missingDocument', 'This widget does not have a canonical chart document yet. Runtime inputs are preserved until a canonical chartDocument is provided by the host.') }}
7101
+ </section>
7102
+ }
5355
7103
  <section class="chart-widget-config-editor__runtime">
5356
7104
  <label class="chart-widget-config-editor__label" for="chart-query-context">
5357
- Query context
7105
+ {{ t('praxis.charts.widget.queryContext.label', 'Query context') }}
5358
7106
  </label>
5359
7107
  <textarea
5360
7108
  id="chart-query-context"
5361
7109
  class="chart-widget-config-editor__textarea"
5362
7110
  rows="5"
5363
7111
  spellcheck="false"
5364
- aria-label="Query context"
7112
+ [attr.aria-label]="t('praxis.charts.widget.queryContext.ariaLabel', 'Chart query context')"
5365
7113
  data-testid="chart-widget-query-context"
5366
7114
  [ngModel]="queryContextText"
5367
7115
  (ngModelChange)="setQueryContextText($event)"
@@ -5373,29 +7121,51 @@ class PraxisChartWidgetConfigEditor {
5373
7121
  }
5374
7122
  </section>
5375
7123
  </section>
5376
- `, isInline: true, styles: [".chart-widget-config-editor__runtime{display:grid;gap:8px;margin-top:16px}.chart-widget-config-editor__label{font-size:12px;font-weight:600;color:#000000b8}.chart-widget-config-editor__textarea{box-sizing:border-box;width:100%;min-height:112px;padding:10px 12px;border:1px solid rgba(0,0,0,.2);border-radius:6px;font:12px/1.45 ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,monospace;resize:vertical}.chart-widget-config-editor__error{color:#b3261e;font-size:12px}\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: "component", type: PraxisChartConfigEditor, selector: "praxis-chart-config-editor", inputs: ["document", "mode", "readonly", "availableResources", "availableFields", "availableTargets"], outputs: ["apply", "save", "resetChange", "documentChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
7124
+ `, isInline: true, styles: [".chart-widget-config-editor__runtime{display:grid;gap:8px;margin-top:16px}.chart-widget-config-editor__diagnostics{display:grid;gap:6px;margin-bottom:16px;padding:12px 14px;border:1px solid color-mix(in srgb,var(--md-sys-color-tertiary, #765b00) 28%,transparent);border-radius:10px;background:color-mix(in srgb,var(--md-sys-color-tertiary-container, #ffdf92) 42%,var(--md-sys-color-surface, #fff));color:var(--md-sys-color-on-tertiary-container, #271900);font-size:12px;line-height:1.4}.chart-widget-config-editor__diagnostics ul{display:grid;gap:4px;margin:0;padding-left:18px}.chart-widget-config-editor__diagnostic--error{color:var(--md-sys-color-error, #b3261e)}.chart-widget-config-editor__label{font-size:12px;font-weight:600;color:#000000b8}.chart-widget-config-editor__textarea{box-sizing:border-box;width:100%;min-height:112px;padding:10px 12px;border:1px solid rgba(0,0,0,.2);border-radius:6px;font:12px/1.45 ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,monospace;resize:vertical}.chart-widget-config-editor__error{color:#b3261e;font-size:12px}.chart-widget-config-editor__notice{padding:12px;border:1px solid rgba(0,0,0,.12);border-radius:6px;color:#000000b8;font-size:12px;line-height:1.4;background:#00000008}\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: "component", type: PraxisChartConfigEditor, selector: "praxis-chart-config-editor", inputs: ["document", "mode", "readonly", "availableResources", "availableFields", "availableTargets"], outputs: ["apply", "save", "resetChange", "documentChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
5377
7125
  }
5378
7126
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: PraxisChartWidgetConfigEditor, decorators: [{
5379
7127
  type: Component,
5380
7128
  args: [{ selector: 'praxis-chart-widget-config-editor', standalone: true, imports: [FormsModule, PraxisChartConfigEditor], template: `
5381
7129
  <section data-testid="chart-widget-config-editor">
5382
- <praxis-chart-config-editor
5383
- #chartEditor
5384
- [document]="chartDocument"
5385
- [availableResources]="availableResources"
5386
- [availableFields]="availableFields"
5387
- [availableTargets]="availableTargets"
5388
- />
7130
+ @if (contextDiagnostics.length) {
7131
+ <section
7132
+ class="chart-widget-config-editor__diagnostics"
7133
+ data-testid="chart-widget-context-diagnostics"
7134
+ role="status"
7135
+ >
7136
+ <strong>{{ t('praxis.charts.widget.contextDiagnostics.title', 'Authoring context') }}</strong>
7137
+ <ul>
7138
+ @for (diagnostic of contextDiagnostics; track diagnostic.code + ':' + (diagnostic.path || '')) {
7139
+ <li [class]="'chart-widget-config-editor__diagnostic--' + (diagnostic.severity || 'info')">
7140
+ {{ contextDiagnosticText(diagnostic) }}
7141
+ </li>
7142
+ }
7143
+ </ul>
7144
+ </section>
7145
+ }
7146
+ @if (chartDocument) {
7147
+ <praxis-chart-config-editor
7148
+ #chartEditor
7149
+ [document]="chartDocument"
7150
+ [availableResources]="availableResources"
7151
+ [availableFields]="availableFields"
7152
+ [availableTargets]="availableTargets"
7153
+ />
7154
+ } @else {
7155
+ <section class="chart-widget-config-editor__notice" data-testid="chart-widget-missing-document">
7156
+ {{ t('praxis.charts.widget.missingDocument', 'This widget does not have a canonical chart document yet. Runtime inputs are preserved until a canonical chartDocument is provided by the host.') }}
7157
+ </section>
7158
+ }
5389
7159
  <section class="chart-widget-config-editor__runtime">
5390
7160
  <label class="chart-widget-config-editor__label" for="chart-query-context">
5391
- Query context
7161
+ {{ t('praxis.charts.widget.queryContext.label', 'Query context') }}
5392
7162
  </label>
5393
7163
  <textarea
5394
7164
  id="chart-query-context"
5395
7165
  class="chart-widget-config-editor__textarea"
5396
7166
  rows="5"
5397
7167
  spellcheck="false"
5398
- aria-label="Query context"
7168
+ [attr.aria-label]="t('praxis.charts.widget.queryContext.ariaLabel', 'Chart query context')"
5399
7169
  data-testid="chart-widget-query-context"
5400
7170
  [ngModel]="queryContextText"
5401
7171
  (ngModelChange)="setQueryContextText($event)"
@@ -5407,9 +7177,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
5407
7177
  }
5408
7178
  </section>
5409
7179
  </section>
5410
- `, changeDetection: ChangeDetectionStrategy.OnPush, styles: [".chart-widget-config-editor__runtime{display:grid;gap:8px;margin-top:16px}.chart-widget-config-editor__label{font-size:12px;font-weight:600;color:#000000b8}.chart-widget-config-editor__textarea{box-sizing:border-box;width:100%;min-height:112px;padding:10px 12px;border:1px solid rgba(0,0,0,.2);border-radius:6px;font:12px/1.45 ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,monospace;resize:vertical}.chart-widget-config-editor__error{color:#b3261e;font-size:12px}\n"] }]
7180
+ `, changeDetection: ChangeDetectionStrategy.OnPush, providers: [providePraxisChartsI18n()], styles: [".chart-widget-config-editor__runtime{display:grid;gap:8px;margin-top:16px}.chart-widget-config-editor__diagnostics{display:grid;gap:6px;margin-bottom:16px;padding:12px 14px;border:1px solid color-mix(in srgb,var(--md-sys-color-tertiary, #765b00) 28%,transparent);border-radius:10px;background:color-mix(in srgb,var(--md-sys-color-tertiary-container, #ffdf92) 42%,var(--md-sys-color-surface, #fff));color:var(--md-sys-color-on-tertiary-container, #271900);font-size:12px;line-height:1.4}.chart-widget-config-editor__diagnostics ul{display:grid;gap:4px;margin:0;padding-left:18px}.chart-widget-config-editor__diagnostic--error{color:var(--md-sys-color-error, #b3261e)}.chart-widget-config-editor__label{font-size:12px;font-weight:600;color:#000000b8}.chart-widget-config-editor__textarea{box-sizing:border-box;width:100%;min-height:112px;padding:10px 12px;border:1px solid rgba(0,0,0,.2);border-radius:6px;font:12px/1.45 ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,monospace;resize:vertical}.chart-widget-config-editor__error{color:#b3261e;font-size:12px}.chart-widget-config-editor__notice{padding:12px;border:1px solid rgba(0,0,0,.12);border-radius:6px;color:#000000b8;font-size:12px;line-height:1.4;background:#00000008}\n"] }]
5411
7181
  }], propDecorators: { inputs: [{
5412
7182
  type: Input
7183
+ }], context: [{
7184
+ type: Input
7185
+ }], contextDiagnostics: [{
7186
+ type: Input
5413
7187
  }], chartEditor: [{
5414
7188
  type: ViewChild,
5415
7189
  args: ['chartEditor']
@@ -5441,6 +7215,25 @@ const PRAXIS_CHART_PALETTE_DEFAULT_DATA = [
5441
7215
  { category: 'B', value: 32 },
5442
7216
  { category: 'C', value: 24 },
5443
7217
  ];
7218
+ const PRAXIS_CHART_PALETTE_DEFAULT_DOCUMENT = {
7219
+ version: PRAXIS_X_UI_CHART_AUTHORABLE_VERSION,
7220
+ kind: 'bar',
7221
+ chartId: 'palette-chart',
7222
+ title: 'Analytics preview',
7223
+ subtitle: 'Sample chart inserted from the Praxis component palette.',
7224
+ source: {
7225
+ kind: 'derived',
7226
+ },
7227
+ dimensions: [{ field: 'category', role: 'category' }],
7228
+ metrics: [{ field: 'value', aggregation: 'sum', label: 'Value' }],
7229
+ legend: { enabled: true },
7230
+ labels: { enabled: false },
7231
+ tooltip: { enabled: true },
7232
+ motion: {
7233
+ enabled: true,
7234
+ preset: 'standard',
7235
+ },
7236
+ };
5444
7237
  const PRAXIS_CHART_PHASE1_PORTS = [
5445
7238
  {
5446
7239
  id: 'queryContext',
@@ -5503,7 +7296,7 @@ const PRAXIS_CHART_PHASE1_PORTS = [
5503
7296
  label: 'Point Click',
5504
7297
  direction: 'output',
5505
7298
  semanticKind: 'event',
5506
- description: 'Interaction event emitted when a point or series is clicked.',
7299
+ description: 'Raw renderer-neutral point evidence emitted when a point or series is clicked.',
5507
7300
  cardinality: 'stream',
5508
7301
  schema: {
5509
7302
  id: 'PraxisChartPointEvent',
@@ -5512,6 +7305,20 @@ const PRAXIS_CHART_PHASE1_PORTS = [
5512
7305
  },
5513
7306
  exposure: { public: true, group: 'interaction' },
5514
7307
  },
7308
+ {
7309
+ id: 'pointAction',
7310
+ label: 'Point Action',
7311
+ direction: 'output',
7312
+ semanticKind: 'event',
7313
+ description: 'Structured configured pointClick action derived from raw point evidence.',
7314
+ cardinality: 'stream',
7315
+ schema: {
7316
+ id: 'PraxisChartPointActionEvent',
7317
+ kind: 'ts-type',
7318
+ ref: 'PraxisChartPointActionEvent',
7319
+ },
7320
+ exposure: { public: true, group: 'interaction' },
7321
+ },
5515
7322
  {
5516
7323
  id: 'selectionChange',
5517
7324
  label: 'Selection Change',
@@ -5526,6 +7333,20 @@ const PRAXIS_CHART_PHASE1_PORTS = [
5526
7333
  },
5527
7334
  exposure: { public: true, group: 'interaction' },
5528
7335
  },
7336
+ {
7337
+ id: 'drillDown',
7338
+ label: 'Drill Down',
7339
+ direction: 'output',
7340
+ semanticKind: 'event',
7341
+ description: 'Structured drill-down action payload derived from selected chart point evidence.',
7342
+ cardinality: 'stream',
7343
+ schema: {
7344
+ id: 'PraxisChartDrillDownEvent',
7345
+ kind: 'ts-type',
7346
+ ref: 'PraxisChartDrillDownEvent',
7347
+ },
7348
+ exposure: { public: true, group: 'interaction' },
7349
+ },
5529
7350
  {
5530
7351
  id: 'crossFilter',
5531
7352
  label: 'Cross Filter',
@@ -5584,6 +7405,23 @@ const PRAXIS_CHART_COMPONENT_METADATA = {
5584
7405
  component: PraxisChartWidgetConfigEditor,
5585
7406
  title: 'Configure chart',
5586
7407
  },
7408
+ authoringManifestRef: {
7409
+ componentId: 'praxis-chart',
7410
+ source: 'PRAXIS_CHARTS_AUTHORING_MANIFEST',
7411
+ },
7412
+ insertionPresets: [
7413
+ {
7414
+ id: 'canonical-chart',
7415
+ label: 'Canonical chart',
7416
+ description: 'Creates a chart widget from the canonical x-ui.chart authoring document.',
7417
+ icon: 'bar_chart',
7418
+ inputs: {
7419
+ chartDocument: PRAXIS_CHART_PALETTE_DEFAULT_DOCUMENT,
7420
+ data: PRAXIS_CHART_PALETTE_DEFAULT_DATA,
7421
+ enableCustomization: true,
7422
+ },
7423
+ },
7424
+ ],
5587
7425
  inputs: [
5588
7426
  {
5589
7427
  name: 'config',
@@ -5601,6 +7439,7 @@ const PRAXIS_CHART_COMPONENT_METADATA = {
5601
7439
  name: 'chartDocument',
5602
7440
  type: 'PraxisXUiChartContract | null',
5603
7441
  description: 'Optional canonical x-ui.chart document used as the authoring source of truth for settings-panel integration.',
7442
+ default: PRAXIS_CHART_PALETTE_DEFAULT_DOCUMENT,
5604
7443
  },
5605
7444
  {
5606
7445
  name: 'queryContext',
@@ -5626,19 +7465,19 @@ const PRAXIS_CHART_COMPONENT_METADATA = {
5626
7465
  {
5627
7466
  name: 'availableResources',
5628
7467
  type: 'ReadonlyArray<ChartEditorResourceOption>',
5629
- description: 'Catalog of remote resources exposed to the runtime chart editor when customization is enabled.',
7468
+ description: 'Transient governed resource catalog. id is the stable discovery identity and path is the root-relative operational value persisted to chartDocument.source.resource; the catalog itself is not persisted.',
5630
7469
  default: [],
5631
7470
  },
5632
7471
  {
5633
7472
  name: 'availableFields',
5634
7473
  type: 'ReadonlyArray<ChartEditorFieldOption>',
5635
- description: 'Catalog of semantic fields exposed to the runtime chart editor when customization is enabled.',
7474
+ description: 'Transient capability-backed field catalog with exact operation, aggregation and distribution terms/histogram eligibility; not intended for widget persistence.',
5636
7475
  default: [],
5637
7476
  },
5638
7477
  {
5639
7478
  name: 'availableTargets',
5640
7479
  type: 'ReadonlyArray<ChartEditorTargetOption>',
5641
- description: 'Catalog of widget or route targets exposed to chart runtime actions in the config editor.',
7480
+ description: 'Transient event-scoped targets from existing top-level links that are compatible with public, non-deprecated widget ports or explicitly writable declared page state. events[] identifies the exact authorized structured Chart events; raw pointClick never authorizes events.pointClick, which uses pointAction. Local transform.output semantic-kind inspection is provisional and does not replace a Core-validated transform-output projection (P2). The catalog is not persisted.',
5642
7481
  default: [],
5643
7482
  },
5644
7483
  ],
@@ -5646,12 +7485,22 @@ const PRAXIS_CHART_COMPONENT_METADATA = {
5646
7485
  {
5647
7486
  name: 'pointClick',
5648
7487
  type: 'PraxisChartPointEvent',
5649
- description: 'Emitted when the host wants to react to a point/series click.',
7488
+ description: 'Raw renderer-neutral point evidence emitted when the host wants to observe a point/series click.',
7489
+ },
7490
+ {
7491
+ name: 'pointAction',
7492
+ type: 'PraxisChartPointActionEvent',
7493
+ description: 'Emitted when a configured pointClick action resolves semantic filters and target evidence from a point/series click.',
5650
7494
  },
5651
7495
  {
5652
7496
  name: 'selectionChange',
5653
7497
  type: 'PraxisChartSelectionEvent',
5654
- description: 'Emitted when declarative chart selection resolves a selected point and its canonical filters.',
7498
+ description: 'Emitted when declarative single-point chart selection resolves selected point evidence and canonical filters.',
7499
+ },
7500
+ {
7501
+ name: 'drillDown',
7502
+ type: 'PraxisChartDrillDownEvent',
7503
+ description: 'Emitted when declarative drill-down resolves action, target and canonical filters from a selected chart point.',
5655
7504
  },
5656
7505
  {
5657
7506
  name: 'crossFilter',
@@ -5695,10 +7544,18 @@ function providePraxisChartsMetadata() {
5695
7544
  return {
5696
7545
  provide: ENVIRONMENT_INITIALIZER,
5697
7546
  multi: true,
5698
- useFactory: (registry) => () => {
5699
- registry.register(PRAXIS_CHART_COMPONENT_METADATA);
7547
+ useFactory: (registry, injector) => () => {
7548
+ registry.register({
7549
+ ...PRAXIS_CHART_COMPONENT_METADATA,
7550
+ configEditor: {
7551
+ ...PRAXIS_CHART_COMPONENT_METADATA.configEditor,
7552
+ contextResolver: (request) => injector
7553
+ .get(PraxisChartConfigEditorContextResolverService)
7554
+ .resolve(request),
7555
+ },
7556
+ });
5700
7557
  },
5701
- deps: [ComponentMetadataRegistry],
7558
+ deps: [ComponentMetadataRegistry, Injector],
5702
7559
  };
5703
7560
  }
5704
7561
 
@@ -5780,7 +7637,9 @@ class PraxisChartShowcaseWidgetComponent {
5780
7637
  enableCustomization = input(false, ...(ngDevMode ? [{ debugName: "enableCustomization" }] : /* istanbul ignore next */ []));
5781
7638
  viewMode = input('chart', ...(ngDevMode ? [{ debugName: "viewMode" }] : /* istanbul ignore next */ []));
5782
7639
  pointClick = output();
7640
+ pointAction = output();
5783
7641
  selectionChange = output();
7642
+ drillDown = output();
5784
7643
  crossFilter = output();
5785
7644
  queryRequest = output();
5786
7645
  loadStateChange = output();
@@ -5896,7 +7755,7 @@ class PraxisChartShowcaseWidgetComponent {
5896
7755
  });
5897
7756
  }
5898
7757
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: PraxisChartShowcaseWidgetComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
5899
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.14", type: PraxisChartShowcaseWidgetComponent, isStandalone: true, selector: "praxis-chart-showcase-widget", inputs: { config: { classPropertyName: "config", publicName: "config", isSignal: true, isRequired: true, transformFunction: null }, data: { classPropertyName: "data", publicName: "data", isSignal: true, isRequired: false, transformFunction: null }, chartDocument: { classPropertyName: "chartDocument", publicName: "chartDocument", isSignal: true, isRequired: false, transformFunction: null }, remoteDataResolver: { classPropertyName: "remoteDataResolver", publicName: "remoteDataResolver", isSignal: true, isRequired: false, transformFunction: null }, enableCustomization: { classPropertyName: "enableCustomization", publicName: "enableCustomization", isSignal: true, isRequired: false, transformFunction: null }, viewMode: { classPropertyName: "viewMode", publicName: "viewMode", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { pointClick: "pointClick", selectionChange: "selectionChange", crossFilter: "crossFilter", queryRequest: "queryRequest", loadStateChange: "loadStateChange" }, ngImport: i0, template: `
7758
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.14", type: PraxisChartShowcaseWidgetComponent, isStandalone: true, selector: "praxis-chart-showcase-widget", inputs: { config: { classPropertyName: "config", publicName: "config", isSignal: true, isRequired: true, transformFunction: null }, data: { classPropertyName: "data", publicName: "data", isSignal: true, isRequired: false, transformFunction: null }, chartDocument: { classPropertyName: "chartDocument", publicName: "chartDocument", isSignal: true, isRequired: false, transformFunction: null }, remoteDataResolver: { classPropertyName: "remoteDataResolver", publicName: "remoteDataResolver", isSignal: true, isRequired: false, transformFunction: null }, enableCustomization: { classPropertyName: "enableCustomization", publicName: "enableCustomization", isSignal: true, isRequired: false, transformFunction: null }, viewMode: { classPropertyName: "viewMode", publicName: "viewMode", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { pointClick: "pointClick", pointAction: "pointAction", selectionChange: "selectionChange", drillDown: "drillDown", crossFilter: "crossFilter", queryRequest: "queryRequest", loadStateChange: "loadStateChange" }, ngImport: i0, template: `
5900
7759
  @if (viewMode() === 'chart') {
5901
7760
  <praxis-chart
5902
7761
  [config]="config()"
@@ -5905,7 +7764,9 @@ class PraxisChartShowcaseWidgetComponent {
5905
7764
  [remoteDataResolver]="remoteDataResolver()"
5906
7765
  [enableCustomization]="enableCustomization()"
5907
7766
  (pointClick)="pointClick.emit($event)"
7767
+ (pointAction)="pointAction.emit($event)"
5908
7768
  (selectionChange)="selectionChange.emit($event)"
7769
+ (drillDown)="drillDown.emit($event)"
5909
7770
  (crossFilter)="crossFilter.emit($event)"
5910
7771
  (queryRequest)="queryRequest.emit($event)"
5911
7772
  (loadStateChange)="loadStateChange.emit($event)"
@@ -5932,7 +7793,7 @@ class PraxisChartShowcaseWidgetComponent {
5932
7793
  (rowClick)="handleRowClick($event)"
5933
7794
  ></praxis-table>
5934
7795
  }
5935
- `, isInline: true, styles: [":host{display:block;height:100%;min-width:0}.showcase-state-card{min-height:240px;display:grid;place-content:center;gap:8px;padding:24px;border-radius:18px;border:1px solid color-mix(in srgb,var(--md-sys-color-outline, #c5c7ce) 72%,transparent);background:linear-gradient(180deg,#fffffff5,#f4f7fbfa);text-align:center}.showcase-state-card h4{margin:0;font-size:1rem;font-weight:600;color:var(--md-sys-color-on-surface, #1a1b20)}.showcase-state-card p{margin:0;color:var(--md-sys-color-on-surface-variant, #5a5d67)}\n"], dependencies: [{ kind: "component", type: PraxisChartComponent, selector: "praxis-chart", inputs: ["config", "data", "chartDocument", "filterCriteria", "queryContext", "remoteDataResolver", "enableCustomization", "availableResources", "availableFields", "availableTargets"], outputs: ["pointClick", "selectionChange", "crossFilter", "queryRequest", "loadStateChange", "chartDocumentApplied", "chartDocumentSaved"] }, { kind: "component", type: PraxisTable, selector: "praxis-table", inputs: ["config", "resourcePath", "data", "tableId", "componentInstanceId", "configPersistenceStrategy", "title", "subtitle", "icon", "autoDelete", "notifyIfOutdated", "snoozeMs", "autoOpenSettingsOnOutdated", "crudContext", "filterCriteria", "queryContext", "aiContext", "aiAssistantVoiceInputMode", "aiAssistantVoiceLanguage", "horizontalScroll", "enableCustomization", "authoringCapability", "dense"], outputs: ["rowClick", "widgetEvent", "resourceEvent", "rowDoubleClick", "rowExpansionChange", "rowAction", "toolbarAction", "bulkAction", "exportAction", "columnReorder", "columnReorderAttempt", "columnResize", "beforeDelete", "afterDelete", "deleteError", "beforeBulkDelete", "afterBulkDelete", "bulkDeleteError", "schemaStatusChange", "configChange", "metadataChange", "loadingStateChange", "collectionLinksChange", "selectionChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
7796
+ `, isInline: true, styles: [":host{display:block;height:100%;min-width:0}.showcase-state-card{min-height:240px;display:grid;place-content:center;gap:8px;padding:24px;border-radius:18px;border:1px solid color-mix(in srgb,var(--md-sys-color-outline, #c5c7ce) 72%,transparent);background:linear-gradient(180deg,#fffffff5,#f4f7fbfa);text-align:center}.showcase-state-card h4{margin:0;font-size:1rem;font-weight:600;color:var(--md-sys-color-on-surface, #1a1b20)}.showcase-state-card p{margin:0;color:var(--md-sys-color-on-surface-variant, #5a5d67)}\n"], dependencies: [{ kind: "component", type: PraxisChartComponent, selector: "praxis-chart", inputs: ["config", "data", "chartDocument", "filterCriteria", "queryContext", "remoteDataResolver", "enableCustomization", "availableResources", "availableFields", "availableTargets"], outputs: ["pointClick", "pointAction", "selectionChange", "drillDown", "crossFilter", "queryRequest", "loadStateChange", "chartDocumentApplied", "chartDocumentSaved"] }, { kind: "component", type: PraxisTable, selector: "praxis-table", inputs: ["config", "resourcePath", "data", "tableId", "componentInstanceId", "configPersistenceStrategy", "title", "subtitle", "icon", "autoDelete", "notifyIfOutdated", "snoozeMs", "autoOpenSettingsOnOutdated", "crudContext", "filterCriteria", "queryContext", "aiContext", "aiAssistantVoiceInputMode", "aiAssistantVoiceLanguage", "horizontalScroll", "enableCustomization", "authoringCapability", "dense"], outputs: ["rowClick", "widgetEvent", "resourceEvent", "rowDoubleClick", "rowExpansionChange", "rowAction", "toolbarAction", "bulkAction", "exportAction", "columnReorder", "columnReorderAttempt", "columnResize", "beforeDelete", "afterDelete", "deleteError", "beforeBulkDelete", "afterBulkDelete", "bulkDeleteError", "schemaStatusChange", "configChange", "metadataChange", "loadingStateChange", "collectionLinksChange", "selectionChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
5936
7797
  }
5937
7798
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: PraxisChartShowcaseWidgetComponent, decorators: [{
5938
7799
  type: Component,
@@ -5945,7 +7806,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
5945
7806
  [remoteDataResolver]="remoteDataResolver()"
5946
7807
  [enableCustomization]="enableCustomization()"
5947
7808
  (pointClick)="pointClick.emit($event)"
7809
+ (pointAction)="pointAction.emit($event)"
5948
7810
  (selectionChange)="selectionChange.emit($event)"
7811
+ (drillDown)="drillDown.emit($event)"
5949
7812
  (crossFilter)="crossFilter.emit($event)"
5950
7813
  (queryRequest)="queryRequest.emit($event)"
5951
7814
  (loadStateChange)="loadStateChange.emit($event)"
@@ -5973,7 +7836,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
5973
7836
  ></praxis-table>
5974
7837
  }
5975
7838
  `, styles: [":host{display:block;height:100%;min-width:0}.showcase-state-card{min-height:240px;display:grid;place-content:center;gap:8px;padding:24px;border-radius:18px;border:1px solid color-mix(in srgb,var(--md-sys-color-outline, #c5c7ce) 72%,transparent);background:linear-gradient(180deg,#fffffff5,#f4f7fbfa);text-align:center}.showcase-state-card h4{margin:0;font-size:1rem;font-weight:600;color:var(--md-sys-color-on-surface, #1a1b20)}.showcase-state-card p{margin:0;color:var(--md-sys-color-on-surface-variant, #5a5d67)}\n"] }]
5976
- }], ctorParameters: () => [], propDecorators: { config: [{ type: i0.Input, args: [{ isSignal: true, alias: "config", required: true }] }], data: [{ type: i0.Input, args: [{ isSignal: true, alias: "data", required: false }] }], chartDocument: [{ type: i0.Input, args: [{ isSignal: true, alias: "chartDocument", required: false }] }], remoteDataResolver: [{ type: i0.Input, args: [{ isSignal: true, alias: "remoteDataResolver", required: false }] }], enableCustomization: [{ type: i0.Input, args: [{ isSignal: true, alias: "enableCustomization", required: false }] }], viewMode: [{ type: i0.Input, args: [{ isSignal: true, alias: "viewMode", required: false }] }], pointClick: [{ type: i0.Output, args: ["pointClick"] }], selectionChange: [{ type: i0.Output, args: ["selectionChange"] }], crossFilter: [{ type: i0.Output, args: ["crossFilter"] }], queryRequest: [{ type: i0.Output, args: ["queryRequest"] }], loadStateChange: [{ type: i0.Output, args: ["loadStateChange"] }] } });
7839
+ }], ctorParameters: () => [], propDecorators: { config: [{ type: i0.Input, args: [{ isSignal: true, alias: "config", required: true }] }], data: [{ type: i0.Input, args: [{ isSignal: true, alias: "data", required: false }] }], chartDocument: [{ type: i0.Input, args: [{ isSignal: true, alias: "chartDocument", required: false }] }], remoteDataResolver: [{ type: i0.Input, args: [{ isSignal: true, alias: "remoteDataResolver", required: false }] }], enableCustomization: [{ type: i0.Input, args: [{ isSignal: true, alias: "enableCustomization", required: false }] }], viewMode: [{ type: i0.Input, args: [{ isSignal: true, alias: "viewMode", required: false }] }], pointClick: [{ type: i0.Output, args: ["pointClick"] }], pointAction: [{ type: i0.Output, args: ["pointAction"] }], selectionChange: [{ type: i0.Output, args: ["selectionChange"] }], drillDown: [{ type: i0.Output, args: ["drillDown"] }], crossFilter: [{ type: i0.Output, args: ["crossFilter"] }], queryRequest: [{ type: i0.Output, args: ["queryRequest"] }], loadStateChange: [{ type: i0.Output, args: ["loadStateChange"] }] } });
5977
7840
  function resolveShowcaseStateMode(config, remoteRuntimeState = 'idle') {
5978
7841
  if (config.preferredLoadState === 'loading') {
5979
7842
  return 'loading';
@@ -6152,13 +8015,23 @@ const PRAXIS_CHART_SHOWCASE_WIDGET_METADATA = {
6152
8015
  {
6153
8016
  name: 'pointClick',
6154
8017
  type: 'PraxisChartPointEvent',
6155
- description: 'Forwards point selection from the chart or mapped row selection from the table.',
8018
+ description: 'Forwards raw point evidence from the chart or mapped row selection from the table.',
8019
+ },
8020
+ {
8021
+ name: 'pointAction',
8022
+ type: 'PraxisChartPointActionEvent',
8023
+ description: 'Forwards configured pointClick action payloads from the chart.',
6156
8024
  },
6157
8025
  {
6158
8026
  name: 'selectionChange',
6159
8027
  type: 'PraxisChartSelectionEvent',
6160
8028
  description: 'Forwards chart selection events with mapped filter values.',
6161
8029
  },
8030
+ {
8031
+ name: 'drillDown',
8032
+ type: 'PraxisChartDrillDownEvent',
8033
+ description: 'Forwards chart drill-down action payloads for Dynamic Page composition links.',
8034
+ },
6162
8035
  {
6163
8036
  name: 'crossFilter',
6164
8037
  type: 'PraxisChartCrossFilterEvent',
@@ -6211,7 +8084,7 @@ function provideChartEngineFactory(options) {
6211
8084
  return {
6212
8085
  provide: PRAXIS_CHART_ENGINE_FACTORY,
6213
8086
  useFactory: (optionBuilder) => () => new EChartsEngineAdapter(optionBuilder),
6214
- deps: [PraxisChartOptionBuilderService],
8087
+ deps: [EChartsOptionBuilderService],
6215
8088
  };
6216
8089
  }
6217
8090
  function providePraxisCharts(options = {}) {
@@ -6229,38 +8102,10 @@ function providePraxisCharts(options = {}) {
6229
8102
  ];
6230
8103
  }
6231
8104
 
6232
- class PraxisChartMetadataRegistrationService {
6233
- registry;
6234
- registered = false;
6235
- constructor(registry) {
6236
- this.registry = registry;
6237
- this.ensureRegistered();
6238
- }
6239
- ensureRegistered() {
6240
- if (this.registered) {
6241
- return;
6242
- }
6243
- this.registry.register(PRAXIS_CHART_COMPONENT_METADATA);
6244
- this.registry.register(PRAXIS_CHART_DRILLDOWN_PANEL_METADATA);
6245
- this.registry.register(PRAXIS_CHART_STATE_PROBE_COMPONENT_METADATA);
6246
- this.registry.register(PRAXIS_CHART_SHOWCASE_WIDGET_METADATA);
6247
- this.registered = true;
6248
- }
6249
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: PraxisChartMetadataRegistrationService, deps: [{ token: i1$1.ComponentMetadataRegistry }], target: i0.ɵɵFactoryTarget.Injectable });
6250
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: PraxisChartMetadataRegistrationService, providedIn: 'root' });
6251
- }
6252
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: PraxisChartMetadataRegistrationService, decorators: [{
6253
- type: Injectable,
6254
- args: [{ providedIn: 'root' }]
6255
- }], ctorParameters: () => [{ type: i1$1.ComponentMetadataRegistry }] });
6256
-
6257
8105
  class PraxisChartSchemaMapperService {
6258
- metadataRegistration;
6259
8106
  canonicalContractMapper;
6260
- constructor(metadataRegistration, canonicalContractMapper) {
6261
- this.metadataRegistration = metadataRegistration;
8107
+ constructor(canonicalContractMapper) {
6262
8108
  this.canonicalContractMapper = canonicalContractMapper;
6263
- this.metadataRegistration.ensureRegistered();
6264
8109
  }
6265
8110
  resolve(input, defaults) {
6266
8111
  const schema = this.normalize(input);
@@ -6349,8 +8194,12 @@ class PraxisChartSchemaMapperService {
6349
8194
  }
6350
8195
  const events = schema.chart?.events;
6351
8196
  const outputs = {};
6352
- if (events?.pointClick || events?.drillDown) {
8197
+ if (events?.pointClick) {
6353
8198
  outputs['pointClick'] = 'emit';
8199
+ outputs['pointAction'] = 'emit';
8200
+ }
8201
+ if (events?.drillDown) {
8202
+ outputs['drillDown'] = 'emit';
6354
8203
  }
6355
8204
  if (events?.selectionChange) {
6356
8205
  outputs['selectionChange'] = 'emit';
@@ -6360,13 +8209,13 @@ class PraxisChartSchemaMapperService {
6360
8209
  }
6361
8210
  return Object.keys(outputs).length ? outputs : undefined;
6362
8211
  }
6363
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: PraxisChartSchemaMapperService, deps: [{ token: PraxisChartMetadataRegistrationService }, { token: PraxisChartCanonicalContractMapperService }], target: i0.ɵɵFactoryTarget.Injectable });
8212
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: PraxisChartSchemaMapperService, deps: [{ token: PraxisChartCanonicalContractMapperService }], target: i0.ɵɵFactoryTarget.Injectable });
6364
8213
  static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: PraxisChartSchemaMapperService, providedIn: 'root' });
6365
8214
  }
6366
8215
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: PraxisChartSchemaMapperService, decorators: [{
6367
8216
  type: Injectable,
6368
8217
  args: [{ providedIn: 'root' }]
6369
- }], ctorParameters: () => [{ type: PraxisChartMetadataRegistrationService }, { type: PraxisChartCanonicalContractMapperService }] });
8218
+ }], ctorParameters: () => [{ type: PraxisChartCanonicalContractMapperService }] });
6370
8219
 
6371
8220
  class PraxisChartBackendPayloadAdapterService {
6372
8221
  chartSchemaMapper;
@@ -6420,7 +8269,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
6420
8269
  }], ctorParameters: () => [{ type: PraxisChartSchemaMapperService }, { type: PraxisChartCanonicalContractMapperService }] });
6421
8270
 
6422
8271
  class AnalyticsChartConfigAdapterService {
8272
+ i18n;
6423
8273
  statsBuilder = new AnalyticsStatsRequestBuilderService();
8274
+ constructor(i18n) {
8275
+ this.i18n = i18n;
8276
+ }
6424
8277
  toPraxisChartConfig(projection, options) {
6425
8278
  const chartType = this.resolveChartType(projection);
6426
8279
  const orientation = chartType === 'horizontal-bar' ? 'horizontal' : undefined;
@@ -6432,6 +8285,11 @@ class AnalyticsChartConfigAdapterService {
6432
8285
  if (!metrics.length) {
6433
8286
  throw new Error(`AnalyticsChartConfigAdapterService requires at least one metric for projection "${projection.id}".`);
6434
8287
  }
8288
+ const crossFilter = Boolean(projection.interactions?.crossFilter);
8289
+ const keyFilterField = dimension.keyFilterField?.trim();
8290
+ if (crossFilter && !keyFilterField) {
8291
+ throw new Error(`AnalyticsChartConfigAdapterService requires primaryDimension.keyFilterField when crossFilter is enabled for projection "${projection.id}".`);
8292
+ }
6435
8293
  return {
6436
8294
  id: projection.id,
6437
8295
  type: chartType,
@@ -6446,6 +8304,17 @@ class AnalyticsChartConfigAdapterService {
6446
8304
  pointClick: Boolean(projection.interactions?.pointSelection || projection.interactions?.drillDown),
6447
8305
  drillDown: Boolean(projection.interactions?.drillDown),
6448
8306
  selection: Boolean(projection.interactions?.pointSelection),
8307
+ crossFilter,
8308
+ ...(crossFilter
8309
+ ? {
8310
+ eventActions: {
8311
+ crossFilter: {
8312
+ action: 'emit',
8313
+ mapping: { key: keyFilterField },
8314
+ },
8315
+ },
8316
+ }
8317
+ : {}),
6449
8318
  },
6450
8319
  };
6451
8320
  }
@@ -6477,18 +8346,27 @@ class AnalyticsChartConfigAdapterService {
6477
8346
  }
6478
8347
  buildSeries(projection, chartType) {
6479
8348
  const dimension = projection.bindings.primaryDimension;
6480
- return this.getDisplayMetrics(projection).map((metric, index) => ({
6481
- id: `${projection.id}.${metric.field}.${index + 1}`,
6482
- name: metric.label ?? metric.field,
6483
- type: chartType,
6484
- categoryField: chartType === 'pie' || chartType === 'donut' ? dimension.field : undefined,
6485
- metric: {
6486
- field: metric.field,
6487
- aggregation: this.mapAggregation(metric.aggregation),
6488
- label: metric.label ?? undefined,
6489
- },
6490
- smooth: chartType === 'line' || chartType === 'area',
6491
- }));
8349
+ const comparison = projection.source.operation === 'comparison';
8350
+ return this.getDisplayMetrics(projection).flatMap((metric, index) => comparison
8351
+ ? ['current', 'previous'].map((period) => ({
8352
+ id: `${projection.id}.${metric.field}.${period}`,
8353
+ name: `${metric.label ?? metric.field} (${this.comparisonPeriodLabel(period)})`,
8354
+ type: chartType,
8355
+ categoryField: dimension.field,
8356
+ metric: { field: this.comparisonMetricField(metric.field, period), aggregation: this.mapAggregation(metric.aggregation) },
8357
+ }))
8358
+ : [{
8359
+ id: `${projection.id}.${metric.field}.${index + 1}`,
8360
+ name: metric.label ?? metric.field,
8361
+ type: chartType,
8362
+ categoryField: chartType === 'pie' || chartType === 'donut' ? dimension.field : undefined,
8363
+ metric: {
8364
+ field: metric.field,
8365
+ aggregation: this.mapAggregation(metric.aggregation),
8366
+ label: metric.label ?? undefined,
8367
+ },
8368
+ smooth: chartType === 'line' || chartType === 'area',
8369
+ }]);
6492
8370
  }
6493
8371
  buildDataSource(projection) {
6494
8372
  const executionPlan = this.statsBuilder.buildExecutionPlan(projection);
@@ -6502,7 +8380,13 @@ class AnalyticsChartConfigAdapterService {
6502
8380
  statsPath: executionPlan.statsPath,
6503
8381
  statsRequest: executionPlan.statsRequest,
6504
8382
  dimensions: executionPlan.dimensions,
6505
- metrics: this.mapExecutionMetrics(executionPlan),
8383
+ metrics: projection.source.operation === 'comparison'
8384
+ ? this.getDisplayMetrics(projection).flatMap((metric) => ['current', 'previous'].map((period) => ({
8385
+ field: this.comparisonMetricField(metric.field, period),
8386
+ aggregation: this.mapAggregation(metric.aggregation),
8387
+ alias: this.comparisonMetricField(metric.field, period),
8388
+ })))
8389
+ : this.mapExecutionMetrics(executionPlan),
6506
8390
  sort: executionPlan.sort,
6507
8391
  limit: executionPlan.limit,
6508
8392
  },
@@ -6552,13 +8436,25 @@ class AnalyticsChartConfigAdapterService {
6552
8436
  ...(projection.bindings.secondaryMetrics ?? []),
6553
8437
  ];
6554
8438
  }
6555
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: AnalyticsChartConfigAdapterService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
8439
+ comparisonMetricField(metricField, period) {
8440
+ return `__praxisComparison_${metricField}_${period}`;
8441
+ }
8442
+ comparisonPeriodLabel(period) {
8443
+ const key = period === 'current'
8444
+ ? 'praxis.charts.runtime.comparisonCurrent'
8445
+ : 'praxis.charts.runtime.comparisonPrevious';
8446
+ const fallback = period === 'current' ? 'Current' : 'Previous';
8447
+ return this.i18n?.t(key, undefined, fallback, 'charts') ?? fallback;
8448
+ }
8449
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: AnalyticsChartConfigAdapterService, deps: [{ token: i1$1.PraxisI18nService, optional: true }], target: i0.ɵɵFactoryTarget.Injectable });
6556
8450
  static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: AnalyticsChartConfigAdapterService, providedIn: 'root' });
6557
8451
  }
6558
8452
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: AnalyticsChartConfigAdapterService, decorators: [{
6559
8453
  type: Injectable,
6560
8454
  args: [{ providedIn: 'root' }]
6561
- }] });
8455
+ }], ctorParameters: () => [{ type: i1$1.PraxisI18nService, decorators: [{
8456
+ type: Optional
8457
+ }] }] });
6562
8458
 
6563
8459
  class AnalyticsChartContractService {
6564
8460
  analyticsSchema;
@@ -6661,7 +8557,7 @@ function normalizeError(error) {
6661
8557
 
6662
8558
  const PRAXIS_CHART_BACKEND_MOCK_BAR = {
6663
8559
  schemaMeta: {
6664
- schemaId: 'api/human-resources/vw-perfil-heroi|post|response|tenant:demo|locale:pt-BR',
8560
+ schemaId: 'api/human-resources/vw-perfil-heroi|post|response|internal:false',
6665
8561
  schemaHash: 'published-hash-hero-universe-v1',
6666
8562
  resourcePath: 'api/human-resources/vw-perfil-heroi',
6667
8563
  operation: 'post',
@@ -6691,7 +8587,7 @@ const PRAXIS_CHART_BACKEND_MOCK_BAR = {
6691
8587
  sizing: { mode: 'fixed', height: 320 },
6692
8588
  source: {
6693
8589
  kind: 'praxis.stats',
6694
- resource: 'api/human-resources/vw-perfil-heroi',
8590
+ resource: '/api/human-resources/vw-perfil-heroi',
6695
8591
  operation: 'group-by',
6696
8592
  options: {
6697
8593
  orderBy: 'value-desc',
@@ -6743,7 +8639,7 @@ const PRAXIS_CHART_BACKEND_MOCK_BAR = {
6743
8639
  };
6744
8640
  const PRAXIS_CHART_BACKEND_MOCK_TIMESERIES = {
6745
8641
  schemaMeta: {
6746
- schemaId: 'api/human-resources/vw-indicadores-incidentes|post|response|tenant:demo|locale:pt-BR',
8642
+ schemaId: 'api/human-resources/vw-indicadores-incidentes|post|response|internal:false',
6747
8643
  schemaHash: 'published-hash-incident-timeseries-v1',
6748
8644
  resourcePath: 'api/human-resources/vw-indicadores-incidentes',
6749
8645
  operation: 'post',
@@ -6767,7 +8663,7 @@ const PRAXIS_CHART_BACKEND_MOCK_TIMESERIES = {
6767
8663
  subtitle: { key: 'charts.incidents.timeline.subtitle', fallback: 'Timeseries em /vw-indicadores-incidentes' },
6768
8664
  source: {
6769
8665
  kind: 'praxis.stats',
6770
- resource: 'api/human-resources/vw-indicadores-incidentes',
8666
+ resource: '/api/human-resources/vw-indicadores-incidentes',
6771
8667
  operation: 'timeseries',
6772
8668
  options: {
6773
8669
  granularity: 'month',
@@ -6799,7 +8695,7 @@ const PRAXIS_CHART_BACKEND_MOCK_TIMESERIES = {
6799
8695
  };
6800
8696
  const PRAXIS_CHART_BACKEND_MOCK_DONUT = {
6801
8697
  schemaMeta: {
6802
- schemaId: 'api/human-resources/vw-indicadores-incidentes|post|response|tenant:demo|locale:pt-BR',
8698
+ schemaId: 'api/human-resources/vw-indicadores-incidentes|post|response|internal:false',
6803
8699
  schemaHash: 'published-hash-incident-severity-v1',
6804
8700
  resourcePath: 'api/human-resources/vw-indicadores-incidentes',
6805
8701
  operation: 'post',
@@ -6822,7 +8718,7 @@ const PRAXIS_CHART_BACKEND_MOCK_DONUT = {
6822
8718
  title: { key: 'charts.incidents.severity.title', fallback: 'Severidade de incidentes' },
6823
8719
  source: {
6824
8720
  kind: 'praxis.stats',
6825
- resource: 'api/human-resources/vw-indicadores-incidentes',
8721
+ resource: '/api/human-resources/vw-indicadores-incidentes',
6826
8722
  operation: 'distribution',
6827
8723
  options: {
6828
8724
  mode: 'terms',
@@ -6855,7 +8751,7 @@ const PRAXIS_CHART_BACKEND_MOCK_DONUT = {
6855
8751
  };
6856
8752
  const PRAXIS_CHART_BACKEND_MOCK_HORIZONTAL_BAR = {
6857
8753
  schemaMeta: {
6858
- schemaId: 'api/human-resources/vw-analytics-folha-pagamento|post|response|tenant:demo|locale:pt-BR',
8754
+ schemaId: 'api/human-resources/vw-analytics-folha-pagamento|post|response|internal:false',
6859
8755
  schemaHash: 'published-hash-payroll-department-ranking-v1',
6860
8756
  resourcePath: 'api/human-resources/vw-analytics-folha-pagamento',
6861
8757
  operation: 'post',
@@ -6881,7 +8777,7 @@ const PRAXIS_CHART_BACKEND_MOCK_HORIZONTAL_BAR = {
6881
8777
  sizing: { mode: 'fixed', height: 340 },
6882
8778
  source: {
6883
8779
  kind: 'praxis.stats',
6884
- resource: 'api/human-resources/vw-analytics-folha-pagamento',
8780
+ resource: '/api/human-resources/vw-analytics-folha-pagamento',
6885
8781
  operation: 'group-by',
6886
8782
  options: {
6887
8783
  orderBy: 'value-desc',
@@ -6917,7 +8813,7 @@ const PRAXIS_CHART_BACKEND_MOCK_HORIZONTAL_BAR = {
6917
8813
  };
6918
8814
  const PRAXIS_CHART_BACKEND_MOCK_STACKED_AREA = {
6919
8815
  schemaMeta: {
6920
- schemaId: 'api/human-resources/vw-analytics-folha-pagamento|post|response|tenant:demo|locale:pt-BR',
8816
+ schemaId: 'api/human-resources/vw-analytics-folha-pagamento|post|response|internal:false',
6921
8817
  schemaHash: 'published-hash-payroll-net-trend-stacked-v1',
6922
8818
  resourcePath: 'api/human-resources/vw-analytics-folha-pagamento',
6923
8819
  operation: 'post',
@@ -6942,7 +8838,7 @@ const PRAXIS_CHART_BACKEND_MOCK_STACKED_AREA = {
6942
8838
  sizing: { mode: 'fixed', height: 340 },
6943
8839
  source: {
6944
8840
  kind: 'praxis.stats',
6945
- resource: 'api/human-resources/vw-analytics-folha-pagamento',
8841
+ resource: '/api/human-resources/vw-analytics-folha-pagamento',
6946
8842
  operation: 'timeseries',
6947
8843
  options: {
6948
8844
  granularity: 'month',
@@ -6973,7 +8869,7 @@ const PRAXIS_CHART_BACKEND_MOCK_STACKED_AREA = {
6973
8869
  };
6974
8870
  const PRAXIS_CHART_BACKEND_MOCK_MULTI_METRIC_BAR = {
6975
8871
  schemaMeta: {
6976
- schemaId: 'api/human-resources/vw-analytics-folha-pagamento|post|response|tenant:demo|locale:pt-BR',
8872
+ schemaId: 'api/human-resources/vw-analytics-folha-pagamento|post|response|internal:false',
6977
8873
  schemaHash: 'published-hash-payroll-multi-metric-bar-v1',
6978
8874
  resourcePath: 'api/human-resources/vw-analytics-folha-pagamento',
6979
8875
  operation: 'post',
@@ -7001,7 +8897,7 @@ const PRAXIS_CHART_BACKEND_MOCK_MULTI_METRIC_BAR = {
7001
8897
  sizing: { mode: 'fixed', height: 340 },
7002
8898
  source: {
7003
8899
  kind: 'praxis.stats',
7004
- resource: 'api/human-resources/vw-analytics-folha-pagamento',
8900
+ resource: '/api/human-resources/vw-analytics-folha-pagamento',
7005
8901
  operation: 'group-by',
7006
8902
  options: {
7007
8903
  orderBy: 'value-desc',
@@ -7039,7 +8935,7 @@ const PRAXIS_CHART_BACKEND_MOCK_MULTI_METRIC_BAR = {
7039
8935
  };
7040
8936
  const PRAXIS_CHART_BACKEND_MOCK_SCATTER = {
7041
8937
  schemaMeta: {
7042
- schemaId: 'api/human-resources/vw-analytics-folha-pagamento|post|response|tenant:demo|locale:pt-BR',
8938
+ schemaId: 'api/human-resources/vw-analytics-folha-pagamento|post|response|internal:false',
7043
8939
  schemaHash: 'published-hash-payroll-scatter-v1',
7044
8940
  resourcePath: 'api/human-resources/vw-analytics-folha-pagamento',
7045
8941
  operation: 'post',
@@ -7064,7 +8960,7 @@ const PRAXIS_CHART_BACKEND_MOCK_SCATTER = {
7064
8960
  sizing: { mode: 'fixed', height: 340 },
7065
8961
  source: {
7066
8962
  kind: 'praxis.stats',
7067
- resource: 'api/human-resources/vw-analytics-folha-pagamento',
8963
+ resource: '/api/human-resources/vw-analytics-folha-pagamento',
7068
8964
  operation: 'group-by',
7069
8965
  options: {
7070
8966
  orderBy: 'key-asc',
@@ -7095,7 +8991,7 @@ const PRAXIS_CHART_BACKEND_MOCK_SCATTER = {
7095
8991
  };
7096
8992
  const PRAXIS_CHART_BACKEND_MOCK_COMBO = {
7097
8993
  schemaMeta: {
7098
- schemaId: 'api/human-resources/vw-analytics-folha-pagamento|post|response|tenant:demo|locale:pt-BR',
8994
+ schemaId: 'api/human-resources/vw-analytics-folha-pagamento|post|response|internal:false',
7099
8995
  schemaHash: 'published-hash-payroll-combo-v2',
7100
8996
  resourcePath: 'api/human-resources/vw-analytics-folha-pagamento',
7101
8997
  operation: 'post',
@@ -7120,7 +9016,7 @@ const PRAXIS_CHART_BACKEND_MOCK_COMBO = {
7120
9016
  sizing: { mode: 'fixed', height: 340 },
7121
9017
  source: {
7122
9018
  kind: 'praxis.stats',
7123
- resource: 'api/human-resources/vw-analytics-folha-pagamento',
9019
+ resource: '/api/human-resources/vw-analytics-folha-pagamento',
7124
9020
  operation: 'timeseries',
7125
9021
  options: {
7126
9022
  granularity: 'month',
@@ -7603,7 +9499,7 @@ class PraxisChartCompositionShowcaseComponent {
7603
9499
  };
7604
9500
  }
7605
9501
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: PraxisChartCompositionShowcaseComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
7606
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.2.14", type: PraxisChartCompositionShowcaseComponent, isStandalone: true, selector: "praxis-chart-composition-showcase", inputs: { enableCustomization: { classPropertyName: "enableCustomization", publicName: "enableCustomization", isSignal: true, isRequired: false, transformFunction: null } }, providers: [providePraxisCharts()], ngImport: i0, template: `
9502
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.2.14", type: PraxisChartCompositionShowcaseComponent, isStandalone: true, selector: "praxis-chart-composition-showcase", inputs: { enableCustomization: { classPropertyName: "enableCustomization", publicName: "enableCustomization", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: `
7607
9503
  <section class="showcase-shell">
7608
9504
  <header class="showcase-hero">
7609
9505
  <div class="showcase-copy">
@@ -7719,7 +9615,7 @@ class PraxisChartCompositionShowcaseComponent {
7719
9615
  }
7720
9616
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: PraxisChartCompositionShowcaseComponent, decorators: [{
7721
9617
  type: Component,
7722
- args: [{ selector: 'praxis-chart-composition-showcase', standalone: true, imports: [DynamicWidgetPageComponent], providers: [providePraxisCharts()], changeDetection: ChangeDetectionStrategy.OnPush, template: `
9618
+ args: [{ selector: 'praxis-chart-composition-showcase', standalone: true, imports: [DynamicWidgetPageComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: `
7723
9619
  <section class="showcase-shell">
7724
9620
  <header class="showcase-hero">
7725
9621
  <div class="showcase-copy">
@@ -7834,11 +9730,65 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
7834
9730
  `, styles: [":host{display:block}.showcase-shell{display:grid;gap:24px}.showcase-hero{display:grid;gap:20px;padding:28px;border-radius:28px;background:radial-gradient(circle at top right,rgba(18,99,180,.18),transparent 30%),linear-gradient(135deg,#071836f2,#1263b4c7);color:#f7fbff}.showcase-copy h2{margin:0 0 10px;font-size:clamp(1.8rem,3vw,2.6rem);line-height:1.05}.showcase-copy p{margin:0;max-width:52rem;color:#f7fbffe0}.showcase-eyebrow,.panel-kicker{margin:0 0 8px;font-size:.78rem;letter-spacing:.14em;text-transform:uppercase;color:#f7fbffb8}.showcase-controls{display:flex;flex-wrap:wrap;gap:16px}.control-group{display:grid;gap:8px}.control-group span{font-size:.84rem;color:#f7fbffc2}.control-buttons{display:flex;flex-wrap:wrap;gap:8px}.control-buttons button{border:1px solid rgba(247,251,255,.22);background:#f7fbff14;color:#f7fbff;border-radius:999px;padding:10px 14px;cursor:pointer;transition:background .16s ease,transform .16s ease}.control-buttons button.active{background:#f7fbff;color:#0d2d5f}.showcase-grid{display:grid;gap:20px;grid-template-columns:minmax(0,1.3fr) minmax(320px,.9fr)}.runtime-panel,.payload-panel{display:grid;gap:16px;padding:20px;border-radius:24px;border:1px solid color-mix(in srgb,var(--md-sys-color-outline, #c5c7ce) 76%,transparent);background:linear-gradient(180deg,#fffffff0,#f4f7fbf5)}.panel-header{display:flex;align-items:start;justify-content:space-between;gap:12px}.panel-header h3{margin:0;color:#142033}.panel-caption{margin:6px 0 0;color:#516074;font-size:.88rem;line-height:1.45}.panel-chip{border-radius:999px;padding:6px 10px;font-size:.76rem;background:#1263b41f;color:#1263b4}.payload-panel pre{margin:0;padding:16px;border-radius:18px;background:#09111f;color:#d7e6ff;overflow:auto;font-size:.84rem;line-height:1.5}@media(max-width:1080px){.showcase-grid{grid-template-columns:1fr}}\n"] }]
7835
9731
  }], propDecorators: { enableCustomization: [{ type: i0.Input, args: [{ isSignal: true, alias: "enableCustomization", required: false }] }] } });
7836
9732
 
9733
+ const PRAXIS_CHART_AUTHORING_COVERAGE = [
9734
+ { path: 'chartDocument', mode: 'advanced-structured', owner: 'chart-ai-manifest', rationale: 'Whole-document authoring is allowed for AI and structured import, but the visual editor must not become a raw JSON primary path.', proof: ['praxis-charts-authoring-manifest.spec.ts', 'praxis-chart-config-editor.spec.ts'] },
9735
+ { path: 'chartDocument.version', mode: 'derived-readonly', owner: 'chart-contract-normalizer', rationale: 'Version is canonical schema governance and is normalized to the authorable x-ui.chart version.', proof: ['chart-authoring-coverage.matrix.spec.ts', 'praxis-charts-authoring-manifest.spec.ts'] },
9736
+ { path: 'chartDocument.kind', mode: 'visual-editable', owner: 'chart-config-editor', rationale: 'Chart kind is a first-class visual decision with runtime mapper and AI manifest parity.', proof: ['praxis-chart-config-editor.spec.ts', 'praxis-charts-authoring-manifest.spec.ts'] },
9737
+ { path: 'chartDocument.preset', mode: 'preserved-only', owner: 'chart-contract-normalizer', rationale: 'Preset is semantic starter intent; the current editor preserves it instead of presenting it as a misleading post-hoc visual control.', proof: ['praxis-chart-config-editor.spec.ts'] },
9738
+ { path: 'chartDocument.chartId', mode: 'visual-editable', owner: 'chart-config-editor', rationale: 'Stable chart identity is editable for host/runtime diagnostics and event correlation.', proof: ['praxis-chart-config-editor.spec.ts'] },
9739
+ { path: 'chartDocument.orientation', mode: 'ai-authorable', owner: 'chart-ai-manifest', rationale: 'Orientation is compiled by chart type and axis decisions; the visual editor currently preserves it without a separate control.', proof: ['praxis-charts-authoring-manifest.spec.ts', 'praxis-chart-config-editor.spec.ts'] },
9740
+ { path: 'chartDocument.title', mode: 'visual-editable', owner: 'chart-config-editor', rationale: 'Title is primary presentation copy in the editor and runtime preview.', proof: ['praxis-chart-config-editor.spec.ts'] },
9741
+ { path: 'chartDocument.subtitle', mode: 'visual-editable', owner: 'chart-config-editor', rationale: 'Subtitle is presentation copy edited alongside title.', proof: ['praxis-chart-config-editor.spec.ts'] },
9742
+ { path: 'chartDocument.sizing', mode: 'visual-editable', owner: 'chart-config-editor', rationale: 'Sizing controls are canonical runtime layout decisions; deprecated height is normalized into sizing.height.', proof: ['praxis-chart-config-editor.spec.ts'] },
9743
+ { path: 'chartDocument.height', mode: 'derived-readonly', owner: 'chart-contract-normalizer', rationale: 'Deprecated compatibility input is consumed and cleared in favor of chartDocument.sizing.height.', proof: ['praxis-chart-config-editor.spec.ts'] },
9744
+ { path: 'chartDocument.source.kind', mode: 'visual-editable', owner: 'chart-config-editor', rationale: 'Source kind selects local derived data versus governed praxis.stats binding.', proof: ['praxis-chart-config-editor.spec.ts', 'praxis-chart-widget-config-editor.spec.ts'] },
9745
+ { path: 'chartDocument.source.resource', mode: 'visual-editable', owner: 'chart-config-editor', rationale: 'Resource selection is governed by catalog options and fails closed when unavailable.', proof: ['praxis-chart-config-editor.spec.ts', 'praxis-charts-settings-panel.smoke.playwright.spec.ts'] },
9746
+ { path: 'chartDocument.source.operation', mode: 'visual-editable', owner: 'chart-config-editor', rationale: 'Stats operation drives field compatibility, runtime request semantics and specialized options.', proof: ['praxis-chart-config-editor.spec.ts', 'praxis-charts-settings-panel.smoke.playwright.spec.ts'] },
9747
+ { path: 'chartDocument.source.options.granularity', mode: 'visual-editable', owner: 'chart-config-editor', rationale: 'Timeseries granularity is visually edited with backend-supported values only.', proof: ['praxis-chart-config-editor.spec.ts', 'praxis-charts-settings-panel.smoke.playwright.spec.ts'] },
9748
+ { path: 'chartDocument.source.options.fillGaps', mode: 'visual-editable', owner: 'chart-config-editor', rationale: 'Timeseries gap filling is a supported runtime option surfaced in the editor.', proof: ['praxis-chart-config-editor.spec.ts'] },
9749
+ { path: 'chartDocument.source.options.mode', mode: 'visual-editable', owner: 'chart-config-editor', rationale: 'Distribution mode is visually edited for terms versus histogram materialization.', proof: ['praxis-chart-config-editor.spec.ts'] },
9750
+ { path: 'chartDocument.source.options.bucketSize', mode: 'visual-editable', owner: 'chart-config-editor', rationale: 'Histogram bucket size is a supported distribution option.', proof: ['praxis-chart-config-editor.spec.ts'] },
9751
+ { path: 'chartDocument.source.options.bucketCount', mode: 'visual-editable', owner: 'chart-config-editor', rationale: 'Histogram bucket count is a supported distribution option.', proof: ['praxis-chart-config-editor.spec.ts'] },
9752
+ { 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'] },
9753
+ { 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'] },
9754
+ { 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'] },
9755
+ { 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'] },
9756
+ { 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'] },
9757
+ { path: 'chartDocument.dimensions[].format', mode: 'preserved-only', owner: 'chart-contract-normalizer', rationale: 'Dimension formatting is runtime metadata and is preserved while not yet visually edited.', proof: ['praxis-chart-config-editor.spec.ts'] },
9758
+ { 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'] },
9759
+ { 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'] },
9760
+ { 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'] },
9761
+ { 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'] },
9762
+ { 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'] },
9763
+ { 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'] },
9764
+ { path: 'chartDocument.filters[]', mode: 'preserved-only', owner: 'chart-contract-normalizer', rationale: 'Static document filters are preserved; dynamic authoring should prefer queryContext and backend capabilities.', proof: ['praxis-chart-config-editor.spec.ts'] },
9765
+ { path: 'chartDocument.limit', mode: 'preserved-only', owner: 'chart-contract-normalizer', rationale: 'Document-level limit remains a structured runtime constraint until a governed visual control is added.', proof: ['praxis-chart-config-editor.spec.ts'] },
9766
+ { path: 'chartDocument.legend', mode: 'visual-editable', owner: 'chart-config-editor', rationale: 'Legend visibility is a visual feature toggle and AI manifest operation.', proof: ['praxis-chart-config-editor.spec.ts', 'praxis-charts-authoring-manifest.spec.ts'] },
9767
+ { path: 'chartDocument.labels', mode: 'visual-editable', owner: 'chart-config-editor', rationale: 'Data labels are a visual feature toggle in the appearance section.', proof: ['praxis-chart-config-editor.spec.ts'] },
9768
+ { path: 'chartDocument.tooltip', mode: 'visual-editable', owner: 'chart-config-editor', rationale: 'Tooltip visibility is a visual feature toggle and AI manifest operation.', proof: ['praxis-chart-config-editor.spec.ts', 'praxis-charts-authoring-manifest.spec.ts'] },
9769
+ { path: 'chartDocument.theme.palette', mode: 'visual-editable', owner: 'chart-config-editor', rationale: 'Palette token/custom values are visually edited and preserved as renderer-neutral theme contract.', proof: ['praxis-chart-config-editor.spec.ts'] },
9770
+ { path: 'chartDocument.theme.variant', mode: 'visual-editable', owner: 'chart-config-editor', rationale: 'Theme variant is a visual editor control and runtime presentation decision.', proof: ['praxis-chart-config-editor.spec.ts', 'praxis-charts-settings-panel.smoke.playwright.spec.ts'] },
9771
+ { path: 'chartDocument.theme.surface.mode', mode: 'visual-editable', owner: 'chart-config-editor', rationale: 'Surface mode is visually edited to align chart containment with host context.', proof: ['praxis-chart-config-editor.spec.ts'] },
9772
+ { path: 'chartDocument.theme.surface.background', mode: 'preserved-only', owner: 'chart-contract-normalizer', rationale: 'Surface color details are contract-supported but not yet governed by tokenized visual controls.', proof: ['praxis-chart-config-editor.spec.ts'] },
9773
+ { path: 'chartDocument.theme.surface.borderColor', mode: 'preserved-only', owner: 'chart-contract-normalizer', rationale: 'Surface border color is preserved until a token-aware border editor exists.', proof: ['praxis-chart-config-editor.spec.ts'] },
9774
+ { path: 'chartDocument.theme.surface.borderWidth', mode: 'preserved-only', owner: 'chart-contract-normalizer', rationale: 'Surface border width is preserved to avoid dropping host-authored surface detail.', proof: ['praxis-chart-config-editor.spec.ts'] },
9775
+ { path: 'chartDocument.theme.surface.borderRadius', mode: 'preserved-only', owner: 'chart-contract-normalizer', rationale: 'Surface border radius is preserved to avoid dropping host-authored surface detail.', proof: ['praxis-chart-config-editor.spec.ts'] },
9776
+ { path: 'chartDocument.state.empty', mode: 'visual-editable', owner: 'chart-config-editor', rationale: 'Empty-state title/description are visible runtime presentation copy.', proof: ['praxis-chart-config-editor.spec.ts'] },
9777
+ { path: 'chartDocument.state.loading', mode: 'visual-editable', owner: 'chart-config-editor', rationale: 'Loading-state title/description are visible runtime presentation copy.', proof: ['praxis-chart-config-editor.spec.ts'] },
9778
+ { path: 'chartDocument.state.error', mode: 'visual-editable', owner: 'chart-config-editor', rationale: 'Error-state title/description are visible runtime presentation copy.', proof: ['praxis-chart-config-editor.spec.ts'] },
9779
+ { path: 'chartDocument.events.pointClick', mode: 'visual-editable', owner: 'chart-config-editor', rationale: 'Point click action is configured visually while raw point evidence remains a separate output.', proof: ['praxis-chart-config-editor.spec.ts', 'praxis-charts-authoring-manifest.spec.ts'] },
9780
+ { path: 'chartDocument.events.selectionChange', mode: 'visual-editable', owner: 'chart-config-editor', rationale: 'Selection change supports the governed single-selection event contract.', proof: ['praxis-chart-config-editor.spec.ts', 'praxis-charts-authoring-manifest.spec.ts'] },
9781
+ { path: 'chartDocument.events.drillDown', mode: 'visual-editable', owner: 'chart-config-editor', rationale: 'Drilldown target and mapping are visually authorable with governed target catalogs.', proof: ['praxis-chart-config-editor.spec.ts', 'praxis-charts-authoring-manifest.spec.ts'] },
9782
+ { path: 'chartDocument.events.crossFilter', mode: 'visual-editable', owner: 'chart-config-editor', rationale: 'Cross-filter target and mapping are visually authorable with governed target catalogs.', proof: ['praxis-chart-config-editor.spec.ts', 'praxis-charts-authoring-manifest.spec.ts'] },
9783
+ { path: 'chartDocument.motion', mode: 'visual-editable', owner: 'chart-config-editor', rationale: 'Motion enablement and preset are visual runtime behavior controls.', proof: ['praxis-chart-config-editor.spec.ts'] },
9784
+ { path: 'queryContext', mode: 'advanced-structured', owner: 'chart-widget-config-editor', rationale: 'Query context is a structured widget runtime input edited as an advanced JSON fallback and validated as object-only.', proof: ['praxis-chart-widget-config-editor.spec.ts', 'praxis-charts-authoring-manifest.spec.ts'] },
9785
+ ];
9786
+
7837
9787
  const chartDocumentSchema = {
7838
9788
  type: 'object',
7839
9789
  required: ['version', 'kind', 'source'],
7840
9790
  properties: {
7841
- version: { const: '0.1.0' },
9791
+ version: { const: PRAXIS_X_UI_CHART_AUTHORABLE_VERSION },
7842
9792
  kind: { enum: ['bar', 'combo', 'horizontal-bar', 'line', 'pie', 'donut', 'area', 'stacked-bar', 'stacked-area', 'scatter'] },
7843
9793
  chartId: { type: 'string' },
7844
9794
  title: { oneOf: [{ type: 'string' }, { type: 'object' }] },
@@ -7867,21 +9817,22 @@ const PRAXIS_CHARTS_AUTHORING_MANIFEST = {
7867
9817
  runtimeInputs: [
7868
9818
  { name: 'chartDocument', type: 'PraxisXUiChartContract | null', description: 'Canonical x-ui.chart document used as the authoring source of truth.' },
7869
9819
  { name: 'config', type: 'PraxisChartConfig', description: 'Runtime chart configuration mapped from or supplied beside the canonical chart document.' },
7870
- { name: 'queryContext', type: 'PraxisChartQueryContext | null', description: 'Declarative query context merged into remote datasource requests.' },
9820
+ { name: 'queryContext', type: 'PraxisChartQueryContext | null', description: 'Canonical @praxisui/core query context observed by remote datasource requests; flat filters execute in praxis.stats and governed filterExpression is preserved but rejected until stats expression support is proven.' },
7871
9821
  { name: 'remoteDataResolver', type: 'PraxisChartRemoteDataResolver | null', description: 'Host-governed remote row resolver for integrations outside the default praxis.stats client.' },
7872
- { name: 'availableResources', type: 'ChartEditorResourceOption[]', description: 'Governed remote resource catalog exposed to the config editor.' },
7873
- { name: 'availableFields', type: 'ChartEditorFieldOption[]', description: 'Governed semantic field catalog exposed to the config editor.' },
7874
- { name: 'availableTargets', type: 'ChartEditorTargetOption[]', description: 'Governed widget or route targets exposed to chart event authoring.' },
9822
+ { name: 'availableResources', type: 'ChartEditorResourceOption[]', description: 'Transient authoring catalog: id is the stable /schemas/catalog discovery identity, while path is the governed root-relative operational value persisted to chartDocument.source.resource.' },
9823
+ { name: 'availableFields', type: 'ChartEditorFieldOption[]', description: 'Transient capability-backed field catalog. operations, aggregations and distributionModes declare exact group-by, timeseries, distribution/terms and distribution/histogram eligibility.' },
9824
+ { name: 'availableTargets', type: 'ChartEditorTargetOption[]', description: 'Transient visual-authoring targets derived from existing top-level composition links to public, non-deprecated widget ports or explicitly writable declared page state; id is the unique authoring identity and events[] declares the exact structured Chart events authorized by compatible links. Raw pointClick evidence never authorizes events.pointClick, which is emitted through pointAction. Local transform.output semantic-kind inspection does not replace a Core-validated transform-output projection (P2), and the visual catalog does not yet publish the target input-schema/port projection required for end-to-end mapping validation.' },
7875
9825
  ],
7876
9826
  editableTargets: [
7877
9827
  { kind: 'chartType', resolver: 'x-ui-chart-kind', description: 'Canonical `kind` field in PraxisXUiChartContract.' },
7878
9828
  { kind: 'series', resolver: 'x-ui-chart-metric-by-field', description: 'Metric-backed series definition in `metrics[]`, keyed by field.' },
7879
9829
  { kind: 'axis', resolver: 'x-ui-chart-dimension-or-metric-axis', description: 'Dimension role, metric axis and chart orientation semantics.' },
7880
9830
  { kind: 'dataBinding', resolver: 'x-ui-chart-source-and-field-catalog', description: 'Source, dimensions, metrics and resource/field bindings.' },
7881
- { kind: 'queryContext', resolver: 'praxis-chart-query-context', description: 'Runtime queryContext contract for filters, sort, limit and page.' },
9831
+ { kind: 'queryContext', resolver: 'praxis-chart-query-context', description: 'Runtime @praxisui/core queryContext contract for filters, filterExpression, sort, limit, page and governance metadata.' },
9832
+ { kind: 'pointClick', resolver: 'x-ui-chart-events-point-click', description: 'Raw pointClick evidence plus a separate pointAction output for configured pointClick actions.' },
7882
9833
  { kind: 'crossFilter', resolver: 'x-ui-chart-events-cross-filter', description: 'Cross-filter event mapping emitted as structured query-context filters.' },
7883
9834
  { kind: 'drilldown', resolver: 'x-ui-chart-events-drill-down', description: 'Drilldown event mapping and governed target.' },
7884
- { kind: 'selection', resolver: 'x-ui-chart-events-selection-change', description: 'Selection event enablement and mapping semantics.' },
9835
+ { 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.' },
7885
9836
  { kind: 'legend', resolver: 'x-ui-chart-legend-feature', description: 'Legend visibility through boolean or toggleable feature object.' },
7886
9837
  { kind: 'tooltip', resolver: 'x-ui-chart-tooltip-feature', description: 'Tooltip visibility through boolean or toggleable feature object.' },
7887
9838
  ],
@@ -8021,7 +9972,7 @@ const PRAXIS_CHARTS_AUTHORING_MANIFEST = {
8021
9972
  required: ['sourceKind'],
8022
9973
  properties: {
8023
9974
  sourceKind: { enum: ['praxis.stats', 'derived'] },
8024
- resource: { type: 'string' },
9975
+ resource: { type: 'string', pattern: '^/(?!/)' },
8025
9976
  operation: { enum: ['group-by', 'timeseries', 'distribution'] },
8026
9977
  dimensions: { type: 'array', items: { type: 'object', required: ['field'], properties: { field: { type: 'string' }, role: { type: 'string' } } } },
8027
9978
  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' } } } },
@@ -8030,10 +9981,10 @@ const PRAXIS_CHARTS_AUTHORING_MANIFEST = {
8030
9981
  effects: [{ kind: 'compile-domain-patch', handler: 'chart-data-resource-bind', handlerContract: {
8031
9982
  reads: ['availableResources[]', 'availableFields[]', 'chartDocument.source', 'chartDocument.dimensions[]', 'chartDocument.metrics[]'],
8032
9983
  writes: ['chartDocument.source', 'chartDocument.dimensions[]', 'chartDocument.metrics[]'],
8033
- identityKeys: ['availableResources[].path', 'availableResources[].id', 'availableFields[].field'],
8034
- inputSchema: { type: 'object', required: ['sourceKind'], properties: { sourceKind: { enum: ['praxis.stats', 'derived'] }, resource: { type: 'string' }, operation: { enum: ['group-by', 'timeseries', 'distribution'] }, dimensions: { type: 'array' }, metrics: { type: 'array' } } },
9984
+ identityKeys: ['availableResources[].path', 'availableFields[].field'],
9985
+ inputSchema: { type: 'object', required: ['sourceKind'], properties: { sourceKind: { enum: ['praxis.stats', 'derived'] }, resource: { type: 'string', pattern: '^/(?!/)' }, operation: { enum: ['group-by', 'timeseries', 'distribution'] }, dimensions: { type: 'array' }, metrics: { type: 'array' } } },
8035
9986
  failureModes: ['resource-not-in-api-metadata', 'field-not-in-schema', 'stats-operation-not-supported', 'remote-binding-incomplete'],
8036
- description: 'Binds a derived or praxis.stats source using the governed resource and field catalogs instead of free-form prompt examples.',
9987
+ description: 'Binds a derived or praxis.stats source using governed catalogs. For praxis.stats, availableResources[].id is discovery-only: validation requires the matching availableResources[].path and compilation persists that path as chartDocument.source.resource.',
8037
9988
  } }],
8038
9989
  destructive: false,
8039
9990
  requiresConfirmation: false,
@@ -8048,15 +9999,37 @@ const PRAXIS_CHARTS_AUTHORING_MANIFEST = {
8048
9999
  scope: 'dataBinding',
8049
10000
  targetKind: 'queryContext',
8050
10001
  target: { kind: 'queryContext', resolver: 'praxis-chart-query-context', ambiguityPolicy: 'fail', required: false },
8051
- inputSchema: { type: 'object', properties: { filters: { type: 'object' }, sort: { type: 'array', items: { type: 'string' } }, limit: { type: 'number' }, page: { type: 'object' }, meta: { type: 'object' } } },
10002
+ inputSchema: { type: 'object', properties: { filters: { type: 'object' }, filterExpression: { type: 'object' }, sort: { type: 'array', items: { type: 'string' } }, limit: { type: 'number' }, page: { type: 'object' }, meta: { type: 'object' } } },
8052
10003
  effects: [{ kind: 'set-value', path: 'queryContext' }],
8053
10004
  destructive: false,
8054
10005
  requiresConfirmation: false,
8055
- validators: ['query-context-structured', 'query-context-fields-exist', 'query-context-safe-values', 'editor-runtime-round-trip'],
10006
+ validators: ['query-context-structured', 'query-context-filter-expression-stats-unsupported', 'query-context-fields-exist', 'query-context-safe-values', 'editor-runtime-round-trip'],
8056
10007
  affectedPaths: ['queryContext'],
8057
10008
  submissionImpact: 'affects-schema-backed-data',
8058
10009
  preconditions: ['config-initialized'],
8059
10010
  },
10011
+ {
10012
+ operationId: 'pointClick.configure',
10013
+ title: 'Configure chart point click',
10014
+ scope: 'eventMapping',
10015
+ targetKind: 'pointClick',
10016
+ target: { kind: 'pointClick', resolver: 'x-ui-chart-events-point-click', ambiguityPolicy: 'fail', required: false },
10017
+ inputSchema: { type: 'object', required: ['action'], properties: { action: { enum: ['filter-widget', 'open-detail', 'navigate', 'update-context', 'emit'] }, target: { type: 'string' }, mapping: { type: 'object', additionalProperties: { type: 'string' } } } },
10018
+ effects: [{ kind: 'compile-domain-patch', handler: 'chart-event-point-click-configure', handlerContract: {
10019
+ reads: ['chartDocument.events.pointClick', 'chartDocument.dimensions[]', 'availableTargets[]'],
10020
+ writes: ['chartDocument.events.pointClick'],
10021
+ identityKeys: ['availableTargets[].id'],
10022
+ inputSchema: { type: 'object', required: ['action'], properties: { action: { enum: ['filter-widget', 'open-detail', 'navigate', 'update-context', 'emit'] }, target: { type: 'string' }, mapping: { type: 'object', additionalProperties: { type: 'string' } } } },
10023
+ failureModes: ['target-not-in-catalog', 'mapping-source-field-missing', 'mapping-target-invalid', 'unsafe-event-action'],
10024
+ description: 'Configures pointClick through the pointAction output while keeping pointClick as raw point evidence.',
10025
+ } }],
10026
+ destructive: false,
10027
+ requiresConfirmation: false,
10028
+ validators: ['point-click-action-structured', 'event-target-governed', 'event-mapping-fields-exist', 'event-action-supported', 'editor-runtime-round-trip'],
10029
+ affectedPaths: ['chartDocument.events.pointClick'],
10030
+ submissionImpact: 'affects-schema-backed-data',
10031
+ preconditions: ['config-initialized'],
10032
+ },
8060
10033
  {
8061
10034
  operationId: 'crossFilter.configure',
8062
10035
  title: 'Configure chart cross-filter',
@@ -8070,7 +10043,7 @@ const PRAXIS_CHARTS_AUTHORING_MANIFEST = {
8070
10043
  identityKeys: ['availableTargets[].id'],
8071
10044
  inputSchema: { type: 'object', required: ['action'], properties: { action: { enum: ['filter-widget', 'open-detail', 'navigate', 'update-context', 'emit'] }, target: { type: 'string' }, mapping: { type: 'object', additionalProperties: { type: 'string' } } } },
8072
10045
  failureModes: ['target-not-in-catalog', 'mapping-source-field-missing', 'mapping-target-invalid', 'unsafe-event-action'],
8073
- description: 'Configures crossFilter as a structured event action that emits safe query-context filters to a governed target.',
10046
+ description: 'Configures crossFilter as a structured event action emitted through the crossFilter output with safe query-context filters to a governed target. For praxis.stats rows, key is governed bucket identity and label is presentation evidence; mappings may use key when the target schema accepts that identity.',
8074
10047
  } }],
8075
10048
  destructive: false,
8076
10049
  requiresConfirmation: false,
@@ -8092,7 +10065,7 @@ const PRAXIS_CHARTS_AUTHORING_MANIFEST = {
8092
10065
  identityKeys: ['availableTargets[].id'],
8093
10066
  inputSchema: { type: 'object', required: ['action'], properties: { action: { enum: ['filter-widget', 'open-detail', 'navigate', 'update-context', 'emit'] }, target: { type: 'string' }, mapping: { type: 'object', additionalProperties: { type: 'string' } } } },
8094
10067
  failureModes: ['target-not-in-catalog', 'mapping-source-field-missing', 'drilldown-target-invalid', 'unsafe-event-action'],
8095
- description: 'Configures drillDown through governed route/widget targets and structured field mappings.',
10068
+ description: 'Configures drillDown through the dedicated drillDown output with governed route/widget targets and structured field mappings.',
8096
10069
  } }],
8097
10070
  destructive: false,
8098
10071
  requiresConfirmation: false,
@@ -8114,11 +10087,11 @@ const PRAXIS_CHARTS_AUTHORING_MANIFEST = {
8114
10087
  identityKeys: ['availableTargets[].id'],
8115
10088
  inputSchema: { type: 'object', required: ['action'], properties: { action: { enum: ['filter-widget', 'open-detail', 'navigate', 'update-context', 'emit'] }, target: { type: 'string' }, mapping: { type: 'object', additionalProperties: { type: 'string' } } } },
8116
10089
  failureModes: ['target-not-in-catalog', 'mapping-source-field-missing', 'mapping-target-invalid', 'unsafe-event-action'],
8117
- description: 'Configures selectionChange as a structured event action whose event key is governed by the operation.',
10090
+ description: 'Configures selectionChange as single-point selection evidence whose event key is governed by the operation.',
8118
10091
  } }],
8119
10092
  destructive: false,
8120
10093
  requiresConfirmation: false,
8121
- validators: ['selection-output-structured', 'event-mapping-fields-exist', 'event-action-supported', 'editor-runtime-round-trip'],
10094
+ validators: ['selection-output-structured', 'event-target-governed', 'event-mapping-fields-exist', 'event-action-supported', 'editor-runtime-round-trip'],
8122
10095
  affectedPaths: ['chartDocument.events.selectionChange'],
8123
10096
  submissionImpact: 'affects-schema-backed-data',
8124
10097
  preconditions: ['config-initialized'],
@@ -8156,7 +10129,7 @@ const PRAXIS_CHARTS_AUTHORING_MANIFEST = {
8156
10129
  ],
8157
10130
  validators: [
8158
10131
  { validatorId: 'chart-document-shape', level: 'error', code: 'CHART_DOCUMENT_SHAPE', description: 'Document must conform to PraxisXUiChartContract.' },
8159
- { validatorId: 'chart-version-supported', level: 'error', code: 'CHART_VERSION_SUPPORTED', description: 'Only x-ui.chart version 0.1.0 is authorable.' },
10132
+ { validatorId: 'chart-version-supported', level: 'error', code: 'CHART_VERSION_SUPPORTED', description: `Only x-ui.chart version ${PRAXIS_X_UI_CHART_AUTHORABLE_VERSION} is authorable.` },
8160
10133
  { validatorId: 'chart-type-supported', level: 'error', code: 'CHART_TYPE_SUPPORTED', description: 'Chart kind must be supported by @praxisui/charts.' },
8161
10134
  { validatorId: 'chart-type-series-axis-compatible', level: 'error', code: 'CHART_TYPE_SERIES_AXIS_COMPATIBLE', description: 'Chart kind, dimensions, metrics and axes must be compatible.' },
8162
10135
  { validatorId: 'pie-single-metric', level: 'error', code: 'PIE_SINGLE_METRIC', description: 'Pie and donut charts support a single metric.' },
@@ -8170,15 +10143,17 @@ const PRAXIS_CHARTS_AUTHORING_MANIFEST = {
8170
10143
  { validatorId: 'axis-field-exists', level: 'error', code: 'AXIS_FIELD_EXISTS', description: 'Axis/dimension fields must exist in availableFields or schema context.' },
8171
10144
  { validatorId: 'secondary-axis-combo-only', level: 'error', code: 'SECONDARY_AXIS_COMBO_ONLY', description: 'Secondary axis is supported only for combo charts.' },
8172
10145
  { validatorId: 'cartesian-dimension-required', level: 'error', code: 'CARTESIAN_DIMENSION_REQUIRED', description: 'Cartesian charts require at least one dimension.' },
8173
- { validatorId: 'remote-resource-in-api-metadata', level: 'error', code: 'REMOTE_RESOURCE_IN_API_METADATA', description: 'Remote praxis.stats resource must come from the governed API metadata/resource catalog.' },
8174
- { validatorId: 'bound-fields-exist', level: 'error', code: 'BOUND_FIELDS_EXIST', description: 'Bound dimensions and metrics must exist in the schema/data context.' },
10146
+ { validatorId: 'remote-resource-in-api-metadata', level: 'error', code: 'REMOTE_RESOURCE_IN_API_METADATA', description: 'Remote praxis.stats resource must equal a governed root-relative availableResources[].path beginning with exactly one slash; a discovery id is not an executable resource path.' },
10147
+ { validatorId: 'bound-fields-exist', level: 'error', code: 'BOUND_FIELDS_EXIST', description: 'Bound dimensions and metrics must exist in the capability-backed field context and satisfy its operation, aggregation and distribution-mode eligibility.' },
8175
10148
  { validatorId: 'stats-operation-supported', level: 'error', code: 'STATS_OPERATION_SUPPORTED', description: 'Remote datasource operation must be supported by the stats backend.' },
8176
- { validatorId: 'query-context-structured', level: 'error', code: 'QUERY_CONTEXT_STRUCTURED', description: 'queryContext must use structured filters, sort, limit and page fields.' },
10149
+ { validatorId: 'query-context-structured', level: 'error', code: 'QUERY_CONTEXT_STRUCTURED', description: 'queryContext must use the canonical @praxisui/core structure for filters, filterExpression, sort, limit, page and meta.' },
10150
+ { validatorId: 'query-context-filter-expression-stats-unsupported', level: 'error', code: 'QUERY_CONTEXT_FILTER_EXPRESSION_STATS_UNSUPPORTED', description: 'praxis.stats currently executes only flat queryContext.filters; governed filterExpression must be preserved for diagnostics and rejected until backend expression support is declared.' },
8177
10151
  { validatorId: 'query-context-fields-exist', level: 'warning', code: 'QUERY_CONTEXT_FIELDS_EXIST', description: 'queryContext filter fields should exist in the schema/data context.' },
8178
10152
  { validatorId: 'query-context-safe-values', level: 'error', code: 'QUERY_CONTEXT_SAFE_VALUES', description: 'queryContext values must be serializable safe data.' },
10153
+ { validatorId: 'point-click-action-structured', level: 'error', code: 'POINT_CLICK_ACTION_STRUCTURED', description: 'Configured pointClick actions must be emitted through pointAction while pointClick remains raw point evidence.' },
8179
10154
  { validatorId: 'cross-filter-output-structured', level: 'error', code: 'CROSS_FILTER_OUTPUT_STRUCTURED', description: 'Cross-filter output must be structured as query-context filters.' },
8180
- { validatorId: 'event-target-governed', level: 'error', code: 'EVENT_TARGET_GOVERNED', description: 'Event targets must be selected from availableTargets or an approved host catalog.' },
8181
- { validatorId: 'event-mapping-fields-exist', level: 'error', code: 'EVENT_MAPPING_FIELDS_EXIST', description: 'Event mapping source fields must exist in chart data context.' },
10155
+ { validatorId: 'event-target-governed', level: 'error', code: 'EVENT_TARGET_GOVERNED', description: 'Event targets must resolve to exactly one availableTargets[].id and declare the authored event plus action; missing, duplicate or cross-event identities fail closed.' },
10156
+ { validatorId: 'event-mapping-fields-exist', level: 'error', code: 'EVENT_MAPPING_FIELDS_EXIST', description: 'Event mapping source fields must exist in chart data context; praxis.stats reserves key for bucket identity and label for presentation.' },
8182
10157
  { validatorId: 'event-action-supported', level: 'error', code: 'EVENT_ACTION_SUPPORTED', description: 'Event action must be one of the supported structured action kinds.' },
8183
10158
  { validatorId: 'drilldown-target-governed', level: 'error', code: 'DRILLDOWN_TARGET_GOVERNED', description: 'Drilldown target must be a governed route or widget target.' },
8184
10159
  { validatorId: 'selection-output-structured', level: 'error', code: 'SELECTION_OUTPUT_STRUCTURED', description: 'Selection output must resolve selected point filters deterministically.' },
@@ -8188,8 +10163,11 @@ const PRAXIS_CHARTS_AUTHORING_MANIFEST = {
8188
10163
  roundTripRequirements: [
8189
10164
  'The canonical saved shape is PraxisXUiChartContract, not raw ECharts options.',
8190
10165
  'PraxisChartConfigEditor must preserve chart kind, source, dimensions, metrics, events, legend and tooltip across apply/save/reset/reopen.',
8191
- 'Remote bindings must be resolved from availableResources/API metadata and availableFields/schema context.',
10166
+ 'Remote bindings persist availableResources[].path; availableResources[].id remains discovery identity, and transient resource/field/target catalogs and diagnostics are never persisted in widget inputs.',
8192
10167
  'Cross-filter, drilldown and selection authoring must persist structured event actions, not prompt examples or command strings.',
10168
+ '[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.',
10169
+ '[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.',
10170
+ 'Enrichment of capability-backed availableFields with /schemas/filtered property metadata remains a later schema-flow gate.',
8193
10171
  ],
8194
10172
  examples: [
8195
10173
  { id: 'set-bar-chart', request: 'Use a bar chart for employees by department.', operationId: 'chart.type.set', params: { kind: 'bar' }, isPositive: true },
@@ -8199,11 +10177,12 @@ const PRAXIS_CHARTS_AUTHORING_MANIFEST = {
8199
10177
  { id: 'bind-stats-resource', request: 'Bind this chart to payroll stats grouped by department.', operationId: 'data.resource.bind', params: { sourceKind: 'praxis.stats', resource: '/api/stats/payroll', operation: 'group-by' }, isPositive: true },
8200
10178
  { id: 'set-query-context', request: 'Limit the chart to active employees and sort by total descending.', operationId: 'queryContext.set', params: { filters: { status: 'ACTIVE' }, sort: ['total,desc'], limit: 10 }, isPositive: true },
8201
10179
  { id: 'configure-cross-filter', request: 'Use selected department to filter the employee table.', operationId: 'crossFilter.configure', params: { action: 'filter-widget', target: 'employeesTable', mapping: { department: 'department' } }, isPositive: true },
10180
+ { id: 'configure-stats-key-cross-filter', request: 'Use the governed department bucket identity to filter the employee table.', operationId: 'crossFilter.configure', params: { action: 'filter-widget', target: 'employeesTable', mapping: { key: 'departmentIdsIn' } }, isPositive: true },
8202
10181
  { id: 'configure-drilldown', request: 'Open the department detail when a point is clicked.', operationId: 'drilldown.configure', params: { action: 'navigate', target: '/departments/detail', mapping: { department: 'departmentId' } }, isPositive: true },
8203
10182
  { id: 'configure-selection', request: 'Emit selected department as a structured selection event.', operationId: 'selection.configure', params: { action: 'emit', mapping: { department: 'department' } }, isPositive: true },
8204
10183
  { id: 'toggle-legend', request: 'Show the legend.', operationId: 'legend.configure', params: { enabled: true }, isPositive: true },
8205
10184
  { id: 'toggle-tooltip', request: 'Disable tooltips.', operationId: 'tooltip.configure', params: { enabled: false }, isPositive: true },
8206
- { 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: '0.1.0', kind: 'bar', source: { kind: 'derived' }, dimensions: [{ field: 'department' }], metrics: [{ field: 'total', aggregation: 'count' }] }, isPositive: true },
10185
+ { 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 },
8207
10186
  { 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 },
8208
10187
  { 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 },
8209
10188
  { id: 'reject-unsafe-target', request: 'Navigate to javascript:alert(1) on drilldown.', operationId: 'drilldown.configure', params: { event: 'drillDown', action: 'navigate', target: 'javascript:alert(1)' }, isPositive: false },
@@ -8218,4 +10197,4 @@ const PRAXIS_CHARTS_AUTHORING_MANIFEST = {
8218
10197
  * Generated bundle index. Do not edit.
8219
10198
  */
8220
10199
 
8221
- export { AnalyticsChartConfigAdapterService, AnalyticsChartContractService, ChartContractNormalizerService, ChartContractValidationService, ChartEditorDefaultsService, ChartEditorPreviewMapperService, PRAXIS_CHARTS_AUTHORING_MANIFEST, PRAXIS_CHARTS_I18N, PRAXIS_CHART_BACKEND_MOCK_BAR, PRAXIS_CHART_BACKEND_MOCK_COMBO, PRAXIS_CHART_BACKEND_MOCK_DONUT, PRAXIS_CHART_BACKEND_MOCK_HORIZONTAL_BAR, PRAXIS_CHART_BACKEND_MOCK_MULTI_METRIC_BAR, PRAXIS_CHART_BACKEND_MOCK_SCATTER, PRAXIS_CHART_BACKEND_MOCK_STACKED_AREA, PRAXIS_CHART_BACKEND_MOCK_TIMESERIES, PRAXIS_CHART_COMPONENT_METADATA, PRAXIS_CHART_DRILLDOWN_DATA_BY_MONTH, PRAXIS_CHART_DRILLDOWN_PANEL_METADATA, PRAXIS_CHART_ENGINE, PRAXIS_CHART_ENGINE_FACTORY, PRAXIS_CHART_PALETTE_TOKENS, PRAXIS_CHART_STATE_PROBE_COMPONENT_METADATA, PRAXIS_CHART_THEME_VARIANTS, PraxisChartBackendPayloadAdapterService, PraxisChartCanonicalContractMapperService, PraxisChartComponent, PraxisChartCompositionShowcaseComponent, PraxisChartConfigEditor, PraxisChartDataTransformerService, PraxisChartDrilldownPanelComponent, PraxisChartOptionBuilderService, PraxisChartSchemaMapperService, PraxisChartStateProbeComponent, PraxisChartStatsApiService, PraxisChartWidgetConfigEditor, PraxisMicroVisualizationComponent, buildPraxisChartInteractiveCanvasPage, buildPraxisChartInteractiveWidgetPage, buildPraxisChartMockCanvasPage, buildPraxisChartMockWidgetPage, createPraxisChartsI18nConfig, isPraxisChartPaletteToken, providePraxisChartDrilldownPanelMetadata, providePraxisChartStateProbeMetadata, providePraxisCharts, providePraxisChartsI18n, providePraxisChartsMetadata, resolvePraxisChartPaletteToken, resolvePraxisChartsText };
10200
+ export { AnalyticsChartConfigAdapterService, AnalyticsChartContractService, ChartContractNormalizerService, ChartContractValidationService, ChartEditorDefaultsService, ChartEditorPreviewMapperService, ChartResourceCapabilityCatalogAdapter, PRAXIS_CHARTS_AUTHORING_MANIFEST, PRAXIS_CHARTS_I18N, PRAXIS_CHART_AUTHORING_COVERAGE, PRAXIS_CHART_BACKEND_MOCK_BAR, PRAXIS_CHART_BACKEND_MOCK_COMBO, PRAXIS_CHART_BACKEND_MOCK_DONUT, PRAXIS_CHART_BACKEND_MOCK_HORIZONTAL_BAR, PRAXIS_CHART_BACKEND_MOCK_MULTI_METRIC_BAR, PRAXIS_CHART_BACKEND_MOCK_SCATTER, PRAXIS_CHART_BACKEND_MOCK_STACKED_AREA, PRAXIS_CHART_BACKEND_MOCK_TIMESERIES, PRAXIS_CHART_COMPONENT_METADATA, PRAXIS_CHART_DRILLDOWN_DATA_BY_MONTH, PRAXIS_CHART_DRILLDOWN_PANEL_METADATA, PRAXIS_CHART_ENGINE, PRAXIS_CHART_ENGINE_FACTORY, PRAXIS_CHART_PALETTE_TOKENS, PRAXIS_CHART_STATE_PROBE_COMPONENT_METADATA, PRAXIS_CHART_THEME_VARIANTS, PRAXIS_X_UI_CHART_AUTHORABLE_VERSION, PraxisChartBackendPayloadAdapterService, PraxisChartCanonicalContractMapperService, PraxisChartComponent, PraxisChartCompositionShowcaseComponent, PraxisChartConfigEditor, PraxisChartDataTransformerService, PraxisChartDrilldownPanelComponent, PraxisChartSchemaMapperService, PraxisChartStateProbeComponent, PraxisChartStatsApiService, PraxisChartWidgetConfigEditor, PraxisMicroVisualizationComponent, buildPraxisChartInteractiveCanvasPage, buildPraxisChartInteractiveWidgetPage, buildPraxisChartMockCanvasPage, buildPraxisChartMockWidgetPage, createPraxisChartsI18nConfig, isPraxisChartPaletteToken, providePraxisChartDrilldownPanelMetadata, providePraxisChartStateProbeMetadata, providePraxisCharts, providePraxisChartsI18n, providePraxisChartsMetadata, resolvePraxisChartPaletteToken, resolvePraxisChartsText };