@praxisui/charts 9.0.0-beta.73 → 9.0.0-beta.75
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -1
- package/ai/component-registry.json +326 -36
- package/fesm2022/praxisui-charts.mjs +2316 -389
- package/package.json +3 -3
- package/types/praxisui-charts.d.ts +473 -58
|
@@ -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,
|
|
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
|
|
56
|
-
const
|
|
57
|
-
const
|
|
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
|
|
79
|
-
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
value
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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,
|
|
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:
|
|
123
|
-
const
|
|
124
|
-
const
|
|
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 ? [
|
|
128
|
-
: (timeAxis ? [
|
|
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
|
-
|
|
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
|
|
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.
|
|
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.
|
|
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();
|
|
@@ -1441,6 +1589,8 @@ class PraxisChartComponent {
|
|
|
1441
1589
|
resizeObserver = signal(null, ...(ngDevMode ? [{ debugName: "resizeObserver" }] : /* istanbul ignore next */ []));
|
|
1442
1590
|
resizeObserverHost = null;
|
|
1443
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 */ []));
|
|
1444
1594
|
currentLoadState = signal('idle', ...(ngDevMode ? [{ debugName: "currentLoadState" }] : /* istanbul ignore next */ []));
|
|
1445
1595
|
remoteResolvedData = signal(null, ...(ngDevMode ? [{ debugName: "remoteResolvedData" }] : /* istanbul ignore next */ []));
|
|
1446
1596
|
remoteRuntimeState = signal('idle', ...(ngDevMode ? [{ debugName: "remoteRuntimeState" }] : /* istanbul ignore next */ []));
|
|
@@ -1456,11 +1606,14 @@ class PraxisChartComponent {
|
|
|
1456
1606
|
previousRemoteSignature = null;
|
|
1457
1607
|
previousRemoteDataResolver = undefined;
|
|
1458
1608
|
previousDocumentSignature = null;
|
|
1609
|
+
inheritedThemeSignature = null;
|
|
1459
1610
|
editorSessionSubscriptions = [];
|
|
1611
|
+
remoteRequestSubscription = null;
|
|
1612
|
+
remoteRequestSequence = 0;
|
|
1460
1613
|
effectiveConfig = computed(() => {
|
|
1461
|
-
const base = this.mappedRuntimeConfig() ?? this.config();
|
|
1462
|
-
const runtimeQueryContext =
|
|
1463
|
-
const runtimeFilters =
|
|
1614
|
+
const base = this.applyInheritedTheme(this.mappedRuntimeConfig() ?? this.config(), this.inheritedTheme());
|
|
1615
|
+
const runtimeQueryContext = normalizePraxisDataQueryContext(this.queryContext());
|
|
1616
|
+
const runtimeFilters = resolvePraxisFilterCriteria(this.filterCriteria(), runtimeQueryContext);
|
|
1464
1617
|
if (!runtimeQueryContext
|
|
1465
1618
|
&& !Object.keys(runtimeFilters).length) {
|
|
1466
1619
|
return base;
|
|
@@ -1535,18 +1688,17 @@ class PraxisChartComponent {
|
|
|
1535
1688
|
};
|
|
1536
1689
|
}
|
|
1537
1690
|
if (remoteState === 'error') {
|
|
1691
|
+
const technicalDetails = this.remoteTechnicalError() ?? config.state?.error?.technicalDetails;
|
|
1538
1692
|
return {
|
|
1539
1693
|
...config,
|
|
1540
1694
|
preferredLoadState: 'error',
|
|
1541
|
-
state:
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
error
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
}
|
|
1549
|
-
: config.state,
|
|
1695
|
+
state: {
|
|
1696
|
+
...config.state,
|
|
1697
|
+
error: {
|
|
1698
|
+
...config.state?.error,
|
|
1699
|
+
technicalDetails,
|
|
1700
|
+
},
|
|
1701
|
+
},
|
|
1550
1702
|
};
|
|
1551
1703
|
}
|
|
1552
1704
|
if (remoteData !== null) {
|
|
@@ -1615,6 +1767,7 @@ class PraxisChartComponent {
|
|
|
1615
1767
|
constructor() {
|
|
1616
1768
|
afterNextRender(() => {
|
|
1617
1769
|
this.observeShellSizingContext();
|
|
1770
|
+
this.observeThemeContext();
|
|
1618
1771
|
});
|
|
1619
1772
|
effect(() => {
|
|
1620
1773
|
const document = this.chartDocument();
|
|
@@ -1647,8 +1800,9 @@ class PraxisChartComponent {
|
|
|
1647
1800
|
const config = this.effectiveConfig();
|
|
1648
1801
|
const explicitData = this.data();
|
|
1649
1802
|
const remoteDataResolver = this.remoteDataResolver();
|
|
1803
|
+
const queryContext = normalizePraxisDataQueryContext(this.queryContext());
|
|
1650
1804
|
const nextSignature = explicitData === null || explicitData === undefined
|
|
1651
|
-
? this.buildRemoteSignature(config)
|
|
1805
|
+
? this.buildRemoteSignature(config, queryContext)
|
|
1652
1806
|
: null;
|
|
1653
1807
|
if (nextSignature === this.previousRemoteSignature
|
|
1654
1808
|
&& remoteDataResolver === this.previousRemoteDataResolver) {
|
|
@@ -1656,6 +1810,7 @@ class PraxisChartComponent {
|
|
|
1656
1810
|
}
|
|
1657
1811
|
this.previousRemoteSignature = nextSignature;
|
|
1658
1812
|
this.previousRemoteDataResolver = remoteDataResolver;
|
|
1813
|
+
this.cancelRemoteDataRequest();
|
|
1659
1814
|
this.remoteResolvedData.set(null);
|
|
1660
1815
|
this.remoteRuntimeState.set('idle');
|
|
1661
1816
|
this.remoteTechnicalError.set(null);
|
|
@@ -1676,25 +1831,42 @@ class PraxisChartComponent {
|
|
|
1676
1831
|
if (this.remoteRuntimeState() === 'loading' || this.remoteResolvedData() !== null) {
|
|
1677
1832
|
return;
|
|
1678
1833
|
}
|
|
1834
|
+
const queryContext = normalizePraxisDataQueryContext(this.queryContext());
|
|
1679
1835
|
const event = {
|
|
1680
1836
|
chartId: effectiveConfig.id,
|
|
1681
1837
|
dataSource: effectiveConfig.dataSource,
|
|
1682
1838
|
query: effectiveConfig.dataSource.query,
|
|
1683
|
-
queryContext
|
|
1839
|
+
queryContext,
|
|
1684
1840
|
};
|
|
1685
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
|
+
}
|
|
1686
1850
|
this.remoteRuntimeState.set('loading');
|
|
1687
1851
|
this.remoteResolvedData.set([]);
|
|
1688
1852
|
this.remoteTechnicalError.set(null);
|
|
1689
|
-
this.
|
|
1853
|
+
this.cancelRemoteDataRequest();
|
|
1854
|
+
const requestId = ++this.remoteRequestSequence;
|
|
1855
|
+
this.remoteRequestSubscription = this.executeRemoteDataRequest(event, effectiveConfig)
|
|
1690
1856
|
.pipe(takeUntilDestroyed(this.destroyRef))
|
|
1691
1857
|
.subscribe({
|
|
1692
1858
|
next: (rows) => {
|
|
1859
|
+
if (requestId !== this.remoteRequestSequence) {
|
|
1860
|
+
return;
|
|
1861
|
+
}
|
|
1693
1862
|
this.remoteResolvedData.set(rows);
|
|
1694
1863
|
this.remoteRuntimeState.set('ready');
|
|
1695
1864
|
this.remoteTechnicalError.set(null);
|
|
1696
1865
|
},
|
|
1697
1866
|
error: (error) => {
|
|
1867
|
+
if (requestId !== this.remoteRequestSequence) {
|
|
1868
|
+
return;
|
|
1869
|
+
}
|
|
1698
1870
|
this.remoteResolvedData.set([]);
|
|
1699
1871
|
this.remoteRuntimeState.set('error');
|
|
1700
1872
|
this.remoteTechnicalError.set(error instanceof Error && error.message.trim()
|
|
@@ -1733,8 +1905,10 @@ class PraxisChartComponent {
|
|
|
1733
1905
|
});
|
|
1734
1906
|
this.destroyRef.onDestroy(() => {
|
|
1735
1907
|
this.clearEditorSessionSubscriptions();
|
|
1908
|
+
this.cancelRemoteDataRequest();
|
|
1736
1909
|
this.destroyResizeObserver();
|
|
1737
1910
|
this.shellObserver()?.disconnect();
|
|
1911
|
+
this.themeObserver()?.disconnect();
|
|
1738
1912
|
this.cancelScheduledResize();
|
|
1739
1913
|
this.cancelScheduledRender();
|
|
1740
1914
|
this.engine.destroy();
|
|
@@ -1793,13 +1967,45 @@ class PraxisChartComponent {
|
|
|
1793
1967
|
this.pointClick.emit(event);
|
|
1794
1968
|
const config = this.renderConfig();
|
|
1795
1969
|
const interactions = config.interactions;
|
|
1796
|
-
if (!interactions?.selection && !interactions?.crossFilter) {
|
|
1970
|
+
if (!interactions?.pointClick && !interactions?.selection && !interactions?.drillDown && !interactions?.crossFilter) {
|
|
1797
1971
|
return;
|
|
1798
1972
|
}
|
|
1799
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
|
+
}
|
|
1800
1990
|
if (interactions.selection) {
|
|
1801
1991
|
this.selectionChange.emit(selectionEvent);
|
|
1802
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
|
+
}
|
|
1803
2009
|
if (interactions.crossFilter) {
|
|
1804
2010
|
const crossFilterAction = interactions.eventActions?.crossFilter;
|
|
1805
2011
|
this.crossFilter.emit({
|
|
@@ -1818,6 +2024,7 @@ class PraxisChartComponent {
|
|
|
1818
2024
|
...event,
|
|
1819
2025
|
chartId: config.id ?? event.chartId,
|
|
1820
2026
|
selected: true,
|
|
2027
|
+
mode: 'single',
|
|
1821
2028
|
filters,
|
|
1822
2029
|
action,
|
|
1823
2030
|
};
|
|
@@ -1913,7 +2120,7 @@ class PraxisChartComponent {
|
|
|
1913
2120
|
const trimmed = value?.trim();
|
|
1914
2121
|
return trimmed || null;
|
|
1915
2122
|
}
|
|
1916
|
-
buildRemoteSignature(config) {
|
|
2123
|
+
buildRemoteSignature(config, queryContext) {
|
|
1917
2124
|
if (config.dataSource?.kind !== 'remote') {
|
|
1918
2125
|
return null;
|
|
1919
2126
|
}
|
|
@@ -1921,8 +2128,14 @@ class PraxisChartComponent {
|
|
|
1921
2128
|
id: config.id,
|
|
1922
2129
|
resourcePath: config.dataSource.resourcePath,
|
|
1923
2130
|
query: config.dataSource.query,
|
|
2131
|
+
queryContext,
|
|
1924
2132
|
});
|
|
1925
2133
|
}
|
|
2134
|
+
cancelRemoteDataRequest() {
|
|
2135
|
+
this.remoteRequestSequence += 1;
|
|
2136
|
+
this.remoteRequestSubscription?.unsubscribe();
|
|
2137
|
+
this.remoteRequestSubscription = null;
|
|
2138
|
+
}
|
|
1926
2139
|
clearEditorSessionSubscriptions() {
|
|
1927
2140
|
for (const subscription of this.editorSessionSubscriptions) {
|
|
1928
2141
|
subscription.unsubscribe();
|
|
@@ -1950,6 +2163,100 @@ class PraxisChartComponent {
|
|
|
1950
2163
|
});
|
|
1951
2164
|
this.shellObserver.set(observer);
|
|
1952
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
|
+
}
|
|
1953
2260
|
shouldFillShellContainer(shell) {
|
|
1954
2261
|
const rect = shell.getBoundingClientRect();
|
|
1955
2262
|
const hasControlledHeight = (rect.height || shell.clientHeight || shell.offsetHeight) > 0;
|
|
@@ -2018,7 +2325,7 @@ class PraxisChartComponent {
|
|
|
2018
2325
|
this.resizeFrameId = null;
|
|
2019
2326
|
}
|
|
2020
2327
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: PraxisChartComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
2021
|
-
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: `
|
|
2022
2329
|
<section
|
|
2023
2330
|
class="praxis-chart-shell"
|
|
2024
2331
|
[class.praxis-chart-shell-fill-container]="isFillContainerMode()"
|
|
@@ -2092,7 +2399,7 @@ class PraxisChartComponent {
|
|
|
2092
2399
|
<div #chartHost class="praxis-chart-host"></div>
|
|
2093
2400
|
}
|
|
2094
2401
|
</section>
|
|
2095
|
-
`, 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 });
|
|
2096
2403
|
}
|
|
2097
2404
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: PraxisChartComponent, decorators: [{
|
|
2098
2405
|
type: Component,
|
|
@@ -2172,65 +2479,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
|
|
|
2172
2479
|
<div #chartHost class="praxis-chart-host"></div>
|
|
2173
2480
|
}
|
|
2174
2481
|
</section>
|
|
2175
|
-
`, 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"] }]
|
|
2176
|
-
}], 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 }] }] } });
|
|
2177
|
-
function normalizeFilterCriteria(criteria) {
|
|
2178
|
-
if (!criteria)
|
|
2179
|
-
return null;
|
|
2180
|
-
const next = Object.entries(criteria).reduce((acc, [key, value]) => {
|
|
2181
|
-
if (value === null || value === undefined)
|
|
2182
|
-
return acc;
|
|
2183
|
-
if (Array.isArray(value) && value.length === 0)
|
|
2184
|
-
return acc;
|
|
2185
|
-
if (typeof value === 'string' && value.trim() === '')
|
|
2186
|
-
return acc;
|
|
2187
|
-
acc[key] = value;
|
|
2188
|
-
return acc;
|
|
2189
|
-
}, {});
|
|
2190
|
-
return Object.keys(next).length ? next : null;
|
|
2191
|
-
}
|
|
2192
|
-
function normalizePraxisDataQueryContextBridge(context) {
|
|
2193
|
-
if (!context || typeof context !== 'object')
|
|
2194
|
-
return null;
|
|
2195
|
-
const filters = normalizeFilterCriteria(context.filters || null);
|
|
2196
|
-
const meta = normalizeFilterCriteria(context.meta || null);
|
|
2197
|
-
const sort = Array.isArray(context.sort)
|
|
2198
|
-
? context.sort
|
|
2199
|
-
.map((item) => (typeof item === 'string' ? item.trim() : ''))
|
|
2200
|
-
.filter((item) => item.length > 0)
|
|
2201
|
-
: [];
|
|
2202
|
-
const limit = typeof context.limit === 'number' && Number.isFinite(context.limit)
|
|
2203
|
-
? Math.max(0, Math.trunc(context.limit))
|
|
2204
|
-
: null;
|
|
2205
|
-
const page = context.page && typeof context.page === 'object'
|
|
2206
|
-
? {
|
|
2207
|
-
index: typeof context.page.index === 'number' && Number.isFinite(context.page.index)
|
|
2208
|
-
? Math.max(0, Math.trunc(context.page.index))
|
|
2209
|
-
: null,
|
|
2210
|
-
size: typeof context.page.size === 'number' && Number.isFinite(context.page.size)
|
|
2211
|
-
? Math.max(0, Math.trunc(context.page.size))
|
|
2212
|
-
: null,
|
|
2213
|
-
}
|
|
2214
|
-
: null;
|
|
2215
|
-
const next = {};
|
|
2216
|
-
if (filters)
|
|
2217
|
-
next.filters = filters;
|
|
2218
|
-
if (sort.length)
|
|
2219
|
-
next.sort = sort;
|
|
2220
|
-
if (limit !== null)
|
|
2221
|
-
next.limit = limit;
|
|
2222
|
-
if (page && (page.index !== null || page.size !== null))
|
|
2223
|
-
next.page = page;
|
|
2224
|
-
if (meta)
|
|
2225
|
-
next.meta = meta;
|
|
2226
|
-
return Object.keys(next).length ? next : null;
|
|
2227
|
-
}
|
|
2228
|
-
function resolvePraxisFilterCriteriaBridge(filterCriteria, queryContext) {
|
|
2229
|
-
return {
|
|
2230
|
-
...(normalizeFilterCriteria(filterCriteria) || {}),
|
|
2231
|
-
...(normalizePraxisDataQueryContextBridge(queryContext)?.filters || {}),
|
|
2232
|
-
};
|
|
2233
|
-
}
|
|
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 }] }] } });
|
|
2234
2484
|
function mergeRemoteQueryContext(config, queryContext, runtimeFilters) {
|
|
2235
2485
|
const dataSource = config.dataSource;
|
|
2236
2486
|
if (dataSource?.kind !== 'remote' || !dataSource.query)
|
|
@@ -2263,6 +2513,17 @@ function mergeRemoteQueryContext(config, queryContext, runtimeFilters) {
|
|
|
2263
2513
|
},
|
|
2264
2514
|
};
|
|
2265
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
|
+
}
|
|
2266
2527
|
function normalizeRemoteDataResolverResult$1(result) {
|
|
2267
2528
|
if (Array.isArray(result)) {
|
|
2268
2529
|
return of(result);
|
|
@@ -3264,7 +3525,7 @@ class PraxisChartDrilldownPanelComponent {
|
|
|
3264
3525
|
[data]="detailData()"
|
|
3265
3526
|
></praxis-chart>
|
|
3266
3527
|
</section>
|
|
3267
|
-
`, 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 });
|
|
3268
3529
|
}
|
|
3269
3530
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: PraxisChartDrilldownPanelComponent, decorators: [{
|
|
3270
3531
|
type: Component,
|
|
@@ -3447,19 +3708,14 @@ class EChartsOptionBuilderService {
|
|
|
3447
3708
|
};
|
|
3448
3709
|
}
|
|
3449
3710
|
const horizontal = config.orientation === 'horizontal' || config.type === 'horizontal-bar';
|
|
3450
|
-
const xAxisType = config
|
|
3711
|
+
const xAxisType = this.usesCanonicalStatsTime(config)
|
|
3712
|
+
? 'time'
|
|
3713
|
+
: config.axes?.x?.type ?? 'category';
|
|
3451
3714
|
return {
|
|
3452
3715
|
backgroundColor: config.theme?.backgroundColor,
|
|
3453
3716
|
color: palette,
|
|
3454
3717
|
title: this.buildTitle(config, 'left', textColor),
|
|
3455
|
-
tooltip: tooltipEnabled
|
|
3456
|
-
? {
|
|
3457
|
-
trigger: config.theme?.tooltip?.trigger ?? 'axis',
|
|
3458
|
-
confine: true,
|
|
3459
|
-
appendToBody: false,
|
|
3460
|
-
valueFormatter: (value) => this.formatValue(value, this.primaryValueFormat(config)),
|
|
3461
|
-
}
|
|
3462
|
-
: undefined,
|
|
3718
|
+
tooltip: tooltipEnabled ? this.buildCartesianTooltip(config) : undefined,
|
|
3463
3719
|
legend: this.buildLegend(config, legendVisible, 'bottom', textColor),
|
|
3464
3720
|
grid: this.buildGrid(config, { horizontal, legendVisible }),
|
|
3465
3721
|
xAxis: horizontal
|
|
@@ -3679,6 +3935,87 @@ class EChartsOptionBuilderService {
|
|
|
3679
3935
|
?? config.axes?.y?.labels?.format
|
|
3680
3936
|
?? config.series[0]?.labels?.format;
|
|
3681
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
|
+
}
|
|
3682
4019
|
seriesLabelFormat(config, seriesId) {
|
|
3683
4020
|
return config.series.find((series) => series.id === seriesId)?.labels?.format;
|
|
3684
4021
|
}
|
|
@@ -3841,6 +4178,14 @@ function truncateLegendText(name) {
|
|
|
3841
4178
|
}
|
|
3842
4179
|
return `${value.slice(0, 25)}...`;
|
|
3843
4180
|
}
|
|
4181
|
+
function escapeTooltipHtml(value) {
|
|
4182
|
+
return String(value ?? '')
|
|
4183
|
+
.replace(/&/g, '&')
|
|
4184
|
+
.replace(/</g, '<')
|
|
4185
|
+
.replace(/>/g, '>')
|
|
4186
|
+
.replace(/"/g, '"')
|
|
4187
|
+
.replace(/'/g, ''');
|
|
4188
|
+
}
|
|
3844
4189
|
|
|
3845
4190
|
use([
|
|
3846
4191
|
AriaComponent,
|
|
@@ -3884,13 +4229,14 @@ class EChartsEngineAdapter {
|
|
|
3884
4229
|
chart.on('click', (params) => {
|
|
3885
4230
|
this.lastSeriesClickAt = Date.now();
|
|
3886
4231
|
this.lastGridClick = undefined;
|
|
4232
|
+
const data = pointData(params?.name, params?.data ?? params?.value);
|
|
3887
4233
|
payload.onPointClick?.({
|
|
3888
4234
|
chartId: payload.config.id,
|
|
3889
4235
|
seriesId: params?.seriesId,
|
|
3890
4236
|
seriesName: params?.seriesName,
|
|
3891
|
-
category: params?.name,
|
|
4237
|
+
category: this.resolvePointCategory(payload, data, params?.name),
|
|
3892
4238
|
value: pointValue(params?.value ?? params?.data),
|
|
3893
|
-
data
|
|
4239
|
+
data,
|
|
3894
4240
|
});
|
|
3895
4241
|
});
|
|
3896
4242
|
const zrender = chart?.getZr?.();
|
|
@@ -3950,6 +4296,26 @@ class EChartsEngineAdapter {
|
|
|
3950
4296
|
this.domClickHandler = undefined;
|
|
3951
4297
|
this.zrenderClickHandler = undefined;
|
|
3952
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);
|
|
4318
|
+
}
|
|
3953
4319
|
emitCategoryClickFromGrid(event, payload) {
|
|
3954
4320
|
const chart = this.chart;
|
|
3955
4321
|
if (!chart || typeof chart.containPixel !== 'function' || typeof chart.convertFromPixel !== 'function') {
|
|
@@ -3977,10 +4343,8 @@ class EChartsEngineAdapter {
|
|
|
3977
4343
|
}
|
|
3978
4344
|
const category = categories[categoryIndex];
|
|
3979
4345
|
const categoryValue = category == null ? undefined : String(category);
|
|
3980
|
-
const categoryField =
|
|
3981
|
-
|
|
3982
|
-
: payload.config.axes?.x?.field
|
|
3983
|
-
?? payload.config.series.find((series) => series.categoryField)?.categoryField;
|
|
4346
|
+
const categoryField = payload.config.axes?.x?.field
|
|
4347
|
+
?? payload.config.series.find((series) => series.categoryField)?.categoryField;
|
|
3984
4348
|
const signature = JSON.stringify({
|
|
3985
4349
|
chartId: payload.config.id,
|
|
3986
4350
|
category: categoryValue,
|
|
@@ -3990,7 +4354,20 @@ class EChartsEngineAdapter {
|
|
|
3990
4354
|
if (this.isDuplicateGridClick(signature)) {
|
|
3991
4355
|
return;
|
|
3992
4356
|
}
|
|
3993
|
-
const
|
|
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
|
+
};
|
|
3994
4371
|
if (categoryField && categoryValue !== undefined) {
|
|
3995
4372
|
data[categoryField] = categoryValue;
|
|
3996
4373
|
}
|
|
@@ -4042,6 +4419,13 @@ const PRAXIS_CHARTS_EN_US = {
|
|
|
4042
4419
|
'praxis.charts.runtime.remoteErrorTitle': 'Chart data could not be loaded',
|
|
4043
4420
|
'praxis.charts.runtime.remoteErrorDescription': 'Review the chart data source and analytics request before continuing.',
|
|
4044
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.',
|
|
4045
4429
|
'praxis.charts.editor.section.general': 'General',
|
|
4046
4430
|
'praxis.charts.editor.section.data': 'Data',
|
|
4047
4431
|
'praxis.charts.editor.section.analytics': 'Analytics',
|
|
@@ -4103,6 +4487,34 @@ const PRAXIS_CHARTS_EN_US = {
|
|
|
4103
4487
|
'praxis.charts.editor.appearance.surfaceTitle': 'Surface',
|
|
4104
4488
|
'praxis.charts.editor.appearance.surfaceHint': 'Use embedded for dashboard widgets and contained only when the chart must own its visual surface.',
|
|
4105
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.',
|
|
4106
4518
|
'praxis.charts.editor.hint.sizingMode': 'Use fill-container only when the host widget provides a defined body height.',
|
|
4107
4519
|
'praxis.charts.editor.hint.height': 'Numbers are saved as pixels; CSS lengths such as 20rem are also accepted.',
|
|
4108
4520
|
'praxis.charts.editor.hint.minHeight': 'Set a readable minimum for compact dashboard widgets.',
|
|
@@ -4195,6 +4607,13 @@ const PRAXIS_CHARTS_PT_BR = {
|
|
|
4195
4607
|
'praxis.charts.runtime.remoteErrorTitle': 'Não foi possível carregar os dados do gráfico',
|
|
4196
4608
|
'praxis.charts.runtime.remoteErrorDescription': 'Revise a fonte de dados e os filtros antes de continuar.',
|
|
4197
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.',
|
|
4198
4617
|
'praxis.charts.editor.section.general': 'Geral',
|
|
4199
4618
|
'praxis.charts.editor.section.data': 'Dados',
|
|
4200
4619
|
'praxis.charts.editor.section.analytics': 'Estrutura',
|
|
@@ -4256,6 +4675,34 @@ const PRAXIS_CHARTS_PT_BR = {
|
|
|
4256
4675
|
'praxis.charts.editor.appearance.surfaceTitle': 'Superfície',
|
|
4257
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.',
|
|
4258
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.',
|
|
4259
4706
|
'praxis.charts.editor.hint.sizingMode': 'Use preencher container apenas quando o widget host fornecer altura definida para o corpo.',
|
|
4260
4707
|
'praxis.charts.editor.hint.height': 'Números são salvos como pixels; medidas CSS como 20rem também são aceitas.',
|
|
4261
4708
|
'praxis.charts.editor.hint.minHeight': 'Defina um mínimo legível para widgets compactos de dashboard.',
|
|
@@ -4394,7 +4841,7 @@ function resolvePraxisChartsText(value, fallback) {
|
|
|
4394
4841
|
class ChartEditorDefaultsService {
|
|
4395
4842
|
create() {
|
|
4396
4843
|
return {
|
|
4397
|
-
version:
|
|
4844
|
+
version: PRAXIS_X_UI_CHART_AUTHORABLE_VERSION,
|
|
4398
4845
|
kind: 'bar',
|
|
4399
4846
|
title: 'Untitled chart',
|
|
4400
4847
|
source: {
|
|
@@ -4470,13 +4917,844 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
|
|
|
4470
4917
|
args: [{ providedIn: 'root' }]
|
|
4471
4918
|
}], ctorParameters: () => [{ type: PraxisChartCanonicalContractMapperService }] });
|
|
4472
4919
|
|
|
4473
|
-
|
|
4474
|
-
|
|
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
|
+
|
|
5751
|
+
class PraxisChartConfigEditor {
|
|
5752
|
+
documentInput = input(null, { ...(ngDevMode ? { debugName: "documentInput" } : /* istanbul ignore next */ {}), alias: 'document' });
|
|
4475
5753
|
modeInput = input('edit', { ...(ngDevMode ? { debugName: "modeInput" } : /* istanbul ignore next */ {}), alias: 'mode' });
|
|
4476
5754
|
readonlyInput = input(false, { ...(ngDevMode ? { debugName: "readonlyInput" } : /* istanbul ignore next */ {}), alias: 'readonly' });
|
|
4477
|
-
availableResourcesInput = input(
|
|
4478
|
-
availableFieldsInput = input(
|
|
4479
|
-
availableTargetsInput = input(
|
|
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' });
|
|
4480
5758
|
apply = output();
|
|
4481
5759
|
save = output();
|
|
4482
5760
|
resetChange = output();
|
|
@@ -4526,22 +5804,38 @@ class PraxisChartConfigEditor {
|
|
|
4526
5804
|
normalizer = inject(ChartContractNormalizerService);
|
|
4527
5805
|
validator = inject(ChartContractValidationService);
|
|
4528
5806
|
previewMapper = inject(ChartEditorPreviewMapperService);
|
|
5807
|
+
contextResolver = inject(PraxisChartConfigEditorContextResolverService);
|
|
4529
5808
|
i18n = inject(PraxisI18nService);
|
|
4530
5809
|
currentDocument = signal(this.defaults.create(), ...(ngDevMode ? [{ debugName: "currentDocument" }] : /* istanbul ignore next */ []));
|
|
4531
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 */ []));
|
|
4532
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 */ []));
|
|
4533
5819
|
normalizedDocument = computed(() => this.normalizer.normalize(this.currentDocument()), ...(ngDevMode ? [{ debugName: "normalizedDocument" }] : /* istanbul ignore next */ []));
|
|
4534
|
-
validation = computed(() =>
|
|
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 */ []));
|
|
4535
5831
|
issues = computed(() => this.validation().issues, ...(ngDevMode ? [{ debugName: "issues" }] : /* istanbul ignore next */ []));
|
|
4536
|
-
|
|
4537
|
-
|
|
4538
|
-
|
|
4539
|
-
|
|
4540
|
-
|
|
4541
|
-
|
|
4542
|
-
availableTargets = computed(() => this.availableTargetsInput().
|
|
4543
|
-
? this.availableTargetsInput()
|
|
4544
|
-
: (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 */ []));
|
|
4545
5839
|
preview = computed(() => {
|
|
4546
5840
|
if (!this.validation().valid) {
|
|
4547
5841
|
return null;
|
|
@@ -4549,6 +5843,21 @@ class PraxisChartConfigEditor {
|
|
|
4549
5843
|
return this.previewMapper.build(this.normalizedDocument());
|
|
4550
5844
|
}, ...(ngDevMode ? [{ debugName: "preview" }] : /* istanbul ignore next */ []));
|
|
4551
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
|
+
});
|
|
4552
5861
|
effect(() => {
|
|
4553
5862
|
const externalDocument = this.documentInput()
|
|
4554
5863
|
?? this.injectedData?.chartDocument
|
|
@@ -4567,6 +5876,29 @@ class PraxisChartConfigEditor {
|
|
|
4567
5876
|
this.lastExternalSignature = signature;
|
|
4568
5877
|
this.refreshState(false);
|
|
4569
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
|
+
});
|
|
4570
5902
|
}
|
|
4571
5903
|
getSettingsValue() {
|
|
4572
5904
|
return structuredClone(this.normalizedDocument());
|
|
@@ -4575,6 +5907,11 @@ class PraxisChartConfigEditor {
|
|
|
4575
5907
|
return this.saveChanges().document;
|
|
4576
5908
|
}
|
|
4577
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);
|
|
4578
5915
|
const snapshot = structuredClone(this.initialDocument());
|
|
4579
5916
|
this.currentDocument.set(snapshot);
|
|
4580
5917
|
this.refreshState(false);
|
|
@@ -4692,15 +6029,29 @@ class PraxisChartConfigEditor {
|
|
|
4692
6029
|
}));
|
|
4693
6030
|
}
|
|
4694
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();
|
|
4695
6035
|
this.patchDocument((document) => ({
|
|
4696
6036
|
...document,
|
|
4697
6037
|
source: document.source.kind === 'praxis.stats'
|
|
4698
6038
|
? {
|
|
4699
6039
|
...document.source,
|
|
4700
|
-
resource:
|
|
6040
|
+
resource: resource || undefined,
|
|
4701
6041
|
}
|
|
4702
6042
|
: document.source,
|
|
4703
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);
|
|
4704
6055
|
}
|
|
4705
6056
|
setOperation(value) {
|
|
4706
6057
|
this.patchDocument((document) => ({
|
|
@@ -4747,10 +6098,18 @@ class PraxisChartConfigEditor {
|
|
|
4747
6098
|
source: document.source.kind === 'praxis.stats'
|
|
4748
6099
|
? {
|
|
4749
6100
|
...document.source,
|
|
4750
|
-
options:
|
|
4751
|
-
|
|
4752
|
-
|
|
4753
|
-
|
|
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
|
+
},
|
|
4754
6113
|
}
|
|
4755
6114
|
: document.source,
|
|
4756
6115
|
}));
|
|
@@ -5019,6 +6378,29 @@ class PraxisChartConfigEditor {
|
|
|
5019
6378
|
resourceOptions() {
|
|
5020
6379
|
return this.availableResources();
|
|
5021
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
|
+
}
|
|
5022
6404
|
granularityValue() {
|
|
5023
6405
|
return this.doc().source.kind === 'praxis.stats'
|
|
5024
6406
|
? (this.doc().source.options?.granularity ?? 'day')
|
|
@@ -5112,8 +6494,15 @@ class PraxisChartConfigEditor {
|
|
|
5112
6494
|
eventTarget(eventKey) {
|
|
5113
6495
|
return this.doc().events?.[eventKey]?.target ?? '';
|
|
5114
6496
|
}
|
|
5115
|
-
targetOptions() {
|
|
5116
|
-
|
|
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;
|
|
5117
6506
|
}
|
|
5118
6507
|
eventMappingText(eventKey) {
|
|
5119
6508
|
const mapping = this.doc().events?.[eventKey]?.mapping;
|
|
@@ -5135,12 +6524,23 @@ class PraxisChartConfigEditor {
|
|
|
5135
6524
|
if (!fields.length) {
|
|
5136
6525
|
return [];
|
|
5137
6526
|
}
|
|
5138
|
-
return fields.filter((field) =>
|
|
5139
|
-
|
|
5140
|
-
|
|
5141
|
-
|
|
5142
|
-
|
|
5143
|
-
|
|
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;
|
|
5144
6544
|
}
|
|
5145
6545
|
showMetricAxisControls() {
|
|
5146
6546
|
return this.doc().kind === 'combo';
|
|
@@ -5215,6 +6615,184 @@ class PraxisChartConfigEditor {
|
|
|
5215
6615
|
return accumulator;
|
|
5216
6616
|
}, {});
|
|
5217
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
|
+
}
|
|
5218
6796
|
normalizeSizingSizeInput(value) {
|
|
5219
6797
|
const trimmed = value.trim();
|
|
5220
6798
|
if (!trimmed) {
|
|
@@ -5258,7 +6836,7 @@ class PraxisChartConfigEditor {
|
|
|
5258
6836
|
this.documentChange.emit(structuredClone(this.normalizedDocument()));
|
|
5259
6837
|
}
|
|
5260
6838
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: PraxisChartConfigEditor, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
5261
|
-
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 });
|
|
5262
6840
|
}
|
|
5263
6841
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: PraxisChartConfigEditor, decorators: [{
|
|
5264
6842
|
type: Component,
|
|
@@ -5271,7 +6849,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
|
|
|
5271
6849
|
MatSelectModule,
|
|
5272
6850
|
MatSlideToggleModule,
|
|
5273
6851
|
PraxisChartComponent
|
|
5274
|
-
], 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"] }]
|
|
5275
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"] }] } });
|
|
5276
6854
|
|
|
5277
6855
|
var praxisChartConfigEditor = /*#__PURE__*/Object.freeze({
|
|
@@ -5280,66 +6858,118 @@ var praxisChartConfigEditor = /*#__PURE__*/Object.freeze({
|
|
|
5280
6858
|
});
|
|
5281
6859
|
|
|
5282
6860
|
class PraxisChartWidgetConfigEditor {
|
|
6861
|
+
i18n = inject(PraxisI18nService);
|
|
5283
6862
|
inputs = null;
|
|
5284
|
-
|
|
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
|
+
}
|
|
5285
6885
|
isDirty$ = new BehaviorSubject(false);
|
|
5286
6886
|
isValid$ = new BehaviorSubject(true);
|
|
5287
6887
|
isBusy$ = new BehaviorSubject(false);
|
|
5288
|
-
|
|
6888
|
+
chartEditorDirty = false;
|
|
5289
6889
|
chartEditorValid = true;
|
|
5290
6890
|
queryContextDirty = false;
|
|
6891
|
+
baselineQueryContextText = '';
|
|
5291
6892
|
queryContextText = '';
|
|
5292
6893
|
queryContextError = '';
|
|
5293
6894
|
get chartDocument() {
|
|
5294
6895
|
return this.inputs?.chartDocument ?? null;
|
|
5295
6896
|
}
|
|
5296
6897
|
get availableResources() {
|
|
5297
|
-
return this.inputs?.availableResources ?? [];
|
|
6898
|
+
return this.context?.availableResources ?? this.inputs?.availableResources ?? [];
|
|
5298
6899
|
}
|
|
5299
6900
|
get availableFields() {
|
|
5300
|
-
return this.inputs?.availableFields ?? [];
|
|
6901
|
+
return this.context?.availableFields ?? this.inputs?.availableFields ?? [];
|
|
5301
6902
|
}
|
|
5302
6903
|
get availableTargets() {
|
|
5303
|
-
return this.inputs?.availableTargets ?? [];
|
|
5304
|
-
}
|
|
5305
|
-
ngAfterViewInit() {
|
|
5306
|
-
if (!this.chartEditor) {
|
|
5307
|
-
return;
|
|
5308
|
-
}
|
|
5309
|
-
this.subscription.add(this.chartEditor.isDirty$.subscribe((value) => this.isDirty$.next(value || this.queryContextDirty)));
|
|
5310
|
-
this.subscription.add(this.chartEditor.isValid$.subscribe((value) => {
|
|
5311
|
-
this.chartEditorValid = value;
|
|
5312
|
-
this.updateValidity();
|
|
5313
|
-
}));
|
|
5314
|
-
this.subscription.add(this.chartEditor.isBusy$.subscribe((value) => this.isBusy$.next(value)));
|
|
6904
|
+
return this.context?.availableTargets ?? this.inputs?.availableTargets ?? [];
|
|
5315
6905
|
}
|
|
5316
6906
|
ngOnChanges(changes) {
|
|
5317
|
-
if (changes['inputs']) {
|
|
5318
|
-
this.
|
|
5319
|
-
this.queryContextError = '';
|
|
5320
|
-
this.queryContextDirty = false;
|
|
5321
|
-
this.isDirty$.next(this.chartEditor?.isDirty$.value ?? false);
|
|
5322
|
-
this.updateValidity();
|
|
6907
|
+
if (changes['inputs'] && (changes['inputs'].firstChange || !this.isDirty$.value)) {
|
|
6908
|
+
this.rebaseQueryContext();
|
|
5323
6909
|
}
|
|
5324
6910
|
}
|
|
5325
6911
|
ngOnDestroy() {
|
|
5326
|
-
this.
|
|
6912
|
+
this.chartEditorSubscription.unsubscribe();
|
|
5327
6913
|
}
|
|
5328
6914
|
setQueryContextText(value) {
|
|
5329
6915
|
this.queryContextText = value;
|
|
5330
|
-
this.queryContextDirty =
|
|
6916
|
+
this.queryContextDirty = value !== this.baselineQueryContextText;
|
|
5331
6917
|
this.queryContextError = this.validateQueryContextText(value);
|
|
5332
|
-
this.
|
|
6918
|
+
this.updateDirty();
|
|
5333
6919
|
this.updateValidity();
|
|
5334
6920
|
}
|
|
5335
6921
|
getSettingsValue() {
|
|
5336
|
-
return this.buildValue(this.
|
|
6922
|
+
return this.buildValue(this.getChartDocumentForApply());
|
|
5337
6923
|
}
|
|
5338
6924
|
onSave() {
|
|
5339
|
-
|
|
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;
|
|
5340
6934
|
}
|
|
5341
6935
|
reset() {
|
|
5342
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
|
+
}
|
|
5343
6973
|
}
|
|
5344
6974
|
buildValue(document) {
|
|
5345
6975
|
const inputs = {
|
|
@@ -5347,19 +6977,57 @@ class PraxisChartWidgetConfigEditor {
|
|
|
5347
6977
|
...(document ? { chartDocument: document } : {}),
|
|
5348
6978
|
};
|
|
5349
6979
|
const queryContext = this.parseQueryContextText(this.queryContextText);
|
|
5350
|
-
if (queryContext === undefined) {
|
|
6980
|
+
if (queryContext.valid && queryContext.value === undefined) {
|
|
5351
6981
|
delete inputs.queryContext;
|
|
5352
6982
|
}
|
|
5353
|
-
else {
|
|
5354
|
-
inputs.queryContext = queryContext;
|
|
6983
|
+
else if (queryContext.valid) {
|
|
6984
|
+
inputs.queryContext = queryContext.value;
|
|
5355
6985
|
}
|
|
5356
6986
|
if (document?.source?.kind === 'praxis.stats') {
|
|
5357
6987
|
delete inputs.data;
|
|
5358
6988
|
}
|
|
6989
|
+
delete inputs.availableResources;
|
|
6990
|
+
delete inputs.availableFields;
|
|
6991
|
+
delete inputs.availableTargets;
|
|
6992
|
+
delete inputs.contextDiagnostics;
|
|
5359
6993
|
return {
|
|
5360
6994
|
inputs,
|
|
5361
6995
|
};
|
|
5362
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
|
+
}
|
|
5363
7031
|
updateValidity() {
|
|
5364
7032
|
this.isValid$.next(this.chartEditorValid && !this.queryContextError);
|
|
5365
7033
|
}
|
|
@@ -5378,42 +7046,70 @@ class PraxisChartWidgetConfigEditor {
|
|
|
5378
7046
|
const parsed = JSON.parse(trimmed);
|
|
5379
7047
|
return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
|
|
5380
7048
|
? ''
|
|
5381
|
-
: 'Query context must be a JSON object.';
|
|
7049
|
+
: this.t('praxis.charts.widget.queryContext.mustBeObject', 'Query context must be a JSON object.');
|
|
5382
7050
|
}
|
|
5383
|
-
catch
|
|
5384
|
-
return
|
|
7051
|
+
catch {
|
|
7052
|
+
return this.t('praxis.charts.widget.queryContext.invalidJson', 'Query context must be valid JSON.');
|
|
5385
7053
|
}
|
|
5386
7054
|
}
|
|
5387
7055
|
parseQueryContextText(value) {
|
|
5388
7056
|
const trimmed = value.trim();
|
|
5389
7057
|
if (!trimmed) {
|
|
5390
|
-
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 };
|
|
5391
7069
|
}
|
|
5392
|
-
const parsed = JSON.parse(trimmed);
|
|
5393
|
-
return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
|
|
5394
|
-
? parsed
|
|
5395
|
-
: null;
|
|
5396
7070
|
}
|
|
5397
7071
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: PraxisChartWidgetConfigEditor, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
5398
|
-
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: `
|
|
5399
7073
|
<section data-testid="chart-widget-config-editor">
|
|
5400
|
-
|
|
5401
|
-
|
|
5402
|
-
|
|
5403
|
-
|
|
5404
|
-
|
|
5405
|
-
|
|
5406
|
-
|
|
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
|
+
}
|
|
5407
7103
|
<section class="chart-widget-config-editor__runtime">
|
|
5408
7104
|
<label class="chart-widget-config-editor__label" for="chart-query-context">
|
|
5409
|
-
Query context
|
|
7105
|
+
{{ t('praxis.charts.widget.queryContext.label', 'Query context') }}
|
|
5410
7106
|
</label>
|
|
5411
7107
|
<textarea
|
|
5412
7108
|
id="chart-query-context"
|
|
5413
7109
|
class="chart-widget-config-editor__textarea"
|
|
5414
7110
|
rows="5"
|
|
5415
7111
|
spellcheck="false"
|
|
5416
|
-
aria-label="
|
|
7112
|
+
[attr.aria-label]="t('praxis.charts.widget.queryContext.ariaLabel', 'Chart query context')"
|
|
5417
7113
|
data-testid="chart-widget-query-context"
|
|
5418
7114
|
[ngModel]="queryContextText"
|
|
5419
7115
|
(ngModelChange)="setQueryContextText($event)"
|
|
@@ -5425,29 +7121,51 @@ class PraxisChartWidgetConfigEditor {
|
|
|
5425
7121
|
}
|
|
5426
7122
|
</section>
|
|
5427
7123
|
</section>
|
|
5428
|
-
`, 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 });
|
|
5429
7125
|
}
|
|
5430
7126
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: PraxisChartWidgetConfigEditor, decorators: [{
|
|
5431
7127
|
type: Component,
|
|
5432
7128
|
args: [{ selector: 'praxis-chart-widget-config-editor', standalone: true, imports: [FormsModule, PraxisChartConfigEditor], template: `
|
|
5433
7129
|
<section data-testid="chart-widget-config-editor">
|
|
5434
|
-
|
|
5435
|
-
|
|
5436
|
-
|
|
5437
|
-
|
|
5438
|
-
|
|
5439
|
-
|
|
5440
|
-
|
|
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
|
+
}
|
|
5441
7159
|
<section class="chart-widget-config-editor__runtime">
|
|
5442
7160
|
<label class="chart-widget-config-editor__label" for="chart-query-context">
|
|
5443
|
-
Query context
|
|
7161
|
+
{{ t('praxis.charts.widget.queryContext.label', 'Query context') }}
|
|
5444
7162
|
</label>
|
|
5445
7163
|
<textarea
|
|
5446
7164
|
id="chart-query-context"
|
|
5447
7165
|
class="chart-widget-config-editor__textarea"
|
|
5448
7166
|
rows="5"
|
|
5449
7167
|
spellcheck="false"
|
|
5450
|
-
aria-label="
|
|
7168
|
+
[attr.aria-label]="t('praxis.charts.widget.queryContext.ariaLabel', 'Chart query context')"
|
|
5451
7169
|
data-testid="chart-widget-query-context"
|
|
5452
7170
|
[ngModel]="queryContextText"
|
|
5453
7171
|
(ngModelChange)="setQueryContextText($event)"
|
|
@@ -5459,9 +7177,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
|
|
|
5459
7177
|
}
|
|
5460
7178
|
</section>
|
|
5461
7179
|
</section>
|
|
5462
|
-
`, 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"] }]
|
|
5463
7181
|
}], propDecorators: { inputs: [{
|
|
5464
7182
|
type: Input
|
|
7183
|
+
}], context: [{
|
|
7184
|
+
type: Input
|
|
7185
|
+
}], contextDiagnostics: [{
|
|
7186
|
+
type: Input
|
|
5465
7187
|
}], chartEditor: [{
|
|
5466
7188
|
type: ViewChild,
|
|
5467
7189
|
args: ['chartEditor']
|
|
@@ -5493,6 +7215,25 @@ const PRAXIS_CHART_PALETTE_DEFAULT_DATA = [
|
|
|
5493
7215
|
{ category: 'B', value: 32 },
|
|
5494
7216
|
{ category: 'C', value: 24 },
|
|
5495
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
|
+
};
|
|
5496
7237
|
const PRAXIS_CHART_PHASE1_PORTS = [
|
|
5497
7238
|
{
|
|
5498
7239
|
id: 'queryContext',
|
|
@@ -5555,7 +7296,7 @@ const PRAXIS_CHART_PHASE1_PORTS = [
|
|
|
5555
7296
|
label: 'Point Click',
|
|
5556
7297
|
direction: 'output',
|
|
5557
7298
|
semanticKind: 'event',
|
|
5558
|
-
description: '
|
|
7299
|
+
description: 'Raw renderer-neutral point evidence emitted when a point or series is clicked.',
|
|
5559
7300
|
cardinality: 'stream',
|
|
5560
7301
|
schema: {
|
|
5561
7302
|
id: 'PraxisChartPointEvent',
|
|
@@ -5564,6 +7305,20 @@ const PRAXIS_CHART_PHASE1_PORTS = [
|
|
|
5564
7305
|
},
|
|
5565
7306
|
exposure: { public: true, group: 'interaction' },
|
|
5566
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
|
+
},
|
|
5567
7322
|
{
|
|
5568
7323
|
id: 'selectionChange',
|
|
5569
7324
|
label: 'Selection Change',
|
|
@@ -5578,6 +7333,20 @@ const PRAXIS_CHART_PHASE1_PORTS = [
|
|
|
5578
7333
|
},
|
|
5579
7334
|
exposure: { public: true, group: 'interaction' },
|
|
5580
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
|
+
},
|
|
5581
7350
|
{
|
|
5582
7351
|
id: 'crossFilter',
|
|
5583
7352
|
label: 'Cross Filter',
|
|
@@ -5636,6 +7405,23 @@ const PRAXIS_CHART_COMPONENT_METADATA = {
|
|
|
5636
7405
|
component: PraxisChartWidgetConfigEditor,
|
|
5637
7406
|
title: 'Configure chart',
|
|
5638
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
|
+
],
|
|
5639
7425
|
inputs: [
|
|
5640
7426
|
{
|
|
5641
7427
|
name: 'config',
|
|
@@ -5653,6 +7439,7 @@ const PRAXIS_CHART_COMPONENT_METADATA = {
|
|
|
5653
7439
|
name: 'chartDocument',
|
|
5654
7440
|
type: 'PraxisXUiChartContract | null',
|
|
5655
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,
|
|
5656
7443
|
},
|
|
5657
7444
|
{
|
|
5658
7445
|
name: 'queryContext',
|
|
@@ -5678,19 +7465,19 @@ const PRAXIS_CHART_COMPONENT_METADATA = {
|
|
|
5678
7465
|
{
|
|
5679
7466
|
name: 'availableResources',
|
|
5680
7467
|
type: 'ReadonlyArray<ChartEditorResourceOption>',
|
|
5681
|
-
description: '
|
|
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.',
|
|
5682
7469
|
default: [],
|
|
5683
7470
|
},
|
|
5684
7471
|
{
|
|
5685
7472
|
name: 'availableFields',
|
|
5686
7473
|
type: 'ReadonlyArray<ChartEditorFieldOption>',
|
|
5687
|
-
description: '
|
|
7474
|
+
description: 'Transient capability-backed field catalog with exact operation, aggregation and distribution terms/histogram eligibility; not intended for widget persistence.',
|
|
5688
7475
|
default: [],
|
|
5689
7476
|
},
|
|
5690
7477
|
{
|
|
5691
7478
|
name: 'availableTargets',
|
|
5692
7479
|
type: 'ReadonlyArray<ChartEditorTargetOption>',
|
|
5693
|
-
description: '
|
|
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.',
|
|
5694
7481
|
default: [],
|
|
5695
7482
|
},
|
|
5696
7483
|
],
|
|
@@ -5698,12 +7485,22 @@ const PRAXIS_CHART_COMPONENT_METADATA = {
|
|
|
5698
7485
|
{
|
|
5699
7486
|
name: 'pointClick',
|
|
5700
7487
|
type: 'PraxisChartPointEvent',
|
|
5701
|
-
description: '
|
|
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.',
|
|
5702
7494
|
},
|
|
5703
7495
|
{
|
|
5704
7496
|
name: 'selectionChange',
|
|
5705
7497
|
type: 'PraxisChartSelectionEvent',
|
|
5706
|
-
description: 'Emitted when declarative chart selection resolves
|
|
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.',
|
|
5707
7504
|
},
|
|
5708
7505
|
{
|
|
5709
7506
|
name: 'crossFilter',
|
|
@@ -5747,10 +7544,18 @@ function providePraxisChartsMetadata() {
|
|
|
5747
7544
|
return {
|
|
5748
7545
|
provide: ENVIRONMENT_INITIALIZER,
|
|
5749
7546
|
multi: true,
|
|
5750
|
-
useFactory: (registry) => () => {
|
|
5751
|
-
registry.register(
|
|
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
|
+
});
|
|
5752
7557
|
},
|
|
5753
|
-
deps: [ComponentMetadataRegistry],
|
|
7558
|
+
deps: [ComponentMetadataRegistry, Injector],
|
|
5754
7559
|
};
|
|
5755
7560
|
}
|
|
5756
7561
|
|
|
@@ -5832,7 +7637,9 @@ class PraxisChartShowcaseWidgetComponent {
|
|
|
5832
7637
|
enableCustomization = input(false, ...(ngDevMode ? [{ debugName: "enableCustomization" }] : /* istanbul ignore next */ []));
|
|
5833
7638
|
viewMode = input('chart', ...(ngDevMode ? [{ debugName: "viewMode" }] : /* istanbul ignore next */ []));
|
|
5834
7639
|
pointClick = output();
|
|
7640
|
+
pointAction = output();
|
|
5835
7641
|
selectionChange = output();
|
|
7642
|
+
drillDown = output();
|
|
5836
7643
|
crossFilter = output();
|
|
5837
7644
|
queryRequest = output();
|
|
5838
7645
|
loadStateChange = output();
|
|
@@ -5948,7 +7755,7 @@ class PraxisChartShowcaseWidgetComponent {
|
|
|
5948
7755
|
});
|
|
5949
7756
|
}
|
|
5950
7757
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: PraxisChartShowcaseWidgetComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
5951
|
-
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: `
|
|
5952
7759
|
@if (viewMode() === 'chart') {
|
|
5953
7760
|
<praxis-chart
|
|
5954
7761
|
[config]="config()"
|
|
@@ -5957,7 +7764,9 @@ class PraxisChartShowcaseWidgetComponent {
|
|
|
5957
7764
|
[remoteDataResolver]="remoteDataResolver()"
|
|
5958
7765
|
[enableCustomization]="enableCustomization()"
|
|
5959
7766
|
(pointClick)="pointClick.emit($event)"
|
|
7767
|
+
(pointAction)="pointAction.emit($event)"
|
|
5960
7768
|
(selectionChange)="selectionChange.emit($event)"
|
|
7769
|
+
(drillDown)="drillDown.emit($event)"
|
|
5961
7770
|
(crossFilter)="crossFilter.emit($event)"
|
|
5962
7771
|
(queryRequest)="queryRequest.emit($event)"
|
|
5963
7772
|
(loadStateChange)="loadStateChange.emit($event)"
|
|
@@ -5984,7 +7793,7 @@ class PraxisChartShowcaseWidgetComponent {
|
|
|
5984
7793
|
(rowClick)="handleRowClick($event)"
|
|
5985
7794
|
></praxis-table>
|
|
5986
7795
|
}
|
|
5987
|
-
`, 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 });
|
|
5988
7797
|
}
|
|
5989
7798
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: PraxisChartShowcaseWidgetComponent, decorators: [{
|
|
5990
7799
|
type: Component,
|
|
@@ -5997,7 +7806,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
|
|
|
5997
7806
|
[remoteDataResolver]="remoteDataResolver()"
|
|
5998
7807
|
[enableCustomization]="enableCustomization()"
|
|
5999
7808
|
(pointClick)="pointClick.emit($event)"
|
|
7809
|
+
(pointAction)="pointAction.emit($event)"
|
|
6000
7810
|
(selectionChange)="selectionChange.emit($event)"
|
|
7811
|
+
(drillDown)="drillDown.emit($event)"
|
|
6001
7812
|
(crossFilter)="crossFilter.emit($event)"
|
|
6002
7813
|
(queryRequest)="queryRequest.emit($event)"
|
|
6003
7814
|
(loadStateChange)="loadStateChange.emit($event)"
|
|
@@ -6025,7 +7836,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
|
|
|
6025
7836
|
></praxis-table>
|
|
6026
7837
|
}
|
|
6027
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"] }]
|
|
6028
|
-
}], 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"] }] } });
|
|
6029
7840
|
function resolveShowcaseStateMode(config, remoteRuntimeState = 'idle') {
|
|
6030
7841
|
if (config.preferredLoadState === 'loading') {
|
|
6031
7842
|
return 'loading';
|
|
@@ -6204,13 +8015,23 @@ const PRAXIS_CHART_SHOWCASE_WIDGET_METADATA = {
|
|
|
6204
8015
|
{
|
|
6205
8016
|
name: 'pointClick',
|
|
6206
8017
|
type: 'PraxisChartPointEvent',
|
|
6207
|
-
description: 'Forwards point
|
|
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.',
|
|
6208
8024
|
},
|
|
6209
8025
|
{
|
|
6210
8026
|
name: 'selectionChange',
|
|
6211
8027
|
type: 'PraxisChartSelectionEvent',
|
|
6212
8028
|
description: 'Forwards chart selection events with mapped filter values.',
|
|
6213
8029
|
},
|
|
8030
|
+
{
|
|
8031
|
+
name: 'drillDown',
|
|
8032
|
+
type: 'PraxisChartDrillDownEvent',
|
|
8033
|
+
description: 'Forwards chart drill-down action payloads for Dynamic Page composition links.',
|
|
8034
|
+
},
|
|
6214
8035
|
{
|
|
6215
8036
|
name: 'crossFilter',
|
|
6216
8037
|
type: 'PraxisChartCrossFilterEvent',
|
|
@@ -6281,38 +8102,10 @@ function providePraxisCharts(options = {}) {
|
|
|
6281
8102
|
];
|
|
6282
8103
|
}
|
|
6283
8104
|
|
|
6284
|
-
class PraxisChartMetadataRegistrationService {
|
|
6285
|
-
registry;
|
|
6286
|
-
registered = false;
|
|
6287
|
-
constructor(registry) {
|
|
6288
|
-
this.registry = registry;
|
|
6289
|
-
this.ensureRegistered();
|
|
6290
|
-
}
|
|
6291
|
-
ensureRegistered() {
|
|
6292
|
-
if (this.registered) {
|
|
6293
|
-
return;
|
|
6294
|
-
}
|
|
6295
|
-
this.registry.register(PRAXIS_CHART_COMPONENT_METADATA);
|
|
6296
|
-
this.registry.register(PRAXIS_CHART_DRILLDOWN_PANEL_METADATA);
|
|
6297
|
-
this.registry.register(PRAXIS_CHART_STATE_PROBE_COMPONENT_METADATA);
|
|
6298
|
-
this.registry.register(PRAXIS_CHART_SHOWCASE_WIDGET_METADATA);
|
|
6299
|
-
this.registered = true;
|
|
6300
|
-
}
|
|
6301
|
-
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 });
|
|
6302
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: PraxisChartMetadataRegistrationService, providedIn: 'root' });
|
|
6303
|
-
}
|
|
6304
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: PraxisChartMetadataRegistrationService, decorators: [{
|
|
6305
|
-
type: Injectable,
|
|
6306
|
-
args: [{ providedIn: 'root' }]
|
|
6307
|
-
}], ctorParameters: () => [{ type: i1$1.ComponentMetadataRegistry }] });
|
|
6308
|
-
|
|
6309
8105
|
class PraxisChartSchemaMapperService {
|
|
6310
|
-
metadataRegistration;
|
|
6311
8106
|
canonicalContractMapper;
|
|
6312
|
-
constructor(
|
|
6313
|
-
this.metadataRegistration = metadataRegistration;
|
|
8107
|
+
constructor(canonicalContractMapper) {
|
|
6314
8108
|
this.canonicalContractMapper = canonicalContractMapper;
|
|
6315
|
-
this.metadataRegistration.ensureRegistered();
|
|
6316
8109
|
}
|
|
6317
8110
|
resolve(input, defaults) {
|
|
6318
8111
|
const schema = this.normalize(input);
|
|
@@ -6401,8 +8194,12 @@ class PraxisChartSchemaMapperService {
|
|
|
6401
8194
|
}
|
|
6402
8195
|
const events = schema.chart?.events;
|
|
6403
8196
|
const outputs = {};
|
|
6404
|
-
if (events?.pointClick
|
|
8197
|
+
if (events?.pointClick) {
|
|
6405
8198
|
outputs['pointClick'] = 'emit';
|
|
8199
|
+
outputs['pointAction'] = 'emit';
|
|
8200
|
+
}
|
|
8201
|
+
if (events?.drillDown) {
|
|
8202
|
+
outputs['drillDown'] = 'emit';
|
|
6406
8203
|
}
|
|
6407
8204
|
if (events?.selectionChange) {
|
|
6408
8205
|
outputs['selectionChange'] = 'emit';
|
|
@@ -6412,13 +8209,13 @@ class PraxisChartSchemaMapperService {
|
|
|
6412
8209
|
}
|
|
6413
8210
|
return Object.keys(outputs).length ? outputs : undefined;
|
|
6414
8211
|
}
|
|
6415
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: PraxisChartSchemaMapperService, deps: [{ token:
|
|
8212
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: PraxisChartSchemaMapperService, deps: [{ token: PraxisChartCanonicalContractMapperService }], target: i0.ɵɵFactoryTarget.Injectable });
|
|
6416
8213
|
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: PraxisChartSchemaMapperService, providedIn: 'root' });
|
|
6417
8214
|
}
|
|
6418
8215
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: PraxisChartSchemaMapperService, decorators: [{
|
|
6419
8216
|
type: Injectable,
|
|
6420
8217
|
args: [{ providedIn: 'root' }]
|
|
6421
|
-
}], ctorParameters: () => [{ type:
|
|
8218
|
+
}], ctorParameters: () => [{ type: PraxisChartCanonicalContractMapperService }] });
|
|
6422
8219
|
|
|
6423
8220
|
class PraxisChartBackendPayloadAdapterService {
|
|
6424
8221
|
chartSchemaMapper;
|
|
@@ -6472,7 +8269,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
|
|
|
6472
8269
|
}], ctorParameters: () => [{ type: PraxisChartSchemaMapperService }, { type: PraxisChartCanonicalContractMapperService }] });
|
|
6473
8270
|
|
|
6474
8271
|
class AnalyticsChartConfigAdapterService {
|
|
8272
|
+
i18n;
|
|
6475
8273
|
statsBuilder = new AnalyticsStatsRequestBuilderService();
|
|
8274
|
+
constructor(i18n) {
|
|
8275
|
+
this.i18n = i18n;
|
|
8276
|
+
}
|
|
6476
8277
|
toPraxisChartConfig(projection, options) {
|
|
6477
8278
|
const chartType = this.resolveChartType(projection);
|
|
6478
8279
|
const orientation = chartType === 'horizontal-bar' ? 'horizontal' : undefined;
|
|
@@ -6484,6 +8285,11 @@ class AnalyticsChartConfigAdapterService {
|
|
|
6484
8285
|
if (!metrics.length) {
|
|
6485
8286
|
throw new Error(`AnalyticsChartConfigAdapterService requires at least one metric for projection "${projection.id}".`);
|
|
6486
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
|
+
}
|
|
6487
8293
|
return {
|
|
6488
8294
|
id: projection.id,
|
|
6489
8295
|
type: chartType,
|
|
@@ -6498,6 +8304,17 @@ class AnalyticsChartConfigAdapterService {
|
|
|
6498
8304
|
pointClick: Boolean(projection.interactions?.pointSelection || projection.interactions?.drillDown),
|
|
6499
8305
|
drillDown: Boolean(projection.interactions?.drillDown),
|
|
6500
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
|
+
: {}),
|
|
6501
8318
|
},
|
|
6502
8319
|
};
|
|
6503
8320
|
}
|
|
@@ -6529,18 +8346,27 @@ class AnalyticsChartConfigAdapterService {
|
|
|
6529
8346
|
}
|
|
6530
8347
|
buildSeries(projection, chartType) {
|
|
6531
8348
|
const dimension = projection.bindings.primaryDimension;
|
|
6532
|
-
|
|
6533
|
-
|
|
6534
|
-
|
|
6535
|
-
|
|
6536
|
-
|
|
6537
|
-
|
|
6538
|
-
|
|
6539
|
-
aggregation: this.mapAggregation(metric.aggregation),
|
|
6540
|
-
|
|
6541
|
-
|
|
6542
|
-
|
|
6543
|
-
|
|
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
|
+
}]);
|
|
6544
8370
|
}
|
|
6545
8371
|
buildDataSource(projection) {
|
|
6546
8372
|
const executionPlan = this.statsBuilder.buildExecutionPlan(projection);
|
|
@@ -6554,7 +8380,13 @@ class AnalyticsChartConfigAdapterService {
|
|
|
6554
8380
|
statsPath: executionPlan.statsPath,
|
|
6555
8381
|
statsRequest: executionPlan.statsRequest,
|
|
6556
8382
|
dimensions: executionPlan.dimensions,
|
|
6557
|
-
metrics:
|
|
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),
|
|
6558
8390
|
sort: executionPlan.sort,
|
|
6559
8391
|
limit: executionPlan.limit,
|
|
6560
8392
|
},
|
|
@@ -6604,13 +8436,25 @@ class AnalyticsChartConfigAdapterService {
|
|
|
6604
8436
|
...(projection.bindings.secondaryMetrics ?? []),
|
|
6605
8437
|
];
|
|
6606
8438
|
}
|
|
6607
|
-
|
|
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 });
|
|
6608
8450
|
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: AnalyticsChartConfigAdapterService, providedIn: 'root' });
|
|
6609
8451
|
}
|
|
6610
8452
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: AnalyticsChartConfigAdapterService, decorators: [{
|
|
6611
8453
|
type: Injectable,
|
|
6612
8454
|
args: [{ providedIn: 'root' }]
|
|
6613
|
-
}]
|
|
8455
|
+
}], ctorParameters: () => [{ type: i1$1.PraxisI18nService, decorators: [{
|
|
8456
|
+
type: Optional
|
|
8457
|
+
}] }] });
|
|
6614
8458
|
|
|
6615
8459
|
class AnalyticsChartContractService {
|
|
6616
8460
|
analyticsSchema;
|
|
@@ -6713,7 +8557,7 @@ function normalizeError(error) {
|
|
|
6713
8557
|
|
|
6714
8558
|
const PRAXIS_CHART_BACKEND_MOCK_BAR = {
|
|
6715
8559
|
schemaMeta: {
|
|
6716
|
-
schemaId: 'api/human-resources/vw-perfil-heroi|post|response|
|
|
8560
|
+
schemaId: 'api/human-resources/vw-perfil-heroi|post|response|internal:false',
|
|
6717
8561
|
schemaHash: 'published-hash-hero-universe-v1',
|
|
6718
8562
|
resourcePath: 'api/human-resources/vw-perfil-heroi',
|
|
6719
8563
|
operation: 'post',
|
|
@@ -6743,7 +8587,7 @@ const PRAXIS_CHART_BACKEND_MOCK_BAR = {
|
|
|
6743
8587
|
sizing: { mode: 'fixed', height: 320 },
|
|
6744
8588
|
source: {
|
|
6745
8589
|
kind: 'praxis.stats',
|
|
6746
|
-
resource: 'api/human-resources/vw-perfil-heroi',
|
|
8590
|
+
resource: '/api/human-resources/vw-perfil-heroi',
|
|
6747
8591
|
operation: 'group-by',
|
|
6748
8592
|
options: {
|
|
6749
8593
|
orderBy: 'value-desc',
|
|
@@ -6795,7 +8639,7 @@ const PRAXIS_CHART_BACKEND_MOCK_BAR = {
|
|
|
6795
8639
|
};
|
|
6796
8640
|
const PRAXIS_CHART_BACKEND_MOCK_TIMESERIES = {
|
|
6797
8641
|
schemaMeta: {
|
|
6798
|
-
schemaId: 'api/human-resources/vw-indicadores-incidentes|post|response|
|
|
8642
|
+
schemaId: 'api/human-resources/vw-indicadores-incidentes|post|response|internal:false',
|
|
6799
8643
|
schemaHash: 'published-hash-incident-timeseries-v1',
|
|
6800
8644
|
resourcePath: 'api/human-resources/vw-indicadores-incidentes',
|
|
6801
8645
|
operation: 'post',
|
|
@@ -6819,7 +8663,7 @@ const PRAXIS_CHART_BACKEND_MOCK_TIMESERIES = {
|
|
|
6819
8663
|
subtitle: { key: 'charts.incidents.timeline.subtitle', fallback: 'Timeseries em /vw-indicadores-incidentes' },
|
|
6820
8664
|
source: {
|
|
6821
8665
|
kind: 'praxis.stats',
|
|
6822
|
-
resource: 'api/human-resources/vw-indicadores-incidentes',
|
|
8666
|
+
resource: '/api/human-resources/vw-indicadores-incidentes',
|
|
6823
8667
|
operation: 'timeseries',
|
|
6824
8668
|
options: {
|
|
6825
8669
|
granularity: 'month',
|
|
@@ -6851,7 +8695,7 @@ const PRAXIS_CHART_BACKEND_MOCK_TIMESERIES = {
|
|
|
6851
8695
|
};
|
|
6852
8696
|
const PRAXIS_CHART_BACKEND_MOCK_DONUT = {
|
|
6853
8697
|
schemaMeta: {
|
|
6854
|
-
schemaId: 'api/human-resources/vw-indicadores-incidentes|post|response|
|
|
8698
|
+
schemaId: 'api/human-resources/vw-indicadores-incidentes|post|response|internal:false',
|
|
6855
8699
|
schemaHash: 'published-hash-incident-severity-v1',
|
|
6856
8700
|
resourcePath: 'api/human-resources/vw-indicadores-incidentes',
|
|
6857
8701
|
operation: 'post',
|
|
@@ -6874,7 +8718,7 @@ const PRAXIS_CHART_BACKEND_MOCK_DONUT = {
|
|
|
6874
8718
|
title: { key: 'charts.incidents.severity.title', fallback: 'Severidade de incidentes' },
|
|
6875
8719
|
source: {
|
|
6876
8720
|
kind: 'praxis.stats',
|
|
6877
|
-
resource: 'api/human-resources/vw-indicadores-incidentes',
|
|
8721
|
+
resource: '/api/human-resources/vw-indicadores-incidentes',
|
|
6878
8722
|
operation: 'distribution',
|
|
6879
8723
|
options: {
|
|
6880
8724
|
mode: 'terms',
|
|
@@ -6907,7 +8751,7 @@ const PRAXIS_CHART_BACKEND_MOCK_DONUT = {
|
|
|
6907
8751
|
};
|
|
6908
8752
|
const PRAXIS_CHART_BACKEND_MOCK_HORIZONTAL_BAR = {
|
|
6909
8753
|
schemaMeta: {
|
|
6910
|
-
schemaId: 'api/human-resources/vw-analytics-folha-pagamento|post|response|
|
|
8754
|
+
schemaId: 'api/human-resources/vw-analytics-folha-pagamento|post|response|internal:false',
|
|
6911
8755
|
schemaHash: 'published-hash-payroll-department-ranking-v1',
|
|
6912
8756
|
resourcePath: 'api/human-resources/vw-analytics-folha-pagamento',
|
|
6913
8757
|
operation: 'post',
|
|
@@ -6933,7 +8777,7 @@ const PRAXIS_CHART_BACKEND_MOCK_HORIZONTAL_BAR = {
|
|
|
6933
8777
|
sizing: { mode: 'fixed', height: 340 },
|
|
6934
8778
|
source: {
|
|
6935
8779
|
kind: 'praxis.stats',
|
|
6936
|
-
resource: 'api/human-resources/vw-analytics-folha-pagamento',
|
|
8780
|
+
resource: '/api/human-resources/vw-analytics-folha-pagamento',
|
|
6937
8781
|
operation: 'group-by',
|
|
6938
8782
|
options: {
|
|
6939
8783
|
orderBy: 'value-desc',
|
|
@@ -6969,7 +8813,7 @@ const PRAXIS_CHART_BACKEND_MOCK_HORIZONTAL_BAR = {
|
|
|
6969
8813
|
};
|
|
6970
8814
|
const PRAXIS_CHART_BACKEND_MOCK_STACKED_AREA = {
|
|
6971
8815
|
schemaMeta: {
|
|
6972
|
-
schemaId: 'api/human-resources/vw-analytics-folha-pagamento|post|response|
|
|
8816
|
+
schemaId: 'api/human-resources/vw-analytics-folha-pagamento|post|response|internal:false',
|
|
6973
8817
|
schemaHash: 'published-hash-payroll-net-trend-stacked-v1',
|
|
6974
8818
|
resourcePath: 'api/human-resources/vw-analytics-folha-pagamento',
|
|
6975
8819
|
operation: 'post',
|
|
@@ -6994,7 +8838,7 @@ const PRAXIS_CHART_BACKEND_MOCK_STACKED_AREA = {
|
|
|
6994
8838
|
sizing: { mode: 'fixed', height: 340 },
|
|
6995
8839
|
source: {
|
|
6996
8840
|
kind: 'praxis.stats',
|
|
6997
|
-
resource: 'api/human-resources/vw-analytics-folha-pagamento',
|
|
8841
|
+
resource: '/api/human-resources/vw-analytics-folha-pagamento',
|
|
6998
8842
|
operation: 'timeseries',
|
|
6999
8843
|
options: {
|
|
7000
8844
|
granularity: 'month',
|
|
@@ -7025,7 +8869,7 @@ const PRAXIS_CHART_BACKEND_MOCK_STACKED_AREA = {
|
|
|
7025
8869
|
};
|
|
7026
8870
|
const PRAXIS_CHART_BACKEND_MOCK_MULTI_METRIC_BAR = {
|
|
7027
8871
|
schemaMeta: {
|
|
7028
|
-
schemaId: 'api/human-resources/vw-analytics-folha-pagamento|post|response|
|
|
8872
|
+
schemaId: 'api/human-resources/vw-analytics-folha-pagamento|post|response|internal:false',
|
|
7029
8873
|
schemaHash: 'published-hash-payroll-multi-metric-bar-v1',
|
|
7030
8874
|
resourcePath: 'api/human-resources/vw-analytics-folha-pagamento',
|
|
7031
8875
|
operation: 'post',
|
|
@@ -7053,7 +8897,7 @@ const PRAXIS_CHART_BACKEND_MOCK_MULTI_METRIC_BAR = {
|
|
|
7053
8897
|
sizing: { mode: 'fixed', height: 340 },
|
|
7054
8898
|
source: {
|
|
7055
8899
|
kind: 'praxis.stats',
|
|
7056
|
-
resource: 'api/human-resources/vw-analytics-folha-pagamento',
|
|
8900
|
+
resource: '/api/human-resources/vw-analytics-folha-pagamento',
|
|
7057
8901
|
operation: 'group-by',
|
|
7058
8902
|
options: {
|
|
7059
8903
|
orderBy: 'value-desc',
|
|
@@ -7091,7 +8935,7 @@ const PRAXIS_CHART_BACKEND_MOCK_MULTI_METRIC_BAR = {
|
|
|
7091
8935
|
};
|
|
7092
8936
|
const PRAXIS_CHART_BACKEND_MOCK_SCATTER = {
|
|
7093
8937
|
schemaMeta: {
|
|
7094
|
-
schemaId: 'api/human-resources/vw-analytics-folha-pagamento|post|response|
|
|
8938
|
+
schemaId: 'api/human-resources/vw-analytics-folha-pagamento|post|response|internal:false',
|
|
7095
8939
|
schemaHash: 'published-hash-payroll-scatter-v1',
|
|
7096
8940
|
resourcePath: 'api/human-resources/vw-analytics-folha-pagamento',
|
|
7097
8941
|
operation: 'post',
|
|
@@ -7116,7 +8960,7 @@ const PRAXIS_CHART_BACKEND_MOCK_SCATTER = {
|
|
|
7116
8960
|
sizing: { mode: 'fixed', height: 340 },
|
|
7117
8961
|
source: {
|
|
7118
8962
|
kind: 'praxis.stats',
|
|
7119
|
-
resource: 'api/human-resources/vw-analytics-folha-pagamento',
|
|
8963
|
+
resource: '/api/human-resources/vw-analytics-folha-pagamento',
|
|
7120
8964
|
operation: 'group-by',
|
|
7121
8965
|
options: {
|
|
7122
8966
|
orderBy: 'key-asc',
|
|
@@ -7147,7 +8991,7 @@ const PRAXIS_CHART_BACKEND_MOCK_SCATTER = {
|
|
|
7147
8991
|
};
|
|
7148
8992
|
const PRAXIS_CHART_BACKEND_MOCK_COMBO = {
|
|
7149
8993
|
schemaMeta: {
|
|
7150
|
-
schemaId: 'api/human-resources/vw-analytics-folha-pagamento|post|response|
|
|
8994
|
+
schemaId: 'api/human-resources/vw-analytics-folha-pagamento|post|response|internal:false',
|
|
7151
8995
|
schemaHash: 'published-hash-payroll-combo-v2',
|
|
7152
8996
|
resourcePath: 'api/human-resources/vw-analytics-folha-pagamento',
|
|
7153
8997
|
operation: 'post',
|
|
@@ -7172,7 +9016,7 @@ const PRAXIS_CHART_BACKEND_MOCK_COMBO = {
|
|
|
7172
9016
|
sizing: { mode: 'fixed', height: 340 },
|
|
7173
9017
|
source: {
|
|
7174
9018
|
kind: 'praxis.stats',
|
|
7175
|
-
resource: 'api/human-resources/vw-analytics-folha-pagamento',
|
|
9019
|
+
resource: '/api/human-resources/vw-analytics-folha-pagamento',
|
|
7176
9020
|
operation: 'timeseries',
|
|
7177
9021
|
options: {
|
|
7178
9022
|
granularity: 'month',
|
|
@@ -7655,7 +9499,7 @@ class PraxisChartCompositionShowcaseComponent {
|
|
|
7655
9499
|
};
|
|
7656
9500
|
}
|
|
7657
9501
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: PraxisChartCompositionShowcaseComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
7658
|
-
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 } },
|
|
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: `
|
|
7659
9503
|
<section class="showcase-shell">
|
|
7660
9504
|
<header class="showcase-hero">
|
|
7661
9505
|
<div class="showcase-copy">
|
|
@@ -7771,7 +9615,7 @@ class PraxisChartCompositionShowcaseComponent {
|
|
|
7771
9615
|
}
|
|
7772
9616
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: PraxisChartCompositionShowcaseComponent, decorators: [{
|
|
7773
9617
|
type: Component,
|
|
7774
|
-
args: [{ selector: 'praxis-chart-composition-showcase', standalone: true, imports: [DynamicWidgetPageComponent],
|
|
9618
|
+
args: [{ selector: 'praxis-chart-composition-showcase', standalone: true, imports: [DynamicWidgetPageComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: `
|
|
7775
9619
|
<section class="showcase-shell">
|
|
7776
9620
|
<header class="showcase-hero">
|
|
7777
9621
|
<div class="showcase-copy">
|
|
@@ -7886,11 +9730,65 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
|
|
|
7886
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"] }]
|
|
7887
9731
|
}], propDecorators: { enableCustomization: [{ type: i0.Input, args: [{ isSignal: true, alias: "enableCustomization", required: false }] }] } });
|
|
7888
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
|
+
|
|
7889
9787
|
const chartDocumentSchema = {
|
|
7890
9788
|
type: 'object',
|
|
7891
9789
|
required: ['version', 'kind', 'source'],
|
|
7892
9790
|
properties: {
|
|
7893
|
-
version: { const:
|
|
9791
|
+
version: { const: PRAXIS_X_UI_CHART_AUTHORABLE_VERSION },
|
|
7894
9792
|
kind: { enum: ['bar', 'combo', 'horizontal-bar', 'line', 'pie', 'donut', 'area', 'stacked-bar', 'stacked-area', 'scatter'] },
|
|
7895
9793
|
chartId: { type: 'string' },
|
|
7896
9794
|
title: { oneOf: [{ type: 'string' }, { type: 'object' }] },
|
|
@@ -7919,21 +9817,22 @@ const PRAXIS_CHARTS_AUTHORING_MANIFEST = {
|
|
|
7919
9817
|
runtimeInputs: [
|
|
7920
9818
|
{ name: 'chartDocument', type: 'PraxisXUiChartContract | null', description: 'Canonical x-ui.chart document used as the authoring source of truth.' },
|
|
7921
9819
|
{ name: 'config', type: 'PraxisChartConfig', description: 'Runtime chart configuration mapped from or supplied beside the canonical chart document.' },
|
|
7922
|
-
{ name: 'queryContext', type: 'PraxisChartQueryContext | null', description: '
|
|
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.' },
|
|
7923
9821
|
{ name: 'remoteDataResolver', type: 'PraxisChartRemoteDataResolver | null', description: 'Host-governed remote row resolver for integrations outside the default praxis.stats client.' },
|
|
7924
|
-
{ name: 'availableResources', type: 'ChartEditorResourceOption[]', description: '
|
|
7925
|
-
{ name: 'availableFields', type: 'ChartEditorFieldOption[]', description: '
|
|
7926
|
-
{ name: 'availableTargets', type: 'ChartEditorTargetOption[]', description: '
|
|
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.' },
|
|
7927
9825
|
],
|
|
7928
9826
|
editableTargets: [
|
|
7929
9827
|
{ kind: 'chartType', resolver: 'x-ui-chart-kind', description: 'Canonical `kind` field in PraxisXUiChartContract.' },
|
|
7930
9828
|
{ kind: 'series', resolver: 'x-ui-chart-metric-by-field', description: 'Metric-backed series definition in `metrics[]`, keyed by field.' },
|
|
7931
9829
|
{ kind: 'axis', resolver: 'x-ui-chart-dimension-or-metric-axis', description: 'Dimension role, metric axis and chart orientation semantics.' },
|
|
7932
9830
|
{ kind: 'dataBinding', resolver: 'x-ui-chart-source-and-field-catalog', description: 'Source, dimensions, metrics and resource/field bindings.' },
|
|
7933
|
-
{ kind: 'queryContext', resolver: 'praxis-chart-query-context', description: 'Runtime queryContext contract for filters, sort, limit and
|
|
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.' },
|
|
7934
9833
|
{ kind: 'crossFilter', resolver: 'x-ui-chart-events-cross-filter', description: 'Cross-filter event mapping emitted as structured query-context filters.' },
|
|
7935
9834
|
{ kind: 'drilldown', resolver: 'x-ui-chart-events-drill-down', description: 'Drilldown event mapping and governed target.' },
|
|
7936
|
-
{ kind: 'selection', resolver: 'x-ui-chart-events-selection-change', description: '
|
|
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.' },
|
|
7937
9836
|
{ kind: 'legend', resolver: 'x-ui-chart-legend-feature', description: 'Legend visibility through boolean or toggleable feature object.' },
|
|
7938
9837
|
{ kind: 'tooltip', resolver: 'x-ui-chart-tooltip-feature', description: 'Tooltip visibility through boolean or toggleable feature object.' },
|
|
7939
9838
|
],
|
|
@@ -8073,7 +9972,7 @@ const PRAXIS_CHARTS_AUTHORING_MANIFEST = {
|
|
|
8073
9972
|
required: ['sourceKind'],
|
|
8074
9973
|
properties: {
|
|
8075
9974
|
sourceKind: { enum: ['praxis.stats', 'derived'] },
|
|
8076
|
-
resource: { type: 'string' },
|
|
9975
|
+
resource: { type: 'string', pattern: '^/(?!/)' },
|
|
8077
9976
|
operation: { enum: ['group-by', 'timeseries', 'distribution'] },
|
|
8078
9977
|
dimensions: { type: 'array', items: { type: 'object', required: ['field'], properties: { field: { type: 'string' }, role: { type: 'string' } } } },
|
|
8079
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' } } } },
|
|
@@ -8082,10 +9981,10 @@ const PRAXIS_CHARTS_AUTHORING_MANIFEST = {
|
|
|
8082
9981
|
effects: [{ kind: 'compile-domain-patch', handler: 'chart-data-resource-bind', handlerContract: {
|
|
8083
9982
|
reads: ['availableResources[]', 'availableFields[]', 'chartDocument.source', 'chartDocument.dimensions[]', 'chartDocument.metrics[]'],
|
|
8084
9983
|
writes: ['chartDocument.source', 'chartDocument.dimensions[]', 'chartDocument.metrics[]'],
|
|
8085
|
-
identityKeys: ['availableResources[].path', '
|
|
8086
|
-
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' } } },
|
|
8087
9986
|
failureModes: ['resource-not-in-api-metadata', 'field-not-in-schema', 'stats-operation-not-supported', 'remote-binding-incomplete'],
|
|
8088
|
-
description: 'Binds a derived or praxis.stats source using the
|
|
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.',
|
|
8089
9988
|
} }],
|
|
8090
9989
|
destructive: false,
|
|
8091
9990
|
requiresConfirmation: false,
|
|
@@ -8100,15 +9999,37 @@ const PRAXIS_CHARTS_AUTHORING_MANIFEST = {
|
|
|
8100
9999
|
scope: 'dataBinding',
|
|
8101
10000
|
targetKind: 'queryContext',
|
|
8102
10001
|
target: { kind: 'queryContext', resolver: 'praxis-chart-query-context', ambiguityPolicy: 'fail', required: false },
|
|
8103
|
-
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' } } },
|
|
8104
10003
|
effects: [{ kind: 'set-value', path: 'queryContext' }],
|
|
8105
10004
|
destructive: false,
|
|
8106
10005
|
requiresConfirmation: false,
|
|
8107
|
-
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'],
|
|
8108
10007
|
affectedPaths: ['queryContext'],
|
|
8109
10008
|
submissionImpact: 'affects-schema-backed-data',
|
|
8110
10009
|
preconditions: ['config-initialized'],
|
|
8111
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
|
+
},
|
|
8112
10033
|
{
|
|
8113
10034
|
operationId: 'crossFilter.configure',
|
|
8114
10035
|
title: 'Configure chart cross-filter',
|
|
@@ -8122,7 +10043,7 @@ const PRAXIS_CHARTS_AUTHORING_MANIFEST = {
|
|
|
8122
10043
|
identityKeys: ['availableTargets[].id'],
|
|
8123
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' } } } },
|
|
8124
10045
|
failureModes: ['target-not-in-catalog', 'mapping-source-field-missing', 'mapping-target-invalid', 'unsafe-event-action'],
|
|
8125
|
-
description: 'Configures crossFilter as a structured event action
|
|
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.',
|
|
8126
10047
|
} }],
|
|
8127
10048
|
destructive: false,
|
|
8128
10049
|
requiresConfirmation: false,
|
|
@@ -8144,7 +10065,7 @@ const PRAXIS_CHARTS_AUTHORING_MANIFEST = {
|
|
|
8144
10065
|
identityKeys: ['availableTargets[].id'],
|
|
8145
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' } } } },
|
|
8146
10067
|
failureModes: ['target-not-in-catalog', 'mapping-source-field-missing', 'drilldown-target-invalid', 'unsafe-event-action'],
|
|
8147
|
-
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.',
|
|
8148
10069
|
} }],
|
|
8149
10070
|
destructive: false,
|
|
8150
10071
|
requiresConfirmation: false,
|
|
@@ -8166,11 +10087,11 @@ const PRAXIS_CHARTS_AUTHORING_MANIFEST = {
|
|
|
8166
10087
|
identityKeys: ['availableTargets[].id'],
|
|
8167
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' } } } },
|
|
8168
10089
|
failureModes: ['target-not-in-catalog', 'mapping-source-field-missing', 'mapping-target-invalid', 'unsafe-event-action'],
|
|
8169
|
-
description: 'Configures selectionChange as
|
|
10090
|
+
description: 'Configures selectionChange as single-point selection evidence whose event key is governed by the operation.',
|
|
8170
10091
|
} }],
|
|
8171
10092
|
destructive: false,
|
|
8172
10093
|
requiresConfirmation: false,
|
|
8173
|
-
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'],
|
|
8174
10095
|
affectedPaths: ['chartDocument.events.selectionChange'],
|
|
8175
10096
|
submissionImpact: 'affects-schema-backed-data',
|
|
8176
10097
|
preconditions: ['config-initialized'],
|
|
@@ -8208,7 +10129,7 @@ const PRAXIS_CHARTS_AUTHORING_MANIFEST = {
|
|
|
8208
10129
|
],
|
|
8209
10130
|
validators: [
|
|
8210
10131
|
{ validatorId: 'chart-document-shape', level: 'error', code: 'CHART_DOCUMENT_SHAPE', description: 'Document must conform to PraxisXUiChartContract.' },
|
|
8211
|
-
{ validatorId: 'chart-version-supported', level: 'error', code: 'CHART_VERSION_SUPPORTED', description:
|
|
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.` },
|
|
8212
10133
|
{ validatorId: 'chart-type-supported', level: 'error', code: 'CHART_TYPE_SUPPORTED', description: 'Chart kind must be supported by @praxisui/charts.' },
|
|
8213
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.' },
|
|
8214
10135
|
{ validatorId: 'pie-single-metric', level: 'error', code: 'PIE_SINGLE_METRIC', description: 'Pie and donut charts support a single metric.' },
|
|
@@ -8222,15 +10143,17 @@ const PRAXIS_CHARTS_AUTHORING_MANIFEST = {
|
|
|
8222
10143
|
{ validatorId: 'axis-field-exists', level: 'error', code: 'AXIS_FIELD_EXISTS', description: 'Axis/dimension fields must exist in availableFields or schema context.' },
|
|
8223
10144
|
{ validatorId: 'secondary-axis-combo-only', level: 'error', code: 'SECONDARY_AXIS_COMBO_ONLY', description: 'Secondary axis is supported only for combo charts.' },
|
|
8224
10145
|
{ validatorId: 'cartesian-dimension-required', level: 'error', code: 'CARTESIAN_DIMENSION_REQUIRED', description: 'Cartesian charts require at least one dimension.' },
|
|
8225
|
-
{ validatorId: 'remote-resource-in-api-metadata', level: 'error', code: 'REMOTE_RESOURCE_IN_API_METADATA', description: 'Remote praxis.stats resource must
|
|
8226
|
-
{ validatorId: 'bound-fields-exist', level: 'error', code: 'BOUND_FIELDS_EXIST', description: 'Bound dimensions and metrics must exist in the
|
|
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.' },
|
|
8227
10148
|
{ validatorId: 'stats-operation-supported', level: 'error', code: 'STATS_OPERATION_SUPPORTED', description: 'Remote datasource operation must be supported by the stats backend.' },
|
|
8228
|
-
{ validatorId: 'query-context-structured', level: 'error', code: 'QUERY_CONTEXT_STRUCTURED', description: 'queryContext must use
|
|
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.' },
|
|
8229
10151
|
{ validatorId: 'query-context-fields-exist', level: 'warning', code: 'QUERY_CONTEXT_FIELDS_EXIST', description: 'queryContext filter fields should exist in the schema/data context.' },
|
|
8230
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.' },
|
|
8231
10154
|
{ validatorId: 'cross-filter-output-structured', level: 'error', code: 'CROSS_FILTER_OUTPUT_STRUCTURED', description: 'Cross-filter output must be structured as query-context filters.' },
|
|
8232
|
-
{ validatorId: 'event-target-governed', level: 'error', code: 'EVENT_TARGET_GOVERNED', description: 'Event targets must
|
|
8233
|
-
{ 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.' },
|
|
8234
10157
|
{ validatorId: 'event-action-supported', level: 'error', code: 'EVENT_ACTION_SUPPORTED', description: 'Event action must be one of the supported structured action kinds.' },
|
|
8235
10158
|
{ validatorId: 'drilldown-target-governed', level: 'error', code: 'DRILLDOWN_TARGET_GOVERNED', description: 'Drilldown target must be a governed route or widget target.' },
|
|
8236
10159
|
{ validatorId: 'selection-output-structured', level: 'error', code: 'SELECTION_OUTPUT_STRUCTURED', description: 'Selection output must resolve selected point filters deterministically.' },
|
|
@@ -8240,8 +10163,11 @@ const PRAXIS_CHARTS_AUTHORING_MANIFEST = {
|
|
|
8240
10163
|
roundTripRequirements: [
|
|
8241
10164
|
'The canonical saved shape is PraxisXUiChartContract, not raw ECharts options.',
|
|
8242
10165
|
'PraxisChartConfigEditor must preserve chart kind, source, dimensions, metrics, events, legend and tooltip across apply/save/reset/reopen.',
|
|
8243
|
-
'Remote bindings
|
|
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.',
|
|
8244
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.',
|
|
8245
10171
|
],
|
|
8246
10172
|
examples: [
|
|
8247
10173
|
{ id: 'set-bar-chart', request: 'Use a bar chart for employees by department.', operationId: 'chart.type.set', params: { kind: 'bar' }, isPositive: true },
|
|
@@ -8251,11 +10177,12 @@ const PRAXIS_CHARTS_AUTHORING_MANIFEST = {
|
|
|
8251
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 },
|
|
8252
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 },
|
|
8253
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 },
|
|
8254
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 },
|
|
8255
10182
|
{ id: 'configure-selection', request: 'Emit selected department as a structured selection event.', operationId: 'selection.configure', params: { action: 'emit', mapping: { department: 'department' } }, isPositive: true },
|
|
8256
10183
|
{ id: 'toggle-legend', request: 'Show the legend.', operationId: 'legend.configure', params: { enabled: true }, isPositive: true },
|
|
8257
10184
|
{ id: 'toggle-tooltip', request: 'Disable tooltips.', operationId: 'tooltip.configure', params: { enabled: false }, isPositive: true },
|
|
8258
|
-
{ 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:
|
|
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 },
|
|
8259
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 },
|
|
8260
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 },
|
|
8261
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 },
|
|
@@ -8270,4 +10197,4 @@ const PRAXIS_CHARTS_AUTHORING_MANIFEST = {
|
|
|
8270
10197
|
* Generated bundle index. Do not edit.
|
|
8271
10198
|
*/
|
|
8272
10199
|
|
|
8273
|
-
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, 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 };
|